/* ---------------- campaigns ---------------- Clean UI rule like everywhere: the section opens on its List tab only - one card per campaign with its progress bar and Start/Pause. Creating a campaign lives in the Create tab; saving returns to the List. While any campaign is running the list refreshes itself every ~15 seconds. */ let CAMP_TAB = "list"; async function renderCampaigns(c) { c.innerHTML = ""; c.appendChild(tabBar([ {id: "list", label: "List"}, {id: "create", label: "Create campaign"} ], CAMP_TAB, t => { CAMP_TAB = t; renderCampaigns(c); })); const holder = document.createElement("div"); c.appendChild(holder); if (CAMP_TAB === "create") await paintCampCreate(holder); else await paintCampList(holder); } function campPill(st) { if (st === "running") return 'running'; if (st === "paused") return 'paused'; if (st === "done") return 'done'; return 'draft'; } async function paintCampList(holder) { const d = await api("/api/campaigns"); const camps = d.campaigns || []; if (PAGE_TIMER) { clearInterval(PAGE_TIMER); PAGE_TIMER = null; } if (camps.some(x => x.status === "running")) { PAGE_TIMER = setInterval(() => { if (VIEW === "campaigns" && CAMP_TAB === "list") renderCampaigns($("#content")); }, 15000); } if (!camps.length) { holder.innerHTML = '

Campaigns

' + '
No campaigns yet. Use ' + "the Create campaign tab to make the first one - it " + "takes a snapshot of the send queue, one message and one " + "sender (an account, or an SMTP group).
"; return; } holder.innerHTML = camps.map(cp => '

' + esc(cp.name || "Untitled campaign") + "

" + campPill(cp.status) + '
Message: ' + esc(cp.message_name || "(deleted)") + " · Sender: " + esc(cp.sender_label) + " · Created " + esc((cp.created_at || "").slice(0, 16).replace("T", " ")) + "
" + '
' + '
' + cp.percent + "% - sent " + fmt(cp.counts.sent) + " of " + fmt(cp.counts.total) + " · skipped " + fmt(cp.counts.skipped) + " · remaining " + fmt(cp.counts.pending) + "
" + '
' + (cp.status === "running" ? '' : (cp.status === "done" ? "" : '')) + (cp.status === "running" ? "" : '') + "
").join(""); holder.querySelectorAll("[data-cstart]").forEach(b => b.addEventListener("click", async () => { b.disabled = true; try { await post("/api/campaigns/start", {id: Number(b.dataset.cstart)}); toast("Campaign started."); FULL = null; renderCampaigns($("#content")); } catch (e) { toast(e.message, true); b.disabled = false; } })); holder.querySelectorAll("[data-cpause]").forEach(b => b.addEventListener("click", async () => { b.disabled = true; try { await post("/api/campaigns/pause", {id: Number(b.dataset.cpause)}); toast("Campaign paused."); FULL = null; renderCampaigns($("#content")); } catch (e) { toast(e.message, true); b.disabled = false; } })); holder.querySelectorAll("[data-cdel]").forEach(b => b.addEventListener("click", async () => { if (b.dataset.armed) { try { await post("/api/campaigns/delete", {id: Number(b.dataset.cdel)}); toast("Campaign deleted - its waiting contacts are " + "free for a new campaign."); FULL = null; renderCampaigns($("#content")); } catch (e) { toast(e.message, true); } } else { b.dataset.armed = "1"; b.textContent = "Tap again to delete"; setTimeout(() => { b.dataset.armed = ""; b.textContent = "Delete"; }, 3500); } })); } async function paintCampCreate(holder) { const [ml, s, gl, av] = await Promise.all([ api("/api/messages"), api("/api/settings"), api("/api/groups"), api("/api/campaigns/available")]); const msgs = ml.messages || []; const accts = (s.mail_accounts || []).filter(a => a.password_set); const groups = gl.groups || []; holder.innerHTML = '

New campaign

' + '
A campaign takes a snapshot of the send ' + "queue, sends it one chosen message, through one sender. " + "Nothing goes out until you press Start on the List " + "tab.
" + '' + '' + '' + '" + '' + '" + '" + '" + '" + '
Audience: ' + "send queue now holds " + fmt(av.queue) + " contacts. " + "This campaign will take a snapshot of " + fmt(av.available) + " of them - anyone already waiting " + "in another open campaign is left out.
" + '
' + '
' + '' + '' + "
"; const syncType = () => { const t = (holder.querySelector( 'input[name="cp-stype"]:checked') || {}).value; $("#cp-acct").disabled = t !== "account"; $("#cp-group").disabled = t !== "group"; }; holder.querySelectorAll('input[name="cp-stype"]').forEach(r => r.addEventListener("change", syncType)); syncType(); $("#cp-cancel").addEventListener("click", () => { CAMP_TAB = "list"; renderCampaigns($("#content")); }); $("#cp-create").addEventListener("click", async ev => { ev.target.disabled = true; const t = (holder.querySelector( 'input[name="cp-stype"]:checked') || {}).value || "account"; try { const r = await post("/api/campaigns", { name: $("#cp-name").value, message_id: Number($("#cp-msg").value || 0), sender_type: t, sender_id: t === "group" ? $("#cp-group").value : $("#cp-acct").value}); toast("Campaign created with " + fmt(r.total) + " contacts. Press Start when ready."); FULL = null; CAMP_TAB = "list"; renderCampaigns($("#content")); } catch (e) { msgInto($("#cp-err"), e.message); ev.target.disabled = false; } }); }