update user list / update Excel & PDF

This commit is contained in:
Naz 2026-04-09 16:27:49 +08:00
parent 9cd3c473cf
commit ed30316685
5 changed files with 258 additions and 226 deletions

View File

@ -36,10 +36,12 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
string? flexiHour = null, string? flexiHour = null,
byte[]? logoImage = null, byte[]? logoImage = null,
bool isSimplifiedExport = false, bool isSimplifiedExport = false,
OtStatusModel? otStatus = null) OtStatusModel? otStatus = null,
bool hideSalaryDetails = false)
{ {
bool isAdminUser = IsAdmin(user.Id); bool isAdminUser = IsAdmin(user.Id);
bool showStationColumn = user.Department?.DepartmentId == 3 || user.Department?.DepartmentId == 2 || isAdminUser; bool showStationColumn = user.Department?.DepartmentId == 3 || user.Department?.DepartmentId == 2 || isAdminUser;
bool hideSalary = hideSalaryDetails || isHoU || isHoD || isManager;
var stream = new MemoryStream(); var stream = new MemoryStream();
@ -100,7 +102,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
} }
else else
{ {
if (isHoU || isHoD || isManager) // If HoU, HoD, or Manager, hide salary details if (hideSalary) // If HoU, HoD, or Manager, hide salary details
{ {
if (showStationColumn) if (showStationColumn)
{ {
@ -263,7 +265,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
decimal otAmtValParsed = 0; decimal otAmtValParsed = 0;
string otAmt = ""; string otAmt = "";
if (!isHoU && !isHoD && !isManager) // Only calculate and show OT amount if not HoU, HoD, or Manager if (!hideSalary) // Only calculate and show OT amount if not HoU, HoD, or Manager
{ {
otAmt = CalculateOtAmount(record, hrp, publicHolidayDates, userSetting?.State?.WeekendId); otAmt = CalculateOtAmount(record, hrp, publicHolidayDates, userSetting?.State?.WeekendId);
otAmtValParsed = decimal.TryParse(otAmt, out decimal val) ? val : 0; otAmtValParsed = decimal.TryParse(otAmt, out decimal val) ? val : 0;
@ -291,7 +293,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
grandTotalPh += currentRowTotalPh; grandTotalPh += currentRowTotalPh;
grandTotalOtAmount += otAmtValParsed; // Only add if it was calculated grandTotalOtAmount += otAmtValParsed; // Only add if it was calculated
if (!isHoU && !isHoD && !isManager) // Only show if not HoU, HoD, or Manager if (!hideSalary) // Only show if not HoU, HoD, or Manager
{ {
// Basic Salary // Basic Salary
worksheet.Cell(currentRow, col).Value = !hasPrintedSalaryDetails ? basicSalary.ToString("N2") : ""; worksheet.Cell(currentRow, col).Value = !hasPrintedSalaryDetails ? basicSalary.ToString("N2") : "";
@ -348,7 +350,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
worksheet.Cell(currentRow, col++).Value = currentRowTotalRd > 0 ? currentRowTotalRd.ToString("N2") : ""; worksheet.Cell(currentRow, col++).Value = currentRowTotalRd > 0 ? currentRowTotalRd.ToString("N2") : "";
worksheet.Cell(currentRow, col++).Value = currentRowTotalPh > 0 ? currentRowTotalPh.ToString("N2") : ""; worksheet.Cell(currentRow, col++).Value = currentRowTotalPh > 0 ? currentRowTotalPh.ToString("N2") : "";
if (!isHoU && !isHoD && !isManager) // Only show if not HoU, HoD, or Manager if (!hideSalary) // Only show if not HoU, HoD, or Manager
{ {
worksheet.Cell(currentRow, col++).Value = otAmt == "0.00" ? "" : otAmt; worksheet.Cell(currentRow, col++).Value = otAmt == "0.00" ? "" : otAmt;
} }
@ -415,7 +417,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
int totalLabelStartColumnIndex = 1; int totalLabelStartColumnIndex = 1;
int totalLabelEndColumnIndex; int totalLabelEndColumnIndex;
if (!isHoU && !isHoD && !isManager) // If not HoU, HoD, or Manager, include salary columns in merge if (!hideSalary) // If not HoU, HoD, or Manager, include salary columns in merge
{ {
totalLabelEndColumnIndex = 3; totalLabelEndColumnIndex = 3;
} }
@ -445,7 +447,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
int colTotalPhIndex = 0; int colTotalPhIndex = 0;
int colOtAmtIndex = 0; int colOtAmtIndex = 0;
if (!isHoU && !isHoD && !isManager) if (!hideSalary)
{ {
colOfficeBreakIndex = 8; colOfficeBreakIndex = 8;
colAfterBreakIndex = 11; colAfterBreakIndex = 11;
@ -500,7 +502,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
worksheet.Cell(currentRow, colTotalRdIndex).Value = grandTotalRd.ToString("N2"); worksheet.Cell(currentRow, colTotalRdIndex).Value = grandTotalRd.ToString("N2");
worksheet.Cell(currentRow, colTotalPhIndex).Value = grandTotalPh.ToString("N2"); worksheet.Cell(currentRow, colTotalPhIndex).Value = grandTotalPh.ToString("N2");
if (!isHoU && !isHoD && !isManager) if (!hideSalary)
{ {
worksheet.Cell(currentRow, colOtAmtIndex).Value = Math.Round(grandTotalOtAmount, MidpointRounding.AwayFromZero).ToString("F2"); worksheet.Cell(currentRow, colOtAmtIndex).Value = Math.Round(grandTotalOtAmount, MidpointRounding.AwayFromZero).ToString("F2");
} }
@ -697,7 +699,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
} }
else else
{ {
if (!isHoU && !isHoD && !isManager) // If not HoU, HoD, or Manager, include salary columns in width adjustment if (!hideSalary) // If not HoU, HoD, or Manager, include salary columns in width adjustment
{ {
worksheet.Column(1).Width = 15; // Basic Salary worksheet.Column(1).Width = 15; // Basic Salary
worksheet.Column(2).Width = 10; // ORP worksheet.Column(2).Width = 10; // ORP

View File

@ -68,8 +68,10 @@
</style> </style>
<div id="reviewApp" style="max-width: 1300px; margin: auto; font-size: 13px;"> <div id="reviewApp" style="width: 100%; padding: 0 20px; font-size: 13px;">
<div class="table-container table-responsive">
<div class="table-container">
<div style="margin-bottom: 15px;"> <div style="margin-bottom: 15px;">
<strong>Employee Name:</strong> {{ userInfo.fullName }}<br /> <strong>Employee Name:</strong> {{ userInfo.fullName }}<br />
<strong>Department:</strong> {{ userInfo.departmentName || 'N/A' }}<br /> <strong>Department:</strong> {{ userInfo.departmentName || 'N/A' }}<br />
@ -83,158 +85,165 @@
</div> </div>
</template> </template>
</div> </div>
<table class="table table-bordered table-sm table-striped">
<thead>
<tr>
<th class="header-orange" rowspan="2" v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])">Basic Salary<br>(RM)</th>
<th class="header-orange" rowspan="2" v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])">ORP</th>
<th class="header-orange" rowspan="2" v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])">HRP</th>
<th class="header-green text-nowrap" rowspan="2">Date</th>
<th class="header-blue" colspan="3">Office Hour</th>
<th class="header-blue" colspan="3">After Office Hour</th>
<th class="header-green" rowspan="2">OT Hrs<br>(Office Hour)</th>
<th class="header-green" rowspan="2">OT Hrs<br>(After Office Hour)</th>
<th class="header-blue" colspan="1">Normal Day</th>
<th class="header-blue" colspan="3">Off Day</th>
<th class="header-blue" colspan="3">Rest Day</th>
<th class="header-blue" colspan="2">Public Holiday</th>
<th class="header-green" rowspan="2">Total OT Hrs</th>
<th class="header-green" rowspan="2">Total OT Hrs<br>ND & OD</th>
<th class="header-green" rowspan="2">Total OT Hrs<br>RD</th>
<th class="header-green" rowspan="2">Total OT Hrs<br>PH</th>
<th class="header-green" rowspan="2" v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])">OT Amt (RM)</th>
<th class="header-orange" rowspan="2" v-if="showStationColumn">Station</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>
<th class="header-blue">ND OT after office hrs</th>
<th class="header-blue">OD Within office hrs &lt; 4hrs</th>
<th class="header-blue">OD Within office hrs &gt; 4hrs &lt; 8 hrs</th>
<th class="header-blue">OD After office hrs</th>
<th class="header-blue">RD Within office hrs &lt; 4hrs</th>
<th class="header-blue">RD Within office hrs &gt; 4hrs &lt; 8 hrs</th>
<th class="header-blue">RD After office hrs</th>
<th class="header-blue">PH Within office hrs &lt; 8 hrs</th>
<th class="header-blue">PH After office hrs</th>
</tr>
</thead>
<tbody>
<tr v-for="(record, index) in sortedOtRecords" :key="record.overtimeId">
<template v-if="index === 0 && !isApproverRole(['HoU', 'HoD', 'Manager'])">
<td :rowspan="sortedOtRecords.length" class="text-nowrap">{{ calculateBasicSalary() }}</td>
<td :rowspan="sortedOtRecords.length" class="text-nowrap">{{ calculateOrp() }}</td>
<td :rowspan="sortedOtRecords.length" class="text-nowrap">{{ calculateHrp() }}</td>
</template>
<td class="text-nowrap">{{ formatDate(record.otDate) }}</td> <div class="table-responsive custom-scrollbar">
<td>{{ formatTime(record.officeFrom) }}</td> <table class="table table-bordered table-sm table-striped" style="min-width: 1500px;">
<td>{{ formatTime(record.officeTo) }}</td> <thead>
<td>{{ formatBreakToHourMinute(record.officeBreak, false) }}</td> <tr>
<td>{{ formatTime(record.afterFrom) }}</td> <th class="header-orange" rowspan="2" v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])">Basic Salary<br>(RM)</th>
<td>{{ formatTime(record.afterTo) }}</td> <th class="header-orange" rowspan="2" v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])">ORP</th>
<td>{{ formatBreakToHourMinute(record.afterBreak, false) }}</td> <th class="header-orange" rowspan="2" v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])">HRP</th>
<td>{{ formatTimeFromDecimal(calculateRawDuration(record.officeFrom, record.officeTo, record.officeBreak)) }}</td> <th class="header-green text-nowrap" rowspan="2">Date</th>
<td>{{ formatTimeFromDecimal(calculateRawDuration(record.afterFrom, record.afterTo, record.afterBreak)) }}</td> <th class="header-blue" colspan="3">Office Hour</th>
<th class="header-blue" colspan="3">After Office Hour</th>
<th class="header-green" rowspan="2">OT Hrs<br>(Office Hour)</th>
<th class="header-green" rowspan="2">OT Hrs<br>(After Office Hour)</th>
<th class="header-blue" colspan="1">Normal Day</th>
<th class="header-blue" colspan="3">Off Day</th>
<th class="header-blue" colspan="3">Rest Day</th>
<th class="header-blue" colspan="2">Public Holiday</th>
<th class="header-green" rowspan="2">Total OT Hrs</th>
<th class="header-green" rowspan="2">Total OT Hrs<br>ND & OD</th>
<th class="header-green" rowspan="2">Total OT Hrs<br>RD</th>
<th class="header-green" rowspan="2">Total OT Hrs<br>PH</th>
<th class="header-green" rowspan="2" v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])">OT Amt (RM)</th>
<th class="header-orange" rowspan="2" v-if="showStationColumn">Station</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>
<th class="header-blue">ND OT after office hrs</th>
<th class="header-blue">OD Within office hrs &lt; 4hrs</th>
<th class="header-blue">OD Within office hrs &gt; 4hrs &lt; 8 hrs</th>
<th class="header-blue">OD After office hrs</th>
<th class="header-blue">RD Within office hrs &lt; 4hrs</th>
<th class="header-blue">RD Within office hrs &gt; 4hrs &lt; 8 hrs</th>
<th class="header-blue">RD After office hrs</th>
<th class="header-blue">PH Within office hrs &lt; 8 hrs</th>
<th class="header-blue">PH After office hrs</th>
</tr>
</thead>
<tbody>
<tr v-for="(record, index) in sortedOtRecords" :key="record.overtimeId">
<template v-if="index === 0 && !isApproverRole(['HoU', 'HoD', 'Manager'])">
<td :rowspan="sortedOtRecords.length" class="text-nowrap">{{ calculateBasicSalary() }}</td>
<td :rowspan="sortedOtRecords.length" class="text-nowrap">{{ calculateOrp() }}</td>
<td :rowspan="sortedOtRecords.length" class="text-nowrap">{{ calculateHrp() }}</td>
</template>
<td>{{ classifyOt(record).ndAfter }}</td> <td class="text-nowrap">{{ formatDate(record.otDate) }}</td>
<td>{{ formatTime(record.officeFrom) }}</td>
<td>{{ formatTime(record.officeTo) }}</td>
<td>{{ formatBreakToHourMinute(record.officeBreak, false) }}</td>
<td>{{ formatTime(record.afterFrom) }}</td>
<td>{{ formatTime(record.afterTo) }}</td>
<td>{{ formatBreakToHourMinute(record.afterBreak, false) }}</td>
<td>{{ formatTimeFromDecimal(calculateRawDuration(record.officeFrom, record.officeTo, record.officeBreak)) }}</td>
<td>{{ formatTimeFromDecimal(calculateRawDuration(record.afterFrom, record.afterTo, record.afterBreak)) }}</td>
<td>{{ classifyOt(record).odUnder4 }}</td> <td>{{ classifyOt(record).ndAfter }}</td>
<td>{{ classifyOt(record).odBetween4And8 }}</td>
<td>{{ classifyOt(record).odAfter }}</td>
<td>{{ classifyOt(record).rdUnder4 }}</td> <td>{{ classifyOt(record).odUnder4 }}</td>
<td>{{ classifyOt(record).rdBetween4And8 }}</td> <td>{{ classifyOt(record).odBetween4And8 }}</td>
<td>{{ classifyOt(record).rdAfter }}</td> <td>{{ classifyOt(record).odAfter }}</td>
<td>{{ classifyOt(record).phUnder8 }}</td> <td>{{ classifyOt(record).rdUnder4 }}</td>
<td>{{ classifyOt(record).phAfter }}</td> <td>{{ classifyOt(record).rdBetween4And8 }}</td>
<td>{{ classifyOt(record).rdAfter }}</td>
<td>{{ calculateTotalOtHrs(record) }}</td> <td>{{ classifyOt(record).phUnder8 }}</td>
<td>{{ classifyOt(record).phAfter }}</td>
<td>{{ calculateNdOdTotal(record) }}</td> <td>{{ calculateTotalOtHrs(record) }}</td>
<td>{{ calculateRdTotal(record) }}</td>
<td>{{ calculatePhTotal(record) }}</td>
<td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])">{{ calculateOtAmount(record) }}</td> <td>{{ calculateNdOdTotal(record) }}</td>
<td>{{ calculateRdTotal(record) }}</td>
<td>{{ calculatePhTotal(record) }}</td>
<td v-if="showStationColumn">{{ record.stationName || 'N/A' }}</td> <td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])">{{ calculateOtAmount(record) }}</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"
v-on:click="editRecord(record.overtimeId)"
:disabled="hasApproverActedLocal">
<i class="bi bi-pencil-fill text-warning fs-5"></i>
</button>
<button class="btn btn-light border rounded-circle"
v-on:click="deleteRecord(record.overtimeId)"
:disabled="hasApproverActedLocal">
<i class="bi bi-trash-fill text-danger fs-5"></i>
</button>
</td>
</tr>
<tr v-if="otRecords.length === 0">
<td :colspan="showStationColumn ? (isApproverRole(['HoU', 'HoD', 'Manager']) ? 27 : 31) : (isApproverRole(['HoU', 'HoD', 'Manager']) ? 26 : 30)">No overtime details found for this submission.</td>
</tr>
</tbody>
<tfoot>
<tr class="fw-bold bg-light">
<td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="3">TOTAL</td>
<td v-else colspan="1">TOTAL</td>
<td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="2"></td> <td v-if="showStationColumn">{{ record.stationName || 'N/A' }}</td>
<td v-else colspan="2"></td> <td class="wrap-text">
<td><strong>{{ formatBreakToHourMinute(totals.officeBreak) }}</strong></td> <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"
v-on:click="editRecord(record.overtimeId)"
:disabled="hasApproverActedLocal">
<i class="bi bi-pencil-fill text-warning fs-5"></i>
</button>
<button class="btn btn-light border rounded-circle"
v-on:click="deleteRecord(record.overtimeId)"
:disabled="hasApproverActedLocal">
<i class="bi bi-trash-fill text-danger fs-5"></i>
</button>
</td>
</tr>
<tr v-if="otRecords.length === 0">
<td :colspan="showStationColumn ? (isApproverRole(['HoU', 'HoD', 'Manager']) ? 27 : 31) : (isApproverRole(['HoU', 'HoD', 'Manager']) ? 26 : 30)">No overtime details found for this submission.</td>
</tr>
</tbody>
<tfoot>
<tr class="fw-bold bg-light">
<td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="3">TOTAL</td>
<td v-else colspan="1">TOTAL</td>
<td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="2"></td> <td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="2"></td>
<td v-else colspan="2"></td> <td v-else colspan="2"></td>
<td><strong>{{ formatBreakToHourMinute(totals.afterBreak) }}</strong></td> <td><strong>{{ formatBreakToHourMinute(totals.officeBreak) }}</strong></td>
<td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="2"></td> <td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="2"></td>
<td v-else colspan="2"></td> <td v-else colspan="2"></td>
<td>{{ totals.ndAfter }}</td> <td><strong>{{ formatBreakToHourMinute(totals.afterBreak) }}</strong></td>
<td>{{ totals.odUnder4 }}</td> <td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="2"></td>
<td>{{ totals.odBetween4And8 }}</td> <td v-else colspan="2"></td>
<td>{{ totals.odAfter }}</td> <td>{{ totals.ndAfter }}</td>
<td>{{ totals.rdUnder4 }}</td> <td>{{ totals.odUnder4 }}</td>
<td>{{ totals.rdBetween4And8 }}</td> <td>{{ totals.odBetween4And8 }}</td>
<td>{{ totals.rdAfter }}</td> <td>{{ totals.odAfter }}</td>
<td>{{ totals.phUnder8 }}</td> <td>{{ totals.rdUnder4 }}</td>
<td>{{ totals.phAfter }}</td> <td>{{ totals.rdBetween4And8 }}</td>
<td>{{ totals.rdAfter }}</td>
<td>{{ totals.totalOtHrs }}</td> <td>{{ totals.phUnder8 }}</td>
<td>{{ totals.totalNdOd }}</td> <td>{{ totals.phAfter }}</td>
<td>{{ totals.totalRd }}</td>
<td>{{ totals.totalPh }}</td>
<td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])">{{ totals.otAmt }}</td> <td>{{ totals.totalOtHrs }}</td>
<td>{{ totals.totalNdOd }}</td>
<td>{{ totals.totalRd }}</td>
<td>{{ totals.totalPh }}</td>
<td v-if="showStationColumn"></td> <td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])">{{ totals.otAmt }}</td>
<td colspan="3"></td> <td v-if="showStationColumn"></td>
</tr>
</tfoot> <td colspan="3"></td>
</table> </tr>
</tfoot>
</table>
</div>
</div> </div>
<div class="mt-3 d-flex flex-wrap gap-2"> <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-primary btn-sm" v-on:click="printPdf(false)"><i class="bi bi-printer"></i> Print</button>
<button class="btn btn-dark btn-sm" v-on:click="saveAsPdf"><i class="bi bi-file-pdf"></i> Save</button> <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"><i class="bi bi-file-earmark-excel"></i> Excel</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">
<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>
</div> </div>
<div class="modal fade" id="editOtModal" tabindex="-1" aria-labelledby="editOtModalLabel" aria-hidden="true"> <div class="modal fade" id="editOtModal" tabindex="-1" aria-labelledby="editOtModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl"> <div class="modal-dialog modal-xl">
<div class="modal-content"> <div class="modal-content">
@ -1212,11 +1221,11 @@
alert("Error deleting record: " + err.message); alert("Error deleting record: " + err.message);
}); });
}, },
saveAsPdf() { saveAsPdf(hideSalary = false) {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const statusId = params.get("statusId"); const statusId = params.get("statusId");
fetch(`/OvertimeAPI/GetOvertimePdfByStatusId/${statusId}`) fetch(`/OvertimeAPI/GetOvertimePdfByStatusId/${statusId}?hideSalary=${hideSalary}`)
.then(response => { .then(response => {
if (!response.ok) throw new Error("Failed to generate PDF"); if (!response.ok) throw new Error("Failed to generate PDF");
const disposition = response.headers.get("Content-Disposition"); const disposition = response.headers.get("Content-Disposition");
@ -1245,11 +1254,11 @@
alert("Failed to generate PDF. Please try again later."); alert("Failed to generate PDF. Please try again later.");
}); });
}, },
printPdf() { printPdf(hideSalary = false) {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const statusId = params.get("statusId"); const statusId = params.get("statusId");
fetch(`/OvertimeAPI/GetOvertimePdfByStatusId/${statusId}`) fetch(`/OvertimeAPI/GetOvertimePdfByStatusId/${statusId}?hideSalary=${hideSalary}`)
.then(response => { .then(response => {
if (!response.ok) throw new Error("Failed to fetch PDF for printing."); if (!response.ok) throw new Error("Failed to fetch PDF for printing.");
return response.blob(); return response.blob();
@ -1273,11 +1282,11 @@
alert("Failed to prepare PDF for printing. Please try again later."); alert("Failed to prepare PDF for printing. Please try again later.");
}); });
}, },
exportToExcel() { exportToExcel(hideSalary = false) {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const statusId = params.get("statusId"); const statusId = params.get("statusId");
fetch(`/OvertimeAPI/GetOvertimeExcelByStatusId/${statusId}`) fetch(`/OvertimeAPI/GetOvertimeExcelByStatusId/${statusId}?hideSalary=${hideSalary}`)
.then(response => { .then(response => {
if (!response.ok) throw new Error("Failed to generate Excel"); if (!response.ok) throw new Error("Failed to generate Excel");
const disposition = response.headers.get("Content-Disposition"); const disposition = response.headers.get("Content-Disposition");

View File

@ -154,7 +154,7 @@
}, },
async fetchUser() { async fetchUser() {
try { try {
const response = await fetch(`/InvMainAPI/UserList/`, { method: 'POST' }); const response = await fetch(`/OvertimeAPI/UserList/`, { method: 'POST' });
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
this.userList = data.filter(e => e.fullName !== "MAAdmin" && e.fullName !== "SysAdmin"); this.userList = data.filter(e => e.fullName !== "MAAdmin" && e.fullName !== "SysAdmin");

View File

@ -66,7 +66,7 @@
} }
</style> </style>
<div id="app" style="max-width: 1300px; margin: auto; font-size: 13px;"> <div id="app" style="width: 100%; padding: 0 20px; font-size: 13px;">
<div class="mb-3 d-flex flex-wrap"> <div class="mb-3 d-flex flex-wrap">
<div class="me-2 mb-2"> <div class="me-2 mb-2">
<label>Month</label> <label>Month</label>
@ -82,79 +82,83 @@
</div> </div>
</div> </div>
<div id="print-section" class="table-container table-responsive"> <div id="print-section" class="table-container">
<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</th>
<th class="header-blue" colspan="3">After Office Hour</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>{{ formatMinutesToHourMinute(record.officeBreak) }}</td>
<td>{{ formatTime(record.afterFrom) }}</td>
<td>{{ formatTime(record.afterTo) }}</td>
<td>{{ formatMinutesToHourMinute(record.afterBreak) }}</td>
<td>{{ formatHourMinute(calcTotalTime(record)) }}</td>
<td>{{ formatMinutesToHourMinute(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> <div class="table-responsive custom-scrollbar">
<tr v-if="filteredRecords.length === 0">
<td :colspan="isPSTWAIR ? 14 : 13">No records found.</td> <table class="table table-bordered table-sm table-striped" style="min-width: 1200px;">
</tr> <thead>
<tr class="table-primary fw-bold"> <tr>
<td>TOTAL</td> <th class="header-green" rowspan="2">Date</th>
<td colspan="6"></td> <th class="header-blue" colspan="3">Office Hour</th>
<td>{{ formatHourMinute(totalHours) }}</td> <th class="header-blue" colspan="3">After Office Hour</th>
<td>{{ formatHourMinute(totalBreak) }}</td> <th class="header-orange" rowspan="2">Total OT Hours</th>
<td>{{ formatHourMinute(totalNetTime) }}</td> <th class="header-orange" rowspan="2">Break (min)</th>
<td v-if="isPSTWAIR"></td> <th class="header-orange" rowspan="2">Net OT Hours</th>
<td colspan="3"></td> <th class="header-orange" rowspan="2" v-if="isPSTWAIR">Station</th>
</tr> <th class="header-green" rowspan="2">Days</th>
</tbody> <th class="header-blue" rowspan="2">Description</th>
</table> <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>{{ formatMinutesToHourMinute(record.officeBreak) }}</td>
<td>{{ formatTime(record.afterFrom) }}</td>
<td>{{ formatTime(record.afterTo) }}</td>
<td>{{ formatMinutesToHourMinute(record.afterBreak) }}</td>
<td>{{ formatHourMinute(calcTotalTime(record)) }}</td>
<td>{{ formatMinutesToHourMinute(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> </div>
<div class="mt-3 d-flex flex-wrap gap-2"> <div class="mt-3 d-flex flex-wrap gap-2">

View File

@ -51,6 +51,22 @@ namespace PSTW_CentralSystem.Controllers.API
_env = env; _env = env;
} }
[HttpPost("UserList")]
public async Task<IActionResult> UserList()
{
var userList = await _centralDbContext.Users
.Include(i => i.Department)
.ToListAsync();
return Json(userList.Select(i => new
{
i.Id,
i.FullName,
i.Department,
i.Department?.DepartmentName,
}));
}
#region Settings #region Settings
[HttpGet("GetUpdateDates")] [HttpGet("GetUpdateDates")]
@ -2478,7 +2494,7 @@ namespace PSTW_CentralSystem.Controllers.API
#region OT Review/ OT Record PDF Excel #region OT Review/ OT Record PDF Excel
[HttpGet("GetOvertimePdfByStatusId/{statusId}")] [HttpGet("GetOvertimePdfByStatusId/{statusId}")]
public IActionResult GetOvertimePdfByStatusId(int statusId) public IActionResult GetOvertimePdfByStatusId(int statusId, [FromQuery] bool hideSalary = false)
{ {
var otStatus = _centralDbContext.Otstatus.FirstOrDefault(s => s.StatusId == statusId); var otStatus = _centralDbContext.Otstatus.FirstOrDefault(s => s.StatusId == statusId);
if (otStatus == null) if (otStatus == null)
@ -2519,14 +2535,14 @@ namespace PSTW_CentralSystem.Controllers.API
.FirstOrDefault(us => us.UserId == user.Id); .FirstOrDefault(us => us.UserId == user.Id);
// Determine if the current logged-in user is HoU, HoD, or Manager for the OT user // Determine if the current logged-in user is HoU, HoD, or Manager for the OT user
bool hideSalaryDetails = false; bool hideSalaryDetails = hideSalary; // Start with the button's request
if (userSetting?.Approvalflow != null) if (userSetting?.Approvalflow != null)
{ {
if (userSetting.Approvalflow.HoU == currentLoggedInUserId || if (userSetting.Approvalflow.HoU == currentLoggedInUserId ||
userSetting.Approvalflow.HoD == currentLoggedInUserId || userSetting.Approvalflow.HoD == currentLoggedInUserId ||
userSetting.Approvalflow.Manager == currentLoggedInUserId) userSetting.Approvalflow.Manager == currentLoggedInUserId)
{ {
hideSalaryDetails = true; hideSalaryDetails = true; // Override to true if they are a manager
} }
} }
@ -2629,7 +2645,7 @@ namespace PSTW_CentralSystem.Controllers.API
} }
[HttpGet("GetOvertimeExcelByStatusId/{statusId}")] [HttpGet("GetOvertimeExcelByStatusId/{statusId}")]
public IActionResult GetOvertimeExcelByStatusId(int statusId) public IActionResult GetOvertimeExcelByStatusId(int statusId, [FromQuery] bool hideSalary = false)
{ {
var otStatus = _centralDbContext.Otstatus.FirstOrDefault(s => s.StatusId == statusId); var otStatus = _centralDbContext.Otstatus.FirstOrDefault(s => s.StatusId == statusId);
if (otStatus == null) if (otStatus == null)
@ -2700,7 +2716,8 @@ namespace PSTW_CentralSystem.Controllers.API
flexiHour, flexiHour,
logoImage, logoImage,
isSimplifiedExport: false, isSimplifiedExport: false,
otStatus otStatus,
hideSalaryDetails: hideSalary
); );
string fileName = $"OvertimeReport_{user.FullName}_{DateTime.Now:yyyyMMdd}.xlsx"; string fileName = $"OvertimeReport_{user.FullName}_{DateTime.Now:yyyyMMdd}.xlsx";