299 lines
12 KiB
Plaintext
299 lines
12 KiB
Plaintext
<div class="inv-filters">
|
|
<div class="inv-search-box">
|
|
<i class="fas fa-file-alt"></i>
|
|
<input type="text" id="inv-srchPRNo" placeholder="PR Number..." />
|
|
</div>
|
|
<div class="inv-search-box">
|
|
<i class="fas fa-hashtag"></i>
|
|
<input type="text" id="inv-srchItemNo" placeholder="Item Number..." />
|
|
</div>
|
|
<div class="inv-search-box">
|
|
<i class="fas fa-box"></i>
|
|
<input type="text" id="inv-srchItemName" placeholder="Item Name..." />
|
|
</div>
|
|
<div class="inv-search-box">
|
|
<i class="fas fa-qrcode"></i>
|
|
<input type="text" id="inv-srchProjectCode" placeholder="Project Code..." />
|
|
</div>
|
|
|
|
<div class="inv-department-wrap" id="inv-departmentWrap">
|
|
<div class="inv-dep-trigger">
|
|
<span class="inv-dep-left">
|
|
<i class="fas fa-building"></i>
|
|
<span class="inv-dep-lbl">All Department</span>
|
|
</span>
|
|
<i class="fas fa-chevron-down inv-dep-caret"></i>
|
|
</div>
|
|
<div class="inv-dep-dropdown">
|
|
<div class="inv-dep-searchbox">
|
|
<i class="fas fa-search"></i>
|
|
<input type="text" placeholder="Search department name..." autocomplete="off" />
|
|
</div>
|
|
<div class="inv-dep-list">
|
|
<div class="inv-dep-opt active" data-value="">
|
|
<i class="fas fa-th-large"></i> All Department
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="inv-filter-right">
|
|
<span class="inv-pgsz-lbl">Show</span>
|
|
<select class="inv-pgsz-sel" id="inv-pageSize">
|
|
<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>
|
|
<option value="96">96 per page</option>
|
|
</select>
|
|
<span class="inv-result-count" id="inv-resultCount">0 results</span>
|
|
</div>
|
|
</div>
|
|
|
|
@* {{-- CARD GRID --}} *@
|
|
<div class="inv-grid" id="inv-grid">
|
|
<div class="inv-state" style="grid-column:1/-1">
|
|
<div class="inv-spinner"></div><p>Loading…</p>
|
|
</div>
|
|
</div>
|
|
|
|
@* {{-- PAGINATION --}} *@
|
|
<div class="inv-pagination">
|
|
<span class="inv-pg-info" id="inv-pageInfo"></span>
|
|
<div class="inv-pg-btns" id="inv-pageButtons"></div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
"use strict";
|
|
const H = window.InventoryHelpers;
|
|
|
|
// ── Single state object ──
|
|
const s = {
|
|
page: 1, pageSize: 12, totalCount: 0,
|
|
Department: "", searchPR: "", searchItem: "", searchName: "",
|
|
searchProjectCode: "", timer: null
|
|
};
|
|
|
|
// ── Elements ──
|
|
const grid = document.getElementById("inv-grid");
|
|
const countEl = document.getElementById("inv-resultCount");
|
|
const pageInfo = document.getElementById("inv-pageInfo");
|
|
const pageBtns = document.getElementById("inv-pageButtons");
|
|
const inItemNo = document.getElementById("inv-srchItemNo");
|
|
const inName = document.getElementById("inv-srchItemName");
|
|
const inPR = document.getElementById("inv-srchPRNo");
|
|
const inSize = document.getElementById("inv-pageSize");
|
|
const depWrap = document.getElementById("inv-departmentWrap");
|
|
|
|
// ── Guard ──
|
|
if (!grid || !depWrap) {
|
|
console.error("Tab 1 init failed. Missing:", { grid, depWrap });
|
|
return;
|
|
}
|
|
|
|
// ── Single dropdown, using s.Department ──
|
|
const depDropdown = H.initDepartmentDropdown(depWrap, val => {
|
|
s.Department = val;
|
|
s.page = 1;
|
|
fetchData();
|
|
});
|
|
|
|
// ── Search inputs ──
|
|
[inItemNo, inName, inPR].forEach(el => {
|
|
if (!el) return;
|
|
el.addEventListener("input", () => {
|
|
clearTimeout(s.timer);
|
|
s.timer = setTimeout(() => {
|
|
s.searchItem = inItemNo?.value.trim() ?? "";
|
|
s.searchName = inName?.value.trim() ?? "";
|
|
s.searchPR = inPR?.value.trim() ?? "";
|
|
s.page = 1;
|
|
fetchData();
|
|
}, 350);
|
|
});
|
|
});
|
|
|
|
if (inSize) {
|
|
inSize.addEventListener("change", () => {
|
|
s.pageSize = parseInt(inSize.value, 10);
|
|
s.page = 1;
|
|
fetchData();
|
|
});
|
|
}
|
|
|
|
async function fetchData() {
|
|
grid.innerHTML = `<div class="inv-state" style="grid-column:1/-1">
|
|
<div class="inv-spinner"></div><p>Loading…</p></div>`;
|
|
|
|
const p = new URLSearchParams({
|
|
searchPRNo: s.searchPR,
|
|
searchItemNo: s.searchItem,
|
|
searchItemName: s.searchName,
|
|
searchDept: s.Department,
|
|
searchProjectCode: s.searchProjectCode ?? "",
|
|
pageNumber: s.page,
|
|
pageSize: s.pageSize,
|
|
draw: Date.now()
|
|
});
|
|
|
|
try {
|
|
const res = await fetch(`/InventoryMgmt/GetInventory?${p}`);
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const json = await res.json();
|
|
|
|
s.totalCount = json.recordsTotal ?? 0;
|
|
|
|
// Populate department dropdown from API response
|
|
if (json.departmentList) depDropdown.setItems(json.departmentList);
|
|
|
|
renderCards(json.data ?? []);
|
|
|
|
H.renderPagination(pageBtns, pageInfo, s, pg => {
|
|
s.page = pg;
|
|
fetchData();
|
|
});
|
|
|
|
if (countEl) countEl.textContent =
|
|
`${s.totalCount.toLocaleString()} result${s.totalCount !== 1 ? "s" : ""}`;
|
|
|
|
} catch (err) {
|
|
console.error("GetInventory error:", err);
|
|
grid.innerHTML = `<div class="inv-state" style="grid-column:1/-1">
|
|
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
|
|
<p>Failed to load data.</p></div>`;
|
|
}
|
|
}
|
|
|
|
function renderCards(data) {
|
|
if (!data.length) {
|
|
grid.innerHTML = `
|
|
<div class="inv-state" style="grid-column:1/-1">
|
|
<i class="fas fa-inbox"></i>
|
|
<p>No records found.</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = data.map(item => buildInventoryCardHtml(item)).join("");
|
|
|
|
// Wire Transact buttons
|
|
grid.querySelectorAll(".btn-transact").forEach(btn => {
|
|
|
|
btn.addEventListener("click", () => {
|
|
const inventoryId = parseInt(btn.dataset.inventoryid, 10);
|
|
TransactModal.open(inventoryId, () => fetchData());
|
|
});
|
|
});
|
|
}
|
|
function buildInventoryCardHtml(item) {
|
|
const H = window.InventoryHelpers;
|
|
const qtyIn = item.qtyIn ?? 0;
|
|
const qtyOut = item.qtyOut ?? 0;
|
|
const qtyOnHand = item.qtyOnHand ?? 0;
|
|
|
|
// Stock level % for progress bar (relative to qtyIn; floor at 0)
|
|
const pct = qtyIn > 0 ? Math.max(0, Math.min(100, Math.round((qtyOnHand / qtyIn) * 100))) : 0;
|
|
const isLow = pct < 20;
|
|
|
|
// Status badge
|
|
const statusHtml = qtyOnHand <= 0
|
|
? `<span class="inv-stock-badge inv-stock-empty">Out of stock</span>`
|
|
: isLow
|
|
? `<span class="inv-stock-badge inv-stock-low">Low stock</span>`
|
|
: `<span class="inv-stock-badge inv-stock-ok">In stock</span>`;
|
|
|
|
return `
|
|
<div class="inv-card">
|
|
|
|
<!-- HEAD -->
|
|
<div class="inv-card-hd">
|
|
<div style="display:flex;align-items:flex-start;gap:10px;flex:1;min-width:0">
|
|
<div class="inv-card-icon-wrap ${isLow ? "inv-card-icon-low" : ""}">
|
|
<i class="fas fa-box${qtyOnHand <= 0 ? "-open" : ""}"></i>
|
|
</div>
|
|
<div style="flex:1;min-width:0">
|
|
<div class="inv-card-code">PR NO #${(item.prNo ?? "—")}</div>
|
|
<div class="inv-card-code">ITEM NO #${H.escHtml(String(item.itemNo ?? "—"))}</div>
|
|
<div class="inv-card-name">${H.escHtml(item.itemName ?? "—")}</div>
|
|
<div class="inv-card-meta-row">
|
|
<span><i class="fas fa-building"></i> ${H.escHtml(item.department ?? "—")}</span>
|
|
<span><i class="fas fa-tag"></i> ${H.escHtml(item.itemCategoryName ?? "—")}</span>
|
|
</div>
|
|
<div class="inv-card-meta-row">
|
|
<span><i class="fas fa-clock"></i> ${_formatDate(item.createdDate)}</span>
|
|
${item.lotNo ? `<span><i class="fas fa-barcode"></i> ${H.escHtml(item.lotNo)}</span>` : ""}
|
|
</div>
|
|
<div class="inv-card-sub">
|
|
<i class="fas fa-qrcode"></i>
|
|
${(item.projectCode ?? "—")}
|
|
</div>
|
|
|
|
</div>
|
|
${statusHtml}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- STATS GRID -->
|
|
<div class="inv-stats-grid">
|
|
<div class="inv-stat-cell inv-stat-in">
|
|
<span class="inv-stat-lbl"><i class="fas fa-arrow-circle-down"></i> QTY IN</span>
|
|
<span class="inv-stat-val">${_fmtNum(qtyIn)}</span>
|
|
</div>
|
|
<div class="inv-stat-cell inv-stat-out">
|
|
<span class="inv-stat-lbl"><i class="fas fa-arrow-circle-up"></i> QTY OUT</span>
|
|
<span class="inv-stat-val">${_fmtNum(qtyOut)}</span>
|
|
</div>
|
|
<div class="inv-stat-cell inv-stat-hand">
|
|
<span class="inv-stat-lbl"><i class="fas fa-layer-group"></i> ON HAND</span>
|
|
<span class="inv-stat-val">${_fmtNum(qtyOnHand)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PROGRESS BAR -->
|
|
<div class="inv-progress-wrap">
|
|
<div class="inv-progress-labels">
|
|
<span>Stock level</span>
|
|
<span>${pct}%</span>
|
|
</div>
|
|
<div class="inv-progress-track">
|
|
<div class="inv-progress-fill ${isLow ? "inv-progress-low" : ""}"
|
|
style="width:${pct}%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ITEM ROW -->
|
|
${item.itemDescription ? `
|
|
<div class="inv-item-desc-row">
|
|
<span class="inv-item-desc-lbl"><i class="fas fa-info-circle"></i> ITEM</span>
|
|
<span class="inv-item-desc-name">${H.escHtml(item.itemName ?? "—")}</span>
|
|
<span class="inv-item-desc-qty">
|
|
<i class="fas fa-cubes"></i> ${_fmtNum(qtyIn)}
|
|
</span>
|
|
</div>` : ""}
|
|
|
|
<!-- FOOTER -->
|
|
<div class="inv-card-ft">
|
|
<button class="inv-btn inv-btn-primary btn-transact"
|
|
data-inventoryid="${item.inventoryId}">
|
|
<i class="fas fa-pen"></i> Transact
|
|
</button>
|
|
</div>
|
|
|
|
</div>`;
|
|
}
|
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
function _fmtNum(n) {
|
|
const v = parseFloat(n);
|
|
return isNaN(v) ? "—" : v.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
}
|
|
|
|
function _formatDate(raw) {
|
|
if (!raw) return "—";
|
|
const d = new Date(raw);
|
|
return isNaN(d) ? raw : d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
}
|
|
fetchData();
|
|
})();
|
|
</script> |