Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/TEMPLATES/cf_admin_test_cards.html
Diff
/var/www/vhosts/3dshawn.com/crm.3dshawn.com/TEMPLATES/cf_admin_test_cards.html
added on local at 2026-07-11 18:31:58
Added
+501
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 504ad13f77d0
to 504ad13f77d0
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.| 1 | <style> | |
| 2 | /* Self-contained module styling so this template renders the same on | |
| 3 | every site regardless of host CSS. Falls back to local CSS variables | |
| 4 | when defined; otherwise uses safe dark-theme defaults. */ | |
| 5 | .module { | |
| 6 | background:var(--col-surface, #161924); | |
| 7 | border:1px solid var(--col-border, rgba(255,255,255,.08)); | |
| 8 | border-radius:12px; | |
| 9 | overflow:hidden; | |
| 10 | } | |
| 11 | .module-head { | |
| 12 | display:flex; align-items:center; gap:14px; | |
| 13 | padding:14px 18px; | |
| 14 | border-bottom:1px solid var(--col-border, rgba(255,255,255,.06)); | |
| 15 | background:rgba(255,255,255,.015); | |
| 16 | } | |
| 17 | .module-head .left { | |
| 18 | display:flex; align-items:center; gap:14px; flex:1; | |
| 19 | } | |
| 20 | .module-icon { | |
| 21 | width:36px; height:36px; flex:0 0 auto; | |
| 22 | border-radius:8px; | |
| 23 | background:rgba(99,102,241,.15); | |
| 24 | color:#a5b4fc; | |
| 25 | display:flex; align-items:center; justify-content:center; | |
| 26 | } | |
| 27 | .module-title { | |
| 28 | font-size:15px; font-weight:600; | |
| 29 | color:var(--col-text, #f1f5f9); | |
| 30 | } | |
| 31 | .module-sub { | |
| 32 | font-size:12px; color:var(--col-text-3, #94a3b8); | |
| 33 | margin-top:2px; line-height:1.5; | |
| 34 | } | |
| 35 | .module-body { padding:14px 18px; } | |
| 36 | .mb-3 { margin-bottom:18px; } | |
| 37 | .page-head { margin-bottom:18px; } | |
| 38 | .page-eyebrow { | |
| 39 | display:inline-flex; align-items:center; gap:6px; | |
| 40 | font-size:11px; color:#a5b4fc; | |
| 41 | text-transform:uppercase; letter-spacing:.08em; | |
| 42 | font-weight:600; margin-bottom:8px; | |
| 43 | } | |
| 44 | .page-eyebrow .dot { | |
| 45 | width:6px; height:6px; border-radius:50%; | |
| 46 | background:#a5b4fc; | |
| 47 | } | |
| 48 | .page-title { | |
| 49 | margin:0 0 6px; font-size:26px; letter-spacing:-.01em; | |
| 50 | color:var(--col-text, #f1f5f9); | |
| 51 | } | |
| 52 | .page-subtitle { | |
| 53 | margin:0 0 0; font-size:13px; color:var(--col-text-2, #cbd5e1); | |
| 54 | line-height:1.55; max-width:780px; | |
| 55 | } | |
| 56 | .tc-card-row { | |
| 57 | display:flex; align-items:center; gap:14px; | |
| 58 | padding:14px 16px; | |
| 59 | background:var(--col-surface-1); | |
| 60 | border:1px solid var(--col-border); | |
| 61 | border-radius:10px; | |
| 62 | margin-bottom:10px; | |
| 63 | } | |
| 64 | .tc-card-row.is-decline { border-left:3px solid #ef4444; } | |
| 65 | .tc-card-row.is-approve { border-left:3px solid #4ade80; } | |
| 66 | .tc-card-row.is-inactive { | |
| 67 | opacity:.65; | |
| 68 | background:rgba(255,255,255,.02); | |
| 69 | border-left:3px solid #6b7280; | |
| 70 | } | |
| 71 | .tc-card-glyph { | |
| 72 | width:48px; height:32px; border-radius:4px; | |
| 73 | display:grid; place-items:center; flex:0 0 auto; | |
| 74 | font:700 10px/1 var(--font-mono, monospace); | |
| 75 | color:#fff; text-transform:uppercase; | |
| 76 | letter-spacing:1px; | |
| 77 | } | |
| 78 | .tc-card-glyph.b-visa { background:linear-gradient(135deg, #1a1f71, #2a3499); } | |
| 79 | .tc-card-glyph.b-mastercard { background:linear-gradient(135deg, #eb001b, #f79e1b); } | |
| 80 | .tc-card-glyph.b-amex { background:linear-gradient(135deg, #2e77bb, #1c4f8c); } | |
| 81 | .tc-card-glyph.b-discover { background:linear-gradient(135deg, #ff6000, #d44d00); } | |
| 82 | .tc-card-glyph.b-jcb { background:linear-gradient(135deg, #0e4c92, #0a3a72); } | |
| 83 | .tc-card-glyph.b-diners { background:linear-gradient(135deg, #0079be, #005c8e); } | |
| 84 | .tc-card-glyph.b-unionpay, | |
| 85 | .tc-card-glyph.b-default { background:linear-gradient(135deg, #475569, #334155); } | |
| 86 | .tc-card-num { | |
| 87 | font:600 14px/1.2 var(--font-mono, monospace); | |
| 88 | color:var(--col-text); | |
| 89 | letter-spacing:0.5px; | |
| 90 | } | |
| 91 | .tc-card-meta { font-size:12px; color:var(--col-text-3); margin-top:2px; } | |
| 92 | .tc-pill { | |
| 93 | font:700 10px/1 inherit; | |
| 94 | letter-spacing:1px; text-transform:uppercase; | |
| 95 | padding:4px 8px; border-radius:999px; | |
| 96 | } | |
| 97 | .tc-pill.is-approve { background:rgba(34,197,94,0.16); color:#86efac; border:1px solid rgba(34,197,94,0.3); } | |
| 98 | .tc-pill.is-decline { background:rgba(239,68,68,0.16); color:#fca5a5; border:1px solid rgba(239,68,68,0.3); } | |
| 99 | .tc-pill.is-inactive { background:rgba(107,114,128,0.18); color:#cbd5e1; border:1px solid rgba(107,114,128,0.35); } | |
| 100 | .tc-owner-pill { | |
| 101 | font-size:11px; padding:3px 8px; border-radius:999px; | |
| 102 | background:rgba(139,92,246,0.13); color:#c4b5fd; | |
| 103 | border:1px solid rgba(139,92,246,0.30); | |
| 104 | margin-left:6px; | |
| 105 | } | |
| 106 | .tc-card-actions { margin-left:auto; display:flex; gap:6px; align-items:center; flex-wrap:wrap; } | |
| 107 | .tc-quickform { display:flex; gap:6px; align-items:center; margin:0; } | |
| 108 | .tc-quickform .select, .tc-quickform .input { padding:6px 8px; font-size:12px; min-width:120px; } | |
| 109 | .tc-create-form { | |
| 110 | display:grid; | |
| 111 | grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); | |
| 112 | gap:12px; | |
| 113 | margin-top:10px; | |
| 114 | } | |
| 115 | .tc-presets { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; } | |
| 116 | .tc-preset-btn { | |
| 117 | background:rgba(255,255,255,0.03); | |
| 118 | border:1px solid var(--col-border); | |
| 119 | color:var(--col-text-2); | |
| 120 | padding:6px 10px; border-radius:6px; font-size:12px; | |
| 121 | cursor:pointer; transition:background 0.12s, color 0.12s; | |
| 122 | } | |
| 123 | .tc-preset-btn:hover { background:rgba(99,102,241,0.12); color:#fff; } | |
| 124 | .tc-section-title { | |
| 125 | font-size:11px; text-transform:uppercase; letter-spacing:1px; | |
| 126 | color:var(--col-text-3); font-weight:700; | |
| 127 | margin:18px 0 10px; | |
| 128 | } | |
| 129 | .tc-bulk-row { | |
| 130 | display:flex; align-items:center; gap:10px; | |
| 131 | padding:10px 14px; | |
| 132 | background:rgba(239,68,68,0.04); | |
| 133 | border:1px solid rgba(239,68,68,0.18); | |
| 134 | border-radius:8px; | |
| 135 | margin-bottom:8px; | |
| 136 | } | |
| 137 | .tc-bulk-row .name { color:#fca5a5; font-weight:600; font-size:13px; } | |
| 138 | .tc-bulk-row .count { color:#cbd5e1; font-size:12px; } | |
| 139 | .tc-audit-row { | |
| 140 | display:flex; align-items:center; gap:12px; | |
| 141 | padding:8px 12px; | |
| 142 | border-bottom:1px solid rgba(255,255,255,.05); | |
| 143 | font-size:12px; | |
| 144 | } | |
| 145 | .tc-audit-row:last-child { border-bottom:none; } | |
| 146 | .tc-audit-action { | |
| 147 | font-size:10px; padding:2px 7px; border-radius:4px; | |
| 148 | text-transform:uppercase; letter-spacing:.06em; font-weight:700; | |
| 149 | background:rgba(99,102,241,.15); color:#a5b4fc; | |
| 150 | } | |
| 151 | .tc-audit-when { color:var(--col-text-3); font-family:var(--font-mono,monospace); font-size:11px; margin-left:auto; } | |
| 152 | </style> | |
| 153 | ||
| 154 | <div class="page-head"> | |
| 155 | <div> | |
| 156 | <span class="page-eyebrow"><span class="dot"></span> Admin console</span> | |
| 157 | <h1 class="page-title">Sandbox Test Credit Cards</h1> | |
| 158 | <p class="page-subtitle">Fake credit cards for end-to-end billing QA. Cards attached here bypass Stripe entirely — a simulated PaymentIntent is returned with the behavior you choose. Tag cards by owner so you can deactivate someone’s set in one click when they leave the team.</p> | |
| 159 | </div> | |
| 160 | </div> | |
| 161 | ||
| 162 | [if:$has_flash] | |
| 163 | <div class="banner [if:$flash_is_ok]success[/if][if:$flash_is_err]error[/if] mb-3" style="padding:12px 14px;border-radius:10px"> | |
| 164 | <div class="text-sm fw-600">$flash_msg</div> | |
| 165 | </div> | |
| 166 | [/if] | |
| 167 | ||
| 168 | [if:$schema_missing] | |
| 169 | <div class="banner warn mb-3" style="padding:12px 14px;border-radius:10px;background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.30)"> | |
| 170 | <strong>Schema not installed.</strong> Run the latest <code>_test_cards_migration.sql</code> to create the <code>admin_test_cards</code> table. | |
| 171 | </div> | |
| 172 | [/if] | |
| 173 | ||
| 174 | [if:$schema_ready] | |
| 175 | ||
| 176 | <!-- Bulk deactivate-by-owner --> | |
| 177 | [if:$has_owner_groups] | |
| 178 | <div class="module mb-3"> | |
| 179 | <div class="module-head"> | |
| 180 | <div class="left"> | |
| 181 | <div class="module-icon" style="background:rgba(239,68,68,0.12);color:#fca5a5"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="17" y1="8" x2="22" y2="13"/><line x1="22" y1="8" x2="17" y2="13"/></svg></div> | |
| 182 | <div> | |
| 183 | <div class="module-title">Offboard a team member</div> | |
| 184 | <div class="module-sub">If someone leaves, deactivate every active card they own in one click. Cards stay in the audit log so you can see who and when.</div> | |
| 185 | </div> | |
| 186 | </div> | |
| 187 | </div> | |
| 188 | <div class="module-body"> | |
| 189 | [loop:@owner_groups] | |
| 190 | <form method="POST" action="/admin_test_cards.cgi" class="tc-bulk-row" | |
| 191 | data-confirm-title="Deactivate all of $loop1.owner_name's test cards?" | |
| 192 | data-confirm-message="All $loop1.n active card(s) owned by $loop1.owner_name will be marked inactive and detached from any user account they're attached to. You can reactivate them later if needed." | |
| 193 | data-confirm-label="Deactivate $loop1.n card(s)" | |
| 194 | data-confirm-style="danger"> | |
| 195 | <input type="hidden" name="action" value="bulk_deactivate_owner"> | |
| 196 | <input type="hidden" name="owner_user_id" value="$loop1.owner_user_id"> | |
| 197 | <div class="name">$loop1.owner_name</div> | |
| 198 | <div class="count">owns $loop1.n active card(s)</div> | |
| 199 | <button type="submit" class="btn btn-danger btn-sm" style="margin-left:auto;font-size:11px">Deactivate all</button> | |
| 200 | </form> | |
| 201 | [/loop] | |
| 202 | </div> | |
| 203 | </div> | |
| 204 | [/if] | |
| 205 | ||
| 206 | <!-- Active cards --> | |
| 207 | <div class="module mb-3"> | |
| 208 | <div class="module-head"> | |
| 209 | <div class="left"> | |
| 210 | <div class="module-icon"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg></div> | |
| 211 | <div> | |
| 212 | <div class="module-title">Active test cards ($n_active)</div> | |
| 213 | <div class="module-sub">Click <strong>Assign to me</strong> to make any card your own default. The next upgrade or charge runs as the simulated behavior.</div> | |
| 214 | </div> | |
| 215 | </div> | |
| 216 | </div> | |
| 217 | <div class="module-body"> | |
| 218 | [if:$has_active] | |
| 219 | [loop:@cards_active] | |
| 220 | <div class="tc-card-row [if:$loop1.is_approve]is-approve[/if][if:$loop1.is_decline]is-decline[/if]"> | |
| 221 | <div class="tc-card-glyph b-$loop1.brand">$loop1.brand_label</div> | |
| 222 | <div> | |
| 223 | <div class="tc-card-num">$loop1.number_full[if:$loop1.has_owner]<span class="tc-owner-pill">owner: $loop1.owner_name</span>[/if]</div> | |
| 224 | <div class="tc-card-meta">$loop1.holder_name · exp $loop1.exp_display · cvc $loop1.cvc[if:$loop1.has_label] · $loop1.label[/if]</div> | |
| 225 | </div> | |
| 226 | <div class="tc-card-actions"> | |
| 227 | <span class="tc-pill [if:$loop1.is_approve]is-approve[/if][if:$loop1.is_decline]is-decline[/if]">$loop1.behavior_label</span> | |
| 228 | ||
| 229 | <form method="POST" action="/admin_test_cards.cgi" class="tc-quickform"> | |
| 230 | <input type="hidden" name="action" value="update_behavior"> | |
| 231 | <input type="hidden" name="card_id" value="$loop1.id"> | |
| 232 | <select class="select" name="behavior" onchange="this.form.submit()" title="Change simulated behavior"> | |
| 233 | [loop:@behavior_options]<option value="$loop2.value"$loop2.selected_attr>$loop2.label</option>[/loop] | |
| 234 | </select> | |
| 235 | </form> | |
| 236 | ||
| 237 | <form method="POST" action="/admin_test_cards.cgi" class="tc-quickform"> | |
| 238 | <input type="hidden" name="action" value="assign"> | |
| 239 | <input type="hidden" name="card_id" value="$loop1.id"> | |
| 240 | <input type="hidden" name="assign_to" value="me"> | |
| 241 | <button type="submit" class="btn btn-secondary btn-sm" style="font-size:11px">Assign to me</button> | |
| 242 | </form> | |
| 243 | ||
| 244 | [if:$has_users] | |
| 245 | <form method="POST" action="/admin_test_cards.cgi" class="tc-quickform"> | |
| 246 | <input type="hidden" name="action" value="assign"> | |
| 247 | <input type="hidden" name="card_id" value="$loop1.id"> | |
| 248 | <select class="select" name="target_user_id" required> | |
| 249 | <option value="">Assign to user...</option> | |
| 250 | [loop:@user_options]<option value="$loop2.id">$loop2.label</option>[/loop] | |
| 251 | </select> | |
| 252 | <button type="submit" class="btn btn-secondary btn-sm" style="font-size:11px">Assign</button> | |
| 253 | </form> | |
| 254 | [/if] | |
| 255 | ||
| 256 | <form method="POST" action="/admin_test_cards.cgi" | |
| 257 | data-confirm-title="Deactivate this test card?" | |
| 258 | data-confirm-message="The card is marked inactive and detached from every user it was attached to. You can reactivate it later. Use Delete if you want it gone for good." | |
| 259 | data-confirm-label="Deactivate" | |
| 260 | data-confirm-style="warn" | |
| 261 | style="margin:0"> | |
| 262 | <input type="hidden" name="action" value="deactivate"> | |
| 263 | <input type="hidden" name="card_id" value="$loop1.id"> | |
| 264 | <button type="submit" class="btn btn-secondary btn-sm" style="font-size:11px;background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.3);color:#fcd34d">Deactivate</button> | |
| 265 | </form> | |
| 266 | ||
| 267 | <form method="POST" action="/admin_test_cards.cgi" | |
| 268 | data-confirm-title="Delete this test credit card?" | |
| 269 | data-confirm-message="The card is removed and detached from every user it was attached to. The audit log keeps a record. This is permanent." | |
| 270 | data-confirm-label="Delete card" | |
| 271 | data-confirm-style="danger" | |
| 272 | style="margin:0"> | |
| 273 | <input type="hidden" name="action" value="delete"> | |
| 274 | <input type="hidden" name="card_id" value="$loop1.id"> | |
| 275 | <button type="submit" class="btn btn-danger btn-sm" style="font-size:11px">Delete</button> | |
| 276 | </form> | |
| 277 | </div> | |
| 278 | </div> | |
| 279 | [/loop] | |
| 280 | [/if] | |
| 281 | [if:!$has_active] | |
| 282 | <div class="text-sm text-dim" style="text-align:center;padding:24px 12px">No active test credit cards. Create one below or use a preset to get started.</div> | |
| 283 | [/if] | |
| 284 | </div> | |
| 285 | </div> | |
| 286 | ||
| 287 | <!-- Inactive cards --> | |
| 288 | [if:$has_inactive] | |
| 289 | <div class="module mb-3"> | |
| 290 | <div class="module-head"> | |
| 291 | <div class="left"> | |
| 292 | <div class="module-icon" style="background:rgba(107,114,128,.15);color:#cbd5e1"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/><line x1="2" y1="2" x2="22" y2="22"/></svg></div> | |
| 293 | <div> | |
| 294 | <div class="module-title">Deactivated cards ($n_inactive)</div> | |
| 295 | <div class="module-sub">Kept for audit history. Reactivate to bring one back, or delete to remove permanently.</div> | |
| 296 | </div> | |
| 297 | </div> | |
| 298 | </div> | |
| 299 | <div class="module-body"> | |
| 300 | [loop:@cards_inactive] | |
| 301 | <div class="tc-card-row is-inactive"> | |
| 302 | <div class="tc-card-glyph b-$loop1.brand">$loop1.brand_label</div> | |
| 303 | <div> | |
| 304 | <div class="tc-card-num">$loop1.number_full[if:$loop1.has_owner]<span class="tc-owner-pill">owner: $loop1.owner_name</span>[/if]</div> | |
| 305 | <div class="tc-card-meta">$loop1.holder_name · exp $loop1.exp_display[if:$loop1.has_deact] · deactivated by $loop1.deact_name on <span class="ts" data-ts="$loop1.deact_epoch" data-fmt="datetime">$loop1.deact_at</span>[/if]</div> | |
| 306 | </div> | |
| 307 | <div class="tc-card-actions"> | |
| 308 | <span class="tc-pill is-inactive">Inactive</span> | |
| 309 | ||
| 310 | <form method="POST" action="/admin_test_cards.cgi" style="margin:0"> | |
| 311 | <input type="hidden" name="action" value="reactivate"> | |
| 312 | <input type="hidden" name="card_id" value="$loop1.id"> | |
| 313 | <button type="submit" class="btn btn-secondary btn-sm" style="font-size:11px;background:rgba(34,197,94,.12);border-color:rgba(34,197,94,.3);color:#86efac">Reactivate</button> | |
| 314 | </form> | |
| 315 | ||
| 316 | <form method="POST" action="/admin_test_cards.cgi" | |
| 317 | data-confirm-title="Delete this test credit card?" | |
| 318 | data-confirm-message="Permanently removes the card. The audit log entries stay." | |
| 319 | data-confirm-label="Delete card" | |
| 320 | data-confirm-style="danger" | |
| 321 | style="margin:0"> | |
| 322 | <input type="hidden" name="action" value="delete"> | |
| 323 | <input type="hidden" name="card_id" value="$loop1.id"> | |
| 324 | <button type="submit" class="btn btn-danger btn-sm" style="font-size:11px">Delete</button> | |
| 325 | </form> | |
| 326 | </div> | |
| 327 | </div> | |
| 328 | [/loop] | |
| 329 | </div> | |
| 330 | </div> | |
| 331 | [/if] | |
| 332 | ||
| 333 | <!-- Create form --> | |
| 334 | <div class="module mb-3"> | |
| 335 | <div class="module-head"> | |
| 336 | <div class="left"> | |
| 337 | <div class="module-icon" style="background:rgba(34,197,94,0.15);color:#4ade80"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></div> | |
| 338 | <div> | |
| 339 | <div class="module-title">Add a test credit card</div> | |
| 340 | <div class="module-sub">Numbers are fake by design — nothing leaves the platform. Tag an owner so you can offboard their cards in one click later.</div> | |
| 341 | </div> | |
| 342 | </div> | |
| 343 | </div> | |
| 344 | <div class="module-body"> | |
| 345 | <form method="POST" action="/admin_test_cards.cgi" id="tc-create-form"> | |
| 346 | <input type="hidden" name="action" value="create"> | |
| 347 | <div class="tc-create-form"> | |
| 348 | <label class="text-xs text-dim"> | |
| 349 | Brand | |
| 350 | <select class="select" name="card_brand" id="tc-brand" style="margin-top:4px"> | |
| 351 | <option value="visa">Visa</option> | |
| 352 | <option value="mastercard">Mastercard</option> | |
| 353 | <option value="amex">American Express</option> | |
| 354 | <option value="discover">Discover</option> | |
| 355 | <option value="jcb">JCB</option> | |
| 356 | <option value="diners">Diners Club</option> | |
| 357 | <option value="unionpay">UnionPay</option> | |
| 358 | </select> | |
| 359 | </label> | |
| 360 | <label class="text-xs text-dim" style="grid-column:span 2"> | |
| 361 | Card number | |
| 362 | <input class="input" type="text" name="card_number" id="tc-number" | |
| 363 | placeholder="4242 4242 4242 4242" required maxlength="23" | |
| 364 | style="margin-top:4px;font-family:var(--font-mono,monospace);letter-spacing:1px"> | |
| 365 | </label> | |
| 366 | <label class="text-xs text-dim"> | |
| 367 | Holder name | |
| 368 | <input class="input" type="text" name="holder_name" value="Test User" | |
| 369 | maxlength="120" style="margin-top:4px"> | |
| 370 | </label> | |
| 371 | <label class="text-xs text-dim"> | |
| 372 | Exp month | |
| 373 | <input class="input" type="number" name="exp_month" value="12" min="1" max="12" style="margin-top:4px"> | |
| 374 | </label> | |
| 375 | <label class="text-xs text-dim"> | |
| 376 | Exp year | |
| 377 | <input class="input" type="number" name="exp_year" value="2030" min="2024" max="2099" style="margin-top:4px"> | |
| 378 | </label> | |
| 379 | <label class="text-xs text-dim"> | |
| 380 | CVC | |
| 381 | <input class="input" type="text" name="cvc" value="123" maxlength="4" style="margin-top:4px"> | |
| 382 | </label> | |
| 383 | <label class="text-xs text-dim"> | |
| 384 | Behavior | |
| 385 | <select class="select" name="behavior" style="margin-top:4px"> | |
| 386 | [loop:@behavior_options]<option value="$loop1.value">$loop1.label</option>[/loop] | |
| 387 | </select> | |
| 388 | </label> | |
| 389 | [if:$has_users] | |
| 390 | <label class="text-xs text-dim" style="grid-column:span 2"> | |
| 391 | Owner (optional) | |
| 392 | <select class="select" name="owner_user_id" style="margin-top:4px"> | |
| 393 | <option value="">— No owner / shared —</option> | |
| 394 | [loop:@user_options]<option value="$loop1.id">$loop1.label</option>[/loop] | |
| 395 | </select> | |
| 396 | </label> | |
| 397 | [/if] | |
| 398 | <label class="text-xs text-dim" style="grid-column:span 2"> | |
| 399 | Label (optional) | |
| 400 | <input class="input" type="text" name="label" maxlength="120" | |
| 401 | placeholder="e.g. Decline path test" style="margin-top:4px"> | |
| 402 | </label> | |
| 403 | </div> | |
| 404 | ||
| 405 | <div class="tc-presets"> | |
| 406 | <div class="text-xs text-dim" style="display:flex;align-items:center;margin-right:4px">Quick presets:</div> | |
| 407 | <button type="button" class="tc-preset-btn" data-preset="visa-approve">Visa · approve</button> | |
| 408 | <button type="button" class="tc-preset-btn" data-preset="visa-decline">Visa · decline</button> | |
| 409 | <button type="button" class="tc-preset-btn" data-preset="mc-approve">Mastercard · approve</button> | |
| 410 | <button type="button" class="tc-preset-btn" data-preset="amex-approve">Amex · approve</button> | |
| 411 | <button type="button" class="tc-preset-btn" data-preset="discover-approve">Discover · approve</button> | |
| 412 | <button type="button" class="tc-preset-btn" data-preset="insufficient">Visa · insufficient funds</button> | |
| 413 | <button type="button" class="tc-preset-btn" data-preset="expired">Visa · expired card</button> | |
| 414 | <button type="button" class="tc-preset-btn" data-preset="processing">Visa · processing error</button> | |
| 415 | <button type="button" class="tc-preset-btn" data-preset="lost">Visa · lost card</button> | |
| 416 | <button type="button" class="tc-preset-btn" data-preset="stolen">Visa · stolen card</button> | |
| 417 | <button type="button" class="tc-preset-btn" data-preset="3ds">Visa · 3DS required</button> | |
| 418 | </div> | |
| 419 | ||
| 420 | <div style="margin-top:14px"> | |
| 421 | <button type="submit" class="btn btn-primary">Create test credit card</button> | |
| 422 | </div> | |
| 423 | </form> | |
| 424 | </div> | |
| 425 | </div> | |
| 426 | ||
| 427 | <!-- Audit log --> | |
| 428 | [if:$has_audit] | |
| 429 | <div class="module mb-3"> | |
| 430 | <div class="module-head"> | |
| 431 | <div class="left"> | |
| 432 | <div class="module-icon" style="background:rgba(99,102,241,.15);color:#a5b4fc"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg></div> | |
| 433 | <div> | |
| 434 | <div class="module-title">Recent activity</div> | |
| 435 | <div class="module-sub">Last 50 events. Who created / deactivated / assigned each card, and when.</div> | |
| 436 | </div> | |
| 437 | </div> | |
| 438 | </div> | |
| 439 | <div class="module-body" style="padding:0"> | |
| 440 | [loop:@audit_rows] | |
| 441 | <div class="tc-audit-row"> | |
| 442 | <span class="tc-audit-action">$loop1.action</span> | |
| 443 | <span style="color:var(--col-text);font-weight:500">$loop1.actor_name</span> | |
| 444 | <span style="color:var(--col-text-2)">$loop1.card_label</span> | |
| 445 | [if:$loop1.has_detail]<span style="color:var(--col-text-3);font-size:11px">$loop1.detail</span>[/if] | |
| 446 | <span class="tc-audit-when"><span class="ts" data-ts="$loop1.created_epoch" data-fmt="datetime">$loop1.created_at</span></span> | |
| 447 | </div> | |
| 448 | [/loop] | |
| 449 | </div> | |
| 450 | </div> | |
| 451 | [/if] | |
| 452 | ||
| 453 | [/if] | |
| 454 | ||
| 455 | <script> | |
| 456 | (function () { | |
| 457 | // Auto-format the card-number input as 4-4-4-4 groups while typing. | |
| 458 | var num = document.getElementById('tc-number'); | |
| 459 | if (num) { | |
| 460 | num.addEventListener('input', function () { | |
| 461 | var digits = num.value.replace(/[^0-9]/g, '').slice(0, 19); | |
| 462 | num.value = digits.replace(/(\d{4})(?=\d)/g, '\$1 '); | |
| 463 | }); | |
| 464 | } | |
| 465 | ||
| 466 | var PRESETS = { | |
| 467 | 'visa-approve': { brand:'visa', number:'4242 4242 4242 4242', name:'Test Visa Approve', behavior:'approve', label:'Approves on charge' }, | |
| 468 | 'visa-decline': { brand:'visa', number:'4000 0000 0000 0002', name:'Test Visa Decline', behavior:'decline', label:'Generic decline' }, | |
| 469 | 'mc-approve': { brand:'mastercard', number:'5555 5555 5555 4444', name:'Test MC Approve', behavior:'approve', label:'Approves on charge' }, | |
| 470 | 'amex-approve': { brand:'amex', number:'3782 822463 10005', name:'Test Amex Approve', behavior:'approve', label:'Approves on charge' }, | |
| 471 | 'discover-approve': { brand:'discover', number:'6011 0000 0000 0004', name:'Test Discover Approve', behavior:'approve', label:'Approves on charge' }, | |
| 472 | 'insufficient': { brand:'visa', number:'4000 0000 0000 9995', name:'Test Insufficient', behavior:'insufficient_funds', label:'Insufficient funds path' }, | |
| 473 | 'expired': { brand:'visa', number:'4000 0000 0000 0069', name:'Test Expired', behavior:'expired_card', label:'Expired card path' }, | |
| 474 | 'processing': { brand:'visa', number:'4000 0000 0000 0119', name:'Test Processing Err', behavior:'processing_error', label:'Processing error path' }, | |
| 475 | 'lost': { brand:'visa', number:'4000 0000 0000 9987', name:'Test Lost', behavior:'lost_card', label:'Lost card path' }, | |
| 476 | 'stolen': { brand:'visa', number:'4000 0000 0000 9979', name:'Test Stolen', behavior:'stolen_card', label:'Stolen card path' }, | |
| 477 | '3ds': { brand:'visa', number:'4000 0027 6000 3184', name:'Test 3DS', behavior:'3ds_required', label:'3D Secure auth required' } | |
| 478 | }; | |
| 479 | ||
| 480 | document.querySelectorAll('.tc-preset-btn').forEach(function (btn) { | |
| 481 | btn.addEventListener('click', function () { | |
| 482 | var p = PRESETS[btn.getAttribute('data-preset')]; | |
| 483 | if (!p) return; | |
| 484 | var f = document.getElementById('tc-create-form'); | |
| 485 | f.card_brand.value = p.brand; | |
| 486 | f.card_number.value = p.number; | |
| 487 | f.holder_name.value = p.name; | |
| 488 | f.behavior.value = p.behavior; | |
| 489 | f.label.value = p.label; | |
| 490 | f.exp_month.value = 12; | |
| 491 | f.exp_year.value = 2030; | |
| 492 | f.cvc.value = (p.brand === 'amex') ? '1234' : '123'; | |
| 493 | f.card_number.style.transition = 'background 0.4s'; | |
| 494 | f.card_number.style.background = 'rgba(99,102,241,0.18)'; | |
| 495 | setTimeout(function () { f.card_number.style.background = ''; }, 600); | |
| 496 | }); | |
| 497 | }); | |
| 498 | })(); | |
| 499 | </script> | |
| 500 | ||
| 501 | <div class="page-foot-spacer"></div> |