234 lines
11 KiB
Plaintext
234 lines
11 KiB
Plaintext
<script>
|
||
window.CanvassHelpers = (function () {
|
||
"use strict";
|
||
|
||
/* ── String helpers ──────────────────────────────── */
|
||
function splitAggr(raw) {
|
||
if (!raw) return [];
|
||
return raw.replace(/<br\s*\/?>/gi, ",").split(",").map(s => s.trim()).filter(Boolean);
|
||
}
|
||
|
||
function escHtml(s) {
|
||
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
||
}
|
||
|
||
function escAttr(s) {
|
||
return String(s).replace(/"/g,""").replace(/'/g,"'");
|
||
}
|
||
|
||
/* ── Pagination helpers ──────────────────────────── */
|
||
function buildPageRange(cur, total) {
|
||
if (total <= 7) return Array.from({length: total}, (_, i) => i + 1);
|
||
if (cur <= 4) return [1, 2, 3, 4, 5, "…", total];
|
||
if (cur >= total - 3) return [1, "…", total-4, total-3, total-2, total-1, total];
|
||
return [1, "…", cur-1, cur, cur+1, "…", total];
|
||
}
|
||
|
||
function mkPageBtn(html, disabled, active) {
|
||
const b = document.createElement("button");
|
||
b.className = "cv-pg-btn" + (active ? " active" : "");
|
||
b.innerHTML = html; b.disabled = !!disabled;
|
||
return b;
|
||
}
|
||
|
||
function renderPagination(container, infoEl, state, onPageChange) {
|
||
const { page, pageSize, totalCount } = state;
|
||
const totalPages = Math.ceil(totalCount / pageSize) || 1;
|
||
const from = Math.min((page - 1) * pageSize + 1, totalCount);
|
||
const to = Math.min(page * pageSize, totalCount);
|
||
|
||
infoEl.textContent = totalCount
|
||
? `Showing ${from.toLocaleString()}–${to.toLocaleString()} of ${totalCount.toLocaleString()}`
|
||
: "No records";
|
||
|
||
container.innerHTML = "";
|
||
|
||
const prev = mkPageBtn('<i class="fas fa-chevron-left"></i>', page <= 1);
|
||
prev.addEventListener("click", () => { if (page > 1) onPageChange(page - 1); });
|
||
container.appendChild(prev);
|
||
|
||
buildPageRange(page, totalPages).forEach(p => {
|
||
if (p === "…") {
|
||
const d = document.createElement("span");
|
||
d.className = "cv-pg-btn"; d.style.cursor = "default"; d.textContent = "…";
|
||
container.appendChild(d); return;
|
||
}
|
||
const b = mkPageBtn(p, false, p === page);
|
||
b.addEventListener("click", () => onPageChange(p));
|
||
container.appendChild(b);
|
||
});
|
||
|
||
const next = mkPageBtn('<i class="fas fa-chevron-right"></i>', page >= totalPages);
|
||
next.addEventListener("click", () => { if (page < totalPages) onPageChange(page + 1); });
|
||
container.appendChild(next);
|
||
}
|
||
|
||
/* ── Generic searchable dropdown factory ─────────────────────────────
|
||
*
|
||
* Works for BOTH suppliers and departments (or any list).
|
||
* CSS class prefixes are passed in so each dropdown is fully independent.
|
||
*
|
||
* param wrap HTMLElement — the root wrapper element
|
||
* param onChange Function — called with the selected value string
|
||
* param opts Object — optional overrides:
|
||
* allLabel : string label for "All" option (default "All")
|
||
* icon : string FA icon class (default "fa-th-large / fa-store")
|
||
* cssPrefix : string CSS class prefix (default "cv-sup")
|
||
*
|
||
* CSS classes expected inside `wrap`:
|
||
* {prefix}-trigger, {prefix}-dropdown, {prefix}-lbl,
|
||
* {prefix}-searchbox > input, {prefix}-list
|
||
*
|
||
* Returns: { setItems(list), getCurrent() }
|
||
──────────────────────────────────────────────────────────────────── */
|
||
function initSearchDropdown(wrap, onChange, opts) {
|
||
opts = opts || {};
|
||
const prefix = opts.cssPrefix || "cv-sup";
|
||
const allLabel = opts.allLabel || "All";
|
||
const allIcon = opts.allIcon || "fas fa-th-large";
|
||
const itemIcon = opts.itemIcon || "fas fa-store";
|
||
|
||
const trigger = wrap.querySelector(`.${prefix}-trigger`);
|
||
const dropdown = wrap.querySelector(`.${prefix}-dropdown`);
|
||
const label = wrap.querySelector(`.${prefix}-lbl`);
|
||
const search = wrap.querySelector(`.${prefix}-searchbox input`);
|
||
const list = wrap.querySelector(`.${prefix}-list`);
|
||
|
||
if (!trigger || !dropdown || !label || !search || !list) {
|
||
console.warn("initSearchDropdown: one or more required elements not found in", wrap);
|
||
return { setItems: () => {}, getCurrent: () => "" };
|
||
}
|
||
|
||
let allItems = [];
|
||
let current = "";
|
||
|
||
trigger.addEventListener("click", () => {
|
||
const open = dropdown.classList.toggle("open");
|
||
trigger.classList.toggle("open", open);
|
||
if (open) { search.value = ""; renderOpts(allItems); search.focus(); }
|
||
});
|
||
|
||
document.addEventListener("click", e => {
|
||
if (!wrap.contains(e.target)) {
|
||
dropdown.classList.remove("open");
|
||
trigger.classList.remove("open");
|
||
}
|
||
});
|
||
|
||
search.addEventListener("input", () => {
|
||
const q = search.value.trim().toLowerCase();
|
||
renderOpts(q ? allItems.filter(s => s.toLowerCase().includes(q)) : allItems);
|
||
});
|
||
|
||
list.addEventListener("click", e => {
|
||
const opt = e.target.closest("[data-value]");
|
||
if (!opt) return;
|
||
current = opt.dataset.value;
|
||
label.textContent = current || allLabel;
|
||
dropdown.classList.remove("open");
|
||
trigger.classList.remove("open");
|
||
list.querySelectorAll("[data-value]").forEach(o =>
|
||
o.classList.toggle("active", o.dataset.value === current));
|
||
onChange(current);
|
||
});
|
||
|
||
function renderOpts(items) {
|
||
const allHtml = `<div class="${prefix}-opt${current === "" ? " active" : ""}" data-value="">
|
||
<i class="${allIcon}"></i> ${escHtml(allLabel)}
|
||
</div>`;
|
||
if (!items.length) {
|
||
list.innerHTML = allHtml + `<div style="padding:12px;text-align:center;font-size:.85rem;color:#6b8890">No results found</div>`;
|
||
return;
|
||
}
|
||
list.innerHTML = allHtml + items.map(n =>
|
||
`<div class="${prefix}-opt${n === current ? " active" : ""}" data-value="${escAttr(n)}">
|
||
<i class="${itemIcon}"></i> ${escHtml(n)}
|
||
</div>`
|
||
).join("");
|
||
}
|
||
|
||
function setItems(newList) {
|
||
allItems = (newList || []).filter(Boolean);
|
||
renderOpts(allItems);
|
||
}
|
||
|
||
return { setItems, getCurrent: () => current };
|
||
}
|
||
|
||
/* ── Convenience wrappers (keep back-compat names) ───────────────── */
|
||
function initSupplierDropdown(wrap, onChange) {
|
||
return initSearchDropdown(wrap, onChange, {
|
||
cssPrefix: "cv-sup",
|
||
allLabel: "All Suppliers",
|
||
allIcon: "fas fa-th-large",
|
||
itemIcon: "fas fa-store"
|
||
});
|
||
}
|
||
|
||
function initDepartmentDropdown(wrap, onChange) {
|
||
return initSearchDropdown(wrap, onChange, {
|
||
cssPrefix: "cv-dep",
|
||
allLabel: "All Departments",
|
||
allIcon: "fas fa-th-large",
|
||
itemIcon: "fas fa-building"
|
||
});
|
||
}
|
||
|
||
/* ── Card HTML builder ───────────────────────────── */
|
||
function buildCardHtml(item, footerHtml) {
|
||
const prNos = splitAggr(item.aggrePRNo);
|
||
const itemNos = splitAggr(item.aggreItemNo);
|
||
const names = splitAggr(item.aggreItemName);
|
||
const MAX = 3;
|
||
const vis = names.slice(0, MAX);
|
||
const more = names.length - MAX;
|
||
|
||
return `
|
||
<div class="cv-card">
|
||
<div class="cv-card-hd">
|
||
<div class="cv-card-code">Supplier #${item.supplierId}</div>
|
||
<div class="cv-card-name">${escHtml(item.supplierName ?? "—")}</div>
|
||
<div class="cv-card-email">
|
||
<i class="fas fa-envelope"></i>
|
||
<span>${escHtml(item.emailAddress ?? "—")}</span>
|
||
</div>
|
||
</div>
|
||
<div class="cv-card-body">
|
||
<div class="cv-agg-row">
|
||
<div class="cv-agg-badge">
|
||
<span class="cv-agg-lbl"><i class="fas fa-file-alt"></i> PR No's</span>
|
||
<span class="cv-agg-val">${escHtml(prNos.join(", ") || "—")}</span>
|
||
</div>
|
||
<div class="cv-agg-badge">
|
||
<span class="cv-agg-lbl"><i class="fas fa-hashtag"></i> Item No's</span>
|
||
<span class="cv-agg-val">${escHtml(itemNos.join(", ") || "—")}</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="cv-item-lbl"><i class="fas fa-box"></i> Item Names</div>
|
||
<div class="cv-item-tags">
|
||
${vis.map(n => `<span class="cv-item-tag" title="${escAttr(n)}">${escHtml(n)}</span>`).join("")}
|
||
${more > 0 ? `<span class="cv-item-more">+${more} more</span>` : ""}
|
||
${names.length === 0 ? `<span style="font-size:.82rem;color:var(--text-muted)">—</span>` : ""}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="cv-card-ft">${footerHtml(item)}</div>
|
||
</div>`;
|
||
}
|
||
|
||
return {
|
||
splitAggr,
|
||
escHtml,
|
||
escAttr,
|
||
buildPageRange,
|
||
mkPageBtn,
|
||
renderPagination,
|
||
buildCardHtml,
|
||
initSearchDropdown,
|
||
initSupplierDropdown,
|
||
initDepartmentDropdown,
|
||
};
|
||
})();
|
||
</script>
|