Subversion Repositories SmartDukaan

Rev

Rev 37331 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
37331 amit 1
<section class="wrapper">
2
<div class="oc-root">
3
 
4
  <style>
5
  /* Scoped to .oc-root so it cannot leak into the rest of the admin theme.
6
     Colours use rgb()/var() and selectors use classes, never id-hashes -
7
     Velocity would otherwise try to parse a leading "#" as a directive. */
8
  .oc-root {
9
    --oc-bg: rgb(248, 250, 252);
10
    --oc-surface: rgb(255, 255, 255);
11
    --oc-border: rgb(226, 232, 240);
12
    --oc-text: rgb(15, 23, 42);
13
    --oc-muted: rgb(100, 116, 139);
14
    --oc-accent: rgb(79, 70, 229);
15
    --oc-accent-soft: rgb(238, 242, 255);
16
    --oc-ok: rgb(22, 163, 74);
17
    --oc-ok-soft: rgb(240, 253, 244);
18
    --oc-warn: rgb(180, 83, 9);
19
    --oc-warn-soft: rgb(255, 251, 235);
20
    --oc-danger: rgb(190, 18, 60);
21
    --oc-danger-soft: rgb(255, 241, 242);
22
    --oc-radius: 10px;
23
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
24
    color: var(--oc-text);
25
    font-size: 13px;
26
    line-height: 1.5;
27
    /* own padding so the page is readable both inside admin.vm and standalone,
28
       where there is no sidebar offset and content would otherwise touch x=0 */
29
    padding: 18px 22px 32px;
30
    box-sizing: border-box;
31
  }
32
  .oc-root *, .oc-root *::before, .oc-root *::after { box-sizing: border-box; }
33
 
34
  .oc-head { display: flex; align-items: flex-start; justify-content: space-between;
35
             gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }
36
  .oc-title { font-size: 20px; font-weight: 650; letter-spacing: -0.01em; margin: 0; }
37
  .oc-sub { color: var(--oc-muted); margin: 4px 0 0; font-size: 12.5px; }
38
 
39
  /* summary tiles */
40
  .oc-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
41
              gap: 10px; margin-bottom: 16px; }
42
  .oc-tile { background: var(--oc-surface); border: 1px solid var(--oc-border);
43
             border-radius: var(--oc-radius); padding: 12px 14px; }
44
  .oc-tile-label { color: var(--oc-muted); font-size: 11px; text-transform: uppercase;
45
                   letter-spacing: 0.05em; font-weight: 600; }
46
  .oc-tile-value { font-size: 22px; font-weight: 650; margin-top: 2px;
47
                   font-variant-numeric: tabular-nums; }
48
 
49
  /* filter bar */
