- configuracion.php (sesion tab): loads approved message_templates and enabled turnero_chat_plantillas; renders styled checklist; guardarPlantillasChat() POSTs to chat_save_plantillas.php - chat.php: replaces free-text template modal with 2-step modal — step 1 lists enabled templates (chat_get_plantillas.php), step 2 fetches variables via get_template_details.php and renders labeled inputs; sendTemplate() builds params array and POSTs to chat_send_message.php Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1061 lines
47 KiB
PHP
1061 lines
47 KiB
PHP
<?php
|
|
/**
|
|
* modules/turnero/views/chat.php
|
|
* Inbox del número WhatsApp del Turnero.
|
|
* Permite ver y responder mensajes de pacientes que escriben al número turnero.
|
|
*/
|
|
|
|
require_once __DIR__ . '/../../../config/config.php';
|
|
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
|
|
|
|
$turneroPhoneId = getConfigFromDB('whatsapp_phone_number_id_turnero', '');
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content">
|
|
<title>Chat Turnero — WhatsApp</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
html, body { height: 100%; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f0f2f5; }
|
|
|
|
.chat-shell { display: flex; height: 100%; }
|
|
|
|
/* ── Sidebar ── */
|
|
.sidebar {
|
|
width: 340px; min-width: 280px; max-width: 380px;
|
|
background: #fff; border-right: 1px solid #e9ecef;
|
|
display: flex; flex-direction: column;
|
|
}
|
|
.sidebar-header {
|
|
padding: 16px; background: #25d366; color: #fff;
|
|
display: flex; align-items: center; gap: 10px;
|
|
}
|
|
.sidebar-header h2 { font-size: 1rem; flex: 1; }
|
|
.sidebar-header .badge-config {
|
|
font-size: .7rem; background: rgba(0,0,0,.2); padding: 3px 8px; border-radius: 12px;
|
|
}
|
|
.search-box { padding: 8px 12px; border-bottom: 1px solid #f0f0f0; }
|
|
.search-box input {
|
|
width: 100%; padding: 8px 12px; border: 1px solid #e0e0e0;
|
|
border-radius: 20px; font-size: .85rem; outline: none;
|
|
background: #f5f5f5;
|
|
}
|
|
.search-box input:focus { border-color: #25d366; background: #fff; }
|
|
|
|
.contact-list { flex: 1; overflow-y: auto; }
|
|
.contact-item {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 12px 16px; cursor: pointer; border-bottom: 1px solid #f7f7f7;
|
|
transition: background .15s;
|
|
}
|
|
.contact-item:hover { background: #f5f5f5; }
|
|
.contact-item.active { background: #e7fce8; }
|
|
.contact-avatar {
|
|
width: 44px; height: 44px; border-radius: 50%;
|
|
background: #25d366; color: #fff; display: flex; align-items: center;
|
|
justify-content: center; font-weight: 700; font-size: 1.1rem; flex-shrink: 0;
|
|
}
|
|
.contact-info { flex: 1; min-width: 0; }
|
|
.contact-name { font-weight: 600; font-size: .9rem; color: #111; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.contact-last { font-size: .78rem; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
|
|
.contact-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
|
|
.contact-time { font-size: .7rem; color: #aaa; }
|
|
.badge-unread {
|
|
background: #25d366; color: #fff; font-size: .65rem; font-weight: 700;
|
|
padding: 2px 6px; border-radius: 10px; min-width: 18px; text-align: center;
|
|
}
|
|
.contact-empty { text-align: center; padding: 40px 16px; color: #aaa; font-size: .85rem; }
|
|
|
|
/* ── Chat window ── */
|
|
.chat-window {
|
|
flex: 1; display: flex; flex-direction: column;
|
|
background: #efeae2;
|
|
}
|
|
.chat-empty-state {
|
|
flex: 1; display: flex; flex-direction: column; align-items: center;
|
|
justify-content: center; color: #aaa; gap: 12px;
|
|
}
|
|
.chat-empty-state i { font-size: 3rem; color: #25d366; }
|
|
.chat-empty-state p { font-size: .95rem; }
|
|
|
|
.chat-topbar {
|
|
background: #fff; padding: 12px 16px;
|
|
display: flex; align-items: center; gap: 12px;
|
|
border-bottom: 1px solid #e9ecef; min-height: 60px;
|
|
position: sticky; top: 0; z-index: 10; flex-shrink: 0;
|
|
}
|
|
.chat-topbar-avatar {
|
|
width: 38px; height: 38px; border-radius: 50%;
|
|
background: #25d366; color: #fff; display: flex;
|
|
align-items: center; justify-content: center; font-weight: 700;
|
|
}
|
|
.chat-topbar-info h3 { font-size: .95rem; font-weight: 600; }
|
|
.chat-topbar-info span { font-size: .78rem; color: #888; }
|
|
|
|
.messages-area {
|
|
flex: 1; overflow-y: auto; padding: 12px 16px;
|
|
display: flex; flex-direction: column; gap: 6px;
|
|
min-height: 0; -webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
.msg-bubble {
|
|
max-width: 65%; padding: 8px 12px; border-radius: 8px;
|
|
font-size: .88rem; line-height: 1.4; position: relative;
|
|
word-break: break-word;
|
|
}
|
|
.msg-bubble.incoming {
|
|
background: #fff; align-self: flex-start;
|
|
border-bottom-left-radius: 2px; box-shadow: 0 1px 2px rgba(0,0,0,.08);
|
|
}
|
|
.msg-bubble.outgoing {
|
|
background: #d9fdd3; align-self: flex-end;
|
|
border-bottom-right-radius: 2px; box-shadow: 0 1px 2px rgba(0,0,0,.08);
|
|
}
|
|
.msg-time {
|
|
font-size: .68rem; color: #aaa; text-align: right; margin-top: 3px;
|
|
}
|
|
.msg-type-badge {
|
|
font-size: .7rem; color: #888; font-style: italic;
|
|
}
|
|
.msg-media-thumb {
|
|
max-width: 220px; max-height: 180px; border-radius: 6px;
|
|
display: block; margin-bottom: 4px; cursor: pointer;
|
|
}
|
|
.msg-media-link {
|
|
display: flex; align-items: center; gap: 6px; font-size: .82rem; color: #1a73e8;
|
|
text-decoration: none; padding: 4px 0;
|
|
}
|
|
.msg-media-link:hover { text-decoration: underline; }
|
|
|
|
.load-more-btn {
|
|
align-self: center; padding: 6px 18px; font-size: .8rem;
|
|
background: #fff; border: 1px solid #ddd; border-radius: 16px;
|
|
cursor: pointer; color: #555;
|
|
}
|
|
.load-more-btn:hover { background: #f5f5f5; }
|
|
|
|
/* ── Input area ── */
|
|
.chat-input-area {
|
|
background: #f0f2f5; padding: 10px 16px;
|
|
padding-bottom: max(10px, env(safe-area-inset-bottom));
|
|
display: flex; align-items: flex-end; gap: 10px;
|
|
border-top: 1px solid #e0e0e0; flex-shrink: 0;
|
|
}
|
|
.chat-input-area textarea {
|
|
flex: 1; padding: 10px 14px; border: none; border-radius: 22px;
|
|
background: #fff; font-size: .9rem; resize: none; outline: none;
|
|
max-height: 120px; line-height: 1.4;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,.08);
|
|
}
|
|
.btn-send {
|
|
width: 42px; height: 42px; border-radius: 50%; border: none;
|
|
background: #25d366; color: #fff; font-size: 1.1rem;
|
|
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
flex-shrink: 0; transition: background .2s;
|
|
}
|
|
.btn-send:hover { background: #1ebe5c; }
|
|
.btn-send:disabled { background: #ccc; cursor: default; }
|
|
|
|
/* ── Alertas ── */
|
|
.alert-no-config {
|
|
background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px;
|
|
padding: 12px 16px; margin: 16px; font-size: .85rem; color: #856404;
|
|
display: flex; align-items: center; gap: 8px;
|
|
}
|
|
.typing-indicator { font-size: .75rem; color: #888; padding: 2px 8px; }
|
|
|
|
/* ── Botones extra en input ── */
|
|
.input-extra-btn {
|
|
background: none; border: none; color: #888; font-size: 1.15rem;
|
|
cursor: pointer; padding: 6px; border-radius: 50%; transition: background .15s, color .15s;
|
|
flex-shrink: 0;
|
|
}
|
|
.input-extra-btn:hover { background: rgba(0,0,0,.06); color: #555; }
|
|
|
|
/* ── Panel emoji ── */
|
|
.emoji-panel {
|
|
position: absolute; bottom: 64px; left: 0;
|
|
background: #fff; border: 1px solid #e0e0e0; border-radius: 12px;
|
|
box-shadow: 0 6px 24px rgba(0,0,0,.15); padding: 10px;
|
|
display: none; flex-wrap: wrap; gap: 2px; width: 280px; z-index: 50;
|
|
}
|
|
.emoji-panel.show { display: flex; }
|
|
.emoji-panel span { font-size: 1.3rem; cursor: pointer; padding: 4px; border-radius: 6px; }
|
|
.emoji-panel span:hover { background: #f0f2f5; }
|
|
|
|
/* ── Reaction picker ── */
|
|
#reaction-picker {
|
|
position: fixed; display: none; z-index: 9000;
|
|
background: #fff; border-radius: 30px; padding: 6px 10px;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,.18);
|
|
gap: 4px; align-items: center;
|
|
}
|
|
#reaction-picker.show { display: flex; }
|
|
#reaction-picker span { font-size: 1.4rem; cursor: pointer; padding: 4px 6px; border-radius: 50%; transition: transform .1s; }
|
|
#reaction-picker span:hover { transform: scale(1.3); }
|
|
.msg-bubble { position: relative; }
|
|
.msg-bubble:hover .react-btn { opacity: 1; }
|
|
.react-btn {
|
|
position: absolute; top: 4px; opacity: 0; transition: opacity .15s;
|
|
background: #fff; border: 1px solid #e0e0e0; border-radius: 50%;
|
|
width: 24px; height: 24px; font-size: .75rem; cursor: pointer;
|
|
display: flex; align-items: center; justify-content: center;
|
|
box-shadow: 0 1px 4px rgba(0,0,0,.12);
|
|
}
|
|
.msg-bubble.incoming .react-btn { right: -28px; }
|
|
.msg-bubble.outgoing .react-btn { left: -28px; }
|
|
.msg-reaction { font-size: .95rem; margin-top: 2px; }
|
|
|
|
/* ── Preview adjunto ── */
|
|
#attach-preview {
|
|
display: none; align-items: center; gap: 8px;
|
|
padding: 8px 12px; background: #e9f5fe;
|
|
border-top: 1px solid #bee3f8; font-size: .85rem;
|
|
}
|
|
#attach-preview img { width: 48px; height: 48px; object-fit: cover; border-radius: 6px; }
|
|
#attach-preview .attach-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
#attach-preview .attach-remove { background: none; border: none; color: #e53e3e; cursor: pointer; font-size: 1rem; }
|
|
|
|
/* ── Recording indicator ── */
|
|
#recording-bar {
|
|
display: none; align-items: center; gap: 10px;
|
|
padding: 8px 14px; background: #fff0f0; border-top: 1px solid #fed7d7;
|
|
font-size: .85rem; color: #e53e3e;
|
|
}
|
|
#recording-bar .rec-dot { width: 10px; height: 10px; border-radius: 50%; background: #e53e3e; animation: blink 1s infinite; }
|
|
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.2} }
|
|
|
|
/* ── Modal plantilla (2 pasos) ── */
|
|
#tpl-modal {
|
|
display: none; position: fixed; inset: 0; z-index: 1000;
|
|
background: rgba(0,0,0,.45); align-items: center; justify-content: center; padding: 16px;
|
|
}
|
|
#tpl-modal.show { display: flex; }
|
|
.tpl-dialog { background: #fff; border-radius: 14px; width: 100%; max-width: 400px; box-shadow: 0 8px 32px rgba(0,0,0,.22); overflow: hidden; }
|
|
.tpl-hdr { display: flex; align-items: center; gap: 8px; padding: 13px 16px; border-bottom: 1px solid #f1f5f9; font-size: .92rem; font-weight: 700; color: #1e293b; }
|
|
.tpl-hdr > span { flex: 1; }
|
|
.tpl-close-btn { background: none; border: none; cursor: pointer; color: #94a3b8; font-size: .95rem; padding: 2px 7px; border-radius: 4px; }
|
|
.tpl-close-btn:hover { color: #475569; background: #f1f5f9; }
|
|
.tpl-back-btn { background: none; border: none; cursor: pointer; color: #64748b; font-size: .85rem; padding: 2px 7px; border-radius: 4px; flex-shrink: 0; }
|
|
.tpl-back-btn:hover { background: #f1f5f9; }
|
|
.tpl-item { display: flex; flex-direction: column; gap: 2px; padding: 9px 12px; cursor: pointer; border-radius: 8px; margin: 0 6px; transition: background .12s; }
|
|
.tpl-item:hover { background: #f0fdf4; }
|
|
.tpl-item-name { font-size: .87rem; font-weight: 600; color: #1e293b; }
|
|
.tpl-item-meta { font-size: .74rem; color: #64748b; }
|
|
.tpl-item-body { font-size: .74rem; color: #94a3b8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.tpl-preview-box { background: #f0fdf4; border-left: 3px solid #25d366; border-radius: 0 6px 6px 0; padding: 8px 12px; font-size: .82rem; color: #374151; margin: 10px 16px 2px; white-space: pre-wrap; word-break: break-word; }
|
|
.tpl-var-row { padding: 0 16px; }
|
|
.tpl-var-row label { font-size: .78rem; font-weight: 600; color: #475569; display: block; margin-bottom: 3px; }
|
|
.tpl-var-row input { width: 100%; padding: 7px 10px; border: 1.5px solid #e2e8f0; border-radius: 8px; font-size: .87rem; outline: none; box-sizing: border-box; }
|
|
.tpl-var-row input:focus { border-color: #25d366; }
|
|
.tpl-actions { display: flex; gap: 8px; justify-content: flex-end; padding: 12px 16px; border-top: 1px solid #f1f5f9; }
|
|
.btn-tpl-cancel { padding: 8px 16px; background: #f5f5f5; border: none; border-radius: 8px; cursor: pointer; font-size: .88rem; }
|
|
.btn-tpl-send { padding: 8px 16px; background: #25d366; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: .88rem; font-weight: 600; }
|
|
|
|
/* ── Media en burbuja ── */
|
|
.msg-img { max-width: 220px; max-height: 200px; border-radius: 8px; cursor: pointer; display: block; margin-bottom: 3px; }
|
|
.msg-video { max-width: 220px; border-radius: 8px; display: block; margin-bottom: 3px; }
|
|
.msg-audio { width: 200px; margin-bottom: 3px; }
|
|
.msg-doc-link { display: flex; align-items: center; gap: 7px; font-size: .83rem; color: #1a73e8; text-decoration: none; padding: 4px 0; }
|
|
.msg-doc-link:hover { text-decoration: underline; }
|
|
|
|
.back-btn { display: none; background: none; border: none; color: #fff; font-size: 1.1rem; cursor: pointer; padding: 4px 8px; }
|
|
|
|
@media (max-width: 680px) {
|
|
.chat-shell { position: relative; overflow: hidden; }
|
|
.sidebar { position: absolute; inset: 0; z-index: 10; transition: transform .2s ease; }
|
|
.sidebar.mobile-hidden { transform: translateX(-100%); pointer-events: none; }
|
|
.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; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="chat-shell">
|
|
|
|
<!-- ── Sidebar: lista de contactos ── -->
|
|
<div class="sidebar" id="sidebar">
|
|
<div class="sidebar-header">
|
|
<a href="<?= BASE_URL ?>erp.php?m=turnero&v=dashboard"
|
|
style="color:rgba(255,255,255,.7);font-size:.85rem;text-decoration:none;display:flex;align-items:center;gap:5px;margin-bottom:8px">
|
|
<i class="fas fa-arrow-left"></i> Menú
|
|
</a>
|
|
<i class="fab fa-whatsapp" style="font-size:1.4rem"></i>
|
|
<h2>Chat Turnero</h2>
|
|
<?php if ($turneroPhoneId): ?>
|
|
<span class="badge-config" title="Número turnero configurado">
|
|
<i class="fas fa-check-circle"></i> Activo
|
|
</span>
|
|
<?php else: ?>
|
|
<span class="badge-config" style="background:rgba(220,53,69,.6)" title="Sin configurar">
|
|
<i class="fas fa-exclamation-circle"></i> Sin config
|
|
</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="search-box">
|
|
<input type="text" id="searchInput" placeholder="Buscar contacto…" autocomplete="off">
|
|
</div>
|
|
<div class="contact-list" id="contactList">
|
|
<div class="contact-empty"><i class="fas fa-spinner fa-spin"></i><br>Cargando…</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Ventana de chat ── -->
|
|
<div class="chat-window" id="chatWindow">
|
|
|
|
<?php if (!$turneroPhoneId): ?>
|
|
<div class="alert-no-config">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
El número de WhatsApp del Turnero no está configurado.
|
|
<a href="/erp.php?m=turnero&v=configuracion" style="margin-left:8px;color:#856404;font-weight:600;">Configurar ahora</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="chat-empty-state" id="emptyState">
|
|
<i class="fab fa-whatsapp"></i>
|
|
<p>Selecciona un contacto para ver la conversación</p>
|
|
</div>
|
|
|
|
<div id="activeChat" style="display:none; flex:1; flex-direction:column; overflow:hidden;">
|
|
<div class="chat-topbar" id="chatTopbar">
|
|
<button class="back-btn" onclick="closeChatMobile()"><i class="fas fa-arrow-left"></i></button>
|
|
<div class="chat-topbar-avatar" id="topbarAvatar">?</div>
|
|
<div class="chat-topbar-info">
|
|
<h3 id="topbarName">—</h3>
|
|
<span id="topbarPhone">—</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="messages-area" id="messagesArea">
|
|
<button class="load-more-btn" id="loadMoreBtn" style="display:none" onclick="loadMoreMessages()">
|
|
<i class="fas fa-chevron-up"></i> Ver mensajes anteriores
|
|
</button>
|
|
</div>
|
|
|
|
<div id="typingIndicator" class="typing-indicator" style="display:none">Escribiendo…</div>
|
|
|
|
<!-- Preview adjunto -->
|
|
<div id="attach-preview">
|
|
<img id="attachThumb" src="" alt="" style="display:none">
|
|
<span class="attach-name" id="attachName"></span>
|
|
<button class="attach-remove" onclick="clearAttach()">✕</button>
|
|
</div>
|
|
|
|
<!-- Barra de grabación -->
|
|
<div id="recording-bar">
|
|
<div class="rec-dot"></div>
|
|
<span>Grabando…</span>
|
|
<span id="recTimer" style="font-weight:700;min-width:32px">0:00</span>
|
|
<button onclick="stopRecording()" style="margin-left:auto;background:#e53e3e;color:#fff;border:none;border-radius:6px;padding:4px 12px;cursor:pointer;font-size:.82rem">Enviar</button>
|
|
<button onclick="cancelRecording()" style="background:#f5f5f5;border:none;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:.82rem">Cancelar</button>
|
|
</div>
|
|
|
|
<input type="file" id="fileInput" style="display:none"
|
|
accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.csv"
|
|
onchange="onFileSelected(event)">
|
|
|
|
<div class="chat-input-area" style="position:relative">
|
|
<div class="emoji-panel" id="emojiPanel"></div>
|
|
<button class="input-extra-btn" onclick="toggleEmojiPanel()" title="Emoji"><i class="fas fa-smile"></i></button>
|
|
<button class="input-extra-btn" onclick="document.getElementById('fileInput').click()" title="Adjuntar"><i class="fas fa-paperclip"></i></button>
|
|
<button class="input-extra-btn" id="micBtn" onclick="toggleRecording()" title="Grabar audio"><i class="fas fa-microphone"></i></button>
|
|
<textarea id="msgInput" rows="1" placeholder="Escribe un mensaje…"
|
|
onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
|
|
<button class="input-extra-btn" onclick="openTemplatePicker()" title="Plantilla"><i class="fas fa-file-alt"></i></button>
|
|
<button class="btn-send" id="btnSend" onclick="sendMessage()" <?= $turneroPhoneId ? '' : 'disabled' ?>>
|
|
<i class="fas fa-paper-plane"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /chat-window -->
|
|
</div><!-- /chat-shell -->
|
|
|
|
<!-- ── Reaction picker ── -->
|
|
<div id="reaction-picker">
|
|
<?php foreach (['👍','❤️','😂','😮','😢','🙏','🔥','👏'] as $em): ?>
|
|
<span onclick="sendReactionEmoji('<?= $em ?>')"><?= $em ?></span>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<!-- ── Modal plantilla (2 pasos) ── -->
|
|
<div id="tpl-modal">
|
|
<!-- Paso 1: lista -->
|
|
<div class="tpl-dialog" id="tpl-step-list">
|
|
<div class="tpl-hdr">
|
|
<span><i class="fas fa-file-alt" style="color:#25d366;margin-right:6px"></i>Seleccionar plantilla</span>
|
|
<button class="tpl-close-btn" onclick="closeTemplatePicker()">✕</button>
|
|
</div>
|
|
<div id="tpl-list-body" style="overflow-y:auto;max-height:360px;display:flex;flex-direction:column;gap:.25rem;padding:.5rem 0"></div>
|
|
</div>
|
|
<!-- Paso 2: variables -->
|
|
<div class="tpl-dialog" id="tpl-step-vars" style="display:none">
|
|
<div class="tpl-hdr">
|
|
<button class="tpl-back-btn" onclick="tplGoBack()"><i class="fas fa-arrow-left"></i></button>
|
|
<span id="tpl-vars-title">Completar variables</span>
|
|
<button class="tpl-close-btn" onclick="closeTemplatePicker()">✕</button>
|
|
</div>
|
|
<div id="tpl-preview-box" style="display:none" class="tpl-preview-box"></div>
|
|
<div id="tpl-vars-body" style="display:flex;flex-direction:column;gap:.6rem;padding:.6rem 0"></div>
|
|
<div class="tpl-actions">
|
|
<button class="btn-tpl-cancel" onclick="tplGoBack()">Atrás</button>
|
|
<button class="btn-tpl-send" onclick="sendTemplate()"><i class="fas fa-paper-plane me-1"></i>Enviar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_LIST = 'modules/turnero/api/chat_get_list.php';
|
|
const API_MESSAGES = 'modules/turnero/api/chat_get_messages.php';
|
|
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';
|
|
let _tplSelected = null;
|
|
const BASE = (function() {
|
|
const s = window.location.pathname;
|
|
return s.replace(/\/erp\.php.*$/, '/') || '/';
|
|
})();
|
|
|
|
let state = {
|
|
contacts: [],
|
|
activeUserId: null,
|
|
activeUser: null,
|
|
messages: [],
|
|
earliest: null,
|
|
earliestId: null,
|
|
hasMore: false,
|
|
polling: null,
|
|
lastPollTime: null,
|
|
};
|
|
|
|
// ── Init ─────────────────────────────────────────────────────────────────────
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadContacts();
|
|
document.getElementById('searchInput').addEventListener('input', debounce(() => {
|
|
loadContacts(document.getElementById('searchInput').value.trim());
|
|
}, 300));
|
|
setInterval(pollMessages, 4000);
|
|
setInterval(() => loadContacts(document.getElementById('searchInput').value.trim(), true), 12000);
|
|
});
|
|
|
|
function debounce(fn, ms) {
|
|
let t;
|
|
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
|
|
}
|
|
|
|
// ── Contactos ─────────────────────────────────────────────────────────────────
|
|
async function loadContacts(search = '', silent = false) {
|
|
const url = BASE + API_LIST + '?limit=100' + (search ? '&search=' + encodeURIComponent(search) : '');
|
|
try {
|
|
const res = await fetch(url);
|
|
const json = await res.json();
|
|
if (!json.success) return;
|
|
state.contacts = json.data;
|
|
renderContacts(json.data);
|
|
} catch(e) {
|
|
if (!silent) console.error('loadContacts:', e);
|
|
}
|
|
}
|
|
|
|
function renderContacts(contacts) {
|
|
const el = document.getElementById('contactList');
|
|
if (!contacts.length) {
|
|
el.innerHTML = '<div class="contact-empty"><i class="fas fa-comment-slash"></i><br>Sin conversaciones aún</div>';
|
|
return;
|
|
}
|
|
el.innerHTML = contacts.map(c => {
|
|
const initials = (c.name || c.phone_number).charAt(0).toUpperCase();
|
|
const preview = escHtml(c.last_message || '—');
|
|
const time = c.last_time ? formatTime(c.last_time) : '';
|
|
const unread = c.unread_count > 0 ? `<span class="badge-unread">${c.unread_count}</span>` : '';
|
|
const active = state.activeUserId === c.user_id ? ' active' : '';
|
|
return `<div class="contact-item${active}" onclick="openChat(${c.user_id})">
|
|
<div class="contact-avatar">${initials}</div>
|
|
<div class="contact-info">
|
|
<div class="contact-name">${escHtml(c.name || c.phone_number)}</div>
|
|
<div class="contact-last">${preview}</div>
|
|
</div>
|
|
<div class="contact-meta">
|
|
<span class="contact-time">${time}</span>
|
|
${unread}
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
// ── Abrir chat ────────────────────────────────────────────────────────────────
|
|
async function openChat(userId) {
|
|
state.activeUserId = userId;
|
|
state.messages = [];
|
|
state.earliest = null;
|
|
state.earliestId = null;
|
|
state.hasMore = false;
|
|
state.lastPollTime = null;
|
|
|
|
const contact = state.contacts.find(c => c.user_id === userId);
|
|
state.activeUser = contact;
|
|
|
|
document.getElementById('emptyState').style.display = 'none';
|
|
const activeEl = document.getElementById('activeChat');
|
|
activeEl.style.display = 'flex';
|
|
activeEl.style.flexDirection = 'column';
|
|
activeEl.style.overflow = 'hidden';
|
|
activeEl.style.flex = '1';
|
|
|
|
const initials = contact ? (contact.name || contact.phone_number).charAt(0).toUpperCase() : '?';
|
|
document.getElementById('topbarAvatar').textContent = initials;
|
|
document.getElementById('topbarName').textContent = contact ? (contact.name || contact.phone_number) : '…';
|
|
document.getElementById('topbarPhone').textContent = contact ? contact.phone_number : '';
|
|
|
|
document.querySelectorAll('.contact-item').forEach(el => el.classList.remove('active'));
|
|
const items = document.querySelectorAll('.contact-item');
|
|
items.forEach(el => { if (el.onclick.toString().includes(`openChat(${userId})`)) el.classList.add('active'); });
|
|
|
|
const area = document.getElementById('messagesArea');
|
|
area.innerHTML = '<div style="text-align:center;padding:20px;color:#aaa"><i class="fas fa-spinner fa-spin"></i></div>';
|
|
|
|
await fetchMessages();
|
|
markRead(userId);
|
|
loadContacts(document.getElementById('searchInput').value.trim(), true);
|
|
openChatMobile();
|
|
}
|
|
|
|
function isMobile() { return window.innerWidth <= 680; }
|
|
function openChatMobile() {
|
|
if (!isMobile()) return;
|
|
document.getElementById('sidebar').classList.add('mobile-hidden');
|
|
document.getElementById('chatWindow').classList.add('mobile-active');
|
|
}
|
|
function closeChatMobile() {
|
|
if (!isMobile()) return;
|
|
document.getElementById('sidebar').classList.remove('mobile-hidden');
|
|
document.getElementById('chatWindow').classList.remove('mobile-active');
|
|
}
|
|
|
|
// ── Mensajes ──────────────────────────────────────────────────────────────────
|
|
async function fetchMessages() {
|
|
if (!state.activeUserId) return;
|
|
const forUser = state.activeUserId; // captura antes del await
|
|
const url = BASE + API_MESSAGES + '?user_id=' + forUser + '&limit=50';
|
|
try {
|
|
const res = await fetch(url);
|
|
const json = await res.json();
|
|
if (forUser !== state.activeUserId) return; // usuario cambió mientras esperaba
|
|
if (!json.success) return;
|
|
state.messages = json.data;
|
|
state.hasMore = json.has_more;
|
|
state.earliest = json.earliest;
|
|
state.earliestId = json.earliest_id;
|
|
state.lastPollTime = json.data.length ? json.data[json.data.length - 1].created_at : null;
|
|
renderMessages(json.data, false);
|
|
} catch(e) { console.error('fetchMessages:', e); }
|
|
}
|
|
|
|
async function loadMoreMessages() {
|
|
if (!state.activeUserId || !state.earliest) return;
|
|
const forUser = state.activeUserId;
|
|
const url = BASE + API_MESSAGES
|
|
+ '?user_id=' + forUser
|
|
+ '&limit=50'
|
|
+ '&before=' + encodeURIComponent(state.earliest)
|
|
+ (state.earliestId ? '&before_id=' + state.earliestId : '');
|
|
try {
|
|
const res = await fetch(url);
|
|
const json = await res.json();
|
|
if (forUser !== state.activeUserId) return;
|
|
if (!json.success || !json.data.length) {
|
|
document.getElementById('loadMoreBtn').style.display = 'none';
|
|
return;
|
|
}
|
|
state.messages = [...json.data, ...state.messages];
|
|
state.hasMore = json.has_more;
|
|
state.earliest = json.earliest;
|
|
state.earliestId = json.earliest_id;
|
|
prependMessages(json.data);
|
|
} catch(e) { console.error('loadMoreMessages:', e); }
|
|
}
|
|
|
|
async function pollMessages() {
|
|
if (!state.activeUserId || !state.lastPollTime) return;
|
|
const forUser = state.activeUserId;
|
|
const sinceTime = state.lastPollTime;
|
|
const url = BASE + API_MESSAGES
|
|
+ '?user_id=' + forUser
|
|
+ '&limit=50'
|
|
+ '&since=' + encodeURIComponent(sinceTime);
|
|
try {
|
|
const res = await fetch(url);
|
|
const json = await res.json();
|
|
if (forUser !== state.activeUserId) return; // usuario cambió mientras esperaba
|
|
if (!json.success || !json.data.length) return;
|
|
json.data.forEach(m => {
|
|
if (!state.messages.find(x => x.id === m.id)) {
|
|
state.messages.push(m);
|
|
appendMessage(m);
|
|
}
|
|
});
|
|
state.lastPollTime = json.data[json.data.length - 1].created_at;
|
|
markRead(forUser);
|
|
loadContacts(document.getElementById('searchInput').value.trim(), true);
|
|
} catch(e) {}
|
|
}
|
|
|
|
// ── Render mensajes ───────────────────────────────────────────────────────────
|
|
function renderMessages(msgs, prepend) {
|
|
const area = document.getElementById('messagesArea');
|
|
area.innerHTML = '';
|
|
|
|
const loadBtn = document.createElement('button');
|
|
loadBtn.className = 'load-more-btn';
|
|
loadBtn.id = 'loadMoreBtn';
|
|
loadBtn.style.display = state.hasMore ? 'block' : 'none';
|
|
loadBtn.onclick = loadMoreMessages;
|
|
loadBtn.innerHTML = '<i class="fas fa-chevron-up"></i> Ver mensajes anteriores';
|
|
area.appendChild(loadBtn);
|
|
|
|
msgs.forEach(m => area.appendChild(buildBubble(m)));
|
|
area.scrollTop = area.scrollHeight;
|
|
}
|
|
|
|
function prependMessages(msgs) {
|
|
const area = document.getElementById('messagesArea');
|
|
const oldTop = area.scrollHeight - area.scrollTop;
|
|
const loadBtn = document.getElementById('loadMoreBtn');
|
|
msgs.forEach(m => area.insertBefore(buildBubble(m), loadBtn.nextSibling));
|
|
if (!state.hasMore) loadBtn.style.display = 'none';
|
|
area.scrollTop = area.scrollHeight - oldTop;
|
|
}
|
|
|
|
function appendMessage(msg) {
|
|
const area = document.getElementById('messagesArea');
|
|
const bubble = buildBubble(msg);
|
|
area.appendChild(bubble);
|
|
area.scrollTop = area.scrollHeight;
|
|
}
|
|
|
|
function buildBubble(msg) {
|
|
const div = document.createElement('div');
|
|
div.className = 'msg-bubble ' + (msg.direction === 'outgoing' ? 'outgoing' : 'incoming');
|
|
div.dataset.msgId = msg.id;
|
|
div.dataset.waId = msg.message_id || '';
|
|
|
|
const mediaSrc = msg.local_file ? ('../' + msg.local_file) : (msg.media_url_external || null);
|
|
let content = '';
|
|
|
|
if (msg.message_type === 'image' && mediaSrc) {
|
|
content = `<img class="msg-img" src="${escHtml(mediaSrc)}" loading="lazy"
|
|
onclick="window.open('${escHtml(mediaSrc)}','_blank')" alt="imagen">`;
|
|
if (msg.content) content += `<div style="font-size:.82rem;margin-top:2px">${escHtml(msg.content)}</div>`;
|
|
} else if (msg.message_type === 'video' && mediaSrc) {
|
|
content = `<video class="msg-video" src="${escHtml(mediaSrc)}" controls preload="metadata"></video>`;
|
|
if (msg.content) content += `<div style="font-size:.82rem;margin-top:2px">${escHtml(msg.content)}</div>`;
|
|
} else if (msg.message_type === 'audio' && mediaSrc) {
|
|
content = `<audio class="msg-audio" src="${escHtml(mediaSrc)}" controls preload="metadata"></audio>`;
|
|
} else if (msg.message_type === 'document' && mediaSrc) {
|
|
const icon = (msg.mime_type || '').includes('pdf') ? 'fa-file-pdf' :
|
|
(msg.mime_type || '').includes('spreadsheet') || (msg.mime_type || '').includes('excel') ? 'fa-file-excel' :
|
|
(msg.mime_type || '').includes('word') ? 'fa-file-word' : 'fa-file-alt';
|
|
content = `<a class="msg-doc-link" href="${escHtml(mediaSrc)}" target="_blank">
|
|
<i class="fas ${icon}" style="font-size:1.3rem"></i>
|
|
<span>${escHtml(msg.filename || 'Documento')}</span>
|
|
</a>`;
|
|
} else {
|
|
content = nl2br(escHtml(msg.content || ''));
|
|
}
|
|
|
|
const time = msg.created_at ? formatTimeFull(msg.created_at) : '';
|
|
const statusIcon = msg.direction === 'outgoing'
|
|
? (msg.status === 'read' ? ' ✓✓' : msg.status === 'delivered' ? ' ✓✓' : ' ✓')
|
|
: '';
|
|
|
|
const reactBtn = msg.message_id
|
|
? `<button class="react-btn" onclick="showReactionPicker(event, '${escHtml(msg.message_id)}')">😊</button>`
|
|
: '';
|
|
|
|
div.innerHTML = content
|
|
+ `<div class="msg-time">${time}<span style="opacity:.7;font-size:.7rem">${statusIcon}</span></div>`
|
|
+ (msg.reaction_emoji ? `<div class="msg-reaction">${escHtml(msg.reaction_emoji)}</div>` : '')
|
|
+ reactBtn;
|
|
return div;
|
|
}
|
|
|
|
// ── Enviar texto ─────────────────────────────────────────────────────────────
|
|
async function sendMessage() {
|
|
// Si hay archivo adjunto, enviar como media
|
|
if (attachFile) { await sendMedia(); return; }
|
|
|
|
const input = document.getElementById('msgInput');
|
|
const message = input.value.trim();
|
|
if (!message || !state.activeUserId) return;
|
|
|
|
const btn = document.getElementById('btnSend');
|
|
btn.disabled = true;
|
|
input.value = '';
|
|
autoResize(input);
|
|
|
|
try {
|
|
const res = await fetch(BASE + API_SEND, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ user_id: state.activeUserId, message })
|
|
});
|
|
const json = await res.json();
|
|
if (!json.success) {
|
|
alert('Error al enviar: ' + (json.error || 'desconocido'));
|
|
input.value = message;
|
|
} else {
|
|
await pollMessages();
|
|
}
|
|
} catch(e) {
|
|
alert('Error de red al enviar el mensaje');
|
|
input.value = message;
|
|
} finally {
|
|
btn.disabled = false;
|
|
input.focus();
|
|
}
|
|
}
|
|
|
|
// ── Adjuntar archivo ──────────────────────────────────────────────────────────
|
|
let attachFile = null;
|
|
|
|
function onFileSelected(e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
attachFile = file;
|
|
const prev = document.getElementById('attach-preview');
|
|
const thumb = document.getElementById('attachThumb');
|
|
const name = document.getElementById('attachName');
|
|
name.textContent = file.name + ' (' + (file.size > 1048576 ? (file.size/1048576).toFixed(1) + ' MB' : Math.round(file.size/1024) + ' KB') + ')';
|
|
if (file.type.startsWith('image/')) {
|
|
thumb.src = URL.createObjectURL(file);
|
|
thumb.style.display = 'block';
|
|
} else {
|
|
thumb.style.display = 'none';
|
|
}
|
|
prev.style.display = 'flex';
|
|
e.target.value = '';
|
|
}
|
|
|
|
function clearAttach() {
|
|
attachFile = null;
|
|
document.getElementById('attach-preview').style.display = 'none';
|
|
document.getElementById('attachThumb').src = '';
|
|
}
|
|
|
|
async function sendMedia() {
|
|
if (!attachFile || !state.activeUserId) return;
|
|
const btn = document.getElementById('btnSend');
|
|
btn.disabled = true;
|
|
const caption = document.getElementById('msgInput').value.trim();
|
|
|
|
const fd = new FormData();
|
|
fd.append('user_id', state.activeUserId);
|
|
fd.append('file', attachFile);
|
|
if (caption) fd.append('caption', caption);
|
|
|
|
try {
|
|
const res = await fetch(BASE + API_MEDIA, { method: 'POST', body: fd });
|
|
const json = await res.json();
|
|
if (!json.success) {
|
|
alert('Error al enviar archivo: ' + (json.error || 'desconocido'));
|
|
} else {
|
|
clearAttach();
|
|
document.getElementById('msgInput').value = '';
|
|
autoResize(document.getElementById('msgInput'));
|
|
await pollMessages();
|
|
}
|
|
} catch(e) {
|
|
alert('Error de red al enviar el archivo');
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// ── Emoji picker ──────────────────────────────────────────────────────────────
|
|
const EMOJI_LIST = ['😀','😂','😍','😎','😢','😡','🤔','👍','👎','❤️','🔥','✅','⭐','🎉','🙏','💪','🤣','😅','😊','🥰','😘','🤗','😴','🤒','😷','💊','🏥','👨⚕️','👩⚕️','📋','📞','📅','⏰','💉','🩺'];
|
|
|
|
function toggleEmojiPanel() {
|
|
const panel = document.getElementById('emojiPanel');
|
|
if (!panel.classList.contains('show')) {
|
|
panel.innerHTML = EMOJI_LIST.map(e => `<span onclick="insertEmoji('${e}')">${e}</span>`).join('');
|
|
}
|
|
panel.classList.toggle('show');
|
|
}
|
|
|
|
function insertEmoji(em) {
|
|
const inp = document.getElementById('msgInput');
|
|
const pos = inp.selectionStart;
|
|
inp.value = inp.value.slice(0, pos) + em + inp.value.slice(pos);
|
|
inp.selectionStart = inp.selectionEnd = pos + em.length;
|
|
inp.focus();
|
|
autoResize(inp);
|
|
document.getElementById('emojiPanel').classList.remove('show');
|
|
}
|
|
|
|
document.addEventListener('click', e => {
|
|
const panel = document.getElementById('emojiPanel');
|
|
if (panel.classList.contains('show') && !e.target.closest('.chat-input-area')) {
|
|
panel.classList.remove('show');
|
|
}
|
|
const rp = document.getElementById('reaction-picker');
|
|
if (rp.classList.contains('show') && !e.target.closest('#reaction-picker') && !e.target.closest('.react-btn')) {
|
|
rp.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// ── Reacciones ────────────────────────────────────────────────────────────────
|
|
let _reactionTarget = null;
|
|
|
|
function showReactionPicker(e, waId) {
|
|
e.stopPropagation();
|
|
_reactionTarget = waId;
|
|
const rp = document.getElementById('reaction-picker');
|
|
rp.classList.add('show');
|
|
const rect = e.target.getBoundingClientRect();
|
|
rp.style.top = (rect.top - 60) + 'px';
|
|
rp.style.left = Math.max(4, rect.left - 80) + 'px';
|
|
}
|
|
|
|
async function sendReactionEmoji(emoji) {
|
|
document.getElementById('reaction-picker').classList.remove('show');
|
|
if (!_reactionTarget || !state.activeUserId) return;
|
|
try {
|
|
await fetch(BASE + API_REACT, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ user_id: state.activeUserId, message_id: _reactionTarget, emoji })
|
|
});
|
|
await pollMessages();
|
|
} catch(e) { console.error('reaction:', e); }
|
|
}
|
|
|
|
// ── Grabación de audio ────────────────────────────────────────────────────────
|
|
let mediaRecorder = null, audioChunks = [], recInterval = null, recSeconds = 0;
|
|
|
|
async function toggleRecording() {
|
|
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
|
stopRecording();
|
|
} else {
|
|
await startRecording();
|
|
}
|
|
}
|
|
|
|
async function startRecording() {
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
audioChunks = [];
|
|
mediaRecorder = new MediaRecorder(stream, { mimeType: MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/ogg' });
|
|
mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); };
|
|
mediaRecorder.start(100);
|
|
|
|
recSeconds = 0;
|
|
document.getElementById('recording-bar').style.display = 'flex';
|
|
document.getElementById('recTimer').textContent = '0:00';
|
|
document.getElementById('micBtn').style.color = '#e53e3e';
|
|
recInterval = setInterval(() => {
|
|
recSeconds++;
|
|
const m = Math.floor(recSeconds/60), s = recSeconds%60;
|
|
document.getElementById('recTimer').textContent = m + ':' + String(s).padStart(2,'0');
|
|
}, 1000);
|
|
} catch(e) {
|
|
alert('No se pudo acceder al micrófono: ' + e.message);
|
|
}
|
|
}
|
|
|
|
async function stopRecording() {
|
|
if (!mediaRecorder) return;
|
|
clearInterval(recInterval);
|
|
document.getElementById('recording-bar').style.display = 'none';
|
|
document.getElementById('micBtn').style.color = '';
|
|
|
|
return new Promise(resolve => {
|
|
mediaRecorder.onstop = async () => {
|
|
const mime = mediaRecorder.mimeType || 'audio/webm';
|
|
const blob = new Blob(audioChunks, { type: mime });
|
|
const ext = mime.includes('ogg') ? 'ogg' : 'webm';
|
|
const file = new File([blob], `audio_${Date.now()}.${ext}`, { type: mime });
|
|
mediaRecorder.stream.getTracks().forEach(t => t.stop());
|
|
mediaRecorder = null;
|
|
|
|
if (!state.activeUserId) { resolve(); return; }
|
|
const fd = new FormData();
|
|
fd.append('user_id', state.activeUserId);
|
|
fd.append('file', file);
|
|
fd.append('is_voice', '1');
|
|
try {
|
|
const res = await fetch(BASE + API_MEDIA, { method: 'POST', body: fd });
|
|
const json = await res.json();
|
|
if (json.success) await pollMessages();
|
|
else alert('Error al enviar audio: ' + (json.error || ''));
|
|
} catch(e) { alert('Error de red al enviar audio'); }
|
|
resolve();
|
|
};
|
|
mediaRecorder.stop();
|
|
});
|
|
}
|
|
|
|
function cancelRecording() {
|
|
if (!mediaRecorder) return;
|
|
clearInterval(recInterval);
|
|
mediaRecorder.stream.getTracks().forEach(t => t.stop());
|
|
mediaRecorder = null;
|
|
audioChunks = [];
|
|
document.getElementById('recording-bar').style.display = 'none';
|
|
document.getElementById('micBtn').style.color = '';
|
|
}
|
|
|
|
// ── Plantilla (2 pasos) ───────────────────────────────────────────────────────
|
|
async function openTemplatePicker() {
|
|
if (!state.activeUserId) return;
|
|
_tplSelected = null;
|
|
document.getElementById('tpl-step-list').style.display = '';
|
|
document.getElementById('tpl-step-vars').style.display = 'none';
|
|
document.getElementById('tpl-modal').classList.add('show');
|
|
|
|
const listBody = document.getElementById('tpl-list-body');
|
|
listBody.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8;font-size:.84rem"><i class="fas fa-spinner fa-spin"></i> Cargando…</div>';
|
|
|
|
try {
|
|
const res = await fetch(BASE + API_PLANTILLAS);
|
|
const json = await res.json();
|
|
if (!json.ok || !json.data?.length) {
|
|
listBody.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8;font-size:.84rem">No hay plantillas habilitadas.<br><small>Configúralas en Configuración → Sesión.</small></div>';
|
|
return;
|
|
}
|
|
listBody.innerHTML = '';
|
|
json.data.forEach(tpl => {
|
|
const div = document.createElement('div');
|
|
div.className = 'tpl-item';
|
|
div.innerHTML =
|
|
`<span class="tpl-item-name">${escHtml(tpl.name)}</span>` +
|
|
`<span class="tpl-item-meta">${escHtml(tpl.template_name)} · ${escHtml(tpl.language_code)}</span>` +
|
|
(tpl.body_text ? `<span class="tpl-item-body">${escHtml(tpl.body_text.substring(0, 80))}${tpl.body_text.length > 80 ? '…' : ''}</span>` : '');
|
|
div.onclick = () => selectTemplate(tpl);
|
|
listBody.appendChild(div);
|
|
});
|
|
} catch(e) {
|
|
listBody.innerHTML = '<div style="padding:20px;text-align:center;color:#ef4444;font-size:.84rem">Error al cargar plantillas.</div>';
|
|
}
|
|
}
|
|
|
|
function closeTemplatePicker() {
|
|
document.getElementById('tpl-modal').classList.remove('show');
|
|
_tplSelected = null;
|
|
}
|
|
|
|
function tplGoBack() {
|
|
document.getElementById('tpl-step-list').style.display = '';
|
|
document.getElementById('tpl-step-vars').style.display = 'none';
|
|
_tplSelected = null;
|
|
}
|
|
|
|
async function selectTemplate(tpl) {
|
|
_tplSelected = tpl;
|
|
document.getElementById('tpl-step-list').style.display = 'none';
|
|
document.getElementById('tpl-step-vars').style.display = '';
|
|
document.getElementById('tpl-vars-title').textContent = tpl.name;
|
|
|
|
const previewBox = document.getElementById('tpl-preview-box');
|
|
if (tpl.body_text) {
|
|
previewBox.textContent = tpl.body_text;
|
|
previewBox.style.display = '';
|
|
} else {
|
|
previewBox.style.display = 'none';
|
|
}
|
|
|
|
const varsBody = document.getElementById('tpl-vars-body');
|
|
varsBody.innerHTML = '<div style="padding:8px 16px;color:#94a3b8;font-size:.82rem"><i class="fas fa-spinner fa-spin"></i> Cargando…</div>';
|
|
|
|
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 = '<div style="padding:8px 16px;font-size:.82rem;color:#64748b">Esta plantilla no requiere variables.</div>';
|
|
} else {
|
|
varsBody.innerHTML = '';
|
|
vars.forEach(v => {
|
|
const row = document.createElement('div');
|
|
row.className = 'tpl-var-row';
|
|
const label = v.label || ('Variable ' + v.index);
|
|
const ph = v.example || v.placeholder || label;
|
|
row.innerHTML = `<label>{{${v.index}}} — ${escHtml(label)}</label>` +
|
|
`<input type="text" data-var-index="${v.index}" placeholder="${escHtml(ph)}">`;
|
|
varsBody.appendChild(row);
|
|
});
|
|
}
|
|
} catch(e) {
|
|
varsBody.innerHTML = '<div style="padding:8px 16px;color:#ef4444;font-size:.82rem">Error al cargar variables.</div>';
|
|
}
|
|
}
|
|
|
|
async function sendTemplate() {
|
|
if (!_tplSelected || !state.activeUserId) return;
|
|
const inputs = [...document.querySelectorAll('#tpl-vars-body input[data-var-index]')];
|
|
const params = inputs.map(el => el.value.trim());
|
|
const missing = params.findIndex(p => !p);
|
|
if (missing !== -1) { inputs[missing].focus(); return; }
|
|
|
|
closeTemplatePicker();
|
|
try {
|
|
const res = await fetch(BASE + API_SEND, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
user_id: state.activeUserId,
|
|
type: 'template',
|
|
template: _tplSelected.template_name,
|
|
lang: _tplSelected.language_code,
|
|
params
|
|
})
|
|
});
|
|
const json = await res.json();
|
|
if (!json.success) alert('Error al enviar plantilla: ' + (json.error || ''));
|
|
else await pollMessages();
|
|
} catch(e) { alert('Error de red al enviar plantilla'); }
|
|
}
|
|
|
|
async function markRead(userId) {
|
|
try {
|
|
await fetch(BASE + API_READ, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ user_id: userId })
|
|
});
|
|
} catch(e) {}
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
function handleKey(e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
|
}
|
|
function autoResize(el) {
|
|
el.style.height = 'auto';
|
|
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
|
|
}
|
|
function escHtml(str) {
|
|
return String(str ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
function nl2br(str) { return str.replace(/\n/g, '<br>'); }
|
|
function formatTime(ts) {
|
|
const d = new Date(ts.replace(' ', 'T'));
|
|
const now = new Date();
|
|
if (d.toDateString() === now.toDateString()) {
|
|
return d.toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
return d.toLocaleDateString('es-CO', { day: '2-digit', month: '2-digit' });
|
|
}
|
|
function formatTimeFull(ts) {
|
|
const d = new Date(ts.replace(' ', 'T'));
|
|
return d.toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|