NonInventPurchasingSystem/CPRNIMS.WebApps/Views/Shared/PagesView/Inventory/_InventoryTransactModal.cshtml
2026-06-15 16:41:50 +08:00

738 lines
32 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;
}
</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, risIssuedTo, 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="fas fa-transfer-alt" 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> Discipline <span class="tm-req">*</span>
</label>
<select id="tm-ris-discipline" class="tm-input">
<option value="">Select discipline…</option>
</select>
</div>
<div class="tm-form-group">
<label class="tm-label">
<i class="fas fa-user"></i> Issued to <span class="tm-req">*</span>
</label>
<input id="tm-ris-issuedto" class="tm-input"
type="text" placeholder="Name or user ID…">
</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" 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");
risIssuedTo = document.getElementById("tm-ris-issuedto");
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 discipline…</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("")
: "");
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, risIssuedTo, risDiscipline, mrsRISSelect, mrsQty].forEach(el => {
if (el) el.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; }
if (!risIssuedTo.value.trim()) { risIssuedTo.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;
}
} 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;
}
// ════════════════════════════════════════════════════════════════════════
// 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,
IssuedTo: risIssuedTo.value.trim(),
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>