1071 lines
44 KiB
Plaintext
1071 lines
44 KiB
Plaintext
@* ── FILTER BAR ── *@
|
|
<div class="inv-filters">
|
|
<div class="inv-search-box">
|
|
<i class="fas fa-hashtag"></i>
|
|
<input type="text" id="ris-srchRISNo" placeholder="RIS Number..." />
|
|
</div>
|
|
<div class="inv-search-box">
|
|
<i class="fas fa-box"></i>
|
|
<input type="text" id="ris-srchItemName" placeholder="Item Name..." />
|
|
</div>
|
|
<div class="inv-search-box">
|
|
<i class="fas fa-user"></i>
|
|
<input type="text" id="ris-srchIssuedTo" placeholder="Project Name..." />
|
|
</div>
|
|
|
|
@* ── Discipline dropdown ── *@
|
|
<div class="inv-department-wrap" id="ris-disciplineWrap">
|
|
<div class="inv-dep-trigger">
|
|
<span class="inv-dep-left">
|
|
<i class="fas fa-tools"></i>
|
|
<span class="inv-dep-lbl">All Trades</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 trades..." autocomplete="off" />
|
|
</div>
|
|
<div class="inv-dep-list">
|
|
<div class="inv-dep-opt active" data-value="">
|
|
<i class="fas fa-th-large"></i> All Trades
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@* ── Status filter ── *@
|
|
<div class="inv-department-wrap" id="ris-statusWrap" style="max-width:160px">
|
|
<div class="inv-dep-trigger">
|
|
<span class="inv-dep-left">
|
|
<i class="fas fa-circle-dot"></i>
|
|
<span class="inv-dep-lbl">All Status</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 status..." autocomplete="off" />
|
|
</div>
|
|
<div class="inv-dep-list">
|
|
<div class="inv-dep-opt active" data-value="">
|
|
<i class="fas fa-th-large"></i> All Status
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="inv-filter-right">
|
|
<span class="inv-pgsz-lbl">Show</span>
|
|
<select class="inv-pgsz-sel" id="ris-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>
|
|
</select>
|
|
<span class="inv-result-count" id="ris-resultCount">0 results</span>
|
|
</div>
|
|
</div>
|
|
|
|
@* ── CARD GRID ── *@
|
|
<div class="ris-grid" id="ris-grid">
|
|
<div class="inv-state">
|
|
<div class="inv-spinner"></div>
|
|
<p>Loading…</p>
|
|
</div>
|
|
</div>
|
|
|
|
@* ── PAGINATION ── *@
|
|
<div class="inv-pagination">
|
|
<span class="inv-pg-info" id="ris-pageInfo"></span>
|
|
<div class="inv-pg-btns" id="ris-pageButtons"></div>
|
|
</div>
|
|
|
|
@* ── APPROVE CONFIRMATION MODAL ── *@
|
|
<div id="ris-approve-modal-overlay" style="display:none;position:fixed;inset:0;
|
|
z-index:1080;background:rgba(0,0,0,.45);
|
|
display:none;align-items:center;justify-content:center;padding:16px">
|
|
<div style="background:var(--card-bg,#fff);border-radius:14px;
|
|
border:1px solid var(--border,#d6eaec);width:100%;max-width:440px;
|
|
overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.2)">
|
|
<div style="background:linear-gradient(135deg,#0d5c63,#0e7c86);
|
|
padding:16px 18px;display:flex;align-items:center;gap:12px">
|
|
<div style="width:38px;height:38px;border-radius:10px;
|
|
background:rgba(255,255,255,.15);
|
|
display:flex;align-items:center;justify-content:center">
|
|
<i class="fas fa-check-circle" style="color:#fff;font-size:16px"></i>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:14px;font-weight:600;color:#fff">Approve RIS</div>
|
|
<div id="ris-approve-subtitle"
|
|
style="font-size:11px;color:rgba(255,255,255,.65);margin-top:2px">
|
|
Loading…
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style="padding:20px 18px">
|
|
<div id="ris-approve-detail"
|
|
style="background:var(--teal-pale,#e6f7f8);border:1px solid var(--border,#d6eaec);
|
|
border-radius:10px;padding:14px 16px;margin-bottom:16px;
|
|
display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
</div>
|
|
<p style="font-size:13px;color:var(--text-dark,#1a2e35);line-height:1.6">
|
|
Approving this slip will confirm the issuance and update the inventory.
|
|
<strong>This action cannot be undone.</strong>
|
|
</p>
|
|
</div>
|
|
<div style="padding:12px 18px;border-top:1px solid var(--border,#d6eaec);
|
|
background:var(--bg-page,#f0f6f7);
|
|
display:flex;align-items:center;justify-content:flex-end;gap:8px">
|
|
<button id="ris-approve-cancel-btn" class="tm-btn-cancel">Cancel</button>
|
|
<button id="ris-approve-confirm-btn"
|
|
style="padding:8px 20px;border-radius:8px;border:none;
|
|
background:#0e7c86;color:#fff;font-family:'DM Sans',sans-serif;
|
|
font-size:.85rem;font-weight:600;cursor:pointer;
|
|
display:flex;align-items:center;gap:7px">
|
|
<i class="fas fa-check"></i> Approve RIS
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@* ── CANCEL CONFIRMATION MODAL ── *@
|
|
<div id="ris-cancel-modal-overlay" style="display:none;position:fixed;inset:0;
|
|
z-index:1080;background:rgba(0,0,0,.45);
|
|
display:none;align-items:center;justify-content:center;padding:16px">
|
|
<div style="background:var(--card-bg,#fff);border-radius:14px;
|
|
border:1px solid var(--border,#d6eaec);width:100%;max-width:440px;
|
|
overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.2)">
|
|
<div style="background:linear-gradient(135deg,#7f1d1d,#b91c1c);
|
|
padding:16px 18px;display:flex;align-items:center;gap:12px">
|
|
<div style="width:38px;height:38px;border-radius:10px;
|
|
background:rgba(255,255,255,.15);
|
|
display:flex;align-items:center;justify-content:center">
|
|
<i class="fas fa-ban" style="color:#fff;font-size:16px"></i>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:14px;font-weight:600;color:#fff">Cancel RIS</div>
|
|
<div id="ris-cancel-subtitle"
|
|
style="font-size:11px;color:rgba(255,255,255,.65);margin-top:2px">
|
|
Loading…
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style="padding:20px 18px">
|
|
<div id="ris-cancel-detail"
|
|
style="background:#fef2f2;border:1px solid #fecaca;
|
|
border-radius:10px;padding:14px 16px;margin-bottom:16px;
|
|
display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
</div>
|
|
<div style="display:flex;gap:8px;padding:10px 12px;background:#fef2f2;
|
|
border:1px solid #fecaca;border-radius:8px;margin-bottom:12px">
|
|
<i class="fas fa-exclamation-triangle"
|
|
style="color:#dc2626;flex-shrink:0;margin-top:2px"></i>
|
|
<p style="font-size:12px;color:#7f1d1d;line-height:1.6;margin:0">
|
|
Cancelling this slip will <strong>reverse the inventory deduction</strong>
|
|
and restore the qty back to on-hand. This cannot be undone.
|
|
</p>
|
|
</div>
|
|
<div class="tm-form-group">
|
|
<label class="tm-label">
|
|
<i class="fas fa-sticky-note"></i> Reason for cancellation
|
|
<span class="tm-req">*</span>
|
|
</label>
|
|
<input id="ris-cancel-reason" class="tm-input"
|
|
type="text" placeholder="Enter reason…" maxlength="500">
|
|
</div>
|
|
</div>
|
|
<div style="padding:12px 18px;border-top:1px solid var(--border,#d6eaec);
|
|
background:var(--bg-page,#f0f6f7);
|
|
display:flex;align-items:center;justify-content:flex-end;gap:8px">
|
|
<button id="ris-cancel-dismiss-btn" class="tm-btn-cancel">Dismiss</button>
|
|
<button id="ris-cancel-confirm-btn"
|
|
style="padding:8px 20px;border-radius:8px;border:none;
|
|
background:#dc2626;color:#fff;font-family:'DM Sans',sans-serif;
|
|
font-size:.85rem;font-weight:600;cursor:pointer;
|
|
display:flex;align-items:center;gap:7px">
|
|
<i class="fas fa-ban"></i> Cancel RIS
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<style>
|
|
/* ── RIS card grid ── */
|
|
.ris-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
gap: 16px;
|
|
min-height: 220px;
|
|
}
|
|
|
|
@@media (max-width: 768px) { .ris-grid { grid-template-columns: 1fr; } }
|
|
@@media (min-width: 769px) and (max-width: 1100px) {
|
|
.ris-grid { grid-template-columns: repeat(2, 1fr); }
|
|
}
|
|
|
|
/* ── RIS card ── */
|
|
.ris-card {
|
|
background: var(--card-bg, #fff);
|
|
border-radius: var(--radius-lg, 14px);
|
|
box-shadow: var(--shadow-card);
|
|
border: 1px solid var(--border, #d6eaec);
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
transition: box-shadow .25s, transform .25s;
|
|
}
|
|
.ris-card:hover {
|
|
box-shadow: var(--shadow-hover);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
/* ── Card header ── */
|
|
.ris-card-hd {
|
|
background: linear-gradient(135deg, var(--teal-dark, #0d5c63), var(--teal-mid, #0e7c86));
|
|
padding: 14px 16px 12px;
|
|
position: relative;
|
|
}
|
|
.ris-card-no {
|
|
font-size: .7rem;
|
|
color: rgba(255,255,255,.6);
|
|
font-weight: 700;
|
|
letter-spacing: .07em;
|
|
text-transform: uppercase;
|
|
margin-bottom: 3px;
|
|
}
|
|
.ris-card-title {
|
|
font-family: 'Space Grotesk', sans-serif;
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
line-height: 1.25;
|
|
word-break: break-word;
|
|
margin-bottom: 6px;
|
|
}
|
|
.ris-card-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
font-size: 11px;
|
|
color: rgba(255,255,255,.65);
|
|
}
|
|
.ris-card-meta span {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
/* ── Status badge positioned top-right ── */
|
|
.ris-status-badge {
|
|
position: absolute;
|
|
top: 12px;
|
|
right: 14px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
padding: 3px 10px;
|
|
border-radius: 50px;
|
|
letter-spacing: .05em;
|
|
text-transform: uppercase;
|
|
}
|
|
.ris-status-draft { background: rgba(255,255,255,.18); color: #fff; }
|
|
.ris-status-approved { background: #dcfce7; color: #166534; }
|
|
.ris-status-cancelled { background: #fee2e2; color: #991b1b; }
|
|
|
|
/* ── Stats row ── */
|
|
.ris-stats-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 1px;
|
|
background: var(--border, #d6eaec);
|
|
border-bottom: 1px solid var(--border, #d6eaec);
|
|
}
|
|
.ris-stat {
|
|
background: var(--card-bg, #fff);
|
|
padding: 10px 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
.ris-stat-lbl {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: .05em;
|
|
color: var(--text-muted, #6b8890);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.ris-stat-lbl i { font-size: 11px; }
|
|
.ris-stat-val {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--text-dark, #1a2e35);
|
|
line-height: 1.2;
|
|
}
|
|
.ris-stat-val.teal { color: var(--teal-mid, #0e7c86); }
|
|
.ris-stat-val.red { color: #dc2626; }
|
|
.ris-stat-val.amber { color: #92400e; }
|
|
|
|
/* ── Body ── */
|
|
.ris-card-body {
|
|
padding: 12px 16px;
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.ris-field-row {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
font-size: 12px;
|
|
}
|
|
.ris-field-lbl {
|
|
min-width: 80px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: .05em;
|
|
color: var(--text-muted, #6b8890);
|
|
padding-top: 1px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
flex-shrink: 0;
|
|
}
|
|
.ris-field-lbl i { font-size: 11px; }
|
|
.ris-field-val {
|
|
color: var(--text-dark, #1a2e35);
|
|
line-height: 1.5;
|
|
word-break: break-word;
|
|
}
|
|
|
|
/* ── Discipline chip ── */
|
|
.ris-discipline-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 3px 10px;
|
|
border-radius: 50px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
background: var(--teal-pale, #e6f7f8);
|
|
color: var(--teal-dark, #0d5c63);
|
|
border: 1px solid var(--border, #d6eaec);
|
|
}
|
|
|
|
/* ── Footer ── */
|
|
.ris-card-ft {
|
|
padding: 10px 16px 14px;
|
|
display: flex;
|
|
gap: 8px;
|
|
border-top: 1px solid var(--border, #d6eaec);
|
|
}
|
|
.ris-btn {
|
|
flex: 1;
|
|
padding: 8px 12px;
|
|
border-radius: var(--radius-sm, 8px);
|
|
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;
|
|
}
|
|
.ris-btn-approve {
|
|
background: var(--teal-mid, #0e7c86);
|
|
color: #fff;
|
|
}
|
|
.ris-btn-approve:hover { background: var(--teal-dark, #0d5c63); }
|
|
.ris-btn-approve:disabled { opacity: .4; cursor: default; }
|
|
|
|
.ris-btn-cancel {
|
|
background: transparent;
|
|
color: #dc2626;
|
|
border: 1.5px solid #fca5a5;
|
|
}
|
|
.ris-btn-cancel:hover { background: #fef2f2; border-color: #dc2626; }
|
|
.ris-btn-cancel:disabled { opacity: .4; cursor: default; }
|
|
|
|
/* ── Approve modal detail field ── */
|
|
.am-field { display: flex; flex-direction: column; gap: 3px; }
|
|
.am-lbl {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: .05em;
|
|
color: var(--text-muted, #6b8890);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.am-lbl i { font-size: 11px; }
|
|
.am-val {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--teal-dark, #0d5c63);
|
|
}
|
|
/* ── Report trigger button ── */
|
|
.ris-report-trigger {
|
|
padding: 7px 14px;
|
|
border-radius: var(--radius-sm, 8px);
|
|
border: 1.5px solid var(--teal-mid, #0e7c86);
|
|
background: var(--teal-pale, #e6f7f8);
|
|
color: var(--teal-dark, #0d5c63);
|
|
font-family: 'DM Sans', sans-serif;
|
|
font-size: .82rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
transition: all .2s;
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.ris-report-trigger:hover {
|
|
background: var(--teal-mid, #0e7c86);
|
|
color: #fff;
|
|
}
|
|
|
|
/* ── Preset chips ── */
|
|
.ris-rep-presets {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.ris-preset {
|
|
padding: 6px 12px;
|
|
border-radius: 50px;
|
|
border: 1.5px solid var(--border, #d6eaec);
|
|
background: var(--card-bg, #fff);
|
|
color: var(--text-muted, #6b8890);
|
|
font-family: 'DM Sans', sans-serif;
|
|
font-size: .78rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all .2s;
|
|
}
|
|
|
|
.ris-preset:hover {
|
|
border-color: var(--teal-mid, #0e7c86);
|
|
}
|
|
|
|
.ris-preset.active {
|
|
background: var(--teal-mid, #0e7c86);
|
|
color: #fff;
|
|
border-color: var(--teal-mid, #0e7c86);
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
(function () {
|
|
"use strict";
|
|
const H = window.InventoryHelpers;
|
|
|
|
// ── State ──────────────────────────────────────────────────────────────
|
|
const s = {
|
|
page: 1, pageSize: 12, totalCount: 0,
|
|
searchRISNo: "", searchItemName: "", searchIssuedTo: "",
|
|
discipline: "", status: "",
|
|
timer: null
|
|
};
|
|
|
|
// ── Elements ───────────────────────────────────────────────────────────
|
|
const grid = document.getElementById("ris-grid");
|
|
const countEl = document.getElementById("ris-resultCount");
|
|
const pageInfo = document.getElementById("ris-pageInfo");
|
|
const pageBtns = document.getElementById("ris-pageButtons");
|
|
const inRISNo = document.getElementById("ris-srchRISNo");
|
|
const inItem = document.getElementById("ris-srchItemName");
|
|
const inIssued = document.getElementById("ris-srchIssuedTo");
|
|
const inSize = document.getElementById("ris-pageSize");
|
|
|
|
const discWrap = document.getElementById("ris-disciplineWrap");
|
|
const statusWrap = document.getElementById("ris-statusWrap");
|
|
|
|
// ── Guard ──────────────────────────────────────────────────────────────
|
|
if (!grid || !discWrap) {
|
|
console.error("RIS tab init failed — missing elements.");
|
|
return;
|
|
}
|
|
|
|
// ── Discipline dropdown ────────────────────────────────────────────────
|
|
const discDropdown = H.initSearchDropdown(discWrap, val => {
|
|
s.discipline = val;
|
|
s.page = 1;
|
|
fetchData();
|
|
}, {
|
|
cssPrefix: "inv-dep",
|
|
allLabel: "All Trades",
|
|
allIcon: "fas fa-th-large",
|
|
itemIcon: "fas fa-tools"
|
|
});
|
|
|
|
// ── Status dropdown (static options) ──────────────────────────────────
|
|
const statusDropdown = H.initSearchDropdown(statusWrap, val => {
|
|
s.status = val;
|
|
s.page = 1;
|
|
fetchData();
|
|
}, {
|
|
cssPrefix: "inv-dep",
|
|
allLabel: "All Status",
|
|
allIcon: "fas fa-th-large",
|
|
itemIcon: "fas fa-circle-dot"
|
|
});
|
|
// Seed static status options immediately
|
|
statusDropdown.setItems(["Draft", "Approved", "Cancelled"]);
|
|
|
|
// ── Search inputs ──────────────────────────────────────────────────────
|
|
[inRISNo, inItem, inIssued].forEach(el => {
|
|
if (!el) return;
|
|
el.addEventListener("input", () => {
|
|
clearTimeout(s.timer);
|
|
s.timer = setTimeout(() => {
|
|
s.searchRISNo = inRISNo?.value.trim() ?? "";
|
|
s.searchItemName = inItem?.value.trim() ?? "";
|
|
s.searchIssuedTo = inIssued?.value.trim() ?? "";
|
|
s.page = 1;
|
|
fetchData();
|
|
}, 350);
|
|
});
|
|
});
|
|
|
|
if (inSize) {
|
|
inSize.addEventListener("change", () => {
|
|
s.pageSize = parseInt(inSize.value, 10);
|
|
s.page = 1;
|
|
fetchData();
|
|
});
|
|
}
|
|
|
|
// ── Status value → short code for API ─────────────────────────────────
|
|
function statusToCode(label) {
|
|
return { "Draft": "0", "Approved": "1", "Cancelled": "2" }[label] ?? "";
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
// FETCH
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
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({
|
|
searchRISNo: s.searchRISNo,
|
|
searchItemName: s.searchItemName,
|
|
searchIssuedTo: s.searchIssuedTo,
|
|
discipline: s.discipline,
|
|
status: statusToCode(s.status),
|
|
pageNumber: s.page,
|
|
pageSize: s.pageSize,
|
|
draw: Date.now()
|
|
});
|
|
|
|
try {
|
|
const res = await fetch(`/RISMgmt/GetRIS?${p}`);
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const json = await res.json();
|
|
|
|
const data = json.data ?? json;
|
|
s.totalCount = json.recordsTotal ?? 0;
|
|
|
|
if (json.disciplineList?.length)
|
|
discDropdown.setItems(json.disciplineList.map(d => d.disciplineName));
|
|
renderCards(json.data ?? []);
|
|
|
|
H.renderPagination(
|
|
document.getElementById("ris-pageButtons"),
|
|
document.getElementById("ris-pageInfo"),
|
|
s,
|
|
pg => { s.page = pg; fetchData(); }
|
|
);
|
|
|
|
const c = document.getElementById("ris-resultCount");
|
|
if (c) c.textContent =
|
|
`${s.totalCount.toLocaleString()} result${s.totalCount !== 1 ? "s" : ""}`;
|
|
|
|
} catch (err) {
|
|
console.error("GetRIS 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>`;
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
// RENDER CARDS
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
function renderCards(data) {
|
|
const g = document.getElementById("ris-grid");
|
|
if (!data.length) {
|
|
g.innerHTML = `<div class="inv-state" style="grid-column:1/-1">
|
|
<i class="fas fa-inbox"></i>
|
|
<p>No RIS records found.</p></div>`;
|
|
return;
|
|
}
|
|
|
|
g.innerHTML = data.map(item => buildRISCardHtml(item)).join("");
|
|
|
|
// ── Wire Approve buttons ──
|
|
g.querySelectorAll(".btn-ris-approve").forEach(btn =>
|
|
btn.addEventListener("click", () => {
|
|
const risId = parseInt(btn.dataset.risid, 10);
|
|
const risNo = btn.dataset.risno;
|
|
openApproveModal(risId, risNo, btn.dataset);
|
|
})
|
|
);
|
|
|
|
// ── Wire Cancel buttons ──
|
|
g.querySelectorAll(".btn-ris-cancel").forEach(btn =>
|
|
btn.addEventListener("click", () => {
|
|
const risId = parseInt(btn.dataset.risid, 10);
|
|
const risNo = btn.dataset.risno;
|
|
openCancelModal(risId, risNo, btn.dataset);
|
|
})
|
|
);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
// CARD HTML BUILDER
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
function buildRISCardHtml(item) {
|
|
const risId = item.risId ?? item.RISId ?? 0;
|
|
const risNo = item.risNo ?? item.RISNo ?? "—";
|
|
const itemName = item.itemName ?? item.ItemName ?? "—";
|
|
const itemNo = item.itemNo ?? item.ItemNo ?? "—";
|
|
const issuedTo = item.projectCode ?? item.projectName ?? "—";
|
|
const discipline = item.disciplineName ?? item.DisciplineName ?? "—";
|
|
const qtyIssued = item.qtyIssued ?? item.QtyIssued ?? 0;
|
|
const totalReturned= item.totalReturned ?? item.TotalReturned ?? 0;
|
|
const netIssued = qtyIssued - totalReturned;
|
|
const status = item.status ?? item.Status ?? 0;
|
|
const statusLabel = item.statusLabel ?? item.StatusLabel ?? _statusLabel(status);
|
|
const remarks = item.remarks ?? item.Remarks ?? null;
|
|
const createdDate = item.createdDate ?? item.CreatedDate ?? null;
|
|
const approvedBy = item.approvedBy ?? item.ApprovedBy ?? null;
|
|
const approvedDate = item.approvedDate ?? item.ApprovedDate ?? null;
|
|
const mrsCount = item.mrsCount ?? item.MRSCount ?? 0;
|
|
|
|
const isDraft = status === 0;
|
|
const isApproved = status === 1;
|
|
const isCancelled = status === 2;
|
|
|
|
const statusCls = isDraft ? "ris-status-draft"
|
|
: isApproved ? "ris-status-approved"
|
|
: "ris-status-cancelled";
|
|
|
|
const statusIcon = isDraft ? "fas fa-clock"
|
|
: isApproved ? "fas fa-check-circle"
|
|
: "fas fa-ban";
|
|
|
|
return `
|
|
<div class="ris-card">
|
|
|
|
<!-- HEAD -->
|
|
<div class="ris-card-hd">
|
|
<span class="${statusCls} ris-status-badge">
|
|
<i class="${statusIcon}"></i> ${H.escHtml(statusLabel)}
|
|
</span>
|
|
<div class="ris-card-no">RIS NO</div>
|
|
<div class="ris-card-title">${H.escHtml(risNo)}</div>
|
|
<div class="ris-card-meta">
|
|
<span><i class="fas fa-box"></i> ${H.escHtml(itemName)}</span>
|
|
<span><i class="fas fa-hashtag"></i> #${H.escHtml(String(itemNo))}</span>
|
|
</div>
|
|
<div class="ris-card-meta" style="margin-top:4px">
|
|
<span><i class="fas fa-user"></i> ${H.escHtml(issuedTo)}</span>
|
|
<span><i class="fas fa-clock"></i> ${_formatDate(createdDate)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- STATS -->
|
|
<div class="ris-stats-row">
|
|
<div class="ris-stat">
|
|
<span class="ris-stat-lbl">
|
|
<i class="fas fa-cubes"></i> Qty Issued
|
|
</span>
|
|
<span class="ris-stat-val teal">${qtyIssued}</span>
|
|
</div>
|
|
<div class="ris-stat">
|
|
<span class="ris-stat-lbl">
|
|
<i class="fas fa-undo"></i> Returned
|
|
</span>
|
|
<span class="ris-stat-val amber">${totalReturned}</span>
|
|
</div>
|
|
<div class="ris-stat">
|
|
<span class="ris-stat-lbl">
|
|
<i class="fas fa-layer-group"></i> Net Issued
|
|
</span>
|
|
<span class="ris-stat-val ${netIssued > 0 ? "red" : ""}">${netIssued}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- BODY -->
|
|
<div class="ris-card-body">
|
|
<div class="ris-field-row">
|
|
<span class="ris-field-lbl">
|
|
<i class="fas fa-tools"></i> Trade
|
|
</span>
|
|
<span class="ris-field-val">
|
|
<span class="ris-discipline-chip">
|
|
<i class="fas fa-tools"></i> ${H.escHtml(discipline)}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
|
|
${mrsCount > 0 ? `
|
|
<div class="ris-field-row">
|
|
<span class="ris-field-lbl">
|
|
<i class="fas fa-file-import"></i> MRS Count
|
|
</span>
|
|
<span class="ris-field-val">
|
|
<span style="background:#E6F1FB;color:#185FA5;padding:2px 8px;
|
|
border-radius:50px;font-size:11px;font-weight:600">
|
|
${mrsCount} return slip${mrsCount !== 1 ? "s" : ""}
|
|
</span>
|
|
</span>
|
|
</div>` : ""}
|
|
|
|
${remarks ? `
|
|
<div class="ris-field-row">
|
|
<span class="ris-field-lbl">
|
|
<i class="fas fa-sticky-note"></i> Remarks
|
|
</span>
|
|
<span class="ris-field-val" style="color:var(--text-muted,#6b8890)">
|
|
${H.escHtml(remarks)}
|
|
</span>
|
|
</div>` : ""}
|
|
|
|
${isApproved && approvedBy ? `
|
|
<div class="ris-field-row">
|
|
<span class="ris-field-lbl">
|
|
<i class="fas fa-user-check"></i> Approved by
|
|
</span>
|
|
<span class="ris-field-val">
|
|
${H.escHtml(approvedBy)}
|
|
<span style="color:var(--text-muted,#6b8890);font-size:11px;margin-left:4px">
|
|
${_formatDate(approvedDate)}
|
|
</span>
|
|
</span>
|
|
</div>` : ""}
|
|
</div>
|
|
|
|
<!-- FOOTER -->
|
|
<div class="ris-card-ft">
|
|
${isDraft ? `
|
|
<button class="ris-btn ris-btn-approve btn-ris-approve"
|
|
data-risid="${risId}"
|
|
data-risno="${H.escAttr(risNo)}"
|
|
data-itemname="${H.escAttr(itemName)}"
|
|
data-issuedto="${H.escAttr(issuedTo)}"
|
|
data-discipline="${H.escAttr(discipline)}"
|
|
data-qtyissued="${qtyIssued}">
|
|
<i class="fas fa-check-circle"></i> Approve
|
|
</button>
|
|
<button class="ris-btn ris-btn-cancel btn-ris-cancel"
|
|
data-risid="${risId}"
|
|
data-risno="${H.escAttr(risNo)}"
|
|
data-itemname="${H.escAttr(itemName)}"
|
|
data-issuedto="${H.escAttr(issuedTo)}"
|
|
data-discipline="${H.escAttr(discipline)}"
|
|
data-qtyissued="${qtyIssued}">
|
|
<i class="fas fa-ban"></i> Cancel
|
|
</button>` : `
|
|
<button class="ris-btn ris-btn-cancel btn-ris-cancel"
|
|
${isCancelled ? "disabled" : ""}
|
|
data-risid="${risId}"
|
|
data-risno="${H.escAttr(risNo)}"
|
|
data-itemname="${H.escAttr(itemName)}"
|
|
data-issuedto="${H.escAttr(issuedTo)}"
|
|
data-discipline="${H.escAttr(discipline)}"
|
|
data-qtyissued="${qtyIssued}"
|
|
style="flex:1">
|
|
<i class="fas fa-ban"></i>
|
|
${isCancelled ? "Cancelled" : "Cancel"}
|
|
</button>`}
|
|
</div>
|
|
|
|
</div>`;
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
// APPROVE MODAL
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
function openApproveModal(risId, risNo, dataset) {
|
|
const overlay = document.getElementById("ris-approve-modal-overlay");
|
|
document.getElementById("ris-approve-subtitle").textContent =
|
|
`${risNo} — ${dataset.itemname ?? ""}`;
|
|
|
|
document.getElementById("ris-approve-detail").innerHTML = `
|
|
<div class="am-field">
|
|
<span class="am-lbl"><i class="fas fa-hashtag"></i> RIS No</span>
|
|
<span class="am-val">${H.escHtml(risNo)}</span>
|
|
</div>
|
|
<div class="am-field">
|
|
<span class="am-lbl"><i class="fas fa-cubes"></i> Qty Issued</span>
|
|
<span class="am-val">${dataset.qtyissued ?? 0} pcs</span>
|
|
</div>
|
|
<div class="am-field">
|
|
<span class="am-lbl"><i class="fas fa-box"></i> Item</span>
|
|
<span class="am-val">${H.escHtml(dataset.itemname ?? "—")}</span>
|
|
</div>
|
|
<div class="am-field">
|
|
<span class="am-lbl"><i class="fas fa-user"></i> Issued To</span>
|
|
<span class="am-val">${H.escHtml(dataset.issuedto ?? "—")}</span>
|
|
</div>
|
|
<div class="am-field">
|
|
<span class="am-lbl"><i class="fas fa-tools"></i> Discipline</span>
|
|
<span class="am-val">${H.escHtml(dataset.discipline ?? "—")}</span>
|
|
</div>`;
|
|
|
|
overlay.style.display = "flex";
|
|
|
|
// ── Wire buttons fresh each open ──
|
|
const confirmBtn = document.getElementById("ris-approve-confirm-btn");
|
|
const cancelBtn = document.getElementById("ris-approve-cancel-btn");
|
|
|
|
const closeModal = () => { overlay.style.display = "none"; };
|
|
|
|
// Clone to remove stale listeners
|
|
const newConfirm = confirmBtn.cloneNode(true);
|
|
const newCancel = cancelBtn.cloneNode(true);
|
|
confirmBtn.replaceWith(newConfirm);
|
|
cancelBtn.replaceWith(newCancel);
|
|
|
|
newCancel.addEventListener("click", closeModal);
|
|
overlay.addEventListener("click", e => { if (e.target === overlay) closeModal(); });
|
|
|
|
newConfirm.addEventListener("click", async () => {
|
|
newConfirm.disabled = true;
|
|
newConfirm.innerHTML = `<i class="fas fa-spinner fa-spin"></i> Approving…`;
|
|
|
|
try {
|
|
const res = await fetch(`/RISMgmt/ApproveRIS`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ risId })
|
|
});
|
|
const json = await res.json();
|
|
const d = json.data ?? json;
|
|
|
|
if (!res.ok || !d.success) {
|
|
showToast("error", d.message ?? "Could not approve RIS.", "Failed", 4000);
|
|
return;
|
|
}
|
|
|
|
showToast("success", d.message ?? `RIS ${risNo} approved.`, "Approved!", 3500);
|
|
closeModal();
|
|
fetchData();
|
|
|
|
} catch (err) {
|
|
console.error("ApproveRIS error:", err);
|
|
showToast("error", "Request failed. Please try again.", "Error", 4000);
|
|
} finally {
|
|
newConfirm.disabled = false;
|
|
newConfirm.innerHTML = `<i class="fas fa-check"></i> Approve RIS`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
// CANCEL MODAL
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
function openCancelModal(risId, risNo, dataset) {
|
|
const overlay = document.getElementById("ris-cancel-modal-overlay");
|
|
const reasonEl = document.getElementById("ris-cancel-reason");
|
|
reasonEl.value = "";
|
|
reasonEl.classList.remove("error");
|
|
|
|
document.getElementById("ris-cancel-subtitle").textContent =
|
|
`${risNo} — ${dataset.itemname ?? ""}`;
|
|
|
|
document.getElementById("ris-cancel-detail").innerHTML = `
|
|
<div class="am-field">
|
|
<span class="am-lbl"><i class="fas fa-hashtag"></i> RIS No</span>
|
|
<span class="am-val" style="color:#991b1b">${H.escHtml(risNo)}</span>
|
|
</div>
|
|
<div class="am-field">
|
|
<span class="am-lbl"><i class="fas fa-cubes"></i> Qty Issued</span>
|
|
<span class="am-val" style="color:#991b1b">${dataset.qtyissued ?? 0} pcs</span>
|
|
</div>
|
|
<div class="am-field">
|
|
<span class="am-lbl"><i class="fas fa-box"></i> Item</span>
|
|
<span class="am-val">${H.escHtml(dataset.itemname ?? "—")}</span>
|
|
</div>
|
|
<div class="am-field">
|
|
<span class="am-lbl"><i class="fas fa-user"></i> Issued To</span>
|
|
<span class="am-val">${H.escHtml(dataset.issuedto ?? "—")}</span>
|
|
</div>`;
|
|
|
|
overlay.style.display = "flex";
|
|
|
|
const confirmBtn = document.getElementById("ris-cancel-confirm-btn");
|
|
const dismissBtn = document.getElementById("ris-cancel-dismiss-btn");
|
|
|
|
const closeModal = () => { overlay.style.display = "none"; };
|
|
|
|
const newConfirm = confirmBtn.cloneNode(true);
|
|
const newDismiss = dismissBtn.cloneNode(true);
|
|
confirmBtn.replaceWith(newConfirm);
|
|
dismissBtn.replaceWith(newDismiss);
|
|
|
|
newDismiss.addEventListener("click", closeModal);
|
|
overlay.addEventListener("click", e => { if (e.target === overlay) closeModal(); });
|
|
|
|
newConfirm.addEventListener("click", async () => {
|
|
const reason = reasonEl.value.trim();
|
|
if (!reason) {
|
|
reasonEl.classList.add("error");
|
|
reasonEl.focus();
|
|
showToast("warning", "Please enter a reason for cancellation.", "Required", 3000);
|
|
return;
|
|
}
|
|
|
|
newConfirm.disabled = true;
|
|
newConfirm.innerHTML = `<i class="fas fa-spinner fa-spin"></i> Cancelling…`;
|
|
|
|
try {
|
|
const res = await fetch(`/RISMgmt/CancelRIS`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ risId, reason })
|
|
});
|
|
const json = await res.json();
|
|
const d = json.data ?? json;
|
|
|
|
if (!res.ok || !d.success) {
|
|
showToast("error", d.message ?? "Could not cancel RIS.", "Failed", 4000);
|
|
return;
|
|
}
|
|
|
|
showToast("success", d.message ?? `RIS ${risNo} cancelled.`, "Cancelled", 3500);
|
|
closeModal();
|
|
fetchData();
|
|
|
|
} catch (err) {
|
|
console.error("CancelRIS error:", err);
|
|
showToast("error", "Request failed. Please try again.", "Error", 4000);
|
|
} finally {
|
|
newConfirm.disabled = false;
|
|
newConfirm.innerHTML = `<i class="fas fa-ban"></i> Cancel RIS`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
// HELPERS
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
function _statusLabel(code) {
|
|
return { 0: "Draft", 1: "Approved", 2: "Cancelled" }[code] ?? "Unknown";
|
|
}
|
|
|
|
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"
|
|
});
|
|
}
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
// REPORT DATE-RANGE MODAL
|
|
// ══════════════════════════════════════════════════════════════════════
|
|
(function initReportModal() {
|
|
const trigger = document.getElementById("ris-report-btn");
|
|
const overlay = document.getElementById("ris-report-modal-overlay");
|
|
if (!trigger || !overlay) return;
|
|
|
|
const fromEl = document.getElementById("ris-rep-from");
|
|
const toEl = document.getElementById("ris-rep-to");
|
|
const errEl = document.getElementById("ris-rep-err");
|
|
const presets = document.getElementById("ris-rep-presets");
|
|
const dismiss = document.getElementById("ris-rep-dismiss");
|
|
const viewBtn = document.getElementById("ris-rep-view");
|
|
const pdfBtn = document.getElementById("ris-rep-pdf");
|
|
|
|
const fmt = d => d.toISOString().slice(0, 10); // yyyy-MM-dd
|
|
const today = () => new Date();
|
|
|
|
function applyPreset(btn) {
|
|
presets.querySelectorAll(".ris-preset")
|
|
.forEach(b => b.classList.remove("active"));
|
|
btn.classList.add("active");
|
|
|
|
const to = today();
|
|
let from = new Date();
|
|
|
|
if (btn.dataset.month) {
|
|
from = new Date(to.getFullYear(), to.getMonth(), 1); // 1st of month
|
|
} else {
|
|
from.setDate(to.getDate() - parseInt(btn.dataset.days, 10));
|
|
}
|
|
fromEl.value = fmt(from);
|
|
toEl.value = fmt(to);
|
|
errEl.style.display = "none";
|
|
}
|
|
|
|
// Default 15-day range on open
|
|
function openModal() {
|
|
const def = presets.querySelector('[data-days="15"]');
|
|
applyPreset(def);
|
|
overlay.style.display = "flex";
|
|
}
|
|
function closeModal() { overlay.style.display = "none"; }
|
|
|
|
trigger.addEventListener("click", openModal);
|
|
dismiss.addEventListener("click", closeModal);
|
|
overlay.addEventListener("click", e => { if (e.target === overlay) closeModal(); });
|
|
|
|
presets.querySelectorAll(".ris-preset").forEach(b =>
|
|
b.addEventListener("click", () => applyPreset(b)));
|
|
|
|
// Manual edits clear the active preset
|
|
[fromEl, toEl].forEach(el =>
|
|
el.addEventListener("input", () => {
|
|
presets.querySelectorAll(".ris-preset")
|
|
.forEach(b => b.classList.remove("active"));
|
|
errEl.style.display = "none";
|
|
}));
|
|
|
|
function validRange() {
|
|
if (!fromEl.value || !toEl.value) return false;
|
|
if (new Date(fromEl.value) > new Date(toEl.value)) {
|
|
errEl.style.display = "block";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function buildUrl(action) {
|
|
const p = new URLSearchParams({
|
|
dateFrom: fromEl.value,
|
|
dateTo: toEl.value
|
|
});
|
|
return `/RISMgmt/${action}?${p}`;
|
|
}
|
|
|
|
viewBtn.addEventListener("click", () => {
|
|
if (!validRange()) return;
|
|
window.open(buildUrl("RISReport"), "_blank"); // viewer in new tab
|
|
closeModal();
|
|
});
|
|
|
|
pdfBtn.addEventListener("click", () => {
|
|
if (!validRange()) return;
|
|
window.open(buildUrl("RISReportPdf"), "_blank"); // PDF download
|
|
closeModal();
|
|
});
|
|
})();
|
|
// ── Initial load ───────────────────────────────────────────────────────
|
|
fetchData();
|
|
})();
|
|
</script>
|