907 lines
39 KiB
Plaintext
907 lines
39 KiB
Plaintext
<style>
|
|
.tm-type-opt {
|
|
border: 1.5px solid var(--border, #d6eaec);
|
|
border-radius: 12px;
|
|
padding: 12px;
|
|
cursor: pointer;
|
|
transition: all .15s;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.tm-type-opt:hover {
|
|
border-color: var(--teal-mid, #0e7c86);
|
|
background: var(--teal-pale, #e6f7f8);
|
|
}
|
|
|
|
.tm-type-opt.selected {
|
|
border-color: var(--teal-mid, #0e7c86);
|
|
background: var(--teal-pale, #e6f7f8);
|
|
}
|
|
|
|
.tm-type-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.tm-icon-ris {
|
|
background: #e6f7f8;
|
|
color: #0e7c86;
|
|
}
|
|
|
|
.tm-icon-mrs {
|
|
background: #E6F1FB;
|
|
color: #185FA5;
|
|
}
|
|
|
|
.tm-type-opt.selected .tm-icon-ris {
|
|
background: #0e7c86;
|
|
color: #fff;
|
|
}
|
|
|
|
.tm-type-opt.selected .tm-icon-mrs {
|
|
background: #185FA5;
|
|
color: #fff;
|
|
}
|
|
|
|
.tm-stock-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 9px 12px;
|
|
background: var(--teal-pale, #e6f7f8);
|
|
border: 1px solid var(--border, #d6eaec);
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
color: var(--text-dark, #1a2e35);
|
|
}
|
|
|
|
.tm-stock-badge i {
|
|
color: var(--teal-mid, #0e7c86);
|
|
}
|
|
|
|
.tm-stock-badge strong {
|
|
margin-left: auto;
|
|
color: var(--teal-dark, #0d5c63);
|
|
}
|
|
|
|
.tm-form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.tm-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--text-muted, #6b8890);
|
|
text-transform: uppercase;
|
|
letter-spacing: .05em;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.tm-label i {
|
|
font-size: 11px;
|
|
}
|
|
|
|
.tm-req {
|
|
color: #e53e3e;
|
|
}
|
|
|
|
.tm-input {
|
|
padding: 8px 10px;
|
|
border: 1.5px solid var(--border, #d6eaec);
|
|
border-radius: 8px;
|
|
background: #fff;
|
|
color: var(--text-dark, #1a2e35);
|
|
font-family: 'DM Sans', sans-serif;
|
|
font-size: 13px;
|
|
width: 100%;
|
|
outline: none;
|
|
transition: border-color .2s;
|
|
}
|
|
|
|
.tm-input:focus {
|
|
border-color: var(--teal-mid, #0e7c86);
|
|
}
|
|
|
|
.tm-input.error {
|
|
border-color: #e53e3e;
|
|
}
|
|
|
|
.tm-warn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 11px;
|
|
color: #c53030;
|
|
}
|
|
|
|
.tm-hint {
|
|
font-size: 11px;
|
|
color: var(--text-muted, #6b8890);
|
|
}
|
|
|
|
.tm-info-box {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: flex-start;
|
|
padding: 10px 12px;
|
|
background: #E6F1FB;
|
|
border: 1px solid #B5D4F4;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
color: #0C447C;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.tm-btn-cancel {
|
|
padding: 8px 18px;
|
|
border-radius: 8px;
|
|
border: 1.5px solid var(--border, #d6eaec);
|
|
background: transparent;
|
|
color: var(--text-muted, #6b8890);
|
|
font-family: 'DM Sans', sans-serif;
|
|
font-size: .85rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.tm-btn-submit {
|
|
padding: 8px 20px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: var(--teal-mid, #0e7c86);
|
|
color: #fff;
|
|
font-family: 'DM Sans', sans-serif;
|
|
font-size: .85rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
transition: background .18s;
|
|
}
|
|
|
|
.tm-btn-submit:hover:not(:disabled) {
|
|
background: var(--teal-dark, #0d5c63);
|
|
}
|
|
|
|
.tm-btn-submit:disabled {
|
|
opacity: .5;
|
|
cursor: default;
|
|
}
|
|
|
|
.tm-search-dropdown {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 1090;
|
|
margin-top: 4px;
|
|
max-height: 220px;
|
|
overflow-y: auto;
|
|
background: #fff;
|
|
border: 1.5px solid var(--border, #d6eaec);
|
|
border-radius: 8px;
|
|
box-shadow: 0 8px 24px rgba(0,0,0,.12);
|
|
display: none;
|
|
}
|
|
|
|
.tm-search-dropdown.open {
|
|
display: block;
|
|
}
|
|
|
|
.tm-search-item {
|
|
padding: 8px 10px;
|
|
font-size: 13px;
|
|
color: var(--text-dark, #1a2e35);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.tm-search-item:hover,
|
|
.tm-search-item.active {
|
|
background: var(--teal-pale, #e6f7f8);
|
|
color: var(--teal-dark, #0d5c63);
|
|
}
|
|
|
|
.tm-search-empty {
|
|
padding: 10px;
|
|
font-size: 12px;
|
|
color: var(--text-muted, #6b8890);
|
|
text-align: center;
|
|
}
|
|
</style>
|
|
<script>
|
|
window.TransactModal = (function () {
|
|
"use strict";
|
|
|
|
const H = window.InventoryHelpers;
|
|
|
|
// ── State ────────────────────────────────────────────────────────────────
|
|
let _ctx = null; // TransactContextDto from server
|
|
let _activeType = "ris"; // "ris" | "mrs"
|
|
let _onSuccess = null; // callback after successful submit
|
|
|
|
// ── DOM refs (resolved once modal is injected) ────────────────────────────
|
|
let modal, overlay, form, btnSubmit, submitLabel;
|
|
let optRIS, optMRS, formRIS, formMRS;
|
|
|
|
// ── RIS fields ────────────────────────────────────────────────────────────
|
|
let risItemBadge, risOnHand, risDiscipline, risQty,
|
|
risPRRef, risRemarks, risQtyWarn;
|
|
|
|
// ── MRS fields ────────────────────────────────────────────────────────────
|
|
let mrsRISSelect, mrsQty, mrsCondition, mrsRemarks,
|
|
mrsQtyWarn, mrsMaxHint;
|
|
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
// PUBLIC API
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
async function open(inventoryId, onSuccessCallback) {
|
|
_onSuccess = onSuccessCallback || null;
|
|
_injectModalHtml();
|
|
_bindRefs();
|
|
_showLoading(true);
|
|
_showOverlay(true);
|
|
|
|
try {
|
|
const res = await fetch(
|
|
`/InventoryMgmt/GetTransactContext?inventoryId=${inventoryId}`
|
|
);
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const json = await res.json();
|
|
|
|
_ctx = json.data ?? json;
|
|
|
|
} catch (err) {
|
|
showToast("error", "Could not load item context.", "Error", 4000);
|
|
_showOverlay(false);
|
|
return;
|
|
}
|
|
|
|
_populateContext();
|
|
_selectType("ris");
|
|
_showLoading(false);
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
// MODAL HTML INJECTION
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
function _injectModalHtml() {
|
|
const existing = document.getElementById("transact-modal-overlay");
|
|
if (existing) existing.remove();
|
|
|
|
document.body.insertAdjacentHTML("beforeend", `
|
|
<div id="transact-modal-overlay" style="
|
|
position:fixed;inset:0;z-index:1080;
|
|
background:rgba(0,0,0,.45);
|
|
display:flex;align-items:center;justify-content:center;
|
|
padding:16px">
|
|
|
|
<div id="transact-modal" style="
|
|
background:var(--card-bg,#fff);
|
|
border-radius:14px;
|
|
border:1px solid var(--border,#d6eaec);
|
|
width:100%;max-width:520px;
|
|
overflow:hidden;
|
|
box-shadow:0 20px 60px rgba(0,0,0,.2)">
|
|
|
|
<!-- HEAD -->
|
|
<div style="background:linear-gradient(135deg,#0d5c63,#0e7c86);
|
|
padding:16px 18px;display:flex;align-items:center;
|
|
justify-content:space-between">
|
|
<div style="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="fa-solid fa-arrow-right-arrow-left" style="color:#fff;font-size:16px"></i>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:14px;font-weight:600;color:#fff">
|
|
New Transaction
|
|
</div>
|
|
<div id="tm-subtitle" style="font-size:11px;color:rgba(255,255,255,.65);margin-top:2px">
|
|
Loading…
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button id="tm-close" style="
|
|
width:30px;height:30px;border-radius:8px;
|
|
background:rgba(255,255,255,.12);
|
|
border:1px solid rgba(255,255,255,.2);
|
|
color:rgba(255,255,255,.85);cursor:pointer;
|
|
display:flex;align-items:center;justify-content:center">
|
|
<i class="fas fa-times" style="font-size:13px"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- LOADING STATE -->
|
|
<div id="tm-loading" style="
|
|
display:flex;align-items:center;justify-content:center;
|
|
gap:12px;padding:60px 20px;color:var(--text-muted,#6b8890)">
|
|
<div class="inv-spinner"></div>
|
|
<span style="font-size:.9rem">Loading…</span>
|
|
</div>
|
|
|
|
<!-- BODY (hidden until loaded) -->
|
|
<div id="tm-body" style="display:none">
|
|
|
|
<!-- TYPE PICKER -->
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;
|
|
padding:14px 16px;border-bottom:1px solid var(--border,#d6eaec)">
|
|
<div id="tm-opt-ris" class="tm-type-opt" data-type="ris">
|
|
<div class="tm-type-icon tm-icon-ris">
|
|
<i class="fas fa-file-export"></i>
|
|
</div>
|
|
<div style="font-size:13px;font-weight:600;color:var(--text-dark,#1a2e35)">
|
|
Return Issuance Slip
|
|
</div>
|
|
<div style="font-size:11px;color:var(--text-muted,#6b8890);line-height:1.4">
|
|
Issue items out of inventory
|
|
</div>
|
|
</div>
|
|
<div id="tm-opt-mrs" class="tm-type-opt" data-type="mrs">
|
|
<div class="tm-type-icon tm-icon-mrs">
|
|
<i class="fas fa-file-import"></i>
|
|
</div>
|
|
<div style="font-size:13px;font-weight:600;color:var(--text-dark,#1a2e35)">
|
|
Material Return Slip
|
|
</div>
|
|
<div style="font-size:11px;color:var(--text-muted,#6b8890);line-height:1.4">
|
|
Return unused items to stock
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RIS FORM -->
|
|
<div id="tm-form-ris" style="padding:14px 16px;display:flex;flex-direction:column;gap:12px">
|
|
<div id="tm-ris-stock-badge" class="tm-stock-badge"></div>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
<div class="tm-form-group">
|
|
<label class="tm-label">
|
|
<i class="fas fa-tools"></i> TRADE <span class="tm-req">*</span>
|
|
</label>
|
|
<select id="tm-ris-discipline" class="tm-input">
|
|
<option value="">Select trade…</option>
|
|
</select>
|
|
</div>
|
|
<div class="tm-form-group" style="position:relative">
|
|
<label class="tm-label">
|
|
<i class="fas fa-diagram-project"></i> Project Name <span class="tm-req">*</span>
|
|
</label>
|
|
<input id="tm-ris-project-code-name-search" class="tm-input"
|
|
type="text" placeholder="Search project name…" autocomplete="off">
|
|
<input type="hidden" id="tm-ris-project-code-name">
|
|
<div id="tm-ris-project-code-name-list" class="tm-search-dropdown"></div>
|
|
</div>
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
<div class="tm-form-group">
|
|
<label class="tm-label">
|
|
<i class="fas fa-cubes"></i> Qty to issue <span class="tm-req">*</span>
|
|
</label>
|
|
<input id="tm-ris-qty" class="tm-input"
|
|
type="number" min="1" placeholder="0">
|
|
<span id="tm-ris-qty-warn" class="tm-warn" style="display:none">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<span id="tm-ris-qty-warn-text"></span>
|
|
</span>
|
|
</div>
|
|
<div class="tm-form-group">
|
|
<label class="tm-label">
|
|
<i class="fas fa-file-alt"></i> PR reference
|
|
</label>
|
|
<input id="tm-ris-prref" class="tm-input"
|
|
type="text" readOnly placeholder="PR-2026-XXXX">
|
|
</div>
|
|
</div>
|
|
<div class="tm-form-group">
|
|
<label class="tm-label">
|
|
<i class="fas fa-sticky-note"></i> Remarks
|
|
</label>
|
|
<input id="tm-ris-remarks" class="tm-input"
|
|
type="text" placeholder="Optional notes…">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MRS FORM -->
|
|
<div id="tm-form-mrs" style="padding:14px 16px;display:none;flex-direction:column;gap:12px">
|
|
<div class="tm-info-box">
|
|
<i class="fas fa-info-circle" style="color:#185FA5;flex-shrink:0;margin-top:2px"></i>
|
|
<span>Select the original RIS for this return.
|
|
Only the qty issued on that slip can be returned.</span>
|
|
</div>
|
|
<div class="tm-form-group">
|
|
<label class="tm-label">
|
|
<i class="fas fa-receipt"></i> Original RIS reference <span class="tm-req">*</span>
|
|
</label>
|
|
<select id="tm-mrs-ris" class="tm-input">
|
|
<option value="">Select RIS…</option>
|
|
</select>
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
<div class="tm-form-group">
|
|
<label class="tm-label">
|
|
<i class="fas fa-cubes"></i> Qty to return <span class="tm-req">*</span>
|
|
</label>
|
|
<input id="tm-mrs-qty" class="tm-input"
|
|
type="number" min="1" placeholder="0">
|
|
<span id="tm-mrs-max-hint" class="tm-hint" style="display:none"></span>
|
|
<span id="tm-mrs-qty-warn" class="tm-warn" style="display:none">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<span id="tm-mrs-qty-warn-text"></span>
|
|
</span>
|
|
</div>
|
|
<div class="tm-form-group">
|
|
<label class="tm-label">
|
|
<i class="fas fa-tag"></i> Condition
|
|
</label>
|
|
<select id="tm-mrs-condition" class="tm-input">
|
|
<option value="Good">Good</option>
|
|
<option value="Damaged">Damaged</option>
|
|
<option value="Partial">Partial</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="tm-form-group">
|
|
<label class="tm-label">
|
|
<i class="fas fa-sticky-note"></i> Remarks
|
|
</label>
|
|
<input id="tm-mrs-remarks" class="tm-input"
|
|
type="text" placeholder="Reason for return…">
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /tm-body -->
|
|
|
|
<!-- FOOTER -->
|
|
<div style="
|
|
padding:12px 16px;
|
|
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="tm-cancel" class="tm-btn-cancel">Cancel</button>
|
|
<button id="tm-submit" class="tm-btn-submit" disabled>
|
|
<i class="fas fa-check"></i>
|
|
<span id="tm-submit-label">Create RIS</span>
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>`);
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
// BIND DOM REFS + EVENTS
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
function _bindRefs() {
|
|
overlay = document.getElementById("transact-modal-overlay");
|
|
modal = document.getElementById("transact-modal");
|
|
btnSubmit = document.getElementById("tm-submit");
|
|
submitLabel = document.getElementById("tm-submit-label");
|
|
optRIS = document.getElementById("tm-opt-ris");
|
|
optMRS = document.getElementById("tm-opt-mrs");
|
|
formRIS = document.getElementById("tm-form-ris");
|
|
formMRS = document.getElementById("tm-form-mrs");
|
|
|
|
// RIS fields
|
|
risItemBadge = document.getElementById("tm-ris-stock-badge");
|
|
risDiscipline = document.getElementById("tm-ris-discipline");
|
|
risProjectCode = document.getElementById("tm-ris-project-code-name");
|
|
risProjectCodeSearch = document.getElementById("tm-ris-project-code-name-search");
|
|
risProjectCodeList = document.getElementById("tm-ris-project-code-name-list");
|
|
|
|
_bindProjectCodeSearch();
|
|
|
|
risQty = document.getElementById("tm-ris-qty");
|
|
risPRRef = document.getElementById("tm-ris-prref");
|
|
risRemarks = document.getElementById("tm-ris-remarks");
|
|
risQtyWarn = document.getElementById("tm-ris-qty-warn");
|
|
|
|
// MRS fields
|
|
mrsRISSelect = document.getElementById("tm-mrs-ris");
|
|
mrsQty = document.getElementById("tm-mrs-qty");
|
|
mrsCondition = document.getElementById("tm-mrs-condition");
|
|
mrsRemarks = document.getElementById("tm-mrs-remarks");
|
|
mrsQtyWarn = document.getElementById("tm-mrs-qty-warn");
|
|
mrsMaxHint = document.getElementById("tm-mrs-max-hint");
|
|
|
|
// Type picker
|
|
[optRIS, optMRS].forEach(el =>
|
|
el.addEventListener("click", () => _selectType(el.dataset.type))
|
|
);
|
|
|
|
// Close / Cancel
|
|
document.getElementById("tm-close").addEventListener("click", _close);
|
|
document.getElementById("tm-cancel").addEventListener("click", _close);
|
|
overlay.addEventListener("click", e => { if (e.target === overlay) _close(); });
|
|
|
|
// Qty live validation
|
|
risQty.addEventListener("input", _validateRISQty);
|
|
mrsRISSelect.addEventListener("change", _onMRSRISChange);
|
|
mrsQty.addEventListener("input", _validateMRSQty);
|
|
|
|
// Submit
|
|
btnSubmit.addEventListener("click", _handleSubmit);
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
// POPULATE FROM CONTEXT
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
function _populateContext() {
|
|
|
|
// Header subtitle
|
|
document.getElementById("tm-subtitle").textContent =
|
|
`${H.escHtml(_ctx.itemName)} · Item #${_ctx.itemNo}`;
|
|
|
|
document.getElementById("tm-ris-prref").value = _ctx.prNo ?? "";
|
|
|
|
// Stock badge
|
|
risItemBadge.innerHTML = `
|
|
<i class="fas fa-layer-group"></i>
|
|
<span>On hand</span>
|
|
<strong>${_ctx.qtyOnHand} pcs</strong>
|
|
<span style="margin-left:8px;color:var(--text-muted,#6b8890)">
|
|
In: ${_ctx.qtyIn} · Out: ${_ctx.qtyOut}
|
|
</span>`;
|
|
|
|
// Discipline dropdown
|
|
risDiscipline.innerHTML = `
|
|
<option value="">Select trade…</option>` +
|
|
(_ctx.disciplines || []).map(d =>
|
|
`
|
|
<option value="${d.disciplineId}">${H.escHtml(d.disciplineName)}</option>`
|
|
).join("");
|
|
|
|
// MRS — open RIS list
|
|
const hasRIS = _ctx.openRISList && _ctx.openRISList.length > 0;
|
|
mrsRISSelect.innerHTML = `
|
|
<option value="">Select RIS…</option>` +
|
|
(hasRIS
|
|
? _ctx.openRISList.map(r =>
|
|
`
|
|
<option value="${r.risId}"
|
|
data-max="${r.qtyAvailableToReturn}"
|
|
data-discipline="${H.escAttr(r.disciplineName)}">
|
|
${H.escHtml(r.risNo)} — ${r.qtyAvailableToReturn} pcs avail — ${H.escHtml(r.disciplineName)}
|
|
</option>`)
|
|
.join("")
|
|
: "");
|
|
|
|
//Project name
|
|
_setProjectCodeOptions(_ctx.projectCodes);
|
|
risProjectCodeSearch.value = "";
|
|
risProjectCode.value = "";
|
|
|
|
if (!hasRIS) {
|
|
optMRS.style.opacity = ".45";
|
|
optMRS.style.cursor = "not-allowed";
|
|
optMRS.title = "No approved RIS records with remaining qty for this item.";
|
|
optMRS.onclick = null;
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
// TYPE SWITCHING
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
function _selectType(type) {
|
|
_activeType = type;
|
|
const isRIS = type === "ris";
|
|
|
|
optRIS.classList.toggle("selected", isRIS);
|
|
optMRS.classList.toggle("selected", !isRIS);
|
|
|
|
formRIS.style.display = isRIS ? "flex" : "none";
|
|
formMRS.style.display = !isRIS ? "flex" : "none";
|
|
|
|
submitLabel.textContent = isRIS ? "Create RIS" : "Create MRS";
|
|
btnSubmit.disabled = false;
|
|
|
|
_clearErrors();
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
// VALIDATION
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
function _validateRISQty() {
|
|
const val = parseInt(risQty.value, 10);
|
|
const max = _ctx?.qtyOnHand ?? 0;
|
|
const over = !isNaN(val) && val > max;
|
|
const zero = !isNaN(val) && val < 1;
|
|
|
|
document.getElementById("tm-ris-qty-warn-text").textContent =
|
|
over ? `Cannot exceed ${max} on hand.`
|
|
: zero ? "Must be at least 1." : "";
|
|
|
|
risQtyWarn.style.display = (over || zero) ? "flex" : "none";
|
|
risQty.classList.toggle("error", over || zero);
|
|
}
|
|
|
|
function _onMRSRISChange() {
|
|
const opt = mrsRISSelect.selectedOptions[0];
|
|
const max = opt ? parseInt(opt.dataset.max, 10) : 0;
|
|
|
|
if (opt && opt.value) {
|
|
mrsMaxHint.style.display = "block";
|
|
mrsMaxHint.textContent = `Max returnable: ${max} pcs`;
|
|
mrsQty.max = max;
|
|
} else {
|
|
mrsMaxHint.style.display = "none";
|
|
mrsQty.max = "";
|
|
}
|
|
_validateMRSQty();
|
|
}
|
|
|
|
function _validateMRSQty() {
|
|
const val = parseInt(mrsQty.value, 10);
|
|
const opt = mrsRISSelect.selectedOptions[0];
|
|
const max = opt ? parseInt(opt.dataset.max, 10) : Infinity;
|
|
const over = !isNaN(val) && val > max;
|
|
const zero = !isNaN(val) && val < 1;
|
|
|
|
document.getElementById("tm-mrs-qty-warn-text").textContent =
|
|
over ? `Cannot exceed ${max} available.`
|
|
: zero ? "Must be at least 1." : "";
|
|
|
|
mrsQtyWarn.style.display = (over || zero) ? "flex" : "none";
|
|
mrsQty.classList.toggle("error", over || zero);
|
|
}
|
|
|
|
function _clearErrors() {
|
|
[risQty, risDiscipline, risProjectCode, mrsRISSelect, mrsQty].forEach(el => {
|
|
if (el) el.classList.remove("error");
|
|
});
|
|
risProjectCodeSearch?.classList.remove("error");
|
|
|
|
[risQtyWarn, mrsQtyWarn].forEach(el => {
|
|
if (el) el.style.display = "none";
|
|
});
|
|
}
|
|
|
|
function _validateForm() {
|
|
let valid = true;
|
|
|
|
if (_activeType === "ris") {
|
|
if (!risDiscipline.value) { risDiscipline.classList.add("error"); valid = false; }
|
|
const qty = parseInt(risQty.value, 10);
|
|
if (isNaN(qty) || qty < 1 || qty > _ctx.qtyOnHand) {
|
|
risQty.classList.add("error"); valid = false;
|
|
}
|
|
if (!risProjectCode.value) {
|
|
risProjectCode.classList.add("error");
|
|
risProjectCodeSearch.classList.add("error");
|
|
valid = false;
|
|
}
|
|
} else {
|
|
if (!mrsRISSelect.value) { mrsRISSelect.classList.add("error"); valid = false; }
|
|
const qty = parseInt(mrsQty.value, 10);
|
|
const opt = mrsRISSelect.selectedOptions[0];
|
|
const maxRet = opt ? parseInt(opt.dataset.max, 10) : 0;
|
|
if (isNaN(qty) || qty < 1 || qty > maxRet) {
|
|
mrsQty.classList.add("error"); valid = false;
|
|
}
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
// ── Searchable dropdown refs ────────────────────────────────────────────────
|
|
let risProjectCode, risProjectCodeSearch, risProjectCodeList;
|
|
let _projectCodeOptions = [];
|
|
let _projectCodeActiveIndex = -1;
|
|
|
|
function _bindIssuedToSearch() {
|
|
risIssuedToSearch = document.getElementById("tm-ris-issuedto-search");
|
|
risIssuedToList = document.getElementById("tm-ris-issuedto-list");
|
|
|
|
risIssuedToSearch.addEventListener("input", _onIssuedToInput);
|
|
risIssuedToSearch.addEventListener("focus", () => _renderIssuedToList(risIssuedToSearch.value));
|
|
risIssuedToSearch.addEventListener("keydown", _onIssuedToKeydown);
|
|
|
|
document.addEventListener("click", (e) => {
|
|
if (!risIssuedToSearch.contains(e.target) && !risIssuedToList.contains(e.target)) {
|
|
_closeIssuedToList();
|
|
}
|
|
});
|
|
}
|
|
|
|
function _bindProjectCodeSearch() {
|
|
risProjectCodeSearch.addEventListener("input", _onProjectCodeInput);
|
|
risProjectCodeSearch.addEventListener("focus", () => _renderProjectCodeList(risProjectCodeSearch.value));
|
|
risProjectCodeSearch.addEventListener("keydown", _onProjectCodeKeydown);
|
|
|
|
document.addEventListener("click", (e) => {
|
|
if (!risProjectCodeSearch.contains(e.target) && !risProjectCodeList.contains(e.target)) {
|
|
_closeProjectCodeList();
|
|
}
|
|
});
|
|
}
|
|
function _setProjectCodeOptions(projectCodes) {
|
|
_projectCodeOptions = (projectCodes || []).map(p => ({
|
|
value: p.projectCodeId,
|
|
code: p.projectCode,
|
|
name: p.projectName,
|
|
label: `${p.projectCode} — ${p.projectName}`
|
|
}));
|
|
}
|
|
|
|
function _onProjectCodeInput() {
|
|
risProjectCode.value = ""; // invalidate committed selection until re-picked
|
|
_renderProjectCodeList(risProjectCodeSearch.value);
|
|
}
|
|
|
|
function _renderProjectCodeList(query) {
|
|
const q = (query || "").trim().toLowerCase();
|
|
const filtered = q
|
|
? _projectCodeOptions.filter(o =>
|
|
o.name.toLowerCase().includes(q) || o.code.toLowerCase().includes(q))
|
|
: _projectCodeOptions;
|
|
|
|
_projectCodeActiveIndex = -1;
|
|
|
|
risProjectCodeList.innerHTML = filtered.length
|
|
? filtered.map((o, i) =>
|
|
`<div class="tm-search-item" data-index="${i}" data-value="${o.value}">
|
|
<div>${H.escHtml(o.name)}</div>
|
|
<div style="font-size:11px;color:var(--text-muted,#6b8890)">${H.escHtml(o.code)}</div>
|
|
</div>`).join("")
|
|
: `<div class="tm-search-empty">No matching projects</div>`;
|
|
|
|
risProjectCodeList.querySelectorAll(".tm-search-item").forEach(item => {
|
|
item.addEventListener("click", () => {
|
|
const opt = _projectCodeOptions[parseInt(item.dataset.index, 10)];
|
|
_selectProjectCode(opt);
|
|
});
|
|
});
|
|
|
|
risProjectCodeList.classList.add("open");
|
|
}
|
|
|
|
function _selectProjectCode(opt) {
|
|
risProjectCode.value = opt.value; // ProjectCodeId — this is what gets posted
|
|
risProjectCodeSearch.value = opt.label; // visible text
|
|
_closeProjectCodeList();
|
|
risProjectCode.classList.remove("error");
|
|
risProjectCodeSearch.classList.remove("error");
|
|
}
|
|
|
|
function _closeProjectCodeList() {
|
|
risProjectCodeList.classList.remove("open");
|
|
_projectCodeActiveIndex = -1;
|
|
}
|
|
|
|
function _onProjectCodeKeydown(e) {
|
|
const items = risProjectCodeList.querySelectorAll(".tm-search-item");
|
|
if (!items.length) return;
|
|
|
|
if (e.key === "ArrowDown") {
|
|
e.preventDefault();
|
|
_projectCodeActiveIndex = Math.min(_projectCodeActiveIndex + 1, items.length - 1);
|
|
_highlightProjectCode(items);
|
|
} else if (e.key === "ArrowUp") {
|
|
e.preventDefault();
|
|
_projectCodeActiveIndex = Math.max(_projectCodeActiveIndex - 1, 0);
|
|
_highlightProjectCode(items);
|
|
} else if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
const idx = _projectCodeActiveIndex >= 0 ? _projectCodeActiveIndex : 0;
|
|
const item = items[idx];
|
|
if (item) _selectProjectCode(_projectCodeOptions[parseInt(item.dataset.index, 10)]);
|
|
} else if (e.key === "Escape") {
|
|
_closeProjectCodeList();
|
|
}
|
|
}
|
|
|
|
function _highlightProjectCode(items) {
|
|
items.forEach((el, i) => el.classList.toggle("active", i === _projectCodeActiveIndex));
|
|
items[_projectCodeActiveIndex]?.scrollIntoView({ block: "nearest" });
|
|
}
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
// SUBMIT
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
async function _handleSubmit() {
|
|
_clearErrors();
|
|
if (!_validateForm()) {
|
|
showToast("warning", "Please fix the highlighted fields.", "Validation", 3000);
|
|
return;
|
|
}
|
|
|
|
const confirmed = await showConfirmation({
|
|
title: _activeType === "ris" ? "Create Return Issuance Slip" : "Create Material Return Slip",
|
|
message: _activeType === "ris"
|
|
? `Issue <strong>${risQty.value} pcs</strong> from inventory?`
|
|
: `Return <strong>${mrsQty.value} pcs</strong> back to stock?`,
|
|
type: "warning",
|
|
confirmText: _activeType === "ris" ? "Create RIS" : "Create MRS",
|
|
cancelText: "Cancel"
|
|
});
|
|
if (!confirmed) return;
|
|
|
|
btnSubmit.disabled = true;
|
|
btnSubmit.querySelector("i").className = "fas fa-spinner fa-spin";
|
|
|
|
try {
|
|
const payload = _activeType === "ris"
|
|
? {
|
|
InventoryId: _ctx.inventoryId,
|
|
PRDetailId: parseInt(risPRRef.value, 10) || 0,
|
|
ProjectCodeId: parseInt(risProjectCode.value, 10),
|
|
DisciplineId: parseInt(risDiscipline.value, 10),
|
|
QtyIssued: parseInt(risQty.value, 10),
|
|
Remarks: risRemarks.value.trim() || null
|
|
}
|
|
: {
|
|
RISId: parseInt(mrsRISSelect.value, 10),
|
|
QtyReturned: parseInt(mrsQty.value, 10),
|
|
ReturnedBy: _ctx.department || "N/A",
|
|
Condition: mrsCondition.value,
|
|
Remarks: mrsRemarks.value.trim() || null
|
|
};
|
|
|
|
const endpoint = _activeType === "ris"
|
|
? "/RISMgmt/CreateRIS"
|
|
: "/MRSMgmt/CreateMRS";
|
|
|
|
const res = await fetch(endpoint, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const json = await res.json();
|
|
|
|
if (!res.ok || !json.success) {
|
|
showToast("error", json.message ?? "An error occurred.", "Failed", 4000);
|
|
return;
|
|
}
|
|
|
|
showToast("success", json.message, "Done!", 3500);
|
|
|
|
const onSuccess = _onSuccess;
|
|
_close();
|
|
|
|
if (typeof onSuccess === "function") onSuccess();
|
|
|
|
} catch (err) {
|
|
showToast("error", "Request failed. Please try again.", "Error", 4000);
|
|
} finally {
|
|
btnSubmit.disabled = false;
|
|
btnSubmit.querySelector("i").className = "fas fa-check";
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
// HELPERS
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
function _showOverlay(show) {
|
|
const el = document.getElementById("transact-modal-overlay");
|
|
if (el) el.style.display = show ? "flex" : "none";
|
|
}
|
|
|
|
function _showLoading(loading) {
|
|
document.getElementById("tm-loading").style.display = loading ? "flex" : "none";
|
|
document.getElementById("tm-body").style.display = loading ? "none" : "block";
|
|
if (btnSubmit) btnSubmit.disabled = loading;
|
|
}
|
|
|
|
function _close() {
|
|
document.getElementById("transact-modal-overlay")?.remove();
|
|
|
|
_ctx = null;
|
|
_onSuccess = null;
|
|
}
|
|
|
|
return { open };
|
|
})();
|
|
</script> |