NonInventPurchasingSystem/CPRNIMS.WebApps/Views/CanvassMgmt/Canvass - Copy.cshtml

1177 lines
38 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.

<style>
:root {
--teal-dark: #0d5c63;
--teal-mid: #0e7c86;
--teal-light: #18a8b5;
--teal-pale: #e6f7f8;
--text-dark: #1a2e35;
--text-muted: #6b8890;
--border: #d6eaec;
--card-bg: #ffffff;
--bg-page: #f0f6f7;
--radius-lg: 14px;
--radius-sm: 8px;
--shadow-card: 0 2px 12px rgba(13,92,99,.10), 0 1px 3px rgba(0,0,0,.06);
--shadow-hover: 0 6px 24px rgba(13,92,99,.18);
--shadow-drop: 0 8px 28px rgba(13,92,99,.20);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* ── PAGE WRAPPER — fills whatever container ASP.NET gives it ── */
.canvass-wrapper {
font-family: 'DM Sans', sans-serif;
background: var(--bg-page);
color: var(--text-dark);
width: 100%;
padding: 20px 24px 48px;
}
/* ── HEADER ─────────────────────────────────────────────── */
.cv-header {
background: linear-gradient(135deg, var(--teal-dark) 0%, var(--teal-mid) 55%, var(--teal-light) 100%);
padding: 26px 30px 22px;
border-radius: var(--radius-lg);
position: relative;
overflow: hidden;
margin-bottom: 18px;
}
.cv-header::before {
content: '';
position: absolute;
inset: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.04'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.cv-header-inner {
position: relative;
z-index: 1;
display: flex;
align-items: flex-start;
gap: 14px;
}
.cv-header-icon {
font-size: 2rem;
color: rgba(255,255,255,.85);
margin-top: 2px;
}
.cv-header h1 {
font-family: 'Space Grotesk', sans-serif;
font-size: 1.7rem;
font-weight: 700;
color: #fff;
line-height: 1.2;
}
.cv-header p {
font-size: .875rem;
color: rgba(255,255,255,.72);
margin-top: 4px;
}
/* ── TAB NAV ──────────────────────────────────────────── */
.cv-tabs {
display: flex;
gap: 6px;
background: #fff;
border-radius: var(--radius-lg);
padding: 6px;
box-shadow: var(--shadow-card);
margin-bottom: 18px;
}
.cv-tab-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 11px 18px;
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text-muted);
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
font-weight: 600;
cursor: pointer;
transition: all .2s ease;
white-space: nowrap;
}
.cv-tab-btn i {
font-size: .88rem;
}
.cv-tab-btn:hover {
background: var(--teal-pale);
color: var(--teal-dark);
}
.cv-tab-btn.active {
background: var(--teal-mid);
color: #fff;
box-shadow: 0 2px 8px rgba(14,124,134,.35);
}
.cv-panel {
display: none;
}
.cv-panel.active {
display: block;
}
/* ── FILTER BAR ───────────────────────────────────────── */
.cv-filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: stretch;
background: #fff;
border-radius: var(--radius-lg);
padding: 14px 16px;
box-shadow: var(--shadow-card);
margin-bottom: 16px;
}
.cv-search-box {
display: flex;
align-items: center;
gap: 8px;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
padding: 0 12px;
flex: 1;
min-width: 140px;
background: #fff;
transition: border-color .2s;
}
.cv-search-box:focus-within {
border-color: var(--teal-mid);
}
.cv-search-box i {
color: var(--text-muted);
font-size: .8rem;
flex-shrink: 0;
}
.cv-search-box input {
border: none;
outline: none;
background: transparent;
padding: 9px 0;
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
color: var(--text-dark);
width: 100%;
}
.cv-search-box input::placeholder {
color: var(--text-muted);
}
/* ── SUPPLIER DROPDOWN ────────────────────────────────── */
.cv-supplier-wrap {
position: relative;
flex: 1;
min-width: 210px;
max-width: 280px;
}
.cv-sup-trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
height: 100%;
min-height: 40px;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 12px;
background: #fff;
cursor: pointer;
user-select: none;
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
transition: border-color .2s;
}
.cv-sup-trigger:hover,
.cv-sup-trigger.open {
border-color: var(--teal-mid);
}
.cv-sup-left {
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
color: var(--text-dark);
}
.cv-sup-left i {
color: var(--text-muted);
font-size: .8rem;
flex-shrink: 0;
}
.cv-sup-lbl {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cv-sup-caret {
color: var(--text-muted);
font-size: .72rem;
flex-shrink: 0;
transition: transform .2s;
}
.cv-sup-trigger.open .cv-sup-caret {
transform: rotate(180deg);
}
.cv-sup-dropdown {
display: none;
position: absolute;
top: calc(100% + 5px);
left: 0;
right: 0;
z-index: 1000;
background: #fff;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-drop);
overflow: hidden;
}
.cv-sup-dropdown.open {
display: flex;
flex-direction: column;
}
.cv-sup-searchbox {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
}
.cv-sup-searchbox i {
color: var(--text-muted);
font-size: .8rem;
flex-shrink: 0;
}
.cv-sup-searchbox input {
border: none;
outline: none;
background: transparent;
font-family: 'DM Sans', sans-serif;
font-size: .85rem;
color: var(--text-dark);
width: 100%;
}
.cv-sup-searchbox input::placeholder {
color: var(--text-muted);
}
.cv-sup-list {
max-height: 250px;
overflow-y: auto;
overscroll-behavior: contain;
}
.cv-sup-list::-webkit-scrollbar {
width: 4px;
}
.cv-sup-list::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
.cv-sup-opt {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
cursor: pointer;
font-size: .85rem;
color: var(--text-dark);
transition: background .15s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cv-sup-opt i {
color: var(--text-muted);
font-size: .78rem;
flex-shrink: 0;
}
.cv-sup-opt:hover {
background: var(--teal-pale);
}
.cv-sup-opt.active {
background: var(--teal-pale);
color: var(--teal-dark);
font-weight: 600;
}
.cv-sup-opt.active i {
color: var(--teal-mid);
}
.cv-sup-empty {
padding: 14px 12px;
font-size: .85rem;
color: var(--text-muted);
text-align: center;
}
/* ── FILTER RIGHT SIDE ────────────────────────────────── */
.cv-filter-right {
display: flex;
align-items: center;
gap: 10px;
margin-left: auto;
flex-shrink: 0;
}
.cv-pgsz-lbl {
font-size: .8rem;
color: var(--text-muted);
font-weight: 500;
}
.cv-pgsz-sel {
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 10px;
background: #fff;
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
color: var(--text-dark);
cursor: pointer;
outline: none;
transition: border-color .2s;
}
.cv-pgsz-sel:focus {
border-color: var(--teal-mid);
}
.cv-result-count {
font-size: .8rem;
font-weight: 600;
background: var(--teal-pale);
color: var(--teal-dark);
padding: 6px 14px;
border-radius: 50px;
white-space: nowrap;
}
/* ── CARD GRID ────────────────────────────────────────── */
.cv-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 18px;
min-height: 220px;
}
/* ── SINGLE CARD ──────────────────────────────────────── */
.cv-card {
background: var(--card-bg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
border: 1px solid var(--border);
overflow: hidden;
display: flex;
flex-direction: column;
transition: box-shadow .25s, transform .25s;
}
.cv-card:hover {
box-shadow: var(--shadow-hover);
transform: translateY(-2px);
}
.cv-card-hd {
background: linear-gradient(135deg, var(--teal-dark), var(--teal-mid));
padding: 14px 16px 12px;
}
.cv-card-code {
font-size: .7rem;
color: rgba(255,255,255,.62);
font-weight: 600;
letter-spacing: .06em;
text-transform: uppercase;
margin-bottom: 3px;
}
.cv-card-name {
font-family: 'Space Grotesk', sans-serif;
font-size: 1rem;
font-weight: 700;
color: #fff;
line-height: 1.25;
word-break: break-word;
}
.cv-card-email {
font-size: .77rem;
color: rgba(255,255,255,.68);
margin-top: 5px;
display: flex;
align-items: flex-start;
gap: 5px;
word-break: break-all;
}
.cv-card-email i {
margin-top: 2px;
flex-shrink: 0;
}
.cv-card-body {
padding: 14px 16px;
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.cv-agg-row {
display: flex;
gap: 8px;
}
.cv-agg-badge {
flex: 1;
background: var(--teal-pale);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 9px 11px;
display: flex;
flex-direction: column;
gap: 3px;
}
.cv-agg-lbl {
font-size: .67rem;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--text-muted);
font-weight: 700;
display: flex;
align-items: center;
gap: 4px;
}
.cv-agg-val {
font-size: .83rem;
color: var(--teal-dark);
font-weight: 600;
line-height: 1.45;
word-break: break-word;
}
.cv-item-lbl {
font-size: .67rem;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--text-muted);
font-weight: 700;
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 6px;
}
.cv-item-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.cv-item-tag {
display: inline-block;
padding: 3px 9px;
background: #f3f8f9;
border: 1px solid var(--border);
border-radius: 4px;
font-size: .78rem;
color: var(--text-dark);
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cv-item-more {
display: inline-block;
padding: 3px 9px;
background: var(--teal-pale);
border: 1px solid var(--border);
border-radius: 4px;
font-size: .78rem;
color: var(--teal-dark);
font-weight: 600;
cursor: default;
}
.cv-card-ft {
padding: 10px 16px 14px;
display: flex;
gap: 8px;
border-top: 1px solid var(--border);
}
.cv-btn {
flex: 1;
padding: 9px 14px;
border-radius: var(--radius-sm);
border: none;
cursor: pointer;
font-family: 'DM Sans', sans-serif;
font-size: .82rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all .2s;
}
.cv-btn-primary {
background: var(--teal-mid);
color: #fff;
}
.cv-btn-primary:hover {
background: var(--teal-dark);
}
.cv-btn-outline {
background: transparent;
color: var(--teal-dark);
border: 1.5px solid var(--teal-mid);
}
.cv-btn-outline:hover {
background: var(--teal-pale);
}
/* ── STATE / SPINNER ──────────────────────────────────── */
.cv-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64px 20px;
gap: 14px;
}
.cv-state i {
font-size: 2.4rem;
color: var(--border);
}
.cv-state p {
color: var(--text-muted);
font-size: .9rem;
}
.cv-spinner {
width: 34px;
height: 34px;
border: 3px solid var(--border);
border-top-color: var(--teal-mid);
border-radius: 50%;
animation: cvspin .7s linear infinite;
}
@@keyframes cvspin {
to {
transform: rotate(360deg);
}
}
/* ── PAGINATION ───────────────────────────────────────── */
.cv-pagination {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-top: 20px;
padding: 14px 18px;
background: #fff;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
}
.cv-pg-info {
font-size: .82rem;
color: var(--text-muted);
}
.cv-pg-btns {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.cv-pg-btn {
min-width: 36px;
height: 36px;
padding: 0 8px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
border: 1.5px solid var(--border);
background: #fff;
color: var(--text-dark);
font-family: 'DM Sans', sans-serif;
font-size: .85rem;
font-weight: 600;
cursor: pointer;
transition: all .18s;
}
.cv-pg-btn:hover:not(:disabled) {
border-color: var(--teal-mid);
color: var(--teal-mid);
background: var(--teal-pale);
}
.cv-pg-btn.active {
background: var(--teal-mid);
border-color: var(--teal-mid);
color: #fff;
}
.cv-pg-btn:disabled {
opacity: .35;
cursor: default;
}
/* ── RESPONSIVE ───────────────────────────────────────── */
@@media (max-width: 768px) {
.canvass-wrapper {
padding: 12px 12px 36px;
}
.cv-grid {
grid-template-columns: 1fr;
}
.cv-filter-right {
margin-left: 0;
width: 100%;
}
.cv-supplier-wrap {
max-width: 100%;
}
.cv-header h1 {
font-size: 1.35rem;
}
}
@@media (min-width: 769px) and (max-width: 1100px) {
.cv-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
<div class="canvass-wrapper">
@* {{-- HEADER --}} *@
<div class="cv-header">
<div class="cv-header-inner">
<i class="fas fa-file-invoice cv-header-icon"></i>
<div>
<h1>Canvass Management</h1>
<p>Manage purchase canvass requests by supplier, status, and comparison</p>
</div>
</div>
</div>
@* {{-- TABS --}} *@
<div class="cv-tabs" role="tablist">
<button class="cv-tab-btn active" data-tab="tab-for-tagging" role="tab">
<i class="fas fa-user-tag"></i> For Tagging
</button>
<button class="cv-tab-btn" data-tab="tab-per-supplier" role="tab">
<i class="fas fa-store"></i> For Canvass
</button>
<button class="cv-tab-btn" data-tab="tab-for-approval" role="tab">
<i class="fas fa-clock"></i> For Approval
</button>
<button class="cv-tab-btn" data-tab="tab-completed" role="tab">
<i class="fas fa-check-circle"></i> Completed
</button>
</div>
<div id="tab-for-tagging" class="cv-panel active">
@* -- Filter bar -- *@
<div class="cv-filters">
<div class="cv-search-box">
<i class="fas fa-hashtag"></i>
<input type="text" id="srchItemNo" placeholder="Item Number..." />
</div>
<div class="cv-search-box">
<i class="fas fa-box"></i>
<input type="text" id="srchItemName" placeholder="Item Name..." />
</div>
<div class="cv-search-box">
<i class="fas fa-file-alt"></i>
<input type="text" id="srchPRNo" placeholder="PR Number..." />
</div>
@* {{-- Searchable supplier dropdown --}} *@
<div class="cv-supplier-wrap" id="supplierWrap">
<div class="cv-sup-trigger" id="supplierTrigger">
<span class="cv-sup-left">
<i class="fas fa-store"></i>
<span class="cv-sup-lbl" id="supplierLabel">All Suppliers</span>
</span>
<i class="fas fa-chevron-down cv-sup-caret"></i>
</div>
<div class="cv-sup-dropdown" id="supplierDropdown">
<div class="cv-sup-searchbox">
<i class="fas fa-search"></i>
<input type="text" id="supplierSearch" placeholder="Search supplier..." autocomplete="off" />
</div>
<div class="cv-sup-list" id="supplierList">
<div class="cv-sup-opt active" data-value="">
<i class="fas fa-th-large"></i> All Suppliers
</div>
</div>
</div>
</div>
<div class="cv-filter-right">
<span class="cv-pgsz-lbl">Show</span>
<select class="cv-pgsz-sel" id="pageSizeSelect">
<option value="6">6 per page</option>
<option value="12" selected>12 per page</option>
<option value="24">24 per page</option>
<option value="48">48 per page</option>
</select>
<span class="cv-result-count" id="resultCount">0 results</span>
</div>
</div>
@* {{-- Card grid --}} *@
<div class="cv-grid" id="canvassGrid">
<div class="cv-state" style="grid-column:1/-1">
<div class="cv-spinner"></div><p>Loading suppliers…</p>
</div>
</div>
@* {{-- Pagination --}} *@
<div class="cv-pagination" id="paginationBar">
<span class="cv-pg-info" id="pageInfo"></span>
<div class="cv-pg-btns" id="pageButtons"></div>
</div>
</div>
@* {{-- ══ TAB 1 — PER SUPPLIER ═══════════════════════════ --}} *@
<div id="tab-per-supplier" class="cv-panel">
@* {{-- Filter bar --}} *@
<div class="cv-filters">
<div class="cv-search-box">
<i class="fas fa-hashtag"></i>
<input type="text" id="srchItemNo" placeholder="Item Number..." />
</div>
<div class="cv-search-box">
<i class="fas fa-box"></i>
<input type="text" id="srchItemName" placeholder="Item Name..." />
</div>
<div class="cv-search-box">
<i class="fas fa-file-alt"></i>
<input type="text" id="srchPRNo" placeholder="PR Number..." />
</div>
@* {{-- Searchable supplier dropdown --}} *@
<div class="cv-supplier-wrap" id="supplierWrap">
<div class="cv-sup-trigger" id="supplierTrigger">
<span class="cv-sup-left">
<i class="fas fa-store"></i>
<span class="cv-sup-lbl" id="supplierLabel">All Suppliers</span>
</span>
<i class="fas fa-chevron-down cv-sup-caret"></i>
</div>
<div class="cv-sup-dropdown" id="supplierDropdown">
<div class="cv-sup-searchbox">
<i class="fas fa-search"></i>
<input type="text" id="supplierSearch" placeholder="Search supplier..." autocomplete="off" />
</div>
<div class="cv-sup-list" id="supplierList">
<div class="cv-sup-opt active" data-value="">
<i class="fas fa-th-large"></i> All Suppliers
</div>
</div>
</div>
</div>
<div class="cv-filter-right">
<span class="cv-pgsz-lbl">Show</span>
<select class="cv-pgsz-sel" id="pageSizeSelect">
<option value="6">6 per page</option>
<option value="12" selected>12 per page</option>
<option value="24">24 per page</option>
<option value="48">48 per page</option>
</select>
<span class="cv-result-count" id="resultCount">0 results</span>
</div>
</div>
@* {{-- Card grid --}} *@
<div class="cv-grid" id="canvassGrid">
<div class="cv-state" style="grid-column:1/-1">
<div class="cv-spinner"></div><p>Loading suppliers…</p>
</div>
</div>
@* {{-- Pagination --}} *@
<div class="cv-pagination" id="paginationBar">
<span class="cv-pg-info" id="pageInfo"></span>
<div class="cv-pg-btns" id="pageButtons"></div>
</div>
</div>
@* {{-- ══ TAB 2 — FOR APPROVAL ════════════════════════════ --}} *@
<div id="tab-for-approval" class="cv-panel">
<div class="cv-state">
<i class="fas fa-clock"></i>
<p>For Approval — wire up this panel as needed.</p>
</div>
</div>
@* {{-- ══ TAB 3 — COMPLETED ═══════════════════════════════ --}} *@
<div id="tab-completed" class="cv-panel">
<div class="cv-state">
<i class="fas fa-check-circle"></i>
<p>Completed — wire up this panel as needed.</p>
</div>
</div>
</div>
<input hidden id="supplierId" />
@await Html.PartialAsync("PagesView/Canvass/_ForCanvass")
<script>
(function () {
"use strict";
/* ── State ── */
const state = {
page: 1, pageSize: 12, totalCount: 0,
supplier: "", searchPR: "", searchItem: "", searchName: "",
allSuppliers: [], debounceTimer: null,
};
/* ── DOM ── */
const grid = document.getElementById("canvassGrid");
const resultCount = document.getElementById("resultCount");
const pageInfo = document.getElementById("pageInfo");
const pageButtons = document.getElementById("pageButtons");
const inItemNo = document.getElementById("srchItemNo");
const inItemName = document.getElementById("srchItemName");
const inPRNo = document.getElementById("srchPRNo");
const inPageSize = document.getElementById("pageSizeSelect");
const supWrap = document.getElementById("supplierWrap");
const supTrigger = document.getElementById("supplierTrigger");
const supDropdown = document.getElementById("supplierDropdown");
const supLabel = document.getElementById("supplierLabel");
const supSearch = document.getElementById("supplierSearch");
const supList = document.getElementById("supplierList");
/* ── Tabs ── */
document.querySelectorAll(".cv-tab-btn").forEach(btn =>
btn.addEventListener("click", () => {
document.querySelectorAll(".cv-tab-btn").forEach(b => b.classList.remove("active"));
document.querySelectorAll(".cv-panel").forEach(p => p.classList.remove("active"));
btn.classList.add("active");
document.getElementById(btn.dataset.tab).classList.add("active");
})
);
/* ── Supplier dropdown ── */
supTrigger.addEventListener("click", () => {
const open = supDropdown.classList.toggle("open");
supTrigger.classList.toggle("open", open);
if (open) { supSearch.value = ""; renderSupOpts(state.allSuppliers); supSearch.focus(); }
});
document.addEventListener("click", e => {
if (!supWrap.contains(e.target)) {
supDropdown.classList.remove("open");
supTrigger.classList.remove("open");
}
});
supSearch.addEventListener("input", () => {
const q = supSearch.value.trim().toLowerCase();
renderSupOpts(q ? state.allSuppliers.filter(s => s.toLowerCase().includes(q)) : state.allSuppliers);
});
supList.addEventListener("click", e => {
const opt = e.target.closest(".cv-sup-opt");
if (!opt) return;
state.supplier = opt.dataset.value;
supLabel.textContent = state.supplier || "All Suppliers";
supDropdown.classList.remove("open");
supTrigger.classList.remove("open");
supList.querySelectorAll(".cv-sup-opt").forEach(o =>
o.classList.toggle("active", o.dataset.value === state.supplier));
state.page = 1;
fetchData();
});
function renderSupOpts(list) {
const allHtml = `<div class="cv-sup-opt${state.supplier === "" ? " active" : ""}" data-value="">
<i class="fas fa-th-large"></i> All Suppliers
</div>`;
if (!list.length) {
supList.innerHTML = allHtml + `<div class="cv-sup-empty">No suppliers found</div>`;
return;
}
supList.innerHTML = allHtml + list.map(n =>
`<div class="cv-sup-opt${n === state.supplier ? " active" : ""}" data-value="${escAttr(n)}">
<i class="fas fa-store"></i> ${escHtml(n)}
</div>`
).join("");
}
/* ── Search inputs ── */
[inItemNo, inItemName, inPRNo].forEach(el =>
el.addEventListener("input", () => {
clearTimeout(state.debounceTimer);
state.debounceTimer = setTimeout(() => {
state.searchItem = inItemNo.value.trim();
state.searchName = inItemName.value.trim();
state.searchPR = inPRNo.value.trim();
state.page = 1;
fetchData();
}, 350);
})
);
inPageSize.addEventListener("change", () => {
state.pageSize = parseInt(inPageSize.value, 10);
state.page = 1;
fetchData();
});
/* ── Fetch ── */
async function fetchData() {
showLoading();
const p = new URLSearchParams({
searchPRNo: state.searchPR,
searchItemNo: state.searchItem,
searchItemName: state.searchName,
searchSupplier: state.supplier,
pageNumber: state.page,
pageSize: state.pageSize,
draw: Date.now()
});
try {
const res = await fetch(`/CanvassMgmt/GetCanvassPerSupplier?${p}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
state.totalCount = json.recordsTotal ?? json.recordsFiltered ?? 0;
if (json.supplierList?.length) {
state.allSuppliers = json.supplierList.filter(Boolean);
renderSupOpts(state.allSuppliers);
}
renderCards(json.data ?? []);
renderPagination();
resultCount.textContent = `${state.totalCount.toLocaleString()} result${state.totalCount !== 1 ? "s" : ""}`;
} catch (err) {
console.error(err);
grid.innerHTML = `<div class="cv-state" style="grid-column:1/-1">
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
<p>Failed to load data. Please try again.</p>
</div>`;
}
}
/* ── Render cards ── */
function renderCards(data) {
if (!data.length) {
grid.innerHTML = `<div class="cv-state" style="grid-column:1/-1">
<i class="fas fa-inbox"></i>
<p>No records found for the selected filters.</p>
</div>`;
return;
}
grid.innerHTML = data.map(item => {
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">
<button class="cv-btn cv-btn-outline btn-email" data-id="${item.supplierId}">
<i class="fas fa-paper-plane"></i> Send Email
</button>
</div>
</div>`;
}).join("");
grid.querySelectorAll(".btn-canvass").forEach(b =>
b.addEventListener("click", () => openCanvass(b.dataset.id)));
grid.querySelectorAll(".btn-email").forEach(b =>
b.addEventListener("click", () => sendEmail(b.dataset.id)));
}
/* ── Pagination ── */
function renderPagination() {
const total = Math.ceil(state.totalCount / state.pageSize) || 1;
const from = Math.min((state.page - 1) * state.pageSize + 1, state.totalCount);
const to = Math.min(state.page * state.pageSize, state.totalCount);
pageInfo.textContent = state.totalCount
? `Showing ${from.toLocaleString()}${to.toLocaleString()} of ${state.totalCount.toLocaleString()}`
: "No records";
pageButtons.innerHTML = "";
const prev = mkBtn('<i class="fas fa-chevron-left"></i>', state.page <= 1);
prev.addEventListener("click", () => { if (state.page > 1) { state.page--; fetchData(); } });
pageButtons.appendChild(prev);
buildRange(state.page, total).forEach(p => {
if (p === "…") {
const d = document.createElement("span");
d.className = "cv-pg-btn"; d.style.cursor = "default"; d.textContent = "…";
pageButtons.appendChild(d); return;
}
const b = mkBtn(p, false, p === state.page);
b.addEventListener("click", () => { state.page = p; fetchData(); });
pageButtons.appendChild(b);
});
const next = mkBtn('<i class="fas fa-chevron-right"></i>', state.page >= total);
next.addEventListener("click", () => { if (state.page < total) { state.page++; fetchData(); } });
pageButtons.appendChild(next);
}
function mkBtn(html, disabled, active) {
const b = document.createElement("button");
b.className = "cv-pg-btn" + (active ? " active" : "");
b.innerHTML = html; b.disabled = !!disabled;
return b;
}
function buildRange(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];
}
/* ── Helpers ── */
function showLoading() {
grid.innerHTML = `<div class="cv-state" style="grid-column:1/-1">
<div class="cv-spinner"></div><p>Loading…</p></div>`;
}
/**
* Convert aggregated string from DB to clean array.
* Handles: "31<br>1049<br>1111" → ["31","1049","1111"]
* "31, 1049, 1111" → ["31","1049","1111"]
*/
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;");
}
/* ── Card actions — hook to your existing logic ── */
function openCanvass(id) {
document.getElementById("supplierId").value = id;
// e.g. $('#canvassModal').modal('show');
console.log("Open canvass:", id);
}
function sendEmail(id) {
console.log("Send email:", id);
}
/* ── Boot ── */
fetchData();
})();
</script>