Update OT
This commit is contained in:
parent
ed30316685
commit
7ab4706524
3
.gitignore
vendored
3
.gitignore
vendored
@ -361,6 +361,3 @@ MigrationBackup/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# Ignore local publish test builds
|
||||
publish-test/
|
||||
@ -150,14 +150,32 @@
|
||||
</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" />
|
||||
<!-- Search + Bulk Actions -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div style="width: 300px;">
|
||||
<input type="text" class="form-control form-control-sm" placeholder="Search by Staff Name or Status..." v-model="searchQuery" />
|
||||
</div>
|
||||
|
||||
<!-- Bulk Action Buttons -->
|
||||
<div v-if="activeTab === 'pending' && selectedItems.length > 0" class="action-buttons">
|
||||
@* <span class="me-3 fw-bold text-primary">{{ selectedItems.length }} Selected</span> *@
|
||||
<button class="btn btn-success btn-sm" v-on:click="bulkUpdateStatus('Approved')">
|
||||
</i> Approve Selected
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" v-on:click="bulkUpdateStatus('Rejected')">
|
||||
</i> Reject Selected
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container table-responsive">
|
||||
<table class="table table-bordered table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- Select All Checkbox -->
|
||||
<th class="header text-center" style="width: 40px;">
|
||||
<input type="checkbox" v-if="activeTab === 'pending'" v-model="selectAll" />
|
||||
</th>
|
||||
<th class="header sortable-header" v-on:click="sortBy('fullName')">
|
||||
Staff Name
|
||||
<span class="sort-icon">
|
||||
@ -182,11 +200,19 @@
|
||||
</template>
|
||||
</span>
|
||||
</th>
|
||||
<th class="header">Action</th>
|
||||
<th class="header text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in paginatedData" :key="row.statusId">
|
||||
<!-- Individual Row Checkbox -->
|
||||
<td class="text-center">
|
||||
<input type="checkbox"
|
||||
:value="row.statusId"
|
||||
v-model="selectedItems"
|
||||
v-if="activeTab === 'pending' && row.canApprove" />
|
||||
</td>
|
||||
|
||||
<td>{{ row.fullName }}</td>
|
||||
<td>{{ formatDate(row.submitDate) }}</td>
|
||||
<td>
|
||||
@ -194,13 +220,14 @@
|
||||
<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>
|
||||
<td class="text-center">
|
||||
<div class="d-flex align-items-center justify-content-center action-buttons">
|
||||
<!-- 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>
|
||||
@ -213,15 +240,12 @@
|
||||
<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>
|
||||
<td colspan="5" class="text-center">No {{ activeTab === 'pending' ? 'pending' : 'completed' }} actions found for your current filters.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -278,19 +302,32 @@
|
||||
searchQuery: '',
|
||||
currentPage: 1,
|
||||
itemsPerPage: 10,
|
||||
overallPendingMonths: []
|
||||
overallPendingMonths: [],
|
||||
selectedItems: JSON.parse(sessionStorage.getItem('approvalSelectedItems')) || []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
activeTab() {
|
||||
this.currentPage = 1;
|
||||
this.searchQuery = '';
|
||||
this.selectedItems = [];
|
||||
},
|
||||
searchQuery() {
|
||||
this.currentPage = 1;
|
||||
this.selectedItems = [];
|
||||
},
|
||||
itemsPerPage() {
|
||||
this.currentPage = 1;
|
||||
this.selectedItems = [];
|
||||
},
|
||||
currentPage() {
|
||||
this.selectedItems = [];
|
||||
},
|
||||
selectedItems: {
|
||||
handler(newVal) {
|
||||
sessionStorage.setItem('approvalSelectedItems', JSON.stringify(newVal));
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -365,6 +402,28 @@
|
||||
},
|
||||
completedActionsCount() {
|
||||
return this.otStatusList.filter(row => row.currentUserStatus === 'Approved' || row.currentUserStatus === 'Rejected').length;
|
||||
},
|
||||
selectAll: {
|
||||
get() {
|
||||
const approvableItems = this.paginatedData.filter(row => row.canApprove);
|
||||
if (approvableItems.length === 0) return false;
|
||||
return approvableItems.every(row => this.selectedItems.includes(row.statusId));
|
||||
},
|
||||
set(isChecked) {
|
||||
const approvableItems = this.paginatedData.filter(row => row.canApprove);
|
||||
if (isChecked) {
|
||||
|
||||
approvableItems.forEach(row => {
|
||||
if (!this.selectedItems.includes(row.statusId)) {
|
||||
this.selectedItems.push(row.statusId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
const approvableIds = approvableItems.map(row => row.statusId);
|
||||
this.selectedItems = this.selectedItems.filter(id => !approvableIds.includes(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -443,6 +502,37 @@
|
||||
this.sortDirection = 'asc';
|
||||
}
|
||||
this.currentPage = 1;
|
||||
},
|
||||
bulkUpdateStatus(decision) {
|
||||
if (this.selectedItems.length === 0) return;
|
||||
|
||||
const actionText = decision === 'Approved' ? 'approve' : 'reject';
|
||||
const confirmed = confirm(`Are you sure you want to ${actionText} ${this.selectedItems.length} selected requests?`);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
const apiCalls = this.selectedItems.map(statusId => {
|
||||
return fetch('/OvertimeAPI/UpdateApprovalStatus', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ statusId: statusId, decision: decision })
|
||||
}).then(res => {
|
||||
if (!res.ok) throw new Error("Failed to update status");
|
||||
return res.json();
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(apiCalls)
|
||||
.then(() => {
|
||||
alert(`Successfully ${actionText}d ${this.selectedItems.length} requests.`);
|
||||
this.selectedItems = [];
|
||||
this.loadData();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error updating bulk status:", err);
|
||||
alert("An error occurred during bulk update. Some requests may not have saved properly. Refreshing data.");
|
||||
this.loadData();
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@ -239,7 +239,8 @@
|
||||
<button class="btn btn-dark btn-sm" v-on:click="saveAsPdf(false)"><i class="bi bi-file-pdf"></i> Save</button>
|
||||
<button class="btn btn-success btn-sm" v-on:click="exportToExcel(false)"><i class="bi bi-file-earmark-excel"></i> Excel</button>
|
||||
</div>
|
||||
<div class="mt-3 d-flex flex-wrap gap-2">
|
||||
<!-- Added v-if="approverRole === 'HR'" to hide these buttons from other roles -->
|
||||
<div class="mt-3 d-flex flex-wrap gap-2" v-if="approverRole === 'HR'">
|
||||
<button class="btn btn-primary btn-sm" v-on:click="printPdf(true)"><i class="bi bi-printer"></i> Print No Salary</button>
|
||||
<button class="btn btn-dark btn-sm" v-on:click="saveAsPdf(true)"><i class="bi bi-file-pdf"></i> Save No Salary</button>
|
||||
<button class="btn btn-success btn-sm" v-on:click="exportToExcel(true)"><i class="bi bi-file-earmark-excel"></i> Excel No Salary</button>
|
||||
|
||||
@ -75,25 +75,32 @@
|
||||
|
||||
<div class="mb-3" v-if="showAirDropdown">
|
||||
<label for="airStationDropdown">Air Station <span class="text-danger" v-if="requiresStation">*</span></label>
|
||||
<select id="airStationDropdown" class="form-control" style="width: 100%;" v-model="selectedAirStation">
|
||||
<option value="">Select Air Station</option>
|
||||
<option v-for="station in airStationList" :key="station.stationId" :value="station.stationId">
|
||||
{{ station.stationName || 'Unnamed Station' }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="border rounded bg-white">
|
||||
<select id="airStationDropdown" class="form-control border-0" style="width: 100%;" v-model="selectedAirStation">
|
||||
<option value="">Select Air Station</option>
|
||||
<option v-for="station in airStationList" :key="station.stationId" :value="station.stationId">
|
||||
{{ station.stationName || 'Unnamed Station' }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<small class="text-danger">*Only for PSTW AIR</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" v-if="showMarineDropdown">
|
||||
<label for="marineStationDropdown">Marine Station <span class="text-danger" v-if="requiresStation">*</span></label>
|
||||
<select id="marineStationDropdown" class="form-control" style="width: 100%;" v-model="selectedMarineStation">
|
||||
<option value="">Select Marine Station</option>
|
||||
<option v-for="station in marineStationList" :key="station.stationId" :value="station.stationId">
|
||||
{{ station.stationName || 'Unnamed Station' }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="border rounded bg-white">
|
||||
<select id="marineStationDropdown" class="form-control border-0" style="width: 100%;" v-model="selectedMarineStation">
|
||||
<option value="">Select Marine Station</option>
|
||||
<option v-for="station in marineStationList" :key="station.stationId" :value="station.stationId">
|
||||
{{ station.stationName || 'Unnamed Station' }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<small class="text-danger">*Only for PSTW MARINE</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="otDescription">Work Brief Description <span class="text-danger">*</span></label>
|
||||
<textarea id="otDescription" class="form-control"
|
||||
@ -139,7 +146,7 @@
|
||||
<button class="btn btn-success ms-3" v-on:click="addOvertime" :disabled="!areUserSettingsComplete">Save</button>
|
||||
</div>
|
||||
<div v-if="!areUserSettingsComplete" class="alert alert-warning mt-3 text-center" role="alert">
|
||||
Action Required: Your Flexi Hours, Approval Flow, Salary, or State settings have not been configured. Please contact the IT or HR department for assistance. You cannot save overtime until these are set.
|
||||
Action Required: Your Flexi Hours, Approval Flow, or State settings have not been configured. Please contact the IT or HR department for assistance. You cannot save overtime until these are set.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -381,7 +388,7 @@
|
||||
this.areUserSettingsComplete = data.isComplete;
|
||||
|
||||
if (!this.areUserSettingsComplete) {
|
||||
alert("Action Required: Your Flexi Hours, Approval Flow, Salary, or State settings have not been configured. Please contact the IT or HR department for assistance.");
|
||||
alert("Action Required: Your Flexi Hours, Approval Flow, or State settings have not been configured. Please contact the IT or HR department for assistance.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking user settings:", error);
|
||||
|
||||
@ -128,9 +128,9 @@
|
||||
|
||||
<div class="filter-row">
|
||||
<div class="filter-group">
|
||||
<label class="form-label">Month/Year</label>
|
||||
<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 value="">All Months / Years</option>
|
||||
<option v-for="(option, index) in monthYearOptions" :key="index" :value="option.value">
|
||||
{{ option.text }}
|
||||
</option>
|
||||
@ -254,49 +254,135 @@
|
||||
<!-- 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 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">
|
||||
<iframe :src="previewUrl" width="100%" height="650px" style="border: none;"></iframe>
|
||||
|
||||
<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">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
|
||||
<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 Modal -->
|
||||
<!-- 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">
|
||||
<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 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">
|
||||
<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 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">
|
||||
{{ entry.changes.some(c => c.field === 'Record Deletion') ? 'deleted the record' : 'updated the record' }}
|
||||
</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>
|
||||
<p class="text-center text-muted">No edit history available for this record.</p>
|
||||
|
||||
<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">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
|
||||
<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>
|
||||
@ -319,6 +405,7 @@
|
||||
historyModalInstance: null,
|
||||
filePreviewModalInstance: null,
|
||||
stations: [],
|
||||
zoomLevel: 1.0,
|
||||
|
||||
// Filtering data
|
||||
filters: {
|
||||
@ -545,7 +632,20 @@
|
||||
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();
|
||||
@ -554,6 +654,24 @@
|
||||
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) {
|
||||
@ -584,77 +702,33 @@
|
||||
|
||||
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 });
|
||||
// Safety check: 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";
|
||||
}
|
||||
|
||||
changes.push({
|
||||
field: key,
|
||||
before: formattedBefore,
|
||||
after: formattedAfter
|
||||
});
|
||||
}
|
||||
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;
|
||||
},
|
||||
|
||||
@ -687,7 +761,7 @@
|
||||
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'})`,
|
||||
before: this.formatDate(logEntry.DeletedRecord.OtDate),
|
||||
after: 'Record Deleted'
|
||||
});
|
||||
} else {
|
||||
|
||||
@ -881,317 +881,315 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
#endregion
|
||||
|
||||
#region Ot Register
|
||||
[HttpGet("GetStationsByDepartment")]
|
||||
public async Task<IActionResult> GetStationsByDepartment([FromQuery] int? departmentId)
|
||||
[HttpGet("GetStationsByDepartment")]
|
||||
public async Task<IActionResult> GetStationsByDepartment([FromQuery] int? departmentId)
|
||||
{
|
||||
if (!departmentId.HasValue)
|
||||
{
|
||||
if (!departmentId.HasValue)
|
||||
_logger.LogWarning("GetStationsByDepartment called without a departmentId.");
|
||||
return Ok(new List<object>());
|
||||
}
|
||||
|
||||
var stations = await _centralDbContext.Stations
|
||||
.Where(s => s.DepartmentId == departmentId.Value)
|
||||
.Select(s => new
|
||||
{
|
||||
_logger.LogWarning("GetStationsByDepartment called without a departmentId.");
|
||||
return Ok(new List<object>());
|
||||
s.StationId,
|
||||
StationName = s.StationName ?? "Unnamed Station"
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(stations);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("AddOvertime")]
|
||||
public async Task<IActionResult> AddOvertimeAsync([FromBody] OvertimeRequestDto request)
|
||||
{
|
||||
_logger.LogInformation("AddOvertimeAsync called.");
|
||||
_logger.LogInformation("Received request: {@Request}", request);
|
||||
|
||||
if (request == null)
|
||||
{
|
||||
_logger.LogError("Request is null.");
|
||||
return BadRequest("Invalid data.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (request.UserId == 0)
|
||||
{
|
||||
_logger.LogWarning("No user ID provided.");
|
||||
return BadRequest("User ID is required.");
|
||||
}
|
||||
|
||||
var stations = await _centralDbContext.Stations
|
||||
.Where(s => s.DepartmentId == departmentId.Value)
|
||||
.Select(s => new
|
||||
var user = await _userManager.FindByIdAsync(request.UserId.ToString());
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogError("User with ID {UserId} not found for overtime submission.", request.UserId);
|
||||
return Unauthorized("User not found.");
|
||||
}
|
||||
|
||||
// Prevent adding records to a month that is already submitted
|
||||
var targetMonth = request.OtDate.Month;
|
||||
var targetYear = request.OtDate.Year;
|
||||
|
||||
var isMonthAlreadySubmitted = await _centralDbContext.Otstatus
|
||||
.AnyAsync(s => s.UserId == request.UserId && s.Month == targetMonth && s.Year == targetYear);
|
||||
|
||||
if (isMonthAlreadySubmitted)
|
||||
{
|
||||
return BadRequest($"Overtime for {request.OtDate:MMMM yyyy} has already been submitted. You cannot add new records to a locked month.");
|
||||
}
|
||||
|
||||
var userRoles = await _userManager.GetRolesAsync(user);
|
||||
var isSuperAdmin = userRoles.Contains("SuperAdmin");
|
||||
var isSystemAdmin = userRoles.Contains("SystemAdmin");
|
||||
|
||||
var userWithDepartment = await _centralDbContext.Users
|
||||
.Include(u => u.Department)
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId);
|
||||
|
||||
int? userDepartmentId = userWithDepartment?.Department?.DepartmentId;
|
||||
|
||||
bool stationRequired = false;
|
||||
if (userDepartmentId == 2 || userDepartmentId == 3)
|
||||
{
|
||||
stationRequired = true;
|
||||
}
|
||||
else if (isSuperAdmin || isSystemAdmin)
|
||||
{
|
||||
stationRequired = true;
|
||||
}
|
||||
|
||||
if (stationRequired && (!request.StationId.HasValue || request.StationId.Value <= 0))
|
||||
{
|
||||
return BadRequest("A station must be selected.");
|
||||
}
|
||||
|
||||
TimeSpan? officeFrom = string.IsNullOrEmpty(request.OfficeFrom) ? (TimeSpan?)null : TimeSpan.Parse(request.OfficeFrom);
|
||||
TimeSpan? officeTo = string.IsNullOrEmpty(request.OfficeTo) ? (TimeSpan?)null : TimeSpan.Parse(request.OfficeTo);
|
||||
TimeSpan? afterFrom = string.IsNullOrEmpty(request.AfterFrom) ? (TimeSpan?)null : TimeSpan.Parse(request.AfterFrom);
|
||||
TimeSpan? afterTo = string.IsNullOrEmpty(request.AfterTo) ? (TimeSpan?)null : TimeSpan.Parse(request.AfterTo);
|
||||
|
||||
if ((officeFrom != null && officeTo == null) || (officeFrom == null && officeTo != null))
|
||||
{
|
||||
return BadRequest("Both Office From and To times must be provided if one is entered.");
|
||||
}
|
||||
if ((afterFrom != null && afterTo == null) || (afterFrom == null && afterTo != null))
|
||||
{
|
||||
return BadRequest("Both After Office From and To times must be provided if one is entered.");
|
||||
}
|
||||
|
||||
TimeSpan minAllowedFromMidnightTo = new TimeSpan(16, 30, 0);
|
||||
TimeSpan maxAllowedFromMidnightTo = new TimeSpan(23, 30, 0);
|
||||
|
||||
if (officeFrom.HasValue && officeTo.HasValue)
|
||||
{
|
||||
if (officeTo == TimeSpan.Zero)
|
||||
{
|
||||
s.StationId,
|
||||
StationName = s.StationName ?? "Unnamed Station"
|
||||
})
|
||||
if (officeFrom == TimeSpan.Zero)
|
||||
{
|
||||
return BadRequest("Invalid Office Hour Time: 'From' and 'To' cannot both be 00:00 (midnight).");
|
||||
}
|
||||
|
||||
if (officeFrom.Value < minAllowedFromMidnightTo || officeFrom.Value > maxAllowedFromMidnightTo)
|
||||
{
|
||||
return BadRequest("Invalid Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between end of your flexi hour to 11:30 pm on the same day.");
|
||||
}
|
||||
}
|
||||
else if (officeTo <= officeFrom)
|
||||
{
|
||||
return BadRequest("Invalid Office Hour Time: 'To' time must be later than 'From' time (same day only).");
|
||||
}
|
||||
}
|
||||
|
||||
if (afterFrom.HasValue && afterTo.HasValue)
|
||||
{
|
||||
if (afterTo == TimeSpan.Zero)
|
||||
{
|
||||
if (afterFrom == TimeSpan.Zero)
|
||||
{
|
||||
return BadRequest("Invalid After Office Hour Time: 'From' and 'To' cannot both be 00:00 (midnight).");
|
||||
}
|
||||
|
||||
if (afterFrom.Value < minAllowedFromMidnightTo || afterFrom.Value > maxAllowedFromMidnightTo)
|
||||
{
|
||||
return BadRequest("Invalid After Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between end of your flexi hour to 11:30 pm on the same day.");
|
||||
}
|
||||
}
|
||||
else if (afterTo <= afterFrom)
|
||||
{
|
||||
return BadRequest("Invalid After Office Hour Time: 'To' time must be later than 'From' time (same day only).");
|
||||
}
|
||||
}
|
||||
|
||||
if ((officeFrom == null && officeTo == null) && (afterFrom == null && afterTo == null))
|
||||
{
|
||||
return BadRequest("Please enter either Office Hours or After Office Hours.");
|
||||
}
|
||||
|
||||
// Convert the request date to a DateOnly for comparison
|
||||
DateOnly requestDate = DateOnly.FromDateTime(request.OtDate);
|
||||
|
||||
// Fetch existing overtime records for the same user and date
|
||||
var existingOvertimeRecords = await _centralDbContext.Otregisters
|
||||
.Where(o => o.UserId == request.UserId && DateOnly.FromDateTime(o.OtDate) == requestDate)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(stations);
|
||||
// Check for overlaps with existing records
|
||||
foreach (var existingRecord in existingOvertimeRecords)
|
||||
{
|
||||
// Convert existing record times to actual TimeSpan objects
|
||||
var existingOfficeFrom = existingRecord.OfficeFrom;
|
||||
var existingOfficeTo = existingRecord.OfficeTo;
|
||||
var existingAfterFrom = existingRecord.AfterFrom;
|
||||
var existingAfterTo = existingRecord.AfterTo;
|
||||
|
||||
// Function to check for overlap between two time ranges
|
||||
Func<TimeSpan?, TimeSpan?, TimeSpan?, TimeSpan?, bool> CheckOverlap =
|
||||
(newStart, newEnd, existingStart, existingEnd) =>
|
||||
{
|
||||
if (!newStart.HasValue || !newEnd.HasValue || !existingStart.HasValue || !existingEnd.HasValue)
|
||||
{
|
||||
return false; // No overlap if either range is incomplete
|
||||
}
|
||||
|
||||
// Handle midnight (00:00) as end of day (24:00) for comparison purposes
|
||||
TimeSpan adjustedNewEnd = (newEnd == TimeSpan.Zero && newStart != TimeSpan.Zero) ? TimeSpan.FromHours(24) : newEnd.Value;
|
||||
TimeSpan adjustedExistingEnd = (existingEnd == TimeSpan.Zero && existingStart != TimeSpan.Zero) ? TimeSpan.FromHours(24) : existingEnd.Value;
|
||||
|
||||
|
||||
// Check for overlap:
|
||||
// New range starts before existing range ends AND
|
||||
// New range ends after existing range starts
|
||||
return (newStart.Value < adjustedExistingEnd && adjustedNewEnd > existingStart.Value);
|
||||
};
|
||||
|
||||
// Check for overlap between new Office Hours and existing Office Hours
|
||||
if (CheckOverlap(officeFrom, officeTo, existingOfficeFrom, existingOfficeTo))
|
||||
{
|
||||
return BadRequest("Your Office Hours entry overlaps with another record on this date. Kindly adjust your time.");
|
||||
}
|
||||
|
||||
// Check for overlap between new After Office Hours and existing After Office Hours
|
||||
if (CheckOverlap(afterFrom, afterTo, existingAfterFrom, existingAfterTo))
|
||||
{
|
||||
return BadRequest("Your After Office Hours entry overlaps with another record on this date. Kindly adjust your time.");
|
||||
}
|
||||
|
||||
// Check for overlap between new Office Hours and existing After Office Hours
|
||||
if (CheckOverlap(officeFrom, officeTo, existingAfterFrom, existingAfterTo))
|
||||
{
|
||||
return BadRequest("Your Office Hours entry overlaps with another record on this date. Kindly adjust your time.");
|
||||
}
|
||||
|
||||
// Check for overlap between new After Office Hours and existing Office Hours
|
||||
if (CheckOverlap(afterFrom, afterTo, existingOfficeFrom, existingOfficeTo))
|
||||
{
|
||||
return BadRequest("Your After Office Hours entry overlaps with another record on this date. Kindly adjust your time.");
|
||||
}
|
||||
}
|
||||
|
||||
var newRecord = new OtRegisterModel
|
||||
{
|
||||
OtDate = request.OtDate,
|
||||
OfficeFrom = officeFrom,
|
||||
OfficeTo = officeTo,
|
||||
OfficeBreak = request.OfficeBreak,
|
||||
AfterFrom = afterFrom,
|
||||
AfterTo = afterTo,
|
||||
AfterBreak = request.AfterBreak,
|
||||
StationId = request.StationId,
|
||||
OtDescription = request.OtDescription,
|
||||
OtDays = request.OtDays,
|
||||
UserId = request.UserId,
|
||||
StatusId = request.StatusId
|
||||
};
|
||||
|
||||
_centralDbContext.Otregisters.Add(newRecord);
|
||||
await _centralDbContext.SaveChangesAsync();
|
||||
|
||||
return Ok(new { message = "Overtime registered successfully." });
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("AddOvertime")]
|
||||
public async Task<IActionResult> AddOvertimeAsync([FromBody] OvertimeRequestDto request)
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation("AddOvertimeAsync called.");
|
||||
_logger.LogInformation("Received request: {@Request}", request);
|
||||
|
||||
if (request == null)
|
||||
{
|
||||
_logger.LogError("Request is null.");
|
||||
return BadRequest("Invalid data.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (request.UserId == 0)
|
||||
{
|
||||
_logger.LogWarning("No user ID provided.");
|
||||
return BadRequest("User ID is required.");
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByIdAsync(request.UserId.ToString());
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogError("User with ID {UserId} not found for overtime submission.", request.UserId);
|
||||
return Unauthorized("User not found.");
|
||||
}
|
||||
|
||||
var userRoles = await _userManager.GetRolesAsync(user);
|
||||
var isSuperAdmin = userRoles.Contains("SuperAdmin");
|
||||
var isSystemAdmin = userRoles.Contains("SystemAdmin");
|
||||
|
||||
var userWithDepartment = await _centralDbContext.Users
|
||||
.Include(u => u.Department)
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId);
|
||||
|
||||
int? userDepartmentId = userWithDepartment?.Department?.DepartmentId;
|
||||
|
||||
bool stationRequired = false;
|
||||
if (userDepartmentId == 2 || userDepartmentId == 3)
|
||||
{
|
||||
stationRequired = true;
|
||||
}
|
||||
else if (isSuperAdmin || isSystemAdmin)
|
||||
{
|
||||
stationRequired = true;
|
||||
}
|
||||
|
||||
if (stationRequired && (!request.StationId.HasValue || request.StationId.Value <= 0))
|
||||
{
|
||||
return BadRequest("A station must be selected.");
|
||||
}
|
||||
|
||||
TimeSpan? officeFrom = string.IsNullOrEmpty(request.OfficeFrom) ? (TimeSpan?)null : TimeSpan.Parse(request.OfficeFrom);
|
||||
TimeSpan? officeTo = string.IsNullOrEmpty(request.OfficeTo) ? (TimeSpan?)null : TimeSpan.Parse(request.OfficeTo);
|
||||
TimeSpan? afterFrom = string.IsNullOrEmpty(request.AfterFrom) ? (TimeSpan?)null : TimeSpan.Parse(request.AfterFrom);
|
||||
TimeSpan? afterTo = string.IsNullOrEmpty(request.AfterTo) ? (TimeSpan?)null : TimeSpan.Parse(request.AfterTo);
|
||||
|
||||
if ((officeFrom != null && officeTo == null) || (officeFrom == null && officeTo != null))
|
||||
{
|
||||
return BadRequest("Both Office From and To times must be provided if one is entered.");
|
||||
}
|
||||
if ((afterFrom != null && afterTo == null) || (afterFrom == null && afterTo != null))
|
||||
{
|
||||
return BadRequest("Both After Office From and To times must be provided if one is entered.");
|
||||
}
|
||||
|
||||
TimeSpan minAllowedFromMidnightTo = new TimeSpan(16, 30, 0);
|
||||
TimeSpan maxAllowedFromMidnightTo = new TimeSpan(23, 30, 0);
|
||||
|
||||
if (officeFrom.HasValue && officeTo.HasValue)
|
||||
{
|
||||
if (officeTo == TimeSpan.Zero)
|
||||
{
|
||||
if (officeFrom == TimeSpan.Zero)
|
||||
{
|
||||
return BadRequest("Invalid Office Hour Time: 'From' and 'To' cannot both be 00:00 (midnight).");
|
||||
}
|
||||
|
||||
if (officeFrom.Value < minAllowedFromMidnightTo || officeFrom.Value > maxAllowedFromMidnightTo)
|
||||
{
|
||||
return BadRequest("Invalid Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between end of your flexi hour to 11:30 pm on the same day.");
|
||||
}
|
||||
}
|
||||
else if (officeTo <= officeFrom)
|
||||
{
|
||||
return BadRequest("Invalid Office Hour Time: 'To' time must be later than 'From' time (same day only).");
|
||||
}
|
||||
}
|
||||
|
||||
if (afterFrom.HasValue && afterTo.HasValue)
|
||||
{
|
||||
if (afterTo == TimeSpan.Zero)
|
||||
{
|
||||
if (afterFrom == TimeSpan.Zero)
|
||||
{
|
||||
return BadRequest("Invalid After Office Hour Time: 'From' and 'To' cannot both be 00:00 (midnight).");
|
||||
}
|
||||
|
||||
if (afterFrom.Value < minAllowedFromMidnightTo || afterFrom.Value > maxAllowedFromMidnightTo)
|
||||
{
|
||||
return BadRequest("Invalid After Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between end of your flexi hour to 11:30 pm on the same day.");
|
||||
}
|
||||
}
|
||||
else if (afterTo <= afterFrom)
|
||||
{
|
||||
return BadRequest("Invalid After Office Hour Time: 'To' time must be later than 'From' time (same day only).");
|
||||
}
|
||||
}
|
||||
|
||||
if ((officeFrom == null && officeTo == null) && (afterFrom == null && afterTo == null))
|
||||
{
|
||||
return BadRequest("Please enter either Office Hours or After Office Hours.");
|
||||
}
|
||||
|
||||
// Convert the request date to a DateOnly for comparison
|
||||
DateOnly requestDate = DateOnly.FromDateTime(request.OtDate);
|
||||
|
||||
// Fetch existing overtime records for the same user and date
|
||||
var existingOvertimeRecords = await _centralDbContext.Otregisters
|
||||
.Where(o => o.UserId == request.UserId && DateOnly.FromDateTime(o.OtDate) == requestDate)
|
||||
.ToListAsync();
|
||||
|
||||
// Check for overlaps with existing records
|
||||
foreach (var existingRecord in existingOvertimeRecords)
|
||||
{
|
||||
// Convert existing record times to actual TimeSpan objects
|
||||
var existingOfficeFrom = existingRecord.OfficeFrom;
|
||||
var existingOfficeTo = existingRecord.OfficeTo;
|
||||
var existingAfterFrom = existingRecord.AfterFrom;
|
||||
var existingAfterTo = existingRecord.AfterTo;
|
||||
|
||||
// Function to check for overlap between two time ranges
|
||||
Func<TimeSpan?, TimeSpan?, TimeSpan?, TimeSpan?, bool> CheckOverlap =
|
||||
(newStart, newEnd, existingStart, existingEnd) =>
|
||||
{
|
||||
if (!newStart.HasValue || !newEnd.HasValue || !existingStart.HasValue || !existingEnd.HasValue)
|
||||
{
|
||||
return false; // No overlap if either range is incomplete
|
||||
}
|
||||
|
||||
// Handle midnight (00:00) as end of day (24:00) for comparison purposes
|
||||
TimeSpan adjustedNewEnd = (newEnd == TimeSpan.Zero && newStart != TimeSpan.Zero) ? TimeSpan.FromHours(24) : newEnd.Value;
|
||||
TimeSpan adjustedExistingEnd = (existingEnd == TimeSpan.Zero && existingStart != TimeSpan.Zero) ? TimeSpan.FromHours(24) : existingEnd.Value;
|
||||
|
||||
|
||||
// Check for overlap:
|
||||
// New range starts before existing range ends AND
|
||||
// New range ends after existing range starts
|
||||
return (newStart.Value < adjustedExistingEnd && adjustedNewEnd > existingStart.Value);
|
||||
};
|
||||
|
||||
// Check for overlap between new Office Hours and existing Office Hours
|
||||
if (CheckOverlap(officeFrom, officeTo, existingOfficeFrom, existingOfficeTo))
|
||||
{
|
||||
return BadRequest("Your Office Hours entry overlaps with another record on this date. Kindly adjust your time.");
|
||||
}
|
||||
|
||||
// Check for overlap between new After Office Hours and existing After Office Hours
|
||||
if (CheckOverlap(afterFrom, afterTo, existingAfterFrom, existingAfterTo))
|
||||
{
|
||||
return BadRequest("Your Office Hours entry overlaps with another record on this date. Kindly adjust your time.");
|
||||
}
|
||||
|
||||
// Check for overlap between new Office Hours and existing After Office Hours
|
||||
if (CheckOverlap(officeFrom, officeTo, existingAfterFrom, existingAfterTo))
|
||||
{
|
||||
return BadRequest("Your Office Hours entry overlaps with another record on this date. Kindly adjust your time.");
|
||||
}
|
||||
|
||||
// Check for overlap between new After Office Hours and existing Office Hours
|
||||
if (CheckOverlap(afterFrom, afterTo, existingOfficeFrom, existingOfficeTo))
|
||||
{
|
||||
return BadRequest("Your Office Hours entry overlaps with another record on this date. Kindly adjust your time.");
|
||||
}
|
||||
}
|
||||
|
||||
var newRecord = new OtRegisterModel
|
||||
{
|
||||
OtDate = request.OtDate,
|
||||
OfficeFrom = officeFrom,
|
||||
OfficeTo = officeTo,
|
||||
OfficeBreak = request.OfficeBreak,
|
||||
AfterFrom = afterFrom,
|
||||
AfterTo = afterTo,
|
||||
AfterBreak = request.AfterBreak,
|
||||
StationId = request.StationId,
|
||||
OtDescription = request.OtDescription,
|
||||
OtDays = request.OtDays,
|
||||
UserId = request.UserId,
|
||||
StatusId = request.StatusId
|
||||
};
|
||||
|
||||
_centralDbContext.Otregisters.Add(newRecord);
|
||||
await _centralDbContext.SaveChangesAsync();
|
||||
|
||||
return Ok(new { message = "Overtime registered successfully." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error registering overtime for user {UserId}.", request.UserId);
|
||||
return StatusCode(500, $"An error occurred while saving overtime: {ex.InnerException?.Message ?? ex.Message}");
|
||||
}
|
||||
_logger.LogError(ex, "Error registering overtime for user {UserId}.", request.UserId);
|
||||
return StatusCode(500, $"An error occurred while saving overtime: {ex.InnerException?.Message ?? ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("GetUserStateAndHolidays/{userId}")]
|
||||
public async Task<IActionResult> GetUserStateAndHolidaysAsync(int userId)
|
||||
[HttpGet("GetUserStateAndHolidays/{userId}")]
|
||||
public async Task<IActionResult> GetUserStateAndHolidaysAsync(int userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
var hrSettings = await _centralDbContext.Hrusersetting
|
||||
.Include(h => h.State)
|
||||
.ThenInclude(s => s.Weekends)
|
||||
.Include(h => h.FlexiHour)
|
||||
.Where(h => h.UserId == userId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (hrSettings?.State == null)
|
||||
{
|
||||
var hrSettings = await _centralDbContext.Hrusersetting
|
||||
.Include(h => h.State)
|
||||
.ThenInclude(s => s.Weekends)
|
||||
.Include(h => h.FlexiHour)
|
||||
.Where(h => h.UserId == userId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (hrSettings?.State == null)
|
||||
{
|
||||
return Ok(new { state = (object)null, publicHolidays = new List<object>() });
|
||||
}
|
||||
|
||||
var publicHolidays = await _centralDbContext.Holidays
|
||||
.Where(ph => ph.StateId == hrSettings.StateId && ph.HolidayDate.Year == DateTime.Now.Year)
|
||||
.Select(ph => new { Date = ph.HolidayDate.ToString("yyyy-MM-dd") })
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
state = new
|
||||
{
|
||||
stateId = hrSettings.StateId,
|
||||
stateName = hrSettings.State?.StateName,
|
||||
weekendDay = hrSettings.State?.Weekends?.Day,
|
||||
weekendId = hrSettings.State?.WeekendId,
|
||||
flexiHour = hrSettings.FlexiHour?.FlexiHour
|
||||
},
|
||||
publicHolidays
|
||||
});
|
||||
return Ok(new { state = (object)null, publicHolidays = new List<object>() });
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
var publicHolidays = await _centralDbContext.Holidays
|
||||
.Where(ph => ph.StateId == hrSettings.StateId && ph.HolidayDate.Year == DateTime.Now.Year)
|
||||
.Select(ph => new { Date = ph.HolidayDate.ToString("yyyy-MM-dd") })
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching user state and public holidays.");
|
||||
return StatusCode(500, "An error occurred while fetching user state and public holidays.");
|
||||
}
|
||||
state = new
|
||||
{
|
||||
stateId = hrSettings.StateId,
|
||||
stateName = hrSettings.State?.StateName,
|
||||
weekendDay = hrSettings.State?.Weekends?.Day,
|
||||
weekendId = hrSettings.State?.WeekendId,
|
||||
flexiHour = hrSettings.FlexiHour?.FlexiHour
|
||||
},
|
||||
publicHolidays
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("CheckUserSettings/{userId}")]
|
||||
public async Task<IActionResult> CheckUserSettings(int userId)
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var hrSettings = await _centralDbContext.Hrusersetting
|
||||
.Where(h => h.UserId == userId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (hrSettings == null)
|
||||
{
|
||||
return Ok(new { isComplete = false });
|
||||
}
|
||||
|
||||
if (hrSettings.FlexiHourId == null || hrSettings.StateId == null || hrSettings.ApprovalFlowId == null)
|
||||
{
|
||||
return Ok(new { isComplete = false });
|
||||
}
|
||||
|
||||
var rateSetting = await _centralDbContext.Rates
|
||||
.Where(r => r.UserId == userId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (rateSetting == null)
|
||||
{
|
||||
return Ok(new { isComplete = false });
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rateSetting.RateValue <= 0.00m)
|
||||
{
|
||||
return Ok(new { isComplete = false });
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new { isComplete = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error checking user settings for user {UserId}", userId);
|
||||
return StatusCode(500, $"An error occurred while checking user settings: {ex.Message}");
|
||||
}
|
||||
_logger.LogError(ex, "Error fetching user state and public holidays.");
|
||||
return StatusCode(500, "An error occurred while fetching user state and public holidays.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("CheckUserSettings/{userId}")]
|
||||
public async Task<IActionResult> CheckUserSettings(int userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var hrSettings = await _centralDbContext.Hrusersetting
|
||||
.Where(h => h.UserId == userId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (hrSettings == null)
|
||||
{
|
||||
return Ok(new { isComplete = false });
|
||||
}
|
||||
|
||||
// NOT check Salary/Rate
|
||||
if (hrSettings.FlexiHourId == null || hrSettings.StateId == null || hrSettings.ApprovalFlowId == null)
|
||||
{
|
||||
return Ok(new { isComplete = false });
|
||||
}
|
||||
|
||||
|
||||
return Ok(new { isComplete = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error checking user settings for user {UserId}", userId);
|
||||
return StatusCode(500, $"An error occurred while checking user settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Ot Records
|
||||
@ -1565,17 +1563,12 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
|
||||
private bool HasOverlappingRecords(OtRegisterUpdateDto model, int userId)
|
||||
{
|
||||
// Retrieve all other overtime records for the same user and date,
|
||||
// excluding the current record being updated.
|
||||
// Get other OT records for this user on the same dat
|
||||
var existingRecords = _centralDbContext.Otregisters
|
||||
.Where(o => o.UserId == userId && o.OtDate.Date == model.OtDate.Date && o.OvertimeId != model.OvertimeId)
|
||||
.ToList();
|
||||
|
||||
// The logic to check for overlap between two time ranges A and B is:
|
||||
// A_start < B_end AND B_start < A_end
|
||||
// This handles all scenarios, including one range being entirely inside another.
|
||||
|
||||
// Check for overlap with the Office Hours block
|
||||
// Overlap formula: Start A < End B && Start B < End A
|
||||
if (!string.IsNullOrEmpty(model.OfficeFrom) && !string.IsNullOrEmpty(model.OfficeTo))
|
||||
{
|
||||
var newOfficeStart = TimeSpan.Parse(model.OfficeFrom);
|
||||
@ -2139,121 +2132,152 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
return Ok(holidays);
|
||||
}
|
||||
|
||||
public class OtUpdateLog
|
||||
{
|
||||
public string ApproverRole { get; set; }
|
||||
public int ApproverUserId { get; set; }
|
||||
public DateTime UpdateTimestamp { get; set; }
|
||||
public string ChangeType { get; set; }
|
||||
|
||||
// Save the exact fields that changed
|
||||
public Dictionary<string, string> BeforeEdit { get; set; }
|
||||
public Dictionary<string, string> AfterEdit { get; set; }
|
||||
|
||||
public object DeletedRecord { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost("UpdateOtRecordByApprover")]
|
||||
public IActionResult UpdateOtRecordByApprover([FromBody] OtRegisterEditDto updatedRecordDto)
|
||||
{
|
||||
if (updatedRecordDto == null)
|
||||
{
|
||||
return BadRequest("Invalid record data.");
|
||||
}
|
||||
if (updatedRecordDto == null) return BadRequest("Invalid record data.");
|
||||
|
||||
var existingRecord = _centralDbContext.Otregisters.FirstOrDefault(o => o.OvertimeId == updatedRecordDto.OvertimeId);
|
||||
if (existingRecord == null)
|
||||
{
|
||||
return NotFound(new { message = "Overtime record not found." });
|
||||
}
|
||||
if (existingRecord == null) return NotFound(new { message = "Overtime record not found." });
|
||||
|
||||
// **NEW LOGIC: Check for overlapping entries**
|
||||
if (IsOverlapping(updatedRecordDto, existingRecord.UserId, updatedRecordDto.OvertimeId))
|
||||
return BadRequest(new { message = "The new overtime time range overlaps with an existing entry." });
|
||||
|
||||
TimeSpan? newOfficeFrom = ParseTimeStringToTimeSpan(updatedRecordDto.OfficeFrom);
|
||||
TimeSpan? newOfficeTo = ParseTimeStringToTimeSpan(updatedRecordDto.OfficeTo);
|
||||
TimeSpan? newAfterFrom = ParseTimeStringToTimeSpan(updatedRecordDto.AfterFrom);
|
||||
TimeSpan? newAfterTo = ParseTimeStringToTimeSpan(updatedRecordDto.AfterTo);
|
||||
|
||||
var beforeChanges = new Dictionary<string, string>();
|
||||
var afterChanges = new Dictionary<string, string>();
|
||||
|
||||
if (existingRecord.OtDate.Date != updatedRecordDto.OtDate.Date)
|
||||
{
|
||||
return BadRequest(new { message = "The new overtime time range overlaps with an existing entry for the same date." });
|
||||
beforeChanges["OtDate"] = existingRecord.OtDate.ToString("yyyy-MM-dd");
|
||||
afterChanges["OtDate"] = updatedRecordDto.OtDate.ToString("yyyy-MM-dd");
|
||||
}
|
||||
if (existingRecord.OfficeFrom != newOfficeFrom)
|
||||
{
|
||||
beforeChanges["OfficeFrom"] = existingRecord.OfficeFrom?.ToString(@"hh\:mm") ?? "Empty";
|
||||
afterChanges["OfficeFrom"] = newOfficeFrom?.ToString(@"hh\:mm") ?? "Empty";
|
||||
}
|
||||
if (existingRecord.OfficeTo != newOfficeTo)
|
||||
{
|
||||
beforeChanges["OfficeTo"] = existingRecord.OfficeTo?.ToString(@"hh\:mm") ?? "Empty";
|
||||
afterChanges["OfficeTo"] = newOfficeTo?.ToString(@"hh\:mm") ?? "Empty";
|
||||
}
|
||||
if ((existingRecord.OfficeBreak ?? 0) != (updatedRecordDto.OfficeBreak ?? 0))
|
||||
{
|
||||
beforeChanges["OfficeBreak"] = (existingRecord.OfficeBreak ?? 0).ToString();
|
||||
afterChanges["OfficeBreak"] = (updatedRecordDto.OfficeBreak ?? 0).ToString();
|
||||
}
|
||||
if (existingRecord.AfterFrom != newAfterFrom)
|
||||
{
|
||||
beforeChanges["AfterFrom"] = existingRecord.AfterFrom?.ToString(@"hh\:mm") ?? "Empty";
|
||||
afterChanges["AfterFrom"] = newAfterFrom?.ToString(@"hh\:mm") ?? "Empty";
|
||||
}
|
||||
if (existingRecord.AfterTo != newAfterTo)
|
||||
{
|
||||
beforeChanges["AfterTo"] = existingRecord.AfterTo?.ToString(@"hh\:mm") ?? "Empty";
|
||||
afterChanges["AfterTo"] = newAfterTo?.ToString(@"hh\:mm") ?? "Empty";
|
||||
}
|
||||
if ((existingRecord.AfterBreak ?? 0) != (updatedRecordDto.AfterBreak ?? 0))
|
||||
{
|
||||
beforeChanges["AfterBreak"] = (existingRecord.AfterBreak ?? 0).ToString();
|
||||
afterChanges["AfterBreak"] = (updatedRecordDto.AfterBreak ?? 0).ToString();
|
||||
}
|
||||
if (existingRecord.StationId != updatedRecordDto.StationId)
|
||||
{
|
||||
beforeChanges["StationId"] = existingRecord.StationId?.ToString() ?? "Empty";
|
||||
afterChanges["StationId"] = updatedRecordDto.StationId?.ToString() ?? "Empty";
|
||||
}
|
||||
if (existingRecord.OtDescription != updatedRecordDto.OtDescription)
|
||||
{
|
||||
beforeChanges["OtDescription"] = existingRecord.OtDescription ?? "Empty";
|
||||
afterChanges["OtDescription"] = updatedRecordDto.OtDescription ?? "Empty";
|
||||
}
|
||||
|
||||
// Accidentally clicked save without changing anything
|
||||
if (beforeChanges.Count == 0)
|
||||
{
|
||||
return Ok(new { message = "No changes detected. Record saved." });
|
||||
}
|
||||
|
||||
// ... (rest of your existing logic for finding otStatus, permissions, and logging)
|
||||
// The previous code for validation will be moved to a helper method or retained if it's still needed
|
||||
var otStatus = _centralDbContext.Otstatus.FirstOrDefault(s => s.StatusId == updatedRecordDto.StatusId);
|
||||
if (otStatus == null)
|
||||
{
|
||||
return NotFound("OT status not found.");
|
||||
}
|
||||
if (otStatus == null) return NotFound("OT status not found.");
|
||||
|
||||
// ... (rest of the code for checking permissions and logging)
|
||||
var currentLoggedInUserId = GetCurrentLoggedInUserId();
|
||||
string approverRole = GetApproverRole(currentLoggedInUserId, otStatus.StatusId);
|
||||
|
||||
bool hasApproverActed = false;
|
||||
switch (approverRole)
|
||||
{
|
||||
case "HoU":
|
||||
hasApproverActed = !string.IsNullOrEmpty(otStatus.HouStatus) && otStatus.HouStatus != "Pending";
|
||||
break;
|
||||
case "HoD":
|
||||
hasApproverActed = !string.IsNullOrEmpty(otStatus.HodStatus) && otStatus.HodStatus != "Pending";
|
||||
break;
|
||||
case "Manager":
|
||||
hasApproverActed = !string.IsNullOrEmpty(otStatus.ManagerStatus) && otStatus.ManagerStatus != "Pending";
|
||||
break;
|
||||
case "HR":
|
||||
hasApproverActed = !string.IsNullOrEmpty(otStatus.HrStatus) && otStatus.HrStatus != "Pending";
|
||||
break;
|
||||
case "HoU": hasApproverActed = !string.IsNullOrEmpty(otStatus.HouStatus) && otStatus.HouStatus != "Pending"; break;
|
||||
case "HoD": hasApproverActed = !string.IsNullOrEmpty(otStatus.HodStatus) && otStatus.HodStatus != "Pending"; break;
|
||||
case "Manager": hasApproverActed = !string.IsNullOrEmpty(otStatus.ManagerStatus) && otStatus.ManagerStatus != "Pending"; break;
|
||||
case "HR": hasApproverActed = !string.IsNullOrEmpty(otStatus.HrStatus) && otStatus.HrStatus != "Pending"; break;
|
||||
}
|
||||
|
||||
if (hasApproverActed)
|
||||
{
|
||||
return Forbid("You cannot edit this record as you have already acted on this OT submission.");
|
||||
}
|
||||
if (hasApproverActed) return Forbid("You cannot edit this record as you have already acted on this OT submission.");
|
||||
|
||||
var originalRecordData = JsonConvert.SerializeObject(existingRecord, new JsonSerializerSettings
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
||||
});
|
||||
|
||||
// Update the existing record with the new data
|
||||
// Update the Database Record
|
||||
existingRecord.OtDate = updatedRecordDto.OtDate;
|
||||
existingRecord.OfficeFrom = ParseTimeStringToTimeSpan(updatedRecordDto.OfficeFrom);
|
||||
existingRecord.OfficeTo = ParseTimeStringToTimeSpan(updatedRecordDto.OfficeTo);
|
||||
existingRecord.OfficeFrom = newOfficeFrom;
|
||||
existingRecord.OfficeTo = newOfficeTo;
|
||||
existingRecord.OfficeBreak = updatedRecordDto.OfficeBreak;
|
||||
existingRecord.AfterFrom = ParseTimeStringToTimeSpan(updatedRecordDto.AfterFrom);
|
||||
existingRecord.AfterTo = ParseTimeStringToTimeSpan(updatedRecordDto.AfterTo);
|
||||
existingRecord.AfterFrom = newAfterFrom;
|
||||
existingRecord.AfterTo = newAfterTo;
|
||||
existingRecord.AfterBreak = updatedRecordDto.AfterBreak;
|
||||
existingRecord.StationId = updatedRecordDto.StationId;
|
||||
existingRecord.OtDescription = updatedRecordDto.OtDescription;
|
||||
|
||||
_centralDbContext.SaveChanges();
|
||||
|
||||
// Create the clean Log
|
||||
var updateLog = new OtUpdateLog
|
||||
{
|
||||
ApproverRole = approverRole,
|
||||
ApproverUserId = currentLoggedInUserId,
|
||||
UpdateTimestamp = DateTime.Now,
|
||||
ChangeType = "Edit",
|
||||
BeforeEdit = JsonConvert.DeserializeObject<OtRegisterModel>(originalRecordData),
|
||||
AfterEdit = updatedRecordDto
|
||||
BeforeEdit = beforeChanges,
|
||||
AfterEdit = afterChanges
|
||||
};
|
||||
|
||||
var logJson = JsonConvert.SerializeObject(updateLog);
|
||||
var logJson = JsonConvert.SerializeObject(updateLog, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
|
||||
|
||||
switch (approverRole)
|
||||
{
|
||||
case "HoU":
|
||||
otStatus.HouUpdate = AppendUpdateLog(otStatus.HouUpdate, logJson);
|
||||
break;
|
||||
case "HoD":
|
||||
otStatus.HodUpdate = AppendUpdateLog(otStatus.HodUpdate, logJson);
|
||||
break;
|
||||
case "Manager":
|
||||
otStatus.ManagerUpdate = AppendUpdateLog(otStatus.ManagerUpdate, logJson);
|
||||
break;
|
||||
case "HR":
|
||||
otStatus.HrUpdate = AppendUpdateLog(otStatus.HrUpdate, logJson);
|
||||
break;
|
||||
default:
|
||||
return Unauthorized("You are not authorized to edit this record.");
|
||||
case "HoU": otStatus.HouUpdate = AppendUpdateLog(otStatus.HouUpdate, logJson); break;
|
||||
case "HoD": otStatus.HodUpdate = AppendUpdateLog(otStatus.HodUpdate, logJson); break;
|
||||
case "Manager": otStatus.ManagerUpdate = AppendUpdateLog(otStatus.ManagerUpdate, logJson); break;
|
||||
case "HR": otStatus.HrUpdate = AppendUpdateLog(otStatus.HrUpdate, logJson); break;
|
||||
default: return Unauthorized("You are not authorized to edit this record.");
|
||||
}
|
||||
|
||||
_centralDbContext.SaveChanges();
|
||||
|
||||
return Ok(new { message = "Overtime record updated successfully and changes logged." });
|
||||
}
|
||||
|
||||
// **NEW HELPER METHOD: IsOverlapping**
|
||||
private bool IsOverlapping(OtRegisterEditDto updatedRecord, int userId, int currentRecordId)
|
||||
{
|
||||
// Filter existing records for the same date, excluding the one being edited
|
||||
var existingRecords = _centralDbContext.Otregisters
|
||||
.Where(o => o.UserId == userId && o.OtDate.Date == updatedRecord.OtDate.Date && o.OvertimeId != currentRecordId)
|
||||
.ToList();
|
||||
|
||||
// Check for overlaps with the new time ranges
|
||||
TimeSpan? newOfficeFrom = ParseTimeStringToTimeSpan(updatedRecord.OfficeFrom);
|
||||
TimeSpan? newOfficeTo = ParseTimeStringToTimeSpan(updatedRecord.OfficeTo);
|
||||
TimeSpan? newAfterFrom = ParseTimeStringToTimeSpan(updatedRecord.AfterFrom);
|
||||
@ -2261,7 +2285,6 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
|
||||
foreach (var existing in existingRecords)
|
||||
{
|
||||
// Check if the new office hours overlap with any existing time range
|
||||
if (newOfficeFrom.HasValue && newOfficeTo.HasValue)
|
||||
{
|
||||
if (CheckOverlapBetween(newOfficeFrom.Value, newOfficeTo.Value, existing.OfficeFrom, existing.OfficeTo) ||
|
||||
@ -2271,7 +2294,6 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the new after office hours overlap with any existing time range
|
||||
if (newAfterFrom.HasValue && newAfterTo.HasValue)
|
||||
{
|
||||
if (CheckOverlapBetween(newAfterFrom.Value, newAfterTo.Value, existing.OfficeFrom, existing.OfficeTo) ||
|
||||
@ -2285,24 +2307,18 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
return false;
|
||||
}
|
||||
|
||||
// **NEW HELPER METHOD: CheckOverlapBetween**
|
||||
private bool CheckOverlapBetween(TimeSpan start1, TimeSpan end1, TimeSpan? start2, TimeSpan? end2)
|
||||
{
|
||||
// If either of the second time range's values is missing, there can be no overlap.
|
||||
if (!start2.HasValue || !end2.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create local, non-nullable variables to work with.
|
||||
TimeSpan s1 = start1;
|
||||
TimeSpan e1 = end1;
|
||||
TimeSpan s2 = start2.Value;
|
||||
TimeSpan e2 = end2.Value;
|
||||
|
||||
// Handle overnight cases for both ranges by adding 24 hours to the end time
|
||||
// if it's on or before the start time.
|
||||
// TimeSpan.FromHours(24) is a cleaner way to express this.
|
||||
if (e1 <= s1)
|
||||
{
|
||||
e1 = e1.Add(TimeSpan.FromHours(24));
|
||||
@ -2312,8 +2328,6 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
e2 = e2.Add(TimeSpan.FromHours(24));
|
||||
}
|
||||
|
||||
// Overlap exists if the start of one range is before the end of the other,
|
||||
// AND the start of the other is before the end of the first.
|
||||
return s1 < e2 && s2 < e1;
|
||||
}
|
||||
|
||||
@ -2363,10 +2377,13 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
return Forbid("You cannot delete this record as you have already acted on this OT submission.");
|
||||
}
|
||||
|
||||
var deletedRecordData = JsonConvert.SerializeObject(recordToDelete, new JsonSerializerSettings
|
||||
var cleanDeletedRecord = new
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
||||
});
|
||||
OvertimeId = recordToDelete.OvertimeId,
|
||||
OtDate = recordToDelete.OtDate.ToString("yyyy-MM-dd"),
|
||||
StationId = recordToDelete.StationId?.ToString(),
|
||||
OtDescription = recordToDelete.OtDescription
|
||||
};
|
||||
|
||||
_centralDbContext.Otregisters.Remove(recordToDelete);
|
||||
_centralDbContext.SaveChanges();
|
||||
@ -2377,7 +2394,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
ApproverUserId = currentLoggedInUserId,
|
||||
UpdateTimestamp = DateTime.Now,
|
||||
ChangeType = "Delete",
|
||||
DeletedRecord = JsonConvert.DeserializeObject<OtRegisterModel>(deletedRecordData)
|
||||
DeletedRecord = cleanDeletedRecord
|
||||
};
|
||||
|
||||
var logJson = JsonConvert.SerializeObject(deleteLog);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user