50
  .oc-filters { background: var(--oc-surface); border: 1px solid var(--oc-border);
51
                border-radius: var(--oc-radius); padding: 14px; margin-bottom: 16px;
52
                display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
53
  .oc-field { display: flex; flex-direction: column; gap: 5px; }
54
  .oc-field > label { font-size: 11px; font-weight: 600; color: var(--oc-muted);
55
                      text-transform: uppercase; letter-spacing: 0.04em; }
56
  .oc-root select.oc-input, .oc-root input.oc-input {
57
    height: 34px; padding: 0 10px; border: 1px solid var(--oc-border);
58
    border-radius: 7px; background: var(--oc-surface); color: var(--oc-text);
59
    font-size: 13px; font-family: inherit; min-width: 140px; box-shadow: none; }
60
  .oc-root input.oc-input:focus, .oc-root select.oc-input:focus {
61
    outline: 2px solid var(--oc-accent-soft); border-color: var(--oc-accent); }
62
  .oc-check { display: flex; align-items: center; gap: 7px; height: 34px;
63
              font-size: 12.5px; color: var(--oc-text); }
64
  .oc-root button.oc-btn {
65
    height: 34px; padding: 0 14px; border-radius: 7px; border: 1px solid var(--oc-accent);
66
    background: var(--oc-accent); color: rgb(255, 255, 255); font-size: 13px;
67
    font-weight: 550; font-family: inherit; cursor: pointer; }
68
  .oc-root button.oc-btn-ghost { background: var(--oc-surface); color: var(--oc-text);
69
                                 border-color: var(--oc-border); }
70
  .oc-root button.oc-btn:hover { filter: brightness(0.95); }
71
 
72
  /* table */
73
  .oc-card { background: var(--oc-surface); border: 1px solid var(--oc-border);
74
             border-radius: var(--oc-radius); overflow: hidden; }
75
  .oc-card-head { padding: 12px 14px; border-bottom: 1px solid var(--oc-border);
76
                  display: flex; align-items: center; justify-content: space-between;
77
                  gap: 12px; flex-wrap: wrap; font-weight: 600; }
78
  .oc-scroll { overflow-x: auto; max-height: 68vh; overflow-y: auto; }
79
  .oc-root table.oc-table { width: 100%; border-collapse: separate; border-spacing: 0;
80
                            font-size: 12.5px; }
81
  .oc-root .oc-table thead th {
82
    position: sticky; top: 0; z-index: 2; background: var(--oc-bg);
83
    text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
84
    color: var(--oc-muted); font-weight: 650; padding: 9px 10px;
85
    border-bottom: 1px solid var(--oc-border); white-space: nowrap; }
86
  .oc-root .oc-table tbody td { padding: 9px 10px; border-bottom: 1px solid var(--oc-border);
87
                                vertical-align: top; }
88
  .oc-root .oc-table tbody tr:hover { background: var(--oc-bg); }
89
  .oc-root .oc-table tbody tr.oc-detail-row:hover { background: transparent; }
90
  /* raw source text wraps to a few lines rather than being ellipsised away -
91
     reading the original cell is the entire purpose of this screen */
92
  .oc-raw { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden;
93
            max-width: 340px; white-space: normal; word-break: break-word; }
94
  .oc-clip { display: block; max-width: 320px; overflow: hidden; text-overflow: ellipsis;
95
             white-space: nowrap; }
96
  .oc-pill-info { background: var(--oc-accent-soft); color: var(--oc-accent);
97
                  border-color: var(--oc-accent); }
98
 
99
  /* raw-vs-parsed comparison inside an expanded row */
100
  .oc-detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
101
                    gap: 24px; align-items: start; }
102
  .oc-parsed { font-size: 12.5px; }
103
  .oc-sub { margin-bottom: 14px; }
104
  .oc-sub-h { display: block; font-size: 11px; text-transform: uppercase;
105
              letter-spacing: 0.04em; color: var(--oc-muted); font-weight: 650;
106
              margin-bottom: 5px; }
107
  .oc-root table.oc-mini { width: 100%; border-collapse: collapse; font-size: 12px;
108
                           background: var(--oc-surface); border: 1px solid var(--oc-border);
109
                           border-radius: 7px; overflow: hidden; }
110
  .oc-root .oc-mini th { text-align: left; font-size: 10.5px; text-transform: uppercase;
111
                         letter-spacing: 0.04em; color: var(--oc-muted); font-weight: 650;
112
                         padding: 6px 9px; border-bottom: 1px solid var(--oc-border);
113
                         background: var(--oc-bg); }
114
  .oc-root .oc-mini td { padding: 6px 9px; border-bottom: 1px solid var(--oc-border);
115
                         vertical-align: top; word-break: break-word; }
116
  .oc-root .oc-mini tr:last-child td { border-bottom: none; }
117
  .oc-root .oc-mini code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
118
                           font-size: 11px; background: var(--oc-bg); padding: 1px 5px;
119
                           border-radius: 4px; border: 1px solid var(--oc-border); }
120
  .oc-num { font-variant-numeric: tabular-nums; color: var(--oc-muted); white-space: nowrap; }
121
  .oc-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
122
             font-size: 12px; }
123
 
124
  /* pills */
125
  .oc-pill { display: inline-block; padding: 2px 8px; border-radius: 999px;
126
             font-size: 11px; font-weight: 600; white-space: nowrap; border: 1px solid transparent; }
127
  .oc-pill-ok { background: var(--oc-ok-soft); color: var(--oc-ok); border-color: var(--oc-ok); }
128
  .oc-pill-warn { background: var(--oc-warn-soft); color: var(--oc-warn); border-color: var(--oc-warn); }
129
  .oc-pill-danger { background: var(--oc-danger-soft); color: var(--oc-danger); border-color: var(--oc-danger); }
