-
This commit is contained in:
parent
6d7dc52724
commit
19a9ade3eb
@ -12,15 +12,17 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Controllers
|
|||||||
{
|
{
|
||||||
[Area("OTcalculate")]
|
[Area("OTcalculate")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class HouDashboardController : Controller
|
public class ApprovalDashboardController : Controller
|
||||||
{
|
{
|
||||||
public IActionResult HouApproval()
|
public IActionResult Approval()
|
||||||
{
|
{
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
public IActionResult OtReview()
|
public IActionResult OtReview(int statusId)
|
||||||
{
|
{
|
||||||
|
ViewBag.StatusId = statusId; // If needed in the view
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
32
Areas/OTcalculate/Models/ApprovalFlowModel.cs
Normal file
32
Areas/OTcalculate/Models/ApprovalFlowModel.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -26,5 +26,11 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
|
|||||||
|
|
||||||
[ForeignKey("StateId")]
|
[ForeignKey("StateId")]
|
||||||
public StateModel? State { get; set; }
|
public StateModel? State { get; set; }
|
||||||
|
|
||||||
|
public int? ApprovalFlowId { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey("ApprovalFlowId")]
|
||||||
|
public ApprovalFlowModel? Approvalflow { get; set; }
|
||||||
|
public DateTime? ApprovalUpdate { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,11 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
|
|||||||
[Required]
|
[Required]
|
||||||
public int UserId { get; set; }
|
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
|
// Convert string times to TimeSpan before saving
|
||||||
public TimeSpan? GetOfficeFrom() => ParseTimeSpan(OfficeFrom?.ToString());
|
public TimeSpan? GetOfficeFrom() => ParseTimeSpan(OfficeFrom?.ToString());
|
||||||
public TimeSpan? GetOfficeTo() => ParseTimeSpan(OfficeTo?.ToString());
|
public TimeSpan? GetOfficeTo() => ParseTimeSpan(OfficeTo?.ToString());
|
||||||
|
|||||||
@ -8,29 +8,46 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
|
|||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
public int StatusId { get; set; }
|
public int StatusId { get; set; }
|
||||||
|
|
||||||
[Required]
|
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
|
|
||||||
[Required]
|
|
||||||
public int Month { get; set; }
|
public int Month { get; set; }
|
||||||
|
|
||||||
[Required]
|
|
||||||
public int Year { get; set; }
|
public int Year { get; set; }
|
||||||
|
|
||||||
public DateTime SubmitDate { 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? HodUpdate { get; set; }
|
||||||
|
public string? ManagerUpdate { get; set; }
|
||||||
public string HrStatus { get; set; } = "Pending";
|
|
||||||
|
|
||||||
// JSON array of ApprovalUpdateLog
|
|
||||||
public string? HrUpdate { 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 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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
public string OtDescription { get; set; }
|
public string OtDescription { get; set; }
|
||||||
public string OtDays { get; set; }
|
public string OtDays { get; set; }
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
|
public int? StatusId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ using PSTW_CentralSystem.Areas.OTcalculate.Models;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using ClosedXML.Excel.Drawings;
|
||||||
|
|
||||||
namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
||||||
{
|
{
|
||||||
@ -19,79 +20,133 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
|||||||
int weekendId,
|
int weekendId,
|
||||||
List<CalendarModel> publicHolidays,
|
List<CalendarModel> publicHolidays,
|
||||||
bool isAdminUser = false,
|
bool isAdminUser = false,
|
||||||
byte[]? logoImage = null // This parameter is missing in the call
|
byte[]? logoImage = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var workbook = new XLWorkbook();
|
var workbook = new XLWorkbook();
|
||||||
var worksheet = workbook.Worksheets.Add("Overtime Records");
|
var worksheet = workbook.Worksheets.Add("Overtime Records");
|
||||||
int currentRow = 1;
|
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))
|
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++;
|
currentRow++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(departmentName))
|
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++;
|
||||||
}
|
}
|
||||||
currentRow++; // Add an empty row after header
|
|
||||||
|
|
||||||
// Header titles
|
currentRow++;
|
||||||
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"
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// 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)
|
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);
|
worksheet.Cell(headerRow1, col).Value = "Station";
|
||||||
cell.Value = headers[i];
|
worksheet.Range(headerRow1, col, headerRow2, col).Merge();
|
||||||
cell.Style.Font.Bold = true;
|
col++;
|
||||||
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
|
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)
|
foreach (var r in records)
|
||||||
{
|
{
|
||||||
TimeSpan totalOT = CalculateTotalOT(r);
|
int dataCol = 1;
|
||||||
int totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0);
|
|
||||||
TimeSpan netOT = totalOT - TimeSpan.FromMinutes(totalBreak);
|
|
||||||
|
|
||||||
int col = 1;
|
bool isSameDateAsPrevious = previousDate == r.OtDate.Date;
|
||||||
var dayCell = worksheet.Cell(currentRow, col++);
|
previousDate = r.OtDate.Date;
|
||||||
dayCell.Value = r.OtDate.ToString("ddd");
|
|
||||||
dayCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
|
||||||
dayCell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
|
||||||
dayCell.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
|
||||||
|
|
||||||
var dateCell = worksheet.Cell(currentRow, col++);
|
var dayCell = worksheet.Cell(currentRow, dataCol++);
|
||||||
dateCell.Value = r.OtDate.ToString("yyyy-MM-dd");
|
var dateCell = worksheet.Cell(currentRow, dataCol++);
|
||||||
dateCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
|
||||||
dateCell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
|
||||||
dateCell.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
|
||||||
|
|
||||||
// Apply background color for weekends and public holidays
|
// Check the type of day first
|
||||||
var dayOfWeek = r.OtDate.DayOfWeek;
|
var dayOfWeek = r.OtDate.DayOfWeek;
|
||||||
bool isWeekend = (weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) ||
|
bool isWeekend = (weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) ||
|
||||||
(weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday));
|
(weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday));
|
||||||
bool isPublicHoliday = publicHolidays.Any(h => h.HolidayDate.Date == r.OtDate.Date);
|
bool isPublicHoliday = publicHolidays.Any(h => h.HolidayDate.Date == r.OtDate.Date);
|
||||||
|
|
||||||
|
// Apply color regardless of whether the value is shown
|
||||||
if (isPublicHoliday)
|
if (isPublicHoliday)
|
||||||
{
|
{
|
||||||
dayCell.Style.Fill.BackgroundColor = XLColor.Pink;
|
dayCell.Style.Fill.BackgroundColor = XLColor.Pink;
|
||||||
@ -103,28 +158,50 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
|||||||
dateCell.Style.Fill.BackgroundColor = XLColor.LightBlue;
|
dateCell.Style.Fill.BackgroundColor = XLColor.LightBlue;
|
||||||
}
|
}
|
||||||
|
|
||||||
worksheet.Cell(currentRow, col++).Value = FormatTime(r.OfficeFrom);
|
// Show value only if it's not repeated
|
||||||
worksheet.Cell(currentRow, col++).Value = FormatTime(r.OfficeTo);
|
if (!isSameDateAsPrevious)
|
||||||
worksheet.Cell(currentRow, col++).Value = r.OfficeBreak;
|
{
|
||||||
|
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, dataCol++).Value = FormatTime(r.OfficeFrom);
|
||||||
worksheet.Cell(currentRow, col++).Value = FormatTime(r.AfterTo);
|
worksheet.Cell(currentRow, dataCol++).Value = FormatTime(r.OfficeTo);
|
||||||
worksheet.Cell(currentRow, col++).Value = r.AfterBreak;
|
worksheet.Cell(currentRow, dataCol++).Value = r.OfficeBreak;
|
||||||
|
|
||||||
worksheet.Cell(currentRow, col++).Value = totalOT.ToString(@"hh\:mm");
|
worksheet.Cell(currentRow, dataCol++).Value = FormatTime(r.AfterFrom);
|
||||||
worksheet.Cell(currentRow, col++).Value = totalBreak;
|
worksheet.Cell(currentRow, dataCol++).Value = FormatTime(r.AfterTo);
|
||||||
worksheet.Cell(currentRow, col++).Value = netOT.ToString(@"hh\:mm");
|
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)
|
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 = 1; i <= col; i++)
|
||||||
for (int i = headers.IndexOf("Office From") + 1; i <= headers.Count; i++)
|
|
||||||
{
|
{
|
||||||
var cell = worksheet.Cell(currentRow, i);
|
var cell = worksheet.Cell(currentRow, i);
|
||||||
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
||||||
|
cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
|
||||||
cell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
cell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
||||||
cell.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
cell.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
||||||
}
|
}
|
||||||
@ -132,18 +209,64 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
|||||||
currentRow++;
|
currentRow++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Total row
|
if (records.Any())
|
||||||
|
{
|
||||||
int totalRow = currentRow;
|
int totalRow = currentRow;
|
||||||
worksheet.Cell(totalRow, 1).Value = "TOTAL";
|
worksheet.Cell(totalRow, 1).Value = "TOTAL";
|
||||||
worksheet.Cell(totalRow, 1).Style.Font.Bold = true;
|
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;
|
|
||||||
|
|
||||||
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();
|
var stream = new MemoryStream();
|
||||||
workbook.SaveAs(stream);
|
workbook.SaveAs(stream);
|
||||||
@ -164,5 +287,18 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
|||||||
{
|
{
|
||||||
return time == null || time == TimeSpan.Zero ? "" : time.Value.ToString(@"hh\:mm");
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
163
Areas/OTcalculate/Views/ApprovalDashboard/Approval.cshtml
Normal file
163
Areas/OTcalculate/Views/ApprovalDashboard/Approval.cshtml
Normal 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>
|
||||||
|
|
||||||
|
|
||||||
226
Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml
Normal file
226
Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml
Normal 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>
|
||||||
|
}
|
||||||
@ -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>
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
@{
|
|
||||||
ViewData["Title"] = "Overtime Approval";
|
|
||||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -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>
|
|
||||||
}
|
|
||||||
@ -1,10 +1,28 @@
|
|||||||
@{
|
@{
|
||||||
|
|
||||||
ViewBag.Title = "User Settings";
|
ViewBag.Title = "User Settings";
|
||||||
|
|
||||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
@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="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-6 col-md-6 col-lg-3">
|
<div class="col-6 col-md-6 col-lg-3">
|
||||||
@ -53,14 +71,20 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" :class="{ 'bg-purple text-white': activeTab === 'state', 'bg-light text-dark': activeTab !== 'state' }"
|
<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')">
|
style="border: 1px solid #ddd;" v-on:click="changeTab('state')">
|
||||||
Region Update
|
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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content mt-3">
|
<div class="tab-content mt-3">
|
||||||
<div v-if="activeTab === 'flexi'" class="card shadow-sm">
|
<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">
|
<form v-on:submit.prevent="updateFlexiHours" data-aos="fade-right">
|
||||||
<div class="d-flex justify-content-center align-items-center mt-3">
|
<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">
|
<div class="card-body d-flex justify-content-center align-items-center gap-3">
|
||||||
@ -96,15 +120,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="activeTab === 'state'" class="card shadow-sm">
|
<div v-if="activeTab === 'state'" class="card shadow-sm">
|
||||||
<div class="card-body d-flex justify-content-center align-items-center gap-3">
|
<div class="card m-3">
|
||||||
<label class="mb-0">Select State:</label>
|
<div class="d-flex justify-content-center align-items-center gap-4 flex-wrap mt-4">
|
||||||
<select class="form-select" v-model="selectedStateAll" style="max-width: 150px;">
|
<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 disabled value="">--Select--</option>
|
||||||
<option v-for="state in stateList" :value="state.stateId">{{ state.stateName }}</option>
|
<option v-for="state in stateList" :value="state.stateId">{{ state.stateName }}</option>
|
||||||
</select>
|
</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>
|
||||||
|
|
||||||
|
<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">
|
<div class="card-body">
|
||||||
<table id="stateUpdateTable" class="table table-bordered table-hover table-striped">
|
<table id="stateUpdateTable" class="table table-bordered table-hover table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
@ -112,17 +157,170 @@
|
|||||||
<th>Full Name</th>
|
<th>Full Name</th>
|
||||||
<th>Department</th>
|
<th>Department</th>
|
||||||
<th>Current State</th>
|
<th>Current State</th>
|
||||||
|
<th>Current Flow</th>
|
||||||
<th class="text-center">Select</th>
|
<th class="text-center">Select</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body text-center">
|
<div class="d-flex justify-content-center gap-2 my-3">
|
||||||
<button class="btn btn-danger" v-on:click="clearStateSelection">Clear Selection</button>
|
<button class="btn btn-danger" v-on:click="clearAllSelectionsStateFlow" style="font-size: 0.900rem; margin-left: 10px;">
|
||||||
<button class="btn btn-success ms-3" v-on:click="saveState">Update User State</button>
|
Clear
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" v-on:click="handleCombinedUpdate" style="font-size: 0.900rem;">
|
||||||
|
Update State & Flow
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
@ -145,14 +343,37 @@
|
|||||||
selectedUsersState: [],
|
selectedUsersState: [],
|
||||||
stateUserList: [],
|
stateUserList: [],
|
||||||
userDatatable: null,
|
userDatatable: null,
|
||||||
stateDatatable: null
|
stateDatatable: null,
|
||||||
|
approvalFlows: [],
|
||||||
|
approvalFlowList: [],
|
||||||
|
selectedApprovalFlowId: '',
|
||||||
|
approvalFlow: {
|
||||||
|
approvalName: '',
|
||||||
|
hou: '',
|
||||||
|
hod: '',
|
||||||
|
manager: '',
|
||||||
|
hr: ''
|
||||||
|
},
|
||||||
|
editFlow: {
|
||||||
|
approvalId: '',
|
||||||
|
approvalName: '',
|
||||||
|
hou: '',
|
||||||
|
hod: '',
|
||||||
|
manager: '',
|
||||||
|
hr: ''
|
||||||
|
},
|
||||||
|
allUsers: []
|
||||||
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
console.log("Vue App Mounted Successfully");
|
console.log("Vue App Mounted Successfully");
|
||||||
this.fetchFlexiHours();
|
this.fetchFlexiHours();
|
||||||
this.fetchStates();
|
this.fetchStates();
|
||||||
this.changeTab('flexi'); // Initialize the default tab
|
this.changeTab('flexi');
|
||||||
|
this.fetchAllUsers();
|
||||||
|
this.fetchUsersState();
|
||||||
|
this.fetchApprovalFlows();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
changeTab(tab) {
|
changeTab(tab) {
|
||||||
@ -165,11 +386,13 @@
|
|||||||
this.initiateTable();
|
this.initiateTable();
|
||||||
}
|
}
|
||||||
} else if (tab === 'state') {
|
} else if (tab === 'state') {
|
||||||
this.clearStateSelection();
|
this.clearAllSelectionsStateFlow();
|
||||||
if (!this.stateUserList.length) {
|
if (!this.stateUserList.length) {
|
||||||
this.fetchUsersState().then(() => this.initiateStateTable());
|
this.fetchUsersState().then(() => {
|
||||||
}
|
this.initiateStateTable();
|
||||||
else{
|
this.fetchApprovalFlows(); // Ensure approval flows are fetched on tab change
|
||||||
|
});
|
||||||
|
} else {
|
||||||
this.initiateStateTable();
|
this.initiateStateTable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,7 +400,7 @@
|
|||||||
|
|
||||||
async updateUserSettings(apiUrl, selectedUsers, selectedValue, successMessage, clearCallback, fetchCallback, valueKey) {
|
async updateUserSettings(apiUrl, selectedUsers, selectedValue, successMessage, clearCallback, fetchCallback, valueKey) {
|
||||||
if (!selectedUsers.length || !selectedValue) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,7 +426,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
async fetchFlexiHours() {
|
async fetchFlexiHours() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/OvertimeAPI/GetFlexiHours");
|
const response = await fetch("/OvertimeAPI/GetFlexiHours");
|
||||||
@ -286,7 +508,7 @@
|
|||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
self.selectedUsers.push(userId);
|
self.selectedUsers.push(userId);
|
||||||
} else {
|
} else {
|
||||||
self.selectedUsers = self.selectedUsers.filter(id => id !== userId);
|
self.selectedUsers= self.selectedUsers.filter(id => id !== userId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -305,6 +527,7 @@
|
|||||||
{ data: "fullName" },
|
{ data: "fullName" },
|
||||||
{ data: "departmentName" },
|
{ data: "departmentName" },
|
||||||
{ data: "state", defaultContent: "N/A" },
|
{ data: "state", defaultContent: "N/A" },
|
||||||
|
{ data: "approvalName", defaultContent: "N/A" },
|
||||||
{
|
{
|
||||||
data: "id",
|
data: "id",
|
||||||
className: "text-center",
|
className: "text-center",
|
||||||
@ -326,42 +549,72 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
clearForm() {
|
async handleCombinedUpdate() {
|
||||||
this.selectedFlexiHourId = '';
|
const hasUsers = this.selectedUsersState.length > 0;
|
||||||
this.selectedUsers = [];
|
const hasState = this.selectedStateAll !== '';
|
||||||
if (this.userDatatable) {
|
const hasFlow = this.selectedApprovalFlowId !== '';
|
||||||
this.userDatatable.rows().every(function () {
|
|
||||||
$(this.node()).find('input[type="checkbox"]').prop('checked', false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
clearStateSelection() {
|
if (!hasUsers && !hasState && !hasFlow) {
|
||||||
this.selectedUsersState = [];
|
alert("Please choose users, a state, or an approval flow to proceed.");
|
||||||
if (this.stateDatatable) {
|
this.clearAllSelectionsStateFlow();
|
||||||
this.stateDatatable.rows().every(function () {
|
return;
|
||||||
$(this.node()).find('input[type="checkbox"]').prop('checked', false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
async saveState() {
|
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(
|
await this.updateUserSettings(
|
||||||
"/OvertimeAPI/UpdateUserStates",
|
"/OvertimeAPI/UpdateUserStates",
|
||||||
this.selectedUsersState,
|
this.selectedUsersState,
|
||||||
this.selectedStateAll,
|
this.selectedStateAll,
|
||||||
"State updated successfully!",
|
"State updated successfully!",
|
||||||
this.clearStateSelection,
|
() => {}, // Don't clear yet
|
||||||
this.fetchUsersState, // This will fetch the updated state users after the update
|
() => {}, // Don't fetch yet
|
||||||
"StateId"
|
"StateId"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch the updated state list to ensure the page reflects the new state data
|
if (hasFlow) {
|
||||||
await this.fetchUsersState(); // This fetch will make sure stateUserList is updated
|
const payload = this.selectedUsersState.map(userId => ({
|
||||||
this.initiateStateTable(); // Reinitialize the state table with new data
|
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 updateFlexiHours() {
|
async updateFlexiHours() {
|
||||||
await this.updateUserSettings(
|
await this.updateUserSettings(
|
||||||
"/OvertimeAPI/UpdateUserFlexiHours",
|
"/OvertimeAPI/UpdateUserFlexiHours",
|
||||||
@ -372,7 +625,148 @@
|
|||||||
this.fetchUsers,
|
this.fetchUsers,
|
||||||
"FlexiHourId"
|
"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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -107,6 +107,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -126,6 +137,7 @@
|
|||||||
calendarUpdateDate: null,
|
calendarUpdateDate: null,
|
||||||
flexiHourUpdateDate: null,
|
flexiHourUpdateDate: null,
|
||||||
regionUpdateDate: null,
|
regionUpdateDate: null,
|
||||||
|
approvalFlowUpdateDate: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -144,6 +156,7 @@
|
|||||||
this.calendarUpdateDate = data.calendarUpdateDate;
|
this.calendarUpdateDate = data.calendarUpdateDate;
|
||||||
this.flexiHourUpdateDate = data.flexiHourUpdateDate;
|
this.flexiHourUpdateDate = data.flexiHourUpdateDate;
|
||||||
this.regionUpdateDate = data.regionUpdateDate;
|
this.regionUpdateDate = data.regionUpdateDate;
|
||||||
|
this.approvalFlowUpdateDate = data.approvalFlowUpdateDate;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -153,6 +153,9 @@
|
|||||||
|
|
||||||
return { label, value: totalMinutes };
|
return { label, value: totalMinutes };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
previousPage: document.referrer
|
||||||
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -434,7 +437,7 @@
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert("Overtime record updated successfully!");
|
alert("Overtime record updated successfully!");
|
||||||
window.location.href = '/OTcalculate/Overtime/OtRecords';
|
window.location.href = this.previousPage;
|
||||||
} else {
|
} else {
|
||||||
alert("Failed to update overtime record.");
|
alert("Failed to update overtime record.");
|
||||||
}
|
}
|
||||||
@ -445,7 +448,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
goBack() {
|
goBack() {
|
||||||
window.location.href = "/OTcalculate/Overtime/OtRecords";
|
window.location.href = this.previousPage;
|
||||||
},
|
},
|
||||||
clearOfficeHours() {
|
clearOfficeHours() {
|
||||||
this.editForm.officeFrom = "";
|
this.editForm.officeFrom = "";
|
||||||
|
|||||||
@ -160,8 +160,8 @@
|
|||||||
<div class="mt-3 d-flex flex-wrap gap-2">
|
<div class="mt-3 d-flex flex-wrap gap-2">
|
||||||
<button class="btn btn-primary btn-sm" v-on:click="printPdf"><i class="bi bi-printer"></i> Print</button>
|
<button class="btn btn-primary btn-sm" v-on:click="printPdf"><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-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" 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" :disabled="hasSubmitted" v-on:click="openSubmitModal"><i class="bi bi-send"></i>{{ hasSubmitted ? ' Submitted' : ' Submit' }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="submitModal" tabindex="-1" aria-labelledby="submitModalLabel" aria-hidden="true">
|
<div class="modal fade" id="submitModal" tabindex="-1" aria-labelledby="submitModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
@ -208,10 +208,10 @@
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
selectedMonth() {
|
selectedMonth() {
|
||||||
this.checkSubmissionStatus();
|
this.getSubmissionStatus(); // Renamed method call
|
||||||
},
|
},
|
||||||
selectedYear() {
|
selectedYear() {
|
||||||
this.checkSubmissionStatus();
|
this.getSubmissionStatus(); // Renamed method call
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -243,7 +243,7 @@
|
|||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.initUserAndRecords();
|
await this.initUserAndRecords();
|
||||||
await this.checkSubmissionStatus();
|
await this.getSubmissionStatus();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async initUserAndRecords() {
|
async initUserAndRecords() {
|
||||||
@ -288,12 +288,21 @@
|
|||||||
console.error("Records fetch error:", err);
|
console.error("Records fetch error:", err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async checkSubmissionStatus() {
|
async getSubmissionStatus() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/OvertimeAPI/CheckOvertimeSubmitted/${this.userId}/${this.selectedMonth}/${this.selectedYear}`);
|
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();
|
this.hasSubmitted = await res.json();
|
||||||
} catch (err) {
|
} 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) {
|
toggleDescription(index) {
|
||||||
@ -424,6 +433,8 @@
|
|||||||
const modalInstance = bootstrap.Modal.getInstance(modalEl);
|
const modalInstance = bootstrap.Modal.getInstance(modalEl);
|
||||||
modalInstance.hide();
|
modalInstance.hide();
|
||||||
|
|
||||||
|
await this.getSubmissionStatus(); // Call this to refresh the hasSubmitted status
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
alert('Submission failed.');
|
alert('Submission failed.');
|
||||||
}
|
}
|
||||||
@ -433,7 +444,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
|||||||
@ -205,6 +205,9 @@
|
|||||||
// Fetch user's state and public holidays after fetching user info
|
// Fetch user's state and public holidays after fetching user info
|
||||||
if (this.userId) {
|
if (this.userId) {
|
||||||
await this.fetchUserStateAndHolidays();
|
await this.fetchUserStateAndHolidays();
|
||||||
|
if (this.userState) {
|
||||||
|
this.statusId = this.userState.defaultStatusId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@ -356,7 +359,8 @@
|
|||||||
stationId: this.isPSTWAIR ? parseInt(this.selectedAirStation) : null,
|
stationId: this.isPSTWAIR ? parseInt(this.selectedAirStation) : null,
|
||||||
otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '),
|
otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '),
|
||||||
otDays: this.detectedDayType, // Use the auto-detected day type
|
otDays: this.detectedDayType, // Use the auto-detected day type
|
||||||
userId: this.userId
|
userId: this.userId,
|
||||||
|
statusId: this.statusId || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -378,9 +382,10 @@
|
|||||||
this.clearForm();
|
this.clearForm();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error adding overtime:", 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() {
|
updateDayType() {
|
||||||
if (!this.selectedDate || !this.userState) {
|
if (!this.selectedDate || !this.userState) {
|
||||||
this.detectedDayType = "";
|
this.detectedDayType = "";
|
||||||
|
|||||||
@ -2,3 +2,148 @@
|
|||||||
ViewData["Title"] = "Overtime Status";
|
ViewData["Title"] = "Overtime Status";
|
||||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
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>
|
||||||
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ using QuestPDF.Helpers;
|
|||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using PSTW_CentralSystem.Areas.OTcalculate.Services;
|
using PSTW_CentralSystem.Areas.OTcalculate.Services;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using DocumentFormat.OpenXml.InkML;
|
||||||
|
|
||||||
|
|
||||||
namespace PSTW_CentralSystem.Controllers.API
|
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 latestCalendarUpdate = _centralDbContext.Holidays.OrderByDescending(c => c.LastUpdated).FirstOrDefault()?.LastUpdated;
|
||||||
var latestFlexiHourUpdate = _centralDbContext.Hrusersetting.OrderByDescending(r => r.FlexiHourUpdate).FirstOrDefault()?.FlexiHourUpdate;
|
var latestFlexiHourUpdate = _centralDbContext.Hrusersetting.OrderByDescending(r => r.FlexiHourUpdate).FirstOrDefault()?.FlexiHourUpdate;
|
||||||
var latestRegionUpdate = _centralDbContext.Hrusersetting.OrderByDescending(c => c.StateUpdate).FirstOrDefault()?.StateUpdate;
|
var latestRegionUpdate = _centralDbContext.Hrusersetting.OrderByDescending(c => c.StateUpdate).FirstOrDefault()?.StateUpdate;
|
||||||
|
var latestApprovalFlowUpdate = _centralDbContext.Hrusersetting.OrderByDescending(c => c.ApprovalUpdate).FirstOrDefault()?.ApprovalUpdate;
|
||||||
|
|
||||||
var updateDates = new
|
var updateDates = new
|
||||||
{
|
{
|
||||||
rateUpdateDate = latestRateUpdate.HasValue ? latestRateUpdate.Value.ToString("dd MMMM yyyy") : null,
|
rateUpdateDate = latestRateUpdate.HasValue ? latestRateUpdate.Value.ToString("dd MMMM yyyy") : null,
|
||||||
calendarUpdateDate = latestCalendarUpdate.HasValue ? latestCalendarUpdate.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,
|
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);
|
return Json(updateDates);
|
||||||
@ -161,8 +164,8 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region FlexiHour State
|
#region FlexiHour State Approval
|
||||||
private async Task UpdateOrInsertUserSettingAsync(int userId, int? flexiHourId = null, int? stateId = null)
|
private async Task UpdateOrInsertUserSettingAsync(int userId, int? flexiHourId = null, int? stateId = null, int? approvalFlowId = null)
|
||||||
{
|
{
|
||||||
var setting = await _centralDbContext.Hrusersetting
|
var setting = await _centralDbContext.Hrusersetting
|
||||||
.FirstOrDefaultAsync(h => h.UserId == userId);
|
.FirstOrDefaultAsync(h => h.UserId == userId);
|
||||||
@ -181,6 +184,12 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
setting.StateUpdate = DateTime.Now;
|
setting.StateUpdate = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (approvalFlowId.HasValue)
|
||||||
|
{
|
||||||
|
setting.ApprovalFlowId = approvalFlowId;
|
||||||
|
setting.ApprovalUpdate = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
_centralDbContext.Hrusersetting.Update(setting);
|
_centralDbContext.Hrusersetting.Update(setting);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -191,7 +200,9 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
FlexiHourId = flexiHourId,
|
FlexiHourId = flexiHourId,
|
||||||
FlexiHourUpdate = flexiHourId.HasValue ? DateTime.Now : null,
|
FlexiHourUpdate = flexiHourId.HasValue ? DateTime.Now : null,
|
||||||
StateId = stateId,
|
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);
|
_centralDbContext.Hrusersetting.Add(newSetting);
|
||||||
}
|
}
|
||||||
@ -285,6 +296,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
|
|
||||||
var hrUserSettings = await _centralDbContext.Hrusersetting
|
var hrUserSettings = await _centralDbContext.Hrusersetting
|
||||||
.Include(h => h.State)
|
.Include(h => h.State)
|
||||||
|
.Include(h => h.Approvalflow)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var result = users.Select(u =>
|
var result = users.Select(u =>
|
||||||
@ -295,7 +307,8 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
u.Id,
|
u.Id,
|
||||||
u.FullName,
|
u.FullName,
|
||||||
DepartmentName = u.Department != null ? u.Department.DepartmentName : "N/A",
|
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();
|
}).ToList();
|
||||||
|
|
||||||
@ -317,8 +330,23 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
{
|
{
|
||||||
foreach (var update in updates)
|
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();
|
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
|
#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
|
#region Calendar
|
||||||
[HttpGet("GetStatesName")]
|
[HttpGet("GetStatesName")]
|
||||||
@ -574,6 +795,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
public async Task<IActionResult> AddOvertimeAsync([FromBody] OvertimeRequestDto request)
|
public async Task<IActionResult> AddOvertimeAsync([FromBody] OvertimeRequestDto request)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("AddOvertimeAsync called.");
|
_logger.LogInformation("AddOvertimeAsync called.");
|
||||||
|
_logger.LogInformation("Received request: {@Request}", request);
|
||||||
|
|
||||||
if (request == null)
|
if (request == null)
|
||||||
{
|
{
|
||||||
@ -618,6 +840,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
OtDescription = request.OtDescription,
|
OtDescription = request.OtDescription,
|
||||||
OtDays = request.OtDays,
|
OtDays = request.OtDays,
|
||||||
UserId = request.UserId,
|
UserId = request.UserId,
|
||||||
|
StatusId = request.StatusId // Pass StatusId as it is, which can be null
|
||||||
};
|
};
|
||||||
|
|
||||||
_centralDbContext.Otregisters.Add(newRecord);
|
_centralDbContext.Otregisters.Add(newRecord);
|
||||||
@ -628,10 +851,11 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error registering overtime.");
|
_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}")]
|
[HttpGet("GetUserStateAndHolidays/{userId}")]
|
||||||
public async Task<IActionResult> GetUserStateAndHolidaysAsync(int 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(
|
var stream = _excelService.GenerateOvertimeExcel(
|
||||||
mergedRecords,
|
mergedRecords,
|
||||||
departmentId,
|
departmentId,
|
||||||
@ -910,9 +1137,10 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
weekendId,
|
weekendId,
|
||||||
publicHolidays,
|
publicHolidays,
|
||||||
isAdminUser: IsAdmin(userId),
|
isAdminUser: IsAdmin(userId),
|
||||||
logoImage: null
|
logoImage: logoImage
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
$"OvertimeRecords_{year}_{month}.xlsx");
|
$"OvertimeRecords_{year}_{month}.xlsx");
|
||||||
}
|
}
|
||||||
@ -923,7 +1151,6 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
if (model.File == null || model.File.Length == 0)
|
if (model.File == null || model.File.Length == 0)
|
||||||
return BadRequest("No file uploaded.");
|
return BadRequest("No file uploaded.");
|
||||||
|
|
||||||
// Get userId from the login token
|
|
||||||
var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId))
|
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId))
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
@ -944,31 +1171,49 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
|
|
||||||
var relativePath = Path.Combine("Media", "Overtime", uniqueFileName).Replace("\\", "/");
|
var relativePath = Path.Combine("Media", "Overtime", uniqueFileName).Replace("\\", "/");
|
||||||
|
|
||||||
// Check if record exists for same month/year/user
|
// Create a NEW OtStatusModel for the resubmission
|
||||||
var existingStatus = _centralDbContext.Otstatus.FirstOrDefault(x => x.UserId == userId && x.Month == model.Month && x.Year == model.Year);
|
var statusModel = new OtStatusModel
|
||||||
|
|
||||||
if (existingStatus != null)
|
|
||||||
{
|
|
||||||
existingStatus.FilePath = relativePath;
|
|
||||||
existingStatus.SubmitDate = DateTime.Now;
|
|
||||||
_centralDbContext.Otstatus.Update(existingStatus);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var newStatus = new OtStatusModel
|
|
||||||
{
|
{
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
Month = model.Month,
|
Month = model.Month,
|
||||||
Year = model.Year,
|
Year = model.Year,
|
||||||
FilePath = relativePath,
|
FilePath = relativePath,
|
||||||
SubmitDate = DateTime.Now,
|
SubmitDate = DateTime.Now,
|
||||||
|
HouStatus = "Pending",
|
||||||
HodStatus = "Pending",
|
HodStatus = "Pending",
|
||||||
|
ManagerStatus = "Pending",
|
||||||
HrStatus = "Pending"
|
HrStatus = "Pending"
|
||||||
};
|
};
|
||||||
_centralDbContext.Otstatus.Add(newStatus);
|
|
||||||
|
_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)
|
||||||
|
{
|
||||||
|
record.StatusId = statusModel.StatusId;
|
||||||
|
}
|
||||||
|
|
||||||
|
_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)
|
||||||
|
{
|
||||||
|
userSetting.ApprovalUpdate = DateTime.Now;
|
||||||
|
_centralDbContext.Hrusersetting.Update(userSetting);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _centralDbContext.SaveChangesAsync();
|
await _centralDbContext.SaveChangesAsync();
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -979,15 +1224,47 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("CheckOvertimeSubmitted/{userId}/{month}/{year}")]
|
[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))
|
try
|
||||||
return BadRequest("Invalid userId.");
|
{
|
||||||
|
// 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
|
if (latestStatus == null)
|
||||||
.AnyAsync(s => s.UserId == parsedUserId && s.Month == month && s.Year == year);
|
{
|
||||||
|
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
|
#endregion
|
||||||
@ -1051,9 +1328,322 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
}
|
}
|
||||||
#endregion
|
#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
|
#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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,6 +107,7 @@ namespace PSTW_CentralSystem.DBContext
|
|||||||
public DbSet<OtStatusModel> Otstatus { get; set; }
|
public DbSet<OtStatusModel> Otstatus { get; set; }
|
||||||
public DbSet<HrUserSettingModel> Hrusersetting { get; set; }
|
public DbSet<HrUserSettingModel> Hrusersetting { get; set; }
|
||||||
public DbSet<FlexiHourModel> Flexihour { get; set; }
|
public DbSet<FlexiHourModel> Flexihour { get; set; }
|
||||||
|
public DbSet<ApprovalFlowModel> Approvalflow { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -531,26 +531,12 @@
|
|||||||
<a class="sidebar-link has-arrow waves-effect waves-dark"
|
<a class="sidebar-link has-arrow waves-effect waves-dark"
|
||||||
href="javascript:void(0)"
|
href="javascript:void(0)"
|
||||||
aria-expanded="false">
|
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>
|
</a>
|
||||||
<ul aria-expanded="false" class="collapse first-level">
|
<ul aria-expanded="false" class="collapse first-level">
|
||||||
<li class="sidebar-item">
|
<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">
|
<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">OT Approval</span>
|
<i class="mdi mdi-view-dashboard"></i><span class="hide-menu">Pending 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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -563,11 +549,6 @@
|
|||||||
<i class="mdi mdi-receipt"></i><span class="hide-menu">HR Dashboard</span>
|
<i class="mdi mdi-receipt"></i><span class="hide-menu">HR Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
<ul aria-expanded="false" class="collapse first-level">
|
<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">
|
<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">
|
<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>
|
<i class="mdi mdi-view-dashboard"></i><span class="hide-menu">Settings</span>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user