This commit is contained in:
Naz 2025-05-13 17:13:30 +08:00
parent 6d7dc52724
commit 19a9ade3eb
38 changed files with 1956 additions and 663 deletions

View File

@ -12,15 +12,17 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Controllers
{
[Area("OTcalculate")]
[Authorize]
public class HouDashboardController : Controller
public class ApprovalDashboardController : Controller
{
public IActionResult HouApproval()
public IActionResult Approval()
{
return View();
}
public IActionResult OtReview()
public IActionResult OtReview(int statusId)
{
ViewBag.StatusId = statusId; // If needed in the view
return View();
}
}
}

View File

@ -1,26 +0,0 @@
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();
}
}
}

View File

@ -0,0 +1,32 @@
using PSTW_CentralSystem.Models;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PSTW_CentralSystem.Areas.OTcalculate.Models
{
public class ApprovalFlowModel
{
[Key]
public int ApprovalFlowId { get; set; }
public string ApprovalName { get; set; }
public int? HoU { get; set; }
[ForeignKey("HoU")]
public virtual UserModel? HeadOfUnit { get; set; }
public int? HoD { get; set; }
[ForeignKey("HoD")]
public virtual UserModel? HeadOfDepartment { get; set; }
public int? Manager { get; set; }
[ForeignKey("Manager")]
public virtual UserModel? ManagerUser { get; set; }
public int? HR { get; set; }
[ForeignKey("HR")]
public virtual UserModel? HRUser { get; set; }
}
}

View File

@ -26,5 +26,11 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
[ForeignKey("StateId")]
public StateModel? State { get; set; }
public int? ApprovalFlowId { get; set; }
[ForeignKey("ApprovalFlowId")]
public ApprovalFlowModel? Approvalflow { get; set; }
public DateTime? ApprovalUpdate { get; set; }
}
}

View File

@ -31,6 +31,11 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
[Required]
public int UserId { get; set; }
public int? StatusId { get; set; }
[ForeignKey("StatusId")]
public virtual OtStatusModel? Otstatus { get; set; }
// Convert string times to TimeSpan before saving
public TimeSpan? GetOfficeFrom() => ParseTimeSpan(OfficeFrom?.ToString());
public TimeSpan? GetOfficeTo() => ParseTimeSpan(OfficeTo?.ToString());

View File

@ -8,29 +8,46 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
{
[Key]
public int StatusId { get; set; }
[Required]
public int UserId { get; set; }
[Required]
public int Month { get; set; }
[Required]
public int Year { get; set; }
public DateTime SubmitDate { get; set; }
public string HodStatus { get; set; } = "Pending";
public string? HouStatus { get; set; }
public string? HodStatus { get; set; }
public string? ManagerStatus { get; set; }
public string? HrStatus { get; set; }
// JSON array of ApprovalUpdateLog
public string? HouUpdate { get; set; }
public string? HodUpdate { get; set; }
public string HrStatus { get; set; } = "Pending";
// JSON array of ApprovalUpdateLog
public string? ManagerUpdate { get; set; }
public string? HrUpdate { get; set; }
public DateTime? HouSubmitDate { get; set; }
public DateTime? HodSubmitDate { get; set; }
public DateTime? ManagerSubmitDate { get; set; }
public DateTime? HrSubmitDate { get; set; }
public string? FilePath { get; set; }
public bool Updated =>
!string.IsNullOrEmpty(HouUpdate) ||
!string.IsNullOrEmpty(HodUpdate) ||
!string.IsNullOrEmpty(ManagerUpdate) ||
!string.IsNullOrEmpty(HrUpdate);
}
public class StatusDto
{
public int StatusId { get; set; }
}
public class UpdateStatusDto
{
public int StatusId { get; set; }
public string Decision { get; set; }
}
}

View File

@ -13,6 +13,7 @@
public string OtDescription { get; set; }
public string OtDays { get; set; }
public int UserId { get; set; }
public int? StatusId { get; set; }
}

View File

