window.pageBoot = async (me) => { const state = { lists: [], cur: null, page: 1, total: 0, pageSize: 50, data: [] }; const fmtDT = window.fmtDT || (s => s || "—"); async function loadLists(){ const d = await GET("/recipients/lists"); state.lists = d.lists || []; $("#listEmpty").style.display = state.lists.length ? "none" : ""; $("#listTbl").style.display = state.lists.length ? "" : "none"; $("#listBody").innerHTML = state.lists.map(l => ` ${esc(l.name)} ${esc(l.description || "")} ${esc(l.source || "—")} ${l.row_count} ${esc(fmtDT(l.created_at))}
`).join(""); } async function loadRecs(){ const list = state.cur; if (!list) return; const d = await GET(`/recipients/list?list_id=${list.id}&q=${encodeURIComponent($("#recSearch").value)}&page=${state.page}&limit=${state.pageSize}`); state.total = d.total; state.data = d.recipients || []; $("#recsTitle").textContent = `${list.name} — Recipients (${d.total})`; $("#recBody").innerHTML = state.data.map(r => ` ${esc(r.email)} ${esc(r.first_name || "")} ${esc(r.last_name || "")} ${esc(r.company_name || "")} ${esc(Object.keys(r.custom_fields||{}).join(", ") || "—")} ${r.is_valid ? 'valid' : `invalid`} `).join("") || `No recipients`; const pages = Math.max(1, Math.ceil(state.total / state.pageSize)); $("#pager").innerHTML = [ ``, `Page ${state.page} / ${pages} · ${state.total} rows`, `` ].join(""); } function renderSummary(s){ return el("div", {class:"vsum"}, el("div", {class:"box all"}, el("div",{class:"n"}, String(s.total)), "parsed"), el("div", {class:"box ok"}, el("div",{class:"n"}, String(s.valid)), "valid"), el("div", {class:"box bad"}, el("div",{class:"n"}, String(s.invalid)), "invalid"), el("div", {class:"box dup"}, el("div",{class:"n"}, String(s.duplicates)), "duplicates"), el("div", {class:"box sup"}, el("div",{class:"n"}, String(s.suppressed)), "suppressed")); } function sampleTable(title, rows, cols){ if (!rows || !rows.length) return null; return el("div", {style:"margin-top:10px"}, el("div", {style:"font-weight:600;margin-bottom:4px"}, title), el("div", {class:"tblwrap"}, el("table", {class:"tbl"}, el("thead",{},el("tr",{},...cols.map(c=>el("th",{},c)))), el("tbody",{},...rows.map(r=>el("tr",{},...cols.map(c=>el("td",{style:"font-size:.82rem"},esc(String(r[c]!==undefined&&r[c]!==null?r[c]:"")))))))))); } function importModal(listId){ const file = el("input", {type:"file", accept:".csv,.txt,text/csv,text/plain", required:"required"}); const res = el("div"); let parsed = null; const form = el("form", {class:"form", onsubmit: async (e) => { e.preventDefault(); if (!file.files[0]) return; res.innerHTML = ""; res.appendChild(el("div",{class:"mut"},"Validating…")); const fd = new FormData(); fd.append("file", file.files[0]); try { const d = await api("/recipients/import-preview", { method:"POST", body: fd }); parsed = d; res.innerHTML = ""; res.appendChild(el("div",{style:"font-weight:600;margin-top:14px"}, `Validation report — ${esc(d.filename)}`)); res.appendChild(renderSummary(d.summary)); if (d.headers && d.headers.length) res.appendChild(el("div",{class:"mut",style:"font-size:.8rem"}, "Headers: " + esc(d.headers.join(", ")))); const inv = sampleTable("Invalid (sample)", d.invalid_sample, ["email","error"]); const dup = sampleTable("Duplicates (sample)", d.duplicates_sample, ["email","error"]); const sup = sampleTable("Suppressed (sample)", d.suppressed_sample, ["email","error"]); [inv,dup,sup].forEach(n => n && res.appendChild(n)); if (d.parse_errors && d.parse_errors.length) res.appendChild(el("div",{class:"notice warn",style:"margin-top:10px;font-size:.82rem"}, "Parse errors: " + esc(d.parse_errors.slice(0,5).join(" | ")))); } catch (err) { res.innerHTML = ""; res.appendChild(el("div",{class:"notice err"}, "Preview failed: " + esc(err.message))); } }}, el("label",{}, "CSV or TXT file (JSON is rejected)"), file, el("div",{class:"btns"}, el("button",{class:"btn secondary",type:"submit"}, "Validate (preview only)"))); openModal("Import Recipients", el("div", {}, el("p",{class:"mut",style:"margin-top:0;font-size:.85rem"}, "CSV must contain an email column (headers: email, first_name, last_name, company_name, company_phone + custom columns). TXT format: email, first, last per line."), form, res), { wide: true, buttons: [{ label:"Commit import", kind:"btn", onClick: async (close) => { if (!file.files[0]) { toast("Choose a file first", "err"); return; } if (parsed && (parsed.summary.invalid > 0 || parsed.summary.duplicates > 0)) toast("Note: only valid, non-duplicate rows will be imported", "warn"); const fd = new FormData(); fd.append("file", file.files[0]); fd.append("list_id", listId); try { const d = await api("/recipients/import", { method:"POST", body: fd }); toast(`Imported ${d.added} recipients (${d.duplicates} dup, ${d.invalid} invalid, ${d.suppressed} suppressed)`, "ok"); close(); await loadLists(); if (state.cur) await loadRecs(); } catch (err) { toast("Import failed: " + err.message, "err"); } }}] }); } function manualModal(listId){ const em = el("input", {class:"inp", type:"email", required:"required", placeholder:"name@example.com"}); const fn = el("input", {class:"inp", placeholder:"First name"}); const ln = el("input", {class:"inp", placeholder:"Last name"}); const cn = el("input", {class:"inp", placeholder:"Company name"}); const cf = el("input", {class:"inp", placeholder:'Custom fields JSON, e.g. {"Account":"ACC-991"}'}); openModal("Add Recipient Manually", el("div", {class:"form"}, el("label",{}, "Email"), em, el("label",{}, "First name"), fn, el("label",{}, "Last name"), ln, el("label",{}, "Company name"), cn, el("label",{}, "Custom fields (optional)"), cf), { buttons: [{ label:"Add", kind:"btn", onClick: async (close) => { let custom = {}; try { if (cf.value.trim()) custom = JSON.parse(cf.value); } catch { toast("Custom fields must be valid JSON","err"); return; } const d = await POST("/recipients/manual", { list_id: listId, recipient: { email: em.value, first_name: fn.value, last_name: ln.value, company_name: cn.value, custom_fields: custom }}); if (d.ok) { toast("Recipient added", "ok"); close(); await loadLists(); if (state.cur) await loadRecs(); } else { const s = d.summary || {}; const why = (d.duplicates && d.duplicates[0] && d.duplicates[0].reason) || (d.invalid && d.invalid[0] && d.invalid[0].reason) || (d.suppressed && d.suppressed[0] && d.suppressed[0].reason) || "rejected"; toast(`Rejected: ${why}`, "err"); } }}] }); } $("#btnNewList").addEventListener("click", () => { const nm = el("input", {class:"inp", required:"required", placeholder:"e.g. Q1 invoices"});