Subversion Repositories SmartDukaan

Rev

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;
    }

    // ------------------------------------------------------------------ rendering

    function 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>');
        });
    }

    // ------------------------------------------------------------------ actions

    function 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 it
    function 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 directly
    window.initOfferCircularScope = load;

    $(document).on('click', '.offer-circular-scope', function (e) {
        e.preventDefault();
        openOfferCircularScope('main-content');
    });
});