@ -5,6 +5,7 @@ using PSTW_CentralSystem.Areas.OTcalculate.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using ClosedXML.Excel.Drawings;
namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{
@ -19,79 +20,133 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
int weekendId,
List<CalendarModel> publicHolidays,
bool isAdminUser = false,
byte[]? logoImage = null // This parameter is missing in the call
byte[]? logoImage = null
)
{
var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Overtime Records");
int currentRow = 1;
int logoBottomRow = 3;
if (logoImage != null)
{
using (var ms = new MemoryStream(logoImage))
{
var picture = worksheet.AddPicture(ms)
.MoveTo(worksheet.Cell(currentRow, 1))
.WithPlacement(XLPicturePlacement.FreeFloating);
picture.Name = "Company Logo";
picture.Scale(0.3);
}
}
currentRow = logoBottomRow + 1;
int mergeCols = 10; // or set to 'col' if you want to merge all active columns
// Add Header Information
if (!string.IsNullOrEmpty(userFullName))
{
worksheet.Cell(currentRow, 1).Value = $"Name: {userFullName}";
var nameCell = worksheet.Range(currentRow, 1, currentRow, mergeCols).Merge();
nameCell.Value = $"Name: {userFullName}";
nameCell.Style.Font.Bold = true;
nameCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left;
nameCell.Style.Alignment.WrapText = true;
currentRow++;
}
if (!string.IsNullOrEmpty(departmentName))
{
worksheet.Cell(currentRow, 1).Value = $"Department: {departmentName}";
var deptCell = worksheet.Range(currentRow, 1, currentRow, mergeCols).Merge();
deptCell.Value = $"Department: {departmentName}";
deptCell.Style.Font.Bold = true;
deptCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left;
deptCell.Style.Alignment.WrapText = true;
currentRow++;
}
currentRow++; // Add an empty row after header
// Header titles
var headers = new List<string>
{
"Day", "Date", "Office From", "Office To", "Office Break",
"After From", "After To", "After Break",
"Total OT", "Break Minutes", "Net OT"
};
if (departmentId == 2 || isAdminUser)
headers.Add("Station");
headers.Add("Description");
// Set header row
int headerRow = currentRow;
for (int i = 0; i < headers.Count; i++)
{
var cell = worksheet.Cell(headerRow, i + 1);
cell.Value = headers[i];
cell.Style.Font.Bold = true;
cell.Style.Fill.BackgroundColor = XLColor.LightGray;
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
cell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
cell.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
}
currentRow++;
// Fill data rows
// Header setup
int headerRow1 = currentRow;
int headerRow2 = currentRow + 1;
worksheet.Cell(headerRow1, 1).Value = "Days";
worksheet.Cell(headerRow1, 2).Value = "Date";
worksheet.Range(headerRow1, 1, headerRow2, 1).Merge(); // Days
worksheet.Range(headerRow1, 2, headerRow2, 2).Merge(); // Date
worksheet.Cell(headerRow1, 3).Value = "Office Hours";
worksheet.Range(headerRow1, 3, headerRow1, 5).Merge();
worksheet.Cell(headerRow2, 3).Value = "From";
worksheet.Cell(headerRow2, 4).Value = "To";
worksheet.Cell(headerRow2, 5).Value = "Break (min)";
worksheet.Cell(headerRow1, 6).Value = "After Office Hours";
worksheet.Range(headerRow1, 6, headerRow1, 8).Merge();
worksheet.Cell(headerRow2, 6).Value = "From";
worksheet.Cell(headerRow2, 7).Value = "To";
worksheet.Cell(headerRow2, 8).Value = "Break (min)";
worksheet.Cell(headerRow1, 9).Value = "Total OT Hours";
worksheet.Cell(headerRow1, 10).Value = "Break Hours (min)";
worksheet.Cell(headerRow1, 11).Value = "Net OT Hours";
worksheet.Range(headerRow1, 9, headerRow2, 9).Merge();
worksheet.Range(headerRow1, 10, headerRow2, 10).Merge();
worksheet.Range(headerRow1, 11, headerRow2, 11).Merge();
int col = 12;
if (departmentId == 2 || isAdminUser)
{
worksheet.Cell(headerRow1, col).Value = "Station";
worksheet.Range(headerRow1, col, headerRow2, col).Merge();
col++;
}
worksheet.Cell(headerRow1, col).Value = "Description";
worksheet.Range(headerRow1, col, headerRow2, col).Merge();
// Apply styling after header setup
var headerRange = worksheet.Range(headerRow1, 1, headerRow2, col);
headerRange.Style.Font.Bold = true;
headerRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
headerRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
headerRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
headerRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
// Background colors for header cells
worksheet.Range(headerRow1, 1, headerRow2, 2).Style.Fill.BackgroundColor = XLColor.LightGreen;
worksheet.Range(headerRow1, 3, headerRow2, 5).Style.Fill.BackgroundColor = XLColor.AliceBlue;
worksheet.Range(headerRow1, 6, headerRow2, 8).Style.Fill.BackgroundColor = XLColor.AliceBlue;
worksheet.Range(headerRow1, 9, headerRow2, 11).Style.Fill.BackgroundColor = XLColor.Peach;
if (departmentId == 2 || isAdminUser)
worksheet.Range(headerRow1, 12, headerRow2, 12).Style.Fill.BackgroundColor = XLColor.LightCyan;
worksheet.Cell(headerRow1, col).Style.Fill.BackgroundColor = XLColor.LightBlue;
// Update currentRow after headers
currentRow = headerRow2 + 1;
DateTime? previousDate = null;
foreach (var r in records)
{
TimeSpan totalOT = CalculateTotalOT(r);
int totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0);
TimeSpan netOT = totalOT - TimeSpan.FromMinutes(totalBreak);
int dataCol = 1;
int col = 1;
var dayCell = worksheet.Cell(currentRow, col++);
dayCell.Value = r.OtDate.ToString("ddd");
dayCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
dayCell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
dayCell.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
bool isSameDateAsPrevious = previousDate == r.OtDate.Date;
previousDate = r.OtDate.Date;
var dateCell = worksheet.Cell(currentRow, col++);
dateCell.Value = r.OtDate.ToString("yyyy-MM-dd");
dateCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
dateCell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
dateCell.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
var dayCell = worksheet.Cell(currentRow, dataCol++);
var dateCell = worksheet.Cell(currentRow, dataCol++);
// Apply background color for weekends and public holidays
// Check the type of day first
var dayOfWeek = r.OtDate.DayOfWeek;
bool isWeekend = (weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) ||
(weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday));
bool isPublicHoliday = publicHolidays.Any(h => h.HolidayDate.Date == r.OtDate.Date);
// Apply color regardless of whether the value is shown
if (isPublicHoliday)
{
dayCell.Style.Fill.BackgroundColor = XLColor.Pink;
@ -103,28 +158,50 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
dateCell.Style.Fill.BackgroundColor = XLColor.LightBlue;
}
worksheet.Cell(currentRow, col++).Value = FormatTime(r.OfficeFrom);
worksheet.Cell(currentRow, col++).Value = FormatTime(r.OfficeTo);
worksheet.Cell(currentRow, col++).Value = r.OfficeBreak;
// Show value only if it's not repeated
if (!isSameDateAsPrevious)
{
dayCell.Value = r.OtDate.ToString("ddd");
dateCell.Value = r.OtDate.ToString("yyyy-MM-dd");
}
else
{
dayCell.Value = "";
dateCell.Value = "";
}
worksheet.Cell(currentRow, col++).Value = FormatTime(r.AfterFrom);
worksheet.Cell(currentRow, col++).Value = FormatTime(r.AfterTo);
worksheet.Cell(currentRow, col++).Value = r.AfterBreak;
worksheet.Cell(currentRow, dataCol++).Value = FormatTime(r.OfficeFrom);
worksheet.Cell(currentRow, dataCol++).Value = FormatTime(r.OfficeTo);
worksheet.Cell(currentRow, dataCol++).Value = r.OfficeBreak;
worksheet.Cell(currentRow, col++).Value = totalOT.ToString(@"hh\:mm");
worksheet.Cell(currentRow, col++).Value = totalBreak;
worksheet.Cell(currentRow, col++).Value = netOT.ToString(@"hh\:mm");
worksheet.Cell(currentRow, dataCol++).Value = FormatTime(r.AfterFrom);
worksheet.Cell(currentRow, dataCol++).Value = FormatTime(r.AfterTo);
worksheet.Cell(currentRow, dataCol++).Value = r.AfterBreak;
TimeSpan totalOT = CalculateTotalOT(r);
int totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0);
TimeSpan netOT = totalOT - TimeSpan.FromMinutes(totalBreak);
var totalOTCell = worksheet.Cell(currentRow, dataCol++);
totalOTCell.Value = totalOT;
totalOTCell.Style.NumberFormat.Format = "hh:mm";
worksheet.Cell(currentRow, dataCol++).Value = totalBreak;
var netOTCell = worksheet.Cell(currentRow, dataCol++);
netOTCell.Value = netOT;
netOTCell.Style.NumberFormat.Format = "hh:mm";
if (departmentId == 2 || isAdminUser)
worksheet.Cell(currentRow, col++).Value = r.Stations?.StationName ?? "";
worksheet.Cell(currentRow, dataCol++).Value = r.Stations?.StationName ?? "";
worksheet.Cell(currentRow, col++).Value = r.OtDescription ?? "";
worksheet.Cell(currentRow, dataCol++).Value = r.OtDescription ?? "";
// Apply border and alignment for the rest of the row
for (int i = headers.IndexOf("Office From") + 1; i <= headers.Count; i++)
for (int i = 1; i <= col; i++)
{
var cell = worksheet.Cell(currentRow, i);
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
cell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
cell.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
}
@ -132,18 +209,64 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
currentRow++;
}
// Add Total row
int totalRow = currentRow;
worksheet.Cell(totalRow, 1).Value = "TOTAL";
worksheet.Cell(totalRow, 1).Style.Font.Bold = true;
worksheet.Cell(totalRow, headers.IndexOf("Total OT") + 1).Value = $"=SUM(I{headerRow + 1}:I{totalRow - 1})";
worksheet.Cell(totalRow, headers.IndexOf("Total OT") + 1).Style.Font.Bold = true;
worksheet.Cell(totalRow, headers.IndexOf("Break Minutes") + 1).Value = $"=SUM(J{headerRow + 1}:J{totalRow - 1})";
worksheet.Cell(totalRow, headers.IndexOf("Break Minutes") + 1).Style.Font.Bold = true;
worksheet.Cell(totalRow, headers.IndexOf("Net OT") + 1).Value = $"=SUM(K{headerRow + 1}:K{totalRow - 1})";
worksheet.Cell(totalRow, headers.IndexOf("Net OT") + 1).Style.Font.Bold = true;
if (records.Any())
{
int totalRow = currentRow;
worksheet.Cell(totalRow, 1).Value = "TOTAL";
worksheet.Cell(totalRow, 1).Style.Font.Bold = true;
worksheet.Columns().AdjustToContents();
int colTotalOT = 9; // Column for Total OT
int colBreakMin = 10; // Column for Break Hours (min)
int colNetOT = 11; // Column for Net OT
var totalOTSumCell = worksheet.Cell(totalRow, colTotalOT);
totalOTSumCell.FormulaA1 = $"SUM({GetColumnLetter(colTotalOT)}{headerRow1 + 1}:{GetColumnLetter(colTotalOT)}{totalRow - 1})";
totalOTSumCell.Style.NumberFormat.Format = "hh:mm";
var breakMinTotalCell = worksheet.Cell(totalRow, colBreakMin);
breakMinTotalCell.FormulaA1 = $"SUM({GetColumnLetter(colBreakMin)}{headerRow1 + 1}:{GetColumnLetter(colBreakMin)}{totalRow - 1})/1440";
breakMinTotalCell.Style.NumberFormat.Format = "hh:mm";
var netOTSumCell = worksheet.Cell(totalRow, colNetOT);
netOTSumCell.FormulaA1 = $"SUM({GetColumnLetter(colNetOT)}{headerRow1 + 1}:{GetColumnLetter(colNetOT)}{totalRow - 1})";
netOTSumCell.Style.NumberFormat.Format = "hh:mm";
for (int i = 1; i <= col; i++)
{
var cell = worksheet.Cell(totalRow, i);
cell.Style.Font.Bold = true;
cell.Style.Fill.BackgroundColor = XLColor.Yellow;
cell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
}
}
// Step 1: Enable wrap text on header range
headerRange.Style.Alignment.WrapText = true;
worksheet.Style.Alignment.WrapText = true;
// Step 2: Adjust row height based on wrap text
worksheet.Rows(headerRow1, headerRow2).AdjustToContents();
// Step 3 (optional): Manually set column widths to ensure long headers show correctly
worksheet.Column(1).Width = 8; // Days
worksheet.Column(2).Width = 13; // Date
worksheet.Column(3).Width = 12; // Office From
worksheet.Column(4).Width = 12; // Office To
worksheet.Column(5).Width = 12; // Office Break
worksheet.Column(6).Width = 12; // After From
worksheet.Column(7).Width = 12; // After To
worksheet.Column(8).Width = 12; // After Break
worksheet.Column(9).Width = 14; // Total OT Hours
worksheet.Column(10).Width = 15.5f; // Break Hours
worksheet.Column(11).Width = 14; // Net OT Hours
int colIndex = 12;
if (departmentId == 2 || isAdminUser)
{
worksheet.Column(colIndex++).Width = 20; // Station
}
worksheet.Column(colIndex).Width = 35; // Description
var stream = new MemoryStream();
workbook.SaveAs(stream);
@ -164,5 +287,18 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{
return time == null || time == TimeSpan.Zero ? "" : time.Value.ToString(@"hh\:mm");
}
private string GetColumnLetter(int columnNumber)
{
string columnString = "";
while (columnNumber > 0)
{
int currentLetterNumber = (columnNumber - 1) % 26;
char currentLetter = (char)(currentLetterNumber + 65);
columnString = currentLetter + columnString;
columnNumber = (columnNumber - 1) / 26;
}
return columnString;
}
}
}

View File

@ -0,0 +1,163 @@
@{
ViewData["Title"] = "Overtime Pending Approval";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<style>
body {
background-color: #f3f4f6;
font-family: Arial, sans-serif;
}
.table-layer {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 25px;
margin-top: 30px;
border: 1px solid #e0e0e0;
}
.table-container table {
width: 100%;
}
.header {
background-color: #007bff;
color: white;
text-align: center;
}
.btn-sm {
font-size: 0.75rem;
}
</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" v-on:change="loadData">
<option v-for="(m, i) in months" :value="i + 1">{{ m }}</option>
</select>
</div>
<div class="mb-2">
<label>Year</label>
<select class="form-control form-control-sm" v-model="selectedYear" v-on:change="loadData">
<option v-for="y in years" :value="y">{{ y }}</option>
</select>
</div>
</div>
<div class="table-layer">
<div class="table-container table-responsive">
<table class="table table-bordered table-sm table-striped">
<thead>
<tr>
<th class="header">Staff Name</th>
<th class="header">Date Submit</th>
<th class="header" v-if="userRoles.includes('HoU')">HoU Status</th>
<th class="header" v-if="userRoles.includes('HoD')">HoD Status</th>
<th class="header" v-if="userRoles.includes('Manager')">Manager Status</th>
<th class="header" v-if="userRoles.includes('HR')">HR Status</th>
<th class="header">Action</th>
</tr>
</thead>
<tbody>
<tr v-for="row in otStatusList" :key="row.statusId">
<td>{{ row.fullName }}</td>
<td>{{ formatDate(row.submitDate) }}</td>
<td v-if="userRoles.includes('HoU')">{{ row.houStatus }}</td>
<td v-if="userRoles.includes('HoD')">{{ row.hodStatus }}</td>
<td v-if="userRoles.includes('Manager')">{{ row.managerStatus }}</td>
<td v-if="userRoles.includes('HR')">{{ row.hrStatus }}</td>
<td>
<button class="btn btn-success btn-sm me-1" v-on:click="updateStatus(row.statusId, 'Approved')">Approve</button>
<button class="btn btn-danger btn-sm me-1" v-on:click="updateStatus(row.statusId, 'Rejected')">Reject</button>
<button class="btn btn-primary btn-sm" v-on:click="viewOtData(row.statusId)">View</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const app = Vue.createApp({
data() {
return {
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
years: Array.from({ length: 10 }, (_, i) => new Date().getFullYear() - 5 + i),
selectedMonth: new Date().getMonth() + 1,
selectedYear: new Date().getFullYear(),
otStatusList: [],
userRoles: []
};
},
methods: {
loadData() {
fetch(`/OvertimeAPI/GetPendingApproval?month=${this.selectedMonth}&year=${this.selectedYear}`)
.then(res => {
if (!res.ok) throw new Error("Network response was not OK");
return res.json();
})
.then(result => {
this.userRoles = result.roles;
this.otStatusList = result.data;
})
.catch(err => {
console.error("Error loading data:", err);
});
},
formatDate(dateStr) {
const d = new Date(dateStr);
return d.toLocaleDateString();
},
updateStatus(statusId, decision) {
console.log('statusId received:', statusId); // Add this for immediate inspection
if (!statusId) {
console.error("Invalid statusId passed to updateStatus.");
return;
}
const actionText = decision === 'Approved' ? 'approve' : 'reject';
const confirmed = confirm(`Are you sure you want to ${actionText} this request?`);
if (!confirmed) return;
fetch('/OvertimeAPI/UpdateApprovalStatus', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ statusId: statusId, decision: decision })
})
.then(res => {
if (!res.ok) throw new Error("Failed to update status");
return res.json();
})
.then(() => {
this.loadData(); // Refresh table
})
.catch(err => {
console.error("Error updating status:", err);
});
},
viewOtData(statusId) {
// Navigate to another page with the statusId in query string
window.location.href = `/OTcalculate/ApprovalDashboard/OtReview?statusId=${statusId}`;
}
},
mounted() {
this.loadData();
}
});
app.mount('#app');
</script>

