Subversion Repositories SmartDukaan

Rev

Rev 37335 | Go to most recent revision | Details | 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>
217
  </div>
218
 
219
  <div class="oc-tiles">
220
    <div class="oc-tile"><div class="oc-tile-label">Rows in circular</div>
221
      <div class="oc-tile-value" data-oc-stat="total">&ndash;</div></div>
222
    <div class="oc-tile"><div class="oc-tile-label">Parsed</div>
223
      <div class="oc-tile-value" data-oc-stat="parsed">&ndash;</div></div>
224
    <div class="oc-tile"><div class="oc-tile-label">Needs review</div>
225
      <div class="oc-tile-value" data-oc-stat="pending">&ndash;</div></div>
226
    <div class="oc-tile"><div class="oc-tile-label">Brands</div>
227
      <div class="oc-tile-value" data-oc-stat="brands">&ndash;</div></div>
228
  </div>
229
 
230
  <!-- Curation queue. Grouped by distinct (OEM, product text) rather than by circular
231
       row, because a saved decision is keyed the same way: answer once and every row
232
       carrying that text is resolved, on this circular and on every later one. -->
233
  <div class="oc-card" style="margin-bottom:16px;">
234
    <div class="oc-card-head">
235
      <span>Resolve products <span class="oc-num" data-oc-resolve-count></span></span>
236
      <span style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
237
        <span class="oc-num" data-oc-resolve-pending></span>
238
        <button class="oc-btn oc-btn-ghost" type="button" data-oc-alias-export
239
                title="Emit every saved decision as SQL so it can be replayed on another environment">Export as SQL</button>
240
        <button class="oc-btn oc-btn-ghost" type="button" data-oc-reingest
241
                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>
242
        <button class="oc-btn" type="button" data-oc-resolve-toggle>Show queue</button>
243
      </span>
244
    </div>
245
    <div data-oc-resolve-wrap style="display:none;">
246
      <div class="oc-scroll" style="max-height:60vh;">
247
        <table class="oc-table">
248
          <thead>
249
            <tr>
250
              <th>Product as printed</th>
251
              <th>OEM</th>
252
              <th>Affects</th>
253
              <th>Closest miss</th>
254
              <th>Decision</th>
255
              <th></th>
256
            </tr>
257
          </thead>
258
          <tbody data-oc-resolve-body>
259
            <tr><td colspan="6" class="oc-spin">Loading decisions&hellip;</td></tr>
260
          </tbody>
261
        </table>
262
      </div>
263
      <div class="oc-foot"><span data-oc-resolve-msg></span></div>
264
    </div>
265
  </div>
266
 
267
  <div class="oc-filters">
268
    <div class="oc-field">
269
      <label for="oc-brand">Brand</label>
270
      <select class="oc-input" id="oc-brand" data-oc-filter="brand">
271
        <option value="">All brands</option>
272
      </select>
273
    </div>
274
    <div class="oc-field">
275
      <label for="oc-month">Circular month</label>
276
      <!-- deliberately blank: a hardcoded month silently returns nothing the moment
277
           the loaded circular is for a different month -->
278
      <input class="oc-input" id="oc-month" type="date" data-oc-filter="periodMonth" />
279
    </div>
280
    <div class="oc-field">
281
      <label for="oc-from">Live from</label>
282
      <input class="oc-input" id="oc-from" type="date" data-oc-filter="fromDate" />
283
    </div>
284
    <div class="oc-field">
285
      <label for="oc-to">Live to</label>
286
      <input class="oc-input" id="oc-to" type="date" data-oc-filter="toDate" />
287
    </div>
288
    <div class="oc-field">
289
      <label for="oc-size">Per page</label>
290
      <select class="oc-input" id="oc-size" data-oc-filter="pageSize" style="min-width:90px;">
291
        <option>25</option><option selected>50</option><option>100</option><option>200</option>
292
      </select>
293
    </div>
294
    <label class="oc-check">
295
      <input type="checkbox" id="oc-unmapped" data-oc-filter="onlyUnmapped" /> Only rows needing review
296
    </label>
297
    <label class="oc-check" title="Mobiles, tablets, smart watches and TVs only. Unchecking also shows laptop, printer and audio rows.">
298
      <input type="checkbox" id="oc-inscope" data-oc-filter="inScopeOnly" checked /> Relevant categories only
299
    </label>
300
    <button class="oc-btn oc-btn-ghost" type="button" data-oc-clear>Clear</button>
301
  </div>
302
 
303
  <div class="oc-card">
304
    <div class="oc-card-head">
305
      <span data-oc-caption>Circular rows</span>
306
      <span class="oc-num" data-oc-range></span>
307
    </div>
308
    <div class="oc-scroll">
309
      <table class="oc-table">
310
        <thead>
311
          <tr>
312
            <th>Page/Row</th>
313
            <th>OEM (as printed)</th>
314
            <th>Brand</th>
315
            <th>Category</th>
316
            <th>Cashback</th>
317
            <th>Offer description</th>
318
            <th>EMI tenure</th>
319
            <th>Eligible products</th>
320
            <th>Validity</th>
321
            <th>Status</th>
322
            <th></th>
323
          </tr>
324
        </thead>
325
        <tbody data-oc-body>
326
          <tr><td colspan="11" class="oc-spin">Loading circular rows&hellip;</td></tr>
327
        </tbody>
328
      </table>
329
    </div>
330
    <div class="oc-foot">
331
      <span data-oc-summary></span>
332
      <span class="oc-pager">
333
        <button class="oc-btn oc-btn-ghost" type="button" data-oc-prev>Previous</button>
334
        <span data-oc-page></span>
335
        <button class="oc-btn oc-btn-ghost" type="button" data-oc-next>Next</button>
336
      </span>
337
    </div>
338
  </div>
339
 
340
</div>
341
 
342
<script type="text/javascript">
343
    // This page can be opened directly, so it cannot rely on admin.vm having
344
    // already set the global "context".
345
    window.ocContext = "${rc.contextPath}";
346
 
347
    // Standalone bootstrap. When this view is injected into admin.vm as a fragment,
348
    // jQuery and offer-circular-review.js are already on the page and this does
349
    // nothing. When the URL is opened directly the fragment arrives alone, with no
350
    // include-scripts.vm, so the screen would sit on "Loading..." forever - load
351
    // what is missing ourselves.
352
    (function () {
353
        var base = window.ocContext || '';
354
        function addScript(src, next) {
355
            var s = document.createElement('script');
356
            s.src = src;
357
            s.onload = next;
358
            s.onerror = function () {
359
                var body = document.querySelector('[data-oc-body]');
360
                if (body) {
361
                    body.innerHTML = '<tr><td colspan="11" class="oc-empty">'
362
                        + 'Could not load ' + src + '</td></tr>';
363
                }
364
            };
365
            document.head.appendChild(s);
366
        }
367
        if (!window.jQuery) {
368
            document.documentElement.setAttribute('data-oc-standalone', '1');
369
            addScript(base + '/resources/js/jquery-3.2.1.min.js', function () {
370
                addScript(base + '/resources/js/offer-circular-review.js?v=${version}');
371
            });
372
        }
373
    })();
374
</script>
375
</section>