fix: multiple consistency bugs found in full audit

conversationFlow saveConfig():
- Was not serializing buttons[] array for button-type menus (would erase
  all buttons on save). Now sends menu_btn_id/menu_btn_title like botConfig.
- Defaulted menu type to 'list' instead of 'button'.

conversationFlow addNew('menu') modal:
- Defaulted to 'list' with header/footer/button fields.
- Now defaults to 'button' with inline buttons editor; toggle via type select.
- saveNewMenu() creates correct {type,body,buttons} or {type,...,sections}.

conversationFlow orphan-flow detection:
- Only checked sections[].rows, so flows linked via button menus appeared
  as orphaned. Now checks buttons[] array for button-type menus.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-27 19:57:55 -05:00
co-authored by Claude Sonnet 4.6
parent 0be3d467ff
commit 0555b35766
+88 -29
View File
@@ -3337,11 +3337,28 @@ function addNew(type) {
} else if (type === 'menu') {
showModal(\`
<h2> Nuevo Menú</h2>
<div class="form-group"><label>ID del menú</label><input type="text" id="newMenuKey" placeholder="ej: show_main, sub_servicios"></div>
<div class="form-group"><label>Header</label><input type="text" id="newMenuHeader" placeholder="Título del menú"></div>
<div class="form-group"><label>Body</label><input type="text" id="newMenuBody" placeholder="Cuerpo del mensaje"></div>
<div class="form-group"><label>Footer</label><input type="text" id="newMenuFooter" placeholder="Footer opcional"></div>
<div class="form-group"><label>Texto del botón</label><input type="text" id="newMenuButton" value="Menú" placeholder="Texto del botón"></div>
<div class="form-row" style="margin-bottom:10px">
<div class="form-group" style="min-width:140px"><label>Tipo</label>
<select id="newMenuType" onchange="newMenuTypeToggle()">
<option value="button" selected>Botones directos</option>
<option value="list">Lista (con modal)</option>
</select>
</div>
<div class="form-group" style="flex:2"><label>ID del menú</label><input type="text" id="newMenuKey" placeholder="ej: show_main, sub_servicios"></div>
<div class="form-group" style="flex:3"><label>Texto del mensaje</label><input type="text" id="newMenuBody" placeholder="Selecciona una opción:"></div>
</div>
<div id="newMenuBtnArea">
<div style="font-size:12px;font-weight:600;color:#3d4552;margin-bottom:6px">Botones (máx 3):</div>
<div id="newMenuBtnRows"></div>
<button class="btn-sec" style="font-size:11px;padding:4px 10px;margin-top:4px" onclick="addNewMenuBtnRow()">+ Botón</button>
</div>
<div id="newMenuListArea" style="display:none">
<div class="form-row" style="margin-bottom:8px">
<div class="form-group"><label>Header</label><input type="text" id="newMenuHeader" placeholder="Título"></div>
<div class="form-group"><label>Footer</label><input type="text" id="newMenuFooter" placeholder="Footer opcional"></div>
<div class="form-group"><label>Botón abrir</label><input type="text" id="newMenuButton" value="Ver opciones"></div>
</div>
</div>
<div class="btn-row">
<button class="btn-cancel" onclick="hideModal()">Cancelar</button>
<button class="btn-save" onclick="saveNewMenu()">Crear</button>
@@ -3387,18 +3404,48 @@ function saveNewCommand() {
saveConfig();
}
function newMenuTypeToggle() {
const isBtn = document.getElementById('newMenuType').value === 'button';
document.getElementById('newMenuBtnArea').style.display = isBtn ? '' : 'none';
document.getElementById('newMenuListArea').style.display = isBtn ? 'none' : '';
}
function addNewMenuBtnRow() {
const cont = document.getElementById('newMenuBtnRows');
const bi = cont.querySelectorAll('.new-btn-row').length;
const div = document.createElement('div');
div.className = 'new-btn-row';
div.style.cssText = 'display:flex;gap:6px;margin-bottom:4px';
div.innerHTML = \`<input type="text" class="nbtn-id-\${bi}" placeholder="ID (ej: ver_info)" style="flex:1;padding:5px 8px;border:1px solid #e2e5ea;border-radius:6px;font-size:12px;font-family:monospace">
<input type="text" class="nbtn-title-\${bi}" placeholder="Título (máx 20 chars)" style="flex:2;padding:5px 8px;border:1px solid #e2e5ea;border-radius:6px;font-size:12px">
<button class="btn-cancel" style="padding:4px 8px" onclick="this.closest('.new-btn-row').remove()">✕</button>\`;
cont.appendChild(div);
}
function saveNewMenu() {
const key = document.getElementById('newMenuKey').value.trim();
if (!key) { alert('El ID del menú es requerido'); return; }
if (CONFIG.menus[key]) { alert('El menú "' + key + '" ya existe'); return; }
CONFIG.menus[key] = {
type: 'list',
header: document.getElementById('newMenuHeader').value.trim(),
body: document.getElementById('newMenuBody').value.trim(),
footer: document.getElementById('newMenuFooter').value.trim(),
button: document.getElementById('newMenuButton').value.trim() || 'Menú',
sections: []
};
const menuType = document.getElementById('newMenuType').value;
const body = document.getElementById('newMenuBody').value.trim();
if (menuType === 'button') {
const buttons = [];
document.getElementById('newMenuBtnRows').querySelectorAll('.new-btn-row').forEach((row, bi) => {
const id = (row.querySelector('.nbtn-id-' + bi) || row.querySelector('input:first-child'))?.value?.trim();
const title = (row.querySelector('.nbtn-title-' + bi) || row.querySelector('input:nth-child(2)'))?.value?.trim();
if (id && title) buttons.push({ id, title: title.substring(0, 20) });
});
CONFIG.menus[key] = { type: 'button', body, buttons };
} else {
CONFIG.menus[key] = {
type: 'list',
header: document.getElementById('newMenuHeader')?.value?.trim() || '',
body,
footer: document.getElementById('newMenuFooter')?.value?.trim() || '',
button: document.getElementById('newMenuButton')?.value?.trim() || 'Ver opciones',
sections: []
};
}
hideModal();
saveConfig();
}
@@ -3442,20 +3489,28 @@ function saveConfig() {
// Menus
let mi = 0;
Object.entries(fullConfig.menus || {}).forEach(([mk, menu]) => {
const mtype = menu.type || 'button';
fd.append('menu_key[]', mk);
fd.append('menu_type[]', menu.type || 'list');
fd.append('menu_header[]', menu.header || '');
fd.append('menu_type[]', mtype);
fd.append('menu_body[]', menu.body || '');
fd.append('menu_footer[]', menu.footer || '');
fd.append('menu_button[]', menu.button || 'Menú');
(menu.sections || []).forEach((section, si) => {
fd.append('menu_sections[' + mi + '][' + si + '][title]', section.title);
(section.rows || []).forEach((row, ri) => {
fd.append('menu_sections[' + mi + '][' + si + '][rows][' + ri + '][id]', row.id);
fd.append('menu_sections[' + mi + '][' + si + '][rows][' + ri + '][title]', row.title);
fd.append('menu_sections[' + mi + '][' + si + '][rows][' + ri + '][description]', row.description || '');
if (mtype === 'button') {
(menu.buttons || []).forEach((btn, bi) => {
fd.append('menu_btn_id[' + mi + '][]', btn.id || '');
fd.append('menu_btn_title[' + mi + '][]', btn.title || '');
});
});
} else {
fd.append('menu_header[]', menu.header || '');
fd.append('menu_footer[]', menu.footer || '');
fd.append('menu_button[]', menu.button || 'Ver opciones');
(menu.sections || []).forEach((section, si) => {
fd.append('menu_sections[' + mi + '][' + si + '][title]', section.title);
(section.rows || []).forEach((row, ri) => {
fd.append('menu_sections[' + mi + '][' + si + '][rows][' + ri + '][id]', row.id);
fd.append('menu_sections[' + mi + '][' + si + '][rows][' + ri + '][title]', row.title);
fd.append('menu_sections[' + mi + '][' + si + '][rows][' + ri + '][description]', row.description || '');
});
});
}
mi++;
});
@@ -3600,13 +3655,17 @@ HTML;
echo '</div>';
}
// ── Render orphan flows (not linked by any menu row) ──
// ── Render orphan flows (not linked by any menu item) ──
$linkedFlows = [];
foreach ($menus as $mk => $menu) {
foreach ($menu['sections'] ?? [] as $section) {
foreach ($section['rows'] ?? [] as $row) {
if (isset($flows[$row['id']])) {
$linkedFlows[$row['id']] = true;
if (($menu['type'] ?? 'button') === 'button') {
foreach ($menu['buttons'] ?? [] as $btn) {
if (isset($flows[$btn['id']])) $linkedFlows[$btn['id']] = true;
}
} else {
foreach ($menu['sections'] ?? [] as $section) {
foreach ($section['rows'] ?? [] as $row) {
if (isset($flows[$row['id']])) $linkedFlows[$row['id']] = true;
}
}
}