Subversion Repositories SmartDukaan

Rev

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 cannot
     wrap usefully - so it is parsed into chips rather than printed. The long drop-reason
     map 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 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>

  <!-- 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 table
       never 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 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>