design
This commit is contained in:
@@ -1399,27 +1399,40 @@ async function crmLoadRecords() {
|
||||
|
||||
/* ─── CRM — Render table ────────────────────────────────────────── */
|
||||
function crmRender() {
|
||||
const search = (
|
||||
document.getElementById("crm-search")?.value ?? ""
|
||||
const searchName = (
|
||||
document.getElementById("crm-search-name")?.value ?? ""
|
||||
).toLowerCase();
|
||||
const searchCompany = (
|
||||
document.getElementById("crm-search-company")?.value ?? ""
|
||||
).toLowerCase();
|
||||
const searchEmail = (
|
||||
document.getElementById("crm-search-email")?.value ?? ""
|
||||
).toLowerCase();
|
||||
const searchStage = (
|
||||
document.getElementById("crm-search-stage")?.value ?? ""
|
||||
).toLowerCase();
|
||||
const searchStrategy = (
|
||||
document.getElementById("crm-search-strategy")?.value ?? ""
|
||||
).toLowerCase();
|
||||
const searchCategory = (
|
||||
document.getElementById("crm-search-category")?.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 name = (r.Name ?? "").toLowerCase();
|
||||
const company = (r.Company ?? "").toLowerCase();
|
||||
const email = (r.email ?? "").toLowerCase();
|
||||
const stage = (r.Stage ?? "").toLowerCase();
|
||||
const strategy = (r.strategy ?? "").toLowerCase();
|
||||
const category = (r.category ?? "").toLowerCase();
|
||||
return (
|
||||
(!searchName || name.includes(searchName)) &&
|
||||
(!searchCompany || company.includes(searchCompany)) &&
|
||||
(!searchEmail || email.includes(searchEmail)) &&
|
||||
(!searchStage || stage.includes(searchStage)) &&
|
||||
(!searchStrategy || strategy.includes(searchStrategy)) &&
|
||||
(!searchCategory || category.includes(searchCategory))
|
||||
);
|
||||
});
|
||||
|
||||
const total = crmRecords.length;
|
||||
@@ -1435,7 +1448,7 @@ function crmRender() {
|
||||
|
||||
const tbody = document.getElementById("crm-table-body");
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="9"><div class="empty-state">
|
||||
tbody.innerHTML = `<tr><td colspan="8"><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"/>
|
||||
@@ -1460,27 +1473,16 @@ function crmRender() {
|
||||
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>
|
||||
// Prevent row click when clicking the checkbox
|
||||
return `<tr data-crm-id="${id}" style="cursor:pointer;" onclick="crmOpenEdit(${id})">
|
||||
<td style="cursor:default;" onclick="event.stopPropagation()"><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><a href="mailto:${email}" style="color:var(--accent)" onclick="event.stopPropagation()">${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("");
|
||||
@@ -1493,8 +1495,7 @@ 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";
|
||||
const label = checked > 0 ? `Delete Checked (${checked})` : "Delete Checked";
|
||||
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"/>
|
||||
@@ -1575,6 +1576,82 @@ async function crmDeleteSelected() {
|
||||
await crmLoadRecords();
|
||||
}
|
||||
|
||||
/* ─── CRM — Edit modal ──────────────────────────────────────────── */
|
||||
function crmOpenEdit(id) {
|
||||
const r = crmRecords.find((c) => c.id === id);
|
||||
if (!r) return;
|
||||
|
||||
document.getElementById("crm-edit-id").value = r.id;
|
||||
document.getElementById("crm-edit-name").value = r.Name ?? "";
|
||||
document.getElementById("crm-edit-company").value = r.Company ?? "";
|
||||
document.getElementById("crm-edit-email").value = r.email ?? "";
|
||||
document.getElementById("crm-edit-stage").value = r.Stage ?? "";
|
||||
document.getElementById("crm-edit-strategy").value = r.strategy ?? "";
|
||||
document.getElementById("crm-edit-category").value = r.category ?? "";
|
||||
document.getElementById("crm-edit-affiliate").value = r.affiliate ?? "";
|
||||
document.getElementById("crm-edit-number").value = r.Number ?? "";
|
||||
document.getElementById("crm-edit-date-next").value = r.date_next_contact
|
||||
? r.date_next_contact.split("T")[0]
|
||||
: "";
|
||||
document.getElementById("crm-edit-history").value = r.History ?? "";
|
||||
|
||||
document.getElementById("crm-edit-title").textContent =
|
||||
`Edit Contact — ${r.Name || r.email || `#${r.id}`}`;
|
||||
document.getElementById("crm-edit-backdrop").classList.add("open");
|
||||
setTimeout(() => document.getElementById("crm-edit-name").focus(), 80);
|
||||
}
|
||||
|
||||
function crmCloseEdit() {
|
||||
document.getElementById("crm-edit-backdrop").classList.remove("open");
|
||||
}
|
||||
|
||||
document.getElementById("crm-edit-backdrop")?.addEventListener("click", (e) => {
|
||||
if (e.target === e.currentTarget) crmCloseEdit();
|
||||
});
|
||||
|
||||
async function crmSaveEdit() {
|
||||
const id = Number(document.getElementById("crm-edit-id").value);
|
||||
const payload = {
|
||||
id,
|
||||
Name: document.getElementById("crm-edit-name").value.trim(),
|
||||
Company: document.getElementById("crm-edit-company").value.trim(),
|
||||
email: document.getElementById("crm-edit-email").value.trim(),
|
||||
Stage: document.getElementById("crm-edit-stage").value,
|
||||
strategy: document.getElementById("crm-edit-strategy").value.trim(),
|
||||
category: document.getElementById("crm-edit-category").value.trim(),
|
||||
affiliate: document.getElementById("crm-edit-affiliate").value.trim(),
|
||||
Number: document.getElementById("crm-edit-number").value.trim(),
|
||||
date_next_contact:
|
||||
document.getElementById("crm-edit-date-next").value || null,
|
||||
History: document.getElementById("crm-edit-history").value.trim(),
|
||||
};
|
||||
|
||||
const btn = document.getElementById("crm-save-btn");
|
||||
btn.disabled = true;
|
||||
const orig = btn.innerHTML;
|
||||
btn.innerHTML = `<div class="spinner" style="width:14px;height:14px;"></div> Saving…`;
|
||||
|
||||
try {
|
||||
const res = await apiFetch(apiUrl(ROUTES.crm), {
|
||||
method: "PUT",
|
||||
headers: apiHeaders(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
toast(
|
||||
`Contact “${payload.Name || payload.email || `#${id}`}” updated`,
|
||||
"success",
|
||||
);
|
||||
crmCloseEdit();
|
||||
await crmLoadRecords();
|
||||
} catch (err) {
|
||||
toast("Save failed: " + err.message, "error");
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = orig;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Init ────────────────────────────────────────────────────────── */
|
||||
settingsLoad();
|
||||
const _boot = getCookie("al_session");
|
||||
|
||||
+228
-15
@@ -730,22 +730,11 @@
|
||||
<div
|
||||
class="form-row"
|
||||
style="
|
||||
flex-wrap: nowrap;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
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"
|
||||
@@ -773,9 +762,75 @@
|
||||
d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"
|
||||
/>
|
||||
</svg>
|
||||
Delete Selected
|
||||
Delete Checked
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 120px"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="crm-search-name"
|
||||
placeholder="Search Name…"
|
||||
oninput="crmRender()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 120px"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="crm-search-company"
|
||||
placeholder="Search Company…"
|
||||
oninput="crmRender()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 140px"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="crm-search-email"
|
||||
placeholder="Search Email…"
|
||||
oninput="crmRender()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 100px"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="crm-search-stage"
|
||||
placeholder="Search Stage…"
|
||||
oninput="crmRender()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 120px"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="crm-search-strategy"
|
||||
placeholder="Search Strategy…"
|
||||
oninput="crmRender()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 100px"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="crm-search-category"
|
||||
placeholder="Search Category…"
|
||||
oninput="crmRender()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -826,7 +881,6 @@
|
||||
<th>Strategy</th>
|
||||
<th>Category</th>
|
||||
<th>Date Next Contact</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="crm-table-body">
|
||||
@@ -1032,6 +1086,165 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ CRM EDIT MODAL ════════════════════════════════════ -->
|
||||
<div class="modal-backdrop" id="crm-edit-backdrop">
|
||||
<div class="modal" style="max-width: 640px">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="crm-edit-title"
|
||||
>Edit Contact</span
|
||||
>
|
||||
<button
|
||||
class="modal-close"
|
||||
onclick="crmCloseEdit()"
|
||||
aria-label="Close"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="crm-edit-id" />
|
||||
<div class="form-row" style="flex-wrap: wrap">
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 160px"
|
||||
>
|
||||
<label>Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="crm-edit-name"
|
||||
placeholder="Contact name"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 160px"
|
||||
>
|
||||
<label>Company</label>
|
||||
<input
|
||||
type="text"
|
||||
id="crm-edit-company"
|
||||
placeholder="Company"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 200px"
|
||||
>
|
||||
<label>Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="crm-edit-email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="flex-wrap: wrap">
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 120px"
|
||||
>
|
||||
<label>Stage</label>
|
||||
<select id="crm-edit-stage">
|
||||
<option value="">— Select —</option>
|
||||
<option>Hold</option>
|
||||
<option>Active</option>
|
||||
<option>Won</option>
|
||||
<option>Lost</option>
|
||||
<option>Lead</option>
|
||||
<option>Prospect</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 160px"
|
||||
>
|
||||
<label>Strategy</label>
|
||||
<input
|
||||
type="text"
|
||||
id="crm-edit-strategy"
|
||||
placeholder="Strategy"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 120px"
|
||||
>
|
||||
<label>Category</label>
|
||||
<input
|
||||
type="text"
|
||||
id="crm-edit-category"
|
||||
placeholder="Category"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="flex-wrap: wrap">
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 120px"
|
||||
>
|
||||
<label>Affiliate</label>
|
||||
<input
|
||||
type="text"
|
||||
id="crm-edit-affiliate"
|
||||
placeholder="Affiliate"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 120px"
|
||||
>
|
||||
<label>Number</label>
|
||||
<input
|
||||
type="text"
|
||||
id="crm-edit-number"
|
||||
placeholder="Phone number"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
style="flex: 1; min-width: 160px"
|
||||
>
|
||||
<label>Date Next Contact</label>
|
||||
<input type="date" id="crm-edit-date-next" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 10px">
|
||||
<label>History / Notes</label>
|
||||
<textarea
|
||||
id="crm-edit-history"
|
||||
rows="4"
|
||||
placeholder="Contact history, notes…"
|
||||
style="width: 100%; resize: vertical"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-ghost" onclick="crmCloseEdit()">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
id="crm-save-btn"
|
||||
onclick="crmSaveEdit()"
|
||||
>
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ CONFIRM DIALOG ══════════════════════════════════════════════ -->
|
||||
<div class="modal-backdrop" id="confirm-backdrop">
|
||||
<div class="modal" id="confirm-modal" style="max-width: 360px">
|
||||
|
||||
Reference in New Issue
Block a user