Subversion Repositories SmartDukaan

Rev

Rev 37331 | Go to most recent revision | 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; }
  .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 how
         each 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, the
         cron job parses it minutes later. Without this the screen looks the same
         before 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">&ndash;</div></div>
    <div class="oc-tile"><div class="oc-tile-label">Parsed</div>
      <div class="oc-tile-value" data-oc-stat="parsed">&ndash;</div></div>
    <div class="oc-tile"><div class="oc-tile-label">Needs review</div>
      <div class="oc-tile-value" data-oc-stat="pending">&ndash;</div></div>
    <div class="oc-tile"><div class="oc-tile-label">Brands</div>
      <div class="oc-tile-value" data-oc-stat="brands">&ndash;</div></div>
  </div>

  <!-- Curation queue. Grouped by distinct (OEM, product text) rather than by circular
       row, because a saved decision is keyed the same way: answer once and every row
       carrying that text is resolved, on this circular and on every later one. -->
  <div class="oc-card" style="margin-bottom:16px;">
    <div class="oc-card-head">
      <span>Resolve products <span class="oc-num" data-oc-resolve-count></span></span>
      <span style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
        <span class="oc-num" data-oc-resolve-pending></span>
        <button class="oc-btn oc-btn-ghost" type="button" data-oc-alias-export
                title="Emit every saved decision as SQL so it can be replayed on another environment">Export as SQL</button>
        <button class="oc-btn oc-btn-ghost" type="button" data-oc-reingest
                title="Saved decisions are only read when the circular is parsed, so they need a re-ingest to take effect">Re-ingest to apply</button>
        <button class="oc-btn" type="button" data-oc-resolve-toggle>Show queue</button>
      </span>
    </div>
    <div data-oc-resolve-wrap style="display:none;">
      <div class="oc-scroll" style="max-height:60vh;">
        <table class="oc-table">
          <thead>
            <tr>
              <th>Product as printed</th>
              <th>OEM</th>
              <th>Affects</th>
              <th>Closest miss</th>
              <th>Decision</th>
              <th></th>
            </tr>
          </thead>
          <tbody data-oc-resolve-body>
            <tr><td colspan="6" class="oc-spin">Loading decisions&hellip;</td></tr>
          </tbody>
        </table>
      </div>
      <div class="oc-foot"><span data-oc-resolve-msg></span></div>
    </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 moment
           the 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&hellip;</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>