Rev 37335 | Blame | Compare with Previous | Last modification | View Log | RSS feed
<section class="wrapper"><div class="oc-root"><style>/* Scoped to .oc-root so it cannot leak into the rest of the admin theme.Colours use rgb()/var() and selectors use classes, never id-hashes -Velocity would otherwise try to parse a leading "#" as a directive. */.oc-root {--oc-bg: rgb(248, 250, 252);--oc-surface: rgb(255, 255, 255);--oc-border: rgb(226, 232, 240);--oc-text: rgb(15, 23, 42);--oc-muted: rgb(100, 116, 139);--oc-accent: rgb(79, 70, 229);--oc-accent-soft: rgb(238, 242, 255);--oc-ok: rgb(22, 163, 74);--oc-ok-soft: rgb(240, 253, 244);--oc-warn: rgb(180, 83, 9);--oc-warn-soft: rgb(255, 251, 235);--oc-danger: rgb(190, 18, 60);--oc-danger-soft: rgb(255, 241, 242);--oc-radius: 10px;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;color: var(--oc-text);font-size: 13px;line-height: 1.5;/* own padding so the page is readable both inside admin.vm and standalone,where there is no sidebar offset and content would otherwise touch x=0 */padding: 18px 22px 32px;box-sizing: border-box;}.oc-root *, .oc-root *::before, .oc-root *::after { box-sizing: border-box; }.oc-head { display: flex; align-items: flex-start; justify-content: space-between;gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }.oc-title { font-size: 20px; font-weight: 650; letter-spacing: -0.01em; margin: 0; }.oc-sub { color: var(--oc-muted); margin: 4px 0 0; font-size: 12.5px; }/* summary tiles */.oc-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));gap: 10px; margin-bottom: 16px; }.oc-tile { background: var(--oc-surface); border: 1px solid var(--oc-border);border-radius: var(--oc-radius); padding: 12px 14px; }.oc-tile-label { color: var(--oc-muted); font-size: 11px; text-transform: uppercase;letter-spacing: 0.05em; font-weight: 600; }.oc-tile-value { font-size: 22px; font-weight: 650; margin-top: 2px;font-variant-numeric: tabular-nums; }/* filter bar */.oc-filters { background: var(--oc-surface); border: 1px solid var(--oc-border);border-radius: var(--oc-radius); padding: 14px; margin-bottom: 16px;display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }.oc-field { display: flex; flex-direction: column; gap: 5px; }.oc-field > label { font-size: 11px; font-weight: 600; color: var(--oc-muted);text-transform: uppercase; letter-spacing: 0.04em; }.oc-root select.oc-input, .oc-root input.oc-input {height: 34px; padding: 0 10px; border: 1px solid var(--oc-border);border-radius: 7px; background: var(--oc-surface); color: var(--oc-text);font-size: 13px; font-family: inherit; min-width: 140px; box-shadow: none; }.oc-root input.oc-input:focus, .oc-root select.oc-input:focus {outline: 2px solid var(--oc-accent-soft); border-color: var(--oc-accent); }.oc-check { display: flex; align-items: center; gap: 7px; height: 34px;font-size: 12.5px; color: var(--oc-text); }.oc-root button.oc-btn {height: 34px; padding: 0 14px; border-radius: 7px; border: 1px solid var(--oc-accent);background: var(--oc-accent); color: rgb(255, 255, 255); font-size: 13px;font-weight: 550; font-family: inherit; cursor: pointer; }.oc-root button.oc-btn-ghost { background: var(--oc-surface); color: var(--oc-text);border-color: var(--oc-border); }.oc-root button.oc-btn:hover { filter: brightness(0.95); }/* table */.oc-card { background: var(--oc-surface); border: 1px solid var(--oc-border);border-radius: var(--oc-radius); overflow: hidden; }.oc-card-head { padding: 12px 14px; border-bottom: 1px solid var(--oc-border);display: flex; align-items: center; justify-content: space-between;gap: 12px; flex-wrap: wrap; font-weight: 600; }.oc-scroll { overflow-x: auto; max-height: 68vh; overflow-y: auto; }.oc-root table.oc-table { width: 100%; border-collapse: separate; border-spacing: 0;font-size: 12.5px; }.oc-root .oc-table thead th {position: sticky; top: 0; z-index: 2; background: var(--oc-bg);text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;color: var(--oc-muted); font-weight: 650; padding: 9px 10px;border-bottom: 1px solid var(--oc-border); white-space: nowrap; }.oc-root .oc-table tbody td { padding: 9px 10px; border-bottom: 1px solid var(--oc-border);vertical-align: top; }.oc-root .oc-table tbody tr:hover { background: var(--oc-bg); }.oc-root .oc-table tbody tr.oc-detail-row:hover { background: transparent; }/* raw source text wraps to a few lines rather than being ellipsised away -reading the original cell is the entire purpose of this screen */.oc-raw { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden;max-width: 340px; white-space: normal; word-break: break-word; }.oc-clip { display: block; max-width: 320px; overflow: hidden; text-overflow: ellipsis;white-space: nowrap; }.oc-pill-info { background: var(--oc-accent-soft); color: var(--oc-accent);border-color: var(--oc-accent); }/* raw-vs-parsed comparison inside an expanded row */.oc-detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));gap: 24px; align-items: start; }.oc-parsed { font-size: 12.5px; }.oc-sub { margin-bottom: 14px; }.oc-sub-h { display: block; font-size: 11px; text-transform: uppercase;letter-spacing: 0.04em; color: var(--oc-muted); font-weight: 650;margin-bottom: 5px; }.oc-root table.oc-mini { width: 100%; border-collapse: collapse; font-size: 12px;background: var(--oc-surface); border: 1px solid var(--oc-border);border-radius: 7px; overflow: hidden; }.oc-root .oc-mini th { text-align: left; font-size: 10.5px; text-transform: uppercase;letter-spacing: 0.04em; color: var(--oc-muted); font-weight: 650;padding: 6px 9px; border-bottom: 1px solid var(--oc-border);background: var(--oc-bg); }.oc-root .oc-mini td { padding: 6px 9px; border-bottom: 1px solid var(--oc-border);vertical-align: top; word-break: break-word; }.oc-root .oc-mini tr:last-child td { border-bottom: none; }.oc-root .oc-mini code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size: 11px; background: var(--oc-bg); padding: 1px 5px;border-radius: 4px; border: 1px solid var(--oc-border); }.oc-num { font-variant-numeric: tabular-nums; color: var(--oc-muted); white-space: nowrap; }/* Ingest summary. It arrives as a Java Map.toString() - one long line that cannotwrap usefully - so it is parsed into chips rather than printed. The long drop-reasonmap goes behind a disclosure: it is the detail you want when a count looks wrong,and noise the rest of the time. */.oc-sum { display: flex; flex-wrap: wrap; gap: 6px; align-items: center;width: 100%; margin-top: 6px; }.oc-sum-chip { display: inline-flex; align-items: baseline; gap: 5px; padding: 2px 9px;border: 1px solid var(--oc-border); border-radius: 999px;background: var(--oc-surface); font-size: 11.5px; color: var(--oc-muted);white-space: nowrap; }.oc-sum-chip b { font-weight: 650; color: var(--oc-text); font-variant-numeric: tabular-nums; }.oc-sum-chip-warn { background: var(--oc-warn-soft); border-color: rgb(253, 230, 138); }.oc-sum-chip-warn b { color: var(--oc-warn); }.oc-sum-toggle { border: 1px solid var(--oc-border); background: var(--oc-surface);border-radius: 999px; padding: 2px 9px; font-size: 11.5px;color: var(--oc-muted); cursor: pointer; }.oc-sum-toggle:hover { border-color: var(--oc-accent); color: var(--oc-accent); }.oc-sum-detail { width: 100%; margin-top: 6px; border: 1px solid var(--oc-border);border-radius: 8px; background: var(--oc-bg); padding: 6px 10px;max-height: 220px; overflow-y: auto; display: none; }.oc-sum-detail div { display: flex; gap: 14px; justify-content: space-between;padding: 3px 0; font-size: 12px; border-bottom: 1px solid var(--oc-border); }.oc-sum-detail div:last-child { border-bottom: 0; }.oc-sum-detail span:first-child { color: var(--oc-text); word-break: break-word; }.oc-sum-detail span:last-child { color: var(--oc-muted); font-variant-numeric: tabular-nums;white-space: nowrap; }/* anything not parsed still has to wrap rather than run off the card */.oc-sum-raw { white-space: normal; word-break: break-word; }.oc-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size: 12px; }/* pills */.oc-pill { display: inline-block; padding: 2px 8px; border-radius: 999px;font-size: 11px; font-weight: 600; white-space: nowrap; border: 1px solid transparent; }.oc-pill-ok { background: var(--oc-ok-soft); color: var(--oc-ok); border-color: var(--oc-ok); }.oc-pill-warn { background: var(--oc-warn-soft); color: var(--oc-warn); border-color: var(--oc-warn); }.oc-pill-danger { background: var(--oc-danger-soft); color: var(--oc-danger); border-color: var(--oc-danger); }.oc-pill-muted { background: var(--oc-bg); color: var(--oc-muted); border-color: var(--oc-border); }.oc-root button.oc-expand { background: none; border: none; cursor: pointer; padding: 0;color: var(--oc-accent); font-weight: 600; font-size: 12px;font-family: inherit; }/* curation editor, opened under the decision it belongs to */.oc-edit { background: var(--oc-bg); padding: 14px 16px; }.oc-edit-actions { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 10px; }.oc-edit-actions label { display: flex; align-items: center; gap: 6px; font-size: 12.5px; }.oc-edit-row { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap;margin-bottom: 10px; }.oc-hits { max-height: 190px; overflow-y: auto; border: 1px solid var(--oc-border);border-radius: 7px; background: var(--oc-surface); }.oc-hit { display: block; width: 100%; text-align: left; background: none; border: none;border-bottom: 1px solid var(--oc-border); padding: 7px 10px; cursor: pointer;font-family: inherit; font-size: 12.5px; color: var(--oc-text); }.oc-hit:last-child { border-bottom: none; }.oc-hit:hover, .oc-hit[data-picked="1"] { background: var(--oc-accent-soft); }/* the closest miss is shown for context and is deliberately never pre-selected:on Aug'26 four new OnePlus phones each "nearly" matched a 2015 OnePlus 2 */.oc-miss { color: var(--oc-muted); font-size: 12px; }/* verbatim original-row detail */.oc-detail { background: var(--oc-bg); padding: 14px 16px; }.oc-detail-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;color: var(--oc-muted); font-weight: 650; margin-bottom: 10px; }.oc-dl { display: grid; grid-template-columns: 190px 1fr; gap: 6px 16px; margin: 0; }.oc-dl dt { color: var(--oc-muted); font-size: 11.5px; font-weight: 600; }.oc-dl dd { margin: 0; white-space: pre-wrap; word-break: break-word;font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size: 12px; }.oc-foot { padding: 11px 14px; border-top: 1px solid var(--oc-border);display: flex; align-items: center; justify-content: space-between;gap: 12px; flex-wrap: wrap; color: var(--oc-muted); font-size: 12.5px; }.oc-pager { display: flex; align-items: center; gap: 8px; }.oc-empty { padding: 44px 16px; text-align: center; color: var(--oc-muted); }.oc-spin { padding: 30px; text-align: center; color: var(--oc-muted); }@media (prefers-color-scheme: dark) {.oc-root {--oc-bg: rgb(15, 23, 42);--oc-surface: rgb(24, 33, 54);--oc-border: rgb(51, 65, 85);--oc-text: rgb(226, 232, 240);--oc-muted: rgb(148, 163, 184);--oc-accent: rgb(129, 140, 248);--oc-accent-soft: rgb(35, 45, 75);--oc-ok-soft: rgb(20, 45, 32);--oc-warn-soft: rgb(52, 39, 12);--oc-danger-soft: rgb(60, 20, 30);}}</style><div class="oc-head"><div><h1 class="oc-title">Offer Circular Review</h1><p class="oc-sub">Verbatim OEM circular rows as printed in the source PDF, alongside howeach row was interpreted. Mobiles, tablets, smart watches and TVs are in scope.</p></div><div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;"><button class="oc-btn oc-btn-ghost" type="button" data-oc-download>Download source PDF</button><button class="oc-btn oc-btn-ghost" type="button" data-oc-export>Export CSV</button><button class="oc-btn" type="button" data-oc-reload>Refresh</button></div></div><div class="oc-filters" style="margin-bottom:16px;"><div class="oc-field" style="flex:1;min-width:260px;"><label for="oc-doc">Circular</label><select class="oc-input" id="oc-doc" data-oc-doc style="width:100%;"><option value="">Loading circulars...</option></select></div><div class="oc-field"><label for="oc-upload-month">Upload for month</label><input class="oc-input" id="oc-upload-month" type="date" data-oc-upload-month /></div><div class="oc-field"><label for="oc-upload-file">New circular PDF</label><input class="oc-input" id="oc-upload-file" type="file" accept="application/pdf"data-oc-upload-file style="padding-top:5px;" /></div><button class="oc-btn" type="button" data-oc-upload>Upload</button><span data-oc-upload-msg style="font-size:12px;color:var(--oc-muted);"></span><!-- Upload and parsing are different processes: the portal stores the PDF, thecron job parses it minutes later. Without this the screen looks the samebefore and after, so "not parsed yet" is indistinguishable from"parsed, found nothing". --><span data-oc-ingest-state style="flex-basis:100%;"></span></div><div class="oc-tiles"><div class="oc-tile"><div class="oc-tile-label">Rows in circular</div><div class="oc-tile-value" data-oc-stat="total">–</div></div><div class="oc-tile"><div class="oc-tile-label">Parsed</div><div class="oc-tile-value" data-oc-stat="parsed">–</div></div><div class="oc-tile"><div class="oc-tile-label">Needs review</div><div class="oc-tile-value" data-oc-stat="pending">–</div></div><div class="oc-tile"><div class="oc-tile-label">Brands</div><div class="oc-tile-value" data-oc-stat="brands">–</div></div></div><!-- Product resolution moved to its own screen (Offer Circular -> Resolve Products):it is a working queue with a form, and a form laid out inside a six-column tablenever aligned. Keeping a copy here would also double-bind the delegated handlers. --><div class="oc-card" style="margin-bottom:16px;"><div class="oc-card-head"><span>Resolve products</span><button class="oc-btn" type="button" onclick="if(window.openOfferCircularResolve){window.openOfferCircularResolve('main-content');}else{window.location='/offerCircularResolve';}">Open the queue</button></div></div><div class="oc-filters"><div class="oc-field"><label for="oc-brand">Brand</label><select class="oc-input" id="oc-brand" data-oc-filter="brand"><option value="">All brands</option></select></div><div class="oc-field"><label for="oc-month">Circular month</label><!-- deliberately blank: a hardcoded month silently returns nothing the momentthe loaded circular is for a different month --><input class="oc-input" id="oc-month" type="date" data-oc-filter="periodMonth" /></div><div class="oc-field"><label for="oc-from">Live from</label><input class="oc-input" id="oc-from" type="date" data-oc-filter="fromDate" /></div><div class="oc-field"><label for="oc-to">Live to</label><input class="oc-input" id="oc-to" type="date" data-oc-filter="toDate" /></div><div class="oc-field"><label for="oc-size">Per page</label><select class="oc-input" id="oc-size" data-oc-filter="pageSize" style="min-width:90px;"><option>25</option><option selected>50</option><option>100</option><option>200</option></select></div><label class="oc-check"><input type="checkbox" id="oc-unmapped" data-oc-filter="onlyUnmapped" /> Only rows needing review</label><label class="oc-check" title="Mobiles, tablets, smart watches and TVs only. Unchecking also shows laptop, printer and audio rows."><input type="checkbox" id="oc-inscope" data-oc-filter="inScopeOnly" checked /> Relevant categories only</label><button class="oc-btn oc-btn-ghost" type="button" data-oc-clear>Clear</button></div><div class="oc-card"><div class="oc-card-head"><span data-oc-caption>Circular rows</span><span class="oc-num" data-oc-range></span></div><div class="oc-scroll"><table class="oc-table"><thead><tr><th>Page/Row</th><th>OEM (as printed)</th><th>Brand</th><th>Category</th><th>Cashback</th><th>Offer description</th><th>EMI tenure</th><th>Eligible products</th><th>Validity</th><th>Status</th><th></th></tr></thead><tbody data-oc-body><tr><td colspan="11" class="oc-spin">Loading circular rows…</td></tr></tbody></table></div><div class="oc-foot"><span data-oc-summary></span><span class="oc-pager"><button class="oc-btn oc-btn-ghost" type="button" data-oc-prev>Previous</button><span data-oc-page></span><button class="oc-btn oc-btn-ghost" type="button" data-oc-next>Next</button></span></div></div></div><script type="text/javascript">// This page can be opened directly, so it cannot rely on admin.vm having// already set the global "context".window.ocContext = "${rc.contextPath}";// Standalone bootstrap. When this view is injected into admin.vm as a fragment,// jQuery and offer-circular-review.js are already on the page and this does// nothing. When the URL is opened directly the fragment arrives alone, with no// include-scripts.vm, so the screen would sit on "Loading..." forever - load// what is missing ourselves.(function () {var base = window.ocContext || '';function addScript(src, next) {var s = document.createElement('script');s.src = src;s.onload = next;s.onerror = function () {var body = document.querySelector('[data-oc-body]');if (body) {body.innerHTML = '<tr><td colspan="11" class="oc-empty">'+ 'Could not load ' + src + '</td></tr>';}};document.head.appendChild(s);}if (!window.jQuery) {document.documentElement.setAttribute('data-oc-standalone', '1');addScript(base + '/resources/js/jquery-3.2.1.min.js', function () {addScript(base + '/resources/js/offer-circular-review.js?v=${version}');});}})();</script></section>