Blame | Last modification | View Log | RSS feed
/*** Offer circular - scope config screen.** Which OEM divisions are ingested, and which alternate labels mean a division that* already exists. Everything here is CONFIG read at ingest time, so nothing on this* screen changes an already-published circular; the copy says so in several places* because that was the single most confusing thing about the product-alias screen.** Like the review screen this is an AJAX fragment, so every handler is delegated from* document and state is rebuilt on each load. It must also work when the URL is opened* directly, which is why it uses $.ajax rather than common.js's helpers.*/$(function () {var state = { divisions: [], aliases: [], brands: [], categories: [], loaded: false };function ctx() {return (typeof context !== 'undefined' && context) ? context : (window.ocsContext || '');}function root() {return $('.ocs-root');}function esc(value) {if (value === null || value === undefined) {return '';}return $('<span>').text(String(value)).html();}function dash(value) {return (value === null || value === undefined || String(value).trim() === '')? '<span class="ocs-muted">-</span>' : esc(value);}function ocsGet(url, onDone, onFail) {$.ajax({ url: url, type: 'GET', cache: false }).done(onDone).fail(onFail || noop);}function ocsPost(url, data, onDone, onFail) {$.ajax({ url: url, type: 'POST', data: data }).done(onDone).fail(onFail || noop);}function noop() {}function failMessage(xhr) {if (xhr.status === 0) { return 'could not reach the server'; }if (xhr.status === 404) { return 'endpoint not found (404) - is this build deployed?'; }if (xhr.status === 403 || xhr.status === 401) { return 'not authorised (' + xhr.status + ')'; }return 'server returned status ' + xhr.status;}function say(ok, message) {var okBox = root().find('[data-ocs-ok]');var badBox = root().find('[data-ocs-bad]');okBox.hide();badBox.hide();var box = ok ? okBox : badBox;box.text(message).show();if (ok) {window.setTimeout(function () { box.fadeOut(200); }, 6000);}}/*** The endpoints answer with the "response" view, which may arrive as a JSON string* or as an already-parsed object depending on how jQuery read the content type.*/function payloadOf(response) {if (typeof response === 'string') {try { return JSON.parse(response); } catch (e) { return null; }}return response;}// ------------------------------------------------------------------ renderingfunction optionList(items, selected, noneLabel) {var html = '<option value="">' + esc(noneLabel) + '</option>';items.forEach(function (item) {html += '<option value="' + esc(item.id) + '"'+ (String(item.id) === String(selected) ? ' selected' : '') + '>'+ esc(item.name) + '</option>';});return html;}function renderPickers() {root().find('[data-ocs-d-brand]').html(optionList(state.brands, '', '- none -'));root().find('[data-ocs-d-category]').html(optionList(state.categories, '', '- none -'));var divisionOptions = state.divisions.map(function (d) {return { id: d.pdfLabel, name: d.pdfLabel + ' (' + (d.category || 'no category') + ')' };});root().find('[data-ocs-a-division]').html(optionList(divisionOptions, '', '- pick one -'));}function renderDivisions() {var body = root().find('[data-ocs-divisions]');if (!state.divisions.length) {body.html('<tr><td colspan="9" class="ocs-empty">No divisions configured.</td></tr>');return;}var html = '';state.divisions.forEach(function (d, index) {html += '<tr class="' + (d.inScope ? '' : 'ocs-off') + '">'+ '<td class="ocs-mono">' + esc(d.pdfLabel) + '</td>'+ '<td>' + dash(d.displayName) + '</td>'+ '<td>' + dash(d.brand) + '</td>'+ '<td>' + dash(d.category) + '</td>'+ '<td><span class="ocs-pill ' + (d.inScope ? 'ocs-pill-on">in scope': 'ocs-pill-off">dropped')+ '</span></td>'+ '<td>' + dash(d.dropReason) + '</td>'+ '<td>' + esc(d.offerCount) + '</td>'+ '<td>' + esc(d.labelAliasCount) + '</td>'+ '<td style="white-space:nowrap">'+ '<button class="ocs-btn ocs-btn-sm" type="button" data-ocs-edit="'+ index + '">Edit</button> '+ '<button class="ocs-btn ocs-btn-sm ' + (d.inScope ? 'ocs-btn-danger' : '')+ '" type="button" data-ocs-toggle="' + index + '">'+ (d.inScope ? 'Remove' : 'Add back') + '</button>'+ '</td></tr>';});body.html(html);}function renderAliases() {var body = root().find('[data-ocs-aliases]');if (!state.aliases.length) {body.html('<tr><td colspan="5" class="ocs-empty">'+ 'No label aliases yet. Add one when a circular prints a spelling of a '+ 'division that is already configured.</td></tr>');return;}var html = '';state.aliases.forEach(function (a) {html += '<tr>'+ '<td class="ocs-mono">' + esc(a.aliasLabel) + '</td>'+ '<td><span class="ocs-pill ocs-pill-alias">' + esc(a.divisionLabel)+ '</span></td>'+ '<td>' + dash(a.note) + '</td>'+ '<td class="ocs-muted">' + dash(a.createdBy) + '</td>'+ '<td><button class="ocs-btn ocs-btn-sm ocs-btn-danger" type="button" '+ 'data-ocs-alias-remove="' + esc(a.aliasLabel) + '">Remove</button></td>'+ '</tr>';});body.html(html);}function load() {ocsGet(ctx() + '/offerCircular/divisions', function (response) {var payload = payloadOf(response);if (!payload) {say(false, 'Could not read the configuration response');return;}state.divisions = payload.divisions || [];state.aliases = payload.aliases || [];state.brands = payload.brands || [];state.categories = payload.categories || [];state.loaded = true;renderPickers();renderDivisions();renderAliases();}, function (xhr) {root().find('[data-ocs-divisions]').html('<tr><td colspan="9" class="ocs-empty">'+ 'Could not load divisions: ' + esc(failMessage(xhr)) + '</td></tr>');});}// ------------------------------------------------------------------ actionsfunction divisionForm() {return {pdfLabel: root().find('[data-ocs-d-label]').val(),displayName: root().find('[data-ocs-d-display]').val(),brandId: root().find('[data-ocs-d-brand]').val(),categoryId: root().find('[data-ocs-d-category]').val(),inScope: root().find('[data-ocs-d-inscope]').val(),dropReason: root().find('[data-ocs-d-reason]').val()};}function clearDivisionForm() {root().find('[data-ocs-d-label], [data-ocs-d-display], [data-ocs-d-reason]').val('');root().find('[data-ocs-d-brand], [data-ocs-d-category]').val('');root().find('[data-ocs-d-inscope]').val('true');}function afterWrite(response, fallback) {var payload = payloadOf(response) || {};say(payload.success !== false, payload.message || fallback);if (payload.success !== false) {if (payload.warning) {window.setTimeout(function () { say(false, payload.warning); }, 300);}load();}}$(document).on('click', '.ocs-root [data-ocs-d-save]', function () {var form = divisionForm();if (!String(form.pdfLabel || '').trim()) {say(false, 'The OEM label is required');return;}ocsPost(ctx() + '/offerCircular/division', form, function (response) {afterWrite(response, 'Saved');}, function (xhr) { say(false, 'Could not save: ' + failMessage(xhr)); });});$(document).on('click', '.ocs-root [data-ocs-d-clear]', clearDivisionForm);$(document).on('click', '.ocs-root [data-ocs-edit]', function () {var d = state.divisions[$(this).data('ocs-edit')];if (!d) { return; }root().find('[data-ocs-d-label]').val(d.pdfLabel);root().find('[data-ocs-d-display]').val(d.displayName || '');root().find('[data-ocs-d-brand]').val(d.brandId === null ? '' : d.brandId);root().find('[data-ocs-d-category]').val(d.categoryId === null ? '' : d.categoryId);root().find('[data-ocs-d-inscope]').val(d.inScope ? 'true' : 'false');root().find('[data-ocs-d-reason]').val(d.dropReason || '');showPanel('divisions');root().find('[data-ocs-d-label]').focus();});$(document).on('click', '.ocs-root [data-ocs-toggle]', function () {var d = state.divisions[$(this).data('ocs-toggle')];if (!d) { return; }var reason = null;if (d.inScope) {// The reason is the only record of why something stopped being ingested, so// it is asked for rather than defaulted.reason = window.prompt('Take "' + d.pdfLabel + '" out of scope.\n\n'+ (d.offerCount > 0? d.offerCount + ' existing offers stay untouched; this changes what the '+ 'next ingest keeps.\n\n' : '')+ 'Why is it out of scope?', d.dropReason || '');if (reason === null) { return; }if (!reason.trim()) {say(false, 'A reason is required to take a division out of scope');return;}}ocsPost(ctx() + '/offerCircular/divisionScope',{ divisionId: d.divisionId, inScope: !d.inScope, dropReason: reason },function (response) { afterWrite(response, 'Updated'); },function (xhr) { say(false, 'Could not update: ' + failMessage(xhr)); });});$(document).on('click', '.ocs-root [data-ocs-a-save]', function () {var aliasLabel = root().find('[data-ocs-a-label]').val();var divisionLabel = root().find('[data-ocs-a-division]').val();if (!String(aliasLabel || '').trim() || !divisionLabel) {say(false, 'Both the printed label and the division it means are required');return;}ocsPost(ctx() + '/offerCircular/divisionAlias',{ aliasLabel: aliasLabel, divisionLabel: divisionLabel,note: root().find('[data-ocs-a-note]').val() },function (response) {var payload = payloadOf(response) || {};if (payload.success !== false) {root().find('[data-ocs-a-label], [data-ocs-a-note]').val('');}afterWrite(response, 'Saved');},function (xhr) { say(false, 'Could not save: ' + failMessage(xhr)); });});$(document).on('click', '.ocs-root [data-ocs-alias-remove]', function () {var label = $(this).data('ocs-alias-remove');if (!window.confirm('Remove the alias "' + label + '"? That label will be reported '+ 'as UNKNOWN on the next ingest.')) {return;}ocsPost(ctx() + '/offerCircular/divisionAliasRemove', { aliasLabel: label },function (response) { afterWrite(response, 'Removed'); },function (xhr) { say(false, 'Could not remove: ' + failMessage(xhr)); });});$(document).on('click', '.ocs-root [data-ocs-reload]', load);$(document).on('click', '.ocs-root [data-ocs-export]', function () {var box = root().find('[data-ocs-export-box]');if (box.is(':visible')) {box.hide();return;}ocsGet(ctx() + '/offerCircular/scopeExport', function (response) {var payload = payloadOf(response) || {};box.val(payload.sql || '').show();box[0].select();}, function (xhr) { say(false, 'Could not export: ' + failMessage(xhr)); });});function showPanel(name) {root().find('[data-ocs-panel]').hide();root().find('[data-ocs-panel="' + name + '"]').show();root().find('[data-ocs-tab]').removeClass('is-on');root().find('[data-ocs-tab="' + name + '"]').addClass('is-on');}$(document).on('click', '.ocs-root [data-ocs-tab]', function () {showPanel($(this).data('ocs-tab'));});// sidebar entry / any trigger loads the fragment then initialises itfunction openOfferCircularScope(domId) {ocsGet(ctx() + '/offerCircularScope', function (response) {$('#' + (domId || 'main-content')).html(response);state = { divisions: [], aliases: [], brands: [], categories: [], loaded: false };load();});}window.openOfferCircularScope = openOfferCircularScope;// called by the standalone bootstrap in the .vm when the URL is opened directlywindow.initOfferCircularScope = load;$(document).on('click', '.offer-circular-scope', function (e) {e.preventDefault();openOfferCircularScope('main-content');});});