crm
This commit is contained in:
@@ -9,6 +9,7 @@ const ROUTES = {
|
|||||||
backup: "/admin/backup-repo",
|
backup: "/admin/backup-repo",
|
||||||
servers: "/admin/server",
|
servers: "/admin/server",
|
||||||
keys: "/admin/key",
|
keys: "/admin/key",
|
||||||
|
crm: "/crm",
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ─── State ──────────────────────────────────────────────────────── */
|
/* ─── State ──────────────────────────────────────────────────────── */
|
||||||
@@ -19,6 +20,7 @@ let backupRecords = [];
|
|||||||
let serversData = [];
|
let serversData = [];
|
||||||
let currentServerIdx = 0;
|
let currentServerIdx = 0;
|
||||||
let keysData = [];
|
let keysData = [];
|
||||||
|
let crmRecords = [];
|
||||||
let currentSession = null;
|
let currentSession = null;
|
||||||
let currentEmail = null;
|
let currentEmail = null;
|
||||||
|
|
||||||
@@ -187,6 +189,7 @@ document.querySelectorAll(".nav-item[data-section]").forEach((el) => {
|
|||||||
servers: "Servers",
|
servers: "Servers",
|
||||||
keys: "Keys",
|
keys: "Keys",
|
||||||
sales: "Sales",
|
sales: "Sales",
|
||||||
|
crm: "CRM",
|
||||||
};
|
};
|
||||||
document.getElementById("page-title").textContent = labels[key] || key;
|
document.getElementById("page-title").textContent = labels[key] || key;
|
||||||
|
|
||||||
@@ -208,6 +211,9 @@ document.querySelectorAll(".nav-item[data-section]").forEach((el) => {
|
|||||||
} else if (key === "keys") {
|
} else if (key === "keys") {
|
||||||
topbar.style.display = "";
|
topbar.style.display = "";
|
||||||
btnRefresh.onclick = keysLoadRecords;
|
btnRefresh.onclick = keysLoadRecords;
|
||||||
|
} else if (key === "crm") {
|
||||||
|
topbar.style.display = "";
|
||||||
|
btnRefresh.onclick = crmLoadRecords;
|
||||||
} else {
|
} else {
|
||||||
topbar.style.display = "none";
|
topbar.style.display = "none";
|
||||||
}
|
}
|
||||||
@@ -225,6 +231,9 @@ document.querySelectorAll(".nav-item[data-section]").forEach((el) => {
|
|||||||
if (key === "keys" && keysData.length === 0) {
|
if (key === "keys" && keysData.length === 0) {
|
||||||
keysLoadRecords();
|
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 ────────────────────────────────────────────────────────── */
|
/* ─── Init ────────────────────────────────────────────────────────── */
|
||||||
settingsLoad();
|
settingsLoad();
|
||||||
const _boot = getCookie("al_session");
|
const _boot = getCookie("al_session");
|
||||||
|
|||||||
+144
@@ -111,6 +111,24 @@
|
|||||||
>
|
>
|
||||||
Dashboards
|
Dashboards
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item" data-section="crm">
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
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>
|
||||||
|
CRM
|
||||||
|
</div>
|
||||||
<div class="nav-item" data-section="sales">
|
<div class="nav-item" data-section="sales">
|
||||||
<svg
|
<svg
|
||||||
width="16"
|
width="16"
|
||||||
@@ -702,6 +720,132 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── CRM Section ─────────────────────────── -->
|
||||||
|
<div class="section" id="section-crm">
|
||||||
|
<!-- Search bar -->
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
style="padding: 14px 20px; margin-bottom: 16px"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="form-row"
|
||||||
|
style="
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="form-group"
|
||||||
|
style="flex: 1; min-width: 140px"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="crm-search"
|
||||||
|
placeholder="Fulltext search by name, company, email, stage, strategy…"
|
||||||
|
oninput="crmRender()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="flex: 0 0 auto">
|
||||||
|
<button
|
||||||
|
class="btn btn-danger btn-sm"
|
||||||
|
id="crm-delete-selected-btn"
|
||||||
|
onclick="crmDeleteSelected()"
|
||||||
|
disabled
|
||||||
|
style="white-space: nowrap"
|
||||||
|
>
|
||||||
|
<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 Selected
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
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>
|
||||||
|
CRM Contacts
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
id="crm-record-count"
|
||||||
|
class="text-muted"
|
||||||
|
style="font-size: 12px"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 36px">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="crm-check-all"
|
||||||
|
onchange="crmToggleAll()"
|
||||||
|
title="Select / deselect all"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Company</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Stage</th>
|
||||||
|
<th>Strategy</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Date Next Contact</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="crm-table-body">
|
||||||
|
<tr>
|
||||||
|
<td colspan="9">
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p style="margin-top: 12px">
|
||||||
|
Loading contacts…
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ─── Sales Dashboard ─────────────────────────── -->
|
<!-- ─── Sales Dashboard ─────────────────────────── -->
|
||||||
<div class="section" id="section-sales">
|
<div class="section" id="section-sales">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
Reference in New Issue
Block a user