Campaigns: archive a finished one, get the credits back, keep the numbers
A member asked for a delete button. Deleting would orphan the credit ledger, the hourly view rows and the P&L, and throw away the figures they actually want, so a campaign is archived instead: out of the working table, listed in full underneath with its views, clicks and spend, and restorable. The credits are the real point. Reserved budget is computed from campaigns that are active or paused, so a paused campaign quietly holds credits the member cannot spend anywhere else. Archiving releases them the moment the status changes, with nothing to refund and nothing that can double-refund. Only a paused or finished campaign can be archived, so pause stays the deliberate first step, and restoring brings it back paused rather than live. The syndication rails are stopped on the way in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -858,6 +858,30 @@
|
||||
}
|
||||
|
||||
// credit activity ledger under the campaign table (2026-09-16): every earn and spend with its reason
|
||||
// Archived campaigns: everything they did, none of the clutter and none of the reservation.
|
||||
function renderArchived(el, r) {
|
||||
const rows = r.archived || [];
|
||||
if (!rows.length) return;
|
||||
const box = document.createElement('div');
|
||||
box.className = 'card';
|
||||
box.style.marginTop = '18px';
|
||||
box.innerHTML = '<div class="card-head"><h3>Archived</h3><span class="sub">' + rows.length + ' finished campaign' + (rows.length === 1 ? '' : 's') + ' · credits released</span></div>'
|
||||
+ '<div class="tablewrap"><table class="adm-table"><thead><tr><th>Name</th><th>Type</th><th class="num">Views here</th><th class="num">Clicks</th><th class="num">Spent</th><th>Ran until</th><th></th></tr></thead><tbody>'
|
||||
+ rows.map(c => '<tr><td>' + esc(c.name || ('#' + c.id)) + '</td><td>' + esc(c.type) + '</td>'
|
||||
+ '<td class="num">' + Number(c.imps || 0).toLocaleString() + '</td>'
|
||||
+ '<td class="num">' + Number(c.clicks || 0).toLocaleString() + '</td>'
|
||||
+ '<td class="num">' + Number(c.spent || 0).toLocaleString() + '</td>'
|
||||
+ '<td class="when muted small">' + (c.created ? new Date(c.created).toLocaleDateString() : '') + '</td>'
|
||||
+ '<td class="act"><button class="btn small sec" data-unarch="' + c.id + '">Restore</button></td></tr>').join('')
|
||||
+ '</tbody></table></div>'
|
||||
+ '<p class="muted small" style="margin:10px 0 0">Archived campaigns keep every number they earned. They serve nothing and reserve nothing, so their unspent credits are back in your Available balance. Restore brings one back, paused.</p>';
|
||||
el.appendChild(box);
|
||||
box.querySelectorAll('[data-unarch]').forEach(b => b.addEventListener('click', async () => {
|
||||
b.disabled = true;
|
||||
try { await api('/api/my/campaigns/' + b.dataset.unarch + '/unarchive'); IAP.status('Restored, and paused. Press Resume when you want it running.', 'ok'); await loadCampaigns(); loadDashboard(); }
|
||||
catch (e) { IAP.status(e.message, 'bad'); b.disabled = false; }
|
||||
}));
|
||||
}
|
||||
async function loadCreditActivity(el) {
|
||||
try {
|
||||
const r = await (await fetch('/api/my/credits/activity')).json();
|
||||
@@ -918,12 +942,14 @@
|
||||
+ '<td data-l="Status">' + (c.status === 'out' ? (c.type === 'visits' ? '<span class="badge">pack delivered</span>' : '<span class="badge amber">budget spent</span>') : c.status === 'done' ? '<span class="muted">' + (c.type === 'featured' ? 'run ended' : 'ended') + '</span>' : c.scheduled ? '<span class="badge">scheduled</span>' : c.status) + '</td>'
|
||||
+ '<td class="act">' + (c.status === 'active' ? '<button class="btn small sec" data-camp="' + c.id + '" data-act="pause">Pause</button>'
|
||||
: c.status === 'paused' ? '<button class="btn small sec" data-camp="' + c.id + '" data-act="resume">Resume</button>' : '')
|
||||
+ (['paused', 'out', 'done'].includes(c.status) ? ' <button class="btn small ghost" data-arch="' + c.id + '" data-archname="' + esc(c.name || ('#' + c.id)) + '" data-archleft="' + Math.max(0, (c.budget || 0) - (c.spent || 0)) + '" title="move it out of this list, keep its numbers">Archive</button>' : '')
|
||||
+ (c.type === 'featured' ? ' <button class="btn small sec" data-extend="' + c.id + '" title="book more days at the daily rate">Extend run</button>'
|
||||
: c.type === 'visits' ? ' <button class="btn small sec" data-morevisits="' + c.id + '">Buy more visits</button>'
|
||||
: ' <button class="btn small sec" data-topup="' + c.id + '">Buy more views</button>')
|
||||
+ '</td></tr>').join('') + '</tbody></table>';
|
||||
el.appendChild(tbl);
|
||||
pageList('camps', tbl, 'tbody tr');
|
||||
renderArchived(el, r);
|
||||
const note = document.createElement('p'); note.className = 'muted small';
|
||||
note.textContent = 'Credits you put into a campaign are set aside the moment you start it, so your Available number drops right away and the campaign spends from its own budget. Featured links and verified-visit packs are paid in full up front; a featured run is sold by the day, so use Extend run to keep it going. Whatever a campaign has not spent comes back to Available when it ends.';
|
||||
el.appendChild(note);
|
||||
@@ -950,6 +976,20 @@
|
||||
try { await api('/api/my/campaigns/' + b.dataset.camp + '/' + b.dataset.act); await loadCampaigns(); }
|
||||
catch (e) { IAP.status(e.message, 'bad'); }
|
||||
}));
|
||||
el.querySelectorAll('[data-arch]').forEach(b => b.addEventListener('click', async () => {
|
||||
const left = Number(b.dataset.archleft) || 0;
|
||||
const ok = await IAP.confirmBox(
|
||||
'It stops appearing in this list but keeps every number it earned, in the Archived section below.'
|
||||
+ (left ? ' Its ' + left.toLocaleString() + ' unspent credits go straight back to your Available balance.' : '')
|
||||
+ ' You can restore it at any time.',
|
||||
{ title: 'Archive ' + b.dataset.archname, ok: 'Archive it', cancel: 'Keep it here' });
|
||||
if (!ok) return;
|
||||
b.disabled = true;
|
||||
try { const x = await api('/api/my/campaigns/' + b.dataset.arch + '/archive');
|
||||
IAP.status('Archived.' + (x.freed ? ' ' + Number(x.freed).toLocaleString() + ' credits released.' : ''), 'ok');
|
||||
await loadCampaigns(); loadDashboard(); }
|
||||
catch (e) { IAP.status(e.message, 'bad'); b.disabled = false; }
|
||||
}));
|
||||
el.querySelectorAll('button[data-topup]').forEach(b => b.addEventListener('click', async () => {
|
||||
const n = await IAP.ask({ title: 'Add credits', text: 'How many credits to add to this campaign? More credits buy more views. They are set aside now and spend as views are delivered; anything unspent comes back when the campaign ends.', type: 'number', placeholder: 'e.g. 100', ok: 'Add credits' });
|
||||
if (!n) return;
|
||||
|
||||
Reference in New Issue
Block a user