PSTW_CentralizeSystem/Areas/OTcalculate/Views/Overtime/OtStatus.cshtml
2026-05-15 15:20:43 +08:00

853 lines
40 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">&laquo; 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 &raquo;</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 border-0 shadow-lg rounded-3">
<div class="modal-header border-bottom-0 pb-0 d-flex justify-content-between align-items-center mt-2">
<div style="width: 150px;">
<h5 class="modal-title fw-bold text-dark" id="filePreviewModalLabel">
<i class="fas fa-file-alt me-2 text-primary"></i>Document
</h5>
</div>
<!-- Sleek Zoom Controls (for images) -->
<div v-if="isImage(previewUrl)" class="bg-light border rounded-pill p-1 mx-auto d-inline-flex align-items-center shadow-sm">
<button class="btn btn-sm btn-light rounded-circle border-0" v-on:click="zoomOut" title="Zoom Out">
<i class="fas fa-search-minus text-secondary"></i>
</button>
<span class="fw-bold text-secondary px-3 text-center" style="min-width: 65px; pointer-events: none; font-size: 14px;">
{{ Math.round(zoomLevel * 100) }}%
</span>
<button class="btn btn-sm btn-light rounded-circle border-0" v-on:click="resetZoom" title="Reset to Fit">
<i class="fas fa-expand text-secondary"></i>
</button>
<button class="btn btn-sm btn-light rounded-circle border-0" v-on:click="zoomIn" title="Zoom In">
<i class="fas fa-search-plus text-secondary"></i>
</button>
</div>
<div v-else class="mx-auto"></div>
<div style="width: 150px; text-align: right;">
<button type="button" class="btn-close shadow-none" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body text-center bg-light mt-3" :class="isImage(previewUrl) ? 'p-4' : 'p-0'" style="height: 75vh; overflow: auto; position: relative;">
<template v-if="isImage(previewUrl)">
<!-- Keep the paper effect for standard images -->
<div style="display: inline-block; transition: transform 0.2s ease-in-out; transform-origin: top center;"
:style="{ transform: 'scale(' + zoomLevel + ')' }">
<img :src="previewUrl" class="bg-white p-1 border shadow-sm rounded" style="max-width: 100%; height: auto; display: block;" alt="File Preview" />
</div>
</template>
<template v-else>
<!-- Remove paper effect for PDFs -->
<iframe :src="previewUrl" width="100%" style="height: 100%; border: none; display: block;"></iframe>
</template>
</div>
<div class="modal-footer border-top-0 bg-light pt-0">
<button type="button" class="btn btn-secondary px-4 shadow-sm" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Edit History -->
<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 border-0 shadow">
<div class="modal-header border-bottom-0 pb-0">
<h5 class="modal-title fw-bold" id="editHistoryModalLabel">
<i class="fas fa-stream text-primary me-2"></i> Edit History
</h5>
<button type="button" class="btn-close shadow-none" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<p class="text-muted mb-4 ms-2">Record: <strong>{{ selectedRecord ? formatMonthYear(selectedRecord.month, selectedRecord.year) : '' }}</strong></p>
<div v-if="parsedHistory.length > 0" class="border-start border-2 border-secondary border-opacity-25 ms-3 ps-4 position-relative">
<!-- Timeline Node -->
<div v-for="(entry, historyIndex) in parsedHistory" :key="historyIndex" class="position-relative mb-5">
<!-- Node Dot -->
<span class="position-absolute translate-middle p-2 rounded-circle border border-2 border-white"
:class="entry.changes.some(c => c.field === 'Record Deletion') ? 'bg-danger' : 'bg-primary'"
style="top: 5px; left: -1.5rem !important;"></span>
<!-- Content -->
<div>
<div class="d-flex justify-content-between align-items-center mb-1">
<h6 class="fw-bold mb-0 text-dark">
{{ entry.approvalRole }}
<span class="text-muted fw-normal fs-6">
<template v-if="entry.changeType === 'Delete'">
deleted the record
</template>
<template v-else>
updated the record for
<strong class="text-primary">{{ formatDate(entry.recordDate) }}</strong>
</template>
</span>
</h6>
<small class="text-muted"><i class="far fa-clock me-1"></i>{{ formatDateTime(entry.date) }}</small>
</div>
<!-- Changes Block -->
<div class="bg-light rounded-3 p-3 mt-2 border">
<div v-if="entry.changes && entry.changes.length > 0">
<div v-for="(change, changeIndex) in entry.changes" :key="changeIndex" class="mb-1" style="font-size: 0.9rem;">
<!-- Delete -->
<div v-if="change.field === 'Record Deletion'" class="text-danger">
<i class="fas fa-trash-alt me-2"></i> <strong>Record Removed:</strong> Date {{ change.before }}
</div>
<!-- Edit -->
<div v-else class="row g-0 align-items-center">
<div class="col-4 fw-semibold text-secondary">{{ change.field }}</div>
<div class="col-8 d-flex align-items-center">
<span class="text-muted text-decoration-line-through me-2">{{ change.before || 'Empty' }}</span>
<i class="fas fa-long-arrow-alt-right text-muted mx-2"></i>
<span class="text-dark fw-bold bg-white px-2 py-1 rounded border shadow-sm">{{ change.after || 'Empty' }}</span>
</div>
</div>
</div>
</div>
<div v-else class="text-muted fst-italic small">No explicit field changes detected.</div>
</div>
</div>
</div>
</div>
<div v-else class="text-center py-5">
<i class="fas fa-history text-muted opacity-25 display-4 mb-3"></i>
<h6 class="text-muted">No modification history found.</h6>
</div>
</div>
<div class="modal-footer border-top-0">
<button type="button" class="btn btn-light border px-4" 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: [],
zoomLevel: 1.0,
// 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;
if (this.includeHou) count++;
if (this.includeHod) count++;
if (this.includeManager) count++;
if (this.includeHr) count++;
return count + 1;
},
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' }
];
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];
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;
});
}
}
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;
});
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}`;
},
formatDateTime(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
if (isNaN(date.getTime())) return '-';
return date.toLocaleString('en-MY', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: true // Uses AM/PM format
});
},
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';
},
isImage(url) {
if (!url) return false;
const lowerUrl = url.toLowerCase();
return lowerUrl.endsWith('.jpg') || lowerUrl.endsWith('.jpeg') ||
lowerUrl.endsWith('.png') || lowerUrl.endsWith('.gif') ||
lowerUrl.endsWith('.webp');
},
zoomIn() {
if (this.zoomLevel < 3) this.zoomLevel += 0.2;
},
zoomOut() {
if (this.zoomLevel > 0.4) this.zoomLevel -= 0.2;
},
resetZoom() {
this.zoomLevel = 1.0;
},
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 there is no before/after data, return empty
if (!before || !after) return changes;
// Loop through ONLY the keys provided by the clean JSON
for (const key in after) {
if (Object.prototype.hasOwnProperty.call(after, key)) {
let formattedBefore = before[key];
let formattedAfter = after[key];
// Resolve Station IDs to Station Names
if (key === 'StationId') {
formattedBefore = formattedBefore !== "Empty" ? this.getStationNameById(parseInt(formattedBefore)) : "Empty";
formattedAfter = formattedAfter !== "Empty" ? this.getStationNameById(parseInt(formattedAfter)) : "Empty";
}
// Format Breaks correctly (e.g., 60 minutes -> 1:00)
else if (['OfficeBreak', 'AfterBreak'].includes(key)) {
formattedBefore = formattedBefore !== "Empty" ? this.formatMinutesToHours(formattedBefore) : "0:00";
formattedAfter = formattedAfter !== "Empty" ? this.formatMinutesToHours(formattedAfter) : "0:00";
}
else if (key === 'OtDate') {
formattedBefore = formattedBefore !== "Empty" ? this.formatDate(formattedBefore) : "Empty";
formattedAfter = formattedAfter !== "Empty" ? this.formatDate(formattedAfter) : "Empty";
}
changes.push({
field: key,
before: formattedBefore,
after: 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: this.formatDate(logEntry.DeletedRecord.OtDate),
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(),
recordDate: logEntry.RecordDate,
changeType: logEntry.ChangeType,
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 = [];
},
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;
},
resetFilters() {
this.filters = {
monthYear: '',
status: ''
};
this.pagination.currentPage = 1;
},
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>
}