View File

@ -0,0 +1,226 @@
@{
ViewData["Title"] = "OT Details Review";
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;
}
.file-info {
margin-top: 10px;
font-size: 14px;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
.file-info a {
color: #007bff;
text-decoration: none;
font-weight: 500;
margin-left: 8px;
}
.file-info a:hover {
text-decoration: underline;
color: #0056b3;
}
</style>
<div id="reviewApp" style="max-width: 1300px; margin: auto; font-size: 13px;">
<div class="table-container table-responsive">
<div style="margin-bottom: 15px;">
<strong>Employee Name:</strong> {{ userInfo.fullName }}<br />
<strong>Department:</strong> {{ userInfo.departmentName || 'N/A' }}<br />
<template v-if="userInfo.filePath">
<div class="file-info">
<strong>Uploaded File:</strong>
<a :href="'/' + userInfo.filePath" target="_blank">
View Uploaded File
</a>
</div>
</template>
</div>
<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<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 (min)</th>
<th class="header-orange" rowspan="2">Net OT Hours</th>
<th class="header-orange" rowspan="2">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 in otRecords" :key="record.overtimeId">
<td>{{ formatDate(record.otDate) }}</td>
<td>{{ formatTime(record.officeFrom) }}</td>
<td>{{ formatTime(record.officeTo) }}</td>
<td>{{ record.officeBreak }}</td>
<td>{{ formatTime(record.afterFrom) }}</td>
<td>{{ formatTime(record.afterTo) }}</td>
<td>{{ record.afterBreak }}</td>
<td>{{ formatHourMinute(calculateTotalTime(record)) }}</td>
<td>{{ calculateTotalBreak(record) }}</td>
<td>{{ formatHourMinute(calculateNetTime(record)) }}</td>
<td>{{ record.stationName || 'N/A' }}</td>
<td>{{ record.otDays }}</td>
<td class="wrap-text">{{ record.otDescription }}</td>
<td>
<button class="btn btn-light border rounded-circle me-1"
v-on:click="editRecord(record.overtimeId)">
<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)">
<i class="bi bi-trash-fill text-danger fs-5"></i>
</button>
</td>
</tr>
<tr v-if="otRecords.length === 0">
<td colspan="14">No overtime details found for this submission.</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=""><i class="bi bi-printer"></i> Print</button>
<button class="btn btn-dark btn-sm" v-on:click=""><i class="bi bi-file-pdf"></i> Save</button>
<button class="btn btn-success btn-sm" v-on:click=""><i class="bi bi-file-earmark-excel"></i> Excel</button>
</div>
</div>
@section Scripts {
<script>
const reviewApp = Vue.createApp({
data() {
return {
otRecords: [],
userInfo: {
fullName: '',
departmentName: '',
filePath: ''
},
};
},
methods: {
fetchOtRecords() {
const params = new URLSearchParams(window.location.search);
const statusId = params.get("statusId");
fetch(`/OvertimeAPI/GetOtRecordsByStatusId/${statusId}`)
.then(res => res.json())
.then(data => {
this.otRecords = data.records;
this.userInfo = data.userInfo;
})
.catch(err => console.error("Error fetching OT records:", err));
},
formatDate(dateStr) {
const d = new Date(dateStr);
return d.toLocaleDateString();
},
formatTime(timeSpanStr) {
if (!timeSpanStr) return null;
const [hours, minutes] = timeSpanStr.split(':');
return `${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}`;
},
calculateTotalTime(record) {
let totalMinutes = 0;
if (record.officeFrom && record.officeTo) {
totalMinutes += this.timeDifferenceInMinutes(record.officeFrom, record.officeTo);
}
if (record.afterFrom && record.afterTo) {
totalMinutes += this.timeDifferenceInMinutes(record.afterFrom, record.afterTo);
}
return totalMinutes;
},
calculateTotalBreak(record) {
return (record.officeBreak || 0) + (record.afterBreak || 0);
},
calculateNetTime(record) {
const totalMinutes = this.calculateTotalTime(record);
const breakMinutes = this.calculateTotalBreak(record);
return totalMinutes - breakMinutes;
},
timeDifferenceInMinutes(start, end) {
const [startHours, startMinutes] = start.split(':').map(Number);
const [endHours, endMinutes] = end.split(':').map(Number);
const startTimeInMinutes = startHours * 60 + startMinutes;
const endTimeInMinutes = endHours * 60 + endMinutes;
return endTimeInMinutes - startTimeInMinutes;
},
formatHourMinute(totalMinutes) {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return `${hours} hr ${minutes} min`;
},
editRecord(id) {
window.location.href = `/OTcalculate/Overtime/EditOvertime?overtimeId=${id}`;
},
deleteRecord(id) {
if (!confirm("Are you sure you want to delete this record?")) return;
fetch(`/OvertimeAPI/DeleteOvertimeInOtReview/${id}`, {
method: 'DELETE'
})
.then(res => {
if (!res.ok) throw new Error("Failed to delete record.");
return res.json();
})
.then(data => {
alert(data.message);
this.fetchOtRecords(); // Refresh the table
})
.catch(err => {
console.error(err);
alert("Error deleting record.");
});
}
},
mounted() {
this.fetchOtRecords();
}
});
reviewApp.mount("#reviewApp");
</script>
}

View File

