| 37526 |
amit |
1 |
/**
|
|
|
2 |
* Offer circular - resolve products.
|
|
|
3 |
*
|
|
|
4 |
* The product-resolution queue, split out of the review screen into its own page. The
|
|
|
5 |
* editor used to be an expanded table row, which is why it never lined up: a form was
|
|
|
6 |
* being laid out inside a grid built for six columns. Here the queue is the list and the
|
|
|
7 |
* decision is a panel beside it.
|
|
|
8 |
*
|
|
|
9 |
* Namespaced .ocr-* / data-ocr-* throughout. The review screen delegates its handlers
|
|
|
10 |
* from document on .oc-root, so sharing a prefix would let both screens' handlers fire on
|
|
|
11 |
* whichever fragment happened to be injected.
|
|
|
12 |
*
|
|
|
13 |
* A decision writes offers.product_alias, which is read at INGEST time - so nothing on
|
|
|
14 |
* this screen changes what a circular currently says until it is re-ingested.
|
|
|
15 |
*/
|
|
|
16 |
$(function () {
|
|
|
17 |
|
|
|
18 |
var state = { decisions: [], tab: 'NAMING', selected: null, documents: [],
|
|
|
19 |
brand: '', huntTimer: null, picked: null, asNaming: {} };
|
|
|
20 |
|
|
|
21 |
function ctx() {
|
|
|
22 |
return (typeof context !== 'undefined' && context) ? context : (window.ocrContext || '');
|
|
|
23 |
}
|
|
|
24 |
|
|
|
25 |
function root() { return $('.ocr-root'); }
|
|
|
26 |
|
|
|
27 |
function esc(value) {
|
|
|
28 |
if (value === null || value === undefined) { return ''; }
|
|
|
29 |
return $('<span>').text(String(value)).html();
|
|
|
30 |
}
|
|
|
31 |
|
|
|
32 |
function ocrGet(url, onDone, onFail) {
|
|
|
33 |
$.ajax({ url: url, type: 'GET', cache: false }).done(onDone).fail(onFail || function () {});
|
|
|
34 |
}
|
|
|
35 |
|
|
|
36 |
function ocrPost(url, data, onDone, onFail) {
|
|
|
37 |
$.ajax({ url: url, type: 'POST', data: data }).done(onDone).fail(onFail || function () {});
|
|
|
38 |
}
|
|
|
39 |
|
|
|
40 |
function payloadOf(response) {
|
|
|
41 |
if (typeof response === 'string') {
|
|
|
42 |
try { return JSON.parse(response); } catch (e) { return null; }
|
|
|
43 |
}
|
|
|
44 |
return response;
|
|
|
45 |
}
|
|
|
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 msg(text, bad) {
|
|
|
55 |
root().find('[data-ocr-msg]')
|
|
|
56 |
.attr('class', 'ocr-msg ' + (text ? (bad ? 'ocr-msg-bad' : 'ocr-msg-ok') : ''))
|
|
|
57 |
.text(text || '');
|
|
|
58 |
}
|
|
|
59 |
|
|
|
60 |
function currentDocumentId() {
|
|
|
61 |
return root().find('[data-ocr-doc]').val() || '';
|
|
|
62 |
}
|
|
|
63 |
|
|
|
64 |
// ------------------------------------------------------------------ queue list
|
|
|
65 |
|
|
|
66 |
function inTab(d) {
|
|
|
67 |
return (d.decisionType || 'NAMING') === state.tab;
|
|
|
68 |
}
|
|
|
69 |
|
|
|
70 |
function visibleDecisions() {
|
|
|
71 |
return state.decisions.filter(function (d) {
|
|
|
72 |
return inTab(d) && (!state.brand || d.divisionLabel === state.brand);
|
|
|
73 |
});
|
|
|
74 |
}
|
|
|
75 |
|
|
|
76 |
/*
|
|
|
77 |
* Brands are listed per TAB, not across the whole queue, and each carries its own
|
|
|
78 |
* count. A brand with nothing left in this tab should not be offered - picking it
|
|
|
79 |
* would empty the list and read as a broken filter rather than as "no work here".
|
|
|
80 |
* The selection is dropped when the brand leaves the tab, for the same reason.
|
|
|
81 |
*/
|
|
|
82 |
function renderBrandFilter() {
|
|
|
83 |
var counts = {};
|
|
|
84 |
state.decisions.forEach(function (d) {
|
|
|
85 |
if (!inTab(d)) { return; }
|
|
|
86 |
var label = d.divisionLabel || '';
|
|
|
87 |
counts[label] = (counts[label] || 0) + 1;
|
|
|
88 |
});
|
|
|
89 |
var labels = Object.keys(counts).sort();
|
|
|
90 |
if (state.brand && counts[state.brand] === undefined) {
|
|
|
91 |
state.brand = '';
|
|
|
92 |
}
|
|
|
93 |
var total = state.decisions.filter(inTab).length;
|
|
|
94 |
var html = '<option value="">All OEMs (' + total + ')</option>'
|
|
|
95 |
+ labels.map(function (label) {
|
|
|
96 |
return '<option value="' + esc(label) + '"'
|
|
|
97 |
+ (state.brand === label ? ' selected' : '') + '>'
|
|
|
98 |
+ esc(label) + ' (' + counts[label] + ')</option>';
|
|
|
99 |
}).join('');
|
|
|
100 |
root().find('[data-ocr-brand]').html(html).val(state.brand);
|
|
|
101 |
}
|
|
|
102 |
|
|
|
103 |
function decisionState(d) {
|
|
|
104 |
if (!d.aliasAction) {
|
|
|
105 |
return '<span class="ocr-pill ocr-pill-todo">undecided</span>';
|
|
|
106 |
}
|
|
|
107 |
var target = d.aliasCanonicalText
|
|
|
108 |
|| (d.aliasCatalogId ? '#' + d.aliasCatalogId : '')
|
|
|
109 |
|| (d.aliasSuperCatalogId ? 'model #' + d.aliasSuperCatalogId : '');
|
|
|
110 |
// "pending" is literal: ProductAliases loads at ingest time, so a saved decision
|
|
|
111 |
// genuinely has not changed anything yet.
|
|
|
112 |
return '<span class="ocr-pill ocr-pill-done">' + esc(d.aliasAction) + '</span>'
|
|
|
113 |
+ (target ? ' <span class="ocr-num">' + esc(target) + '</span>' : '')
|
|
|
114 |
+ '<div class="ocr-num">pending re-ingest</div>';
|
|
|
115 |
}
|
|
|
116 |
|
|
|
117 |
function renderQueue() {
|
|
|
118 |
var body = root().find('[data-ocr-body]');
|
|
|
119 |
renderBrandFilter();
|
|
|
120 |
var rows = visibleDecisions();
|
|
|
121 |
var note = root().find('[data-ocr-listnote]');
|
|
|
122 |
|
|
|
123 |
if (state.tab === 'COVERAGE') {
|
|
|
124 |
note.text('an alias cannot answer these');
|
|
|
125 |
} else {
|
|
|
126 |
note.text(rows.length + (rows.length === 1 ? ' decision' : ' decisions'));
|
|
|
127 |
}
|
|
|
128 |
|
|
|
129 |
if (!rows.length) {
|
|
|
130 |
var empty = state.brand
|
|
|
131 |
? 'Nothing left to resolve for ' + esc(state.brand) + ' here.'
|
|
|
132 |
: (state.tab === 'COVERAGE'
|
|
|
133 |
? 'No bundle rows on this circular.'
|
|
|
134 |
: 'Nothing left to resolve here.');
|
|
|
135 |
body.html('<tr><td colspan="5" class="ocr-empty">' + empty + '</td></tr>');
|
|
|
136 |
return;
|
|
|
137 |
}
|
|
|
138 |
|
|
|
139 |
body.html(rows.map(function (d) {
|
|
|
140 |
var i = state.decisions.indexOf(d);
|
|
|
141 |
var coverage = (d.decisionType || 'NAMING') === 'COVERAGE';
|
|
|
142 |
// Coverage rows are reviewable too - they carry two safe actions - so they get
|
|
|
143 |
// the same affordance. Previously they had no button and no pointer, which
|
|
|
144 |
// read as "this row is inert".
|
|
|
145 |
var action = '<button class="ocr-btn ocr-btn-sm" type="button" '
|
|
|
146 |
+ 'data-ocr-pick-row="' + i + '">'
|
|
|
147 |
+ (coverage ? 'Review' : (d.aliasAction ? 'Change' : 'Resolve')) + '</button>';
|
|
|
148 |
// ONE class attribute. Emitting two made the browser ignore the second, so the
|
|
|
149 |
// selected row never highlighted.
|
|
|
150 |
var cls = ['ocr-rowlink'];
|
|
|
151 |
if (state.selected === i) { cls.push('ocr-row-on'); }
|
|
|
152 |
return '<tr data-ocr-row="' + i + '" class="' + cls.join(' ') + '">'
|
|
|
153 |
+ '<td class="ocr-product">' + esc(d.rawText) + '</td>'
|
|
|
154 |
+ '<td>' + esc(d.divisionLabel) + '</td>'
|
|
|
155 |
+ '<td class="ocr-affects">' + esc(d.rowCount) + ' rows · '
|
|
|
156 |
+ esc(d.offerCount) + ' offers</td>'
|
|
|
157 |
+ '<td>' + (coverage
|
|
|
158 |
? '<span class="ocr-pill ocr-pill-muted">coverage</span>'
|
|
|
159 |
: decisionState(d)) + '</td>'
|
|
|
160 |
+ '<td class="ocr-col-act">' + action + '</td>'
|
|
|
161 |
+ '</tr>';
|
|
|
162 |
}).join(''));
|
|
|
163 |
}
|
|
|
164 |
|
|
|
165 |
// ------------------------------------------------------------------ decision panel
|
|
|
166 |
|
|
|
167 |
function choice(value, title, hint, checked) {
|
|
|
168 |
return '<label class="ocr-choice' + (checked ? ' is-on' : '') + '" data-ocr-choice="'
|
|
|
169 |
+ value + '">'
|
|
|
170 |
+ '<input type="radio" name="ocr-action" value="' + value + '" '
|
|
|
171 |
+ (checked ? 'checked' : '') + ' data-ocr-act />'
|
|
|
172 |
+ '<span class="ocr-choice-text"><b>' + title + '</b><span>' + hint + '</span></span>'
|
|
|
173 |
+ '</label>';
|
|
|
174 |
}
|
|
|
175 |
|
|
|
176 |
function renderPanel() {
|
|
|
177 |
var wrap = root().find('[data-ocr-panel-body]');
|
|
|
178 |
var d = state.selected === null ? null : state.decisions[state.selected];
|
|
|
179 |
root().find('[data-ocr-panel-state]').text('');
|
|
|
180 |
|
|
|
181 |
if (!d) {
|
|
|
182 |
wrap.html('<div class="ocr-panel-empty">Pick a product on the left to resolve it.</div>');
|
|
|
183 |
return;
|
|
|
184 |
}
|
|
|
185 |
// Coverage entries cannot be answered by an alias, but "no answer available" is
|
|
|
186 |
// not the same as "nothing to do". Two things ARE safe and available today, so
|
|
|
187 |
// they are offered rather than leaving the panel a dead end.
|
|
|
188 |
if ((d.decisionType || 'NAMING') === 'COVERAGE' && !state.asNaming[state.selected]) {
|
|
|
189 |
root().find('[data-ocr-panel-state]').text('needs a coverage call');
|
|
|
190 |
wrap.html('<div class="ocr-panel-body">'
|
|
|
191 |
+ '<div class="ocr-subject">' + esc(d.rawText) + '</div>'
|
|
|
192 |
+ '<div class="ocr-context">' + esc(d.divisionLabel) + ' · '
|
|
|
193 |
+ esc(d.rowCount) + ' rows · ' + esc(d.offerCount) + ' offers</div>'
|
|
|
194 |
|
|
|
195 |
+ '<div class="ocr-note">The cashback here is quoted for the phone '
|
|
|
196 |
+ '<em>plus</em> the accessory. Whether it also applies to the phone sold '
|
|
|
197 |
+ 'on its own is a commercial call the circular does not make, and that '
|
|
|
198 |
+ 'needs a per-offer answer, which does not exist yet.</div>'
|
|
|
199 |
|
|
|
200 |
+ '<div class="ocr-choices" style="margin-top:14px;">'
|
|
|
201 |
+ '<label class="ocr-choice" data-ocr-cov-choice="IGNORE">'
|
|
|
202 |
+ '<input type="radio" name="ocr-coverage" value="IGNORE" data-ocr-cov />'
|
|
|
203 |
+ '<span class="ocr-choice-text"><b>We do not stock this bundle</b>'
|
|
|
204 |
+ '<span>Safe: it is a statement about the name, so it needs no coverage '
|
|
|
205 |
+ 'call, and it stops this reappearing every month.</span></span></label>'
|
|
|
206 |
+ '<label class="ocr-choice" data-ocr-cov-choice="NAMING">'
|
|
|
207 |
+ '<input type="radio" name="ocr-coverage" value="NAMING" data-ocr-cov />'
|
|
|
208 |
+ '<span class="ocr-choice-text"><b>This is not a bundle</b>'
|
|
|
209 |
+ '<span>The suffix was mis-read as an accessory. Resolve it as a normal '
|
|
|
210 |
+ 'name instead.</span></span></label>'
|
|
|
211 |
+ '</div>'
|
|
|
212 |
|
|
|
213 |
+ '<div class="ocr-field">'
|
|
|
214 |
+ ' <label>Why</label>'
|
|
|
215 |
+ ' <input class="ocr-input" type="text" data-ocr-note '
|
|
|
216 |
+ ' placeholder="optional \u2014 the next reviewer reads this" />'
|
|
|
217 |
+ '</div>'
|
|
|
218 |
|
|
|
219 |
+ '<div class="ocr-actions">'
|
|
|
220 |
+ ' <button class="ocr-btn ocr-btn-primary" type="button" data-ocr-cov-save>Save decision</button>'
|
|
|
221 |
+ ' <button class="ocr-btn" type="button" data-ocr-clear>Cancel</button>'
|
|
|
222 |
+ '</div>'
|
|
|
223 |
+ '<p class="ocr-help">Leaving it alone is a valid choice: the offer stays '
|
|
|
224 |
+ 'unmatched, so no partner is shown cashback we might not honour. Nothing '
|
|
|
225 |
+ 'is over-credited by waiting.</p>'
|
|
|
226 |
+ '<div class="ocr-msg" data-ocr-msg></div>'
|
|
|
227 |
+ '</div>');
|
|
|
228 |
return;
|
|
|
229 |
}
|
|
|
230 |
|
|
|
231 |
var action = d.aliasAction || 'PIN';
|
|
|
232 |
root().find('[data-ocr-panel-state]').text(d.aliasAction ? 'saved' : 'undecided');
|
|
|
233 |
|
|
|
234 |
wrap.html('<div class="ocr-panel-body">'
|
|
|
235 |
+ '<div class="ocr-subject">' + esc(d.rawText) + '</div>'
|
|
|
236 |
+ '<div class="ocr-context">' + esc(d.divisionLabel) + ' · '
|
|
|
237 |
+ esc(d.rowCount) + ' rows · ' + esc(d.offerCount) + ' offers'
|
|
|
238 |
+ (d.closestCatalogName
|
|
|
239 |
? ' · closest miss ' + esc(d.closestCatalogName)
|
|
|
240 |
+ ' (' + esc(d.closestScore) + ')'
|
|
|
241 |
: ' · no candidate') + '</div>'
|
|
|
242 |
|
|
|
243 |
+ '<div class="ocr-choices">'
|
|
|
244 |
+ choice('PIN', 'Pin to a catalog product',
|
|
|
245 |
'The SKU exists under a different name', action === 'PIN')
|
|
|
246 |
+ choice('REWRITE', 'Rewrite to the catalog spelling',
|
|
|
247 |
'Same product, different wording', action === 'REWRITE')
|
|
|
248 |
+ choice('IGNORE', 'Not ours — ignore',
|
|
|
249 |
'Accessory, or a SKU we do not carry', action === 'IGNORE')
|
|
|
250 |
+ '</div>'
|
|
|
251 |
|
|
|
252 |
+ '<div data-ocr-pane="PIN"' + (action === 'PIN' ? '' : ' style="display:none;"') + '>'
|
|
|
253 |
+ ' <div class="ocr-field">'
|
|
|
254 |
+ ' <label>Find the product in ' + esc(d.divisionLabel) + '</label>'
|
|
|
255 |
+ ' <input class="ocr-input" type="text" data-ocr-hunt '
|
|
|
256 |
+ ' placeholder="type part of the model name" />'
|
|
|
257 |
+ ' <div class="ocr-hits" data-ocr-hits>'
|
|
|
258 |
+ ' <div class="ocr-hint">Type at least two characters to search this '
|
|
|
259 |
+ ' OEM’s catalog.</div>'
|
|
|
260 |
+ ' </div>'
|
|
|
261 |
+ (d.closestCatalogName
|
|
|
262 |
? ' <p class="ocr-help">The closest miss is shown for context only. When '
|
|
|
263 |
+ 'the catalog genuinely lacks the SKU, Ignore is the right answer, not a '
|
|
|
264 |
+ 'near-miss pin.</p>'
|
|
|
265 |
: '')
|
|
|
266 |
+ ' </div>'
|
|
|
267 |
+ '</div>'
|
|
|
268 |
|
|
|
269 |
+ '<div data-ocr-pane="REWRITE"' + (action === 'REWRITE' ? '' : ' style="display:none;"') + '>'
|
|
|
270 |
+ ' <div class="ocr-field">'
|
|
|
271 |
+ ' <label>Catalog spelling</label>'
|
|
|
272 |
+ ' <input class="ocr-input" type="text" data-ocr-canonical '
|
|
|
273 |
+ ' value="' + esc(d.aliasCanonicalText || '') + '" '
|
|
|
274 |
+ ' placeholder="e.g. iPhone 17 Air" />'
|
|
|
275 |
+ ' <p class="ocr-help">The variant spec carries over automatically, so write '
|
|
|
276 |
+ ' the model name only.</p>'
|
|
|
277 |
+ ' </div>'
|
|
|
278 |
+ '</div>'
|
|
|
279 |
|
|
|
280 |
+ '<div class="ocr-field">'
|
|
|
281 |
+ ' <label>Why</label>'
|
|
|
282 |
+ ' <input class="ocr-input" type="text" data-ocr-note '
|
|
|
283 |
+ ' placeholder="optional — the next reviewer reads this" />'
|
|
|
284 |
+ '</div>'
|
|
|
285 |
|
|
|
286 |
+ '<div class="ocr-actions">'
|
|
|
287 |
+ ' <button class="ocr-btn ocr-btn-primary" type="button" data-ocr-save>Save decision</button>'
|
|
|
288 |
+ ' <button class="ocr-btn" type="button" data-ocr-clear>Cancel</button>'
|
|
|
289 |
+ (d.aliasAction
|
|
|
290 |
? ' <button class="ocr-btn" type="button" data-ocr-forget>Remove decision</button>'
|
|
|
291 |
: '')
|
|
|
292 |
+ '</div>'
|
|
|
293 |
+ '<div class="ocr-msg" data-ocr-msg></div>'
|
|
|
294 |
+ '</div>');
|
|
|
295 |
|
|
|
296 |
state.picked = d.aliasCatalogId || null;
|
|
|
297 |
if (state.picked) {
|
|
|
298 |
root().find('[data-ocr-hits]').html('<div class="ocr-hint">Currently pinned to #'
|
|
|
299 |
+ esc(state.picked) + '. Search to pin a different product.</div>');
|
|
|
300 |
}
|
|
|
301 |
}
|
|
|
302 |
|
|
|
303 |
// ------------------------------------------------------------------ loading
|
|
|
304 |
|
|
|
305 |
function loadQueue() {
|
|
|
306 |
var documentId = currentDocumentId();
|
|
|
307 |
ocrGet(ctx() + '/offerCircular/resolveQueue'
|
|
|
308 |
+ (documentId ? '?documentId=' + documentId : ''),
|
|
|
309 |
function (response) {
|
|
|
310 |
var payload = payloadOf(response);
|
|
|
311 |
if (!payload) {
|
|
|
312 |
root().find('[data-ocr-body]').html('<tr><td colspan="5" class="ocr-empty">'
|
|
|
313 |
+ 'Could not read the decision queue.</td></tr>');
|
|
|
314 |
return;
|
|
|
315 |
}
|
|
|
316 |
state.decisions = payload.decisions || [];
|
|
|
317 |
state.selected = null;
|
|
|
318 |
root().find('[data-ocr-count-naming]').text(payload.naming);
|
|
|
319 |
root().find('[data-ocr-count-coverage]').text(payload.coverage);
|
|
|
320 |
var pending = root().find('[data-ocr-pending]');
|
|
|
321 |
if (payload.pendingReingest) {
|
|
|
322 |
pending.text(payload.pendingReingest + ' saved, awaiting re-ingest').show();
|
|
|
323 |
} else {
|
|
|
324 |
pending.hide();
|
|
|
325 |
}
|
|
|
326 |
renderQueue();
|
|
|
327 |
renderPanel();
|
|
|
328 |
},
|
|
|
329 |
function (xhr) {
|
|
|
330 |
root().find('[data-ocr-body]').html('<tr><td colspan="5" class="ocr-empty">'
|
|
|
331 |
+ 'Could not load decisions: ' + esc(failMessage(xhr)) + '</td></tr>');
|
|
|
332 |
});
|
|
|
333 |
}
|
|
|
334 |
|
|
|
335 |
function loadDocuments() {
|
|
|
336 |
ocrGet(ctx() + '/offerCircular/documents', function (response) {
|
|
|
337 |
var payload = payloadOf(response);
|
|
|
338 |
var docs = (payload && (payload.documents || payload)) || [];
|
|
|
339 |
var sel = root().find('[data-ocr-doc]');
|
|
|
340 |
sel.empty().append($('<option>').attr('value', '').text('All circulars'));
|
|
|
341 |
docs.forEach(function (doc) {
|
|
|
342 |
sel.append($('<option>').attr('value', doc.documentId)
|
|
|
343 |
.text(doc.title + ' (' + doc.status + ')'));
|
|
|
344 |
});
|
|
|
345 |
loadQueue();
|
|
|
346 |
}, function () { loadQueue(); });
|
|
|
347 |
}
|
|
|
348 |
|
|
|
349 |
// ------------------------------------------------------------------ handlers
|
|
|
350 |
|
|
|
351 |
$(document).on('click', '.ocr-root [data-ocr-tab]', function () {
|
|
|
352 |
state.tab = $(this).data('ocr-tab');
|
|
|
353 |
state.selected = null;
|
|
|
354 |
root().find('[data-ocr-tab]').removeClass('is-on');
|
|
|
355 |
$(this).addClass('is-on');
|
|
|
356 |
renderQueue();
|
|
|
357 |
renderPanel();
|
|
|
358 |
});
|
|
|
359 |
|
|
|
360 |
// Filtering never re-fetches: the whole queue is already in state, and a round trip
|
|
|
361 |
// would drop the panel's unsaved selection. Clearing the selection is deliberate -
|
|
|
362 |
// the selected row can be one the filter just hid.
|
|
|
363 |
$(document).on('change', '.ocr-root [data-ocr-brand]', function () {
|
|
|
364 |
state.brand = $(this).val() || '';
|
|
|
365 |
state.selected = null;
|
|
|
366 |
renderQueue();
|
|
|
367 |
renderPanel();
|
|
|
368 |
});
|
|
|
369 |
|
|
|
370 |
$(document).on('change', '.ocr-root [data-ocr-doc]', loadQueue);
|
|
|
371 |
|
|
|
372 |
function select(index) {
|
|
|
373 |
state.selected = index;
|
|
|
374 |
root().find('[data-ocr-row]').removeClass('ocr-row-on');
|
|
|
375 |
root().find('[data-ocr-row="' + index + '"]').addClass('ocr-row-on');
|
|
|
376 |
renderPanel();
|
|
|
377 |
}
|
|
|
378 |
|
|
|
379 |
$(document).on('click', '.ocr-root [data-ocr-pick-row]', function (e) {
|
|
|
380 |
e.stopPropagation();
|
|
|
381 |
select($(this).data('ocr-pick-row'));
|
|
|
382 |
});
|
|
|
383 |
|
|
|
384 |
$(document).on('click', '.ocr-root tr[data-ocr-row]', function () {
|
|
|
385 |
select($(this).data('ocr-row'));
|
|
|
386 |
});
|
|
|
387 |
|
|
|
388 |
$(document).on('change', '.ocr-root [data-ocr-act]', function () {
|
|
|
389 |
var chosen = $(this).val();
|
|
|
390 |
root().find('[data-ocr-pane]').each(function () {
|
|
|
391 |
$(this).toggle($(this).attr('data-ocr-pane') === chosen);
|
|
|
392 |
});
|
|
|
393 |
root().find('[data-ocr-choice]').removeClass('is-on');
|
|
|
394 |
root().find('[data-ocr-choice="' + chosen + '"]').addClass('is-on');
|
|
|
395 |
});
|
|
|
396 |
|
|
|
397 |
$(document).on('input', '.ocr-root [data-ocr-hunt]', function () {
|
|
|
398 |
var d = state.decisions[state.selected];
|
|
|
399 |
var term = $(this).val();
|
|
|
400 |
var hits = root().find('[data-ocr-hits]');
|
|
|
401 |
if (!term || term.length < 2) {
|
|
|
402 |
hits.html('<div class="ocr-hint">Type at least two characters to search this '
|
|
|
403 |
+ 'OEM’s catalog.</div>');
|
|
|
404 |
return;
|
|
|
405 |
}
|
|
|
406 |
clearTimeout(state.huntTimer);
|
|
|
407 |
state.huntTimer = setTimeout(function () {
|
|
|
408 |
ocrGet(ctx() + '/offerCircular/catalogSearch?divisionLabel='
|
|
|
409 |
+ encodeURIComponent(d.divisionLabel) + '&q=' + encodeURIComponent(term),
|
|
|
410 |
function (response) {
|
|
|
411 |
var list = payloadOf(response) || [];
|
|
|
412 |
if (!list.length) {
|
|
|
413 |
hits.html('<div class="ocr-hint">No catalog product matches in '
|
|
|
414 |
+ esc(d.divisionLabel) + '’s brand and category. If the SKU '
|
|
|
415 |
+ 'genuinely does not exist, Ignore is the right answer.</div>');
|
|
|
416 |
return;
|
|
|
417 |
}
|
|
|
418 |
hits.html(list.map(function (c) {
|
|
|
419 |
return '<button type="button" class="ocr-hit" data-ocr-pick="'
|
|
|
420 |
+ c.catalogId + '">' + esc(c.name)
|
|
|
421 |
+ ' <span class="ocr-num">#' + c.catalogId + '</span>'
|
|
|
422 |
+ (c.superCatalogName
|
|
|
423 |
? ' <span class="ocr-num">· ' + esc(c.superCatalogName)
|
|
|
424 |
+ '</span>' : '')
|
|
|
425 |
+ '</button>';
|
|
|
426 |
}).join(''));
|
|
|
427 |
},
|
|
|
428 |
function (xhr) {
|
|
|
429 |
hits.html('<div class="ocr-hint">Search failed: '
|
|
|
430 |
+ esc(failMessage(xhr)) + '</div>');
|
|
|
431 |
});
|
|
|
432 |
}, 250);
|
|
|
433 |
});
|
|
|
434 |
|
|
|
435 |
$(document).on('click', '.ocr-root [data-ocr-pick]', function () {
|
|
|
436 |
root().find('[data-ocr-pick]').attr('data-picked', '0');
|
|
|
437 |
$(this).attr('data-picked', '1');
|
|
|
438 |
state.picked = $(this).data('ocr-pick');
|
|
|
439 |
msg('');
|
|
|
440 |
});
|
|
|
441 |
|
|
|
442 |
$(document).on('change', '.ocr-root [data-ocr-cov]', function () {
|
|
|
443 |
var chosen = $(this).val();
|
|
|
444 |
root().find('[data-ocr-cov-choice]').removeClass('is-on');
|
|
|
445 |
root().find('[data-ocr-cov-choice="' + chosen + '"]').addClass('is-on');
|
|
|
446 |
});
|
|
|
447 |
|
|
|
448 |
$(document).on('click', '.ocr-root [data-ocr-cov-save]', function () {
|
|
|
449 |
var d = state.decisions[state.selected];
|
|
|
450 |
var chosen = root().find('[data-ocr-cov]:checked').val();
|
|
|
451 |
if (!chosen) {
|
|
|
452 |
msg('Pick one of the two options first.', true);
|
|
|
453 |
return;
|
|
|
454 |
}
|
|
|
455 |
// "not a bundle" is a reclassification, not a saved decision - it just hands the
|
|
|
456 |
// entry to the normal naming flow for this session.
|
|
|
457 |
if (chosen === 'NAMING') {
|
|
|
458 |
state.asNaming[state.selected] = true;
|
|
|
459 |
renderPanel();
|
|
|
460 |
return;
|
|
|
461 |
}
|
|
|
462 |
var btn = $(this).prop('disabled', true).text('Saving\u2026');
|
|
|
463 |
ocrPost(ctx() + '/offerCircular/alias',
|
|
|
464 |
{ divisionLabel: d.divisionLabel, rawText: d.rawText, action: 'IGNORE',
|
|
|
465 |
note: root().find('[data-ocr-note]').val() || '' },
|
|
|
466 |
function (response) {
|
|
|
467 |
var r = payloadOf(response) || {};
|
|
|
468 |
btn.prop('disabled', false).text('Save decision');
|
|
|
469 |
if (r.success === false) { msg(r.message || 'Could not save', true); return; }
|
|
|
470 |
loadQueue();
|
|
|
471 |
},
|
|
|
472 |
function (xhr) {
|
|
|
473 |
btn.prop('disabled', false).text('Save decision');
|
|
|
474 |
msg('Could not save: ' + failMessage(xhr), true);
|
|
|
475 |
});
|
|
|
476 |
});
|
|
|
477 |
|
|
|
478 |
$(document).on('click', '.ocr-root [data-ocr-clear]', function () {
|
|
|
479 |
state.selected = null;
|
|
|
480 |
root().find('[data-ocr-row]').removeClass('ocr-row-on');
|
|
|
481 |
renderPanel();
|
|
|
482 |
});
|
|
|
483 |
|
|
|
484 |
$(document).on('click', '.ocr-root [data-ocr-save]', function () {
|
|
|
485 |
var d = state.decisions[state.selected];
|
|
|
486 |
if (!d) { return; }
|
|
|
487 |
var action = root().find('[data-ocr-act]:checked').val();
|
|
|
488 |
var payload = {
|
|
|
489 |
divisionLabel: d.divisionLabel,
|
|
|
490 |
rawText: d.rawText,
|
|
|
491 |
action: action,
|
|
|
492 |
note: root().find('[data-ocr-note]').val() || ''
|
|
|
493 |
};
|
|
|
494 |
if (action === 'PIN') {
|
|
|
495 |
if (!state.picked) {
|
|
|
496 |
msg('Pick a catalog product first.', true);
|
|
|
497 |
return;
|
|
|
498 |
}
|
|
|
499 |
payload.catalogId = state.picked;
|
|
|
500 |
payload.matchLevel = 'VARIANT';
|
|
|
501 |
} else if (action === 'REWRITE') {
|
|
|
502 |
payload.canonicalText = root().find('[data-ocr-canonical]').val() || '';
|
|
|
503 |
}
|
|
|
504 |
|
|
|
505 |
var btn = $(this).prop('disabled', true).text('Saving…');
|
|
|
506 |
ocrPost(ctx() + '/offerCircular/alias', payload, function (response) {
|
|
|
507 |
var r = payloadOf(response) || {};
|
|
|
508 |
btn.prop('disabled', false).text('Save decision');
|
|
|
509 |
if (r.success === false) {
|
|
|
510 |
msg(r.message || 'Could not save', true);
|
|
|
511 |
return;
|
|
|
512 |
}
|
|
|
513 |
loadQueue();
|
|
|
514 |
}, function (xhr) {
|
|
|
515 |
btn.prop('disabled', false).text('Save decision');
|
|
|
516 |
msg('Could not save: ' + failMessage(xhr), true);
|
|
|
517 |
});
|
|
|
518 |
});
|
|
|
519 |
|
|
|
520 |
$(document).on('click', '.ocr-root [data-ocr-forget]', function () {
|
|
|
521 |
var d = state.decisions[state.selected];
|
|
|
522 |
if (!d) { return; }
|
|
|
523 |
ocrPost(ctx() + '/offerCircular/aliasRemove',
|
|
|
524 |
{ divisionLabel: d.divisionLabel, rawText: d.rawText },
|
|
|
525 |
function () { loadQueue(); },
|
|
|
526 |
function (xhr) { msg('Could not remove: ' + failMessage(xhr), true); });
|
|
|
527 |
});
|
|
|
528 |
|
|
|
529 |
$(document).on('click', '.ocr-root [data-ocr-export]', function () {
|
|
|
530 |
window.location = ctx() + '/offerCircular/aliasExport';
|
|
|
531 |
});
|
|
|
532 |
|
|
|
533 |
$(document).on('click', '.ocr-root [data-ocr-reingest]', function () {
|
|
|
534 |
var documentId = currentDocumentId();
|
|
|
535 |
if (!documentId) {
|
|
|
536 |
alert('Pick a single circular first - re-ingest re-parses one circular.');
|
|
|
537 |
return;
|
|
|
538 |
}
|
|
|
539 |
if (!window.confirm('Re-parse this circular so the saved decisions take effect?\n\n'
|
|
|
540 |
+ 'It re-reads the stored PDF and rebuilds this circular\'s rows. Saved '
|
|
|
541 |
+ 'decisions and scope config are not touched.')) {
|
|
|
542 |
return;
|
|
|
543 |
}
|
|
|
544 |
var btn = $(this).prop('disabled', true).text('Re-ingesting…');
|
|
|
545 |
ocrPost(ctx() + '/offerCircular/reingest', { documentId: documentId },
|
|
|
546 |
function (response) {
|
|
|
547 |
var r = payloadOf(response) || {};
|
|
|
548 |
btn.prop('disabled', false).text('Re-ingest to apply');
|
|
|
549 |
alert(r.message || 'Re-ingest queued.');
|
|
|
550 |
loadQueue();
|
|
|
551 |
},
|
|
|
552 |
function (xhr) {
|
|
|
553 |
btn.prop('disabled', false).text('Re-ingest to apply');
|
|
|
554 |
alert('Could not re-ingest: ' + failMessage(xhr));
|
|
|
555 |
});
|
|
|
556 |
});
|
|
|
557 |
|
|
|
558 |
function openOfferCircularResolve(domId) {
|
|
|
559 |
ocrGet(ctx() + '/offerCircularResolve', function (response) {
|
|
|
560 |
$('#' + (domId || 'main-content')).html(response);
|
|
|
561 |
state = { decisions: [], tab: 'NAMING', selected: null, documents: [],
|
|
|
562 |
huntTimer: null, picked: null, asNaming: {} };
|
|
|
563 |
loadDocuments();
|
|
|
564 |
});
|
|
|
565 |
}
|
|
|
566 |
window.openOfferCircularResolve = openOfferCircularResolve;
|
|
|
567 |
window.initOfferCircularResolve = loadDocuments;
|
|
|
568 |
|
|
|
569 |
$(document).on('click', '.offer-circular-resolve', function (e) {
|
|
|
570 |
e.preventDefault();
|
|
|
571 |
openOfferCircularResolve('main-content');
|
|
|
572 |
});
|
|
|
573 |
});
|