PSTW_CentralizeSystem/Areas/IT/Views/ApprovalDashboard/SectionB.cshtml
2025-11-10 10:26:57 +08:00

361 lines
16 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@{
ViewData["Title"] = "IT Section B Asset Information";
Layout = "~/Views/Shared/_Layout.cshtml";
// Expect ?statusId=123
}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
<style>
:root{ --card-r:16px; --soft-b:#eef2f6; --soft-s:0 8px 24px rgba(0,0,0,.08); --muted:#6b7280; }
#bApp{ max-width:1100px; margin:auto; font-size:14px; }
.ui-card{ background:#fff; border:1px solid var(--soft-b); border-radius:var(--card-r); box-shadow:var(--soft-s); margin-bottom:16px; overflow:hidden; }
.ui-head{ display:flex; align-items:center; justify-content:space-between; padding:14px 18px; border-bottom:1px solid var(--soft-b); background:linear-gradient(180deg,#fbfdff,#f7fafc); }
.ui-head h6{ margin:0; font-weight:800; color:#0b5ed7; }
.ui-body{ padding:16px 18px; }
.muted{ color:var(--muted); }
.grid2{ display:grid; grid-template-columns:1fr 1fr; gap:12px; }
@@media (max-width:768px){ .grid2{ grid-template-columns:1fr; } }
.readonly-pill{ display:inline-flex; align-items:center; gap:.4rem; padding:.28rem .6rem; border-radius:999px; background:#eef2f7; color:#334155; font-weight:700; font-size:12px; }
.sig-box{ border:1px dashed #d1d5db; border-radius:8px; padding:12px; background:#fbfbfc; }
.submit-bar{ position:sticky; bottom:12px; z-index:5; display:flex; justify-content:flex-end; gap:10px; padding:12px; border-radius:12px; background:rgba(255,255,255,.85); backdrop-filter:blur(6px); border:1px solid var(--soft-b); box-shadow:var(--soft-s); margin:6px 0 30px; }
</style>
<div id="bApp">
<!-- Page head -->
<div class="d-flex align-items-center justify-content-between my-3">
<h5 class="m-0 fw-bold">Section B Asset Information</h5>
<div class="d-flex align-items-center gap-2">
<span class="readonly-pill">Overall: {{ meta.overallStatus || '—' }}</span>
<span class="readonly-pill">Requestor: {{ meta.requestorName || '—' }}</span>
<span class="readonly-pill" v-if="meta.isItMember">You are IT</span>
<span class="readonly-pill" :class="meta.sectionBSent ? '' : 'bg-warning-subtle'">
{{ meta.sectionBSent ? 'Sent' : 'Draft' }}
</span>
</div>
</div>
<div v-if="error" class="alert alert-danger py-2">{{ error }}</div>
<div v-if="busy" class="alert alert-secondary py-2">Loading…</div>
<!-- Gate -->
<div v-if="!busy && meta.overallStatus !== 'Approved'" class="alert alert-warning">
Section B is available only after the request is <strong>Approved</strong>. Current status: <strong>{{ meta.overallStatus || '—' }}</strong>.
</div>
<template v-if="!busy && meta.overallStatus === 'Approved'">
<!-- Asset Information -->
<div class="ui-card">
<div class="ui-head">
<h6><i class="bi bi-hdd-stack"></i> Asset Information</h6>
<span class="muted" v-if="meta.lastEditedBy">
Last edited by {{ meta.lastEditedBy }} at {{ fmtDT(meta.lastEditedAt) }}
</span>
</div>
<div class="ui-body">
<div v-if="!meta.isItMember" class="alert alert-info py-2">
Youre not in the IT Team. You can view once saved, but cannot edit.
</div>
<div v-if="meta.locked" class="alert alert-light border">
Locked for editing (sent). Acceptances can proceed below.
</div>
<div class="grid2">
<div>
<label class="form-label">Asset No</label>
<input class="form-control" v-model.trim="form.assetNo" :disabled="!meta.isItMember || meta.locked">
</div>
<div>
<label class="form-label">Machine ID</label>
<input class="form-control" v-model.trim="form.machineId" :disabled="!meta.isItMember || meta.locked">
</div>
<div>
<label class="form-label">IP Address</label>
<input class="form-control" v-model.trim="form.ipAddress" :disabled="!meta.isItMember || meta.locked">
</div>
<div>
<label class="form-label">Wired MAC Address</label>
<input class="form-control" v-model.trim="form.wiredMac" :disabled="!meta.isItMember || meta.locked">
</div>
<div>
<label class="form-label">Wi-Fi MAC Address</label>
<input class="form-control" v-model.trim="form.wifiMac" :disabled="!meta.isItMember || meta.locked">
</div>
<div>
<label class="form-label">Dial-up Account</label>
<input class="form-control" v-model.trim="form.dialupAcc" :disabled="!meta.isItMember || meta.locked">
</div>
</div>
<div class="mt-2">
<label class="form-label">Remarks</label>
<textarea rows="4" class="form-control" v-model.trim="form.remarks" :disabled="!meta.isItMember || meta.locked"></textarea>
</div>
</div>
</div>
<!-- Acceptances -->
<div class="ui-card">
<div class="ui-head">
<h6><i class="bi bi-patch-check"></i> Acceptances</h6>
</div>
<div class="ui-body">
<div v-if="!meta.sectionBSent" class="alert alert-light border">
Send Section B first to enable acceptances.
</div>
<div class="row g-3" v-else>
<div class="col-md-6">
<div class="sig-box">
<div class="d-flex justify-content-between align-items-center">
<strong>Requestor Acknowledgement</strong>
<span class="badge" :class="meta.requestorAccepted ? 'bg-success' : 'bg-secondary'">
{{ meta.requestorAccepted ? 'Accepted' : 'Pending' }}
</span>
</div>
<div class="mt-2">
<div>Name: <strong>{{ meta.requestorName || '—' }}</strong></div>
<div>Date: <strong>{{ fmtDT(meta.requestorAcceptedAt) || '—' }}</strong></div>
</div>
<div class="mt-2">
<button class="btn btn-outline-primary btn-sm"
:disabled="busy || !meta.isRequestor || meta.requestorAccepted"
@@click="accept('REQUESTOR')">
I Accept
</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="sig-box">
<div class="d-flex justify-content-between align-items-center">
<strong>Completed by (IT)</strong>
<span class="badge" :class="meta.itAccepted ? 'bg-success' : 'bg-secondary'">
{{ meta.itAccepted ? 'Accepted' : 'Pending' }}
</span>
</div>
<div class="mt-2">
<div>Name: <strong>{{ meta.itAcceptedBy || (meta.isItMember ? '(you?)' : '—') }}</strong></div>
<div>Date: <strong>{{ fmtDT(meta.itAcceptedAt) || '—' }}</strong></div>
</div>
<div class="mt-2">
<button class="btn btn-outline-primary btn-sm"
:disabled="busy || !meta.isItMember || meta.itAccepted"
@@click="accept('IT')">
IT Accept
</button>
</div>
</div>
</div>
</div>
<button class="btn btn-outline-secondary btn-sm mt-2"
:disabled="busy || !(meta.requestorAccepted && meta.itAccepted)"
@@click="downloadPdf">
Download PDF
</button>
</div>
</div>
<!-- Sticky action bar -->
<div class="submit-bar">
<button class="btn btn-light me-auto" @@click="goBack">
<i class="bi bi-arrow-left"></i> Back
</button>
<button class="btn btn-secondary"
:disabled="busy || !meta.isItMember || meta.locked"
@@click="resetDraft">
Reset
</button>
<button class="btn btn-primary"
:disabled="busy || !meta.isItMember || meta.locked"
@@click="saveDraft">
<span v-if="saving" class="spinner-border spinner-border-sm me-2"></span>
Save Draft
</button>
<button class="btn btn-success"
:disabled="busy || !meta.isItMember || meta.locked"
@@click="sendNow">
Send Now
</button>
</div>
</template>
</div>
@section Scripts{
<script>
const bApp = Vue.createApp({
data(){
const url = new URL(window.location.href);
return {
busy:false, saving:false, error:null,
statusId: Number(url.searchParams.get('statusId')) || 0,
returnUrl: url.searchParams.get('returnUrl'),
meta:{
overallStatus:null, requestorName:null,
isRequestor:false, isItMember:false,
sectionBSent:false, sectionBSentAt:null,
locked:false,
lastEditedBy:null, lastEditedAt:null,
requestorAccepted:false, requestorAcceptedAt:null,
itAccepted:false, itAcceptedAt:null, itAcceptedBy:null
},
form:{ assetNo:"", machineId:"", ipAddress:"", wiredMac:"", wifiMac:"", dialupAcc:"", remarks:"" }
};
},
methods:{
goBack() {
const go = (u) => window.location.href = u;
// 1) Prefer returnUrl if it's local
if (this.returnUrl) {
try {
const u = new URL(this.returnUrl, window.location.origin);
if (u.origin === window.location.origin) { go(u.href); return; }
} catch { }
}
// 2) Else use Referer if it's local
if (document.referrer) {
try {
const u = new URL(document.referrer);
if (u.origin === window.location.origin) { go(document.referrer); return; }
} catch { }
}
// 3) Else use history
if (history.length > 1) { history.back(); return; }
// 4) Final hard fallback by role
go(this.meta.isItMember ? '/IT/ApprovalDashboard' : '/IT/MyRequests');
},
fmtDT(d){ if(!d) return ""; const dt=new Date(d); return isNaN(dt)?"":dt.toLocaleString(); },
async load(){
try{
this.busy=true; this.error=null;
const r = await fetch(`/ItRequestAPI/sectionB/meta?statusId=${this.statusId}`);
if(!r.ok) throw new Error(`Load failed (${r.status})`);
const j = await r.json();
this.meta = {
overallStatus: j.overallStatus,
requestorName: j.requestorName,
isRequestor: j.isRequestor,
isItMember: j.isItMember,
sectionBSent: !!j.sectionBSent,
sectionBSentAt: j.sectionBSentAt,
locked: !!j.locked,
lastEditedBy: j.sectionB?.lastEditedBy || null,
lastEditedAt: j.sectionB?.lastEditedAt || null,
requestorAccepted: !!j.requestorAccepted,
requestorAcceptedAt: j.requestorAcceptedAt || null,
itAccepted: !!j.itAccepted,
itAcceptedAt: j.itAcceptedAt || null,
itAcceptedBy: j.itAcceptedBy || null
};
const s = j.sectionB || {};
this.form = {
assetNo: s.assetNo || "",
machineId: s.machineId || "",
ipAddress: s.ipAddress || "",
wiredMac: s.wiredMac || "",
wifiMac: s.wifiMac || "",
dialupAcc: s.dialupAcc || "",
remarks: s.remarks || ""
};
}catch(e){
this.error = e.message || 'Failed to load.';
}finally{
this.busy=false;
}
},
async saveDraft(){
try{
this.saving=true; this.error=null;
const res = await fetch('/ItRequestAPI/sectionB/save', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ statusId:this.statusId, ...this.form })
});
const j = await res.json().catch(()=> ({}));
if(!res.ok) throw new Error(j.message || `Save failed (${res.status})`);
await this.load();
}catch(e){ this.error = e.message || 'Unable to save.'; }
finally{ this.saving=false; }
},
async resetDraft(){
if(!confirm('Reset Section B to an empty draft?')) return;
try{
this.busy=true; this.error=null;
const res = await fetch('/ItRequestAPI/sectionB/reset', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ statusId:this.statusId })
});
const j = await res.json().catch(()=> ({}));
if(!res.ok) throw new Error(j.message || `Reset failed (${res.status})`);
await this.load();
}catch(e){ this.error = e.message || 'Unable to reset.'; }
finally{ this.busy=false; }
},
async sendNow() {
if (!confirm('Send Section B now? You will not be able to edit afterwards.')) return;
// optional guard (mirrors server rule)
const a = (this.form.assetNo || "").trim(), m = (this.form.machineId || "").trim(), i = (this.form.ipAddress || "").trim();
if (!a && !m && !i) { alert('Please provide at least Asset No, Machine ID, or IP Address.'); return; }
try {
this.busy = true; this.error = null;
const res = await fetch('/ItRequestAPI/sectionB/send', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
statusId: this.statusId,
assetNo: this.form.assetNo,
machineId: this.form.machineId,
ipAddress: this.form.ipAddress,
wiredMac: this.form.wiredMac,
wifiMac: this.form.wifiMac,
dialupAcc: this.form.dialupAcc,
remarks: this.form.remarks
})
});
const j = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(j.message || `Send failed (${res.status})`);
alert(j.message || 'Section B sent and locked for editing.');
await this.load();
} catch (e) {
this.error = e.message || 'Unable to send.';
} finally {
this.busy = false;
}
},
async accept(kind){
try{
this.busy=true; this.error=null;
const res = await fetch('/ItRequestAPI/sectionB/accept', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ statusId:this.statusId, by: kind })
});
const j = await res.json().catch(()=> ({}));
if(!res.ok) throw new Error(j.message || `Accept failed (${res.status})`);
await this.load();
}catch(e){ this.error = e.message || 'Action failed.'; }
finally{ this.busy=false; }
},
downloadPdf(){
window.open(`/ItRequestAPI/sectionB/pdf?statusId=${this.statusId}`,'_blank');
}
},
mounted(){
if(!this.statusId){ this.error='Missing statusId in the URL.'; return; }
this.load();
}
});
bApp.mount('#bApp');
</script>
}