diff --git a/app.js b/app.js
index 791a6bb..eb04999 100644
--- a/app.js
+++ b/app.js
@@ -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 = `
|
`;
+ 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 = ` |
`;
+ 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 = `
+
+ No contacts found
+ |
`;
+ 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 `
+ |
+ ${name || "—"} |
+ ${company || "—"} |
+ ${email || "—"} |
+ ${stage || "—"} |
+ ${strategy || "—"} |
+ ${category ? escHtml(category) : "—"} |
+ ${dateNext} |
+
+
+ |
+
`;
+ })
+ .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 = ` ${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");
diff --git a/index.html b/index.html
index 8f489bb..b600c07 100644
--- a/index.html
+++ b/index.html
@@ -111,6 +111,24 @@
>
Dashboards
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+ Name |
+ Company |
+ Email |
+ Stage |
+ Strategy |
+ Category |
+ Date Next Contact |
+ Actions |
+
+
+
+
+
+
+
+
+ Loading contacts…
+
+
+ |
+
+
+
+
+
+
+