This commit is contained in:
Naz 2025-04-18 10:50:23 +08:00
parent 695af0f339
commit be8084ca85
9 changed files with 262 additions and 23 deletions

View File

@ -18,5 +18,9 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Controllers
{
return View();
}
public IActionResult OtReview()
{
return View();
}
}
}

View File

@ -15,7 +15,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
int departmentId,
string userFullName,
string departmentName,
byte[]? logoImage = null // Optional logo image
byte[]? logoImage = null
)
{
records = records
@ -31,7 +31,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
page.Size(PageSizes.A4.Landscape());
page.Margin(30);
// Header section with logo and user info
page.Content().Column(column =>
{
column.Item().Row(row =>
@ -103,7 +102,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
AddHeaderCell("Description", "#e3f2fd");
});
// Data Rows
// Data Rows
double totalOTSum = 0;
int totalBreakSum = 0;
@ -112,7 +110,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
if (!records.Any())
{
// Show message row if no records
uint colspan = (uint)(departmentId == 2 ? 13 : 12);
table.Cell().ColumnSpan(colspan)
@ -124,7 +121,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
.FontColor(Colors.Grey.Darken2)
.Italic();
}
else
{
foreach (var r in records)
@ -165,7 +161,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
table.Cell().Background(rowBg).Border(0.25f).Padding(5).Text(r.OtDescription ?? "-").FontSize(9).WrapAnywhere().LineHeight(1.2f);
}
// Totals Row
var totalOTTimeSpan = TimeSpan.FromHours(totalOTSum);
var totalBreakTimeSpan = TimeSpan.FromMinutes(totalBreakSum);
@ -196,7 +191,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
return stream;
}
private TimeSpan CalculateTotalOT(OtRegisterModel r)
{
TimeSpan office = (r.OfficeTo ?? TimeSpan.Zero) - (r.OfficeFrom ?? TimeSpan.Zero);

View File

@ -2,16 +2,67 @@
ViewData["Title"] = "Overtime Approval";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<div class="container">
<div class="row justify-content-center">
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<h6 class="text-white">Rate</h6>
</div>
<div class="container mt-4" id="hod-approval-app">
<table class="table table-bordered table-striped mt-3">
<thead class="thead-light">
<tr>
<th>Name</th>
<th>Submission Date</th>
<th>Month/Year</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="submission in submittedRecords" :key="submission.userId + '-' + submission.monthYear">
<td><a :href="'/OTcalculate/HodDashboard/OtReview/' + submission.userId + '/' + getMonthYearParts(submission.monthYear).month + '/' + getMonthYearParts(submission.monthYear).year">{{ submission.userName }}</a></td>
<td>{{ formatDate(submission.submissionDate) }}</td>
<td>{{ submission.monthYear }}</td>
<td><button class="btn btn-info btn-sm">Review</button></td>
</tr>
<tr v-if="!submittedRecords.length">
<td colspan="4">No overtime records submitted for review.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
@section Scripts {
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
<script>
const hodApprovalApp = Vue.createApp({
data() {
return {
submittedRecords: []
};
},
mounted() {
this.fetchSubmittedRecords();
},
methods: {
async fetchSubmittedRecords() {
try {
const res = await fetch('/OvertimeAPI/GetSubmittedOvertimeRecords');
if (res.ok) {
this.submittedRecords = await res.json();
} else {
console.error("Failed to fetch submitted records:", await res.text());
alert("Error fetching submitted records.");
}
} catch (error) {
console.error("Error fetching submitted records:", error);
alert("An unexpected error occurred.");
}
},
formatDate(dateString) {
if (!dateString) return '-';
return new Date(dateString).toLocaleDateString();
},
getMonthYearParts(monthYearString) {
const parts = monthYearString.split('/');
return { month: parts[0], year: parts[1] };
}
}
}).mount("#hod-approval-app");
</script>
}

View File

@ -0,0 +1,169 @@
@{
ViewData["Title"] = "Overtime Review";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div id="ot-review-app" style="max-width: 1300px; margin: auto; font-size: 13px;">
<h2>Overtime Records for {{ reviewedUserName }} - {{ formatMonthYear(selectedMonth, selectedYear) }}</h2>
<div id="print-section" class="table-container table-responsive">
<table class="table table-bordered table-sm table-striped text-center align-middle">
<thead>
</thead>
<tbody>
<tr v-for="(record, index) in otRecords" :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>{{ 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>
<span v-if="record.filePath">
<a :href="record.filePath" target="_blank" class="btn btn-light border rounded-circle" title="View PDF">
<i class="bi bi-file-earmark-pdf-fill fs-5"></i>
</a>
</span>
<span v-else>-</span>
</td>
<td>
<button class="btn btn-success btn-sm me-1">Approve</button>
<button class="btn btn-danger btn-sm">Reject</button>
</td>
</tr>
<tr v-if="otRecords.length === 0">
<td :colspan="isPSTWAIR ? 14 : 13">No overtime records found for this user and period.</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="4"></td>
</tr>
</tbody>
</table>
</div>
</div>
@section Scripts {
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
<script>
const otReviewApp = Vue.createApp({
data() {
return {
userId: null,
selectedMonth: null,
selectedYear: null,
otRecords: [],
isPSTWAIR: false, // You might need to fetch this based on the reviewed user
expandedDescriptions: {},
reviewedUserName: '' // To display the name of the user being reviewed
};
},
computed: {
totalHours() { /* ... your totalHours calculation ... */ },
totalBreak() { /* ... your totalBreak calculation ... */ },
totalNetTime() { /* ... your totalNetTime calculation ... */ }
},
mounted() {
this.getParamsFromUrl();
this.fetchOtRecords();
this.fetchUserInfo(); // To get the name and department of the reviewed user
},
methods: {
getParamsFromUrl() {
const pathSegments = window.location.pathname.split('/');
this.userId = parseInt(pathSegments[4]); // Assuming the URL structure is /OTcalculate/HodDashboard/OtReview/{userId}/{month}/{year}
this.selectedMonth = parseInt(pathSegments[5]);
this.selectedYear = parseInt(pathSegments[6]);
},
async fetchUserInfo() {
try {
const res = await fetch(`/IdentityAPI/GetUserInformationById/${this.userId}`, { method: 'POST' }); // Assuming you have an endpoint to get user info by ID
if (res.ok) {
const data = await res.json();
this.reviewedUserName = data.userInfo?.fullName;
const department = data.userInfo?.department;
const deptName = department?.departmentName?.toUpperCase?.() || '';
this.isPSTWAIR = deptName.includes("PSTW") && deptName.includes("AIR") && department?.departmentId === 2;
} else {
console.error("Failed to fetch user info:", await res.text());
}
} catch (error) {
console.error("Error fetching user info:", error);
}
},
async fetchOtRecords() {
try {
const res = await fetch(`/OvertimeAPI/GetSubmittedUserOvertimeRecords/<span class="math-inline">\{this\.userId\}/</span>{this.selectedMonth}/${this.selectedYear}`);
if (res.ok) {
this.otRecords = await res.json();
} else {
console.error("Failed to fetch OT records:", await res.text());
alert("Error fetching overtime records.");
}
} catch (error) {
console.error("Error fetching OT records:", error);
alert("An unexpected error occurred.");
}
},
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.outsideFrom, r.outsideTo);
},
calcBreakTotal(r) {
return (r.officeBreak || 0) + (r.outsideBreak || 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` : '-';
},
formatMonthYear(month, year) {
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
return `${monthNames[month - 1]} ${year}`;
}
// Add methods for Approve and Reject actions here
}
}).mount("#ot-review-app");
</script>
}

View File

@ -353,13 +353,34 @@
},
async submitRecords() {
try {
const res = await fetch('/OvertimeAPI/SaveOvertimeRecordsWithPdf', {
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,
outsideFrom: record.outsideFrom,
outsideTo: record.outsideTo,
outsideBreak: record.outsideBreak,
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(this.otRecords)
body: JSON.stringify(recordsToSubmit)
});
if (res.ok) alert("Overtime records submitted successfully.");
else alert("Submission failed.");
if (res.ok) {
alert("Overtime records submitted for review.");
// Optionally, clear the local records or redirect the user
} else {
alert("Submission failed: " + await res.text());
}
} catch (err) {
console.error("Submission error:", err);
alert("An error occurred during submission.");

View File

@ -251,7 +251,7 @@
formatTime(timeString) {
if (!timeString) return null;
const [hours, minutes] = timeString.split(':');
return `${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}:00`; // Ensure valid HH:mm:ss format
return `${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}:00`; // HH:mm:ss format
},
async addOvertime() {
if (this.isPSTWAIR && !this.selectedAirStation) {