PSTW_CentralizeSystem/Areas/OTcalculate/Views/Overtime/OtRecords.cshtml
2025-04-29 17:19:25 +08:00

442 lines
19 KiB
Plaintext

@{
ViewData["Title"] = "Overtime Records";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<style>
.table-container {
background-color: white;
border-radius: 15px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
padding: 25px;
margin-bottom: 20px;
}
.table th, .table td {
vertical-align: middle;
text-align: center;
font-size: 14px;
}
.header-green {
background-color: #d9ead3 !important;
}
.header-blue {
background-color: #cfe2f3 !important;
}
.header-orange {
background-color: #fce5cd !important;
}
input.form-control,
select.form-control,
.btn {
border-radius: 10px;
font-size: 13px;
}
.table-primary td {
background-color: #e6f0ff !important;
}
.table-responsive {
overflow-x: auto;
}
td.wrap-text {
white-space: pre-wrap;
word-wrap: break-word;
max-width: 300px;
text-align: left;
}
.description-preview {
max-height: 3.6em;
overflow: hidden;
cursor: pointer;
transition: max-height 0.3s ease;
white-space: pre-wrap;
word-wrap: break-word;
}
.description-preview.expanded {
max-height: none;
}
</style>
<div id="app" style="max-width: 1300px; margin: auto; font-size: 13px;">
<div class="mb-3 d-flex flex-wrap">
<div class="me-2 mb-2">
<label>Month</label>
<select class="form-control form-control-sm" v-model="selectedMonth">
<option v-for="(m, i) in months" :value="i + 1">{{ m }}</option>
</select>
</div>
<div class="mb-2">
<label>Year</label>
<select class="form-control form-control-sm" v-model="selectedYear">
<option v-for="y in years" :value="y">{{ y }}</option>
</select>
</div>
</div>
<div id="print-section" class="table-container table-responsive">
<table class="table table-bordered table-sm table-striped">
<thead>
<tr>
<th class="header-green" rowspan="2">Date</th>
<th class="header-blue" colspan="3">Office Hour<br><small>(8:30 - 17:30)</small></th>
<th class="header-blue" colspan="3">After Office Hour<br><small>(17:30 - 8:30)</small></th>
<th class="header-orange" rowspan="2">Total OT Hours</th>
<th class="header-orange" rowspan="2">Break (min)</th>
<th class="header-orange" rowspan="2">Net OT Hours</th>
<th class="header-orange" rowspan="2" v-if="isPSTWAIR">Station</th>
<th class="header-green" rowspan="2">Days</th>
<th class="header-blue" rowspan="2">Description</th>
<th class="header-green" rowspan="2">Action</th>
</tr>
<tr>
<th class="header-blue">From</th>
<th class="header-blue">To</th>
<th class="header-blue">Break</th>
<th class="header-blue">From</th>
<th class="header-blue">To</th>
<th class="header-blue">Break</th>
</tr>
</thead>
<tbody>
<tr v-for="(record, index) in filteredRecords" :key="record.overtimeId">
<td>{{ formatDate(record.otDate) }}</td>
<td>{{ formatTime(record.officeFrom) }}</td>
<td>{{ formatTime(record.officeTo) }}</td>
<td>{{ record.officeBreak }}</td>
<td>{{ formatTime(record.afterFrom) }}</td>
<td>{{ formatTime(record.afterTo) }}</td>
<td>{{ record.afterBreak }}</td>
<td>{{ formatHourMinute(calcTotalTime(record)) }}</td>
<td>{{ calcBreakTotal(record) }}</td>
<td>{{ formatHourMinute(calcNetHours(record)) }}</td>
<td v-if="isPSTWAIR">{{ record.stationName || 'N/A' }}</td>
<td>{{ record.otDays }}</td>
<td class="wrap-text">
<div class="description-preview" v-on:click ="toggleDescription(index)" :class="{ expanded: expandedDescriptions[index] }">
{{ record.otDescription }}
</div>
</td>
<td>
<button class="btn btn-light border rounded-circle me-1"
title="Edit"
:disabled="hasSubmitted"
v-on:click="editRecord(index)">
<i class="bi bi-pencil-fill text-warning fs-5"></i>
</button>
<button class="btn btn-light border rounded-circle"
title="Delete"
:disabled="hasSubmitted"
v-on:click="deleteRecord(index)">
<i class="bi bi-trash-fill text-danger fs-5"></i>
</button>
</td>
</tr>
<tr v-if="filteredRecords.length === 0">
<td :colspan="isPSTWAIR ? 14 : 13">No records found.</td>
</tr>
<tr class="table-primary fw-bold">
<td>TOTAL</td>
<td colspan="6"></td>
<td>{{ formatHourMinute(totalHours) }}</td>
<td>{{ formatHourMinute(totalBreak) }}</td>
<td>{{ formatHourMinute(totalNetTime) }}</td>
<td v-if="isPSTWAIR"></td>
<td colspan="3"></td>
</tr>
</tbody>
</table>
</div>
<div class="mt-3 d-flex flex-wrap gap-2">
<button class="btn btn-primary btn-sm" v-on:click="printPdf"><i class="bi bi-printer"></i> Print</button>
<button class="btn btn-dark btn-sm" v-on:click="downloadPdf"><i class="bi bi-file-pdf"></i> Save</button>
<button class="btn btn-success btn-sm" v-on:click="downloadExcel(selectedMonth, selectedYear)"><i class="bi bi-file-earmark-excel"></i>Excel</button>
<button class="btn btn-success btn-sm" :disabled="hasSubmitted" v-on:click="openSubmitModal"><i class="bi bi-send"></i>{{ hasSubmitted ? 'Submitted' : 'Submit' }}</button>
</div>
<div class="modal fade" id="submitModal" tabindex="-1" aria-labelledby="submitModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="submitModalLabel">Submit Overtime</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="file" class="form-control" v-on:change ="handleFileUpload">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" v-on:click ="submitOvertime">Submit</button>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
const app = Vue.createApp({
data() {
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth() + 1;
return {
otRecords: [],
userId: null,
isPSTWAIR: false,
selectedMonth: new Date().getMonth() + 1,
selectedYear: currentYear,
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
years: Array.from({ length: 10 }, (_, i) => currentYear - 5 + i),
expandedDescriptions: {},
selectedFile: null,
hasSubmitted: false,
};
},
watch: {
selectedMonth() {
this.checkSubmissionStatus();
},
selectedYear() {
this.checkSubmissionStatus();
}
},
computed: {
filteredRecords() {
return this.otRecords
.filter(r => new Date(r.otDate).getMonth() + 1 === this.selectedMonth && new Date(r.otDate).getFullYear() === this.selectedYear)
.sort((a, b) => new Date(a.otDate) - new Date(b.otDate));
},
totalHours() {
const total = this.filteredRecords.reduce((sum, r) => sum + this.calcTotalHours(r), 0);
const hours = Math.floor(total);
const minutes = Math.round((total - hours) * 60);
return { hours, minutes };
},
totalBreak() {
const totalMin = this.filteredRecords.reduce((sum, r) => sum + this.calcBreakTotal(r), 0);
const hours = Math.floor(totalMin / 60);
const minutes = totalMin % 60;
return { hours, minutes };
},
totalNetTime() {
const totalMinutes = (this.totalHours.hours * 60 + this.totalHours.minutes) -
(this.totalBreak.hours * 60 + this.totalBreak.minutes);
return {
hours: Math.floor(totalMinutes / 60),
minutes: Math.round(totalMinutes % 60)
};
},
},
async mounted() {
await this.initUserAndRecords();
await this.checkSubmissionStatus();
},
methods: {
async initUserAndRecords() {
await this.fetchUser();
if (this.userId) {
await this.fetchOtRecords();
}
},
async fetchUser() {
try {
const response = await fetch(`/IdentityAPI/GetUserInformation/`, { method: 'POST' });
if (response.ok) {
const data = await response.json();
this.currentUser = data?.userInfo || null;
this.userId = this.currentUser?.id || null;
console.log("Fetched User:", this.currentUser);
console.log("Dept ID:", this.currentUser?.department?.departmentId);
console.log("Roles:", this.currentUser?.role);
const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin");
const isSystemAdmin = this.currentUser?.role?.includes("SystemAdmin");
const isDepartmentTwo = this.currentUser?.department?.departmentId === 2;
this.isPSTWAIR = isSuperAdmin || isSystemAdmin || isDepartmentTwo;
console.log("isPSTWAIR:", this.isPSTWAIR);
} else {
console.error(`Failed to fetch user: ${response.statusText}`);
}
} catch (error) {
console.error("Error fetching user:", error);
}
},
async fetchOtRecords() {
try {
const res = await fetch(`/OvertimeAPI/GetUserOvertimeRecords/${this.userId}`);
this.otRecords = await res.json();
} catch (err) {
console.error("Records fetch error:", err);
}
},
async checkSubmissionStatus() {
try {
const res = await fetch(`/OvertimeAPI/CheckOvertimeSubmitted/${this.userId}/${this.selectedMonth}/${this.selectedYear}`);
this.hasSubmitted = await res.json();
} catch (err) {
console.error("Failed to check submission status", err);
}
},
toggleDescription(index) {
this.expandedDescriptions[index] = !this.expandedDescriptions[index];
},
formatDate(d) {
return new Date(d).toLocaleDateString();
},
formatTime(t) {
if (!t) return "-";
const [hours, minutes] = t.split(':').map(Number);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
},
getTimeDiff(from, to) {
if (!from || !to) return 0;
const [fh, fm] = from.split(":").map(Number);
const [th, tm] = to.split(":").map(Number);
return ((th * 60 + tm) - (fh * 60 + fm)) / 60;
},
calcTotalTime(r) {
const totalMinutes = this.calcTotalHours(r) * 60;
return {
hours: Math.floor(totalMinutes / 60),
minutes: Math.round(totalMinutes % 60)
};
},
calcTotalHours(r) {
return this.getTimeDiff(r.officeFrom, r.officeTo) + this.getTimeDiff(r.afterFrom, r.afterTo);
},
calcBreakTotal(r) {
return (r.officeBreak || 0) + (r.afterBreak || 0);
},
calcNetHours(r) {
const totalMinutes = (this.calcTotalHours(r) * 60) - this.calcBreakTotal(r);
return {
hours: Math.floor(totalMinutes / 60),
minutes: Math.round(totalMinutes % 60)
};
},
formatHourMinute(timeObj) {
if (!timeObj) return "-";
const hours = timeObj.hours.toString().padStart(2, '0');
const minutes = timeObj.minutes.toString().padStart(2, '0');
return `${hours}:${minutes}`;
},
editRecord(index) {
const record = this.filteredRecords[index];
window.location.href = `/OTcalculate/Overtime/EditOvertime?overtimeId=${record.overtimeId}`;
},
async deleteRecord(index) {
const record = this.filteredRecords[index];
if (!confirm("Are you sure you want to delete this record?")) return;
try {
const res = await fetch(`/OvertimeAPI/DeleteOvertimeRecord/${record.overtimeId}`, { method: 'DELETE' });
if (res.ok) this.otRecords.splice(this.otRecords.indexOf(record), 1);
else alert("Failed to delete: " + await res.text());
} catch (err) {
console.error("Delete failed", err);
alert("Error deleting record.");
}
},
async printPdf() {
try {
const response = await fetch(`/OvertimeAPI/GenerateOvertimePdf?month=${this.selectedMonth}&year=${this.selectedYear}`);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const printWindow = window.open(blobUrl, '_blank');
printWindow.onload = () => {
printWindow.focus();
printWindow.print();
};
} catch (error) {
console.error("Error generating PDF:", error);
}
},
async downloadPdf() {
try {
const res = await fetch(`/OvertimeAPI/GenerateOvertimePdf?month=${this.selectedMonth}&year=${this.selectedYear}`);
if (res.ok) {
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `OvertimeRecords_${this.selectedYear}_${this.selectedMonth}.pdf`;
a.click();
URL.revokeObjectURL(url);
} else {
alert("Failed to generate PDF: " + await res.text());
}
} catch (err) {
console.error("PDF download error:", err);
alert("An error occurred while generating the PDF.");
}
},
downloadExcel(month, year) {
window.open(`/OvertimeAPI/GenerateOvertimeExcel?month=${month}&year=${year}`, '_blank');
},
openSubmitModal() {
const modal = new bootstrap.Modal(document.getElementById('submitModal'));
modal.show();
},
handleFileUpload(event) {
this.selectedFile = event.target.files[0];
},
async submitOvertime() {
if (!this.selectedFile) {
alert("Please select a file to upload.");
return;
}
const formData = new FormData();
formData.append('Month', this.selectedMonth);
formData.append('Year', this.selectedYear);
formData.append('File', this.selectedFile);
try {
const res = await axios.post('/OvertimeAPI/SubmitOvertime', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
if (res.status === 200) {
alert('Overtime submitted successfully!');
const modalEl = document.getElementById('submitModal');
const modalInstance = bootstrap.Modal.getInstance(modalEl);
modalInstance.hide();
} else {
alert('Submission failed.');
}
} catch (error) {
console.error(error);
alert('An error occurred during submission.');
}
}
}
});
app.mount("#app");
</script>
}