Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/TEMPLATES/cf_admin_promotions.html
Diff
/var/www/vhosts/3dshawn.com/crm.3dshawn.com/TEMPLATES/cf_admin_promotions.html
added on local at 2026-07-11 18:31:56
Added
+0
lines
Removed
-0
lines
Context
470
unchanged
Blobs
from 3ad49d98eb51
to 3ad49d98eb51
to 3ad49d98eb51
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.| 1 | 1 | <!-- ===================================================================== |
| 2 | 2 | Admin Promotions -- platform-wide. ContactForge subscription |
| 3 | 3 | discounts (coupons, sales, flash sales) covering every customer. |
| 4 | 4 | ===================================================================== --> |
| 5 | 5 | |
| 6 | 6 | <div class="page-head"> |
| 7 | 7 | <div> |
| 8 | 8 | <span class="page-eyebrow"><span class="dot"></span> Admin · Catalogue</span> |
| 9 | 9 | <h1 class="page-title">Pricing & Promotions</h1> |
| 10 | 10 | <p class="page-subtitle">Coupons, sales, and discounts that apply across every ContactForge subscription. Use for new-customer acquisition, seasonal pushes, and limited-time marketing events.</p> |
| 11 | 11 | </div> |
| 12 | 12 | <div class="page-actions"> |
| 13 | 13 | [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] |
| 14 | 14 | [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] |
| 15 | 15 | [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] |
| 16 | 16 | [if:$is_new]<a href="/admin_promotions.cgi" class="btn btn-secondary">Cancel</a>[/if] |
| 17 | 17 | [if:$is_edit]<a href="/admin_promotions.cgi" class="btn btn-secondary">Back to list</a>[/if] |
| 18 | 18 | </div> |
| 19 | 19 | </div> |
| 20 | 20 | |
| 21 | 21 | <style> |
| 22 | 22 | .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; } |
| 23 | 23 | .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; } |
| 24 | 24 | .pp-tab:hover { color:var(--col-text); } |
| 25 | 25 | .pp-tab.is-active { background:var(--col-surface-3); color:#fff; box-shadow:inset 0 0 0 1px var(--col-border-2); } |
| 26 | 26 | .pr-flash { padding:10px 14px; border-radius:8px; margin-bottom:14px; font-size:13px; } |
| 27 | 27 | .pr-flash.ok { background:rgba(34,197,94,0.10); color:#86efac; border:1px solid rgba(34,197,94,0.30); } |
| 28 | 28 | .pr-flash.err { background:rgba(239,68,68,0.10); color:#fca5a5; border:1px solid rgba(239,68,68,0.30); } |
| 29 | 29 | .pr-kpis { display:grid; grid-template-columns:repeat(6,1fr); gap:12px; margin-bottom:22px; } |
| 30 | 30 | .pr-kpi { background:var(--col-surface-1); border:1px solid var(--col-border); border-radius:10px; padding:14px 16px; } |
| 31 | 31 | .pr-kpi .lbl { font-size:11px; text-transform:uppercase; letter-spacing:0.08em; color:var(--col-text-2); font-weight:600; } |
| 32 | 32 | .pr-kpi .val { font-size:22px; font-weight:700; color:#fff; margin-top:4px; } |
| 33 | 33 | .pr-kpi.is-active .val { color:#86efac; } |
| 34 | 34 | .pr-kpi.is-scheduled .val { color:#7dd3fc; } |
| 35 | 35 | .pr-kpi.is-paused .val { color:#fcd34d; } |
| 36 | 36 | .pr-kpi.is-expired .val { color:#fca5a5; } |
| 37 | 37 | .pr-table { width:100%; border-collapse:collapse; background:var(--col-surface-1); border:1px solid var(--col-border); border-radius:10px; overflow:hidden; } |
| 38 | 38 | .pr-table th, .pr-table td { text-align:left; padding:12px 14px; border-bottom:1px solid var(--col-border); font-size:13px; vertical-align:top; } |
| 39 | 39 | .pr-table th { background:var(--col-surface-2); color:var(--col-text-2); font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:0.06em; } |
| 40 | 40 | .pr-table th.num, .pr-table td.num { text-align:right; } |
| 41 | 41 | .pr-table tr:last-child td { border-bottom:none; } |
| 42 | 42 | .pr-table .name { color:#fff; font-weight:600; } |
| 43 | 43 | .pr-table .meta { color:var(--col-text-2); font-size:12px; margin-top:3px; } |
| 44 | 44 | .pr-table .actions { white-space:nowrap; } |
| 45 | 45 | .pr-table .actions a, .pr-table .actions button { background:none; border:none; color:var(--col-brand); font-size:13px; padding:0; cursor:pointer; } |
| 46 | 46 | .pr-table .actions a:hover, .pr-table .actions button:hover { text-decoration:underline; } |
| 47 | 47 | .pr-table .actions form { display:inline; } |
| 48 | 48 | .pr-table .actions .sep { color:var(--col-text-2); padding:0 6px; } |
| 49 | 49 | .pill { display:inline-block; padding:3px 8px; border-radius:99px; font-size:11px; font-weight:600; } |
| 50 | 50 | .pill.kind { background:rgba(125,211,252,0.12); color:#7dd3fc; } |
| 51 | 51 | .pill.kind.sale { background:rgba(252,211,77,0.12); color:#fcd34d; } |
| 52 | 52 | .pill.kind.bundle { background:rgba(167,139,250,0.12); color:#a78bfa; } |
| 53 | 53 | .pill.kind.flash { background:rgba(244,114,182,0.12); color:#f472b6; } |
| 54 | 54 | .pill.active { background:rgba(34,197,94,0.12); color:#86efac; } |
| 55 | 55 | .pill.paused { background:rgba(252,211,77,0.12); color:#fcd34d; } |
| 56 | 56 | .pill.scheduled { background:rgba(125,211,252,0.12); color:#7dd3fc; } |
| 57 | 57 | .pill.expired { background:rgba(239,68,68,0.12); color:#fca5a5; } |
| 58 | 58 | .pill.draft { background:rgba(148,163,184,0.12); color:#94a3b8; } |
| 59 | 59 | .pr-form { background:var(--col-surface-1); border:1px solid var(--col-border); border-radius:12px; padding:24px; } |
| 60 | 60 | .pr-form-grid { display:grid; grid-template-columns:1fr 1fr; gap:18px 24px; } |
| 61 | 61 | .pr-field { display:flex; flex-direction:column; gap:6px; } |
| 62 | 62 | .pr-field.full { grid-column:1 / -1; } |
| 63 | 63 | .pr-field label { font-size:12px; font-weight:600; color:var(--col-text-2); text-transform:uppercase; letter-spacing:0.05em; } |
| 64 | 64 | .pr-field input[type=text], .pr-field input[type=number], .pr-field input[type=datetime-local], .pr-field textarea, .pr-field select { background:var(--col-surface-2); border:1px solid var(--col-border); color:#fff; padding:10px 12px; border-radius:8px; font-size:14px; font-family:inherit; } |
| 65 | 65 | .pr-field textarea { min-height:70px; resize:vertical; } |
| 66 | 66 | .pr-radio-row { display:flex; gap:8px; flex-wrap:wrap; } |
| 67 | 67 | .pr-radio { display:inline-flex; align-items:center; gap:6px; padding:8px 14px; background:var(--col-surface-2); border:1px solid var(--col-border); border-radius:8px; font-size:13px; cursor:pointer; } |
| 68 | 68 | .pr-radio.is-on { background:rgba(125,211,252,0.10); border-color:rgba(125,211,252,0.40); color:#7dd3fc; } |
| 69 | 69 | .pr-radio input { accent-color:#7dd3fc; } |
| 70 | 70 | .hint { font-size:12px; color:var(--col-text-2); } |
| 71 | 71 | .pr-actions { display:flex; gap:10px; margin-top:22px; } |
| 72 | 72 | </style> |
| 73 | 73 | <nav class="pp-tabs"> |
| 74 | 74 | <a class="pp-tab" href="/admin_packages.cgi">Packages</a> |
| 75 | 75 | <a class="pp-tab is-active" href="/admin_promotions.cgi">Promotions</a> |
| 76 | 76 | </nav> |
| 77 | 77 | |
| 78 | 78 | [if:$tutorial_mode][if:$is_list] |
| 79 | 79 | <div class="tutorial-banner"> |
| 80 | 80 | <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> |
| 81 | 81 | <div class="tutorial-banner-body"> |
| 82 | 82 | <div class="tutorial-banner-title">Tutorial · Sample data only</div> |
| 83 | 83 | <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> |
| 84 | 84 | </div> |
| 85 | 85 | <a href="/admin_promotions.cgi" class="btn btn-secondary btn-sm">Exit sample view</a> |
| 86 | 86 | </div> |
| 87 | 87 | [/if][/if] |
| 88 | 88 | |
| 89 | 89 | [if:$has_flash] |
| 90 | 90 | <div class="pr-flash $flash_kind">$flash_msg</div> |
| 91 | 91 | [/if] |
| 92 | 92 | |
| 93 | 93 | [if:$schema_missing] |
| 94 | 94 | <div class="an-banner" style="background:rgba(245,158,11,0.10);border-color:rgba(245,158,11,0.30)"> |
| 95 | 95 | <strong>Promotions schema not yet installed.</strong> Run the promotions migration against the database. |
| 96 | 96 | </div> |
| 97 | 97 | [/if] |
| 98 | 98 | |
| 99 | 99 | [if:$is_list][if:$schema_ready] |
| 100 | 100 | <div class="pr-kpis"> |
| 101 | 101 | <div class="pr-kpi is-active"> |
| 102 | 102 | <div class="lbl">Active</div> |
| 103 | 103 | <div class="val">$cnt_active</div> |
| 104 | 104 | </div> |
| 105 | 105 | <div class="pr-kpi is-scheduled"> |
| 106 | 106 | <div class="lbl">Scheduled</div> |
| 107 | 107 | <div class="val">$cnt_scheduled</div> |
| 108 | 108 | </div> |
| 109 | 109 | <div class="pr-kpi is-paused"> |
| 110 | 110 | <div class="lbl">Paused</div> |
| 111 | 111 | <div class="val">$cnt_paused</div> |
| 112 | 112 | </div> |
| 113 | 113 | <div class="pr-kpi is-expired"> |
| 114 | 114 | <div class="lbl">Expired</div> |
| 115 | 115 | <div class="val">$cnt_expired</div> |
| 116 | 116 | </div> |
| 117 | 117 | <div class="pr-kpi"> |
| 118 | 118 | <div class="lbl">Total redemptions</div> |
| 119 | 119 | <div class="val">$sum_redemptions</div> |
| 120 | 120 | </div> |
| 121 | 121 | <div class="pr-kpi"> |
| 122 | 122 | <div class="lbl">Discount given</div> |
| 123 | 123 | <div class="val">$sum_discount_display</div> |
| 124 | 124 | </div> |
| 125 | 125 | </div> |
| 126 | 126 | |
| 127 | 127 | [if:$has_rows] |
| 128 | 128 | <table class="pr-table"> |
| 129 | 129 | <thead> |
| 130 | 130 | <tr> |
| 131 | 131 | <th>Promotion</th> |
| 132 | 132 | <th>Kind</th> |
| 133 | 133 | <th>Discount</th> |
| 134 | 134 | <th>Window</th> |
| 135 | 135 | <th>Usage</th> |
| 136 | 136 | <th class="num">Discount given</th> |
| 137 | 137 | <th>Status</th> |
| 138 | 138 | <th>Actions</th> |
| 139 | 139 | </tr> |
| 140 | 140 | </thead> |
| 141 | 141 | <tbody> |
| 142 | 142 | [loop:@rows] |
| 143 | 143 | <tr> |
| 144 | 144 | <td> |
| 145 | 145 | <div class="name">$loop1.name</div> |
| 146 | 146 | <div class="meta"> |
| 147 | 147 | [if:$loop1.code_present]Code: <code>$loop1.code</code>[/if] |
| 148 | 148 | [if:$loop1.slug_present][if:$loop1.code_present] · [/if]Link: <code>/promo/$loop1.public_slug</code>[/if] |
| 149 | 149 | [if:$loop1.new_customers_only] · <span style="color:#86efac">New customers only</span>[/if] |
| 150 | 150 | </div> |
| 151 | 151 | </td> |
| 152 | 152 | <td> |
| 153 | 153 | [if:$loop1.is_coupon]<span class="pill kind">Coupon</span>[/if] |
| 154 | 154 | [if:$loop1.is_sale]<span class="pill kind sale">Sale</span>[/if] |
| 155 | 155 | [if:$loop1.is_bundle]<span class="pill kind bundle">Bundle</span>[/if] |
| 156 | 156 | [if:$loop1.is_flash]<span class="pill kind flash">Flash sale</span>[/if] |
| 157 | 157 | </td> |
| 158 | 158 | <td>$loop1.discount_label</td> |
| 159 | 159 | <td> |
| 160 | 160 | [if:$loop1.has_window] |
| 161 | 161 | <div class="meta" style="font-size:12px;color:var(--col-text-2)"> |
| 162 | 162 | <span class="ts" data-ts="$loop1.starts_epoch" data-fmt="date">$loop1.starts_at</span> <br>→ <span class="ts" data-ts="$loop1.ends_epoch" data-fmt="date">$loop1.ends_at</span> |
| 163 | 163 | </div> |
| 164 | 164 | [/if] |
| 165 | 165 | [if:!$loop1.has_window]<span class="text-xs text-dim">No time limit</span>[/if] |
| 166 | 166 | </td> |
| 167 | 167 | <td>$loop1.cap_label</td> |
| 168 | 168 | <td class="num">$loop1.discount_given_display</td> |
| 169 | 169 | <td> |
| 170 | 170 | [if:$loop1.is_active]<span class="pill active">Active</span>[/if] |
| 171 | 171 | [if:$loop1.is_paused]<span class="pill paused">Paused</span>[/if] |
| 172 | 172 | [if:$loop1.is_scheduled]<span class="pill scheduled">Scheduled</span>[/if] |
| 173 | 173 | [if:$loop1.is_expired]<span class="pill expired">Expired</span>[/if] |
| 174 | 174 | [if:$loop1.is_draft]<span class="pill draft">Draft</span>[/if] |
| 175 | 175 | </td> |
| 176 | 176 | <td class="actions"> |
| 177 | 177 | <a href="$loop1.edit_url">Edit</a> |
| 178 | 178 | [if:$loop1.is_active] |
| 179 | 179 | <span class="sep">·</span> |
| 180 | 180 | <form method="POST" action="/admin_promotion_action.cgi"> |
| 181 | 181 | <input type="hidden" name="act" value="pause"> |
| 182 | 182 | <input type="hidden" name="id" value="$loop1.id"> |
| 183 | 183 | <button type="submit">Pause</button> |
| 184 | 184 | </form> |
| 185 | 185 | [/if] |
| 186 | 186 | [if:$loop1.is_scheduled] |
| 187 | 187 | <span class="sep">·</span> |
| 188 | 188 | <form method="POST" action="/admin_promotion_action.cgi"> |
| 189 | 189 | <input type="hidden" name="act" value="pause"> |
| 190 | 190 | <input type="hidden" name="id" value="$loop1.id"> |
| 191 | 191 | <button type="submit">Pause</button> |
| 192 | 192 | </form> |
| 193 | 193 | [/if] |
| 194 | 194 | [if:$loop1.is_paused] |
| 195 | 195 | <span class="sep">·</span> |
| 196 | 196 | <form method="POST" action="/admin_promotion_action.cgi"> |
| 197 | 197 | <input type="hidden" name="act" value="activate"> |
| 198 | 198 | <input type="hidden" name="id" value="$loop1.id"> |
| 199 | 199 | <button type="submit">Activate</button> |
| 200 | 200 | </form> |
| 201 | 201 | [/if] |
| 202 | 202 | [if:$loop1.is_draft] |
| 203 | 203 | <span class="sep">·</span> |
| 204 | 204 | <form method="POST" action="/admin_promotion_action.cgi"> |
| 205 | 205 | <input type="hidden" name="act" value="activate"> |
| 206 | 206 | <input type="hidden" name="id" value="$loop1.id"> |
| 207 | 207 | <button type="submit">Activate</button> |
| 208 | 208 | </form> |
| 209 | 209 | [/if] |
| 210 | 210 | <span class="sep">·</span> |
| 211 | 211 | <form method="POST" action="/admin_promotion_action.cgi" |
| 212 | 212 | onsubmit="return confirm('Delete this promotion permanently? Redemption history is also removed.');"> |
| 213 | 213 | <input type="hidden" name="act" value="delete"> |
| 214 | 214 | <input type="hidden" name="id" value="$loop1.id"> |
| 215 | 215 | <button type="submit" style="color:#fca5a5">Delete</button> |
| 216 | 216 | </form> |
| 217 | 217 | </td> |
| 218 | 218 | </tr> |
| 219 | 219 | [/loop] |
| 220 | 220 | </tbody> |
| 221 | 221 | </table> |
| 222 | 222 | [/if] |
| 223 | 223 | |
| 224 | 224 | [if:!$has_rows] |
| 225 | 225 | <div style="text-align:center;padding:60px 24px;background:var(--col-surface-1);border:1px solid var(--col-border);border-radius:12px"> |
| 226 | 226 | <div class="text-sm fw-600" style="color:#fff;margin-bottom:6px">No platform promotions yet</div> |
| 227 | 227 | <div class="text-xs text-dim" style="margin-bottom:14px;max-width:380px;margin-left:auto;margin-right:auto">Create coupons or sales to bring new customers onto the platform.</div> |
| 228 | 228 | <a href="/admin_promotions.cgi?new=1" class="btn btn-primary btn-sm">Create your first platform promotion</a> |
| 229 | 229 | </div> |
| 230 | 230 | [/if] |
| 231 | 231 | [/if][/if] |
| 232 | 232 | |
| 233 | 233 | [if:$is_new][if:$schema_ready] |
| 234 | 234 | <form class="pr-form" method="POST" action="/admin_promotion_action.cgi"> |
| 235 | 235 | <input type="hidden" name="act" value="create"> |
| 236 | 236 | <div class="pr-form-grid"> |
| 237 | 237 | <div class="pr-field full"> |
| 238 | 238 | <label>Name</label> |
| 239 | 239 | <input type="text" name="name" required maxlength="160" placeholder="e.g. New customer 20% off first month"> |
| 240 | 240 | </div> |
| 241 | 241 | |
| 242 | 242 | <div class="pr-field full"> |
| 243 | 243 | <label>Description</label> |
| 244 | 244 | <textarea name="description" maxlength="500"></textarea> |
| 245 | 245 | </div> |
| 246 | 246 | |
| 247 | 247 | <div class="pr-field full"> |
| 248 | 248 | <label>Kind</label> |
| 249 | 249 | <div class="pr-radio-row"> |
| 250 | 250 | <label class="pr-radio is-on"><input type="radio" name="kind" value="coupon" checked> Coupon</label> |
| 251 | 251 | <label class="pr-radio"><input type="radio" name="kind" value="sale"> Sale</label> |
| 252 | 252 | <label class="pr-radio"><input type="radio" name="kind" value="flash_sale"> Flash sale</label> |
| 253 | 253 | </div> |
| 254 | 254 | </div> |
| 255 | 255 | |
| 256 | 256 | <div class="pr-field full"> |
| 257 | 257 | <label>Applies to plans</label> |
| 258 | 258 | <div class="pr-radio-row" data-applies-radio> |
| 259 | 259 | <label class="pr-radio is-on"><input type="radio" name="plan_scope" value="all" checked data-plan-scope> All paid plans</label> |
| 260 | 260 | <label class="pr-radio"><input type="radio" name="plan_scope" value="specific" data-plan-scope> Specific plan(s)</label> |
| 261 | 261 | </div> |
| 262 | 262 | <div class="hint" style="margin-top:6px">Platform promos always discount ContactForge subscriptions.</div> |
| 263 | 263 | [if:$has_plan_options] |
| 264 | 264 | <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)"> |
| 265 | 265 | <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> |
| 266 | 266 | [loop:@plan_options] |
| 267 | 267 | <label class="pr-radio" style="display:flex;align-items:center;gap:8px;margin-bottom:6px"> |
| 268 | 268 | <input type="checkbox" name="plan_ids" value="[$loop1.id]"> <strong>[$loop1.display_name]</strong> — [$loop1.price_display] / mo |
| 269 | 269 | </label> |
| 270 | 270 | [/loop] |
| 271 | 271 | </div> |
| 272 | 272 | [/if] |
| 273 | 273 | </div> |
| 274 | 274 | |
| 275 | 275 | <div class="pr-field"> |
| 276 | 276 | <label>Discount type</label> |
| 277 | 277 | <select name="discount_type"> |
| 278 | 278 | <option value="percent">Percent off</option> |
| 279 | 279 | <option value="fixed_amount">Fixed amount off</option> |
| 280 | 280 | </select> |
| 281 | 281 | </div> |
| 282 | 282 | |
| 283 | 283 | <div class="pr-field"> |
| 284 | 284 | <label>Discount value</label> |
| 285 | 285 | <div style="display:flex;gap:8px;align-items:center"> |
| 286 | 286 | <input type="number" name="discount_value_pct" min="0" max="100" step="1" placeholder="0" style="width:80px"> |
| 287 | 287 | <span class="hint">% off</span> |
| 288 | 288 | <span class="hint">or</span> |
| 289 | 289 | <input type="number" name="discount_value_amt" min="0" step="0.01" placeholder="0.00" style="width:100px"> |
| 290 | 290 | <span class="hint">\$ off</span> |
| 291 | 291 | </div> |
| 292 | 292 | </div> |
| 293 | 293 | |
| 294 | 294 | <div class="pr-field"> |
| 295 | 295 | <label>Coupon code</label> |
| 296 | 296 | <input type="text" name="code" maxlength="64" placeholder="NEWBIE20" style="text-transform:uppercase"> |
| 297 | 297 | </div> |
| 298 | 298 | |
| 299 | 299 | <div class="pr-field"> |
| 300 | 300 | <label>Public slug</label> |
| 301 | 301 | <input type="text" name="public_slug" maxlength="64" placeholder="newbie20" style="text-transform:lowercase"> |
| 302 | 302 | </div> |
| 303 | 303 | |
| 304 | 304 | <div class="pr-field"> |
| 305 | 305 | <label>Starts at</label> |
| 306 | 306 | <input type="datetime-local" name="starts_at_local"> |
| 307 | 307 | </div> |
| 308 | 308 | |
| 309 | 309 | <div class="pr-field"> |
| 310 | 310 | <label>Ends at</label> |
| 311 | 311 | <input type="datetime-local" name="ends_at_local"> |
| 312 | 312 | </div> |
| 313 | 313 | |
| 314 | 314 | <div class="pr-field"> |
| 315 | 315 | <label>Max total redemptions</label> |
| 316 | 316 | <input type="number" name="max_redemptions" min="0" placeholder="Unlimited"> |
| 317 | 317 | </div> |
| 318 | 318 | |
| 319 | 319 | <div class="pr-field"> |
| 320 | 320 | <label>Max per customer</label> |
| 321 | 321 | <input type="number" name="max_per_user" min="1" value="1"> |
| 322 | 322 | </div> |
| 323 | 323 | |
| 324 | 324 | <div class="pr-field"> |
| 325 | 325 | <label>Minimum order total</label> |
| 326 | 326 | <input type="number" name="minimum_order" min="0" step="0.01" placeholder="0.00"> |
| 327 | 327 | </div> |
| 328 | 328 | |
| 329 | 329 | <div class="pr-field"> |
| 330 | 330 | <label>New customers only</label> |
| 331 | 331 | <label class="pr-radio" style="display:inline-flex"><input type="checkbox" name="new_customers_only" value="1"> Only first-time subscribers</label> |
| 332 | 332 | </div> |
| 333 | 333 | </div> |
| 334 | 334 | |
| 335 | 335 | <div class="pr-actions"> |
| 336 | 336 | <button type="submit" name="publish_now" value="1" class="btn btn-primary">Create & activate</button> |
| 337 | 337 | <button type="submit" class="btn btn-secondary">Save as draft</button> |
| 338 | 338 | </div> |
| 339 | 339 | </form> |
| 340 | 340 | [/if][/if] |
| 341 | 341 | |
| 342 | 342 | [if:$is_edit][if:$schema_ready][if:$has_edit] |
| 343 | 343 | <form class="pr-form" method="POST" action="/admin_promotion_action.cgi"> |
| 344 | 344 | <input type="hidden" name="act" value="update"> |
| 345 | 345 | <input type="hidden" name="id" value="$edit.id"> |
| 346 | 346 | <div class="pr-form-grid"> |
| 347 | 347 | <div class="pr-field full"> |
| 348 | 348 | <label>Name</label> |
| 349 | 349 | <input type="text" name="name" required maxlength="160" value="$edit.name"> |
| 350 | 350 | </div> |
| 351 | 351 | |
| 352 | 352 | <div class="pr-field full"> |
| 353 | 353 | <label>Description</label> |
| 354 | 354 | <textarea name="description" maxlength="500">$edit.description</textarea> |
| 355 | 355 | </div> |
| 356 | 356 | |
| 357 | 357 | <div class="pr-field full"> |
| 358 | 358 | <label>Kind</label> |
| 359 | 359 | <div class="pr-radio-row"> |
| 360 | 360 | <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> |
| 361 | 361 | <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> |
| 362 | 362 | <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> |
| 363 | 363 | </div> |
| 364 | 364 | </div> |
| 365 | 365 | |
| 366 | 366 | <div class="pr-field full"> |
| 367 | 367 | <label>Applies to plans</label> |
| 368 | 368 | <div class="pr-radio-row" data-applies-radio> |
| 369 | 369 | <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> |
| 370 | 370 | <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> |
| 371 | 371 | </div> |
| 372 | 372 | [if:$has_plan_options] |
| 373 | 373 | <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)"> |
| 374 | 374 | <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> |
| 375 | 375 | [loop:@plan_options] |
| 376 | 376 | <label class="pr-radio" style="display:flex;align-items:center;gap:8px;margin-bottom:6px"> |
| 377 | 377 | <input type="checkbox" name="plan_ids" value="[$loop1.id]" [$loop1.checked]> <strong>[$loop1.display_name]</strong> — [$loop1.price_display] / mo |
| 378 | 378 | </label> |
| 379 | 379 | [/loop] |
| 380 | 380 | </div> |
| 381 | 381 | [/if] |
| 382 | 382 | </div> |
| 383 | 383 | |
| 384 | 384 | <div class="pr-field"> |
| 385 | 385 | <label>Discount type</label> |
| 386 | 386 | <select name="discount_type"> |
| 387 | 387 | <option value="percent"[if:$edit.is_dtype_pct] selected[/if]>Percent off</option> |
| 388 | 388 | <option value="fixed_amount"[if:$edit.is_dtype_amt] selected[/if]>Fixed amount off</option> |
| 389 | 389 | </select> |
| 390 | 390 | </div> |
| 391 | 391 | |
| 392 | 392 | <div class="pr-field"> |
| 393 | 393 | <label>Discount value</label> |
| 394 | 394 | <div style="display:flex;gap:8px;align-items:center"> |
| 395 | 395 | <input type="number" name="discount_value_pct" min="0" max="100" step="1" value="$edit.discount_value_pct" style="width:80px"> |
| 396 | 396 | <span class="hint">% off</span> |
| 397 | 397 | <span class="hint">or</span> |
| 398 | 398 | <input type="number" name="discount_value_amt" min="0" step="0.01" value="$edit.discount_value_amt" style="width:100px"> |
| 399 | 399 | <span class="hint">\$ off</span> |
| 400 | 400 | </div> |
| 401 | 401 | </div> |
| 402 | 402 | |
| 403 | 403 | <div class="pr-field"> |
| 404 | 404 | <label>Coupon code</label> |
| 405 | 405 | <input type="text" name="code" maxlength="64" value="$edit.code" style="text-transform:uppercase"> |
| 406 | 406 | </div> |
| 407 | 407 | |
| 408 | 408 | <div class="pr-field"> |
| 409 | 409 | <label>Public slug</label> |
| 410 | 410 | <input type="text" name="public_slug" maxlength="64" value="$edit.public_slug" style="text-transform:lowercase"> |
| 411 | 411 | </div> |
| 412 | 412 | |
| 413 | 413 | <div class="pr-field"> |
| 414 | 414 | <label>Starts at</label> |
| 415 | 415 | <input type="datetime-local" name="starts_at_local" value="$edit.starts_at_local"> |
| 416 | 416 | </div> |
| 417 | 417 | |
| 418 | 418 | <div class="pr-field"> |
| 419 | 419 | <label>Ends at</label> |
| 420 | 420 | <input type="datetime-local" name="ends_at_local" value="$edit.ends_at_local"> |
| 421 | 421 | </div> |
| 422 | 422 | |
| 423 | 423 | <div class="pr-field"> |
| 424 | 424 | <label>Max total redemptions</label> |
| 425 | 425 | <input type="number" name="max_redemptions" min="0" value="$edit.max_redemptions" placeholder="Unlimited"> |
| 426 | 426 | </div> |
| 427 | 427 | |
| 428 | 428 | <div class="pr-field"> |
| 429 | 429 | <label>Max per customer</label> |
| 430 | 430 | <input type="number" name="max_per_user" min="1" value="$edit.max_per_user"> |
| 431 | 431 | </div> |
| 432 | 432 | |
| 433 | 433 | <div class="pr-field"> |
| 434 | 434 | <label>Minimum order total</label> |
| 435 | 435 | <input type="number" name="minimum_order" min="0" step="0.01" value="$edit.minimum_order"> |
| 436 | 436 | </div> |
| 437 | 437 | |
| 438 | 438 | <div class="pr-field"> |
| 439 | 439 | <label>New customers only</label> |
| 440 | 440 | <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 first-time subscribers</label> |
| 441 | 441 | </div> |
| 442 | 442 | </div> |
| 443 | 443 | |
| 444 | 444 | <div class="pr-actions"> |
| 445 | 445 | <button type="submit" class="btn btn-primary">Save changes</button> |
| 446 | 446 | <a href="/admin_promotions.cgi" class="btn btn-secondary">Cancel</a> |
| 447 | 447 | </div> |
| 448 | 448 | </form> |
| 449 | 449 | [/if][/if][/if] |
| 450 | 450 | |
| 451 | 451 | <script> |
| 452 | 452 | (function () { |
| 453 | 453 | document.querySelectorAll('form').forEach(function (form) { |
| 454 | 454 | var radios = form.querySelectorAll('[data-plan-scope]'); |
| 455 | 455 | var picker = form.querySelector('[data-plan-picker]'); |
| 456 | 456 | if (!radios.length || !picker) return; |
| 457 | 457 | function sync() { |
| 458 | 458 | var sel = form.querySelector('[data-plan-scope]:checked'); |
| 459 | 459 | picker.style.display = (sel && sel.value === 'specific') ? '' : 'none'; |
| 460 | 460 | if (!sel || sel.value !== 'specific') { |
| 461 | 461 | picker.querySelectorAll('input[type=checkbox]').forEach(function (cb) { cb.checked = false; }); |
| 462 | 462 | } |
| 463 | 463 | } |
| 464 | 464 | radios.forEach(function (r) { r.addEventListener('change', sync); }); |
| 465 | 465 | sync(); |
| 466 | 466 | }); |
| 467 | 467 | })(); |
| 468 | 468 | </script> |
| 469 | 469 | |
| 470 | 470 | <div class="adm-foot-spacer" data-end="admin-promotions"> </div> |