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

699 lines
33 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"] = "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 &amp; 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 wont 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>
}