feat(turnero): template whitelist checklist + 2-step picker in chat
- 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>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
8a7e7a6658
commit
548ad3140d
+141
-48
@@ -228,19 +228,30 @@ html, body { height: 100%; overflow: hidden; font-family: -apple-system, BlinkMa
|
||||
#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 ── */
|
||||
#template-modal {
|
||||
/* ── Modal plantilla (2 pasos) ── */
|
||||
#tpl-modal {
|
||||
display: none; position: fixed; inset: 0; z-index: 1000;
|
||||
background: rgba(0,0,0,.4); align-items: center; justify-content: center;
|
||||
background: rgba(0,0,0,.45); align-items: center; justify-content: center; padding: 16px;
|
||||
}
|
||||
#template-modal.show { display: flex; }
|
||||
.tpl-card { background: #fff; border-radius: 14px; padding: 24px; width: 340px; box-shadow: 0 8px 32px rgba(0,0,0,.2); }
|
||||
.tpl-card h3 { font-size: 1rem; font-weight: 700; margin-bottom: 16px; }
|
||||
.tpl-field { margin-bottom: 12px; }
|
||||
.tpl-field label { font-size: .8rem; color: #555; font-weight: 600; display: block; margin-bottom: 4px; }
|
||||
.tpl-field input { width: 100%; padding: 8px 10px; border: 1.5px solid #e0e0e0; border-radius: 8px; font-size: .9rem; outline: none; }
|
||||
.tpl-field input:focus { border-color: #25d366; }
|
||||
.tpl-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
|
||||
#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; }
|
||||
|
||||
@@ -371,36 +382,41 @@ html, body { height: 100%; overflow: hidden; font-family: -apple-system, BlinkMa
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- ── Modal plantilla ── -->
|
||||
<div id="template-modal">
|
||||
<div class="tpl-card">
|
||||
<h3><i class="fas fa-file-alt" style="color:#25d366;margin-right:6px"></i>Enviar Plantilla</h3>
|
||||
<div class="tpl-field">
|
||||
<label>Nombre de plantilla</label>
|
||||
<input type="text" id="tplName" placeholder="ej: consentimiento_turno">
|
||||
<!-- ── 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 class="tpl-field">
|
||||
<label>Idioma</label>
|
||||
<input type="text" id="tplLang" value="es_CO" placeholder="es_CO">
|
||||
</div>
|
||||
<div class="tpl-field">
|
||||
<label>Parámetros (JSON, opcional)</label>
|
||||
<input type="text" id="tplParams" placeholder='[{"type":"text","text":"valor"}]'>
|
||||
<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="closeTemplatePicker()">Cancelar</button>
|
||||
<button class="btn-tpl-send" onclick="sendTemplate()">Enviar</button>
|
||||
<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_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.*$/, '/') || '/';
|
||||
@@ -894,33 +910,110 @@ function cancelRecording() {
|
||||
document.getElementById('micBtn').style.color = '';
|
||||
}
|
||||
|
||||
// ── Plantilla ─────────────────────────────────────────────────────────────────
|
||||
function openTemplatePicker() {
|
||||
// ── Plantilla (2 pasos) ───────────────────────────────────────────────────────
|
||||
async function openTemplatePicker() {
|
||||
if (!state.activeUserId) return;
|
||||
document.getElementById('template-modal').classList.add('show');
|
||||
document.getElementById('tplName').focus();
|
||||
_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('template-modal').classList.remove('show');
|
||||
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() {
|
||||
const name = document.getElementById('tplName').value.trim();
|
||||
const lang = document.getElementById('tplLang').value.trim() || 'es_CO';
|
||||
const rawP = document.getElementById('tplParams').value.trim();
|
||||
if (!name || !state.activeUserId) return alert('Ingresa el nombre de la plantilla');
|
||||
|
||||
let params = [];
|
||||
if (rawP) {
|
||||
try { params = JSON.parse(rawP); } catch(e) { return alert('Los parámetros no son JSON válido'); }
|
||||
}
|
||||
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: name, lang, params })
|
||||
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 || ''));
|
||||
|
||||
@@ -91,6 +91,22 @@ try {
|
||||
foreach ($cfgRows as $r) { $cfg[$r['clave']] = $r['valor']; }
|
||||
} catch (\Throwable $e) { $_loadErrors[] = 'lab_config: ' . $e->getMessage(); }
|
||||
|
||||
$plantillasAprobadas = [];
|
||||
$plantillasHabilitadas = [];
|
||||
try {
|
||||
$plantillasAprobadas = $pdo->query(
|
||||
"SELECT id, name, template_name, language_code, body_text
|
||||
FROM message_templates WHERE status='approved' ORDER BY name ASC"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS turnero_chat_plantillas (
|
||||
template_id INT NOT NULL, PRIMARY KEY (template_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
$plantillasHabilitadas = array_column(
|
||||
$pdo->query("SELECT template_id FROM turnero_chat_plantillas")->fetchAll(PDO::FETCH_ASSOC),
|
||||
'template_id'
|
||||
);
|
||||
} catch (\Throwable $e) { $_loadErrors[] = 'plantillas_chat: ' . $e->getMessage(); }
|
||||
|
||||
$tab = $_GET['tab'] ?? 'lugares';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
@@ -127,6 +143,14 @@ $tab = $_GET['tab'] ?? 'lugares';
|
||||
.session-open { background:#dcfce7; color:#15803d; }
|
||||
.session-closed{ background:#f1f5f9; color:#64748b; }
|
||||
.consent-tag { font-size:.7rem; background:#ede9fe; color:#5b21b6; border-radius:99px; padding:2px 8px; white-space:normal; word-break:break-word; max-width:260px; }
|
||||
.plantilla-chk-row { display:flex; align-items:flex-start; gap:.65rem; padding:.6rem .8rem; border:1.5px solid #e2e8f0; border-radius:8px; cursor:pointer; background:#fff; transition:border-color .15s,background .15s; }
|
||||
.plantilla-chk-row:hover { background:#f8fafc; border-color:#cbd5e1; }
|
||||
.plantilla-chk-row.is-checked { border-color:#3b82f6; background:#eff6ff; }
|
||||
.plantilla-chk-row input[type=checkbox] { margin-top:.2rem; flex-shrink:0; accent-color:#3b82f6; }
|
||||
.plantilla-chk-body { display:flex; flex-direction:column; gap:.1rem; min-width:0; }
|
||||
.plantilla-chk-name { font-size:.875rem; font-weight:600; color:#1e293b; }
|
||||
.plantilla-chk-meta { font-size:.75rem; color:#64748b; }
|
||||
.plantilla-chk-preview { font-size:.75rem; color:#94a3b8; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:480px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -892,6 +916,41 @@ $tab = $_GET['tab'] ?? 'lugares';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plantillas habilitadas en el chat turnero -->
|
||||
<div class="mt-4">
|
||||
<p class="section-title"><i class="fas fa-comment-dots me-1"></i>Plantillas habilitadas en el chat</p>
|
||||
<p class="text-muted small mb-3">Marca las plantillas aprobadas disponibles en el chat del turnero.</p>
|
||||
<?php if (empty($plantillasAprobadas)): ?>
|
||||
<div class="alert alert-warning py-2 px-3" style="font-size:.85rem">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>
|
||||
No hay plantillas aprobadas. Sincroniza primero desde WhatsApp Business.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="plantillas-chat-list" style="display:grid;gap:.4rem;margin-bottom:.75rem">
|
||||
<?php foreach ($plantillasAprobadas as $pt):
|
||||
$checked = in_array((int)$pt['id'], array_map('intval', $plantillasHabilitadas));
|
||||
?>
|
||||
<label class="plantilla-chk-row<?= $checked ? ' is-checked' : '' ?>">
|
||||
<input type="checkbox" value="<?= (int)$pt['id'] ?>" <?= $checked ? 'checked' : '' ?>
|
||||
onchange="this.closest('label').classList.toggle('is-checked',this.checked)">
|
||||
<div class="plantilla-chk-body">
|
||||
<span class="plantilla-chk-name"><?= htmlspecialchars($pt['name']) ?></span>
|
||||
<span class="plantilla-chk-meta"><?= htmlspecialchars($pt['template_name']) ?> • <?= htmlspecialchars($pt['language_code']) ?></span>
|
||||
<?php if (!empty($pt['body_text'])): ?>
|
||||
<span class="plantilla-chk-preview"><?= htmlspecialchars(mb_substr($pt['body_text'], 0, 100)) . (mb_strlen($pt['body_text']) > 100 ? '…' : '') ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button class="btn btn-primary btn-sm" onclick="guardarPlantillasChat()">
|
||||
<i class="fas fa-save me-1"></i>Guardar plantillas habilitadas
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════════════════
|
||||
TAB 5 — PANTALLA TV
|
||||
════════════════════════════════════════ -->
|
||||
@@ -1358,6 +1417,20 @@ async function subirFotoPerfil(input) {
|
||||
}
|
||||
}
|
||||
|
||||
async function guardarPlantillasChat() {
|
||||
const ids = [...document.querySelectorAll('#plantillas-chat-list input[type=checkbox]:checked')]
|
||||
.map(el => parseInt(el.value, 10));
|
||||
try {
|
||||
const res = await fetch(API + 'chat_save_plantillas.php', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids })
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { toast(json.error || 'Error al guardar', 'error'); return; }
|
||||
toast('Plantillas guardadas (' + ids.length + ')');
|
||||
} catch (e) { toast('Error de conexión', 'error'); }
|
||||
}
|
||||
|
||||
async function guardarWhatsApp() {
|
||||
const template = document.getElementById('wa-template')?.value.trim();
|
||||
const templateMuestra = document.getElementById('wa-template-muestra')?.value.trim();
|
||||
|
||||
Reference in New Issue
Block a user