773 lines
35 KiB
Plaintext
773 lines
35 KiB
Plaintext
@{
|
|
ViewData["Title"] = "Overtime Status";
|
|
Layout = "~/Views/Shared/_Layout.cshtml";
|
|
}
|
|
|
|
<style>
|
|
.white-box {
|
|
background-color: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.filter-container {
|
|
background-color: #f8f9fa;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.filter-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 15px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.filter-group {
|
|
flex: 1;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.history-entry {
|
|
background-color: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 5px;
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.history-entry strong {
|
|
color: #343a40;
|
|
}
|
|
|
|
.history-entry em {
|
|
color: #6c757d;
|
|
}
|
|
|
|
.history-entry ul {
|
|
list-style: none;
|
|
padding-left: 0;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.history-entry li {
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.sortable-header {
|
|
cursor: pointer;
|
|
position: relative;
|
|
padding-right: 20px;
|
|
}
|
|
|
|
.sortable-header:hover {
|
|
background-color: #f1f1f1;
|
|
}
|
|
|
|
.sortable-header::after {
|
|
content: "↕";
|
|
position: absolute;
|
|
right: 5px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
font-size: 12px;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.sortable-header.asc::after {
|
|
content: "↑";
|
|
opacity: 1;
|
|
}
|
|
|
|
.sortable-header.desc::after {
|
|
content: "↓";
|
|
opacity: 1;
|
|
}
|
|
|
|
.badge-status {
|
|
font-size: 0.85em;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.badge-pending {
|
|
background-color: #ffc107;
|
|
color: #212529;
|
|
}
|
|
|
|
.badge-approved {
|
|
background-color: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.badge-rejected {
|
|
background-color: #dc3545;
|
|
color: white;
|
|
}
|
|
</style>
|
|
|
|
<div id="app" style="max-width: 1300px; margin: auto; font-size: 13px;">
|
|
<div class="table-layer">
|
|
<div class="white-box">
|
|
<!-- Simplified Filter Section -->
|
|
<div class="filter-container">
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<h5>Filters</h5>
|
|
</div>
|
|
<div class="col-md-6 text-end">
|
|
<button class="btn btn-sm btn-outline-secondary" v-on:click="resetFilters">
|
|
<i class="fas fa-redo"></i> Reset Filters
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="filter-row">
|
|
<div class="filter-group">
|
|
<label class="form-label">Month/Year</label>
|
|
<select class="form-select form-select-sm" v-model="filters.monthYear">
|
|
<option value="">All Months/Years</option>
|
|
<option v-for="(option, index) in monthYearOptions" :key="index" :value="option.value">
|
|
{{ option.text }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label class="form-label">Status</label>
|
|
<select class="form-select form-select-sm" v-model="filters.status">
|
|
<option value="">All Statuses</option>
|
|
<option v-for="(option, index) in statusOptions" :key="index" :value="option.value">
|
|
{{ option.text }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table Section -->
|
|
<div class="table-container table-responsive">
|
|
<table id="otStatusTable" class="table table-bordered table-sm table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th class="sortable-header"
|
|
:class="{'asc': sort.field === 'monthYear' && sort.order === 'asc', 'desc': sort.field === 'monthYear' && sort.order === 'desc'}"
|
|
v-on:click="sortBy('monthYear')">Month/Year</th>
|
|
<th class="sortable-header"
|
|
:class="{'asc': sort.field === 'submitDate' && sort.order === 'asc', 'desc': sort.field === 'submitDate' && sort.order === 'desc'}"
|
|
v-on:click="sortBy('submitDate')">Submit Date</th>
|
|
<th v-if="includeHou" class="sortable-header"
|
|
:class="{'asc': sort.field === 'houStatus' && sort.order === 'asc', 'desc': sort.field === 'houStatus' && sort.order === 'desc'}"
|
|
v-on:click="sortBy('houStatus')">HoU Status</th>
|
|
<th v-if="includeHod" class="sortable-header"
|
|
:class="{'asc': sort.field === 'hodStatus' && sort.order === 'asc', 'desc': sort.field === 'hodStatus' && sort.order === 'desc'}"
|
|
v-on:click="sortBy('hodStatus')">HoD Status</th>
|
|
<th v-if="includeManager" class="sortable-header"
|
|
:class="{'asc': sort.field === 'managerStatus' && sort.order === 'asc', 'desc': sort.field === 'managerStatus' && sort.order === 'desc'}"
|
|
v-on:click="sortBy('managerStatus')">Manager Status</th>
|
|
<th v-if="includeHr" class="sortable-header"
|
|
:class="{'asc': sort.field === 'hrStatus' && sort.order === 'asc', 'desc': sort.field === 'hrStatus' && sort.order === 'desc'}"
|
|
v-on:click="sortBy('hrStatus')">HR Status</th>
|
|
<th>Edit History</th>
|
|
<th>File</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(item, index) in filteredRecords" :key="index">
|
|
<td>{{ formatMonthYear(item.month, item.year) }}</td>
|
|
<td>{{ formatDate(item.submitDate) }}</td>
|
|
<td v-if="includeHou">
|
|
<span v-if="item.houStatus" :class="getStatusBadgeClass(item.houStatus)">
|
|
{{ item.houStatus }}
|
|
</span>
|
|
<span v-else>-</span>
|
|
</td>
|
|
<td v-if="includeHod">
|
|
<span v-if="item.hodStatus" :class="getStatusBadgeClass(item.hodStatus)">
|
|
{{ item.hodStatus }}
|
|
</span>
|
|
<span v-else>-</span>
|
|
</td>
|
|
<td v-if="includeManager">
|
|
<span v-if="item.managerStatus" :class="getStatusBadgeClass(item.managerStatus)">
|
|
{{ item.managerStatus }}
|
|
</span>
|
|
<span v-else>-</span>
|
|
</td>
|
|
<td v-if="includeHr">
|
|
<span v-if="item.hrStatus" :class="getStatusBadgeClass(item.hrStatus)">
|
|
{{ item.hrStatus }}
|
|
</span>
|
|
<span v-else>-</span>
|
|
</td>
|
|
<td>
|
|
<button v-if="item.updated" class="btn btn-sm btn-info" v-on:click="showEditHistory(item)">
|
|
<i class="fas fa-history"></i> View History
|
|
</button>
|
|
<span v-else>-</span>
|
|
</td>
|
|
<td>
|
|
<button v-if="item.filePath" class="btn btn-sm btn-primary" v-on:click="previewFile(item.filePath)">
|
|
<i class="fas fa-file-alt"></i> View
|
|
</button>
|
|
<span v-else>-</span>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="filteredRecords.length === 0">
|
|
<td :colspan="columnCount" class="text-center">No records found matching your criteria.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- Pagination -->
|
|
<div class="row mt-3" v-if="filteredRecords.length > 0">
|
|
<div class="col-md-6">
|
|
<div class="dataTables_info">
|
|
Showing {{ pagination.startItem }} to {{ pagination.endItem }} of {{ filteredRecords.length }} entries
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<nav aria-label="Page navigation" class="float-end">
|
|
<ul class="pagination pagination-sm">
|
|
<li class="page-item" :class="{disabled: pagination.currentPage === 1}">
|
|
<a class="page-link" href="#" v-on:click.prevent="prevPage">« Previous</a>
|
|
</li>
|
|
<li class="page-item" v-for="page in pagination.totalPages" :key="page"
|
|
:class="{active: pagination.currentPage === page}">
|
|
<a class="page-link" href="#" v-on:click.prevent="goToPage(page)">{{ page }}</a>
|
|
</li>
|
|
<li class="page-item" :class="{disabled: pagination.currentPage === pagination.totalPages}">
|
|
<a class="page-link" href="#" v-on:click.prevent="nextPage">Next »</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File Preview Modal -->
|
|
<div class="modal fade" id="filePreviewModal" tabindex="-1" aria-labelledby="filePreviewModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="filePreviewModalLabel">File Preview</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<iframe :src="previewUrl" width="100%" height="650px" style="border: none;"></iframe>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit History Modal -->
|
|
<div class="modal fade" id="editHistoryModal" tabindex="-1" aria-labelledby="editHistoryModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="editHistoryModalLabel">Edit History for {{ selectedRecord ? formatMonthYear(selectedRecord.month, selectedRecord.year) : '' }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div v-if="parsedHistory.length > 0">
|
|
<div v-for="(entry, historyIndex) in parsedHistory" :key="historyIndex" class="history-entry">
|
|
<p><strong>Edited by:</strong> {{ entry.approvalRole }}</p>
|
|
<p><strong>Date:</strong> {{ formatDate(entry.date) }}</p>
|
|
<p><strong>Changes:</strong></p>
|
|
<ul v-if="entry.changes && entry.changes.length > 0">
|
|
<li v-for="(change, changeIndex) in entry.changes" :key="changeIndex">
|
|
<strong>{{ change.field }}:</strong> Changed from " <em>{{ change.before }}</em> " to " <em>{{ change.after }}</em> "
|
|
</li>
|
|
</ul>
|
|
<p v-else class="text-muted fst-italic">No specific changes detailed for this approval step.</p>
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
<p class="text-center text-muted">No edit history available for this record.</p>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
const app = Vue.createApp({
|
|
data() {
|
|
return {
|
|
otRecords: [],
|
|
includeHou: false,
|
|
includeHod: false,
|
|
includeManager: false,
|
|
includeHr: false,
|
|
previewUrl: '',
|
|
selectedRecord: null,
|
|
parsedHistory: [],
|
|
historyModalInstance: null,
|
|
filePreviewModalInstance: null,
|
|
stations: [],
|
|
|
|
// Filtering data
|
|
filters: {
|
|
monthYear: '',
|
|
status: ''
|
|
},
|
|
|
|
// Sorting data
|
|
sort: {
|
|
field: 'submitDate',
|
|
order: 'desc'
|
|
},
|
|
|
|
// Pagination data
|
|
pagination: {
|
|
currentPage: 1,
|
|
itemsPerPage: 10,
|
|
totalPages: 1,
|
|
startItem: 1,
|
|
endItem: 10
|
|
}
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
columnCount() {
|
|
let count = 3; // Month/Year, SubmitDate, Edit History
|
|
if (this.includeHou) count++;
|
|
if (this.includeHod) count++;
|
|
if (this.includeManager) count++;
|
|
if (this.includeHr) count++;
|
|
return count + 1; // File column
|
|
},
|
|
|
|
monthYearOptions() {
|
|
const uniqueMonths = [...new Set(this.otRecords.map(item =>
|
|
`${item.month.toString().padStart(2, '0')}/${item.year}`
|
|
))];
|
|
return uniqueMonths.map(my => ({
|
|
value: my,
|
|
text: my
|
|
})).sort((a, b) => {
|
|
// Sort by year then month
|
|
const [aMonth, aYear] = a.value.split('/').map(Number);
|
|
const [bMonth, bYear] = b.value.split('/').map(Number);
|
|
return bYear - aYear || bMonth - aMonth;
|
|
});
|
|
},
|
|
|
|
statusOptions() {
|
|
const options = [
|
|
{ value: 'Pending', text: 'Pending' },
|
|
{ value: 'Approved', text: 'Approved' },
|
|
{ value: 'Rejected', text: 'Rejected' }
|
|
];
|
|
|
|
// Add combined status options if multiple approval levels exist
|
|
if ((this.includeHou && this.includeHod) ||
|
|
(this.includeHou && this.includeManager) ||
|
|
(this.includeHod && this.includeManager)) {
|
|
options.push(
|
|
{ value: 'PartiallyApproved', text: 'Partially Approved' },
|
|
{ value: 'FullyApproved', text: 'Fully Approved' }
|
|
);
|
|
}
|
|
|
|
return options;
|
|
},
|
|
|
|
filteredRecords() {
|
|
let filtered = [...this.otRecords];
|
|
|
|
// Apply filters
|
|
if (this.filters.monthYear) {
|
|
const [month, year] = this.filters.monthYear.split('/').map(Number);
|
|
filtered = filtered.filter(item =>
|
|
item.month === month && item.year === year
|
|
);
|
|
}
|
|
|
|
if (this.filters.status) {
|
|
if (this.filters.status === 'PartiallyApproved') {
|
|
filtered = filtered.filter(item => {
|
|
const statuses = [];
|
|
if (this.includeHou) statuses.push(item.houStatus);
|
|
if (this.includeHod) statuses.push(item.hodStatus);
|
|
if (this.includeManager) statuses.push(item.managerStatus);
|
|
if (this.includeHr) statuses.push(item.hrStatus);
|
|
|
|
const approvedCount = statuses.filter(s => s === 'Approved').length;
|
|
const rejectedCount = statuses.filter(s => s === 'Rejected').length;
|
|
const pendingCount = statuses.filter(s => s === 'Pending' || !s).length;
|
|
|
|
return approvedCount > 0 && pendingCount > 0;
|
|
});
|
|
} else if (this.filters.status === 'FullyApproved') {
|
|
filtered = filtered.filter(item => {
|
|
const statuses = [];
|
|
if (this.includeHou) statuses.push(item.houStatus);
|
|
if (this.includeHod) statuses.push(item.hodStatus);
|
|
if (this.includeManager) statuses.push(item.managerStatus);
|
|
if (this.includeHr) statuses.push(item.hrStatus);
|
|
|
|
return statuses.every(s => s === 'Approved');
|
|
});
|
|
} else {
|
|
filtered = filtered.filter(item => {
|
|
if (this.includeHou && item.houStatus === this.filters.status) return true;
|
|
if (this.includeHod && item.hodStatus === this.filters.status) return true;
|
|
if (this.includeManager && item.managerStatus === this.filters.status) return true;
|
|
if (this.includeHr && item.hrStatus === this.filters.status) return true;
|
|
return false;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Apply sorting
|
|
filtered.sort((a, b) => {
|
|
let aValue, bValue;
|
|
|
|
switch (this.sort.field) {
|
|
case 'monthYear':
|
|
aValue = new Date(a.year, a.month - 1);
|
|
bValue = new Date(b.year, b.month - 1);
|
|
break;
|
|
case 'submitDate':
|
|
aValue = new Date(a.submitDate);
|
|
bValue = new Date(b.submitDate);
|
|
break;
|
|
case 'houStatus':
|
|
aValue = a.houStatus || '';
|
|
bValue = b.houStatus || '';
|
|
break;
|
|
case 'hodStatus':
|
|
aValue = a.hodStatus || '';
|
|
bValue = b.hodStatus || '';
|
|
break;
|
|
case 'managerStatus':
|
|
aValue = a.managerStatus || '';
|
|
bValue = b.managerStatus || '';
|
|
break;
|
|
case 'hrStatus':
|
|
aValue = a.hrStatus || '';
|
|
bValue = b.hrStatus || '';
|
|
break;
|
|
default:
|
|
return 0;
|
|
}
|
|
|
|
if (aValue < bValue) return this.sort.order === 'asc' ? -1 : 1;
|
|
if (aValue > bValue) return this.sort.order === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
|
|
// Update pagination
|
|
this.pagination.totalPages = Math.ceil(filtered.length / this.pagination.itemsPerPage);
|
|
this.pagination.currentPage = Math.min(this.pagination.currentPage, this.pagination.totalPages || 1);
|
|
|
|
const startIndex = (this.pagination.currentPage - 1) * this.pagination.itemsPerPage;
|
|
const endIndex = startIndex + this.pagination.itemsPerPage;
|
|
|
|
this.pagination.startItem = filtered.length > 0 ? startIndex + 1 : 0;
|
|
this.pagination.endItem = Math.min(endIndex, filtered.length);
|
|
|
|
return filtered.slice(startIndex, endIndex);
|
|
}
|
|
},
|
|
|
|
mounted() {
|
|
Promise.all([
|
|
fetch('/OvertimeAPI/GetUserOtStatus')
|
|
.then(res => {
|
|
if (!res.ok) {
|
|
throw new Error('Failed to fetch OT status');
|
|
}
|
|
return res.json();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching OT status:', error);
|
|
return {
|
|
includeHou: false,
|
|
includeHod: false,
|
|
includeManager: false,
|
|
includeHr: false,
|
|
otStatuses: [],
|
|
hasApprovalFlow: false
|
|
};
|
|
}),
|
|
fetch('/OvertimeAPI/GetAllStations').then(res => res.json())
|
|
])
|
|
.then(([otData, stationData]) => {
|
|
this.includeHou = otData.includeHou || false;
|
|
this.includeHod = otData.includeHod || false;
|
|
this.includeManager = otData.includeManager || false;
|
|
this.includeHr = otData.includeHr || false;
|
|
this.otRecords = (otData.otStatuses || []).map(item => ({
|
|
...item,
|
|
monthYear: `${item.month.toString().padStart(2, '0')}/${item.year}`
|
|
}));
|
|
|
|
if (otData.hasApprovalFlow === false) {
|
|
alert("Note: Your approval flow is not yet configured. Only basic information is shown.");
|
|
}
|
|
|
|
this.stations = stationData;
|
|
this.historyModalInstance = new bootstrap.Modal(document.getElementById('editHistoryModal'));
|
|
this.filePreviewModalInstance = new bootstrap.Modal(document.getElementById('filePreviewModal'));
|
|
})
|
|
.catch(error => {
|
|
console.error('Error initializing data:', error);
|
|
this.includeHou = false;
|
|
this.includeHod = false;
|
|
this.includeManager = false;
|
|
this.includeHr = false;
|
|
this.otRecords = [];
|
|
});
|
|
},
|
|
|
|
methods: {
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
const date = new Date(dateStr);
|
|
if (isNaN(date.getTime())) return '-';
|
|
return date.toLocaleDateString('en-MY', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
},
|
|
|
|
formatMonthYear(month, year) {
|
|
if (!month || !year) return '-';
|
|
return `${month.toString().padStart(2, '0')}/${year}`;
|
|
},
|
|
|
|
getStatusBadgeClass(status) {
|
|
if (!status) return '';
|
|
const statusLower = status.toLowerCase();
|
|
if (statusLower.includes('approve')) return 'badge badge-status badge-approved';
|
|
if (statusLower.includes('reject')) return 'badge badge-status badge-rejected';
|
|
return 'badge badge-status badge-pending';
|
|
},
|
|
|
|
previewFile(path) {
|
|
this.previewUrl = '/' + path.replace(/^\/+/, '');
|
|
if (this.filePreviewModalInstance) {
|
|
this.filePreviewModalInstance.show();
|
|
}
|
|
},
|
|
|
|
getStationNameById(stationId) {
|
|
if (!stationId) return 'N/A';
|
|
const station = this.stations.find(s => s.stationId === stationId);
|
|
return station ? station.stationName : `Unknown Station (ID: ${stationId})`;
|
|
},
|
|
|
|
formatMinutesToHours(minutes) {
|
|
if (minutes === null || minutes === undefined || isNaN(minutes)) {
|
|
return '';
|
|
}
|
|
if (minutes < 0) return 'Invalid (Negative)';
|
|
|
|
const hours = Math.floor(minutes / 60);
|
|
const remainingMinutes = minutes % 60;
|
|
|
|
const formattedHours = String(hours).padStart(1, '0');
|
|
const formattedMinutes = String(remainingMinutes).padStart(2, '0');
|
|
|
|
return `${formattedHours}:${formattedMinutes}`;
|
|
},
|
|
|
|
getChanges(before, after) {
|
|
const changes = [];
|
|
if (!before && !after) return [];
|
|
if (!before && after) {
|
|
for (const key in after) {
|
|
if (Object.prototype.hasOwnProperty.call(after, key)) {
|
|
let displayValue = after[key];
|
|
if (key === 'StationId') {
|
|
displayValue = this.getStationNameById(after[key]);
|
|
} else if (['OfficeBreak', 'AfterBreak'].includes(key)) {
|
|
displayValue = this.formatMinutesToHours(after[key]);
|
|
}
|
|
changes.push({ field: key, before: 'N/A (New Record)', after: displayValue });
|
|
}
|
|
}
|
|
return changes;
|
|
}
|
|
if (before && !after) {
|
|
return [];
|
|
}
|
|
|
|
const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
|
|
allKeys.forEach(key => {
|
|
const beforeValue = before[key];
|
|
const afterValue = after[key];
|
|
|
|
let formattedBefore = beforeValue;
|
|
let formattedAfter = afterValue;
|
|
|
|
const timeFields = ['OfficeFrom', 'OfficeTo', 'AfterFrom', 'AfterTo'];
|
|
if (timeFields.includes(key)) {
|
|
if (typeof beforeValue === 'string' && beforeValue.match(/^\d{2}:\d{2}(:\d{2})?$/)) {
|
|
formattedBefore = beforeValue.substring(0, 5);
|
|
}
|
|
if (typeof afterValue === 'string' && afterValue.match(/^\d{2}:\d{2}(:\d{2})?$/)) {
|
|
formattedAfter = afterValue.substring(0, 5);
|
|
}
|
|
|
|
if (formattedBefore === "00:00" || formattedBefore === "00:00:00") formattedBefore = "";
|
|
if (formattedAfter === "00:00" || formattedAfter === "00:00:00") formattedAfter = "";
|
|
|
|
if (formattedBefore === null) formattedBefore = "";
|
|
if (formattedAfter === null) formattedAfter = "";
|
|
}
|
|
|
|
if (key === 'StationId') {
|
|
formattedBefore = this.getStationNameById(beforeValue);
|
|
formattedAfter = this.getStationNameById(afterValue);
|
|
}
|
|
else if (['OfficeBreak', 'AfterBreak'].includes(key)) {
|
|
formattedBefore = this.formatMinutesToHours(beforeValue);
|
|
formattedAfter = this.formatMinutesToHours(afterValue);
|
|
}
|
|
else if (key.includes('Date')) {
|
|
formattedBefore = beforeValue ? this.formatDate(beforeValue) : '';
|
|
formattedAfter = afterValue ? this.formatDate(afterValue) : '';
|
|
}
|
|
|
|
formattedBefore = (formattedBefore === null || formattedBefore === undefined) ? '' : formattedBefore;
|
|
formattedAfter = (formattedAfter === null || formattedAfter === undefined) ? '' : formattedAfter;
|
|
|
|
if (String(formattedBefore) !== String(formattedAfter)) {
|
|
if (!['StatusId', 'Otstatus', 'UserId', 'OvertimeId'].includes(key)) {
|
|
changes.push({
|
|
field: key,
|
|
before: formattedBefore === '' ? 'Empty' : formattedBefore,
|
|
after: formattedAfter === '' ? 'Empty' : formattedAfter
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return changes;
|
|
},
|
|
|
|
showEditHistory(record) {
|
|
this.selectedRecord = record;
|
|
this.parsedHistory = [];
|
|
|
|
const updateFields = ['houUpdate', 'hodUpdate', 'managerUpdate', 'hrUpdate'];
|
|
const approvalRoles = {
|
|
'houUpdate': 'HoU',
|
|
'hodUpdate': 'HoD',
|
|
'managerUpdate': 'Manager',
|
|
'hrUpdate': 'HR'
|
|
};
|
|
|
|
updateFields.forEach(field => {
|
|
if (record[field]) {
|
|
try {
|
|
const updateLogArray = JSON.parse(record[field]);
|
|
|
|
if (Array.isArray(updateLogArray)) {
|
|
updateLogArray.forEach(logEntry => {
|
|
let changesDetail = [];
|
|
|
|
if (logEntry.ChangeType === 'Edit') {
|
|
if (logEntry.BeforeEdit && logEntry.AfterEdit) {
|
|
changesDetail = this.getChanges(logEntry.BeforeEdit, logEntry.AfterEdit);
|
|
}
|
|
} else if (logEntry.ChangeType === 'Delete') {
|
|
if (logEntry.DeletedRecord) {
|
|
changesDetail.push({
|
|
field: 'Record Deletion',
|
|
before: `Overtime ID: ${logEntry.DeletedRecord.OvertimeId} (Date: ${this.formatDate(logEntry.DeletedRecord.OtDate)}, Station: ${this.getStationNameById(logEntry.DeletedRecord.StationId)}, Desc: ${logEntry.DeletedRecord.OtDescription ? logEntry.DeletedRecord.OtDescription.substring(0, 50) + '...' : 'N/A'})`,
|
|
after: 'Record Deleted'
|
|
});
|
|
} else {
|
|
changesDetail.push({ field: 'Record Deletion', before: 'Unknown Record', after: 'Record Deleted' });
|
|
}
|
|
}
|
|
|
|
this.parsedHistory.push({
|
|
approvedBy: logEntry.ApproverRole || 'N/A',
|
|
approvalRole: logEntry.ApproverRole || approvalRoles[field],
|
|
date: logEntry.UpdateTimestamp || new Date().toISOString(),
|
|
changes: changesDetail
|
|
});
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error(`Error parsing JSON for ${field}:`, e, record[field]);
|
|
}
|
|
}
|
|
});
|
|
|
|
this.parsedHistory.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
|
|
if (this.historyModalInstance) {
|
|
this.historyModalInstance.show();
|
|
}
|
|
},
|
|
|
|
closeEditHistory() {
|
|
if (this.historyModalInstance) {
|
|
this.historyModalInstance.hide();
|
|
}
|
|
this.selectedRecord = null;
|
|
this.parsedHistory = [];
|
|
},
|
|
|
|
// Sorting methods
|
|
sortBy(field) {
|
|
if (this.sort.field === field) {
|
|
this.sort.order = this.sort.order === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
this.sort.field = field;
|
|
this.sort.order = 'asc';
|
|
}
|
|
this.pagination.currentPage = 1; // Reset to first page when sorting changes
|
|
},
|
|
|
|
// Filter methods
|
|
resetFilters() {
|
|
this.filters = {
|
|
monthYear: '',
|
|
status: ''
|
|
};
|
|
this.pagination.currentPage = 1;
|
|
},
|
|
|
|
// Pagination methods
|
|
prevPage() {
|
|
if (this.pagination.currentPage > 1) {
|
|
this.pagination.currentPage--;
|
|
}
|
|
},
|
|
|
|
nextPage() {
|
|
if (this.pagination.currentPage < this.pagination.totalPages) {
|
|
this.pagination.currentPage++;
|
|
}
|
|
},
|
|
|
|
goToPage(page) {
|
|
if (page >= 1 && page <= this.pagination.totalPages) {
|
|
this.pagination.currentPage = page;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
app.mount('#app');
|
|
</script>
|
|
} |