@ -1,68 +0,0 @@
@{
ViewData["Title"] = "Overtime Approval";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<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>
@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

@ -1,169 +0,0 @@
@{
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>
}

View File

@ -1,5 +0,0 @@
@{
ViewData["Title"] = "Overtime Approval";
Layout = "~/Views/Shared/_Layout.cshtml";
}

View File

@ -1,169 +0,0 @@
@{
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>
}

View File

@ -1,10 +1,28 @@
@{
ViewBag.Title = "User Settings";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<style>
#ApprovalFlowTable {
background-color: white;
}
#ApprovalFlowTable th,
#ApprovalFlowTable td {
background-color: white !important; /* Ensure no transparency from Bootstrap */
color: #323;
}
</style>
<div class="container">
<div class="row justify-content-center">
<div class="col-6 col-md-6 col-lg-3">
@ -46,21 +64,27 @@
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" :class="{ 'bg-purple text-white': activeTab === 'flexi', 'bg-light text-dark': activeTab !== 'flexi' }"
style="border: 1px solid #ddd;" v-on:click="changeTab('flexi')">
style="border: 1px solid #ddd;" v-on:click="changeTab('flexi')">
Flexi Hour Settings
</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{ 'bg-purple text-white': activeTab === 'state', 'bg-light text-dark': activeTab !== 'state' }"
style="border: 1px solid #ddd;" v-on:click="changeTab('state')">
Region Update
style="border: 1px solid #ddd;" v-on:click="changeTab('state')">
Region & Flow Update
</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{ 'bg-purple text-white': activeTab === 'approval', 'bg-light text-dark': activeTab !== 'approval' }"
style="border: 1px solid #ddd;" v-on:click="changeTab('approval')">
Approval Flow
</a>
</li>
</ul>
<div class="tab-content mt-3">
<div v-if="activeTab === 'flexi'" class="card shadow-sm">
<div class="card m-1">
<div class="card m-2">
<form v-on:submit.prevent="updateFlexiHours" data-aos="fade-right">
<div class="d-flex justify-content-center align-items-center mt-3">
<div class="card-body d-flex justify-content-center align-items-center gap-3">
@ -96,32 +120,206 @@
</div>
<div v-if="activeTab === 'state'" class="card shadow-sm">
<div class="card-body d-flex justify-content-center align-items-center gap-3">
<label class="mb-0">Select State:</label>
<select class="form-select" v-model="selectedStateAll" style="max-width: 150px;">
<option disabled value="">--Select--</option>
<option v-for="state in stateList" :value="state.stateId">{{ state.stateName }}</option>
</select>
<button class="btn btn-danger" v-on:click="clearStateSelection">Cancel</button>
<button class="btn btn-success" v-on:click="saveState">Update State</button>
</div>
<div class="card-body">
<table id="stateUpdateTable" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Full Name</th>
<th>Department</th>
<th>Current State</th>
<th class="text-center">Select</th>
</tr>
</thead>
</table>
</div>
<div class="card-body text-center">
<button class="btn btn-danger" v-on:click="clearStateSelection">Clear Selection</button>
<button class="btn btn-success ms-3" v-on:click="saveState">Update User State</button>
<div class="card m-3">
<div class="d-flex justify-content-center align-items-center gap-4 flex-wrap mt-4">
<div class="d-flex align-items-center gap-2 flex-nowrap">
<label class="form-label mb-0">Select State:</label>
<select class="form-select form-select-sm" v-model="selectedStateAll" style="width: 150px; font-size: 0.900rem;">
<option disabled value="">--Select--</option>
<option v-for="state in stateList" :value="state.stateId">{{ state.stateName }}</option>
</select>
</div>
<div class="d-flex align-items-center gap-2 flex-nowrap">
<label class="form-label mb-0">Select Approval Flow:</label>
<select class="form-select form-select-sm" v-model="selectedApprovalFlowId" style="width: 150px; font-size: 0.900rem;">
<option disabled value="">--Select--</option>
<option v-for="flow in approvalFlowList" :key="flow.approvalFlowId" :value="flow.approvalFlowId">
{{ flow.approvalName }}
</option>
</select>
</div>
<div class="d-flex align-items-center gap-2 flex-nowrap">
<button class="btn btn-danger" v-on:click="clearAllSelectionsStateFlow" style="font-size: 0.900rem; margin-left: 10px;">
Clear
</button>
<button class="btn btn-primary" v-on:click="handleCombinedUpdate" style="font-size: 0.900rem;">
Update State & Flow
</button>
</div>
</div>
<div class="card-body">
<table id="stateUpdateTable" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Full Name</th>
<th>Department</th>
<th>Current State</th>
<th>Current Flow</th>
<th class="text-center">Select</th>
</tr>
</thead>
</table>
</div>
<div class="d-flex justify-content-center gap-2 my-3">
<button class="btn btn-danger" v-on:click="clearAllSelectionsStateFlow" style="font-size: 0.900rem; margin-left: 10px;">
Clear
</button>
<button class="btn btn-primary" v-on:click="handleCombinedUpdate" style="font-size: 0.900rem;">
Update State & Flow
</button>
</div>
</div>
</div>
<div v-if="activeTab === 'approval'" class="card shadow-sm">
<div class="card m-3">
<div class="card-body">
<h5 class="card-title text-center">Create Approval Flow</h5>
<div class="d-flex justify-content-center">
<form v-on:submit.prevent="submitApprovalFlow" class="w-75">
<div class="mb-3">
<label for="approvalName" class="form-label">Approval Name</label>
<input type="text" id="approvalName" class="form-control" v-model="approvalFlow.approvalName" placeholder="Enter approval name" required />
</div>
<div class="mb-3">
<label class="form-label">Approval Flow:</label>
</div>
<div class="mb-3">
<label for="hou" class="form-label ms-3">Head of Unit (HoU) / Executive</label>
<select id="hou" class="form-select" v-model="approvalFlow.hou">
<option disabled value="">--Select--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="mb-3">
<label for="hod" class="form-label ms-3">Head of Department (HoD)</label>
<select id="hod" class="form-select" v-model="approvalFlow.hod">
<option disabled value="">--Select--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="mb-3">
<label for="manager" class="form-label ms-3">Manager</label>
<select id="manager" class="form-select" v-model="approvalFlow.manager">
<option disabled value="">--Select--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="mb-3">
<label for="hr" class="form-label ms-3">HR</label>
<select id="hr" class="form-select" v-model="approvalFlow.hr" required>
<option disabled value="">--Select--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="d-flex justify-content-end" style="gap: 10px;">
<button type="button" class="btn btn-danger" v-on:click="clearFormApproval">Clear</button>
<button type="submit" class="btn btn-success">Submit Approval Flow</button>
</div>
</form>
</div>
<div class="d-flex justify-content-center">
<div class="table-container table-responsive w-100" style="margin-top: 20px; max-width: 900px;">
<table id="ApprovalFlowTable" class="table table-bordered table-striped align-middle">
<thead class="table-light">
<tr>
<th style="width: 50px;">No.</th>
<th>Approval Name</th>
<th style="width: 120px;">Action</th>
</tr>
</thead>
<tbody>
<tr v-if="approvalFlowList.length === 0">
<td colspan="3" class="text-center text-muted">No approval flows found.</td>
</tr>
<tr v-for="(flow, index) in approvalFlowList" :key="flow.approvalId">
<td>{{ index + 1 }}</td>
<td>{{ flow.approvalName }}</td>
<td>
<!-- Edit Button with Pencil Icon -->
<button class="btn btn-sm btn-primary me-2" v-on:click="openEditModal(flow)" title="Edit">
<i class="bi bi-pencil"></i>
</button>
<!-- Delete Button with Trash Icon -->
<button class="btn btn-sm btn-danger" v-on:click="deleteApprovalFlow(flow.approvalId)" title="Delete">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editApprovalModal" tabindex="-1" aria-labelledby="editApprovalModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form v-on:submit.prevent="submitEditApprovalFlow">
<div class="modal-header">
<h5 class="modal-title" id="editApprovalModalLabel">Edit Approval Flow</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Approval Name</label>
<input type="text" class="form-control" v-model="editFlow.approvalName" required />
</div>
<div class="mb-3">
<label class="form-label">Head of Unit (HoU)</label>
<select class="form-select" v-model="editFlow.hou">
<option :value="null">--None--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Head of Department (HoD)</label>
<select class="form-select" v-model="editFlow.hod">
<option :value="null">--None--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Manager</label>
<select class="form-select" v-model="editFlow.manager">
<option :value="null">--None--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">HR</label>
<select class="form-select" v-model="editFlow.hr">
<option :value="null">--None--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-success">Save Changes</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@ -145,14 +343,37 @@
selectedUsersState: [],
stateUserList: [],
userDatatable: null,
stateDatatable: null
stateDatatable: null,
approvalFlows: [],
approvalFlowList: [],
selectedApprovalFlowId: '',
approvalFlow: {
approvalName: '',
hou: '',
hod: '',
manager: '',
hr: ''
},
editFlow: {
approvalId: '',
approvalName: '',
hou: '',
hod: '',
manager: '',
hr: ''
},
allUsers: []
};
},
mounted() {
console.log("Vue App Mounted Successfully");
this.fetchFlexiHours();
this.fetchStates();
this.changeTab('flexi'); // Initialize the default tab
this.changeTab('flexi');
this.fetchAllUsers();
this.fetchUsersState();
this.fetchApprovalFlows();
},
methods: {
changeTab(tab) {
@ -165,11 +386,13 @@
this.initiateTable();
}
} else if (tab === 'state') {
this.clearStateSelection();
this.clearAllSelectionsStateFlow();
if (!this.stateUserList.length) {
this.fetchUsersState().then(() => this.initiateStateTable());
}
else{
this.fetchUsersState().then(() => {
this.initiateStateTable();
this.fetchApprovalFlows(); // Ensure approval flows are fetched on tab change
});
} else {
this.initiateStateTable();
}
}
@ -177,7 +400,7 @@
async updateUserSettings(apiUrl, selectedUsers, selectedValue, successMessage, clearCallback, fetchCallback, valueKey) {
if (!selectedUsers.length || !selectedValue) {
alert("Please select at least one user and a value.");
alert("Please select at least one user and a flexi hour.");
return;
}
@ -203,7 +426,6 @@
}
},
async fetchFlexiHours() {
try {
const response = await fetch("/OvertimeAPI/GetFlexiHours");
@ -286,7 +508,7 @@
if (e.target.checked) {
self.selectedUsers.push(userId);
} else {
self.selectedUsers = self.selectedUsers.filter(id => id !== userId);
self.selectedUsers= self.selectedUsers.filter(id => id !== userId);
}
});
});
@ -305,6 +527,7 @@
{ data: "fullName" },
{ data: "departmentName" },
{ data: "state", defaultContent: "N/A" },
{ data: "approvalName", defaultContent: "N/A" },
{
data: "id",
className: "text-center",
@ -326,42 +549,72 @@
});
},
clearForm() {
this.selectedFlexiHourId = '';
this.selectedUsers = [];
if (this.userDatatable) {
this.userDatatable.rows().every(function () {
$(this.node()).find('input[type="checkbox"]').prop('checked', false);
});
async handleCombinedUpdate() {
const hasUsers = this.selectedUsersState.length > 0;
const hasState = this.selectedStateAll !== '';
const hasFlow = this.selectedApprovalFlowId !== '';
if (!hasUsers && !hasState && !hasFlow) {
alert("Please choose users, a state, or an approval flow to proceed.");
this.clearAllSelectionsStateFlow();
return;
}
},
clearStateSelection() {
this.selectedUsersState = [];
if (this.stateDatatable) {
this.stateDatatable.rows().every(function () {
$(this.node()).find('input[type="checkbox"]').prop('checked', false);
});
if (!hasUsers) {
alert("Please select at least one user.");
return;
}
if (!hasState && !hasFlow) {
alert("Please select either a State or Approval Flow.");
return;
}
if (hasState) {
await this.updateUserSettings(
"/OvertimeAPI/UpdateUserStates",
this.selectedUsersState,
this.selectedStateAll,
"State updated successfully!",
() => {}, // Don't clear yet
() => {}, // Don't fetch yet
"StateId"
);
}
if (hasFlow) {
const payload = this.selectedUsersState.map(userId => ({
UserId: userId,
ApprovalFlowId: this.selectedApprovalFlowId
}));
try {
const response = await fetch("/OvertimeAPI/UpdateUserApprovalFlow", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
alert(errorData.message || "Failed to update Approval Flow.");
return;
}
alert("Approval Flow updated successfully!");
} catch (error) {
console.error("Error updating approval flow:", error);
alert("An unexpected error occurred.");
}
}
// After updates, refresh and clear
await this.fetchUsersState();
this.initiateStateTable();
this.clearAllSelectionsStateFlow();
},
async saveState() {
await this.updateUserSettings(
"/OvertimeAPI/UpdateUserStates",
this.selectedUsersState,
this.selectedStateAll,
"State updated successfully!",
this.clearStateSelection,
this.fetchUsersState, // This will fetch the updated state users after the update
"StateId"
);
// Fetch the updated state list to ensure the page reflects the new state data
await this.fetchUsersState(); // This fetch will make sure stateUserList is updated
this.initiateStateTable(); // Reinitialize the state table with new data
},
async updateFlexiHours() {
await this.updateUserSettings(
"/OvertimeAPI/UpdateUserFlexiHours",
@ -372,7 +625,148 @@
this.fetchUsers,
"FlexiHourId"
);
},
async fetchAllUsers() {
try {
const response = await fetch("/OvertimeAPI/GetAllUsers");
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Fetch failed: ${response.status} ${errorText}`);
}
const data = await response.json();
console.log("Fetched users:", data);
this.allUsers = data.filter(e => e.fullName !== "MAAdmin" && e.fullName !== "SysAdmin");
} catch (error) {
console.error("Error fetching all users:", error);
}
},
async fetchApprovalFlows() {
try {
const response = await fetch('/OvertimeAPI/GetApprovalFlowList');
const data = await response.json();
this.approvalFlowList = data; // Update the data correctly
console.log('Fetched approval flows:', this.approvalFlowList); // Verify data
} catch (error) {
console.error('Error fetching approval flows:', error);
}
},
async submitApprovalFlow() {
try {
const payload = { ...this.approvalFlow };
if (payload.hou === "") payload.hou = null;
if (payload.hod === "") payload.hod = null;
if (payload.manager === "") payload.manager = null;
const response = await fetch('/OvertimeAPI/CreateApprovalFlow', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
alert('Approval flow created successfully!');
this.clearFormApproval();
await this.fetchApprovalFlows();
} else {
const errorData = await response.json();
alert(errorData.message || 'Failed to create approval flow. Please try again.');
}
} catch (error) {
console.error('Error submitting approval flow:', error);
alert('An unexpected error occurred.');
}
},
clearFormApproval() {
this.approvalFlow = {
approvalName: '',
hou: '',
hod: '',
manager: '',
hr: ''
};
},
async deleteApprovalFlow(approvalId) {
if (!confirm("Are you sure you want to delete this approval flow?")) return;
try {
const response = await fetch(`/OvertimeAPI/DeleteApprovalFlow/${approvalId}`, { method: "DELETE" });
if (!response.ok) throw new Error("Failed to delete");
alert("Approval flow deleted.");
await this.fetchApprovalFlows();
} catch (error) {
console.error("Error deleting flow:", error);
}
},
clearForm() {
this.selectedFlexiHourId = '';
this.selectedUsers = [];
if (this.userDatatable) {
this.userDatatable.rows().every(function () {
$(this.node()).find('input[type="checkbox"]').prop('checked', false);
});
}
},
clearAllSelectionsStateFlow() {
this.selectedStateAll = '';
this.selectedApprovalFlowId = '';
this.selectedUsersState = [];
if (this.stateDatatable) {
this.stateDatatable.rows().every(function () {
$(this.node()).find('input[type="checkbox"]').prop('checked', false);
});
}
},
openEditModal(flow) {
// Set the editFlow to the selected flow
this.editFlow = { ...flow };
// Optionally, log the data to ensure it's correct
console.log(this.editFlow);
// Show the modal
this.fetchAllUsers().then(() => {
const modal = new bootstrap.Modal(document.getElementById('editApprovalModal'));
modal.show();
});
},
async submitEditApprovalFlow() {
try {
const response = await fetch('/OvertimeAPI/EditApprovalFlow', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.editFlow)
});
if (response.ok) {
alert('Approval flow updated successfully!');
await this.fetchApprovalFlows();
const modalElement = document.getElementById('editApprovalModal');
const modal = bootstrap.Modal.getInstance(modalElement);
modal.hide();
} else {
const error = await response.json();
alert(error.message || 'Failed to update approval flow.');
}
} catch (error) {
console.error('Error updating flow:', error);
alert('Unexpected error occurred.');
}
}
}
});

View File

@ -107,6 +107,17 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
<div class="card-header text-center" style="background-color: white;">
<label class="date-heading text-center">Staff Approval Flow Latest Update: {{ approvalFlowUpdateDate || 'N/A' }}</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -126,6 +137,7 @@
calendarUpdateDate: null,
flexiHourUpdateDate: null,
regionUpdateDate: null,
approvalFlowUpdateDate: null,
};
},
mounted() {
@ -144,6 +156,7 @@
this.calendarUpdateDate = data.calendarUpdateDate;
this.flexiHourUpdateDate = data.flexiHourUpdateDate;
this.regionUpdateDate = data.regionUpdateDate;
this.approvalFlowUpdateDate = data.approvalFlowUpdateDate;
} catch (error) {
console.error(error);
}

View File

@ -153,6 +153,9 @@
return { label, value: totalMinutes };
}),
previousPage: document.referrer
};
},
@ -434,7 +437,7 @@
if (response.ok) {
alert("Overtime record updated successfully!");
window.location.href = '/OTcalculate/Overtime/OtRecords';
window.location.href = this.previousPage;
} else {
alert("Failed to update overtime record.");
}
@ -445,7 +448,7 @@
},
goBack() {
window.location.href = "/OTcalculate/Overtime/OtRecords";
window.location.href = this.previousPage;
},
clearOfficeHours() {
this.editForm.officeFrom = "";

View File

@ -160,8 +160,8 @@
<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-file-pdf"></i> Save</button>
<button class="btn btn-success btn-sm" v-on:click="downloadExcel(selectedMonth, selectedYear)"><i class="bi bi-file-earmark-excel"></i>Excel</button>
<button class="btn btn-success btn-sm" :disabled="hasSubmitted" v-on:click="openSubmitModal"><i class="bi bi-send"></i>{{ hasSubmitted ? 'Submitted' : 'Submit' }}</button>
<button class="btn btn-success btn-sm" v-on:click="downloadExcel(selectedMonth, selectedYear)"><i class="bi bi-file-earmark-excel"></i> Excel</button>
<button class="btn btn-success btn-sm" :disabled="hasSubmitted" v-on:click="openSubmitModal"><i class="bi bi-send"></i>{{ hasSubmitted ? ' Submitted' : ' Submit' }}</button>
</div>
<div class="modal fade" id="submitModal" tabindex="-1" aria-labelledby="submitModalLabel" aria-hidden="true">
<div class="modal-dialog">
@ -208,10 +208,10 @@
},
watch: {
selectedMonth() {
this.checkSubmissionStatus();
this.getSubmissionStatus(); // Renamed method call
},
selectedYear() {
this.checkSubmissionStatus();
this.getSubmissionStatus(); // Renamed method call
}
},
computed: {
@ -243,7 +243,7 @@
},
async mounted() {
await this.initUserAndRecords();
await this.checkSubmissionStatus();
await this.getSubmissionStatus();
},
methods: {
async initUserAndRecords() {
@ -288,12 +288,21 @@
console.error("Records fetch error:", err);
}
},
async checkSubmissionStatus() {
async getSubmissionStatus() {
try {
const res = await fetch(`/OvertimeAPI/CheckOvertimeSubmitted/${this.userId}/${this.selectedMonth}/${this.selectedYear}`);
if (!res.ok) {
const errorText = await res.text();
console.error("Failed to check submission status:", errorText);
alert("Error checking submission status. Please try again.");
this.hasSubmitted = false; // Or handle as appropriate
return;
}
this.hasSubmitted = await res.json();
} catch (err) {
console.error("Failed to check submission status", err);
console.error("Failed to parse submission status:", err);
alert("An unexpected error occurred while checking submission status.");
this.hasSubmitted = false; // Or handle as appropriate
}
},
toggleDescription(index) {
@ -424,6 +433,8 @@
const modalInstance = bootstrap.Modal.getInstance(modalEl);
modalInstance.hide();
await this.getSubmissionStatus(); // Call this to refresh the hasSubmitted status
} else {
alert('Submission failed.');
}
@ -433,7 +444,6 @@
}
}
}
});
app.mount("#app");

View File

@ -205,6 +205,9 @@
// Fetch user's state and public holidays after fetching user info
if (this.userId) {
await this.fetchUserStateAndHolidays();
if (this.userState) {
this.statusId = this.userState.defaultStatusId;
}
}
} else {
@ -356,7 +359,8 @@
stationId: this.isPSTWAIR ? parseInt(this.selectedAirStation) : null,
otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '),
otDays: this.detectedDayType, // Use the auto-detected day type
userId: this.userId
userId: this.userId,
statusId: this.statusId || null,
};
try {
@ -378,9 +382,10 @@
this.clearForm();
} catch (error) {
console.error("Error adding overtime:", error);
alert("Failed to save overtime. Please check the console for errors.");
alert(`Failed to save overtime. Error: ${error.message}`);
}
},
updateDayType() {
if (!this.selectedDate || !this.userState) {
this.detectedDayType = "";

View File

@ -2,3 +2,148 @@
ViewData["Title"] = "Overtime Status";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<style>
.white-box {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
</style>
<div id="app" style="max-width: 1300px; margin: auto; font-size: 13px;">
<div class="table-layer">
<div class="white-box">
<div class="table-container table-responsive">
<table id="otStatusTable" class="table table-bordered table-sm table-striped">
<thead>
<tr>
<th>Month/Year</th>
<th>Submit Date</th>
<th v-if="includeHou">HoU Status</th>
<th v-if="includeHod">HoD Status</th>
<th v-if="includeManager">Manager Status</th>
<th v-if="includeHr">HR Status</th>
<th>Updated</th>
<th>File</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in otRecords" :key="index">
<td>{{ formatMonthYear(item.month, item.year) }}</td>
<td>{{ formatDate(item.submitDate) }}</td>
<td v-if="includeHou">{{ item.houStatus ?? '-' }}</td>
<td v-if="includeHod">{{ item.hodStatus ?? '-' }}</td>
<td v-if="includeManager">{{ item.managerStatus ?? '-' }}</td>
<td v-if="includeHr">{{ item.hrStatus ?? '-' }}</td>
<td>{{ item.updated ? 'Yes' : 'No' }}</td>
<td>
<button v-if="item.filePath" class="btn btn-sm btn-primary" v-on:click ="previewFile(item.filePath)">
View
</button>
<span v-else>-</span>
</td>
</tr>
<tr v-if="otRecords.length === 0">
<td :colspan="columnCount" class="text-center">No records found.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div v-if="showPreview" class="modal-backdrop">
<div class="modal-dialog-box">
<div class="modal-header" style="background-color: white;">
<h5 class="modal-title">File Preview</h5>
<button type="button" class="btn-close" aria-label="Close Preview" v-on:click ="closePreview"></button>
</div>
<div class="modal-body">
<iframe :src="previewUrl" width="100%" height="650px" style="border: none;"></iframe>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
const app = Vue.createApp({
data() {
return {
otRecords: [],
includeHou: false,
includeHod: false,
includeManager: false,
includeHr: false,
showPreview: false,
previewUrl: ''
};
},
computed: {
columnCount() {
let count = 3; // Month/Year, SubmitDate, Updated
if (this.includeHou) count++;
if (this.includeHod) count++;
if (this.includeManager) count++;
if (this.includeHr) count++;
return count + 1; // File column
}
},
mounted() {
fetch('/OvertimeAPI/GetUserOtStatus')
.then(res => res.json())
.then(data => {
this.includeHou = data.includeHou;
this.includeHod = data.includeHod;
this.includeManager = data.includeManager;
this.includeHr = data.includeHr;
this.otRecords = (data.otStatuses || []).sort((a, b) => {
// Sort in descending order of SubmitDate (latest first)
return new Date(b.submitDate) - new Date(a.submitDate);
});
this.$nextTick(() => {
// Delay to ensure table is rendered
if ($.fn.dataTable.isDataTable('#otStatusTable')) {
$('#otStatusTable').DataTable().destroy();
}
$('#otStatusTable').DataTable({
responsive: true,
pageLength: 10,
order: []
});
});
})
.catch(err => {
console.error("Error fetching OT records:", err);
});
},
methods: {
formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString();
},
formatMonthYear(month, year) {
if (!month || !year) return '-';
return `${month.toString().padStart(2, '0')}/${year}`;
},
previewFile(path) {
this.previewUrl = '/' + path.replace(/^\/+/, '');
this.showPreview = true;
document.body.style.overflow = 'hidden';
},
closePreview() {
this.previewUrl = '';
this.showPreview = false;
document.body.style.overflow = '';
}
}
});
app.mount('#app');
</script>
}

View File

@ -25,6 +25,7 @@ using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using PSTW_CentralSystem.Areas.OTcalculate.Services;
using Microsoft.AspNetCore.Hosting;
using DocumentFormat.OpenXml.InkML;
namespace PSTW_CentralSystem.Controllers.API
@ -61,13 +62,15 @@ namespace PSTW_CentralSystem.Controllers.API
var latestCalendarUpdate = _centralDbContext.Holidays.OrderByDescending(c => c.LastUpdated).FirstOrDefault()?.LastUpdated;
var latestFlexiHourUpdate = _centralDbContext.Hrusersetting.OrderByDescending(r => r.FlexiHourUpdate).FirstOrDefault()?.FlexiHourUpdate;
var latestRegionUpdate = _centralDbContext.Hrusersetting.OrderByDescending(c => c.StateUpdate).FirstOrDefault()?.StateUpdate;
var latestApprovalFlowUpdate = _centralDbContext.Hrusersetting.OrderByDescending(c => c.ApprovalUpdate).FirstOrDefault()?.ApprovalUpdate;
var updateDates = new
{
rateUpdateDate = latestRateUpdate.HasValue ? latestRateUpdate.Value.ToString("dd MMMM yyyy") : null,
calendarUpdateDate = latestCalendarUpdate.HasValue ? latestCalendarUpdate.Value.ToString("dd MMMM yyyy") : null,
flexiHourUpdateDate = latestFlexiHourUpdate.HasValue ? latestFlexiHourUpdate.Value.ToString("dd MMMM yyyy") : null,
regionUpdateDate = latestRegionUpdate.HasValue ? latestRegionUpdate.Value.ToString("dd MMMM yyyy") : null
regionUpdateDate = latestRegionUpdate.HasValue ? latestRegionUpdate.Value.ToString("dd MMMM yyyy") : null,
approvalFlowUpdateDate = latestApprovalFlowUpdate.HasValue ? latestApprovalFlowUpdate.Value.ToString("dd MMMM yyyy") : null
};
return Json(updateDates);
@ -161,8 +164,8 @@ namespace PSTW_CentralSystem.Controllers.API
}
#endregion
#region FlexiHour State
private async Task UpdateOrInsertUserSettingAsync(int userId, int? flexiHourId = null, int? stateId = null)
#region FlexiHour State Approval
private async Task UpdateOrInsertUserSettingAsync(int userId, int? flexiHourId = null, int? stateId = null, int? approvalFlowId = null)
{
var setting = await _centralDbContext.Hrusersetting
.FirstOrDefaultAsync(h => h.UserId == userId);
@ -181,6 +184,12 @@ namespace PSTW_CentralSystem.Controllers.API
setting.StateUpdate = DateTime.Now;
}
if (approvalFlowId.HasValue)
{
setting.ApprovalFlowId = approvalFlowId;
setting.ApprovalUpdate = DateTime.Now;
}
_centralDbContext.Hrusersetting.Update(setting);
}
else
@ -191,7 +200,9 @@ namespace PSTW_CentralSystem.Controllers.API
FlexiHourId = flexiHourId,
FlexiHourUpdate = flexiHourId.HasValue ? DateTime.Now : null,
StateId = stateId,
StateUpdate = stateId.HasValue ? DateTime.Now : null
StateUpdate = stateId.HasValue ? DateTime.Now : null,
ApprovalFlowId = approvalFlowId,
ApprovalUpdate = approvalFlowId.HasValue ? DateTime.Now : null
};
_centralDbContext.Hrusersetting.Add(newSetting);
}
@ -285,6 +296,7 @@ namespace PSTW_CentralSystem.Controllers.API
var hrUserSettings = await _centralDbContext.Hrusersetting
.Include(h => h.State)
.Include(h => h.Approvalflow)
.ToListAsync();
var result = users.Select(u =>
@ -295,7 +307,8 @@ namespace PSTW_CentralSystem.Controllers.API
u.Id,
u.FullName,
DepartmentName = u.Department != null ? u.Department.DepartmentName : "N/A",
State = hrSetting != null && hrSetting.State != null ? hrSetting.State.StateName : "N/A"
State = hrSetting != null && hrSetting.State != null ? hrSetting.State.StateName : "N/A",
approvalName = hrSetting?.Approvalflow?.ApprovalName ?? "N/A"
};
}).ToList();
@ -317,8 +330,23 @@ namespace PSTW_CentralSystem.Controllers.API
{
foreach (var update in updates)
{
await UpdateOrInsertUserSettingAsync(update.UserId, stateId: update.StateId);
var existingSetting = await _centralDbContext.Hrusersetting.FirstOrDefaultAsync(h => h.UserId == update.UserId);
if (existingSetting != null)
{
existingSetting.StateId = update.StateId;
}
else
{
// Handle the case where a user setting doesn't exist.
// You might want to create a new one with a default ApprovalFlowId or return an error.
_centralDbContext.Hrusersetting.Add(new HrUserSettingModel
{
UserId = update.UserId,
StateId = update.StateId,
// Consider setting a default ApprovalFlowId here if needed
});
}
}
await _centralDbContext.SaveChangesAsync();
@ -330,9 +358,202 @@ namespace PSTW_CentralSystem.Controllers.API
}
}
[HttpPost("UpdateUserApprovalFlow")]
public async Task<IActionResult> UpdateUserApprovalFlow([FromBody] List<HrUserSettingModel> updates)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
try
{
foreach (var update in updates)
{
var existingSetting = await _centralDbContext.Hrusersetting.FirstOrDefaultAsync(h => h.UserId == update.UserId);
if (existingSetting != null)
{
existingSetting.ApprovalFlowId = update.ApprovalFlowId;
existingSetting.ApprovalUpdate = DateTime.Now;
}
else
{
_centralDbContext.Hrusersetting.Add(new HrUserSettingModel
{
UserId = update.UserId,
ApprovalFlowId = update.ApprovalFlowId,
ApprovalUpdate = DateTime.Now
});
}
}
await _centralDbContext.SaveChangesAsync();
return Ok();
}
catch (Exception ex)
{
return BadRequest(new { message = ex.Message });
}
}
[HttpPost("UpdateApprovalFlow")]
public async Task<IActionResult> UpdateApprovalFlow([FromBody] ApprovalFlowModel model)
{
if (!ModelState.IsValid)
return BadRequest("Invalid data");
var existing = await _centralDbContext.Approvalflow.FindAsync(model.ApprovalFlowId);
if (existing == null)
return NotFound("Approval flow not found");
existing.ApprovalName = model.ApprovalName;
existing.HoU = model.HoU;
existing.HoD = model.HoD;
existing.Manager = model.Manager;
existing.HR = model.HR;
await _centralDbContext.SaveChangesAsync();
return Ok();
}
[HttpGet("GetApprovalFlowList")]
public async Task<IActionResult> GetApprovalFlowList()
{
try
{
var flows = await _centralDbContext.Approvalflow
.Select(af => new
{
af.ApprovalFlowId,
af.ApprovalName,
hou = af.HoU,
hod = af.HoD,
manager = af.Manager,
hr = af.HR
})
.ToListAsync();
return Ok(flows);
}
catch (Exception ex)
{
return StatusCode(500, new { message = ex.Message });
}
}
#endregion
#region Approval Flow
[HttpPost("CreateApprovalFlow")]
public async Task<IActionResult> CreateApprovalFlow([FromBody] ApprovalFlowModel model)
{
if (string.IsNullOrEmpty(model.ApprovalName))
return BadRequest(new { message = "Approval Name is required." });
if (!model.HR.HasValue)
return BadRequest(new { message = "HR approver is required." });
if (!ModelState.IsValid)
return BadRequest(new { message = "Invalid approval flow data." });
try
{
_centralDbContext.Approvalflow.Add(model);
await _centralDbContext.SaveChangesAsync();
return Ok(new { message = "Approval flow created successfully." });
}
catch (Exception ex)
{
return StatusCode(500, new { message = $"Error saving approval flow: {ex.InnerException?.Message ?? ex.Message}" });
}
}
[HttpGet("GetApprovalFlowRecord")]
public async Task<IActionResult> GetApprovalFlowRecord()
{
try
{
var approvalFlows = await _centralDbContext.Approvalflow
.Select(af => new { af.ApprovalFlowId, af.ApprovalName })
.ToListAsync();
return Ok(approvalFlows);
}
catch (Exception ex)
{
return StatusCode(500, new { message = $"Error fetching approval flows: {ex.Message}" });
}
}
[HttpPut("EditApprovalFlow")]
public async Task<IActionResult> EditApprovalFlow([FromBody] ApprovalFlowModel model)
{
if (!ModelState.IsValid)
{
return BadRequest("Invalid data.");
}
var existingFlow = await _centralDbContext.Approvalflow.FindAsync(model.ApprovalFlowId);
if (existingFlow == null)
{
return NotFound("Approval flow not found.");
}
// Update fields
existingFlow.ApprovalName = model.ApprovalName;
existingFlow.HoU = model.HoU;
existingFlow.HoD = model.HoD;
existingFlow.Manager = model.Manager;
existingFlow.HR = model.HR;
try
{
await _centralDbContext.SaveChangesAsync();
return Ok(new { message = "Approval flow updated successfully." });
}
catch (Exception ex)
{
// Log the exception
return StatusCode(500, new { message = "Failed to update approval flow.", detail = ex.Message });
}
}
[HttpDelete("DeleteApprovalFlow/{id}")]
public async Task<IActionResult> DeleteApprovalFlow(int id)
{
try
{
var approvalFlow = await _centralDbContext.Approvalflow.FindAsync(id);
if (approvalFlow == null)
{
return NotFound(new { message = "Approval flow not found." });
}
_centralDbContext.Approvalflow.Remove(approvalFlow);
await _centralDbContext.SaveChangesAsync();
return Ok(new { message = "Approval flow deleted successfully." });
}
catch (Exception ex)
{
return StatusCode(500, new { message = $"Error deleting approval flow: {ex.Message}" });
}
}
[HttpGet("GetAllUsers")]
public async Task<IActionResult> GetAllUsers()
{
try
{
var users = await _centralDbContext.Users
.Select(u => new { u.Id, u.FullName })
.ToListAsync();
return Ok(users);
}
catch (Exception ex)
{
return StatusCode(500, new { message = ex.Message });
}
}
#endregion
#region Calendar
[HttpGet("GetStatesName")]
@ -574,6 +795,7 @@ namespace PSTW_CentralSystem.Controllers.API
public async Task<IActionResult> AddOvertimeAsync([FromBody] OvertimeRequestDto request)
{
_logger.LogInformation("AddOvertimeAsync called.");
_logger.LogInformation("Received request: {@Request}", request);
if (request == null)
{
@ -618,6 +840,7 @@ namespace PSTW_CentralSystem.Controllers.API
OtDescription = request.OtDescription,
OtDays = request.OtDays,
UserId = request.UserId,
StatusId = request.StatusId // Pass StatusId as it is, which can be null
};
_centralDbContext.Otregisters.Add(newRecord);
@ -628,10 +851,11 @@ namespace PSTW_CentralSystem.Controllers.API
catch (Exception ex)
{
_logger.LogError(ex, "Error registering overtime.");
return StatusCode(500, "An error occurred while saving overtime.");
return StatusCode(500, $"An error occurred while saving overtime: {ex.InnerException?.Message ?? ex.Message}");
}
}
[HttpGet("GetUserStateAndHolidays/{userId}")]
public async Task<IActionResult> GetUserStateAndHolidaysAsync(int userId)
{
@ -901,6 +1125,9 @@ namespace PSTW_CentralSystem.Controllers.API
}
}
var logoPath = Path.Combine(_env.WebRootPath, "images", "logo.jpg");
byte[]? logoImage = System.IO.File.Exists(logoPath) ? System.IO.File.ReadAllBytes(logoPath) : null;
var stream = _excelService.GenerateOvertimeExcel(
mergedRecords,
departmentId,
@ -910,9 +1137,10 @@ namespace PSTW_CentralSystem.Controllers.API
weekendId,
publicHolidays,
isAdminUser: IsAdmin(userId),
logoImage: null
logoImage: logoImage
);
return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"OvertimeRecords_{year}_{month}.xlsx");
}
@ -923,7 +1151,6 @@ namespace PSTW_CentralSystem.Controllers.API
if (model.File == null || model.File.Length == 0)
return BadRequest("No file uploaded.");
// Get userId from the login token
var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId))
return Unauthorized();
@ -944,31 +1171,49 @@ namespace PSTW_CentralSystem.Controllers.API
var relativePath = Path.Combine("Media", "Overtime", uniqueFileName).Replace("\\", "/");
// Check if record exists for same month/year/user
var existingStatus = _centralDbContext.Otstatus.FirstOrDefault(x => x.UserId == userId && x.Month == model.Month && x.Year == model.Year);
// Create a NEW OtStatusModel for the resubmission
var statusModel = new OtStatusModel
{
UserId = userId,
Month = model.Month,
Year = model.Year,
FilePath = relativePath,
SubmitDate = DateTime.Now,
HouStatus = "Pending",
HodStatus = "Pending",
ManagerStatus = "Pending",
HrStatus = "Pending"
};
if (existingStatus != null)
_centralDbContext.Otstatus.Add(statusModel);
await _centralDbContext.SaveChangesAsync(); // Save the new OtStatus record to get its StatusId
// Update StatusId in OtRegister records for the current month/year
var monthStart = new DateTime(model.Year, model.Month, 1);
var monthEnd = monthStart.AddMonths(1);
var otRecords = _centralDbContext.Otregisters
.Where(r => r.UserId == userId && r.OtDate >= monthStart && r.OtDate < monthEnd)
.ToList();
foreach (var record in otRecords)
{
existingStatus.FilePath = relativePath;
existingStatus.SubmitDate = DateTime.Now;
_centralDbContext.Otstatus.Update(existingStatus);
record.StatusId = statusModel.StatusId;
}
else
_centralDbContext.Otregisters.UpdateRange(otRecords);
await _centralDbContext.SaveChangesAsync();
// Update HrUserSetting with the NEW StatusId
var userSetting = _centralDbContext.Hrusersetting.FirstOrDefault(s => s.UserId == userId);
if (userSetting != null)
{
var newStatus = new OtStatusModel
{
UserId = userId,
Month = model.Month,
Year = model.Year,
FilePath = relativePath,
SubmitDate = DateTime.Now,
HodStatus = "Pending",
HrStatus = "Pending"
};
_centralDbContext.Otstatus.Add(newStatus);
userSetting.ApprovalUpdate = DateTime.Now;
_centralDbContext.Hrusersetting.Update(userSetting);
}
await _centralDbContext.SaveChangesAsync();
return Ok();
}
catch (Exception ex)
@ -979,15 +1224,47 @@ namespace PSTW_CentralSystem.Controllers.API
}
[HttpGet("CheckOvertimeSubmitted/{userId}/{month}/{year}")]
public async Task<IActionResult> CheckOvertimeSubmitted(string userId, int month, int year)
public IActionResult CheckOvertimeSubmitted(int userId, int month, int year)
{
if (!int.TryParse(userId, out int parsedUserId))
return BadRequest("Invalid userId.");
try
{
// Get the latest OtStatus record for the user, month, and year
var latestStatus = _centralDbContext.Otstatus
.Where(s => s.UserId == userId && s.Month == month && s.Year == year)
.OrderByDescending(s => s.SubmitDate)
.FirstOrDefault();
var isSubmitted = await _centralDbContext.Otstatus
.AnyAsync(s => s.UserId == parsedUserId && s.Month == month && s.Year == year);
if (latestStatus == null)
{
return Ok(false); // Not submitted yet
}
return Ok(isSubmitted);
// Check if the latest submission has been rejected at any level
if (latestStatus.HouStatus?.ToLower() == "rejected" ||
latestStatus.HodStatus?.ToLower() == "rejected" ||
latestStatus.ManagerStatus?.ToLower() == "rejected" ||
latestStatus.HrStatus?.ToLower() == "rejected")
{
return Ok(false); // Latest submission was rejected, enable submit
}
// If not rejected, check if it's in a pending state (new submission)
if (latestStatus.HouStatus?.ToLower() == "pending" ||
latestStatus.HodStatus?.ToLower() == "pending" ||
latestStatus.ManagerStatus?.ToLower() == "pending" ||
latestStatus.HrStatus?.ToLower() == "pending")
{
return Ok(true); // Newly submitted or resubmitted, disable submit
}
// If not pending and not rejected, it implies it's fully approved or in a final rejected state
return Ok(true); // Disable submit
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while checking overtime submission status.");
return StatusCode(500, new { error = "Internal server error occurred." });
}
}
#endregion
@ -1051,9 +1328,322 @@ namespace PSTW_CentralSystem.Controllers.API
}
#endregion
#region OtStatus
#region Ot Status
[HttpGet("GetUserOtStatus")]
public IActionResult GetUserOtStatus()
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdClaim))
return BadRequest("User ID is not available.");
if (!int.TryParse(userIdClaim, out int userId))
return BadRequest("Invalid User ID.");
var approvalFlowId = _centralDbContext.Hrusersetting
.Where(x => x.UserId == userId)
.Select(x => x.ApprovalFlowId)
.FirstOrDefault();
var flow = _centralDbContext.Approvalflow
.FirstOrDefault(f => f.ApprovalFlowId == approvalFlowId);
if (flow == null)
return BadRequest("Approval flow not found.");
bool includeHou = flow.HoU.HasValue;
bool includeHod = flow.HoD.HasValue;
bool includeManager = flow.Manager.HasValue;
bool includeHr = flow.HR.HasValue;
var otStatuses = _centralDbContext.Otstatus
.Where(o => o.UserId == userId)
.Select(o => new
{
o.Month,
o.Year,
o.SubmitDate,
HouStatus = includeHou ? o.HouStatus : null,
HodStatus = includeHod ? o.HodStatus : null,
ManagerStatus = includeManager ? o.ManagerStatus : null,
HrStatus = includeHr ? o.HrStatus : null,
o.FilePath,
Updated = o.HouUpdate != null || o.HodUpdate != null || o.ManagerUpdate != null || o.HrUpdate != null
})
.ToList();
return Ok(new
{
includeHou,
includeHod,
includeManager,
includeHr,
otStatuses
});
}
#endregion
#region OT Approval
[HttpGet("GetPendingApproval")]
public IActionResult GetPendingApproval(int month, int year)
{
var userIdStr = _userManager.GetUserId(User);
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int currentUserId))
return Unauthorized("Invalid or missing user ID");
// Get all flows where the user is an approver
var flows = _centralDbContext.Approvalflow
.Where(f => f.HoU == currentUserId || f.HoD == currentUserId || f.Manager == currentUserId || f.HR == currentUserId)
.ToList();
if (!flows.Any())
return Unauthorized("You are not assigned to any approval flow.");
// Map flow ID to role (the role user is in for that flow)
var flowRoleMap = new Dictionary<int, string>();
foreach (var flow in flows)
{
if (flow.HoU == currentUserId) flowRoleMap[flow.ApprovalFlowId] = "HoU";
if (flow.HoD == currentUserId) flowRoleMap[flow.ApprovalFlowId] = "HoD";
if (flow.Manager == currentUserId) flowRoleMap[flow.ApprovalFlowId] = "Manager";
if (flow.HR == currentUserId) flowRoleMap[flow.ApprovalFlowId] = "HR";
}
// Load all OT status entries and their associated approval flow IDs
var otEntriesWithFlow = (from status in _centralDbContext.Otstatus
join user in _centralDbContext.Users on status.UserId equals user.Id
join setting in _centralDbContext.Hrusersetting on status.UserId equals setting.UserId
where status.Month == month && status.Year == year && setting.ApprovalFlowId.HasValue
select new
{
status.StatusId,
status.SubmitDate,
status.HouStatus,
status.HodStatus,
status.ManagerStatus,
status.HrStatus,
setting.ApprovalFlowId,
fullName = user.FullName
}).ToList();
var filteredList = new List<object>();
var distinctRoles = new HashSet<string>();
foreach (var entry in otEntriesWithFlow)
{
if (!flowRoleMap.ContainsKey(entry.ApprovalFlowId.Value))
continue; // User is not an approver for this flow
var role = flowRoleMap[entry.ApprovalFlowId.Value];
distinctRoles.Add(role);
var flow = _centralDbContext.Approvalflow.FirstOrDefault(f => f.ApprovalFlowId == entry.ApprovalFlowId);
if (flow == null) continue;
// If any earlier status is rejected, do not allow further approvals
if (entry.HouStatus == "Rejected" || entry.HodStatus == "Rejected" || entry.ManagerStatus == "Rejected" || entry.HrStatus == "Rejected")
continue;
bool shouldShow = false;
// Show if the current user still needs to act
if ((role == "HoU" && entry.HouStatus == "Pending") ||
(role == "HoD" && (!flow.HoU.HasValue || entry.HouStatus == "Approved") && entry.HodStatus == "Pending") ||
(role == "Manager" && (!flow.HoU.HasValue || entry.HouStatus == "Approved") && (!flow.HoD.HasValue || entry.HodStatus == "Approved") && entry.ManagerStatus == "Pending") ||
(role == "HR" && (!flow.HoU.HasValue || entry.HouStatus == "Approved") && (!flow.HoD.HasValue || entry.HodStatus == "Approved") && (!flow.Manager.HasValue || entry.ManagerStatus == "Approved") && entry.HrStatus == "Pending"))
{
shouldShow = true;
}
// Also show if the current user has already acted (approved or rejected)
else if ((role == "HoU" && (entry.HouStatus == "Approved" || entry.HouStatus == "Rejected")) ||
(role == "HoD" && (entry.HodStatus == "Approved" || entry.HodStatus == "Rejected")) ||
(role == "Manager" && (entry.ManagerStatus == "Approved" || entry.ManagerStatus == "Rejected")) ||
(role == "HR" && (entry.HrStatus == "Approved" || entry.HrStatus == "Rejected")))
{
shouldShow = true;
}
if (shouldShow)
{
filteredList.Add(entry);
}
}
return Json(new
{
Roles = distinctRoles.ToList(),
Data = filteredList
});
}
[HttpPost("UpdateApprovalStatus")]
public IActionResult UpdateApprovalStatus([FromBody] UpdateStatusDto dto)
{
if (dto == null)
return BadRequest("DTO is null or improperly formatted.");
if (dto.StatusId == 0 || string.IsNullOrWhiteSpace(dto.Decision))
return BadRequest("Missing StatusId or Decision.");
var userIdStr = _userManager.GetUserId(User);
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int currentUserId))
return Unauthorized("Invalid user");
var status = _centralDbContext.Otstatus.FirstOrDefault(s => s.StatusId == dto.StatusId);
if (status == null) return NotFound("OT Status not found");
var setting = _centralDbContext.Hrusersetting.FirstOrDefault(s => s.UserId == status.UserId);
if (setting == null) return BadRequest("User setting not found");
var flow = _centralDbContext.Approvalflow.FirstOrDefault(f => f.ApprovalFlowId == setting.ApprovalFlowId);
if (flow == null) return BadRequest("Approval flow not found");
var now = DateTime.Now;
if (dto.Decision.ToLower() == "rejected")
{
if (flow.HoU == currentUserId && status.HouStatus == "Pending")
{
status.HouStatus = "Rejected";
status.HouSubmitDate = now;
}
else if (flow.HoD == currentUserId && status.HodStatus == "Pending" && (!flow.HoU.HasValue || status.HouStatus == "Approved"))
{
status.HodStatus = "Rejected";
status.HodSubmitDate = now;
}
else if (flow.Manager == currentUserId && status.ManagerStatus == "Pending" &&
(!flow.HoU.HasValue || status.HouStatus == "Approved") &&
(!flow.HoD.HasValue || status.HodStatus == "Approved"))
{
status.ManagerStatus = "Rejected";
status.ManagerSubmitDate = now;
}
else if (flow.HR == currentUserId && status.HrStatus == "Pending" &&
(!flow.HoU.HasValue || status.HouStatus == "Approved") &&
(!flow.HoD.HasValue || status.HodStatus == "Approved") &&
(!flow.Manager.HasValue || status.ManagerStatus == "Approved"))
{
status.HrStatus = "Rejected";
status.HrSubmitDate = now;
}
else
{
return BadRequest("Not authorized to reject this request or already decided based on the flow.");
}
}
else if (dto.Decision.ToLower() == "approved")
{
if (flow.HoU == currentUserId && status.HouStatus == "Pending")
{
status.HouStatus = "Approved";
status.HouSubmitDate = now;
}
else if (flow.HoD == currentUserId && status.HodStatus == "Pending" && (!flow.HoU.HasValue || status.HouStatus == "Approved"))
{
status.HodStatus = "Approved";
status.HodSubmitDate = now;
}
else if (flow.Manager == currentUserId && status.ManagerStatus == "Pending" &&
(!flow.HoU.HasValue || status.HouStatus == "Approved") &&
(!flow.HoD.HasValue || status.HodStatus == "Approved"))
{
status.ManagerStatus = "Approved";
status.ManagerSubmitDate = now;
}
else if (flow.HR == currentUserId && status.HrStatus == "Pending" &&
(!flow.HoU.HasValue || status.HouStatus == "Approved") &&
(!flow.HoD.HasValue || status.HodStatus == "Approved") &&
(!flow.Manager.HasValue || status.ManagerStatus == "Approved"))
{
status.HrStatus = "Approved";
status.HrSubmitDate = now;
}
else
{
return BadRequest("Not authorized to approve this request or already decided based on the flow.");
}
}
else
{
return BadRequest("Invalid decision value.");
}
_centralDbContext.SaveChanges();
return Ok(new { message = "Status updated successfully." });
}
#endregion
#region OT Review
[HttpGet("GetOtRecordsByStatusId/{statusId}")]
public IActionResult GetOtRecordsByStatusId(int statusId)
{
var otStatus = _centralDbContext.Otstatus.FirstOrDefault(s => s.StatusId == statusId);
if (otStatus == null)
return NotFound("OT status not found.");
var user = _centralDbContext.Users.FirstOrDefault(u => u.Id == otStatus.UserId);
if (user == null)
return NotFound("User not found.");
var department = _centralDbContext.Departments
.Where(d => d.DepartmentId == user.departmentId)
.Select(d => d.DepartmentName)
.FirstOrDefault();
var otRecords = _centralDbContext.Otregisters
.Include(o => o.Stations)
.Where(o => o.StatusId == statusId)
.Select(o => new
{
o.OvertimeId,
o.OtDate,
o.OfficeFrom,
o.OfficeTo,
o.OfficeBreak,
o.AfterFrom,
o.AfterTo,
o.AfterBreak,
o.StationId,
StationName = o.Stations != null ? o.Stations.StationName : "N/A",
o.OtDescription,
o.OtDays,
o.UserId
})
.ToList();
var result = new
{
userInfo = new
{
fullName = user.FullName,
departmentName = department,
filePath = otStatus.FilePath
},
records = otRecords
};
return Ok(result);
}
[HttpDelete("DeleteOvertimeInOtReview/{id}")]
public IActionResult DeleteOvertimeInOtReview(int id)
{
var record = _centralDbContext.Otregisters.FirstOrDefault(o => o.OvertimeId == id);
if (record == null)
return NotFound(new { message = "Overtime record not found." });
_centralDbContext.Otregisters.Remove(record);
_centralDbContext.SaveChanges();
return Ok(new { message = "Overtime record deleted successfully." });
}
#endregion
}
}

View File

@ -107,6 +107,7 @@ namespace PSTW_CentralSystem.DBContext
public DbSet<OtStatusModel> Otstatus { get; set; }
public DbSet<HrUserSettingModel> Hrusersetting { get; set; }
public DbSet<FlexiHourModel> Flexihour { get; set; }
public DbSet<ApprovalFlowModel> Approvalflow { get; set; }
}
}

View File

@ -531,26 +531,12 @@
<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">HoD Dashboard</span>
<i class="mdi mdi-receipt"></i><span class="hide-menu">Overtime Approval</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="HodDashboard" asp-action="HodApproval" 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">
<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 class="sidebar-link waves-effect waves-dark sidebar-link" asp-area="OTcalculate" asp-controller="ApprovalDashboard" asp-action="Approval" aria-expanded="false">
<i class="mdi mdi-view-dashboard"></i><span class="hide-menu">Pending Approval</span>
</a>
</li>
</ul>
@ -563,11 +549,6 @@
<i class="mdi mdi-receipt"></i><span class="hide-menu">HR 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="HrDashboard" asp-action="OtApproval" aria-expanded="false">
<i class="mdi mdi-view-dashboard"></i><span class="hide-menu">OT Approval</span>
</a>
</li>
<li class="sidebar-item">
<a class="sidebar-link waves-effect waves-dark sidebar-link" asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="Settings" aria-expanded="false">
<i class="mdi mdi-view-dashboard"></i><span class="hide-menu">Settings</span>