699 lines
33 KiB
Plaintext
699 lines
33 KiB
Plaintext
@{
|
||
ViewData["Title"] = "Edit IT Request";
|
||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||
}
|
||
|
||
<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;
|
||
}
|
||
|
||
#editApp {
|
||
max-width: 1100px;
|
||
margin: auto;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.page-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
margin: 16px 0 10px;
|
||
}
|
||
|
||
.subtle {
|
||
color: var(--muted);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.note {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.mini-table {
|
||
border: 1px solid var(--soft-b);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.mini-head {
|
||
background: #f3f6fb;
|
||
padding: 10px 12px;
|
||
font-weight: 700;
|
||
color: #334155;
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
letter-spacing: .4px;
|
||
}
|
||
|
||
.mini-row {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
border-top: 1px solid #f1f5f9;
|
||
}
|
||
|
||
.mini-row:hover {
|
||
background: #fafcff;
|
||
}
|
||
|
||
.btn-soft {
|
||
border-radius: 10px;
|
||
padding: .5rem .8rem;
|
||
font-weight: 700;
|
||
letter-spacing: .2px;
|
||
border: 1px solid transparent;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,.06);
|
||
}
|
||
|
||
.btn-add {
|
||
background: #0b5ed7;
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-add:hover {
|
||
background: #0a53be;
|
||
}
|
||
|
||
.btn-del {
|
||
background: #fff;
|
||
color: #dc2626;
|
||
border: 1px solid #f1d2d2;
|
||
}
|
||
|
||
.btn-del:hover {
|
||
background: #fff5f5;
|
||
}
|
||
|
||
.stacked-checks .form-check {
|
||
margin-bottom: .4rem;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.btn-go {
|
||
background: #22c55e;
|
||
color: #fff;
|
||
border-radius: 10px;
|
||
padding: .6rem .95rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.btn-go:hover {
|
||
background: #16a34a;
|
||
}
|
||
|
||
.btn-reset {
|
||
background: #fff;
|
||
color: #334155;
|
||
border: 1px solid var(--soft-b);
|
||
border-radius: 10px;
|
||
padding: .6rem .95rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.btn-reset:hover {
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.invalid-hint {
|
||
color: #dc2626;
|
||
font-size: 12px;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.countdown-box {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
border-radius: 8px;
|
||
padding: 4px 10px;
|
||
border: 1px solid #e2e8f0;
|
||
box-shadow: 0 2px 6px rgba(0,0,0,.05);
|
||
min-width: 80px;
|
||
justify-content: center;
|
||
transition: background .3s ease, color .3s ease;
|
||
}
|
||
|
||
.countdown-active {
|
||
background: #ecfdf5;
|
||
color: #166534;
|
||
border-color: #bbf7d0;
|
||
}
|
||
|
||
.countdown-expired {
|
||
background: #fef2f2;
|
||
color: #991b1b;
|
||
border-color: #fecaca;
|
||
}
|
||
|
||
.perm-flags {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
</style>
|
||
|
||
<div id="editApp">
|
||
<div class="page-head">
|
||
<div class="subtle d-flex align-items-center gap-2">
|
||
<span class="badge" :class="isEditable ? 'bg-success' : 'bg-secondary'">{{ isEditable ? 'Editable' : 'Locked' }}</span>
|
||
<div class="countdown-box" :class="isEditable ? 'countdown-active' : 'countdown-expired'">
|
||
<i class="bi bi-clock-history me-1"></i>
|
||
<span id="countdown">—</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Requester -->
|
||
<div class="ui-card">
|
||
<div class="ui-head">
|
||
<h6><i class="bi bi-person-badge"></i> Requester Details</h6>
|
||
<span class="note">These fields are snapshotted at submission</span>
|
||
</div>
|
||
<div class="ui-body">
|
||
<div class="req-grid">
|
||
<div>
|
||
<label class="form-label">Staff Name</label>
|
||
<input type="text" class="form-control" v-model.trim="model.staffName" readonly>
|
||
</div>
|
||
<div>
|
||
<label class="form-label">Designation</label>
|
||
<input type="text" class="form-control" v-model.trim="model.designation" :disabled="!isEditable">
|
||
</div>
|
||
<div>
|
||
<label class="form-label">Company</label>
|
||
<input type="text" class="form-control" v-model.trim="model.companyName" readonly>
|
||
</div>
|
||
<div>
|
||
<label class="form-label">Div/Dept</label>
|
||
<input type="text" class="form-control" v-model.trim="model.departmentName" readonly>
|
||
</div>
|
||
<div>
|
||
<label class="form-label">Location</label>
|
||
<input type="text" class="form-control" v-model.trim="model.location" :disabled="!isEditable">
|
||
</div>
|
||
<div>
|
||
<label class="form-label">Phone Ext</label>
|
||
<input type="text" class="form-control" v-model.trim="model.phoneExt" :disabled="!isEditable">
|
||
</div>
|
||
<div>
|
||
<label class="form-label">Employment Status</label>
|
||
<select class="form-select" v-model="model.employmentStatus" :disabled="!isEditable">
|
||
<option value="">--</option>
|
||
<option>Permanent</option>
|
||
<option>Contract</option>
|
||
<option>Temp</option>
|
||
<option>New Staff</option>
|
||
</select>
|
||
</div>
|
||
<div v-if="model.employmentStatus==='Contract' || model.employmentStatus==='Temp'">
|
||
<label class="form-label">Contract End Date</label>
|
||
<input type="date" class="form-control" v-model="model.contractEndDate" :disabled="!isEditable">
|
||
</div>
|
||
<div>
|
||
<label class="form-label">Required Date <span class="text-danger">*</span></label>
|
||
<input type="date" class="form-control" v-model="model.requiredDate" :min="minReqISO" :disabled="!isEditable">
|
||
<div class="invalid-hint" v-if="validation.requiredDate">{{ validation.requiredDate }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hardware -->
|
||
<div class="ui-card">
|
||
<div class="ui-head">
|
||
<h6><i class="bi bi-cpu"></i> Hardware Requirements</h6>
|
||
</div>
|
||
<div class="ui-body">
|
||
<div class="row g-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label">Purpose <span class="text-danger" v-if="hardwareCount>0">*</span></label>
|
||
<select class="form-select" v-model="hardwarePurpose" :disabled="!isEditable">
|
||
<option value="">-- Select --</option>
|
||
<option value="NewRecruitment">New Staff Recruitment</option>
|
||
<option value="Replacement">Replacement</option>
|
||
<option value="Additional">Additional</option>
|
||
</select>
|
||
<div class="invalid-hint" v-if="validation.hardwarePurpose">{{ validation.hardwarePurpose }}</div>
|
||
|
||
<label class="form-label mt-3">Justification (for hardware change)</label>
|
||
<textarea class="form-control" rows="3" v-model="hardwareJustification" :disabled="!isEditable" placeholder="-"></textarea>
|
||
</div>
|
||
|
||
<div class="col-md-6">
|
||
<div class="d-flex align-items-center justify-content-between">
|
||
<label class="form-label">Select below</label>
|
||
<div class="small text-muted">All-inclusive toggles will auto-select sensible accessories</div>
|
||
</div>
|
||
<div class="stacked-checks">
|
||
<div class="form-check" v-for="opt in hardwareCategories" :key="opt.key">
|
||
<input class="form-check-input" type="checkbox" :id="'cat_'+opt.key"
|
||
v-model="opt.include" :disabled="!isEditable" @@change="onHardwareToggle(opt.key)">
|
||
<label class="form-check-label" :for="'cat_'+opt.key">{{ opt.label }}</label>
|
||
</div>
|
||
</div>
|
||
<div class="mt-2">
|
||
<label class="form-label">Other (Specify)</label>
|
||
<input class="form-control form-control-sm" v-model.trim="hardwareOther" :disabled="!isEditable" placeholder="-">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Email -->
|
||
<div class="ui-card">
|
||
<div class="ui-head">
|
||
<h6><i class="bi bi-envelope-paper"></i> Email</h6>
|
||
<div class="d-flex align-items-center gap-2">
|
||
<span class="note">Enter proposed address(es) without <code>@@domain</code></span>
|
||
<button class="btn-soft btn-add" @@click="addEmail" :disabled="!isEditable"><i class="bi bi-plus"></i> Add</button>
|
||
</div>
|
||
</div>
|
||
<div class="ui-body">
|
||
<div class="mini-table">
|
||
<div class="mini-head">Proposed Address (without @@domain)</div>
|
||
<div v-for="(row, i) in emailRows" :key="'em-'+i" class="mini-row">
|
||
<div class="flex-grow-1">
|
||
<input class="form-control form-control-sm" v-model.trim="row.proposedAddress" :disabled="!isEditable" placeholder="e.g. j.doe">
|
||
</div>
|
||
<button class="btn btn-del btn-sm" @@click="removeEmail(i)" :disabled="!isEditable"><i class="bi bi-x"></i></button>
|
||
</div>
|
||
<div v-if="emailRows.length===0" class="mini-row">
|
||
<div class="text-muted">No email rows</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OS -->
|
||
<div class="ui-card">
|
||
<div class="ui-head">
|
||
<h6><i class="bi bi-windows"></i> Operating System Requirements</h6>
|
||
<button class="btn-soft btn-add" @@click="addOs" :disabled="!isEditable"><i class="bi bi-plus"></i> Add</button>
|
||
</div>
|
||
<div class="ui-body">
|
||
<div class="mini-table">
|
||
<div class="mini-head">Requirement</div>
|
||
<div v-for="(row, i) in osReqs" :key="'os-'+i" class="mini-row">
|
||
<textarea class="form-control" rows="2" v-model="row.requirementText" :disabled="!isEditable" placeholder="e.g. Windows 11 Pro required due to ..."></textarea>
|
||
<button class="btn btn-del btn-sm" @@click="removeOs(i)" :disabled="!isEditable"><i class="bi bi-x"></i></button>
|
||
</div>
|
||
<div v-if="osReqs.length===0" class="mini-row">
|
||
<div class="text-muted">No OS requirements</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Software -->
|
||
<div class="ui-card">
|
||
<div class="ui-head">
|
||
<h6><i class="bi bi-boxes"></i> Software</h6>
|
||
<span class="note">Tick to include; use Others to specify anything not listed</span>
|
||
</div>
|
||
<div class="ui-body">
|
||
<div class="row g-3">
|
||
<div class="col-md-4">
|
||
<h6 class="mb-2">General Software</h6>
|
||
<div class="form-check" v-for="opt in softwareGeneralOpts" :key="'gen-'+opt">
|
||
<input class="form-check-input" type="checkbox" :id="'gen_'+opt" v-model="softwareGeneral[opt]" :disabled="!isEditable">
|
||
<label class="form-check-label" :for="'gen_'+opt">{{ opt }}</label>
|
||
</div>
|
||
<div class="mt-2">
|
||
<label class="form-label">Others (Specify)</label>
|
||
<input class="form-control form-control-sm" v-model.trim="softwareGeneralOther" :disabled="!isEditable" placeholder="-">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-4">
|
||
<h6 class="mb-2">Utility Software</h6>
|
||
<div class="form-check" v-for="opt in softwareUtilityOpts" :key="'utl-'+opt">
|
||
<input class="form-check-input" type="checkbox" :id="'utl_'+opt" v-model="softwareUtility[opt]" :disabled="!isEditable">
|
||
<label class="form-check-label" :for="'utl_'+opt">{{ opt }}</label>
|
||
</div>
|
||
<div class="mt-2">
|
||
<label class="form-label">Others (Specify)</label>
|
||
<input class="form-control form-control-sm" v-model.trim="softwareUtilityOther" :disabled="!isEditable" placeholder="-">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-4">
|
||
<h6 class="mb-2">Custom Software</h6>
|
||
<label class="form-label">Others (Specify)</label>
|
||
<input class="form-control form-control-sm" v-model.trim="softwareCustomOther" :disabled="!isEditable" placeholder="-">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Shared Permissions (CAP 6) -->
|
||
<div class="ui-card">
|
||
<div class="ui-head">
|
||
<h6><i class="bi bi-share"></i> Shared Permissions</h6>
|
||
<div class="d-flex align-items-center gap-2">
|
||
<span class="note">Max 6 entries</span>
|
||
<button class="btn-soft btn-add" @@click="addPerm" :disabled="!isEditable || sharedPerms.length>=6"><i class="bi bi-plus"></i> Add</button>
|
||
</div>
|
||
</div>
|
||
<div class="ui-body">
|
||
<div class="mini-table">
|
||
<div class="mini-head">Share Name & Permissions</div>
|
||
<div v-for="(p, i) in sharedPerms" :key="'sp-'+i" class="mini-row">
|
||
<input class="form-control form-control-sm" style="max-width:280px"
|
||
v-model.trim="p.shareName" :disabled="!isEditable" placeholder="e.g. Finance Shared Folder">
|
||
<div class="perm-flags">
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" v-model="p.canRead" :id="'r'+i" :disabled="!isEditable">
|
||
<label class="form-check-label" :for="'r'+i">Read</label>
|
||
</div>
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" v-model="p.canWrite" :id="'w'+i" :disabled="!isEditable">
|
||
<label class="form-check-label" :for="'w'+i">Write</label>
|
||
</div>
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" v-model="p.canDelete" :id="'d'+i" :disabled="!isEditable">
|
||
<label class="form-check-label" :for="'d'+i">Delete</label>
|
||
</div>
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" v-model="p.canRemove" :id="'u'+i" :disabled="!isEditable">
|
||
<label class="form-check-label" :for="'u'+i">Remove User</label>
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-del btn-sm" @@click="removePerm(i)" :disabled="!isEditable"><i class="bi bi-x"></i></button>
|
||
</div>
|
||
<div v-if="sharedPerms.length===0" class="mini-row">
|
||
<div class="text-muted">No shared permissions</div>
|
||
</div>
|
||
</div>
|
||
<div class="invalid-hint" v-if="validation.sharedPerms">{{ validation.sharedPerms }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sticky Submit Bar -->
|
||
<div class="submit-bar">
|
||
<button class="btn btn-reset" @@click="reload" :disabled="busy">Reload</button>
|
||
<button class="btn btn-primary" @@click="save" :disabled="busy || !isEditable">
|
||
<span v-if="busy" class="spinner-border spinner-border-sm me-2"></span>
|
||
Save Draft
|
||
</button>
|
||
<button class="btn btn-go" @@click="sendNow" :disabled="busy || !isEditable">Send now</button>
|
||
</div>
|
||
</div>
|
||
|
||
@section Scripts {
|
||
<script>
|
||
const statusId = new URLSearchParams(location.search).get('statusId');
|
||
|
||
const app = Vue.createApp({
|
||
data() {
|
||
const plus7 = new Date(); plus7.setDate(plus7.getDate() + 7);
|
||
return {
|
||
busy: false,
|
||
isEditable: false,
|
||
remaining: 0,
|
||
minReqISO: plus7.toISOString().slice(0, 10),
|
||
validation: { requiredDate: "", hardwarePurpose: "", sharedPerms: "" },
|
||
|
||
model: {
|
||
userId: 0, staffName: "", companyName: "", departmentName: "",
|
||
designation: "", location: "", employmentStatus: "", contractEndDate: null,
|
||
requiredDate: "", phoneExt: ""
|
||
},
|
||
|
||
hardwarePurpose: "",
|
||
hardwareJustification: "",
|
||
hardwareCategories: [
|
||
{ key: "DesktopAllIn", label: "Desktop (all inclusive)", include: false },
|
||
{ key: "NotebookAllIn", label: "Notebook (all inclusive)", include: false },
|
||
{ key: "DesktopOnly", label: "Desktop only", include: false },
|
||
{ key: "NotebookOnly", label: "Notebook only", include: false },
|
||
{ key: "NotebookBattery", label: "Notebook battery", include: false },
|
||
{ key: "PowerAdapter", label: "Power Adapter", include: false },
|
||
{ key: "Mouse", label: "Computer Mouse", include: false },
|
||
{ key: "ExternalHDD", label: "External Hard Drive", include: false }
|
||
],
|
||
hardwareOther: "",
|
||
|
||
emailRows: [],
|
||
osReqs: [],
|
||
|
||
softwareGeneralOpts: ["MS Word", "MS Excel", "MS Outlook", "MS PowerPoint", "MS Access", "MS Project", "Acrobat Standard", "AutoCAD", "Worktop/ERP Login"],
|
||
softwareUtilityOpts: ["PDF Viewer", "7Zip", "AutoCAD Viewer", "Smart Draw"],
|
||
softwareGeneral: {},
|
||
softwareUtility: {},
|
||
softwareGeneralOther: "",
|
||
softwareUtilityOther: "",
|
||
softwareCustomOther: "",
|
||
|
||
// shared perms
|
||
sharedPerms: []
|
||
};
|
||
},
|
||
computed: {
|
||
hardwareCount() {
|
||
let c = this.hardwareCategories.filter(x => x.include).length;
|
||
if (this.hardwareOther.trim()) c += 1;
|
||
return c;
|
||
}
|
||
},
|
||
methods: {
|
||
// timers
|
||
startCountdown() {
|
||
const el = document.getElementById('countdown');
|
||
if (!el) return;
|
||
if (this._timer) { clearTimeout(this._timer); this._timer = null; }
|
||
const tick = () => {
|
||
if (this.remaining <= 0) { el.textContent = '0s'; this.isEditable = false; return; }
|
||
const h = Math.floor(this.remaining / 3600);
|
||
const m = Math.floor((this.remaining % 3600) / 60);
|
||
const s = this.remaining % 60;
|
||
el.textContent = (h ? `${h}h ` : '') + (m ? `${m}m ` : (h ? '0m ' : '')) + `${s}s`;
|
||
this.remaining--; this._timer = setTimeout(tick, 1000);
|
||
}; this.$nextTick(tick);
|
||
},
|
||
startSyncRemaining() {
|
||
if (this._syncTimer) { clearInterval(this._syncTimer); this._syncTimer = null; }
|
||
const doSync = async () => {
|
||
try {
|
||
const r = await fetch(`/ItRequestAPI/editWindow/${statusId}`);
|
||
if (!r.ok) return;
|
||
const j = await r.json();
|
||
const srvRemaining = (j && typeof j.remainingSeconds === 'number') ? j.remainingSeconds : null;
|
||
if (srvRemaining != null) { this.remaining = srvRemaining; this.isEditable = !!(j.isEditable); }
|
||
if (!this.isEditable || this.remaining <= 0) {
|
||
if (this._timer) { clearTimeout(this._timer); this._timer = null; }
|
||
if (this._syncTimer) { clearInterval(this._syncTimer); this._syncTimer = null; }
|
||
const el = document.getElementById('countdown'); if (el) el.textContent = '0s';
|
||
}
|
||
} catch { }
|
||
};
|
||
doSync(); this._syncTimer = setInterval(doSync, 30000);
|
||
},
|
||
teardownTimers() { if (this._timer) clearTimeout(this._timer); if (this._syncTimer) clearInterval(this._syncTimer); },
|
||
|
||
// UI helpers
|
||
addEmail() { if (!this.isEditable) return; this.emailRows.push({ proposedAddress: "" }); },
|
||
removeEmail(i) { if (!this.isEditable) return; this.emailRows.splice(i, 1); },
|
||
addOs() { if (!this.isEditable) return; this.osReqs.push({ requirementText: "" }); },
|
||
removeOs(i) { if (!this.isEditable) return; this.osReqs.splice(i, 1); },
|
||
|
||
addPerm() { if (!this.isEditable) return; if (this.sharedPerms.length >= 6) return; this.sharedPerms.push({ shareName: "", canRead: true, canWrite: false, canDelete: false, canRemove: false }); },
|
||
removePerm(i) { if (!this.isEditable) return; this.sharedPerms.splice(i, 1); },
|
||
|
||
onHardwareToggle(key) {
|
||
if (!this.isEditable) return;
|
||
const set = (k, v) => { const t = this.hardwareCategories.find(x => x.key === k); if (t) t.include = v; };
|
||
if (key === "DesktopAllIn") {
|
||
const on = this.hardwareCategories.find(x => x.key === "DesktopAllIn")?.include;
|
||
if (on) { set("NotebookAllIn", false); set("NotebookOnly", false); set("Mouse", true); }
|
||
}
|
||
if (key === "NotebookAllIn") {
|
||
const on = this.hardwareCategories.find(x => x.key === "NotebookAllIn")?.include;
|
||
if (on) { set("DesktopAllIn", false); set("DesktopOnly", false); set("PowerAdapter", true); set("Mouse", true); set("NotebookBattery", true); }
|
||
}
|
||
if (key === "DesktopOnly") { const on = this.hardwareCategories.find(x => x.key === "DesktopOnly")?.include; if (on) { set("DesktopAllIn", false); } }
|
||
if (key === "NotebookOnly") { const on = this.hardwareCategories.find(x => x.key === "NotebookOnly")?.include; if (on) { set("NotebookAllIn", false); } }
|
||
},
|
||
|
||
// validation
|
||
validate() {
|
||
this.validation = { requiredDate: "", hardwarePurpose: "", sharedPerms: "" };
|
||
if (!this.model.requiredDate) this.validation.requiredDate = "Required Date is mandatory.";
|
||
else if (this.model.requiredDate < this.minReqISO) this.validation.requiredDate = "Required Date must be at least 7 days from today.";
|
||
if (this.hardwareCount > 0 && !this.hardwarePurpose) this.validation.hardwarePurpose = "Please select a Hardware Purpose.";
|
||
if (this.sharedPerms.length > 6) this.validation.sharedPerms = "Maximum 6 shared permissions.";
|
||
return !this.validation.requiredDate && !this.validation.hardwarePurpose && !this.validation.sharedPerms;
|
||
},
|
||
|
||
// dto
|
||
buildDto() {
|
||
const hardware = [];
|
||
const justification = this.hardwareJustification || "";
|
||
const purpose = this.hardwarePurpose || "";
|
||
this.hardwareCategories.forEach(c => { if (c.include) hardware.push({ category: c.key, purpose, justification, otherDescription: "" }); });
|
||
if (this.hardwareOther.trim()) { hardware.push({ category: "Other", purpose, justification, otherDescription: this.hardwareOther.trim() }); }
|
||
const emails = this.emailRows.map(x => ({ proposedAddress: x.proposedAddress || "" }));
|
||
const oSReqs = this.osReqs.map(x => ({ requirementText: x.requirementText }));
|
||
const software = [];
|
||
Object.keys(this.softwareGeneral).forEach(n => { if (this.softwareGeneral[n]) software.push({ bucket: "General", name: n, otherName: "", notes: "" }); });
|
||
Object.keys(this.softwareUtility).forEach(n => { if (this.softwareUtility[n]) software.push({ bucket: "Utility", name: n, otherName: "", notes: "" }); });
|
||
if (this.softwareGeneralOther?.trim()) software.push({ bucket: "General", name: "Others", otherName: this.softwareGeneralOther.trim(), notes: "" });
|
||
if (this.softwareUtilityOther?.trim()) software.push({ bucket: "Utility", name: "Others", otherName: this.softwareUtilityOther.trim(), notes: "" });
|
||
if (this.softwareCustomOther?.trim()) software.push({ bucket: "Custom", name: "Others", otherName: this.softwareCustomOther.trim(), notes: "" });
|
||
|
||
const sharedPerms = this.sharedPerms.slice(0, 6).map(x => ({
|
||
shareName: x.shareName || "", canRead: !!x.canRead, canWrite: !!x.canWrite, canDelete: !!x.canDelete, canRemove: !!x.canRemove
|
||
}));
|
||
|
||
return {
|
||
staffName: this.model.staffName, companyName: this.model.companyName, departmentName: this.model.departmentName,
|
||
designation: this.model.designation, location: this.model.location, employmentStatus: this.model.employmentStatus,
|
||
contractEndDate: this.model.contractEndDate || null, requiredDate: this.model.requiredDate, phoneExt: this.model.phoneExt,
|
||
hardware, emails, oSReqs, software, sharedPerms
|
||
};
|
||
},
|
||
|
||
async load() {
|
||
try {
|
||
this.busy = true;
|
||
const r = await fetch(`/ItRequestAPI/request/${statusId}`); if (!r.ok) throw new Error('Failed to load request');
|
||
const j = await r.json();
|
||
|
||
const req = j.request || {};
|
||
this.model.staffName = req.staffName || ""; this.model.companyName = req.companyName || "";
|
||
this.model.departmentName = req.departmentName || ""; this.model.designation = req.designation || "";
|
||
this.model.location = req.location || ""; this.model.employmentStatus = req.employmentStatus || "";
|
||
this.model.contractEndDate = req.contractEndDate || null;
|
||
this.model.requiredDate = req.requiredDate ? req.requiredDate.substring(0, 10) : "";
|
||
this.model.phoneExt = req.phoneExt || "";
|
||
|
||
this.hardwarePurpose = ""; this.hardwareJustification = ""; this.hardwareOther = "";
|
||
this.hardwareCategories.forEach(x => x.include = false);
|
||
(j.hardware || []).forEach(h => {
|
||
if (h.purpose && !this.hardwarePurpose) this.hardwarePurpose = h.purpose;
|
||
if (h.justification && !this.hardwareJustification) this.hardwareJustification = h.justification;
|
||
if (h.category === "Other") { if (h.otherDescription) this.hardwareOther = h.otherDescription; }
|
||
else { const t = this.hardwareCategories.find(c => c.key === h.category); if (t) t.include = true; }
|
||
});
|
||
|
||
this.emailRows = (j.emails || []).map(e => ({ proposedAddress: e.proposedAddress || "" }));
|
||
this.osReqs = (j.osreqs || []).map(o => ({ requirementText: o.requirementText || "" }));
|
||
|
||
this.softwareGeneral = {}; this.softwareUtility = {};
|
||
this.softwareGeneralOther = ""; this.softwareUtilityOther = ""; this.softwareCustomOther = "";
|
||
(j.software || []).forEach(sw => {
|
||
if (sw.bucket === "General" && sw.name !== "Others") this.softwareGeneral[sw.name] = true;
|
||
else if (sw.bucket === "Utility" && sw.name !== "Others") this.softwareUtility[sw.name] = true;
|
||
else if (sw.bucket === "General" && sw.name === "Others") this.softwareGeneralOther = sw.otherName || "";
|
||
else if (sw.bucket === "Utility" && sw.name === "Others") this.softwareUtilityOther = sw.otherName || "";
|
||
else if (sw.bucket === "Custom") this.softwareCustomOther = sw.otherName || "";
|
||
});
|
||
|
||
// Shared perms from API (if present)
|
||
this.sharedPerms = (j.sharedPerms || []).map(sp => ({
|
||
shareName: sp.shareName || "",
|
||
canRead: !!sp.canRead, canWrite: !!sp.canWrite,
|
||
canDelete: !!sp.canDelete, canRemove: !!sp.canRemove
|
||
})).slice(0, 6);
|
||
|
||
this.isEditable = !!(j.edit && j.edit.isEditable);
|
||
this.remaining = (j.edit && j.edit.remainingSeconds) || 0;
|
||
|
||
this.startCountdown(); this.startSyncRemaining();
|
||
} catch (e) { alert(e.message || 'Load error'); } finally { this.busy = false; }
|
||
},
|
||
|
||
async save() {
|
||
if (!this.isEditable) return;
|
||
if (!this.validate()) return;
|
||
try {
|
||
this.busy = true;
|
||
const dto = this.buildDto();
|
||
const r = await fetch(`/ItRequestAPI/edit/${statusId}`, {
|
||
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(Object.assign({ statusId: +statusId }, dto))
|
||
});
|
||
const j = await r.json().catch(() => ({}));
|
||
if (!r.ok) throw new Error(j.message || 'Save failed');
|
||
if (typeof j.remainingSeconds === 'number') { this.remaining = j.remainingSeconds; this.startCountdown(); }
|
||
} catch (e) { alert(e.message || 'Save error'); } finally { this.busy = false; }
|
||
},
|
||
|
||
async sendNow() {
|
||
if (!this.isEditable) return;
|
||
if (!this.validate()) return;
|
||
if (!confirm('Send to approvals now? You won’t be able to edit after this.')) return;
|
||
try {
|
||
this.busy = true;
|
||
const r = await fetch(`/ItRequestAPI/sendNow/${statusId}`, { method: 'POST' });
|
||
const j = await r.json().catch(() => ({}));
|
||
if (!r.ok) throw new Error(j.message || 'Send failed');
|
||
alert('Sent to approvals.');
|
||
window.location.href = `/IT/ApprovalDashboard/MyRequests`;
|
||
} catch (e) { alert(e.message || 'Send error'); } finally { this.busy = false; }
|
||
},
|
||
|
||
async reload() { this.teardownTimers(); await this.load(); }
|
||
},
|
||
mounted() {
|
||
this.load();
|
||
window.addEventListener('beforeunload', this.teardownTimers);
|
||
}
|
||
});
|
||
app.mount('#editApp');
|
||
</script>
|
||
}
|