| 37526 |
amit |
1 |
<section class="wrapper">
|
|
|
2 |
<div class="ocs-root">
|
|
|
3 |
|
|
|
4 |
<style>
|
|
|
5 |
/* Scoped to .ocs-root so it cannot leak into the admin theme. Colours use rgb()
|
|
|
6 |
and selectors use classes, never id-hashes - Velocity would try to parse a
|
|
|
7 |
leading "#" as a directive. */
|
|
|
8 |
.ocs-root {
|
|
|
9 |
--ocs-surface: rgb(255, 255, 255);
|
|
|
10 |
--ocs-border: rgb(226, 232, 240);
|
|
|
11 |
--ocs-text: rgb(15, 23, 42);
|
|
|
12 |
--ocs-muted: rgb(100, 116, 139);
|
|
|
13 |
--ocs-accent: rgb(79, 70, 229);
|
|
|
14 |
--ocs-accent-soft: rgb(238, 242, 255);
|
|
|
15 |
--ocs-ok: rgb(22, 163, 74);
|
|
|
16 |
--ocs-ok-soft: rgb(240, 253, 244);
|
|
|
17 |
--ocs-warn: rgb(180, 83, 9);
|
|
|
18 |
--ocs-warn-soft: rgb(255, 251, 235);
|
|
|
19 |
--ocs-danger: rgb(190, 18, 60);
|
|
|
20 |
--ocs-danger-soft: rgb(255, 241, 242);
|
|
|
21 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
|
22 |
color: var(--ocs-text);
|
|
|
23 |
font-size: 13px;
|
|
|
24 |
line-height: 1.5;
|
|
|
25 |
padding: 18px 22px 32px;
|
|
|
26 |
box-sizing: border-box;
|
|
|
27 |
}
|
|
|
28 |
.ocs-root *, .ocs-root *::before, .ocs-root *::after { box-sizing: border-box; }
|
|
|
29 |
|
|
|
30 |
.ocs-head { display: flex; align-items: flex-start; justify-content: space-between;
|
|
|
31 |
gap: 16px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
|
32 |
.ocs-title { font-size: 20px; font-weight: 650; letter-spacing: -0.01em; margin: 0; }
|
|
|
33 |
.ocs-sub { color: var(--ocs-muted); margin: 4px 0 0; font-size: 12.5px; max-width: 70ch; }
|
|
|
34 |
|
|
|
35 |
.ocs-note { background: var(--ocs-warn-soft); border: 1px solid rgb(253, 230, 138);
|
|
|
36 |
color: var(--ocs-warn); border-radius: 8px; padding: 9px 12px;
|
|
|
37 |
font-size: 12.5px; margin: 14px 0 18px; }
|
|
|
38 |
|
|
|
39 |
.ocs-tabs { display: flex; gap: 6px; border-bottom: 1px solid var(--ocs-border);
|
|
|
40 |
margin-bottom: 16px; }
|
|
|
41 |
.ocs-tab { border: 0; background: none; padding: 8px 14px; font-size: 13px;
|
|
|
42 |
color: var(--ocs-muted); cursor: pointer; border-bottom: 2px solid transparent; }
|
|
|
43 |
.ocs-tab.is-on { color: var(--ocs-accent); border-bottom-color: var(--ocs-accent);
|
|
|
44 |
font-weight: 600; }
|
|
|
45 |
|
|
|
46 |
.ocs-card { background: var(--ocs-surface); border: 1px solid var(--ocs-border);
|
|
|
47 |
border-radius: 10px; padding: 16px 18px; margin-bottom: 18px; }
|
|
|
48 |
.ocs-card h3 { margin: 0 0 4px; font-size: 14px; font-weight: 650; }
|
|
|
49 |
.ocs-card p.ocs-hint { margin: 0 0 14px; color: var(--ocs-muted); font-size: 12.5px;
|
|
|
50 |
max-width: 78ch; }
|
|
|
51 |
|
|
|
52 |
.ocs-form { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
|
|
|
53 |
.ocs-field { display: flex; flex-direction: column; gap: 4px; }
|
|
|
54 |
.ocs-field label { font-size: 11.5px; color: var(--ocs-muted); text-transform: uppercase;
|
|
|
55 |
letter-spacing: 0.04em; }
|
|
|
56 |
.ocs-field input, .ocs-field select {
|
|
|
57 |
border: 1px solid var(--ocs-border); border-radius: 7px; padding: 6px 9px;
|
|
|
58 |
font-size: 13px; min-width: 170px; background: var(--ocs-surface);
|
|
|
59 |
color: var(--ocs-text); height: 32px; }
|
|
|
60 |
.ocs-field-wide input { min-width: 260px; }
|
|
|
61 |
|
|
|
62 |
.ocs-btn { border: 1px solid var(--ocs-border); background: var(--ocs-surface);
|
|
|
63 |
color: var(--ocs-text); border-radius: 7px; padding: 6px 13px;
|
|
|
64 |
font-size: 12.5px; cursor: pointer; height: 32px; }
|
|
|
65 |
.ocs-btn:hover { border-color: var(--ocs-accent); color: var(--ocs-accent); }
|
|
|
66 |
.ocs-btn-primary { background: var(--ocs-accent); border-color: var(--ocs-accent);
|
|
|
67 |
color: rgb(255, 255, 255); }
|
|
|
68 |
.ocs-btn-primary:hover { color: rgb(255, 255, 255); opacity: 0.92; }
|
|
|
69 |
.ocs-btn-danger { color: var(--ocs-danger); }
|
|
|
70 |
.ocs-btn-danger:hover { border-color: var(--ocs-danger); color: var(--ocs-danger); }
|
|
|
71 |
.ocs-btn-sm { height: 26px; padding: 2px 9px; font-size: 12px; }
|
|
|
72 |
|
|
|
73 |
table.ocs-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
|
|
74 |
table.ocs-table th { text-align: left; font-weight: 600; color: var(--ocs-muted);
|
|
|
75 |
font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.04em;
|
|
|
76 |
padding: 8px 10px; border-bottom: 1px solid var(--ocs-border);
|
|
|
77 |
white-space: nowrap; }
|
|
|
78 |
table.ocs-table td { padding: 8px 10px; border-bottom: 1px solid var(--ocs-border);
|
|
|
79 |
vertical-align: middle; }
|
|
|
80 |
table.ocs-table tr.ocs-off td { background: rgb(250, 250, 251); color: var(--ocs-muted); }
|
|
|
81 |
.ocs-scroll { overflow-x: auto; }
|
|
|
82 |
|
|
|
83 |
.ocs-pill { display: inline-block; border-radius: 999px; padding: 1px 8px;
|
|
|
84 |
font-size: 11.5px; font-weight: 600; }
|
|
|
85 |
.ocs-pill-on { background: var(--ocs-ok-soft); color: var(--ocs-ok); }
|
|
|
86 |
.ocs-pill-off { background: var(--ocs-danger-soft); color: var(--ocs-danger); }
|
|
|
87 |
.ocs-pill-alias { background: var(--ocs-accent-soft); color: var(--ocs-accent); }
|
|
|
88 |
.ocs-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
|
89 |
font-size: 12px; }
|
|
|
90 |
.ocs-muted { color: var(--ocs-muted); }
|
|
|
91 |
.ocs-empty { text-align: center; color: var(--ocs-muted); padding: 22px; }
|
|
|
92 |
|
|
|
93 |
.ocs-msg { border-radius: 8px; padding: 9px 12px; font-size: 12.5px; margin-bottom: 14px;
|
|
|
94 |
display: none; }
|
|
|
95 |
.ocs-msg-ok { background: var(--ocs-ok-soft); color: var(--ocs-ok);
|
|
|
96 |
border: 1px solid rgb(187, 247, 208); }
|
|
|
97 |
.ocs-msg-bad { background: var(--ocs-danger-soft); color: var(--ocs-danger);
|
|
|
98 |
border: 1px solid rgb(254, 205, 211); }
|
|
|
99 |
|
|
|
100 |
.ocs-export { width: 100%; min-height: 220px; font-family: ui-monospace, Menlo, monospace;
|
|
|
101 |
font-size: 12px; border: 1px solid var(--ocs-border); border-radius: 8px;
|
|
|
102 |
padding: 10px; display: none; }
|
|
|
103 |
</style>
|
|
|
104 |
|
|
|
105 |
<div class="ocs-head">
|
|
|
106 |
<div>
|
|
|
107 |
<h2 class="ocs-title">Offer circular — scope config</h2>
|
|
|
108 |
<p class="ocs-sub">Which OEM divisions get ingested from the monthly circular, and
|
|
|
109 |
which alternate labels mean a division that already exists. This is the only
|
|
|
110 |
authority on scope — the parser has no built-in list.</p>
|
|
|
111 |
</div>
|
|
|
112 |
<div>
|
|
|
113 |
<button class="ocs-btn" type="button" data-ocs-export>Export as SQL</button>
|
|
|
114 |
<button class="ocs-btn" type="button" data-ocs-reload>Reload</button>
|
|
|
115 |
</div>
|
|
|
116 |
</div>
|
|
|
117 |
|
|
|
118 |
<div class="ocs-note">
|
|
|
119 |
Changes here take effect on the <strong>next ingest</strong>. Scope is read when a
|
|
|
120 |
circular is parsed, so editing it does not alter a circular that is already published
|
|
|
121 |
— re-ingest that circular from the review screen to apply it.
|
|
|
122 |
</div>
|
|
|
123 |
|
|
|
124 |
<div class="ocs-msg ocs-msg-ok" data-ocs-ok></div>
|
|
|
125 |
<div class="ocs-msg ocs-msg-bad" data-ocs-bad></div>
|
|
|
126 |
|
|
|
127 |
<div class="ocs-tabs">
|
|
|
128 |
<button class="ocs-tab is-on" type="button" data-ocs-tab="divisions">Divisions</button>
|
|
|
129 |
<button class="ocs-tab" type="button" data-ocs-tab="aliases">Label aliases</button>
|
|
|
130 |
</div>
|
|
|
131 |
|
|
|
132 |
<div data-ocs-panel="divisions">
|
|
|
133 |
<div class="ocs-card">
|
|
|
134 |
<h3>Add or edit a division</h3>
|
|
|
135 |
<p class="ocs-hint">The label must match the circular's “OEM Name” cell
|
|
|
136 |
exactly. Saving an existing label edits it. A division needs a category so the
|
|
|
137 |
matcher knows which catalog bucket to search; a brand may be missing (Google has
|
|
|
138 |
no catalog brand row), in which case its products stay unresolved.</p>
|
|
|
139 |
<div class="ocs-form">
|
|
|
140 |
<div class="ocs-field ocs-field-wide">
|
|
|
141 |
<label>OEM label (verbatim)</label>
|
|
|
142 |
<input type="text" data-ocs-d-label placeholder="Samsung Tabs" />
|
|
|
143 |
</div>
|
|
|
144 |
<div class="ocs-field">
|
|
|
145 |
<label>Display name</label>
|
|
|
146 |
<input type="text" data-ocs-d-display placeholder="Samsung Tablets" />
|
|
|
147 |
</div>
|
|
|
148 |
<div class="ocs-field">
|
|
|
149 |
<label>Brand</label>
|
|
|
150 |
<select data-ocs-d-brand><option value="">- none -</option></select>
|
|
|
151 |
</div>
|
|
|
152 |
<div class="ocs-field">
|
|
|
153 |
<label>Category</label>
|
|
|
154 |
<select data-ocs-d-category><option value="">- none -</option></select>
|
|
|
155 |
</div>
|
|
|
156 |
<div class="ocs-field">
|
|
|
157 |
<label>In scope</label>
|
|
|
158 |
<select data-ocs-d-inscope>
|
|
|
159 |
<option value="true">Yes - ingest it</option>
|
|
|
160 |
<option value="false">No - drop it</option>
|
|
|
161 |
</select>
|
|
|
162 |
</div>
|
|
|
163 |
<div class="ocs-field ocs-field-wide">
|
|
|
164 |
<label>Reason (required when out of scope)</label>
|
|
|
165 |
<input type="text" data-ocs-d-reason placeholder="Laptops - out of scope" />
|
|
|
166 |
</div>
|
|
|
167 |
<button class="ocs-btn ocs-btn-primary" type="button" data-ocs-d-save>Save division</button>
|
|
|
168 |
<button class="ocs-btn" type="button" data-ocs-d-clear>Clear</button>
|
|
|
169 |
</div>
|
|
|
170 |
</div>
|
|
|
171 |
|
|
|
172 |
<div class="ocs-card">
|
|
|
173 |
<h3>Divisions</h3>
|
|
|
174 |
<p class="ocs-hint">Taking a division out of scope never deletes anything —
|
|
|
175 |
offers already ingested keep their history. It changes what the next ingest keeps.</p>
|
|
|
176 |
<div class="ocs-scroll">
|
|
|
177 |
<table class="ocs-table">
|
|
|
178 |
<thead>
|
|
|
179 |
<tr>
|
|
|
180 |
<th>OEM label</th><th>Display name</th><th>Brand</th><th>Category</th>
|
|
|
181 |
<th>Scope</th><th>Reason</th><th>Offers</th><th>Aliases</th><th></th>
|
|
|
182 |
</tr>
|
|
|
183 |
</thead>
|
|
|
184 |
<tbody data-ocs-divisions>
|
|
|
185 |
<tr><td colspan="9" class="ocs-empty">Loading…</td></tr>
|
|
|
186 |
</tbody>
|
|
|
187 |
</table>
|
|
|
188 |
</div>
|
|
|
189 |
</div>
|
|
|
190 |
</div>
|
|
|
191 |
|
|
|
192 |
<div data-ocs-panel="aliases" style="display:none">
|
|
|
193 |
<div class="ocs-card">
|
|
|
194 |
<h3>Add a label alias</h3>
|
|
|
195 |
<p class="ocs-hint">Use this when the circular prints a different spelling of a
|
|
|
196 |
division that already exists — “XIAOMI CE” for
|
|
|
197 |
“Xiaomi CE”, say. An alias only says two labels mean the same
|
|
|
198 |
division; it carries no brand or category of its own, so the scope decision stays
|
|
|
199 |
in one place. If the label needs a <em>different</em> category, it is a division,
|
|
|
200 |
not an alias.</p>
|
|
|
201 |
<div class="ocs-form">
|
|
|
202 |
<div class="ocs-field ocs-field-wide">
|
|
|
203 |
<label>Label as printed</label>
|
|
|
204 |
<input type="text" data-ocs-a-label placeholder="XIAOMI CE" />
|
|
|
205 |
</div>
|
|
|
206 |
<div class="ocs-field">
|
|
|
207 |
<label>Means this division</label>
|
|
|
208 |
<select data-ocs-a-division><option value="">- pick one -</option></select>
|
|
|
209 |
</div>
|
|
|
210 |
<div class="ocs-field ocs-field-wide">
|
|
|
211 |
<label>Note (optional)</label>
|
|
|
212 |
<input type="text" data-ocs-a-note placeholder="Sep'26 circular prints it upper case" />
|
|
|
213 |
</div>
|
|
|
214 |
<button class="ocs-btn ocs-btn-primary" type="button" data-ocs-a-save>Save alias</button>
|
|
|
215 |
</div>
|
|
|
216 |
</div>
|
|
|
217 |
|
|
|
218 |
<div class="ocs-card">
|
|
|
219 |
<h3>Label aliases</h3>
|
|
|
220 |
<div class="ocs-scroll">
|
|
|
221 |
<table class="ocs-table">
|
|
|
222 |
<thead>
|
|
|
223 |
<tr><th>Label as printed</th><th>Means</th><th>Note</th><th>Added by</th><th></th></tr>
|
|
|
224 |
</thead>
|
|
|
225 |
<tbody data-ocs-aliases>
|
|
|
226 |
<tr><td colspan="5" class="ocs-empty">Loading…</td></tr>
|
|
|
227 |
</tbody>
|
|
|
228 |
</table>
|
|
|
229 |
</div>
|
|
|
230 |
</div>
|
|
|
231 |
</div>
|
|
|
232 |
|
|
|
233 |
<textarea class="ocs-export" data-ocs-export-box readonly></textarea>
|
|
|
234 |
|
|
|
235 |
</div>
|
|
|
236 |
|
|
|
237 |
<script type="text/javascript">
|
|
|
238 |
// This page can be opened directly, so it cannot rely on admin.vm having already
|
|
|
239 |
// set the global "context".
|
|
|
240 |
window.ocsContext = "${rc.contextPath}";
|
|
|
241 |
|
|
|
242 |
// Standalone bootstrap - same reason as the review screen. Injected as a fragment
|
|
|
243 |
// jQuery is already present and this does nothing; opened by URL the fragment
|
|
|
244 |
// arrives alone with no include-scripts.vm, and the screen would never initialise.
|
|
|
245 |
(function () {
|
|
|
246 |
var base = window.ocsContext || '';
|
|
|
247 |
function addScript(src, next) {
|
|
|
248 |
var s = document.createElement('script');
|
|
|
249 |
s.src = src;
|
|
|
250 |
s.onload = next;
|
|
|
251 |
document.head.appendChild(s);
|
|
|
252 |
}
|
|
|
253 |
// The script registers everything inside $(function(){...}), which runs on the
|
|
|
254 |
// NEXT tick - so at this script's onload the init function does not exist yet.
|
|
|
255 |
// Calling it here directly is a silent no-op that leaves the screen on "Loading".
|
|
|
256 |
// Defer through jQuery's ready queue instead: our callback is registered after
|
|
|
257 |
// the file's own, so it runs after it.
|
|
|
258 |
function start() {
|
|
|
259 |
if (!window.jQuery) { return; }
|
|
|
260 |
window.jQuery(function () {
|
|
|
261 |
if (window.initOfferCircularScope) { window.initOfferCircularScope(); }
|
|
|
262 |
});
|
|
|
263 |
}
|
|
|
264 |
if (!window.jQuery) {
|
|
|
265 |
addScript(base + '/resources/js/jquery-3.2.1.min.js', function () {
|
|
|
266 |
addScript(base + '/resources/js/offer-circular-scope.js?v=${version}', start);
|
|
|
267 |
});
|
|
|
268 |
} else if (!window.initOfferCircularScope) {
|
|
|
269 |
addScript(base + '/resources/js/offer-circular-scope.js?v=${version}', start);
|
|
|
270 |
} else {
|
|
|
271 |
start();
|
|
|
272 |
}
|
|
|
273 |
})();
|
|
|
274 |
</script>
|
|
|
275 |
</section>
|