| 37179 |
amit |
1 |
#[[<!DOCTYPE html>
|
|
|
2 |
<html lang="en">
|
|
|
3 |
<head>
|
|
|
4 |
<meta charset="utf-8">
|
|
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
6 |
<title>PJP Agenda Rule Config</title>
|
|
|
7 |
<style>
|
|
|
8 |
*{box-sizing:border-box}
|
|
|
9 |
body{margin:0;background:#f4f6fa;color:#1f2937;
|
|
|
10 |
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
|
11 |
.wrap{max-width:1080px;margin:0 auto;padding:24px 20px 60px}
|
|
|
12 |
h1{font-size:22px;margin:0 0 4px}
|
|
|
13 |
.sub{color:#6b7280;font-size:13px;margin-bottom:22px}
|
|
|
14 |
.card{background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:18px 20px;margin-bottom:22px}
|
|
|
15 |
.card h2{font-size:15px;margin:0 0 12px}
|
|
|
16 |
table{width:100%;border-collapse:collapse;font-size:13.5px}
|
|
|
17 |
th{color:#6b7280;text-align:left;font-weight:600;padding:6px 10px;border-bottom:1px solid #e5e7eb}
|
|
|
18 |
td{padding:8px 10px;border-bottom:1px solid #f1f5f9;vertical-align:middle}
|
|
|
19 |
input[type=number],input[type=text]{width:90px;padding:5px 7px;border:1px solid #d1d5db;border-radius:6px;font-size:13px}
|
|
|
20 |
input.wide{width:130px}
|
|
|
21 |
button{background:#4f46e5;color:#fff;border:0;border-radius:6px;padding:6px 14px;font-size:13px;cursor:pointer}
|
|
|
22 |
button.ghost{background:#fff;color:#4f46e5;border:1px solid #c7d2fe}
|
|
|
23 |
button:disabled{opacity:.5;cursor:default}
|
|
|
24 |
.muted{color:#9ca3af;font-size:12px}
|
|
|
25 |
.msg{font-size:13px;margin-left:10px}
|
|
|
26 |
.msg.ok{color:#059669}
|
|
|
27 |
.msg.err{color:#dc2626}
|
|
|
28 |
select{padding:5px 7px;border:1px solid #d1d5db;border-radius:6px;font-size:13px}
|
|
|
29 |
.note{background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 14px;font-size:12.5px;color:#92400e;margin-bottom:18px}
|
|
|
30 |
</style>
|
|
|
31 |
</head>
|
|
|
32 |
<body>
|
|
|
33 |
<div class="wrap">
|
|
|
34 |
<h1>PJP Agenda Rule Config</h1>
|
|
|
35 |
<div class="sub">Auto-agenda thresholds. Defaults apply to every partner; a per-partner override row wins key-by-key.</div>
|
|
|
36 |
<div class="note">
|
|
|
37 |
LOW_SALES <b>windowDays/baselineDays</b> apply globally only (the nightly sync batches one query for all partners);
|
|
|
38 |
per-partner overrides of those two keys are ignored. flagPct / clearPct do resolve per partner.
|
|
|
39 |
REVIVAL has no config here — its threshold belongs to the 23:30 activation-type cron.
|
|
|
40 |
</div>
|
|
|
41 |
|
|
|
42 |
<div class="card">
|
|
|
43 |
<h2>Global defaults</h2>
|
|
|
44 |
<table id="defaultsTable"><thead>
|
|
|
45 |
<tr><th style="width:170px">Agenda</th><th>Params</th><th style="width:210px">Last update</th><th style="width:110px"></th></tr>
|
|
|
46 |
</thead><tbody></tbody></table>
|
|
|
47 |
</div>
|
|
|
48 |
|
|
|
49 |
<div class="card">
|
|
|
50 |
<h2>Per-partner overrides</h2>
|
|
|
51 |
<table id="overridesTable"><thead>
|
|
|
52 |
<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>
|
|
|
53 |
</thead><tbody></tbody></table>
|
|
|
54 |
<div style="margin-top:14px">
|
|
|
55 |
<b style="font-size:13.5px">Add override:</b>
|
|
|
56 |
fofoId <input type="number" id="newFofoId" min="1">
|
|
|
57 |
<select id="newType"></select>
|
|
|
58 |
<span id="newParams"></span>
|
|
|
59 |
<button id="addOverrideBtn">Save override</button>
|
|
|
60 |
<span class="msg" id="addMsg"></span>
|
|
|
61 |
</div>
|
|
|
62 |
</div>
|
|
|
63 |
</div>
|
|
|
64 |
|
|
|
65 |
<script>
|
|
|
66 |
var BASE = location.pathname.replace(/\/$/,'');
|
|
|
67 |
var state = {rows: [], types: []};
|
|
|
68 |
|
|
|
69 |
function paramInputs(prefix, paramsJson) {
|
|
|
70 |
var p = {};
|
|
|
71 |
try { p = JSON.parse(paramsJson || '{}'); } catch (e) {}
|
|
|
72 |
var h = '';
|
|
|
73 |
Object.keys(p).forEach(function (k) {
|
|
|
74 |
h += '<label style="margin-right:12px">' + k +
|
|
|
75 |
' <input type="number" step="any" min="0.01" data-key="' + k + '" class="' + prefix + '-param" value="' + p[k] + '"></label>';
|
|
|
76 |
});
|
|
|
77 |
return h;
|
|
|
78 |
}
|
|
|
79 |
|
|
|
80 |
function collectParams($scope) {
|
|
|
81 |
var params = {};
|
|
|
82 |
$scope.querySelectorAll('input[data-key]').forEach(function (inp) {
|
|
|
83 |
params[inp.getAttribute('data-key')] = parseFloat(inp.value);
|
|
|
84 |
});
|
|
|
85 |
return params;
|
|
|
86 |
}
|
|
|
87 |
|
|
|
88 |
function save(agendaType, fofoId, params, msgEl, after) {
|
|
|
89 |
msgEl.textContent = 'Saving…'; msgEl.className = 'msg';
|
|
|
90 |
fetch(BASE + '/save', {
|
|
|
91 |
method: 'POST',
|
|
|
92 |
headers: {'Content-Type': 'application/json'},
|
|
|
93 |
body: JSON.stringify({agendaType: agendaType, fofoId: fofoId, params: params})
|
|
|
94 |
}).then(function (r) { return r.json().then(function (j) { return {ok: r.ok, j: j}; }); })
|
|
|
95 |
.then(function (res) {
|
|
|
96 |
if (res.ok) { msgEl.textContent = 'Saved'; msgEl.className = 'msg ok'; if (after) after(); }
|
|
|
97 |
else { msgEl.textContent = res.j.error || 'Failed'; msgEl.className = 'msg err'; }
|
|
|
98 |
}).catch(function () { msgEl.textContent = 'Failed'; msgEl.className = 'msg err'; });
|
|
|
99 |
}
|
|
|
100 |
|
|
|
101 |
function render() {
|
|
|
102 |
var dt = document.querySelector('#defaultsTable tbody');
|
|
|
103 |
var ot = document.querySelector('#overridesTable tbody');
|
|
|
104 |
dt.innerHTML = ''; ot.innerHTML = '';
|
|
|
105 |
state.rows.forEach(function (row) {
|
|
|
106 |
var tr = document.createElement('tr');
|
|
|
107 |
var upd = (row.updatedBy || '') + (row.updatedOn ? ' · ' + row.updatedOn.replace('T', ' ').slice(0, 16) : '');
|
|
|
108 |
if (row.fofoId === 0) {
|
|
|
109 |
tr.innerHTML = '<td><b>' + row.agendaType + '</b></td>' +
|
|
|
110 |
'<td>' + paramInputs('d' + row.id, row.params) + '</td>' +
|
|
|
111 |
'<td class="muted">' + upd + '</td>' +
|
|
|
112 |
'<td><button>Save</button><span class="msg"></span></td>';
|
|
|
113 |
tr.querySelector('button').onclick = function () {
|
|
|
114 |
save(row.agendaType, 0, collectParams(tr), tr.querySelector('.msg'));
|
|
|
115 |
};
|
|
|
116 |
dt.appendChild(tr);
|
|
|
117 |
} else {
|
|
|
118 |
tr.innerHTML = '<td><b>' + row.fofoId + '</b></td><td>' + row.agendaType + '</td>' +
|
|
|
119 |
'<td>' + paramInputs('o' + row.id, row.params) + '</td>' +
|
|
|
120 |
'<td class="muted">' + upd + '</td>' +
|
|
|
121 |
'<td><button>Save</button> <button class="ghost">Remove</button><span class="msg"></span></td>';
|
|
|
122 |
var btns = tr.querySelectorAll('button');
|
|
|
123 |
btns[0].onclick = function () {
|
|
|
124 |
save(row.agendaType, row.fofoId, collectParams(tr), tr.querySelector('.msg'));
|
|
|
125 |
};
|
|
|
126 |
btns[1].onclick = function () {
|
|
|
127 |
if (!confirm('Remove override for fofo ' + row.fofoId + ' / ' + row.agendaType + '?')) return;
|
|
|
128 |
save(row.agendaType, row.fofoId, {}, tr.querySelector('.msg'), load);
|
|
|
129 |
};
|
|
|
130 |
ot.appendChild(tr);
|
|
|
131 |
}
|
|
|
132 |
});
|
|
|
133 |
if (!ot.children.length) {
|
|
|
134 |
ot.innerHTML = '<tr><td colspan="5" class="muted">No per-partner overrides yet.</td></tr>';
|
|
|
135 |
}
|
|
|
136 |
|
|
|
137 |
var sel = document.getElementById('newType');
|
|
|
138 |
sel.innerHTML = '';
|
|
|
139 |
state.types.forEach(function (t) {
|
|
|
140 |
var o = document.createElement('option'); o.value = t; o.textContent = t; sel.appendChild(o);
|
|
|
141 |
});
|
|
|
142 |
sel.onchange = renderNewParams;
|
|
|
143 |
renderNewParams();
|
|
|
144 |
}
|
|
|
145 |
|
|
|
146 |
function renderNewParams() {
|
|
|
147 |
var t = document.getElementById('newType').value;
|
|
|
148 |
var def = state.rows.filter(function (r) { return r.fofoId === 0 && r.agendaType === t; })[0];
|
|
|
149 |
document.getElementById('newParams').innerHTML = def ? paramInputs('new', def.params) : '';
|
|
|
150 |
}
|
|
|
151 |
|
|
|
152 |
document.getElementById('addOverrideBtn').onclick = function () {
|
|
|
153 |
var fofoId = parseInt(document.getElementById('newFofoId').value, 10);
|
|
|
154 |
var msgEl = document.getElementById('addMsg');
|
|
|
155 |
if (!fofoId || fofoId < 1) { msgEl.textContent = 'Enter a fofoId'; msgEl.className = 'msg err'; return; }
|
|
|
156 |
save(document.getElementById('newType').value, fofoId,
|
|
|
157 |
collectParams(document.getElementById('newParams')), msgEl, load);
|
|
|
158 |
};
|
|
|
159 |
|
|
|
160 |
function load() {
|
|
|
161 |
fetch(BASE + '/data', {headers: {'Accept': 'application/json'}})
|
|
|
162 |
.then(function (r) { return r.json(); })
|
|
|
163 |
.then(function (j) { state.rows = j.rows || []; state.types = j.configurableTypes || []; render(); })
|
|
|
164 |
.catch(function () {
|
|
|
165 |
document.querySelector('#defaultsTable tbody').innerHTML =
|
|
|
166 |
'<tr><td colspan="4" class="msg err">Failed to load config</td></tr>';
|
|
|
167 |
});
|
|
|
168 |
}
|
|
|
169 |
load();
|
|
|
170 |
</script>
|
|
|
171 |
</body>
|
|
|
172 |
</html>]]#
|