130
  .oc-pill-muted { background: var(--oc-bg); color: var(--oc-muted); border-color: var(--oc-border); }
131
 
132
  .oc-root button.oc-expand { background: none; border: none; cursor: pointer; padding: 0;
133
                              color: var(--oc-accent); font-weight: 600; font-size: 12px;
134
                              font-family: inherit; }
135
 
136
  /* curation editor, opened under the decision it belongs to */
137
  .oc-edit { background: var(--oc-bg); padding: 14px 16px; }
138
  .oc-edit-actions { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 10px; }
139
  .oc-edit-actions label { display: flex; align-items: center; gap: 6px; font-size: 12.5px; }
140
  .oc-edit-row { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap;
141
                 margin-bottom: 10px; }
142
  .oc-hits { max-height: 190px; overflow-y: auto; border: 1px solid var(--oc-border);
143
             border-radius: 7px; background: var(--oc-surface); }
144
  .oc-hit { display: block; width: 100%; text-align: left; background: none; border: none;
145
            border-bottom: 1px solid var(--oc-border); padding: 7px 10px; cursor: pointer;
146
            font-family: inherit; font-size: 12.5px; color: var(--oc-text); }
147
  .oc-hit:last-child { border-bottom: none; }
148
  .oc-hit:hover, .oc-hit[data-picked="1"] { background: var(--oc-accent-soft); }
149
  /* the closest miss is shown for context and is deliberately never pre-selected:
150
     on Aug'26 four new OnePlus phones each "nearly" matched a 2015 OnePlus 2 */
151
  .oc-miss { color: var(--oc-muted); font-size: 12px; }
152
 
153
  /* verbatim original-row detail */
154
  .oc-detail { background: var(--oc-bg); padding: 14px 16px; }
155
  .oc-detail-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
156
                     color: var(--oc-muted); font-weight: 650; margin-bottom: 10px; }
157
  .oc-dl { display: grid; grid-template-columns: 190px 1fr; gap: 6px 16px; margin: 0; }
158
  .oc-dl dt { color: var(--oc-muted); font-size: 11.5px; font-weight: 600; }
159
  .oc-dl dd { margin: 0; white-space: pre-wrap; word-break: break-word;
160
              font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
161
              font-size: 12px; }
162
 
163
  .oc-foot { padding: 11px 14px; border-top: 1px solid var(--oc-border);
164
             display: flex; align-items: center; justify-content: space-between;
165
             gap: 12px; flex-wrap: wrap; color: var(--oc-muted); font-size: 12.5px; }
166
  .oc-pager { display: flex; align-items: center; gap: 8px; }
167
  .oc-empty { padding: 44px 16px; text-align: center; color: var(--oc-muted); }
168
  .oc-spin { padding: 30px; text-align: center; color: var(--oc-muted); }
169
 
170
  @media (prefers-color-scheme: dark) {
171
    .oc-root {
172
      --oc-bg: rgb(15, 23, 42);
173
      --oc-surface: rgb(24, 33, 54);
174
      --oc-border: rgb(51, 65, 85);
175
      --oc-text: rgb(226, 232, 240);
176
      --oc-muted: rgb(148, 163, 184);
177
      --oc-accent: rgb(129, 140, 248);
178
      --oc-accent-soft: rgb(35, 45, 75);
179
      --oc-ok-soft: rgb(20, 45, 32);
180
      --oc-warn-soft: rgb(52, 39, 12);
181
      --oc-danger-soft: rgb(60, 20, 30);
182
    }
183
  }
184
  </style>
185
 
186
  <div class="oc-head">
187
    <div>
188
      <h1 class="oc-title">Offer Circular Review</h1>
189
      <p class="oc-sub">Verbatim OEM circular rows as printed in the source PDF, alongside how
190
         each row was interpreted. Mobiles, tablets, smart watches and TVs are in scope.</p>
191
    </div>
192
    <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
193
      <button class="oc-btn oc-btn-ghost" type="button" data-oc-download>Download source PDF</button>
194
      <button class="oc-btn oc-btn-ghost" type="button" data-oc-export>Export CSV</button>
195
      <button class="oc-btn" type="button" data-oc-reload>Refresh</button>
196
    </div>
197
  </div>
198
 
