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

474 lines
19 KiB
Plaintext

@{
ViewData["Title"] = "IT Request Assignments";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<!-- Bootstrap Icons (kill this if you already include it in _Layout) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
<style>
.card {
border-radius: 14px;
box-shadow: 0 6px 18px rgba(0,0,0,.06);
border: 0;
}
.card-header {
background: #f9fbff;
border-bottom: 1px solid #eef2f7;
border-top-left-radius: 14px;
border-top-right-radius: 14px;
}
.table-container {
background: #fff;
border-radius: 14px;
box-shadow: 0 6px 18px rgba(0,0,0,.06);
padding: 16px;
}
.form-text {
font-size: 12px;
color: #6c757d;
}
.w-110 {
width: 110px;
}
/* === IT Team nicer UI === */
.it-card .card-header {
background: #f8faff;
}
.it-list {
max-height: 280px;
overflow: auto;
border: 1px solid #eef2f7;
border-radius: 10px;
padding: 6px;
background: #fff;
}
.it-item {
display: flex;
align-items: center;
gap: .5rem;
padding: 6px 8px;
border-radius: 8px;
cursor: pointer;
}
.it-item:hover {
background: #f7f9fc;
}
.it-item input {
transform: translateY(1px);
}
.it-name {
font-weight: 600;
color: #334155;
}
.it-selected {
min-height: 48px;
border: 1px dashed #dbe3ef;
background: #fbfdff;
border-radius: 10px;
padding: 8px;
}
.chip {
display: inline-flex;
align-items: center;
gap: .4rem;
padding: .28rem .5rem;
margin: 4px;
border-radius: 999px;
background: #eef2ff;
color: #3949ab;
font-weight: 600;
font-size: 12px;
}
.chip-x {
border: 0;
background: transparent;
color: #6b7280;
line-height: 1;
font-size: 16px;
padding: 0 2px;
cursor: pointer;
}
.chip-x:hover {
color: #111827;
}
</style>
<div id="flowApp" style="max-width:1200px; margin:auto; font-size:13px;">
<!-- FLOWS CARD -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="m-0"></h5>
<div>
<button class="btn btn-primary btn-sm" @@click="openCreate">New Flow</button>
<button class="btn btn-outline-secondary btn-sm ms-2" @@click="load">Refresh</button>
</div>
</div>
<div class="card-body">
<div v-if="error" class="alert alert-danger py-2">{{ error }}</div>
<div v-if="busy" class="alert alert-secondary py-2">Loading…</div>
<div class="table-container table-responsive">
<table class="table table-bordered table-sm table-striped align-middle text-center">
<thead class="table-light">
<tr>
<th class="w-110">Flow ID</th>
<th>Flow Name</th>
<th>HOD</th>
<th>Group IT HOD</th>
<th>FIN HOD</th>
<th>MGMT</th>
<th style="width:200px;">Action</th>
</tr>
</thead>
<tbody>
<tr v-for="f in flows" :key="f.itApprovalFlowId">
<td>{{ f.itApprovalFlowId }}</td>
<td class="text-start">{{ f.flowName }}</td>
<!-- show id + resolved name so admins aren't guessing -->
<td>{{ f.hodUserId ? `${f.hodUserId} — ${resolveUserName(f.hodUserId)}` : '-' }}</td>
<td>{{ f.groupItHodUserId ? `${f.groupItHodUserId} — ${resolveUserName(f.groupItHodUserId)}` : '-' }}</td>
<td>{{ f.finHodUserId ? `${f.finHodUserId} — ${resolveUserName(f.finHodUserId)}` : '-' }}</td>
<td>{{ f.mgmtUserId ? `${f.mgmtUserId} — ${resolveUserName(f.mgmtUserId)}` : '-' }}</td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" @@click="openEdit(f)">Edit</button>
<button class="btn btn-sm btn-outline-danger" @@click="del(f)">Delete</button>
</div>
</td>
</tr>
<tr v-if="!flows.length && !busy">
<td colspan="7" class="text-muted">No flows yet. Click “New Flow”.</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-2 text-muted">
<small>
Heads up: <code>/ItRequestAPI/create</code> uses the first flow in the DB. Make sure one exists w/ approvers.
</small>
</div>
</div>
</div>
<!-- IT TEAM CARD -->
<div class="card mt-3 it-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="m-0">IT Team Members</h6>
<button class="btn btn-sm btn-primary" @@click="saveItTeam" :disabled="savingTeam">
{{ savingTeam ? 'Saving…' : 'Save' }}
</button>
</div>
<div class="card-body">
<div class="text-muted mb-3">
<small>Select existing users to mark them as IT Team (they can edit Section B + do IT acceptance).</small>
</div>
<div class="row g-3 align-items-start">
<!-- LEFT: Search + Available -->
<div class="col-md-7">
<div class="input-group input-group-sm mb-2">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" placeholder="Search users by name…" v-model.trim="itSearch">
</div>
<div class="it-list">
<label v-for="u in filteredUsers" :key="'avail-'+u.id" class="it-item">
<input type="checkbox" :value="u.id" v-model="itTeamUserIds">
<span class="it-name">{{ u.name }}</span>
</label>
<div v-if="!filteredUsers.length" class="text-muted small p-2">
No users match your search.
</div>
</div>
</div>
<!-- RIGHT: Selected chips -->
<div class="col-md-5">
<div class="d-flex justify-content-between align-items-center mb-2">
<strong>Selected ({{ selectedUsers.length }})</strong>
<button class="btn btn-link btn-sm text-decoration-none"
@@click="itTeamUserIds = []"
:disabled="!selectedUsers.length">
Clear all
</button>
</div>
<div class="it-selected">
<span v-for="u in selectedUsers" :key="'sel-'+u.id" class="chip">
{{ u.name }}
<button class="chip-x" @@click="removeIt(u.id)" aria-label="Remove">&times;</button>
</span>
<div v-if="!selectedUsers.length" class="text-muted small">Nobody selected yet.</div>
</div>
</div>
</div>
</div>
</div>
<!-- FLOW MODAL -->
<div class="modal fade" id="flowModal" tabindex="-1" aria-hidden="true" ref="modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title">{{ form.itApprovalFlowId ? 'Edit Flow' : 'Create Flow' }}</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @@click="closeModal"></button>
</div>
<div class="modal-body">
<div v-if="formError" class="alert alert-danger py-2">{{ formError }}</div>
<div class="mb-2">
<label class="form-label">Flow Name</label>
<input class="form-control form-control-sm" v-model.trim="form.flowName" placeholder="e.g., Default Flow">
</div>
<div class="row g-2">
<div class="col-6">
<label class="form-label">HOD Approver</label>
<select class="form-select form-select-sm" v-model.number="form.hodUserId">
<option :value="null">— None —</option>
<option v-for="u in users" :key="'hod-'+u.id" :value="u.id">{{ u.name }}</option>
</select>
<div class="form-text">Approver for HOD stage</div>
</div>
<div class="col-6">
<label class="form-label">Group IT HOD</label>
<select class="form-select form-select-sm" v-model.number="form.groupItHodUserId">
<option :value="null">— None —</option>
<option v-for="u in users" :key="'git-'+u.id" :value="u.id">{{ u.name }}</option>
</select>
<div class="form-text">Approver for Group IT HOD</div>
</div>
<div class="col-6">
<label class="form-label">Finance HOD</label>
<select class="form-select form-select-sm" v-model.number="form.finHodUserId">
<option :value="null">— None —</option>
<option v-for="u in users" :key="'fin-'+u.id" :value="u.id">{{ u.name }}</option>
</select>
<div class="form-text">Approver for Finance HOD</div>
</div>
<div class="col-6">
<label class="form-label">Management</label>
<select class="form-select form-select-sm" v-model.number="form.mgmtUserId">
<option :value="null">— None —</option>
<option v-for="u in users" :key="'mgmt-'+u.id" :value="u.id">{{ u.name }}</option>
</select>
<div class="form-text">Final management approver</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal" @@click="closeModal">Cancel</button>
<button class="btn btn-primary btn-sm" :disabled="saving" @@click="save">
{{ saving ? 'Saving…' : 'Save' }}
</button>
</div>
</div>
</div>
</div>
</div>
<script>
const flowApp = Vue.createApp({
data() {
return {
flows: [],
users: [], // all users (name only for display)
itTeamUserIds: [], // selected user IDs for IT team
itSearch: '', // search term
busy: false,
error: null,
saving: false,
savingTeam: false,
formError: null,
form: {
itApprovalFlowId: null,
flowName: '',
hodUserId: null,
groupItHodUserId: null,
finHodUserId: null,
mgmtUserId: null
},
bsModal: null
};
},
computed: {
filteredUsers() {
const q = (this.itSearch || '').toLowerCase();
if (!q) return this.users;
return this.users.filter(u => (u.name || '').toLowerCase().includes(q));
},
selectedUsers() {
const set = new Set(this.itTeamUserIds);
return this.users.filter(u => set.has(u.id));
}
},
methods: {
async load() {
try {
this.busy = true; this.error = null;
const r = await fetch('/ItRequestAPI/flows');
if (!r.ok) throw new Error(`Load failed (${r.status})`);
const data = await r.json();
this.flows = Array.isArray(data)
? data.map(x => ({
itApprovalFlowId: x.itApprovalFlowId ?? x.ItApprovalFlowId,
flowName: x.flowName ?? x.FlowName,
hodUserId: x.hodUserId ?? x.HodUserId,
groupItHodUserId: x.groupItHodUserId ?? x.GroupItHodUserId,
finHodUserId: x.finHodUserId ?? x.FinHodUserId,
mgmtUserId: x.mgmtUserId ?? x.MgmtUserId
}))
: [];
} catch (e) {
this.error = e.message || 'Failed to load flows.';
} finally {
this.busy = false;
}
},
async loadUsers() {
const res = await fetch('/ItRequestAPI/users');
this.users = await res.json();
},
async loadItTeam() {
const r = await fetch('/ItRequestAPI/itTeam');
this.itTeamUserIds = await r.json(); // array<int>
},
resolveUserName(id) {
const u = this.users.find(x => x.id === id);
return u ? u.name : '(unknown)';
},
openCreate() {
this.formError = null;
this.form = { itApprovalFlowId: null, flowName: '', hodUserId: null, groupItHodUserId: null, finHodUserId: null, mgmtUserId: null };
this.showModal();
this.loadUsers();
},
openEdit(f) {
this.formError = null;
this.form = JSON.parse(JSON.stringify(f));
this.showModal();
this.loadUsers();
},
async save() {
try {
if (this.saving) return;
this.saving = true; this.formError = null;
if (!this.form.flowName || !this.form.flowName.trim()) {
this.formError = 'Flow name is required.'; this.saving = false; return;
}
const payload = {
flowName: this.form.flowName.trim(),
hodUserId: this.nullIfEmpty(this.form.hodUserId),
groupItHodUserId: this.nullIfEmpty(this.form.groupItHodUserId),
finHodUserId: this.nullIfEmpty(this.form.finHodUserId),
mgmtUserId: this.nullIfEmpty(this.form.mgmtUserId)
};
let res;
if (this.form.itApprovalFlowId) {
res = await fetch(`/ItRequestAPI/flows/${this.form.itApprovalFlowId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} else {
res = await fetch('/ItRequestAPI/flows', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
if (!res.ok) {
const j = await res.json().catch(() => ({}));
throw new Error(j.message || `Save failed (${res.status})`);
}
await this.load();
this.closeModal();
} catch (e) {
this.formError = e.message || 'Unable to save flow.';
} finally {
this.saving = false;
}
},
async del(f) {
if (!confirm(`Delete flow "${f.flowName}"?`)) return;
try {
const r = await fetch(`/ItRequestAPI/flows/${f.itApprovalFlowId}`, { method: 'DELETE' });
if (!r.ok) {
const j = await r.json().catch(() => ({}));
throw new Error(j.message || `Delete failed (${r.status})`);
}
await this.load();
} catch (e) {
alert(e.message || 'Delete failed.');
}
},
nullIfEmpty(v) {
if (v === undefined || v === null || v === '') return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
},
showModal() {
const el = document.getElementById('flowModal');
this.bsModal = new bootstrap.Modal(el);
this.bsModal.show();
},
closeModal() {
if (this.bsModal) this.bsModal.hide();
},
removeIt(uid) {
this.itTeamUserIds = this.itTeamUserIds.filter(id => id !== uid);
},
async saveItTeam() {
try {
this.savingTeam = true;
const r = await fetch('/ItRequestAPI/itTeam', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userIds: this.itTeamUserIds })
});
if (!r.ok) {
const j = await r.json().catch(() => ({}));
throw new Error(j.message || `Save failed (${r.status})`);
}
alert('IT Team updated.');
} catch (e) {
alert(e.message || 'Failed to update IT Team.');
} finally {
this.savingTeam = false;
}
}
},
async mounted() {
await Promise.all([this.load(), this.loadUsers(), this.loadItTeam()]);
}
});
flowApp.mount('#flowApp');
</script>