826 lines
34 KiB
Plaintext
826 lines
34 KiB
Plaintext
@{
|
|
ViewData["Title"] = "New 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;
|
|
--ok: #16a34a;
|
|
--warn: #f59e0b;
|
|
--err: #dc2626;
|
|
}
|
|
|
|
#itFormApp {
|
|
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;
|
|
}
|
|
|
|
.page-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .6rem;
|
|
margin: 0;
|
|
font-weight: 800;
|
|
letter-spacing: .2px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: .45rem;
|
|
padding: .25rem .6rem;
|
|
border-radius: 999px;
|
|
font-weight: 700;
|
|
font-size: 12px;
|
|
border: 1px solid rgba(0,0,0,.06);
|
|
background: #eef2f7;
|
|
color: #334155;
|
|
}
|
|
|
|
.chip i {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.chip-ok {
|
|
background: #e7f7ed;
|
|
color: #166534;
|
|
border-color: #c7ecd3;
|
|
}
|
|
|
|
.chip-warn {
|
|
background: #fff7e6;
|
|
color: #92400e;
|
|
border-color: #fdebd1;
|
|
}
|
|
|
|
.form-label .req {
|
|
color: var(--err);
|
|
margin-left: .2rem;
|
|
}
|
|
|
|
.invalid-hint {
|
|
color: var(--err);
|
|
font-size: 12px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.req-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2,1fr);
|
|
gap: 12px 18px;
|
|
}
|
|
|
|
@@media (max-width:768px) {
|
|
.req-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.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-send {
|
|
background: #0b5ed7;
|
|
color: #fff;
|
|
border-radius: 10px;
|
|
padding: .6rem .95rem;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.btn-send:hover {
|
|
background: #0a53be;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.muted {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.perm-flags {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.divider {
|
|
height: 1px;
|
|
background: #eef2f6;
|
|
margin: 8px 0;
|
|
}
|
|
</style>
|
|
|
|
<div id="itFormApp">
|
|
<div class="page-head">
|
|
<h3 class="page-title"></h3>
|
|
<div class="subtle">Stages: HOD → Group IT HOD → Finance HOD → Management</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" readonly>
|
|
</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" readonly>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Phone Ext</label>
|
|
<input type="text" class="form-control" v-model.trim="model.phoneExt" readonly>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Employment Status</label>
|
|
<select class="form-select" v-model="model.employmentStatus" disabled>
|
|
<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" readonly>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Required Date <span class="req">*</span></label>
|
|
<input type="date" class="form-control" v-model="model.requiredDate" :min="minReqISO">
|
|
<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 class="d-flex align-items-center gap-2">
|
|
<span class="chip" v-if="hardwareCount===0"><i class="bi bi-inboxes"></i>None selected</span>
|
|
<span class="chip chip-ok" v-else><i class="bi bi-check2"></i>{{ hardwareCount }} selected</span>
|
|
</div>
|
|
</div>
|
|
<div class="ui-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Purpose <span class="req" v-if="hardwareCount>0">*</span></label>
|
|
<select class="form-select" v-model="hardwarePurpose">
|
|
<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" 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" @@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" 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"><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" placeholder="e.g. j.doe">
|
|
</div>
|
|
<button class="btn btn-del btn-sm" @@click="removeEmail(i)"><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"><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" placeholder="e.g. Windows 11 Pro required due to ..."></textarea>
|
|
<button class="btn btn-del btn-sm" @@click="removeOs(i)"><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]">
|
|
<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" 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]">
|
|
<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" 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" 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="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" 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">
|
|
<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">
|
|
<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">
|
|
<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">
|
|
<label class="form-check-label" :for="'u'+i">Remove</label>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-del btn-sm" @@click="removePerm(i)"><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>
|
|
|
|
<!-- Submit bar -->
|
|
<div class="submit-bar">
|
|
<div class="me-auto d-flex align-items-center gap-2">
|
|
<span class="chip" :class="model.requiredDate ? 'chip-ok' : 'chip-warn'">
|
|
<i class="bi" :class="model.requiredDate ? 'bi-check2' : 'bi-exclamation-triangle'"></i>
|
|
{{ model.requiredDate ? 'Required date set' : 'Required date missing' }}
|
|
</span>
|
|
<span class="chip" v-if="hardwareCount>0 && !hardwarePurpose"><i class="bi bi-exclamation-triangle"></i> Hardware purpose required</span>
|
|
<span class="chip chip-ok" v-else-if="hardwareCount>0"><i class="bi bi-check2"></i> Hardware purpose ok</span>
|
|
<span class="chip" v-if="sharedPerms.length>6"><i class="bi bi-exclamation-triangle"></i> Max 6 permissions</span>
|
|
</div>
|
|
<button class="btn btn-reset" @@click="resetForm" :disabled="saving">Reset (sections)</button>
|
|
<button class="btn btn-go" @@click="saveDraft" :disabled="saving">
|
|
<span v-if="saving && intent==='draft'" class="spinner-border spinner-border-sm me-2"></span>
|
|
Save Draft
|
|
</button>
|
|
<button class="btn btn-send" @@click="openConfirm" :disabled="saving">
|
|
<span v-if="saving && intent==='send'" class="spinner-border spinner-border-sm me-2"></span>
|
|
Send Now
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="sendConfirm" tabindex="-1" aria-labelledby="sendConfirmLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content" style="border-radius:14px;">
|
|
<div class="modal-header">
|
|
<h6 class="modal-title fw-bold" id="sendConfirmLabel">Submit & Lock</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
Please double-check your entries. Once sent, this request becomes <strong>Pending</strong> and is <strong>locked</strong> from editing.
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" id="confirmSendBtn">Yes, Send Now</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
async function ensureBootstrapModal() {
|
|
if (window.bootstrap && window.bootstrap.Modal) return;
|
|
await new Promise((resolve) => {
|
|
const s = document.createElement('script');
|
|
s.src = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js";
|
|
s.async = true;
|
|
s.onload = resolve;
|
|
s.onerror = resolve;
|
|
document.head.appendChild(s);
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
const EDIT_WINDOW_HOURS = 24;
|
|
|
|
const app = Vue.createApp({
|
|
data() {
|
|
const plus7 = new Date(); plus7.setDate(plus7.getDate() + 7);
|
|
return {
|
|
saving: false,
|
|
intent: '',
|
|
validation: { requiredDate: "", hardwarePurpose: "", sharedPerms: "" },
|
|
minReqISO: plus7.toISOString().slice(0, 10),
|
|
|
|
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 permissions
|
|
sharedPerms: []
|
|
};
|
|
},
|
|
computed: {
|
|
hardwareCount() {
|
|
let c = this.hardwareCategories.filter(x => x.include).length;
|
|
if (this.hardwareOther.trim()) c += 1;
|
|
return c;
|
|
}
|
|
},
|
|
methods: {
|
|
// ----- Hardware helpers -----
|
|
onHardwareToggle(key) {
|
|
const set = (k, v) => {
|
|
const t = this.hardwareCategories.find(x => x.key === k);
|
|
if (t) t.include = v;
|
|
};
|
|
if (key === "DesktopAllIn") {
|
|
const allIn = this.hardwareCategories.find(x => x.key === "DesktopAllIn")?.include;
|
|
if (allIn) {
|
|
// mutually exclusive with NotebookOnly/NotebookAllIn
|
|
set("NotebookAllIn", false);
|
|
set("NotebookOnly", false);
|
|
// sensible accessories
|
|
set("Mouse", true);
|
|
// desktop doesn't need PowerAdapter
|
|
}
|
|
}
|
|
if (key === "NotebookAllIn") {
|
|
const allIn = this.hardwareCategories.find(x => x.key === "NotebookAllIn")?.include;
|
|
if (allIn) {
|
|
set("DesktopAllIn", false);
|
|
set("DesktopOnly", false);
|
|
// sensible accessories
|
|
set("PowerAdapter", true);
|
|
set("Mouse", true);
|
|
set("NotebookBattery", true);
|
|
}
|
|
}
|
|
if (key === "DesktopOnly") {
|
|
const only = this.hardwareCategories.find(x => x.key === "DesktopOnly")?.include;
|
|
if (only) {
|
|
set("DesktopAllIn", false);
|
|
}
|
|
}
|
|
if (key === "NotebookOnly") {
|
|
const only = this.hardwareCategories.find(x => x.key === "NotebookOnly")?.include;
|
|
if (only) {
|
|
set("NotebookAllIn", false);
|
|
}
|
|
}
|
|
},
|
|
|
|
// ----- Email/OS -----
|
|
addEmail() { this.emailRows.push({ proposedAddress: "" }); },
|
|
removeEmail(i) { this.emailRows.splice(i, 1); },
|
|
addOs() { this.osReqs.push({ requirementText: "" }); },
|
|
removeOs(i) { this.osReqs.splice(i, 1); },
|
|
|
|
// ----- Shared perms -----
|
|
addPerm() {
|
|
if (this.sharedPerms.length >= 6) return;
|
|
this.sharedPerms.push({ shareName: "", canRead: true, canWrite: false, canDelete: false, canRemove: false });
|
|
},
|
|
removePerm(i) { this.sharedPerms.splice(i, 1); },
|
|
|
|
// ----- 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.";
|
|
}
|
|
|
|
const anyHardware = this.hardwareCount > 0;
|
|
if (anyHardware && !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(name => { if (this.softwareGeneral[name]) software.push({ bucket: "General", name, otherName: "", notes: "" }); });
|
|
Object.keys(this.softwareUtility).forEach(name => { if (this.softwareUtility[name]) software.push({ bucket: "Utility", name, 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: "" });
|
|
|
|
// shared perms (cap at 6 client-side)
|
|
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,
|
|
editWindowHours: EDIT_WINDOW_HOURS,
|
|
hardware, emails, OSReqs, software, sharedPerms
|
|
};
|
|
},
|
|
|
|
async createRequest(sendNow = false) {
|
|
const dto = this.buildDto();
|
|
dto.sendNow = !!sendNow;
|
|
const r = await fetch('/ItRequestAPI/create', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(dto)
|
|
});
|
|
const ct = r.headers.get('content-type') || '';
|
|
const payload = ct.includes('application/json') ? await r.json() : { message: await r.text() };
|
|
if (!r.ok) throw new Error(payload?.message || `Create failed (${r.status})`);
|
|
const statusId = payload?.statusId;
|
|
if (!statusId) throw new Error('Create succeeded but no statusId returned.');
|
|
return statusId;
|
|
},
|
|
|
|
async saveDraft() {
|
|
if (!this.validate()) return;
|
|
this.saving = true; this.intent = 'draft';
|
|
try {
|
|
await this.createRequest(false);
|
|
window.location.href = `/IT/ApprovalDashboard/MyRequests`;
|
|
} catch (e) {
|
|
alert('Error: ' + (e?.message || e));
|
|
} finally { this.saving = false; this.intent = ''; }
|
|
},
|
|
|
|
async openConfirm() {
|
|
if (!this.validate()) return;
|
|
await ensureBootstrapModal();
|
|
const modalEl = document.getElementById('sendConfirm');
|
|
if (!window.bootstrap || !bootstrap.Modal) {
|
|
if (confirm('Submit & Lock?\nOnce sent, this request becomes Pending and is locked from editing.')) {
|
|
this.sendNow();
|
|
}
|
|
return;
|
|
}
|
|
const Modal = bootstrap.Modal;
|
|
const inst = (typeof Modal.getOrCreateInstance === 'function')
|
|
? Modal.getOrCreateInstance(modalEl)
|
|
: (Modal.getInstance(modalEl) || new Modal(modalEl));
|
|
const btn = document.getElementById('confirmSendBtn');
|
|
btn.onclick = () => { inst.hide(); this.sendNow(); };
|
|
inst.show();
|
|
},
|
|
|
|
async sendNow() {
|
|
if (!this.validate()) return;
|
|
this.saving = true; this.intent = 'send';
|
|
try {
|
|
await this.createRequest(true);
|
|
window.location.href = `/IT/ApprovalDashboard/MyRequests`;
|
|
} catch (e) {
|
|
alert('Error: ' + (e?.message || e));
|
|
} finally {
|
|
this.saving = false; this.intent = '';
|
|
}
|
|
},
|
|
|
|
resetForm() {
|
|
this.hardwarePurpose = "";
|
|
this.hardwareJustification = "";
|
|
this.hardwareCategories.forEach(x => x.include = false);
|
|
this.hardwareOther = "";
|
|
this.emailRows = [];
|
|
this.osReqs = [];
|
|
this.softwareGeneral = {};
|
|
this.softwareUtility = {};
|
|
this.softwareGeneralOther = "";
|
|
this.softwareUtilityOther = "";
|
|
this.softwareCustomOther = "";
|
|
this.sharedPerms = [];
|
|
this.model.requiredDate = "";
|
|
this.validation = { requiredDate: "", hardwarePurpose: "", sharedPerms: "" };
|
|
},
|
|
|
|
async prefillFromServer() {
|
|
try {
|
|
const res = await fetch('/ItRequestAPI/me');
|
|
if (!res.ok) return;
|
|
const me = await res.json();
|
|
this.model.userId = me.userId || 0;
|
|
this.model.staffName = me.staffName || "";
|
|
this.model.companyName = me.companyName || "";
|
|
this.model.departmentName = me.departmentName || "";
|
|
this.model.designation = me.designation || "";
|
|
this.model.location = me.location || "";
|
|
this.model.employmentStatus = me.employmentStatus || "";
|
|
this.model.contractEndDate = me.contractEndDate || null;
|
|
this.model.phoneExt = me.phoneExt || "";
|
|
} catch { /* ignore */ }
|
|
}
|
|
},
|
|
mounted() { this.prefillFromServer(); }
|
|
});
|
|
app.mount('#itFormApp');
|
|
</script>
|
|
}
|