199
  <div class="oc-filters" style="margin-bottom:16px;">
200
    <div class="oc-field" style="flex:1;min-width:260px;">
201
      <label for="oc-doc">Circular</label>
202
      <select class="oc-input" id="oc-doc" data-oc-doc style="width:100%;">
203
        <option value="">Loading circulars...</option>
204
      </select>
205
    </div>
206
    <div class="oc-field">
207
      <label for="oc-upload-month">Upload for month</label>
208
      <input class="oc-input" id="oc-upload-month" type="date" data-oc-upload-month />
209
    </div>
210
    <div class="oc-field">
211
      <label for="oc-upload-file">New circular PDF</label>
212
      <input class="oc-input" id="oc-upload-file" type="file" accept="application/pdf"
213
             data-oc-upload-file style="padding-top:5px;" />
214
    </div>
215
    <button class="oc-btn" type="button" data-oc-upload>Upload</button>
216
    <span data-oc-upload-msg style="font-size:12px;color:var(--oc-muted);"></span>
37335 amit 217
    <!-- Upload and parsing are different processes: the portal stores the PDF, the
218
         cron job parses it minutes later. Without this the screen looks the same
219
         before and after, so "not parsed yet" is indistinguishable from
220
         "parsed, found nothing". -->
221
    <span data-oc-ingest-state style="flex-basis:100%;"></span>
37331 amit 222
  </div>
223
 
224
  <div class="oc-tiles">
225
    <div class="oc-tile"><div class="oc-tile-label">Rows in circular</div>
226
      <div class="oc-tile-value" data-oc-stat="total">&ndash;</div></div>
227
    <div class="oc-tile"><div class="oc-tile-label">Parsed</div>
228
      <div class="oc-tile-value" data-oc-stat="parsed">&ndash;</div></div>
229
    <div class="oc-tile"><div class="oc-tile-label">Needs review</div>
230
      <div class="oc-tile-value" data-oc-stat="pending">&ndash;</div></div>
231
    <div class="oc-tile"><div class="oc-tile-label">Brands</div>
232
      <div class="oc-tile-value" data-oc-stat="brands">&ndash;</div></div>
233
  </div>
234
 
235
  <!-- Curation queue. Grouped by distinct (OEM, product text) rather than by circular
236
       row, because a saved decision is keyed the same way: answer once and every row
237
       carrying that text is resolved, on this circular and on every later one. -->
238
  <div class="oc-card" style="margin-bottom:16px;">
239
    <div class="oc-card-head">
240
      <span>Resolve products <span class="oc-num" data-oc-resolve-count></span></span>
241
      <span style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
242
        <span class="oc-num" data-oc-resolve-pending></span>
243
        <button class="oc-btn oc-btn-ghost" type="button" data-oc-alias-export
244
                title="Emit every saved decision as SQL so it can be replayed on another environment">Export as SQL</button>
245
        <button class="oc-btn oc-btn-ghost" type="button" data-oc-reingest
246
                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>
247
        <button class="oc-btn" type="button" data-oc-resolve-toggle>Show queue</button>
248
      </span>
249
    </div>
250
    <div data-oc-resolve-wrap style="display:none;">
251
      <div class="oc-scroll" style="max-height:60vh;">
252
        <table class="oc-table">
253
          <thead>
254
            <tr>
255
              <th>Product as printed</th>
256
              <th>OEM</th>
257
              <th>Affects</th>
258
              <th>Closest miss</th>
259
              <th>Decision</th>
260
              <th></th>
261
            </tr>
262
          </thead>
263
          <tbody data-oc-resolve-body>
264
            <tr><td colspan="6" class="oc-spin">Loading decisions&hellip;</td></tr>
265
          </tbody>
266
        </table>
267
      </div>
268
      <div class="oc-foot"><span data-oc-resolve-msg></span></div>
269
    </div>
270
  </div>
271
 
272
  <div class="oc-filters">
273
    <div class="oc-field">
274
      <label for="oc-brand">Brand</label>
275
      <select class="oc-input" id="oc-brand" data-oc-filter="brand">
276
        <option value="">All brands</option>
277
      </select>
278
    </div>
279
    <div class="oc-field">
280
      <label for="oc-month">Circular month</label>
281
      <!-- deliberately blank: a hardcoded month silently returns nothing the moment
