PSTW_CentralizeSystem/Areas/OTcalculate/Views/Overtime/OtRecords.cshtml
2025-04-09 17:30:56 +08:00

299 lines
12 KiB
Plaintext

@{
ViewData["Title"] = "My 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 {
border-radius: 10px;
}
select.form-control {
border-radius: 10px;
}
.btn {
border-radius: 10px !important;
font-size: 13px;
}
.table-primary td {
background-color: #e6f0ff !important;
}
.table-responsive {
overflow-x: auto;
}
</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, index) in months" :value="index + 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 text-center align-middle">
<thead>
<tr>
<th class="header-green" rowspan="2">Date</th>
<th class="header-blue" colspan="3">Office<br><small>(8:30 - 17:30)</small></th>
<th class="header-blue" colspan="3">Outside<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 Hours (min)</th>
<th class="header-orange" rowspan="2">Net OT</th>
<th class="header-orange" rowspan="2">Station</th>
<th class="header-blue" rowspan="2">Description</th>
<th class="header-blue" rowspan="2">File</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 }} min</td>
<td>{{ formatTime(record.outsideFrom) }}</td>
<td>{{ formatTime(record.outsideTo) }}</td>
<td>{{ record.outsideBreak }} min</td>
<td>{{ calcTotalHours(record).toFixed(2) }}</td>
<td>{{ calcBreakTotal(record) }}</td>
<td>{{ formatHourMinute(calcNetHours(record)) }}</td>
<td>{{ record.stationName || 'N/A' }}</td>
<td>{{ record.otDescription }}</td>
<td>
<span v-if="record.pdfBase64">
<button class="btn btn-light border rounded-circle" title="View PDF" v-on:click="viewPdf(record.pdfBase64)">
<i class="bi bi-file-earmark-pdf-fill fs-5"></i>
</button>
</span>
<span v-else>-</span>
</td>
<td>
<button class="btn btn-light border rounded-circle me-1" title="Edit Record" 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 Record" 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="14">No records found for selected month and year.</td>
</tr>
<tr class="table-primary fw-bold">
<td>TOTAL</td>
<td colspan="6"></td>
<td>{{ totalHours.toFixed(2) }}</td>
<td>{{ totalBreak }}</td>
<td>{{ formatHourMinute(totalNetTime) }}</td>
<td colspan="4"></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="printTable">
<i class="bi bi-printer"></i> Print
</button>
<button class="btn btn-dark btn-sm" v-on:click="downloadPdf">
<i class="bi bi-download"></i> Save
</button>
<button class="btn btn-success btn-sm" v-on:click="submitRecords">
<i class="bi bi-send"></i> Submit
</button>
</div>
</div>
@section Scripts {
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.2/html2pdf.bundle.min.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
otRecords: [],
userId: null,
selectedMonth: new Date().getMonth() + 1,
selectedYear: new Date().getFullYear(),
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
years: Array.from({ length: 10 }, (_, i) => new Date().getFullYear() - 5 + i)
};
},
computed: {
filteredRecords() {
return this.otRecords
.filter(record => {
const date = new Date(record.otDate);
return date.getMonth() + 1 === this.selectedMonth && date.getFullYear() === this.selectedYear;
})
.sort((a, b) => new Date(a.otDate) - new Date(b.otDate));
},
totalHours() {
return this.filteredRecords.reduce((sum, r) => sum + this.calcTotalHours(r), 0);
},
totalBreak() {
return this.filteredRecords.reduce((sum, r) => sum + this.calcBreakTotal(r), 0);
},
netHours() {
return this.totalHours - (this.totalBreak / 60);
},
totalNetTime() {
const totalMinutes = (this.totalHours * 60) - this.totalBreak;
const hours = Math.floor(totalMinutes / 60);
const minutes = Math.round(totalMinutes % 60);
return { hours, minutes };
}
},
async mounted() {
await this.initUserAndRecords();
},
methods: {
async initUserAndRecords() {
await this.fetchUser();
if (this.userId) {
await this.fetchOtRecords();
}
},
async fetchUser() {
try {
const res = await fetch('/IdentityAPI/GetUserInformation', { method: 'POST' });
const data = await res.json();
this.userId = data.userInfo?.id;
} catch (err) { console.error("User fetch error", err); }
},
async fetchOtRecords() {
try {
const res = await fetch(`/OvertimeAPI/GetUserOvertimeRecords/${this.userId}`);
this.otRecords = await res.json();
} catch (err) { console.error("Records fetch error", err); }
},
formatDate(d) {
return new Date(d).toLocaleDateString();
},
formatTime(t) {
return t ? t.slice(0, 5) : "-";
},
calcTotalHours(r) {
return this.getTimeDiff(r.officeFrom, r.officeTo) + this.getTimeDiff(r.outsideFrom, r.outsideTo);
},
calcBreakTotal(r) {
return (r.officeBreak || 0) + (r.outsideBreak || 0);
},
calcNetHours(r) {
const totalHours = this.calcTotalHours(r);
const breakMinutes = this.calcBreakTotal(r);
const netMinutes = (totalHours * 60) - breakMinutes;
const hours = Math.floor(netMinutes / 60);
const minutes = Math.round(netMinutes % 60);
return { hours, minutes };
},
formatHourMinute(timeObj) {
if (!timeObj) return '-';
return `${timeObj.hours} hr ${timeObj.minutes} min`;
},
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;
},
editRecord(index) {
},
async deleteRecord(index) {
const record = this.filteredRecords[index];
const confirmed = confirm("Are you sure you want to delete this record?");
if (!confirmed) return;
try {
const res = await fetch(`/OvertimeAPI/DeleteOvertimeRecord/${record.overtimeId}`, {
method: 'DELETE'
});
if (res.ok) {
// Remove from local state after successful backend deletion
this.otRecords.splice(this.otRecords.indexOf(record), 1);
} else {
const errorText = await res.text();
alert("Failed to delete: " + errorText);
}
} catch (err) {
console.error("Delete failed", err);
alert("Error deleting record.");
}
},
printTable() {
window.print();
},
downloadPdf() {
const element = document.getElementById("print-section");
html2pdf().from(element).save("OT_Records.pdf");
},
submitRecords() {
alert("Submitting records...");
},
viewPdf(base64) {
const byteArray = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const blob = new Blob([byteArray], { type: 'application/pdf' });
window.open(URL.createObjectURL(blob));
}
}
});
app.mount("#app");
</script>
}