Blame | Last modification | View Log | RSS feed
#[[<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>PJP Agenda Rule Config</title><style>*{box-sizing:border-box}body{margin:0;background:#f4f6fa;color:#1f2937;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}.wrap{max-width:1080px;margin:0 auto;padding:24px 20px 60px}h1{font-size:22px;margin:0 0 4px}.sub{color:#6b7280;font-size:13px;margin-bottom:22px}.card{background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:18px 20px;margin-bottom:22px}.card h2{font-size:15px;margin:0 0 12px}table{width:100%;border-collapse:collapse;font-size:13.5px}th{color:#6b7280;text-align:left;font-weight:600;padding:6px 10px;border-bottom:1px solid #e5e7eb}td{padding:8px 10px;border-bottom:1px solid #f1f5f9;vertical-align:middle}input[type=number],input[type=text]{width:90px;padding:5px 7px;border:1px solid #d1d5db;border-radius:6px;font-size:13px}input.wide{width:130px}button{background:#4f46e5;color:#fff;border:0;border-radius:6px;padding:6px 14px;font-size:13px;cursor:pointer}button.ghost{background:#fff;color:#4f46e5;border:1px solid #c7d2fe}button:disabled{opacity:.5;cursor:default}.muted{color:#9ca3af;font-size:12px}.msg{font-size:13px;margin-left:10px}.msg.ok{color:#059669}.msg.err{color:#dc2626}select{padding:5px 7px;border:1px solid #d1d5db;border-radius:6px;font-size:13px}.note{background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 14px;font-size:12.5px;color:#92400e;margin-bottom:18px}</style></head><body><div class="wrap"><h1>PJP Agenda Rule Config</h1><div class="sub">Auto-agenda thresholds. Defaults apply to every partner; a per-partner override row wins key-by-key.</div><div class="note">LOW_SALES <b>windowDays/baselineDays</b> apply globally only (the nightly sync batches one query for all partners);per-partner overrides of those two keys are ignored. flagPct / clearPct do resolve per partner.REVIVAL has no config here — its threshold belongs to the 23:30 activation-type cron.</div><div class="card"><h2>Global defaults</h2><table id="defaultsTable"><thead><tr><th style="width:170px">Agenda</th><th>Params</th><th style="width:210px">Last update</th><th style="width:110px"></th></tr></thead><tbody></tbody></table></div><div class="card"><h2>Per-partner overrides</h2><table id="overridesTable"><thead><tr><th style="width:120px">Fofo ID</th><th style="width:170px">Agenda</th><th>Params</th><th style="width:210px">Last update</th><th style="width:170px"></th></tr></thead><tbody></tbody></table><div style="margin-top:14px"><b style="font-size:13.5px">Add override:</b>fofoId <input type="number" id="newFofoId" min="1"><select id="newType"></select><span id="newParams"></span><button id="addOverrideBtn">Save override</button><span class="msg" id="addMsg"></span></div></div></div><script>var BASE = location.pathname.replace(/\/$/,'');var state = {rows: [], types: []};function paramInputs(prefix, paramsJson) {var p = {};try { p = JSON.parse(paramsJson || '{}'); } catch (e) {}var h = '';Object.keys(p).forEach(function (k) {h += '<label style="margin-right:12px">' + k +' <input type="number" step="any" min="0.01" data-key="' + k + '" class="' + prefix + '-param" value="' + p[k] + '"></label>';});return h;}function collectParams($scope) {var params = {};$scope.querySelectorAll('input[data-key]').forEach(function (inp) {params[inp.getAttribute('data-key')] = parseFloat(inp.value);});return params;}function save(agendaType, fofoId, params, msgEl, after) {msgEl.textContent = 'Saving…'; msgEl.className = 'msg';fetch(BASE + '/save', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({agendaType: agendaType, fofoId: fofoId, params: params})}).then(function (r) { return r.json().then(function (j) { return {ok: r.ok, j: j}; }); }).then(function (res) {if (res.ok) { msgEl.textContent = 'Saved'; msgEl.className = 'msg ok'; if (after) after(); }else { msgEl.textContent = res.j.error || 'Failed'; msgEl.className = 'msg err'; }}).catch(function () { msgEl.textContent = 'Failed'; msgEl.className = 'msg err'; });}function render() {var dt = document.querySelector('#defaultsTable tbody');var ot = document.querySelector('#overridesTable tbody');dt.innerHTML = ''; ot.innerHTML = '';state.rows.forEach(function (row) {var tr = document.createElement('tr');var upd = (row.updatedBy || '') + (row.updatedOn ? ' · ' + row.updatedOn.replace('T', ' ').slice(0, 16) : '');if (row.fofoId === 0) {tr.innerHTML = '<td><b>' + row.agendaType + '</b></td>' +'<td>' + paramInputs('d' + row.id, row.params) + '</td>' +'<td class="muted">' + upd + '</td>' +'<td><button>Save</button><span class="msg"></span></td>';tr.querySelector('button').onclick = function () {save(row.agendaType, 0, collectParams(tr), tr.querySelector('.msg'));};dt.appendChild(tr);} else {tr.innerHTML = '<td><b>' + row.fofoId + '</b></td><td>' + row.agendaType + '</td>' +'<td>' + paramInputs('o' + row.id, row.params) + '</td>' +'<td class="muted">' + upd + '</td>' +'<td><button>Save</button> <button class="ghost">Remove</button><span class="msg"></span></td>';var btns = tr.querySelectorAll('button');btns[0].onclick = function () {save(row.agendaType, row.fofoId, collectParams(tr), tr.querySelector('.msg'));};btns[1].onclick = function () {if (!confirm('Remove override for fofo ' + row.fofoId + ' / ' + row.agendaType + '?')) return;save(row.agendaType, row.fofoId, {}, tr.querySelector('.msg'), load);};ot.appendChild(tr);}});if (!ot.children.length) {ot.innerHTML = '<tr><td colspan="5" class="muted">No per-partner overrides yet.</td></tr>';}var sel = document.getElementById('newType');sel.innerHTML = '';state.types.forEach(function (t) {var o = document.createElement('option'); o.value = t; o.textContent = t; sel.appendChild(o);});sel.onchange = renderNewParams;renderNewParams();}function renderNewParams() {var t = document.getElementById('newType').value;var def = state.rows.filter(function (r) { return r.fofoId === 0 && r.agendaType === t; })[0];document.getElementById('newParams').innerHTML = def ? paramInputs('new', def.params) : '';}document.getElementById('addOverrideBtn').onclick = function () {var fofoId = parseInt(document.getElementById('newFofoId').value, 10);var msgEl = document.getElementById('addMsg');if (!fofoId || fofoId < 1) { msgEl.textContent = 'Enter a fofoId'; msgEl.className = 'msg err'; return; }save(document.getElementById('newType').value, fofoId,collectParams(document.getElementById('newParams')), msgEl, load);};function load() {fetch(BASE + '/data', {headers: {'Accept': 'application/json'}}).then(function (r) { return r.json(); }).then(function (j) { state.rows = j.rows || []; state.types = j.configurableTypes || []; render(); }).catch(function () {document.querySelector('#defaultsTable tbody').innerHTML ='<tr><td colspan="4" class="msg err">Failed to load config</td></tr>';});}load();</script></body></html>]]#