NonInventPurchasingSystem/CPRNIMS.WebApps/Views/Shared/PagesView/Canvass/_CanvassHelpers.cshtml

234 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}
function escAttr(s) {
return String(s).replace(/"/g,"&quot;").replace(/'/g,"&#39;");
}
/* ── 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>