function togglePromoFields(purpose) { var discountFields = document.getElementById('discount-fields'); var trialFields = document.getElementById('trial-fields'); var codeInput = document.getElementById('pc-code'); if (purpose === 'discount') { discountFields.classList.remove('is-hidden'); trialFields.classList.remove('is-visible'); codeInput.required = true; codeInput.placeholder = 'e.g. LAUNCH50'; } else if (purpose === 'free_trial') { discountFields.classList.add('is-hidden'); trialFields.classList.add('is-visible'); codeInput.required = true; codeInput.placeholder = 'e.g. TRIAL14'; } else { discountFields.classList.add('is-hidden'); trialFields.classList.remove('is-visible'); codeInput.required = false; codeInput.placeholder = 'Auto-generated'; } } /// Show a modal listing redemptions of a single promo code. Fetched on /// demand so the page-render path doesn't pay for it. Guest checkouts /// surface as the guest's email; missing item titles render as "—". function showPromoRedemptions(codeId, codeLabel) { var existing = document.getElementById('promo-redemptions-modal'); if (existing) existing.remove(); var overlay = document.createElement('div'); overlay.id = 'promo-redemptions-modal'; overlay.className = 'modal-overlay'; overlay.style.display = 'flex'; overlay.onclick = function (e) { if (e.target === overlay) overlay.remove(); }; var content = document.createElement('div'); content.className = 'modal-content'; content.style.maxWidth = '640px'; content.style.padding = '2rem'; content.innerHTML = '' + '
Loading…
'; // Wire the close button via JS, not an inline onclick — the app CSP has no // script-src 'unsafe-inline', so a generated inline handler is dead (Run 20). var closeBtn = content.querySelector('.modal-close'); if (closeBtn) closeBtn.onclick = function () { overlay.remove(); }; overlay.appendChild(content); document.body.appendChild(overlay); fetch('/api/promo-codes/' + codeId + '/redemptions', { credentials: 'same-origin' }) .then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); }) .then(function (data) { var body = document.getElementById('promo-redemptions-body'); if (!body) return; var rows = (data && data.redemptions) || []; if (rows.length === 0) { body.textContent = 'No redemptions yet.'; return; } var html = '' + ''; for (var i = 0; i < rows.length; i++) { var r = rows[i]; var when = new Date(r.redeemed_at).toLocaleDateString(); var who = r.username ? '@' + escapeHtml(r.username) : (r.guest_email ? escapeHtml(r.guest_email) + ' (guest)' : '—'); var item = r.item_title ? escapeHtml(r.item_title) : '—'; var paid = r.amount_cents === 0 ? 'Free' : '$' + (r.amount_cents / 100).toFixed(2); html += ''; } html += '
WhenWhoItemPaid
' + when + '' + who + '' + item + '' + paid + '
'; if (rows.length === 500) { html += '

Showing the 500 most recent redemptions.

'; } body.innerHTML = html; }) .catch(function () { var body = document.getElementById('promo-redemptions-body'); if (body) body.textContent = 'Could not load redemptions.'; }); } /// Switch between preset trial lengths and a custom number input. The /// hidden #pc-trial-days carries the actual submitted value so we can keep /// the server-side contract (a single `trial_days` field) untouched. function onTrialPresetChange() { var preset = document.getElementById('pc-trial-preset'); var hidden = document.getElementById('pc-trial-days'); var custom = document.getElementById('pc-trial-days-custom'); if (preset.value === 'custom') { custom.hidden = false; custom.required = true; custom.focus(); hidden.value = custom.value || ''; } else { custom.hidden = true; custom.required = false; hidden.value = preset.value; } }