This commit is contained in:
oliver
2026-06-14 20:10:20 -03:00
parent 880577e498
commit cc1bff3be0
2 changed files with 370 additions and 0 deletions
+226
View File
@@ -9,6 +9,7 @@ const ROUTES = {
backup: "/admin/backup-repo",
servers: "/admin/server",
keys: "/admin/key",
crm: "/crm",
};
/* ─── State ──────────────────────────────────────────────────────── */
@@ -19,6 +20,7 @@ let backupRecords = [];
let serversData = [];
let currentServerIdx = 0;
let keysData = [];
let crmRecords = [];
let currentSession = null;
let currentEmail = null;
@@ -187,6 +189,7 @@ document.querySelectorAll(".nav-item[data-section]").forEach((el) => {
servers: "Servers",
keys: "Keys",
sales: "Sales",
crm: "CRM",
};
document.getElementById("page-title").textContent = labels[key] || key;
@@ -208,6 +211,9 @@ document.querySelectorAll(".nav-item[data-section]").forEach((el) => {
} else if (key === "keys") {
topbar.style.display = "";
btnRefresh.onclick = keysLoadRecords;
} else if (key === "crm") {
topbar.style.display = "";
btnRefresh.onclick = crmLoadRecords;
} else {
topbar.style.display = "none";
}
@@ -225,6 +231,9 @@ document.querySelectorAll(".nav-item[data-section]").forEach((el) => {
if (key === "keys" && keysData.length === 0) {
keysLoadRecords();
}
if (key === "crm" && crmRecords.length === 0) {
crmLoadRecords();
}
});
});
@@ -1356,6 +1365,223 @@ async function keysDelete(btn) {
}
}
/* ─── CRM — Load ────────────────────────────────────────────────── */
async function crmLoadRecords() {
const tbody = document.getElementById("crm-table-body");
tbody.innerHTML = `<tr><td colspan="9"><div class="empty-state"><div class="spinner"></div><p style="margin-top:12px;">Loading…</p></div></td></tr>`;
try {
const res = await apiFetch(apiUrl(ROUTES.crm), {
headers: apiHeaders(),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const json = await res.json();
// Normalise: response may be [[{...},...]] or [{...},...] or {response:[...]}
let data;
if (Array.isArray(json)) {
if (json.length > 0 && Array.isArray(json[0])) {
data = json[0];
} else {
data = json;
}
} else if (json && json.response) {
data = json.response;
} else {
data = [];
}
crmRecords = Array.isArray(data) ? data : [];
crmRender();
toast(
`Loaded ${crmRecords.length} contact${crmRecords.length !== 1 ? "s" : ""}`,
"success",
);
} catch (err) {
crmRecords = [];
tbody.innerHTML = `<tr><td colspan="9"><div class="empty-state">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
<p style="margin-top:10px; color:var(--danger);">${err.message}</p>
</div></td></tr>`;
toast("Failed to load CRM contacts: " + err.message, "error");
}
}
/* ─── CRM — Render table ────────────────────────────────────────── */
function crmRender() {
const search = (
document.getElementById("crm-search")?.value ?? ""
).toLowerCase();
const filtered = crmRecords.filter((r) => {
if (!search) return true;
const haystack = [
r.Name,
r.Company,
r.email,
r.Stage,
r.strategy,
r.category,
r.affiliate,
r.History,
r.Number,
]
.filter((v) => v != null)
.join(" ")
.toLowerCase();
return haystack.includes(search);
});
const total = crmRecords.length;
const countEl = document.getElementById("crm-record-count");
countEl.textContent =
filtered.length === total
? `${total} contact${total !== 1 ? "s" : ""}`
: `${filtered.length} / ${total} contacts`;
// Reset check-all state
const checkAll = document.getElementById("crm-check-all");
if (checkAll) checkAll.checked = false;
const tbody = document.getElementById("crm-table-body");
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="9"><div class="empty-state">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<p>No contacts found</p>
</div></td></tr>`;
document.getElementById("crm-delete-selected-btn").disabled = true;
return;
}
tbody.innerHTML = filtered
.map((r) => {
const id = r.id;
const name = escHtml(r.Name ?? "");
const company = escHtml(r.Company ?? "");
const email = escHtml(r.email ?? "");
const stage = escHtml(r.Stage ?? "");
const strategy = escHtml(r.strategy ?? "");
const category = escHtml(r.category ?? "");
const dateNext = r.date_next_contact
? formatDate(r.date_next_contact)
: "—";
return `<tr data-crm-id="${id}">
<td><input type="checkbox" class="crm-select" data-id="${id}" onchange="crmUpdateDeleteBtn()" /></td>
<td><strong>${name || "—"}</strong></td>
<td>${company || "—"}</td>
<td><a href="mailto:${email}" style="color:var(--accent)">${email || "—"}</a></td>
<td><span class="badge ${stage === "Hold" ? "badge-other" : "badge-green"}">${stage || "—"}</span></td>
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${strategy}">${strategy || "—"}</td>
<td>${category ? escHtml(category) : "—"}</td>
<td class="text-muted">${dateNext}</td>
<td style="white-space:nowrap;">
<button class="btn btn-danger btn-sm"
data-id="${id}"
data-name="${name}"
onclick="crmDeleteContact(this)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
<path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
</svg>
Delete
</button>
</td>
</tr>`;
})
.join("");
crmUpdateDeleteBtn();
}
/* ─── CRM — Checkbox helpers ────────────────────────────────────── */
function crmUpdateDeleteBtn() {
const checked = document.querySelectorAll(".crm-select:checked").length;
const btn = document.getElementById("crm-delete-selected-btn");
btn.disabled = checked === 0;
const label =
checked > 0 ? `Delete Selected (${checked})` : "Delete Selected";
btn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right:4px;vertical-align:middle">
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
<path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
</svg> ${label}`;
}
function crmToggleAll() {
const checked = document.getElementById("crm-check-all").checked;
document.querySelectorAll(".crm-select").forEach((cb) => {
cb.checked = checked;
});
crmUpdateDeleteBtn();
}
/* ─── CRM — Delete single contact ──────────────────────────────── */
async function crmDeleteContact(btn) {
const id = btn.dataset.id;
const name = btn.dataset.name || `#${id}`;
const ok = await confirmDialog(
"Delete Contact",
`Delete contact “${name}” (id: ${id})? This cannot be undone.`,
);
if (!ok) return;
try {
const res = await apiFetch(apiUrl(ROUTES.crm), {
method: "DELETE",
headers: apiHeaders(),
body: JSON.stringify({ id: Number(id) }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
toast(`Contact “${name}” deleted`, "success");
await crmLoadRecords();
} catch (err) {
toast("Delete failed: " + err.message, "error");
}
}
/* ─── CRM — Delete selected contacts ────────────────────────────── */
async function crmDeleteSelected() {
const ids = Array.from(document.querySelectorAll(".crm-select:checked")).map(
(cb) => Number(cb.dataset.id),
);
if (ids.length === 0) return;
const ok = await confirmDialog(
"Delete Selected Contacts",
`Delete ${ids.length} contact${ids.length !== 1 ? "s" : ""}? This cannot be undone.`,
);
if (!ok) return;
// Delete sequentially
let failed = 0;
for (const id of ids) {
try {
const res = await apiFetch(apiUrl(ROUTES.crm), {
method: "DELETE",
headers: apiHeaders(),
body: JSON.stringify({ id }),
});
if (!res.ok) failed++;
} catch {
failed++;
}
}
if (failed === 0) {
toast(
`Deleted ${ids.length} contact${ids.length !== 1 ? "s" : ""}`,
"success",
);
} else {
toast(
`Deleted ${ids.length - failed} contact${ids.length - failed !== 1 ? "s" : ""} (${failed} failed)`,
"warning",
);
}
await crmLoadRecords();
}
/* ─── Init ────────────────────────────────────────────────────────── */
settingsLoad();
const _boot = getCookie("al_session");