282
           the loaded circular is for a different month -->
283
      <input class="oc-input" id="oc-month" type="date" data-oc-filter="periodMonth" />
284
    </div>
285
    <div class="oc-field">
286
      <label for="oc-from">Live from</label>
287
      <input class="oc-input" id="oc-from" type="date" data-oc-filter="fromDate" />
288
    </div>
289
    <div class="oc-field">
290
      <label for="oc-to">Live to</label>
291
      <input class="oc-input" id="oc-to" type="date" data-oc-filter="toDate" />
292
    </div>
293
    <div class="oc-field">
294
      <label for="oc-size">Per page</label>
295
      <select class="oc-input" id="oc-size" data-oc-filter="pageSize" style="min-width:90px;">
296
        <option>25</option><option selected>50</option><option>100</option><option>200</option>
297
      </select>
298
    </div>
299
    <label class="oc-check">
300
      <input type="checkbox" id="oc-unmapped" data-oc-filter="onlyUnmapped" /> Only rows needing review
301
    </label>
302
    <label class="oc-check" title="Mobiles, tablets, smart watches and TVs only. Unchecking also shows laptop, printer and audio rows.">
303
      <input type="checkbox" id="oc-inscope" data-oc-filter="inScopeOnly" checked /> Relevant categories only
304
    </label>
305
    <button class="oc-btn oc-btn-ghost" type="button" data-oc-clear>Clear</button>
306
  </div>
307
 
308
  <div class="oc-card">
309
    <div class="oc-card-head">
310
      <span data-oc-caption>Circular rows</span>
311
      <span class="oc-num" data-oc-range></span>
312
    </div>
313
    <div class="oc-scroll">
314
      <table class="oc-table">
315
        <thead>
316
          <tr>
317
            <th>Page/Row</th>
318
            <th>OEM (as printed)</th>
319
            <th>Brand</th>
320
            <th>Category</th>
321
            <th>Cashback</th>
322
            <th>Offer description</th>
323
            <th>EMI tenure</th>
324
            <th>Eligible products</th>
325
            <th>Validity</th>
326
            <th>Status</th>
327
            <th></th>
328
          </tr>
329
        </thead>
330
        <tbody data-oc-body>
331
          <tr><td colspan="11" class="oc-spin">Loading circular rows&hellip;</td></tr>
332
        </tbody>
333
      </table>
334
    </div>
335
    <div class="oc-foot">
336
      <span data-oc-summary></span>
337
      <span class="oc-pager">
338
        <button class="oc-btn oc-btn-ghost" type="button" data-oc-prev>Previous</button>
339
        <span data-oc-page></span>
340
        <button class="oc-btn oc-btn-ghost" type="button" data-oc-next>Next</button>
341
      </span>
342
    </div>
343
  </div>
344
 
345
</div>
346
 
347
<script type="text/javascript">
348
    // This page can be opened directly, so it cannot rely on admin.vm having
349
    // already set the global "context".
350
    window.ocContext = "${rc.contextPath}";
351
 
352
    // Standalone bootstrap. When this view is injected into admin.vm as a fragment,
353
    // jQuery and offer-circular-review.js are already on the page and this does
354
    // nothing. When the URL is opened directly the fragment arrives alone, with no
355
    // include-scripts.vm, so the screen would sit on "Loading..." forever - load
356
    // what is missing ourselves.
357
    (function () {
358
        var base = window.ocContext || '';
359
        function addScript(src, next) {
360
            var s = document.createElement('script');
361
            s.src = src;
362
            s.onload = next;
363
            s.onerror = function () {
364
                var body = document.querySelector('[data-oc-body]');
365
                if (body) {
366
                    body.innerHTML = '<tr><td colspan="11" class="oc-empty">'
367
                        + 'Could not load ' + src + '</td></tr>';
368
                }
369
            };
370
            document.head.appendChild(s);
371
        }
372
        if (!window.jQuery) {
373
            document.documentElement.setAttribute('data-oc-standalone', '1');
374
            addScript(base + '/resources/js/jquery-3.2.1.min.js', function () {
375
                addScript(base + '/resources/js/offer-circular-review.js?v=${version}');
376
            });
377
        }
378
    })();
379
</script>
380
</section>