Diff -- /var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/TEMPLATES/webstls_admin_promotions.html

O Operator
Diff

/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/TEMPLATES/webstls_admin_promotions.html

added on local at 2026-07-11 18:38:19

Added
+467
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to cb9f91bb303a
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1<!-- =====================================================================
2 Admin Promotions -- platform-wide. Same shape as seller promotions
3 but every row applies across every storefront (typically targeted
4 at new-customer acquisition).
5===================================================================== -->
6
7<div class="page-head">
8 <div>
9 <span class="page-eyebrow"><span class="dot"></span> Admin &middot; Catalogue</span>
10 <h1 class="page-title">Pricing &amp; Promotions</h1>
11 <p class="page-subtitle">Coupons, sales, and discounts that apply across every storefront on WebSTLs. Use for new-customer acquisition, holiday pushes, and platform-wide marketing.</p>
12 </div>
13 <div class="page-actions">
14 [if:$is_list][if:$not_tutorial_mode]<a href="/admin_promotions.cgi?tutorial=1" class="btn btn-secondary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>Sample Data View</a>[/if][/if]
15 [if:$is_list][if:$tutorial_mode]<a href="/admin_promotions.cgi" class="btn btn-secondary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>Exit sample view</a>[/if][/if]
16 [if:$is_list]<a href="/admin_promotions.cgi?new=1" class="btn btn-primary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>New platform promotion</a>[/if]
17 [if:$is_new]<a href="/admin_promotions.cgi" class="btn btn-secondary">Cancel</a>[/if]
18 [if:$is_edit]<a href="/admin_promotions.cgi" class="btn btn-secondary">Back to list</a>[/if]
19 </div>
20</div>
21
22<!-- Shared tab nav with admin_packages.cgi. Same .pp-tab classes; if
23 the admin lands here directly we redeclare the styles so the
24 two URLs can be visited independently without depending on the
25 packages page's <style> block. -->
26<style>
27 .pp-tabs { display:flex; gap:4px; padding:4px; background:var(--col-surface-2); border:1px solid var(--col-border); border-radius:10px; margin-bottom:22px; width:fit-content; }
28 .pp-tab { padding:8px 18px; font-size:13px; font-weight:600; color:var(--col-text-2); border-radius:7px; text-decoration:none; transition:background 0.12s, color 0.12s; white-space:nowrap; }
29 .pp-tab:hover { color:var(--col-text); }
30 .pp-tab.is-active { background:var(--col-surface-3); color:#fff; box-shadow:inset 0 0 0 1px var(--col-border-2); }
31</style>
32<nav class="pp-tabs">
33 <a class="pp-tab" href="/admin_packages.cgi">Packages</a>
34 <a class="pp-tab is-active" href="/admin_promotions.cgi">Promotions</a>
35</nav>
36
37[if:$tutorial_mode][if:$is_list]
38<div class="tutorial-banner">
39 <div class="tutorial-banner-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
40 <div class="tutorial-banner-body">
41 <div class="tutorial-banner-title">Tutorial &middot; Sample data only</div>
42 <div class="tutorial-banner-text">Three example platform promotions are shown: a new-customer coupon (active, no time limit), a summer sale (scheduled), and a Black Friday flash sale (scheduled with a 5000-redemption cap).</div>
43 </div>
44 <a href="/admin_promotions.cgi" class="btn btn-secondary btn-sm">Exit sample view</a>
45</div>
46
47<div class="tutorial-intro">
48 <div class="tutorial-intro-eyebrow">A walkthrough of Platform Promotions</div>
49 <h2 class="tutorial-intro-title">Drive buyer acquisition across every storefront.</h2>
50 <p class="tutorial-intro-text">Platform promotions live one level above seller promos &mdash; they apply to <strong>every storefront</strong> on WebSTLs. Use them for new-customer offers (e.g. "20% off your first order anywhere on the platform"), site-wide seasonal pushes, or limited-time acquisition events. The same options are available as seller promos: kind, discount type, time window, redemption cap, per-user limit, code or auto-apply, and a shareable <code>/promo/&lt;slug&gt;</code> link.</p>
51 <p class="tutorial-intro-text">Toggle <strong>new customers only</strong> to restrict a promo to buyers with zero prior orders &mdash; this is how to safely run aggressive first-order discounts without cannibalizing repeat revenue. Each redemption writes a row to <code>promotion_redemptions</code> tagged with the buyer email, so the per-promo discount-given column tracks the marketing cost across the platform.</p>
52 <p class="tutorial-intro-text tutorial-intro-foot">Click <a href="/admin_promotions.cgi" class="text-brand fw-700">Exit sample view</a> to see real platform promotions.</p>
53</div>
54[/if][/if]
55
56[if:$has_flash]
57<div class="pr-flash $flash_kind">$flash_msg</div>
58[/if]
59
60[if:$schema_missing]
61<div class="an-banner" style="background:rgba(245,158,11,0.10);border-color:rgba(245,158,11,0.30)">
62 <strong>Promotions schema not yet installed.</strong> Run the latest <code>db_schema.sql</code> against the database.
63</div>
64[/if]
65
66[if:$is_list][if:$schema_ready]
67<div class="pr-kpis">
68 <div class="pr-kpi is-active">
69 <div class="lbl">Active</div>
70 <div class="val">$cnt_active</div>
71 </div>
72 <div class="pr-kpi is-scheduled">
73 <div class="lbl">Scheduled</div>
74 <div class="val">$cnt_scheduled</div>
75 </div>
76 <div class="pr-kpi is-paused">
77 <div class="lbl">Paused</div>
78 <div class="val">$cnt_paused</div>
79 </div>
80 <div class="pr-kpi is-expired">
81 <div class="lbl">Expired</div>
82 <div class="val">$cnt_expired</div>
83 </div>
84 <div class="pr-kpi">
85 <div class="lbl">Total redemptions</div>
86 <div class="val">$sum_redemptions</div>
87 </div>
88 <div class="pr-kpi">
89 <div class="lbl">Discount given</div>
90 <div class="val">$sum_discount_display</div>
91 </div>
92</div>
93
94[if:$has_rows]
95<table class="pr-table">
96 <thead>
97 <tr>
98 <th>Promotion</th>
99 <th>Kind</th>
100 <th>Discount</th>
101 <th>Window</th>
102 <th>Usage</th>
103 <th class="num">Discount given</th>
104 <th>Status</th>
105 <th>Actions</th>
106 </tr>
107 </thead>
108 <tbody>
109 [loop:@rows]
110 <tr>
111 <td>
112 <div class="name">$loop1.name</div>
113 <div class="meta">
114 [if:$loop1.code_present]Code: <code>$loop1.code</code>[/if]
115 [if:$loop1.slug_present][if:$loop1.code_present] &middot; [/if]Link: <code>/promo/$loop1.public_slug</code>[/if]
116 [if:$loop1.new_customers_only] &middot; <span style="color:#86efac">New customers only</span>[/if]
117 </div>
118 </td>
119 <td>
120 [if:$loop1.is_coupon]<span class="pill kind">Coupon</span>[/if]
121 [if:$loop1.is_sale]<span class="pill kind sale">Sale</span>[/if]
122 [if:$loop1.is_bundle]<span class="pill kind bundle">Bundle</span>[/if]
123 [if:$loop1.is_flash]<span class="pill kind flash">Flash sale</span>[/if]
124 </td>
125 <td>$loop1.discount_label</td>
126 <td>
127 [if:$loop1.has_window]
128 <div class="meta" style="font-size:12px;color:var(--col-text-2)">
129 <span class="ts" data-ts="$loop1.starts_at_epoch" data-fmt="datetime">$loop1.starts_at</span> <br>&rarr; <span class="ts" data-ts="$loop1.ends_at_epoch" data-fmt="datetime">$loop1.ends_at</span>
130 </div>
131 [/if]
132 [if:!$loop1.has_window]<span class="text-xs text-dim">No time limit</span>[/if]
133 </td>
134 <td>$loop1.cap_label</td>
135 <td class="num">$loop1.discount_given_display</td>
136 <td>
137 [if:$loop1.is_active]<span class="pill active">Active</span>[/if]
138 [if:$loop1.is_paused]<span class="pill paused">Paused</span>[/if]
139 [if:$loop1.is_scheduled]<span class="pill scheduled">Scheduled</span>[/if]
140 [if:$loop1.is_expired]<span class="pill expired">Expired</span>[/if]
141 [if:$loop1.is_draft]<span class="pill draft">Draft</span>[/if]
142 </td>
143 <td class="actions">
144 <a href="$loop1.edit_url">Edit</a>
145 [if:$loop1.is_active]
146 <span class="sep">&middot;</span>
147 <form method="POST" action="/admin_promotion_action.cgi"
148 data-confirm-title="Pause this platform promotion?"
149 data-confirm-message="It will stop applying everywhere immediately. You can reactivate it later."
150 data-confirm-label="Pause promotion"
151 data-confirm-style="warning">
152 <input type="hidden" name="act" value="pause">
153 <input type="hidden" name="id" value="$loop1.id">
154 <button type="submit">Pause</button>
155 </form>
156 [/if]
157 [if:$loop1.is_scheduled]
158 <span class="sep">&middot;</span>
159 <form method="POST" action="/admin_promotion_action.cgi"
160 data-confirm-title="Pause this scheduled promotion?"
161 data-confirm-message="It will not start at the scheduled time until you reactivate it."
162 data-confirm-label="Pause promotion"
163 data-confirm-style="warning">
164 <input type="hidden" name="act" value="pause">
165 <input type="hidden" name="id" value="$loop1.id">
166 <button type="submit">Pause</button>
167 </form>
168 [/if]
169 [if:$loop1.is_expired]
170 <span class="sep">&middot;</span>
171 <form method="POST" action="/admin_promotion_action.cgi"
172 data-confirm-title="Pause this expired promotion?"
173 data-confirm-message="It is already past its end date. Pausing locks it so it cannot be reactivated without an Edit."
174 data-confirm-label="Pause promotion"
175 data-confirm-style="warning">
176 <input type="hidden" name="act" value="pause">
177 <input type="hidden" name="id" value="$loop1.id">
178 <button type="submit">Pause</button>
179 </form>
180 [/if]
181 [if:$loop1.is_paused]
182 <span class="sep">&middot;</span>
183 <form method="POST" action="/admin_promotion_action.cgi">
184 <input type="hidden" name="act" value="activate">
185 <input type="hidden" name="id" value="$loop1.id">
186 <button type="submit">Activate</button>
187 </form>
188 [/if]
189 [if:$loop1.is_draft]
190 <span class="sep">&middot;</span>
191 <form method="POST" action="/admin_promotion_action.cgi">
192 <input type="hidden" name="act" value="activate">
193 <input type="hidden" name="id" value="$loop1.id">
194 <button type="submit">Activate</button>
195 </form>
196 [/if]
197 <span class="sep">&middot;</span>
198 <form method="POST" action="/admin_promotion_action.cgi"
199 data-confirm-title="Delete this promotion permanently?"
200 data-confirm-message="Redemption history is also removed. This cannot be undone."
201 data-confirm-label="Delete promotion"
202 data-confirm-style="danger">
203 <input type="hidden" name="act" value="delete">
204 <input type="hidden" name="id" value="$loop1.id">
205 <button type="submit" style="color:#fca5a5">Delete</button>
206 </form>
207 </td>
208 </tr>
209 [/loop]
210 </tbody>
211</table>
212[/if]
213
214[if:!$has_rows]
215<div style="text-align:center;padding:60px 24px;background:var(--col-surface-1);border:1px solid var(--col-border);border-radius:12px">
216 <div class="text-sm fw-600" style="color:#fff;margin-bottom:6px">No platform promotions yet</div>
217 <div class="text-xs text-dim" style="margin-bottom:14px;max-width:380px;margin-left:auto;margin-right:auto">Create site-wide offers to bring new customers onto the platform.</div>
218 <a href="/admin_promotions.cgi?new=1" class="btn btn-primary btn-sm">Create your first platform promotion</a>
219</div>
220[/if]
221[/if][/if]
222
223[if:$is_new][if:$schema_ready]
224<form class="pr-form" method="POST" action="/admin_promotion_action.cgi">
225 <input type="hidden" name="act" value="create">
226 <div class="pr-form-grid">
227 <div class="pr-field full">
228 <label>Name</label>
229 <input type="text" name="name" required maxlength="160" placeholder="e.g. New customer 20% off first order">
230 </div>
231
232 <div class="pr-field full">
233 <label>Description</label>
234 <textarea name="description" maxlength="500"></textarea>
235 </div>
236
237 <div class="pr-field full">
238 <label>Kind</label>
239 <div class="pr-radio-row">
240 <label class="pr-radio is-on"><input type="radio" name="kind" value="coupon" checked> Coupon</label>
241 <label class="pr-radio"><input type="radio" name="kind" value="sale"> Sale</label>
242 <label class="pr-radio"><input type="radio" name="kind" value="flash_sale"> Flash sale</label>
243 </div>
244 </div>
245
246 <div class="pr-field full">
247 <label>Applies to plans</label>
248 <div class="pr-radio-row" data-applies-radio>
249 <label class="pr-radio is-on"><input type="radio" name="plan_scope" value="all" checked data-plan-scope> All paid plans</label>
250 <label class="pr-radio"><input type="radio" name="plan_scope" value="specific" data-plan-scope> Specific plan(s)</label>
251 </div>
252 <div class="hint" style="margin-top:6px">Platform promos always discount our subscription plans &mdash; never a seller's product catalog. Each seller sets their own product discounts in Promotions.</div>
253 [if:$has_plan_options]
254 <div data-plan-picker style="display:none;margin-top:10px;padding:10px 12px;border:1px solid var(--col-border);border-radius:8px;background:rgba(255,255,255,0.02)">
255 <div class="hint" style="margin:0 0 8px">Check every plan this code should be redeemable on. Leave all unchecked to fall back to "All paid plans".</div>
256 [loop:@plan_options]
257 <label class="pr-radio" style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
258 <input type="checkbox" name="plan_ids" value="[$loop1.id]"> <strong>[$loop1.display_name]</strong> &mdash; [$loop1.price_display] / mo
259 </label>
260 [/loop]
261 </div>
262 [/if]
263 </div>
264
265 <div class="pr-field">
266 <label>Discount type</label>
267 <select name="discount_type">
268 <option value="percent">Percent off</option>
269 <option value="fixed_amount">Fixed amount off</option>
270 </select>
271 </div>
272
273 <div class="pr-field">
274 <label>Discount value</label>
275 <div style="display:flex;gap:8px;align-items:center">
276 <input type="number" name="discount_value_pct" min="0" max="100" step="1" placeholder="0" style="width:80px">
277 <span class="hint">% off</span>
278 <span class="hint">or</span>
279 <input type="number" name="discount_value_amt" min="0" step="0.01" placeholder="0.00" style="width:100px">
280 <span class="hint">\$ off</span>
281 </div>
282 </div>
283
284 <div class="pr-field">
285 <label>Coupon code</label>
286 <input type="text" name="code" maxlength="64" placeholder="NEWBIE20" style="text-transform:uppercase">
287 </div>
288
289 <div class="pr-field">
290 <label>Public slug</label>
291 <input type="text" name="public_slug" maxlength="64" placeholder="newbie20" style="text-transform:lowercase">
292 </div>
293
294 <div class="pr-field">
295 <label>Starts at</label>
296 <input type="datetime-local" name="starts_at_local">
297 </div>
298
299 <div class="pr-field">
300 <label>Ends at</label>
301 <input type="datetime-local" name="ends_at_local">
302 </div>
303
304 <div class="pr-field">
305 <label>Max total redemptions</label>
306 <input type="number" name="max_redemptions" min="0" placeholder="Unlimited">
307 </div>
308
309 <div class="pr-field">
310 <label>Max per buyer</label>
311 <input type="number" name="max_per_user" min="1" value="1">
312 </div>
313
314 <div class="pr-field">
315 <label>Minimum cart total</label>
316 <input type="number" name="minimum_order" min="0" step="0.01" placeholder="0.00">
317 </div>
318
319 <div class="pr-field">
320 <label>New customers only</label>
321 <label class="pr-radio" style="display:inline-flex"><input type="checkbox" name="new_customers_only" value="1"> Only buyers with zero prior orders</label>
322 </div>
323 </div>
324
325 <div class="pr-actions">
326 <button type="submit" name="publish_now" value="1" class="btn btn-primary">Create &amp; activate</button>
327 <button type="submit" class="btn btn-secondary">Save as draft</button>
328 </div>
329</form>
330[/if][/if]
331
332[if:$is_edit][if:$schema_ready][if:$has_edit]
333<form class="pr-form" method="POST" action="/admin_promotion_action.cgi">
334 <input type="hidden" name="act" value="update">
335 <input type="hidden" name="id" value="$edit.id">
336 <div class="pr-form-grid">
337 <div class="pr-field full">
338 <label>Name</label>
339 <input type="text" name="name" required maxlength="160" value="$edit.name">
340 </div>
341
342 <div class="pr-field full">
343 <label>Description</label>
344 <textarea name="description" maxlength="500">$edit.description</textarea>
345 </div>
346
347 <div class="pr-field full">
348 <label>Kind</label>
349 <div class="pr-radio-row">
350 <label class="pr-radio[if:$edit.is_coupon] is-on[/if]"><input type="radio" name="kind" value="coupon"[if:$edit.is_coupon] checked[/if]> Coupon</label>
351 <label class="pr-radio[if:$edit.is_sale] is-on[/if]"><input type="radio" name="kind" value="sale"[if:$edit.is_sale] checked[/if]> Sale</label>
352 <label class="pr-radio[if:$edit.is_flash] is-on[/if]"><input type="radio" name="kind" value="flash_sale"[if:$edit.is_flash] checked[/if]> Flash sale</label>
353 </div>
354 </div>
355
356 <div class="pr-field full">
357 <label>Applies to plans</label>
358 <div class="pr-radio-row" data-applies-radio>
359 <label class="pr-radio[if:$edit.applies_all_plans] is-on[/if]"><input type="radio" name="plan_scope" value="all"[if:$edit.applies_all_plans] checked[/if] data-plan-scope> All paid plans</label>
360 <label class="pr-radio[if:$edit.applies_specific] is-on[/if]"><input type="radio" name="plan_scope" value="specific"[if:$edit.applies_specific] checked[/if] data-plan-scope> Specific plan(s)</label>
361 </div>
362 <div class="hint" style="margin-top:6px">Platform promos always discount our subscription plans &mdash; never a seller's product catalog. Each seller sets their own product discounts in Promotions.</div>
363 [if:$has_plan_options]
364 <div data-plan-picker style="margin-top:10px;padding:10px 12px;border:1px solid var(--col-border);border-radius:8px;background:rgba(255,255,255,0.02)">
365 <div class="hint" style="margin:0 0 8px">Check every plan this code should be redeemable on. Leave all unchecked to fall back to "All paid plans".</div>
366 [loop:@plan_options]
367 <label class="pr-radio" style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
368 <input type="checkbox" name="plan_ids" value="[$loop1.id]" [$loop1.checked]> <strong>[$loop1.display_name]</strong> &mdash; [$loop1.price_display] / mo
369 </label>
370 [/loop]
371 </div>
372 [/if]
373 </div>
374
375 <div class="pr-field">
376 <label>Discount type</label>
377 <select name="discount_type">
378 <option value="percent"[if:$edit.is_dtype_pct] selected[/if]>Percent off</option>
379 <option value="fixed_amount"[if:$edit.is_dtype_amt] selected[/if]>Fixed amount off</option>
380 </select>
381 </div>
382
383 <div class="pr-field">
384 <label>Discount value</label>
385 <div style="display:flex;gap:8px;align-items:center">
386 <input type="number" name="discount_value_pct" min="0" max="100" step="1" value="$edit.discount_value_pct" style="width:80px">
387 <span class="hint">% off</span>
388 <span class="hint">or</span>
389 <input type="number" name="discount_value_amt" min="0" step="0.01" value="$edit.discount_value_amt" style="width:100px">
390 <span class="hint">\$ off</span>
391 </div>
392 </div>
393
394 <div class="pr-field">
395 <label>Coupon code</label>
396 <input type="text" name="code" maxlength="64" value="$edit.code" style="text-transform:uppercase">
397 </div>
398
399 <div class="pr-field">
400 <label>Public slug</label>
401 <input type="text" name="public_slug" maxlength="64" value="$edit.public_slug" style="text-transform:lowercase">
402 </div>
403
404 <div class="pr-field">
405 <label>Starts at</label>
406 <input type="datetime-local" name="starts_at_local" value="$edit.starts_at_local">
407 </div>
408
409 <div class="pr-field">
410 <label>Ends at</label>
411 <input type="datetime-local" name="ends_at_local" value="$edit.ends_at_local">
412 </div>
413
414 <div class="pr-field">
415 <label>Max total redemptions</label>
416 <input type="number" name="max_redemptions" min="0" value="$edit.max_redemptions" placeholder="Unlimited">
417 </div>
418
419 <div class="pr-field">
420 <label>Max per buyer</label>
421 <input type="number" name="max_per_user" min="1" value="$edit.max_per_user">
422 </div>
423
424 <div class="pr-field">
425 <label>Minimum cart total</label>
426 <input type="number" name="minimum_order" min="0" step="0.01" value="$edit.minimum_order">
427 </div>
428
429 <div class="pr-field">
430 <label>New customers only</label>
431 <label class="pr-radio" style="display:inline-flex"><input type="checkbox" name="new_customers_only" value="1"[if:$edit.new_customers_only] checked[/if]> Only buyers with zero prior orders</label>
432 </div>
433 </div>
434
435 <div class="pr-actions">
436 <button type="submit" class="btn btn-primary">Save changes</button>
437 <a href="/admin_promotions.cgi" class="btn btn-secondary">Cancel</a>
438 </div>
439</form>
440[/if][/if][/if]
441
442<script>
443 /* Show/hide the per-plan checklist based on the "Applies to plans"
444 radio. Operates per-form so the create + edit forms both work
445 without colliding. The "Specific plan(s)" radio reveals the
446 [data-plan-picker] sibling block in the same .pr-field. */
447 (function () {
448 document.querySelectorAll('form').forEach(function (form) {
449 var radios = form.querySelectorAll('[data-plan-scope]');
450 var picker = form.querySelector('[data-plan-picker]');
451 if (!radios.length || !picker) return;
452 function sync() {
453 var sel = form.querySelector('[data-plan-scope]:checked');
454 picker.style.display = (sel && sel.value === 'specific') ? '' : 'none';
455 // If switching back to "all", clear any checkbox picks so they
456 // don't survive the save under a contradictory plan_scope.
457 if (!sel || sel.value !== 'specific') {
458 picker.querySelectorAll('input[type=checkbox]').forEach(function (cb) { cb.checked = false; });
459 }
460 }
461 radios.forEach(function (r) { r.addEventListener('change', sync); });
462 sync();
463 });
464 })();
465</script>
466
467<div class="adm-foot-spacer" data-end="admin-promotions">&nbsp;</div>