-
This commit is contained in:
parent
c0c2c59ea3
commit
4248484877
@ -12,9 +12,9 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Controllers
|
|||||||
{
|
{
|
||||||
[Area("OTcalculate")]
|
[Area("OTcalculate")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class HodDashboardController : Controller
|
public class HouDashboardController : Controller
|
||||||
{
|
{
|
||||||
public IActionResult HodApproval()
|
public IActionResult HouApproval()
|
||||||
{
|
{
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|||||||
26
Areas/OTcalculate/Controllers/HouDashboardController.cs
Normal file
26
Areas/OTcalculate/Controllers/HouDashboardController.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using PSTW_CentralSystem.Areas.OTcalculate.Models;
|
||||||
|
using PSTW_CentralSystem.DBContext;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace PSTW_CentralSystem.Areas.OTcalculate.Controllers
|
||||||
|
{
|
||||||
|
[Area("OTcalculate")]
|
||||||
|
[Authorize]
|
||||||
|
public class HodDashboardController : Controller
|
||||||
|
{
|
||||||
|
public IActionResult HodApproval()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
public IActionResult OtReview()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Controllers
|
|||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult OtSTatus()
|
public IActionResult OtStatus()
|
||||||
{
|
{
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,6 +62,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
|||||||
{
|
{
|
||||||
table.ColumnsDefinition(columns =>
|
table.ColumnsDefinition(columns =>
|
||||||
{
|
{
|
||||||
|
columns.RelativeColumn(0.7f); // Days
|
||||||
columns.RelativeColumn(1.1f); // Date
|
columns.RelativeColumn(1.1f); // Date
|
||||||
columns.RelativeColumn(0.8f); // Office From
|
columns.RelativeColumn(0.8f); // Office From
|
||||||
columns.RelativeColumn(0.8f); // Office To
|
columns.RelativeColumn(0.8f); // Office To
|
||||||
@ -74,13 +75,13 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
|||||||
columns.RelativeColumn(); // Net OT
|
columns.RelativeColumn(); // Net OT
|
||||||
if (departmentId == 2)
|
if (departmentId == 2)
|
||||||
columns.RelativeColumn(); // Station
|
columns.RelativeColumn(); // Station
|
||||||
columns.RelativeColumn(0.9f); // Day Type
|
|
||||||
columns.RelativeColumn(2.7f); // Description
|
columns.RelativeColumn(2.7f); // Description
|
||||||
});
|
});
|
||||||
|
|
||||||
table.Header(header =>
|
table.Header(header =>
|
||||||
{
|
{
|
||||||
// Row 1 — grouped headers
|
// Row 1 — grouped headers
|
||||||
|
header.Cell().RowSpan(2).Background("#e0f7da").Border(0.25f).Padding(5).Text("Days").FontSize(9).Bold().AlignCenter();
|
||||||
header.Cell().RowSpan(2).Background("#d0ead2").Border(0.25f).Padding(5).Text("Date").FontSize(9).Bold().AlignCenter();
|
header.Cell().RowSpan(2).Background("#d0ead2").Border(0.25f).Padding(5).Text("Date").FontSize(9).Bold().AlignCenter();
|
||||||
|
|
||||||
header.Cell().ColumnSpan(3).Background("#dceefb").Border(0.25f).Padding(5).Text("Office Hours\n(8:30 - 17:30)").FontSize(9).Bold().AlignCenter();
|
header.Cell().ColumnSpan(3).Background("#dceefb").Border(0.25f).Padding(5).Text("Office Hours\n(8:30 - 17:30)").FontSize(9).Bold().AlignCenter();
|
||||||
@ -93,17 +94,17 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
|||||||
if (departmentId == 2)
|
if (departmentId == 2)
|
||||||
header.Cell().RowSpan(2).Background("#d0f0ef").Border(0.25f).Padding(5).Text("Station").FontSize(9).Bold().AlignCenter();
|
header.Cell().RowSpan(2).Background("#d0f0ef").Border(0.25f).Padding(5).Text("Station").FontSize(9).Bold().AlignCenter();
|
||||||
|
|
||||||
header.Cell().RowSpan(2).Background("#e0f7da").Border(0.25f).Padding(5).Text("Days").FontSize(9).Bold().AlignCenter();
|
|
||||||
header.Cell().RowSpan(2).Background("#e3f2fd").Border(0.25f).Padding(5).Text("Description").FontSize(9).Bold().AlignCenter();
|
header.Cell().RowSpan(2).Background("#e3f2fd").Border(0.25f).Padding(5).Text("Description").FontSize(9).Bold().AlignCenter();
|
||||||
|
|
||||||
|
|
||||||
// Row 2 — subheaders only for grouped columns
|
// Row 2 — subheaders only for grouped columns
|
||||||
header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("From").FontSize(9).Bold().AlignCenter();
|
header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("From").FontSize(9).Bold().AlignCenter();
|
||||||
header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("To").FontSize(9).Bold().AlignCenter();
|
header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("To").FontSize(9).Bold().AlignCenter();
|
||||||
header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("Break").FontSize(9).Bold().AlignCenter();
|
header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("Break (min)").FontSize(9).Bold().AlignCenter();
|
||||||
|
|
||||||
header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("From").FontSize(9).Bold().AlignCenter();
|
header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("From").FontSize(9).Bold().AlignCenter();
|
||||||
header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("To").FontSize(9).Bold().AlignCenter();
|
header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("To").FontSize(9).Bold().AlignCenter();
|
||||||
header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("Break").FontSize(9).Bold().AlignCenter();
|
header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("Break (min)").FontSize(9).Bold().AlignCenter();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -128,7 +129,13 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var r in records)
|
var groupedRecords = records.GroupBy(r => r.OtDate.Date);
|
||||||
|
|
||||||
|
foreach (var group in groupedRecords)
|
||||||
|
{
|
||||||
|
bool isFirstRow = true;
|
||||||
|
|
||||||
|
foreach (var r in group)
|
||||||
{
|
{
|
||||||
var totalOT = CalculateTotalOT(r);
|
var totalOT = CalculateTotalOT(r);
|
||||||
var totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0);
|
var totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0);
|
||||||
@ -150,21 +157,26 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
|||||||
text.AlignCenter();
|
text.AlignCenter();
|
||||||
}
|
}
|
||||||
|
|
||||||
AddCell(r.OtDate.ToString("dd/MM/yyyy"));
|
AddCell(isFirstRow ? $"{r.OtDate:ddd}" : "");
|
||||||
|
AddCell(isFirstRow ? r.OtDate.ToString("dd/MM/yyyy") : "");
|
||||||
|
|
||||||
AddCell(FormatTime(r.OfficeFrom));
|
AddCell(FormatTime(r.OfficeFrom));
|
||||||
AddCell(FormatTime(r.OfficeTo));
|
AddCell(FormatTime(r.OfficeTo));
|
||||||
AddCell($"{r.OfficeBreak ?? 0} min");
|
AddCell($"{r.OfficeBreak ?? 0}");
|
||||||
AddCell(FormatTime(r.AfterFrom));
|
AddCell(FormatTime(r.AfterFrom));
|
||||||
AddCell(FormatTime(r.AfterTo));
|
AddCell(FormatTime(r.AfterTo));
|
||||||
AddCell($"{r.AfterBreak ?? 0} min");
|
AddCell($"{r.AfterBreak ?? 0}");
|
||||||
AddCell($"{(int)totalOT.TotalHours} hr {totalOT.Minutes} min");
|
AddCell($"{(int)totalOT.TotalHours} hr {totalOT.Minutes} min");
|
||||||
AddCell($"{totalBreak}");
|
AddCell($"{totalBreak}");
|
||||||
AddCell($"{netOT.Hours} hr {netOT.Minutes} min");
|
AddCell($"{netOT.Hours} hr {netOT.Minutes} min");
|
||||||
if (departmentId == 2)
|
if (departmentId == 2)
|
||||||
AddCell(r.Stations?.StationName ?? "N/A");
|
AddCell(r.Stations?.StationName ?? "N/A");
|
||||||
AddCell(r.OtDays);
|
|
||||||
table.Cell().Background(rowBg).Border(0.25f).Padding(5).Text(r.OtDescription ?? "-").FontSize(9).WrapAnywhere().LineHeight(1.2f);
|
table.Cell().Background(rowBg).Border(0.25f).Padding(5).Text(r.OtDescription ?? "-").FontSize(9).WrapAnywhere().LineHeight(1.2f);
|
||||||
|
|
||||||
|
isFirstRow = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var totalOTTimeSpan = TimeSpan.FromHours(totalOTSum);
|
var totalOTTimeSpan = TimeSpan.FromHours(totalOTSum);
|
||||||
var totalBreakTimeSpan = TimeSpan.FromMinutes(totalBreakSum);
|
var totalBreakTimeSpan = TimeSpan.FromMinutes(totalBreakSum);
|
||||||
|
|||||||
5
Areas/OTcalculate/Views/HouDashboard/HouApproval.cshtml
Normal file
5
Areas/OTcalculate/Views/HouDashboard/HouApproval.cshtml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Overtime Approval";
|
||||||
|
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||||
|
}
|
||||||
|
|
||||||
169
Areas/OTcalculate/Views/HouDashboard/OtReview.cshtml
Normal file
169
Areas/OTcalculate/Views/HouDashboard/OtReview.cshtml
Normal 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.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>
|
||||||
|
<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.afterFrom, 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` : '-';
|
||||||
|
},
|
||||||
|
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>
|
||||||
|
}
|
||||||
@ -30,16 +30,10 @@
|
|||||||
background-color: #fce5cd !important;
|
background-color: #fce5cd !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.form-control {
|
input.form-control,
|
||||||
border-radius: 10px;
|
select.form-control,
|
||||||
}
|
|
||||||
|
|
||||||
select.form-control {
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
border-radius: 10px !important;
|
border-radius: 10px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,35 +46,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
td.wrap-text {
|
td.wrap-text {
|
||||||
white-space: pre-wrap; /* Keep line breaks and wrap text */
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word; /* Break long words if necessary */
|
word-wrap: break-word;
|
||||||
max-width: 300px; /* Adjust as needed */
|
max-width: 300px;
|
||||||
text-align: left; /* Optional: left-align description */
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description-preview {
|
.description-preview {
|
||||||
max-height: 3.6em; /* approx. 2 lines */
|
max-height: 3.6em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: max-height 0.3s ease;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
transition: max-height 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.description-preview.expanded {
|
.description-preview.expanded {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<div id="app" style="max-width: 1300px; margin: auto; font-size: 13px;">
|
<div id="app" style="max-width: 1300px; margin: auto; 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>
|
||||||
<select class="form-control form-control-sm" v-model="selectedMonth">
|
<select class="form-control form-control-sm" v-model="selectedMonth">
|
||||||
<option v-for="(m, index) in months" :value="index + 1">{{ m }}</option>
|
<option v-for="(m, i) in months" :value="i + 1">{{ m }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@ -92,19 +83,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="print-section" class="table-container table-responsive">
|
<div id="print-section" class="table-container table-responsive">
|
||||||
<table class="table table-bordered table-sm table-striped text-center align-middle">
|
<table class="table table-bordered table-sm table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="header-green" rowspan="2">Date</th>
|
<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">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-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">Total OT Hours</th>
|
||||||
<th class="header-orange" rowspan="2">Break Hours (min)</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">Net OT Hours</th>
|
||||||
<th class="header-orange" rowspan="2" v-if="isPSTWAIR">Station</th>
|
<th class="header-orange" rowspan="2" v-if="isPSTWAIR">Station</th>
|
||||||
<th class="header-green" rowspan="2">Days</th>
|
<th class="header-green" rowspan="2">Days</th>
|
||||||
<th class="header-blue" rowspan="2">Description</th>
|
<th class="header-blue" rowspan="2">Description</th>
|
||||||
<th class="header-green" rowspan="2" v-if="!isAlreadySubmitted">Action</th>
|
<th class="header-green" rowspan="2">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="header-blue">From</th>
|
<th class="header-blue">From</th>
|
||||||
@ -128,24 +119,23 @@
|
|||||||
<td>{{ calcBreakTotal(record) }}</td>
|
<td>{{ calcBreakTotal(record) }}</td>
|
||||||
<td>{{ formatHourMinute(calcNetHours(record)) }}</td>
|
<td>{{ formatHourMinute(calcNetHours(record)) }}</td>
|
||||||
<td v-if="isPSTWAIR">{{ record.stationName || 'N/A' }}</td>
|
<td v-if="isPSTWAIR">{{ record.stationName || 'N/A' }}</td>
|
||||||
<td>{{ record.otDays}}</td>
|
<td>{{ record.otDays }}</td>
|
||||||
<td class="wrap-text">
|
<td class="wrap-text">
|
||||||
<div class="description-preview" v-on:click ="toggleDescription(index)" :class="{ expanded: expandedDescriptions[index] }">
|
<div class="description-preview" v-on:click ="toggleDescription(index)" :class="{ expanded: expandedDescriptions[index] }">
|
||||||
{{ record.otDescription }}
|
{{ record.otDescription }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="!isAlreadySubmitted">
|
<td>
|
||||||
<button class="btn btn-light border rounded-circle me-1" title="Edit Record" v-on:click="editRecord(index)">
|
<button class="btn btn-light border rounded-circle me-1" title="Edit" v-on:click ="editRecord(index)">
|
||||||
<i class="bi bi-pencil-fill text-warning fs-5"></i>
|
<i class="bi bi-pencil-fill text-warning fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-light border rounded-circle" title="Delete Record" v-on:click="deleteRecord(index)">
|
<button class="btn btn-light border rounded-circle" title="Delete" v-on:click="deleteRecord(index)">
|
||||||
<i class="bi bi-trash-fill text-danger fs-5"></i>
|
<i class="bi bi-trash-fill text-danger fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="filteredRecords.length === 0">
|
<tr v-if="filteredRecords.length === 0">
|
||||||
<td :colspan="isPSTWAIR ? 14 : 13">No records found for selected month and year.</td>
|
<td :colspan="isPSTWAIR ? 14 : 13">No records found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="table-primary fw-bold">
|
<tr class="table-primary fw-bold">
|
||||||
<td>TOTAL</td>
|
<td>TOTAL</td>
|
||||||
@ -161,49 +151,22 @@
|
|||||||
</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">
|
<button class="btn btn-primary btn-sm" v-on:click="printPdf"><i class="bi bi-printer"></i> Print</button>
|
||||||
<i class="bi bi-printer"></i> Print
|
<button class="btn btn-dark btn-sm" v-on:click="downloadPdf"><i class="bi bi-download"></i> Save</button>
|
||||||
</button>
|
<button class="btn btn-success btn-sm" v-on:click=""><i class="bi bi-send"></i> Submit
|
||||||
<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="openSubmitModal"
|
|
||||||
:disabled="isSubmitting || isAlreadySubmitted">
|
|
||||||
<i class="bi bi-send"></i> {{ isAlreadySubmitted ? 'Submitted' : 'Submit' }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit Modal -->
|
|
||||||
<div v-if="showSubmitModal" class="modal show d-block" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content p-3">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Submit OT Records</h5>
|
|
||||||
<button type="button" class="btn-close" v-on:click="showSubmitModal = false"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<input type="file" class="form-control" v-on:change="handleFileChange" accept=".pdf" />
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-success" v-on:click="submitToHod">Submit</button>
|
|
||||||
<button class="btn btn-secondary" v-on:click="showSubmitModal = false">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
|
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const app = Vue.createApp({
|
const app = Vue.createApp({
|
||||||
data() {
|
data() {
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
const currentMonth = new Date().getMonth() + 1;
|
||||||
return {
|
return {
|
||||||
otRecords: [],
|
otRecords: [],
|
||||||
userId: null,
|
userId: null,
|
||||||
@ -213,9 +176,6 @@
|
|||||||
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
||||||
years: Array.from({ length: 10 }, (_, i) => currentYear - 5 + i),
|
years: Array.from({ length: 10 }, (_, i) => currentYear - 5 + i),
|
||||||
expandedDescriptions: {},
|
expandedDescriptions: {},
|
||||||
showSubmitModal: false,
|
|
||||||
submitFile: null,
|
|
||||||
submittedStatus: {},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -244,10 +204,6 @@
|
|||||||
minutes: Math.round(totalMinutes % 60)
|
minutes: Math.round(totalMinutes % 60)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
isAlreadySubmitted() {
|
|
||||||
const key = `${this.selectedYear}-${String(this.selectedMonth).padStart(2, '0')}`;
|
|
||||||
return !!this.submittedStatus[key];
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.initUserAndRecords();
|
await this.initUserAndRecords();
|
||||||
@ -257,19 +213,8 @@
|
|||||||
await this.fetchUser();
|
await this.fetchUser();
|
||||||
if (this.userId) {
|
if (this.userId) {
|
||||||
await this.fetchOtRecords();
|
await this.fetchOtRecords();
|
||||||
await this.fetchSubmissionStatus();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async fetchSubmissionStatus() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/OvertimeAPI/GetSubmissionStatus/${this.userId}`);
|
|
||||||
const data = await res.json();
|
|
||||||
this.submittedStatus = data; // expect format like { '2025-04': true }
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Submission status fetch error:", err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchUser() {
|
async fetchUser() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/IdentityAPI/GetUserInformation', { method: 'POST' });
|
const res = await fetch('/IdentityAPI/GetUserInformation', { method: 'POST' });
|
||||||
@ -347,8 +292,8 @@
|
|||||||
},
|
},
|
||||||
printPdf() {
|
printPdf() {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const month = today.getMonth() + 1;
|
const month = this.selectedMonth;
|
||||||
const year = today.getFullYear();
|
const year = this.selectedYear;
|
||||||
|
|
||||||
fetch(`/OvertimeAPI/GenerateOvertimePdf?month=${month}&year=${year}`)
|
fetch(`/OvertimeAPI/GenerateOvertimePdf?month=${month}&year=${year}`)
|
||||||
.then(response => response.blob())
|
.then(response => response.blob())
|
||||||
@ -385,55 +330,6 @@
|
|||||||
alert("An error occurred while generating the PDF.");
|
alert("An error occurred while generating the PDF.");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openSubmitModal() {
|
|
||||||
this.showSubmitModal = true;
|
|
||||||
},
|
|
||||||
handleFileChange(event) {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (file && file.type !== 'application/pdf') {
|
|
||||||
alert("Only PDF files are allowed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.submitFile = file;
|
|
||||||
},
|
|
||||||
|
|
||||||
async submitToHod() {
|
|
||||||
this.isSubmitting = true;
|
|
||||||
try {
|
|
||||||
if (!this.submitFile) {
|
|
||||||
alert("Please upload a PDF file.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", this.submitFile);
|
|
||||||
|
|
||||||
// Add month & year selection logic if needed
|
|
||||||
formData.append("month", new Date().getMonth() + 1);
|
|
||||||
formData.append("year", new Date().getFullYear());
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/OvertimeAPI/SubmitOvertimeRecords", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error("Submission failed");
|
|
||||||
alert("Submission successful!");
|
|
||||||
this.showSubmitModal = false;
|
|
||||||
|
|
||||||
const key = `${this.selectedYear}-${String(this.selectedMonth).padStart(2, '0')}`;
|
|
||||||
this.submittedStatus[key] = true;
|
|
||||||
this.showSubmitModal = false;
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
alert("Error: " + err.message);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.isSubmitting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
|||||||
@ -304,7 +304,7 @@
|
|||||||
afterFrom: this.afterFrom ? this.formatTime(this.afterFrom) : null, // Set to null if empty
|
afterFrom: this.afterFrom ? this.formatTime(this.afterFrom) : null, // Set to null if empty
|
||||||
afterTo: this.afterTo ? this.formatTime(this.afterTo) : null, // Set to null if empty
|
afterTo: this.afterTo ? this.formatTime(this.afterTo) : null, // Set to null if empty
|
||||||
afterBreak: this.afterBreak || null, // Make this optional
|
afterBreak: this.afterBreak || null, // Make this optional
|
||||||
stationId: this.isPSTWAIR ? parseInt(this.selectedAirStation) : "",
|
stationId: this.isPSTWAIR ? parseInt(this.selectedAirStation) : null,
|
||||||
otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '),
|
otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '),
|
||||||
otDays: this.selectedDayType,
|
otDays: this.selectedDayType,
|
||||||
userId: this.userId
|
userId: this.userId
|
||||||
|
|||||||
@ -2,122 +2,3 @@
|
|||||||
ViewData["Title"] = "Overtime Status";
|
ViewData["Title"] = "Overtime Status";
|
||||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||||
}
|
}
|
||||||
|
|
||||||
<style>
|
|
||||||
.hodstatus, .hrstatus {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-mask {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0,0,0,0.6);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container {
|
|
||||||
background: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div id="otStatusApp" class="container mt-4">
|
|
||||||
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Month</th>
|
|
||||||
<th>Year</th>
|
|
||||||
<th>Submitted On</th>
|
|
||||||
<th>HOD Status</th>
|
|
||||||
<th>HR Status</th>
|
|
||||||
<th>Details</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="status in statusList" :key="status.statusId">
|
|
||||||
<td>{{ getMonthName(status.month) }}</td>
|
|
||||||
<td>{{ status.year }}</td>
|
|
||||||
<td>{{ formatDate(status.submitDate) }}</td>
|
|
||||||
<td :class="status.hodStatus.toLowerCase()">{{ status.hodStatus }}</td>
|
|
||||||
<td :class="status.hrStatus.toLowerCase()">{{ status.hrStatus }}</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-primary" v-on:click ="viewUpdates(status)">View Updates</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- Modal -->
|
|
||||||
<div v-if="selectedStatus" class="modal-mask">
|
|
||||||
<div class="modal-container">
|
|
||||||
<h5>Status History</h5>
|
|
||||||
<p><strong>HOD Updates:</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li v-for="update in parseJson(selectedStatus.hodUpdate)">
|
|
||||||
{{ formatUpdate(update) }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>HR Updates:</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li v-for="update in parseJson(selectedStatus.hrUpdate)">
|
|
||||||
{{ formatUpdate(update) }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<button class="btn btn-secondary" v-on:click ="selectedStatus = null">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
|
||||||
<script>
|
|
||||||
const { createApp } = Vue;
|
|
||||||
|
|
||||||
createApp({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
statusList: [],
|
|
||||||
selectedStatus: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
fetch('/OvertimeAPI/GetUserOtStatus')
|
|
||||||
.then(res => {
|
|
||||||
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.then(data => this.statusList = data)
|
|
||||||
.catch(err => console.error("Fetch error:", err));
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
formatDate(dateStr) {
|
|
||||||
return new Date(dateStr).toLocaleDateString();
|
|
||||||
},
|
|
||||||
getMonthName(month) {
|
|
||||||
return new Date(2000, month - 1, 1).toLocaleString('default', { month: 'long' });
|
|
||||||
},
|
|
||||||
parseJson(jsonStr) {
|
|
||||||
try {
|
|
||||||
return jsonStr ? JSON.parse(jsonStr) : [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formatUpdate(update) {
|
|
||||||
return `${update.timestamp} - ${update.updatedBy} changed ${update.field} from '${update.oldValue}' to '${update.newValue}'`;
|
|
||||||
},
|
|
||||||
viewUpdates(status) {
|
|
||||||
this.selectedStatus = status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).mount('#otStatusApp');
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
|
|||||||
@ -454,6 +454,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Ot Records
|
#region Ot Records
|
||||||
|
|
||||||
[HttpGet("GetUserOvertimeRecords/{userId}")]
|
[HttpGet("GetUserOvertimeRecords/{userId}")]
|
||||||
public IActionResult GetUserOvertimeRecords(int userId)
|
public IActionResult GetUserOvertimeRecords(int userId)
|
||||||
{
|
{
|
||||||
@ -461,6 +462,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
{
|
{
|
||||||
var records = _centralDbContext.Otregisters
|
var records = _centralDbContext.Otregisters
|
||||||
.Where(o => o.UserId == userId)
|
.Where(o => o.UserId == userId)
|
||||||
|
.OrderByDescending(o => o.OtDate)
|
||||||
.Select(o => new
|
.Select(o => new
|
||||||
{
|
{
|
||||||
o.OvertimeId,
|
o.OvertimeId,
|
||||||
@ -477,7 +479,6 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
o.OtDays,
|
o.OtDays,
|
||||||
o.UserId
|
o.UserId
|
||||||
})
|
})
|
||||||
.OrderByDescending(o => o.OtDate)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return Ok(records);
|
return Ok(records);
|
||||||
@ -501,7 +502,6 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
return NotFound("Overtime record not found.");
|
return NotFound("Overtime record not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the record from the database (No file handling anymore)
|
|
||||||
_centralDbContext.Otregisters.Remove(record);
|
_centralDbContext.Otregisters.Remove(record);
|
||||||
_centralDbContext.SaveChanges();
|
_centralDbContext.SaveChanges();
|
||||||
|
|
||||||
@ -518,22 +518,18 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
[HttpGet("GenerateOvertimePdf")]
|
[HttpGet("GenerateOvertimePdf")]
|
||||||
public IActionResult GenerateOvertimePdf(int month, int year)
|
public IActionResult GenerateOvertimePdf(int month, int year)
|
||||||
{
|
{
|
||||||
var userIdString = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId))
|
||||||
if (string.IsNullOrEmpty(userIdString) || !int.TryParse(userIdString, out int userId))
|
|
||||||
{
|
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
}
|
|
||||||
|
|
||||||
// Get user and department info
|
|
||||||
var user = _centralDbContext.Users
|
var user = _centralDbContext.Users
|
||||||
.Include(u => u.Department)
|
.Include(u => u.Department)
|
||||||
.FirstOrDefault(u => u.Id == userId);
|
.FirstOrDefault(u => u.Id == userId);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
return NotFound("User not found");
|
return NotFound("User not found.");
|
||||||
|
|
||||||
var userFullName = $"{user.FullName}";
|
var fullName = user.FullName;
|
||||||
var departmentId = user.departmentId ?? 0;
|
var departmentId = user.departmentId ?? 0;
|
||||||
var departmentName = user.Department?.DepartmentName ?? "N/A";
|
var departmentName = user.Department?.DepartmentName ?? "N/A";
|
||||||
|
|
||||||
@ -542,101 +538,53 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
.Where(o => o.UserId == userId && o.OtDate.Month == month && o.OtDate.Year == year)
|
.Where(o => o.UserId == userId && o.OtDate.Month == month && o.OtDate.Year == year)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
// Step 1: Generate all days of the month
|
||||||
|
var daysInMonth = DateTime.DaysInMonth(year, month);
|
||||||
|
var allDays = Enumerable.Range(1, daysInMonth)
|
||||||
|
.Select(day => new DateTime(year, month, day))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Step 2: Merge records with missing days
|
||||||
|
var mergedRecords = new List<OtRegisterModel>();
|
||||||
|
|
||||||
|
foreach (var date in allDays)
|
||||||
|
{
|
||||||
|
var dayRecords = records
|
||||||
|
.Where(r => r.OtDate.Date == date.Date)
|
||||||
|
.OrderBy(r => r.OfficeFrom)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (dayRecords.Any())
|
||||||
|
{
|
||||||
|
mergedRecords.AddRange(dayRecords);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mergedRecords.Add(new OtRegisterModel
|
||||||
|
{
|
||||||
|
OtDate = date,
|
||||||
|
OtDays = date.DayOfWeek.ToString(),
|
||||||
|
OtDescription = "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
byte[]? logoImage = null;
|
byte[]? logoImage = null;
|
||||||
var logoPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images", "logo.jpg");
|
var logoPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images", "logo.jpg");
|
||||||
|
|
||||||
if (System.IO.File.Exists(logoPath))
|
if (System.IO.File.Exists(logoPath))
|
||||||
{
|
|
||||||
logoImage = System.IO.File.ReadAllBytes(logoPath);
|
logoImage = System.IO.File.ReadAllBytes(logoPath);
|
||||||
}
|
|
||||||
|
|
||||||
var stream = _pdfService.GenerateOvertimeTablePdf(records, departmentId, userFullName, departmentName, logoImage);
|
var stream = _pdfService.GenerateOvertimeTablePdf(mergedRecords, departmentId, fullName, departmentName, logoImage);
|
||||||
|
|
||||||
return File(stream, "application/pdf", $"OvertimeRecords_{year}_{month}.pdf");
|
return File(stream, "application/pdf", $"OvertimeRecords_{year}_{month}.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("SubmitOvertimeRecords")]
|
|
||||||
public async Task<IActionResult> SubmitOvertimeRecords([FromForm] IFormFile file, [FromForm] int month, [FromForm] int year)
|
|
||||||
{
|
|
||||||
var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
||||||
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId))
|
|
||||||
return Unauthorized();
|
|
||||||
|
|
||||||
if (file == null || file.Length == 0)
|
|
||||||
return BadRequest("No file uploaded.");
|
|
||||||
|
|
||||||
var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Media", "Overtime");
|
|
||||||
|
|
||||||
if (!Directory.Exists(uploadsFolder))
|
|
||||||
Directory.CreateDirectory(uploadsFolder);
|
|
||||||
|
|
||||||
var fileName = $"OT_{userId}_{year}_{month}_{DateTime.Now.Ticks}.pdf";
|
|
||||||
var filePath = Path.Combine(uploadsFolder, fileName);
|
|
||||||
|
|
||||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
|
||||||
{
|
|
||||||
await file.CopyToAsync(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
var statusRecord = new OtStatusModel
|
|
||||||
{
|
|
||||||
UserId = userId,
|
|
||||||
Month = month,
|
|
||||||
Year = year,
|
|
||||||
SubmitDate = DateTime.Now,
|
|
||||||
HodStatus = "Pending",
|
|
||||||
HrStatus = "Pending",
|
|
||||||
FilePath = $"/Media/Overtime/{fileName}"
|
|
||||||
};
|
|
||||||
|
|
||||||
_centralDbContext.OtStatus.Add(statusRecord);
|
|
||||||
await _centralDbContext.SaveChangesAsync();
|
|
||||||
|
|
||||||
return Ok(new { message = "Overtime records submitted successfully." });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("CheckSubmissionStatus")]
|
|
||||||
public IActionResult CheckSubmissionStatus(int month, int year)
|
|
||||||
{
|
|
||||||
var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
||||||
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId))
|
|
||||||
return Unauthorized();
|
|
||||||
|
|
||||||
var isSubmitted = _centralDbContext.OtStatus
|
|
||||||
.Any(s => s.UserId == userId && s.Month == month && s.Year == year);
|
|
||||||
|
|
||||||
return Ok(new { isSubmitted });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("GetSubmissionStatus/{userId}")]
|
|
||||||
public IActionResult GetSubmissionStatus(int userId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var statuses = _centralDbContext.OtStatus
|
|
||||||
.Where(s => s.UserId == userId)
|
|
||||||
.OrderByDescending(s => s.SubmitDate)
|
|
||||||
.Select(s => new
|
|
||||||
{
|
|
||||||
s.UserId,
|
|
||||||
s.Month,
|
|
||||||
s.Year,
|
|
||||||
s.HodStatus,
|
|
||||||
s.HrStatus,
|
|
||||||
s.SubmitDate
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return Ok(statuses);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to fetch submission statuses.");
|
|
||||||
return StatusCode(500, "Error retrieving submission statuses.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region Ot Edit
|
#region Ot Edit
|
||||||
[HttpGet("GetOvertimeRecordById/{id}")]
|
[HttpGet("GetOvertimeRecordById/{id}")]
|
||||||
public async Task<IActionResult> GetOvertimeRecordById(int id)
|
public async Task<IActionResult> GetOvertimeRecordById(int id)
|
||||||
@ -697,20 +645,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region OtStatus
|
#region OtStatus
|
||||||
[HttpGet("GetUserOtStatus")]
|
|
||||||
public IActionResult GetUserOtStatus()
|
|
||||||
{
|
|
||||||
var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
||||||
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId))
|
|
||||||
return Unauthorized();
|
|
||||||
|
|
||||||
var records = _centralDbContext.OtStatus
|
|
||||||
.Where(s => s.UserId == userId)
|
|
||||||
.OrderByDescending(s => s.Year).ThenByDescending(s => s.Month)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return Ok(records);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|||||||
@ -561,6 +561,20 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<a class="sidebar-link has-arrow waves-effect waves-dark"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
aria-expanded="false">
|
||||||
|
<i class="mdi mdi-receipt"></i><span class="hide-menu">HoU Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<ul aria-expanded="false" class="collapse first-level">
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<a class="sidebar-link waves-effect waves-dark sidebar-link" asp-area="OTcalculate" asp-controller="HouDashboard" asp-action="HouApproval" aria-expanded="false">
|
||||||
|
<i class="mdi mdi-view-dashboard"></i><span class="hide-menu">OT Approval</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<!-- <li class="sidebar-item">
|
<!-- <li class="sidebar-item">
|
||||||
<a class="sidebar-link waves-effect waves-dark sidebar-link"
|
<a class="sidebar-link waves-effect waves-dark sidebar-link"
|
||||||
href="charts.html"
|
href="charts.html"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user