384 lines
16 KiB
Plaintext
384 lines
16 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 {
|
|
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;
|
|
}
|
|
|
|
td.wrap-text {
|
|
white-space: pre-wrap; /* Keep line breaks and wrap text */
|
|
word-wrap: break-word; /* Break long words if necessary */
|
|
max-width: 300px; /* Adjust as needed */
|
|
text-align: left; /* Optional: left-align description */
|
|
}
|
|
|
|
.description-preview {
|
|
max-height: 3.6em; /* approx. 2 lines */
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
cursor: pointer;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
transition: max-height 0.3s ease;
|
|
}
|
|
|
|
.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, 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 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 Hours (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 }} min</td>
|
|
<td>{{ formatTime(record.afterFrom) }}</td>
|
|
<td>{{ formatTime(record.afterTo) }}</td>
|
|
<td>{{ record.afterBreak }} min</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 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="isPSTWAIR ? 14 : 13">No records found for selected month and year.</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-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>
|
|
const app = Vue.createApp({
|
|
data() {
|
|
const currentYear = new Date().getFullYear();
|
|
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: {}
|
|
};
|
|
},
|
|
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();
|
|
},
|
|
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();
|
|
const department = data.userInfo?.department;
|
|
this.userId = data.userInfo?.id;
|
|
const deptName = department?.departmentName?.toUpperCase?.() || '';
|
|
this.isPSTWAIR = deptName.includes("PSTW") && deptName.includes("AIR") && department?.departmentId === 2;
|
|
} 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);
|
|
}
|
|
},
|
|
toggleDescription(index) {
|
|
this.expandedDescriptions[index] = !this.expandedDescriptions[index];
|
|
},
|
|
|
|
formatDate(d) {
|
|
return new Date(d).toLocaleDateString();
|
|
},
|
|
formatTime(t) {
|
|
return t ? t.slice(0, 5) : "-";
|
|
},
|
|
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) {
|
|
return timeObj ? `${timeObj.hours} h ${timeObj.minutes} m` : '-';
|
|
},
|
|
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.");
|
|
}
|
|
},
|
|
printPdf() {
|
|
const today = new Date();
|
|
const month = today.getMonth() + 1;
|
|
const year = today.getFullYear();
|
|
|
|
fetch(`/OvertimeAPI/GenerateOvertimePdf?month=${month}&year=${year}`)
|
|
.then(response => response.blob())
|
|
.then(blob => {
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
const printWindow = window.open(blobUrl, '_blank');
|
|
|
|
// Trigger print after window loads
|
|
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.");
|
|
}
|
|
},
|
|
async submitRecords() {
|
|
try {
|
|
const recordsToSubmit = this.filteredRecords.map(record => ({
|
|
overtimeId: record.overtimeId, // Make sure to include the ID for updates
|
|
otDate: record.otDate,
|
|
officeFrom: record.officeFrom,
|
|
officeTo: record.officeTo,
|
|
officeBreak: record.officeBreak,
|
|
afterFrom: record.afterFrom,
|
|
afterTo: record.afterTo,
|
|
afterBreak: record.afterBreak,
|
|
stationId: record.stationId,
|
|
otDescription: record.otDescription,
|
|
otDays: record.otDays,
|
|
filePath: record.filePath, // Include existing file path
|
|
userId: this.userId
|
|
// Add other relevant fields if necessary
|
|
}));
|
|
|
|
const res = await fetch('/OvertimeAPI/SubmitOvertimeRecords', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(recordsToSubmit)
|
|
});
|
|
if (res.ok) {
|
|
alert("Overtime records submitted for review.");
|
|
} else {
|
|
alert("Submission failed: " + await res.text());
|
|
}
|
|
} catch (err) {
|
|
console.error("Submission error:", err);
|
|
alert("An error occurred during submission.");
|
|
}
|
|
},
|
|
|
|
}
|
|
});
|
|
app.mount("#app");
|
|
</script>
|
|
} |