const m = mapper(it);
const on = cur.includes(m.v);
return ``;
}).join("") + "";
}
function bindStep() {
if (step === 1) $("#w_name").oninput = e => C.name = e.target.value;
if (step === 2) bindPick("recipient_list_id", true);
if (step === 3) { bindPick("subject_set_ids"); $$('input[name="subjRot"]').forEach(r => r.onchange = () => C.subject_rotation = r.value); }
if (step === 4) { bindPick("template_ids"); $$('input[name="tplMode"]').forEach(r => r.onchange = () => C.template_sequence_mode = r.value); }
if (step === 5) {
$$('input[name="attMode"]').forEach(r => r.onchange = () => { C.attach_mode = r.value; if (C.attach_mode !== "pdf") C.attach_pdf = false; bindAttExtra(); renderStep(); });
}
if (step === 6) {
const keep = (k, id) => { const n = $(id); if (n) n.oninput = () => C.variable_config[k] = n.value; };
keep("invoice_prefix", "#w_invoice_prefix");
keep("invoice_format", "#w_invoice_format");
["amount_min", "amount_max", "shipping_min", "shipping_max"].forEach(k => {
const n = $("#w_" + k);
if (n) n.oninput = () => C.variable_config[k] = n.value === "" ? undefined : parseFloat(n.value);
});
}
if (step === 7) bindPick("account_ids");
if (step === 8) $$('input[name="rotStrat"]').forEach(r => r.onchange = () => { C.rotation_strategy = r.value; bindRotExtra(); });
if (step === 9) ["max_per_account", "send_interval_seconds", "rate_per_minute", "max_retries", "retry_delay_seconds"].forEach(k => {
const n = $("#w_" + k); if (n) n.oninput = () => C[k] = n.value === "" ? undefined : parseFloat(n.value);
});
if (step === 10) {
$$('input[name="phSel"]').forEach(r => r.onchange = () => { C.phone_selection = r.value; renderStep(); });
bindPick("phone_fixed", true);
$$('#pk_phone_fixed input').forEach(i => i.onchange = () => { C.variable_config.phone_id = +i.value; });
}
if (step === 11) {
$("#w_unsub").onchange = e => C.unsubscribe_enabled = e.target.checked;
$("#w_pss").oninput = e => C.preview_sample_size = parseInt(e.target.value, 10) || 5;
}
}
function bindPick(key, radio = false) {
const inputs = $$(`#pk_${key} input`);
inputs.forEach(i => i.onchange = () => {
if (radio) {
C[key] = +i.value;
} else {
C[key] = inputs.filter(x => x.checked).map(x => +x.value);
}
});
}
function bindAttExtra() {
const box = $("#attExtra");
if (!box) return;
const mode = C.attach_mode;
if (mode === "none") { box.innerHTML = ""; return; }
if (mode === "pdf") {
C.attach_pdf = true;
box.innerHTML = `` +
pickList("templates", C.attach_pdf_template_id, t => ({ v: t.id, main: t.name, sub: "" }), "attach_pdf_template_id", true);
bindPick("attach_pdf_template_id", true);
} else {
box.innerHTML = `` +
pickList("attachments", C.attachment_id, a => ({ v: a.id, main: a.name + " (" + a.kind + ")", sub: a.filename }), "attachment_id", true);
bindPick("attachment_id", true);
}
}
function bindRotExtra() {
const box = $("#rotExtra");
if (!box) return;
if (C.rotation_strategy !== "weighted") { box.innerHTML = ""; return; }
const accs = (OPT.accounts || []).filter(a => C.account_ids.includes(a.id));
if (!accs.length) { box.innerHTML = `
⚠Select accounts in Step 7 first, then return here to set weights.
`; return; }
box.innerHTML = `` +
accs.map(a => `
`).join("");
$$("#rotExtra input[data-acc]").forEach(inp => inp.oninput = () => {
C.rotation_weights[String(inp.dataset.acc)] = parseInt(inp.value, 10) || 1;
});
}
function collect() {
// re-read any uncommitted inputs
if (step === 1) C.name = ($("#w_name")?.value || "").trim();
const keyMap = { 6: ["amount_min", "amount_max", "shipping_min", "shipping_max"], 9: ["max_per_account", "send_interval_seconds", "rate_per_minute", "max_retries", "retry_delay_seconds"] };
(keyMap[step] || []).forEach(k => { const n = $("#w_" + k); if (n) C[k] = parseFloat(n.value); });
}
async function next() {
collect();
if (step < 12) {
// client+server validation between steps
const serverStep = step;
try {
const d = await POST("/campaigns/validate-step", { step: serverStep, data: snapshotFor(serverStep) });
if (!d.valid) {
toast(d.errors.join(" · "), "err", 6000);
return;
}
} catch (e) { toast(e.message, "err", 6000); return; }
step++;
renderStep();
return;
}
// step 12 → create or save
const payload = payloadFromC();
try {
let saved;
if (editingId) { saved = await POST(`/campaigns/${editingId}/patch`, payload); toast("Campaign updated", "ok"); }
else { saved = await POST("/campaigns/create", payload); toast(`Campaign #${saved.id} created`, "ok"); }
closeModal();
loadList();
openMonitor(saved.id);
} catch (e) {
const errs = e.data?.detail?.errors;
toast(errs && errs.length ? errs.join(" · ") : e.message, "err", 7000);
}
}
function snapshotFor(s) {
return { name: C.name, recipient_list_id: C.recipient_list_id,
subject_set_ids: C.subject_set_ids, subject_rotation: C.subject_rotation,
template_ids: C.template_ids, template_sequence_mode: C.template_sequence_mode,
attach_mode: C.attach_mode, attach_pdf_template_id: C.attach_pdf_template_id,
attachment_id: C.attachment_id, variable_config: cleanVC(),
account_ids: C.account_ids, rotation_strategy: C.rotation_strategy,
rotation_weights: C.rotation_weights, max_per_account: C.max_per_account,
send_interval_seconds: C.send_interval_seconds,
phone_selection: C.phone_selection, unsubscribe_enabled: C.unsubscribe_enabled };
}
function cleanVC() {
const out = {};
for (const [k, v] of Object.entries(C.variable_config || {})) {
if (v === undefined || v === null || v === "") continue;
out[k] = v;
}
return out;
}
function payloadFromC() {
const vc = cleanVC();
const payload = {
name: C.name, recipient_list_id: C.recipient_list_id,
subject_set_ids: C.subject_set_ids, template_ids: C.template_ids,
message_type: C.message_type, attach_mode: C.attach_mode,
attach_pdf: C.attach_mode === "pdf", attach_pdf_template_id: C.attach_mode === "pdf" ? C.attach_pdf_template_id : null,
attachment_id: (C.attach_mode === "file" || C.attach_mode === "image") ? C.attachment_id : null,
variable_config: vc, account_ids: C.account_ids,
rotation_strategy: C.rotation_strategy,
rotation_weights: C.rotation_strategy === "weighted" ? C.rotation_weights : {},
subject_rotation: C.subject_rotation, template_sequence_mode: C.template_sequence_mode,
phone_selection: C.phone_selection, max_per_account: C.max_per_account,
send_interval_seconds: C.send_interval_seconds,
};
payload.rate_per_minute = C.rate_per_minute;
payload.max_retries = C.max_retries;
payload.retry_delay_seconds = C.retry_delay_seconds;
payload.unsubscribe_enabled = C.unsubscribe_enabled;
payload.preview_sample_size = C.preview_sample_size;
return payload;
}
/* ---------------- wire-up ---------------- */
$("#btnNew").onclick = () => openWizard(null);
$("#btnClear").onclick = () =>
confirmedAction("Clear ALL campaigns? Drafts and finished campaigns are removed. This cannot be undone.",
async () => { const r = await POST("/campaigns/clear-all"); loadList(); return `Cleared ${r.deleted ?? "?"} campaign(s)`; },
{ title: "Clear all campaigns", confirmLabel: "Clear all" });
$("#fGo").onclick = () => { lst.status = $("#fStatus").value; lst.q = $("#fSearch").value.trim(); lst.page = 1; loadList(); };
$("#fSearch").onkeydown = e => { if (e.key === "Enter") $("#fGo").click(); };
$$("#modeTabs .mode-tab").forEach(t => t.onclick = () => setMode(t.dataset.mode));
await loadList();
};