458 lines
20 KiB
Plaintext
458 lines
20 KiB
Plaintext
@{
|
|
ViewData["Title"] = "Overtime Pending Approval";
|
|
Layout = "~/Views/Shared/_Layout.cshtml";
|
|
}
|
|
|
|
<style>
|
|
body {
|
|
background-color: #f3f4f6;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
.table-layer {
|
|
background-color: #fff;
|
|
border-radius: 10px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
padding: 25px;
|
|
margin-top: 20px;
|
|
border: 1px solid #e0e0e0;
|
|
}
|
|
|
|
.table-container table {
|
|
width: 100%;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.header {
|
|
background-color: #007bff;
|
|
color: white;
|
|
text-align: center;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.btn-sm {
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.badge-pending {
|
|
background-color: #ffc107;
|
|
color: #343a40;
|
|
}
|
|
|
|
.badge-approved {
|
|
background-color: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.badge-rejected {
|
|
background-color: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
.badge-secondary {
|
|
background-color: #6c757d;
|
|
color: white;
|
|
}
|
|
|
|
.action-buttons button {
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.action-buttons button:last-child {
|
|
margin-right: 0;
|
|
}
|
|
|
|
.sortable-header {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
position: relative;
|
|
padding-right: 20px;
|
|
}
|
|
|
|
.sortable-header .sort-icon {
|
|
position: absolute;
|
|
right: 5px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
font-size: 0.8em;
|
|
}
|
|
|
|
.pagination-controls {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 10px 0;
|
|
border-top: 1px solid #e9ecef;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.pagination-info {
|
|
font-size: 0.85em;
|
|
color: #555;
|
|
margin-right: 15px;
|
|
}
|
|
|
|
.pagination-items-per-page {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.pagination-items-per-page label {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.formal-pending-notice {
|
|
margin-top: 15px;
|
|
margin-bottom: 20px;
|
|
padding: 12px 20px;
|
|
border-radius: 8px;
|
|
background-color: #f8d7da;
|
|
border: 1px solid #dc3545;
|
|
color: #721c24;
|
|
font-weight: bold;
|
|
text-align: left;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
}
|
|
</style>
|
|
|
|
<div id="app" style="max-width: 1300px; margin: auto; font-size: 13px;">
|
|
<div class="row mb-3 align-items-end">
|
|
<div class="col-md-auto me-3">
|
|
<label for="monthSelect" class="form-label mb-1">Month</label>
|
|
<select id="monthSelect" class="form-control form-control-sm" v-model="selectedMonth" v-on:change="loadData">
|
|
<option v-for="(m, i) in months" :value="i + 1">{{ m }}</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-auto">
|
|
<label for="yearSelect" class="form-label mb-1">Year</label>
|
|
<select id="yearSelect" class="form-control form-control-sm" v-model="selectedYear" v-on:change="loadData">
|
|
<option v-for="y in years" :value="y">{{ y }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="overallPendingMonths.length > 0 && activeTab === 'pending'" class="formal-pending-notice">
|
|
<b>Pending Action :</b> {{ overallPendingMonths.join(', ') }}
|
|
</div>
|
|
|
|
<ul class="nav nav-tabs mb-3">
|
|
<li class="nav-item">
|
|
<a class="nav-link" :class="{ active: activeTab === 'pending' }" v-on:click="activeTab = 'pending'" href="#">
|
|
Pending Actions <span class="badge bg-warning text-dark">{{ pendingActionsCount }}</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" :class="{ active: activeTab === 'completed' }" v-on:click="activeTab = 'completed'" href="#">
|
|
Completed Actions <span class="badge bg-info">{{ completedActionsCount }}</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="table-layer">
|
|
<div class="mb-3">
|
|
<input type="text" class="form-control form-control-sm" placeholder="Search by Staff Name or Status..." v-model="searchQuery" />
|
|
</div>
|
|
|
|
<div class="table-container table-responsive">
|
|
<table class="table table-bordered table-sm table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th class="header sortable-header" v-on:click="sortBy('fullName')">
|
|
Staff Name
|
|
<span class="sort-icon">
|
|
<template v-if="sortByColumn === 'fullName'">
|
|
{{ sortDirection === 'asc' ? '▲' : '▼' }}
|
|
</template>
|
|
</span>
|
|
</th>
|
|
<th class="header sortable-header" v-on:click="sortBy('submitDate')">
|
|
Date Submit
|
|
<span class="sort-icon">
|
|
<template v-if="sortByColumn === 'submitDate'">
|
|
{{ sortDirection === 'asc' ? '▲' : '▼' }}
|
|
</template>
|
|
</span>
|
|
</th>
|
|
<th class="header sortable-header" v-on:click="sortBy('currentUserStatus')">
|
|
Status
|
|
<span class="sort-icon">
|
|
<template v-if="sortByColumn === 'currentUserStatus'">
|
|
{{ sortDirection === 'asc' ? '▲' : '▼' }}
|
|
</template>
|
|
</span>
|
|
</th>
|
|
<th class="header">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="row in paginatedData" :key="row.statusId">
|
|
<td>{{ row.fullName }}</td>
|
|
<td>{{ formatDate(row.submitDate) }}</td>
|
|
<td>
|
|
<div v-if="row.role === 'HoU'">HoU: <span :class="getStatusBadgeClass(row.houStatus)">{{ row.houStatus }}</span></div>
|
|
<div v-else-if="row.role === 'HoD'">HoD: <span :class="getStatusBadgeClass(row.hodStatus)">{{ row.hodStatus }}</span></div>
|
|
<div v-else-if="row.role === 'Manager'">Manager: <span :class="getStatusBadgeClass(row.managerStatus)">{{ row.managerStatus }}</span></div>
|
|
<div v-else-if="row.role === 'HR'">HR: <span :class="getStatusBadgeClass(row.hrStatus)">{{ row.hrStatus }}</span></div>
|
|
<div v-if="row.IsOverallRejected && (row.currentUserStatus !== 'Approved' && row.currentUserStatus !== 'Rejected')" class="mt-1">
|
|
<span class="badge bg-danger">Rejected by a previous approver</span>
|
|
</div>
|
|
</td>
|
|
|
|
<td>
|
|
<div class="d-flex align-items-center justify-content-center action-buttons">
|
|
<template v-if="activeTab === 'pending'">
|
|
<template v-if="row.canApprove">
|
|
<button class="btn btn-success btn-sm" v-on:click="updateStatus(row.statusId, 'Approved')">Approve</button>
|
|
<button class="btn btn-danger btn-sm" v-on:click="updateStatus(row.statusId, 'Rejected')">Reject</button>
|
|
</template>
|
|
<template v-else-if="!row.canApprove && row.IsOverallRejected">
|
|
<span class="badge bg-danger">Rejected</span>
|
|
</template>
|
|
<template v-else-if="!row.canApprove">
|
|
<span class="badge bg-secondary">Awaiting previous approval</span>
|
|
</template>
|
|
</template>
|
|
|
|
<template v-else-if="activeTab === 'completed'"></template>
|
|
|
|
<button class="btn btn-primary btn-sm" v-on:click="viewOtData(row.statusId)">View</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="paginatedData.length === 0">
|
|
<td colspan="4" class="text-center">No {{ activeTab === 'pending' ? 'pending' : 'completed' }} actions found for your current filters.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="pagination-controls">
|
|
<div class="pagination-info">
|
|
Showing {{ (currentPage - 1) * itemsPerPage + 1 }} to {{ Math.min(currentPage * itemsPerPage, filteredAndSortedOtStatusList.length) }} of {{ filteredAndSortedOtStatusList.length }} entries
|
|
</div>
|
|
<nav aria-label="Page navigation" class="mx-auto">
|
|
<ul class="pagination pagination-sm mb-0">
|
|
<li class="page-item" :class="{ disabled: currentPage === 1 }">
|
|
<a class="page-link" href="#" v-on:click.prevent="currentPage--" aria-label="Previous">
|
|
<span aria-hidden="true">«</span>
|
|
</a>
|
|
</li>
|
|
<li class="page-item" v-for="page in totalPages" :key="page" :class="{ active: currentPage === page }">
|
|
<a class="page-link" href="#" v-on:click.prevent="currentPage = page">{{ page }}</a>
|
|
</li>
|
|
<li class="page-item" :class="{ disabled: currentPage === totalPages }">
|
|
<a class="page-link" href="#" v-on:click.prevent="currentPage++" aria-label="Next">
|
|
<span aria-hidden="true">»</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
<div class="pagination-items-per-page">
|
|
<label class="me-2 mb-0">Items per page:</label>
|
|
<select class="form-select form-select-sm" v-model.number="itemsPerPage">
|
|
<option :value="5">5</option>
|
|
<option :value="10">10</option>
|
|
<option :value="20">20</option>
|
|
<option :value="50">50</option>
|
|
<option :value="filteredAndSortedOtStatusList.length">All</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const app = Vue.createApp({
|
|
data() {
|
|
return {
|
|
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
|
years: Array.from({ length: 10 }, (_, i) => new Date().getFullYear() - 5 + i),
|
|
selectedMonth: parseInt(sessionStorage.getItem('approvalSelectedMonth')) || (new Date().getMonth() + 1),
|
|
selectedYear: parseInt(sessionStorage.getItem('approvalSelectedYear')) || new Date().getFullYear(),
|
|
otStatusList: [],
|
|
activeTab: 'pending',
|
|
userRoles: [],
|
|
sortByColumn: 'submitDate',
|
|
sortDirection: 'desc',
|
|
searchQuery: '',
|
|
currentPage: 1,
|
|
itemsPerPage: 10,
|
|
overallPendingMonths: []
|
|
};
|
|
},
|
|
watch: {
|
|
activeTab() {
|
|
this.currentPage = 1;
|
|
this.searchQuery = '';
|
|
},
|
|
searchQuery() {
|
|
this.currentPage = 1;
|
|
},
|
|
itemsPerPage() {
|
|
this.currentPage = 1;
|
|
}
|
|
},
|
|
computed: {
|
|
filteredByTabOtStatusList() {
|
|
if (this.activeTab === 'pending') {
|
|
return this.otStatusList.filter(row => row.canApprove || (row.IsOverallRejected && (row.currentUserStatus !== 'Approved' && row.currentUserStatus !== 'Rejected')));
|
|
} else if (this.activeTab === 'completed') {
|
|
return this.otStatusList.filter(row => row.currentUserStatus === 'Approved' || row.currentUserStatus === 'Rejected');
|
|
}
|
|
return [];
|
|
},
|
|
searchedOtStatusList() {
|
|
if (!this.searchQuery) {
|
|
return this.filteredByTabOtStatusList;
|
|
}
|
|
const query = this.searchQuery.toLowerCase();
|
|
return this.filteredByTabOtStatusList.filter(row => {
|
|
if (row.fullName && row.fullName.toLowerCase().includes(query)) return true;
|
|
if (row.currentUserStatus && row.currentUserStatus.toLowerCase().includes(query)) return true;
|
|
if (row.houStatus && row.houStatus.toLowerCase().includes(query)) return true;
|
|
if (row.hodStatus && row.hodStatus.toLowerCase().includes(query)) return true;
|
|
if (row.managerStatus && row.managerStatus.toLowerCase().includes(query)) return true;
|
|
if (row.hrStatus && row.hrStatus.toLowerCase().includes(query)) return true;
|
|
return false;
|
|
});
|
|
},
|
|
filteredAndSortedOtStatusList() {
|
|
let sortedList = [...this.searchedOtStatusList];
|
|
if (this.sortByColumn) {
|
|
sortedList.sort((a, b) => {
|
|
let valA = a[this.sortByColumn];
|
|
let valB = b[this.sortByColumn];
|
|
|
|
if (this.sortByColumn === 'currentUserStatus') {
|
|
const statusOrder = { 'Pending': 1, 'Approved': 2, 'Rejected': 3, 'N/A': 4, null: 5, undefined: 6, '': 7 };
|
|
const orderA = statusOrder[valA] || 99;
|
|
const orderB = statusOrder[valB] || 99;
|
|
if (this.sortDirection === 'asc') return orderA - orderB;
|
|
else return orderB - orderA;
|
|
}
|
|
else if (this.sortByColumn === 'submitDate') {
|
|
valA = valA ? new Date(valA) : new Date(0);
|
|
valB = valB ? new Date(valB) : new Date(0);
|
|
if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1;
|
|
if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1;
|
|
}
|
|
else if (typeof valA === 'string' && typeof valB === 'string') {
|
|
valA = valA.toLowerCase();
|
|
valB = valB.toLowerCase();
|
|
if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1;
|
|
if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1;
|
|
}
|
|
else {
|
|
if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1;
|
|
if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1;
|
|
}
|
|
return 0;
|
|
});
|
|
}
|
|
return sortedList;
|
|
},
|
|
totalPages() {
|
|
return Math.ceil(this.filteredAndSortedOtStatusList.length / this.itemsPerPage);
|
|
},
|
|
paginatedData() {
|
|
const start = (this.currentPage - 1) * this.itemsPerPage;
|
|
const end = start + this.itemsPerPage;
|
|
return this.filteredAndSortedOtStatusList.slice(start, end);
|
|
},
|
|
pendingActionsCount() {
|
|
return this.otStatusList.filter(row => row.canApprove).length;
|
|
},
|
|
completedActionsCount() {
|
|
return this.otStatusList.filter(row => row.currentUserStatus === 'Approved' || row.currentUserStatus === 'Rejected').length;
|
|
}
|
|
},
|
|
methods: {
|
|
loadData() {
|
|
fetch(`/OvertimeAPI/GetPendingApproval?month=${this.selectedMonth}&year=${this.selectedYear}`)
|
|
.then(res => {
|
|
if (!res.ok) throw new Error("Network response was not OK");
|
|
return res.json();
|
|
})
|
|
.then(result => {
|
|
this.userRoles = result.roles;
|
|
this.otStatusList = result.data;
|
|
this.overallPendingMonths = result.overallPendingMonths || [];
|
|
this.currentPage = 1;
|
|
})
|
|
.catch(err => {
|
|
console.error("Error loading data:", err);
|
|
alert("Error loading data: " + err.message);
|
|
});
|
|
},
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return '';
|
|
const d = new Date(dateStr);
|
|
return d.toLocaleDateString();
|
|
},
|
|
getStatusBadgeClass(status) {
|
|
switch (status) {
|
|
case 'Approved': return 'badge badge-approved';
|
|
case 'Rejected': return 'badge badge-rejected';
|
|
case 'Pending': return 'badge badge-pending';
|
|
default: return 'badge bg-secondary';
|
|
}
|
|
},
|
|
updateStatus(statusId, decision) {
|
|
if (!statusId) {
|
|
console.error("Invalid statusId passed to updateStatus.");
|
|
alert("Error: Invalid request ID.");
|
|
return;
|
|
}
|
|
|
|
const actionText = decision === 'Approved' ? 'approve' : 'reject';
|
|
const confirmed = confirm(`Are you sure you want to ${actionText} this request?`);
|
|
|
|
if (!confirmed) return;
|
|
|
|
fetch('/OvertimeAPI/UpdateApprovalStatus', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ statusId: statusId, decision: decision })
|
|
})
|
|
.then(res => {
|
|
if (!res.ok) {
|
|
return res.json().then(err => { throw new Error(err.message || "Failed to update status"); });
|
|
}
|
|
return res.json();
|
|
})
|
|
.then(() => {
|
|
alert(`Request ${decision.toLowerCase()} successfully.`);
|
|
this.loadData();
|
|
})
|
|
.catch(err => {
|
|
console.error("Error updating status:", err);
|
|
alert("Error: " + err.message);
|
|
});
|
|
},
|
|
viewOtData(statusId) {
|
|
sessionStorage.setItem('approvalSelectedMonth', this.selectedMonth);
|
|
sessionStorage.setItem('approvalSelectedYear', this.selectedYear);
|
|
window.location.href = `/OTcalculate/ApprovalDashboard/OtReview?statusId=${statusId}`;
|
|
},
|
|
sortBy(column) {
|
|
if (this.sortByColumn === column) {
|
|
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
this.sortByColumn = column;
|
|
this.sortDirection = 'asc';
|
|
}
|
|
this.currentPage = 1;
|
|
}
|
|
},
|
|
mounted() {
|
|
this.loadData();
|
|
},
|
|
beforeUnmount() {
|
|
sessionStorage.setItem('approvalSelectedMonth', this.selectedMonth);
|
|
sessionStorage.setItem('approvalSelectedYear', this.selectedYear);
|
|
}
|
|
});
|
|
|
|
app.mount('#app');
|
|
</script> |