| 37526 |
amit |
1 |
/**
|
|
|
2 |
* Offer circular - scope config screen.
|
|
|
3 |
*
|
|
|
4 |
* Which OEM divisions are ingested, and which alternate labels mean a division that
|
|
|
5 |
* already exists. Everything here is CONFIG read at ingest time, so nothing on this
|
|
|
6 |
* screen changes an already-published circular; the copy says so in several places
|
|
|
7 |
* because that was the single most confusing thing about the product-alias screen.
|
|
|
8 |
*
|
|
|
9 |
* Like the review screen this is an AJAX fragment, so every handler is delegated from
|
|
|
10 |
* document and state is rebuilt on each load. It must also work when the URL is opened
|
|
|
11 |
* directly, which is why it uses $.ajax rather than common.js's helpers.
|
|
|
12 |
*/
|
|
|
13 |
$(function () {
|
|
|
14 |
|
|
|
15 |
var state = { divisions: [], aliases: [], brands: [], categories: [], loaded: false };
|
|
|
16 |
|
|
|
17 |
function ctx() {
|
|
|
18 |
return (typeof context !== 'undefined' && context) ? context : (window.ocsContext || '');
|
|
|
19 |
}
|
|
|
20 |
|
|
|
21 |
function root() {
|
|
|
22 |
return $('.ocs-root');
|
|
|
23 |
}
|
|
|
24 |
|
|
|
25 |
function esc(value) {
|
|
|
26 |
if (value === null || value === undefined) {
|
|
|
27 |
return '';
|
|
|
28 |
}
|
|
|
29 |
return $('<span>').text(String(value)).html();
|
|
|
30 |
}
|
|
|
31 |
|
|
|
32 |
function dash(value) {
|
|
|
33 |
return (value === null || value === undefined || String(value).trim() === '')
|
|
|
34 |
? '<span class="ocs-muted">-</span>' : esc(value);
|
|
|
35 |
}
|
|
|
36 |
|
|
|
37 |
function ocsGet(url, onDone, onFail) {
|
|
|
38 |
$.ajax({ url: url, type: 'GET', cache: false }).done(onDone).fail(onFail || noop);
|
|
|
39 |
}
|
|
|
40 |
|
|
|
41 |
function ocsPost(url, data, onDone, onFail) {
|
|
|
42 |
$.ajax({ url: url, type: 'POST', data: data }).done(onDone).fail(onFail || noop);
|
|
|
43 |
}
|
|
|
44 |
|
|
|
45 |
function noop() {}
|
|
|
46 |
|
|
|
47 |
function failMessage(xhr) {
|
|
|
48 |
if (xhr.status === 0) { return 'could not reach the server'; }
|
|
|
49 |
if (xhr.status === 404) { return 'endpoint not found (404) - is this build deployed?'; }
|
|
|
50 |
if (xhr.status === 403 || xhr.status === 401) { return 'not authorised (' + xhr.status + ')'; }
|
|
|
51 |
return 'server returned status ' + xhr.status;
|
|
|
52 |
}
|
|
|
53 |
|
|
|
54 |
function say(ok, message) {
|
|
|
55 |
var okBox = root().find('[data-ocs-ok]');
|
|
|
56 |
var badBox = root().find('[data-ocs-bad]');
|
|
|
57 |
okBox.hide();
|
|
|
58 |
badBox.hide();
|
|
|
59 |
var box = ok ? okBox : badBox;
|
|
|
60 |
box.text(message).show();
|
|
|
61 |
if (ok) {
|
|
|
62 |
window.setTimeout(function () { box.fadeOut(200); }, 6000);
|
|
|
63 |
}
|
|
|
64 |
}
|
|
|
65 |
|
|
|
66 |
/**
|
|
|
67 |
* The endpoints answer with the "response" view, which may arrive as a JSON string
|
|
|
68 |
* or as an already-parsed object depending on how jQuery read the content type.
|
|
|
69 |
*/
|
|
|
70 |
function payloadOf(response) {
|
|
|
71 |
if (typeof response === 'string') {
|
|
|
72 |
try { return JSON.parse(response); } catch (e) { return null; }
|
|
|
73 |
}
|
|
|
74 |
return response;
|
|
|
75 |
}
|
|
|
76 |
|
|
|
77 |
// ------------------------------------------------------------------ rendering
|
|
|
78 |
|
|
|
79 |
function optionList(items, selected, noneLabel) {
|
|
|
80 |
var html = '<option value="">' + esc(noneLabel) + '</option>';
|
|
|
81 |
items.forEach(function (item) {
|
|
|
82 |
html += '<option value="' + esc(item.id) + '"'
|
|
|
83 |
+ (String(item.id) === String(selected) ? ' selected' : '') + '>'
|
|
|
84 |
+ esc(item.name) + '</option>';
|
|
|
85 |
});
|
|
|
86 |
return html;
|
|
|
87 |
}
|
|
|
88 |
|
|
|
89 |
function renderPickers() {
|
|
|
90 |
root().find('[data-ocs-d-brand]').html(optionList(state.brands, '', '- none -'));
|
|
|
91 |
root().find('[data-ocs-d-category]').html(optionList(state.categories, '', '- none -'));
|
|
|
92 |
|
|
|
93 |
var divisionOptions = state.divisions.map(function (d) {
|
|
|
94 |
return { id: d.pdfLabel, name: d.pdfLabel + ' (' + (d.category || 'no category') + ')' };
|
|
|
95 |
});
|
|
|
96 |
root().find('[data-ocs-a-division]')
|
|
|
97 |
.html(optionList(divisionOptions, '', '- pick one -'));
|
|
|
98 |
}
|
|
|
99 |
|
|
|
100 |
function renderDivisions() {
|
|
|
101 |
var body = root().find('[data-ocs-divisions]');
|
|
|
102 |
if (!state.divisions.length) {
|
|
|
103 |
body.html('<tr><td colspan="9" class="ocs-empty">No divisions configured.</td></tr>');
|
|
|
104 |
return;
|
|
|
105 |
}
|
|
|
106 |
var html = '';
|
|
|
107 |
state.divisions.forEach(function (d, index) {
|
|
|
108 |
html += '<tr class="' + (d.inScope ? '' : 'ocs-off') + '">'
|
|
|
109 |
+ '<td class="ocs-mono">' + esc(d.pdfLabel) + '</td>'
|
|
|
110 |
+ '<td>' + dash(d.displayName) + '</td>'
|
|
|
111 |
+ '<td>' + dash(d.brand) + '</td>'
|
|
|
112 |
+ '<td>' + dash(d.category) + '</td>'
|
|
|
113 |
+ '<td><span class="ocs-pill ' + (d.inScope ? 'ocs-pill-on">in scope'
|
|
|
114 |
: 'ocs-pill-off">dropped')
|
|
|
115 |
+ '</span></td>'
|
|
|
116 |
+ '<td>' + dash(d.dropReason) + '</td>'
|
|
|
117 |
+ '<td>' + esc(d.offerCount) + '</td>'
|
|
|
118 |
+ '<td>' + esc(d.labelAliasCount) + '</td>'
|
|
|
119 |
+ '<td style="white-space:nowrap">'
|
|
|
120 |
+ '<button class="ocs-btn ocs-btn-sm" type="button" data-ocs-edit="'
|
|
|
121 |
+ index + '">Edit</button> '
|
|
|
122 |
+ '<button class="ocs-btn ocs-btn-sm ' + (d.inScope ? 'ocs-btn-danger' : '')
|
|
|
123 |
+ '" type="button" data-ocs-toggle="' + index + '">'
|
|
|
124 |
+ (d.inScope ? 'Remove' : 'Add back') + '</button>'
|
|
|
125 |
+ '</td></tr>';
|
|
|
126 |
});
|
|
|
127 |
body.html(html);
|
|
|
128 |
}
|
|
|
129 |
|
|
|
130 |
function renderAliases() {
|
|
|
131 |
var body = root().find('[data-ocs-aliases]');
|
|
|
132 |
if (!state.aliases.length) {
|
|
|
133 |
body.html('<tr><td colspan="5" class="ocs-empty">'
|
|
|
134 |
+ 'No label aliases yet. Add one when a circular prints a spelling of a '
|
|
|
135 |
+ 'division that is already configured.</td></tr>');
|
|
|
136 |
return;
|
|
|
137 |
}
|
|
|
138 |
var html = '';
|
|
|
139 |
state.aliases.forEach(function (a) {
|
|
|
140 |
html += '<tr>'
|
|
|
141 |
+ '<td class="ocs-mono">' + esc(a.aliasLabel) + '</td>'
|
|
|
142 |
+ '<td><span class="ocs-pill ocs-pill-alias">' + esc(a.divisionLabel)
|
|
|
143 |
+ '</span></td>'
|
|
|
144 |
+ '<td>' + dash(a.note) + '</td>'
|
|
|
145 |
+ '<td class="ocs-muted">' + dash(a.createdBy) + '</td>'
|
|
|
146 |
+ '<td><button class="ocs-btn ocs-btn-sm ocs-btn-danger" type="button" '
|
|
|
147 |
+ 'data-ocs-alias-remove="' + esc(a.aliasLabel) + '">Remove</button></td>'
|
|
|
148 |
+ '</tr>';
|
|
|
149 |
});
|
|
|
150 |
body.html(html);
|
|
|
151 |
}
|
|
|
152 |
|
|
|
153 |
function load() {
|
|
|
154 |
ocsGet(ctx() + '/offerCircular/divisions', function (response) {
|
|
|
155 |
var payload = payloadOf(response);
|
|
|
156 |
if (!payload) {
|
|
|
157 |
say(false, 'Could not read the configuration response');
|
|
|
158 |
return;
|
|
|
159 |
}
|
|
|
160 |
state.divisions = payload.divisions || [];
|
|
|
161 |
state.aliases = payload.aliases || [];
|
|
|
162 |
state.brands = payload.brands || [];
|
|
|
163 |
state.categories = payload.categories || [];
|
|
|
164 |
state.loaded = true;
|
|
|
165 |
renderPickers();
|
|
|
166 |
renderDivisions();
|
|
|
167 |
renderAliases();
|
|
|
168 |
}, function (xhr) {
|
|
|
169 |
root().find('[data-ocs-divisions]').html('<tr><td colspan="9" class="ocs-empty">'
|
|
|
170 |
+ 'Could not load divisions: ' + esc(failMessage(xhr)) + '</td></tr>');
|
|
|
171 |
});
|
|
|
172 |
}
|
|
|
173 |
|
|
|
174 |
// ------------------------------------------------------------------ actions
|
|
|
175 |
|
|
|
176 |
function divisionForm() {
|
|
|
177 |
return {
|
|
|
178 |
pdfLabel: root().find('[data-ocs-d-label]').val(),
|
|
|
179 |
displayName: root().find('[data-ocs-d-display]').val(),
|
|
|
180 |
brandId: root().find('[data-ocs-d-brand]').val(),
|
|
|
181 |
categoryId: root().find('[data-ocs-d-category]').val(),
|
|
|
182 |
inScope: root().find('[data-ocs-d-inscope]').val(),
|
|
|
183 |
dropReason: root().find('[data-ocs-d-reason]').val()
|
|
|
184 |
};
|
|
|
185 |
}
|
|
|
186 |
|
|
|
187 |
function clearDivisionForm() {
|
|
|
188 |
root().find('[data-ocs-d-label], [data-ocs-d-display], [data-ocs-d-reason]').val('');
|
|
|
189 |
root().find('[data-ocs-d-brand], [data-ocs-d-category]').val('');
|
|
|
190 |
root().find('[data-ocs-d-inscope]').val('true');
|
|
|
191 |
}
|
|
|
192 |
|
|
|
193 |
function afterWrite(response, fallback) {
|
|
|
194 |
var payload = payloadOf(response) || {};
|
|
|
195 |
say(payload.success !== false, payload.message || fallback);
|
|
|
196 |
if (payload.success !== false) {
|
|
|
197 |
if (payload.warning) {
|
|
|
198 |
window.setTimeout(function () { say(false, payload.warning); }, 300);
|
|
|
199 |
}
|
|
|
200 |
load();
|
|
|
201 |
}
|
|
|
202 |
}
|
|
|
203 |
|
|
|
204 |
$(document).on('click', '.ocs-root [data-ocs-d-save]', function () {
|
|
|
205 |
var form = divisionForm();
|
|
|
206 |
if (!String(form.pdfLabel || '').trim()) {
|
|
|
207 |
say(false, 'The OEM label is required');
|
|
|
208 |
return;
|
|
|
209 |
}
|
|
|
210 |
ocsPost(ctx() + '/offerCircular/division', form, function (response) {
|
|
|
211 |
afterWrite(response, 'Saved');
|
|
|
212 |
}, function (xhr) { say(false, 'Could not save: ' + failMessage(xhr)); });
|
|
|
213 |
});
|
|
|
214 |
|
|
|
215 |
$(document).on('click', '.ocs-root [data-ocs-d-clear]', clearDivisionForm);
|
|
|
216 |
|
|
|
217 |
$(document).on('click', '.ocs-root [data-ocs-edit]', function () {
|
|
|
218 |
var d = state.divisions[$(this).data('ocs-edit')];
|
|
|
219 |
if (!d) { return; }
|
|
|
220 |
root().find('[data-ocs-d-label]').val(d.pdfLabel);
|
|
|
221 |
root().find('[data-ocs-d-display]').val(d.displayName || '');
|
|
|
222 |
root().find('[data-ocs-d-brand]').val(d.brandId === null ? '' : d.brandId);
|
|
|
223 |
root().find('[data-ocs-d-category]').val(d.categoryId === null ? '' : d.categoryId);
|
|
|
224 |
root().find('[data-ocs-d-inscope]').val(d.inScope ? 'true' : 'false');
|
|
|
225 |
root().find('[data-ocs-d-reason]').val(d.dropReason || '');
|
|
|
226 |
showPanel('divisions');
|
|
|
227 |
root().find('[data-ocs-d-label]').focus();
|
|
|
228 |
});
|
|
|
229 |
|
|
|
230 |
$(document).on('click', '.ocs-root [data-ocs-toggle]', function () {
|
|
|
231 |
var d = state.divisions[$(this).data('ocs-toggle')];
|
|
|
232 |
if (!d) { return; }
|
|
|
233 |
var reason = null;
|
|
|
234 |
if (d.inScope) {
|
|
|
235 |
// The reason is the only record of why something stopped being ingested, so
|
|
|
236 |
// it is asked for rather than defaulted.
|
|
|
237 |
reason = window.prompt('Take "' + d.pdfLabel + '" out of scope.\n\n'
|
|
|
238 |
+ (d.offerCount > 0
|
|
|
239 |
? d.offerCount + ' existing offers stay untouched; this changes what the '
|
|
|
240 |
+ 'next ingest keeps.\n\n' : '')
|
|
|
241 |
+ 'Why is it out of scope?', d.dropReason || '');
|
|
|
242 |
if (reason === null) { return; }
|
|
|
243 |
if (!reason.trim()) {
|
|
|
244 |
say(false, 'A reason is required to take a division out of scope');
|
|
|
245 |
return;
|
|
|
246 |
}
|
|
|
247 |
}
|
|
|
248 |
ocsPost(ctx() + '/offerCircular/divisionScope',
|
|
|
249 |
{ divisionId: d.divisionId, inScope: !d.inScope, dropReason: reason },
|
|
|
250 |
function (response) { afterWrite(response, 'Updated'); },
|
|
|
251 |
function (xhr) { say(false, 'Could not update: ' + failMessage(xhr)); });
|
|
|
252 |
});
|
|
|
253 |
|
|
|
254 |
$(document).on('click', '.ocs-root [data-ocs-a-save]', function () {
|
|
|
255 |
var aliasLabel = root().find('[data-ocs-a-label]').val();
|
|
|
256 |
var divisionLabel = root().find('[data-ocs-a-division]').val();
|
|
|
257 |
if (!String(aliasLabel || '').trim() || !divisionLabel) {
|
|
|
258 |
say(false, 'Both the printed label and the division it means are required');
|
|
|
259 |
return;
|
|
|
260 |
}
|
|
|
261 |
ocsPost(ctx() + '/offerCircular/divisionAlias',
|
|
|
262 |
{ aliasLabel: aliasLabel, divisionLabel: divisionLabel,
|
|
|
263 |
note: root().find('[data-ocs-a-note]').val() },
|
|
|
264 |
function (response) {
|
|
|
265 |
var payload = payloadOf(response) || {};
|
|
|
266 |
if (payload.success !== false) {
|
|
|
267 |
root().find('[data-ocs-a-label], [data-ocs-a-note]').val('');
|
|
|
268 |
}
|
|
|
269 |
afterWrite(response, 'Saved');
|
|
|
270 |
},
|
|
|
271 |
function (xhr) { say(false, 'Could not save: ' + failMessage(xhr)); });
|
|
|
272 |
});
|
|
|
273 |
|
|
|
274 |
$(document).on('click', '.ocs-root [data-ocs-alias-remove]', function () {
|
|
|
275 |
var label = $(this).data('ocs-alias-remove');
|
|
|
276 |
if (!window.confirm('Remove the alias "' + label + '"? That label will be reported '
|
|
|
277 |
+ 'as UNKNOWN on the next ingest.')) {
|
|
|
278 |
return;
|
|
|
279 |
}
|
|
|
280 |
ocsPost(ctx() + '/offerCircular/divisionAliasRemove', { aliasLabel: label },
|
|
|
281 |
function (response) { afterWrite(response, 'Removed'); },
|
|
|
282 |
function (xhr) { say(false, 'Could not remove: ' + failMessage(xhr)); });
|
|
|
283 |
});
|
|
|
284 |
|
|
|
285 |
$(document).on('click', '.ocs-root [data-ocs-reload]', load);
|
|
|
286 |
|
|
|
287 |
$(document).on('click', '.ocs-root [data-ocs-export]', function () {
|
|
|
288 |
var box = root().find('[data-ocs-export-box]');
|
|
|
289 |
if (box.is(':visible')) {
|
|
|
290 |
box.hide();
|
|
|
291 |
return;
|
|
|
292 |
}
|
|
|
293 |
ocsGet(ctx() + '/offerCircular/scopeExport', function (response) {
|
|
|
294 |
var payload = payloadOf(response) || {};
|
|
|
295 |
box.val(payload.sql || '').show();
|
|
|
296 |
box[0].select();
|
|
|
297 |
}, function (xhr) { say(false, 'Could not export: ' + failMessage(xhr)); });
|
|
|
298 |
});
|
|
|
299 |
|
|
|
300 |
function showPanel(name) {
|
|
|
301 |
root().find('[data-ocs-panel]').hide();
|
|
|
302 |
root().find('[data-ocs-panel="' + name + '"]').show();
|
|
|
303 |
root().find('[data-ocs-tab]').removeClass('is-on');
|
|
|
304 |
root().find('[data-ocs-tab="' + name + '"]').addClass('is-on');
|
|
|
305 |
}
|
|
|
306 |
|
|
|
307 |
$(document).on('click', '.ocs-root [data-ocs-tab]', function () {
|
|
|
308 |
showPanel($(this).data('ocs-tab'));
|
|
|
309 |
});
|
|
|
310 |
|
|
|
311 |
// sidebar entry / any trigger loads the fragment then initialises it
|
|
|
312 |
function openOfferCircularScope(domId) {
|
|
|
313 |
ocsGet(ctx() + '/offerCircularScope', function (response) {
|
|
|
314 |
$('#' + (domId || 'main-content')).html(response);
|
|
|
315 |
state = { divisions: [], aliases: [], brands: [], categories: [], loaded: false };
|
|
|
316 |
load();
|
|
|
317 |
});
|
|
|
318 |
}
|
|
|
319 |
window.openOfferCircularScope = openOfferCircularScope;
|
|
|
320 |
|
|
|
321 |
// called by the standalone bootstrap in the .vm when the URL is opened directly
|
|
|
322 |
window.initOfferCircularScope = load;
|
|
|
323 |
|
|
|
324 |
$(document).on('click', '.offer-circular-scope', function (e) {
|
|
|
325 |
e.preventDefault();
|
|
|
326 |
openOfferCircularScope('main-content');
|
|
|
327 |
});
|
|
|
328 |
});
|