Subversion Repositories SmartDukaan

Rev

Rev 37335 | 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>
  </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>