From 19a9ade3eba217288ad86fae2339d85d34f59cff Mon Sep 17 00:00:00 2001 From: Naz <2022755409@student.uitm.edu.my> Date: Tue, 13 May 2025 17:13:30 +0800 Subject: [PATCH] - --- ...ller.cs => ApprovalDashboardController.cs} | 8 +- .../Controllers/HouDashboardController.cs | 26 - Areas/OTcalculate/Models/ApprovalFlowModel.cs | 32 + .../OTcalculate/Models/HrUserSettingModel.cs | 8 +- Areas/OTcalculate/Models/OtRegisterModel.cs | 5 + Areas/OTcalculate/Models/OtStatusModel.cs | 43 +- .../OTcalculate/Models/OvertimeRequestDto.cs | 1 + .../Services/OvertimeExcelService.cs | 278 ++++++-- .../Views/ApprovalDashboard/Approval.cshtml | 163 +++++ .../Views/ApprovalDashboard/OtReview.cshtml | 226 ++++++ .../Views/HodDashboard/HodApproval.cshtml | 68 -- .../Views/HodDashboard/OtReview.cshtml | 169 ----- .../Views/HouDashboard/HouApproval.cshtml | 5 - .../Views/HouDashboard/OtReview.cshtml | 169 ----- .../Views/HrDashboard/HrUserSetting.cshtml | 532 ++++++++++++-- .../Views/HrDashboard/Settings.cshtml | 13 + .../Views/Overtime/EditOvertime.cshtml | 7 +- .../Views/Overtime/OtRecords.cshtml | 26 +- .../Views/Overtime/OtRegister.cshtml | 9 +- .../Views/Overtime/OtStatus.cshtml | 145 ++++ Controllers/API/OvertimeAPI.cs | 660 +++++++++++++++++- DBContext/CentralSystemContext.cs | 1 + Views/Shared/_Layout.cshtml | 25 +- ...6a-6d22b24a6b8a_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...597040c6782_OvertimeRecords_2025_4 (1).pdf | Bin 85435 -> 0 bytes ...e0-314d4fb6bfd2_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...15-096da2363557_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...16-1a29cdb5ce4d_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...a4-ba9cc1663d6c_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...4d-c295f64e121e_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...2e-f00cf87e87f3_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...c0-b06772909c92_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...d0-95db36f5632b_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...5b-557df7f0356f_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...8a1b0e8918f_OvertimeRecords_2025_4 (1).pdf | Bin 85435 -> 0 bytes ...ba-4f8b16fa09c1_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...1a-25f9efcb2d2d_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...ca-341c2ffe4523_OvertimeRecords_2025_4.pdf | Bin 84468 -> 0 bytes 38 files changed, 1956 insertions(+), 663 deletions(-) rename Areas/OTcalculate/Controllers/{HodDashboardController.cs => ApprovalDashboardController.cs} (70%) delete mode 100644 Areas/OTcalculate/Controllers/HouDashboardController.cs create mode 100644 Areas/OTcalculate/Models/ApprovalFlowModel.cs create mode 100644 Areas/OTcalculate/Views/ApprovalDashboard/Approval.cshtml create mode 100644 Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml delete mode 100644 Areas/OTcalculate/Views/HodDashboard/HodApproval.cshtml delete mode 100644 Areas/OTcalculate/Views/HodDashboard/OtReview.cshtml delete mode 100644 Areas/OTcalculate/Views/HouDashboard/HouApproval.cshtml delete mode 100644 Areas/OTcalculate/Views/HouDashboard/OtReview.cshtml create mode 100644 wwwroot/Media/Overtime/29664d53-5eff-4692-856a-6d22b24a6b8a_OvertimeRecords_2025_5.pdf delete mode 100644 wwwroot/Media/Overtime/4d829513-08d1-455f-b13e-9597040c6782_OvertimeRecords_2025_4 (1).pdf create mode 100644 wwwroot/Media/Overtime/5818d6a1-5987-4cf7-98e0-314d4fb6bfd2_OvertimeRecords_2025_5.pdf create mode 100644 wwwroot/Media/Overtime/6109192d-0879-42d6-8a15-096da2363557_OvertimeRecords_2025_5.pdf create mode 100644 wwwroot/Media/Overtime/656c247b-7d09-462b-8016-1a29cdb5ce4d_OvertimeRecords_2025_5.pdf create mode 100644 wwwroot/Media/Overtime/6d6340c8-0aa7-44f6-a4a4-ba9cc1663d6c_OvertimeRecords_2025_5.pdf create mode 100644 wwwroot/Media/Overtime/796771bd-bda3-4b66-b64d-c295f64e121e_OvertimeRecords_2025_5.pdf create mode 100644 wwwroot/Media/Overtime/951938e6-1f43-47b0-b02e-f00cf87e87f3_OvertimeRecords_2025_5.pdf create mode 100644 wwwroot/Media/Overtime/af04c033-873e-4762-a0c0-b06772909c92_OvertimeRecords_2025_5.pdf create mode 100644 wwwroot/Media/Overtime/c23ac69a-dfc3-4536-9ad0-95db36f5632b_OvertimeRecords_2025_5.pdf create mode 100644 wwwroot/Media/Overtime/c6335a89-f73c-4817-815b-557df7f0356f_OvertimeRecords_2025_5.pdf delete mode 100644 wwwroot/Media/Overtime/cedf17a4-a813-43ac-a7de-a8a1b0e8918f_OvertimeRecords_2025_4 (1).pdf create mode 100644 wwwroot/Media/Overtime/e1ec27e3-6ff3-4315-beba-4f8b16fa09c1_OvertimeRecords_2025_5.pdf create mode 100644 wwwroot/Media/Overtime/f1ba9abe-3813-44f6-921a-25f9efcb2d2d_OvertimeRecords_2025_5.pdf delete mode 100644 wwwroot/Media/Overtime/f93fe1d8-87f4-45bd-85ca-341c2ffe4523_OvertimeRecords_2025_4.pdf diff --git a/Areas/OTcalculate/Controllers/HodDashboardController.cs b/Areas/OTcalculate/Controllers/ApprovalDashboardController.cs similarity index 70% rename from Areas/OTcalculate/Controllers/HodDashboardController.cs rename to Areas/OTcalculate/Controllers/ApprovalDashboardController.cs index ff10d85..e4ebfa6 100644 --- a/Areas/OTcalculate/Controllers/HodDashboardController.cs +++ b/Areas/OTcalculate/Controllers/ApprovalDashboardController.cs @@ -12,15 +12,17 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Controllers { [Area("OTcalculate")] [Authorize] - public class HouDashboardController : Controller + public class ApprovalDashboardController : Controller { - public IActionResult HouApproval() + public IActionResult Approval() { return View(); } - public IActionResult OtReview() + public IActionResult OtReview(int statusId) { + ViewBag.StatusId = statusId; // If needed in the view return View(); } + } } diff --git a/Areas/OTcalculate/Controllers/HouDashboardController.cs b/Areas/OTcalculate/Controllers/HouDashboardController.cs deleted file mode 100644 index 0f1f920..0000000 --- a/Areas/OTcalculate/Controllers/HouDashboardController.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/Areas/OTcalculate/Models/ApprovalFlowModel.cs b/Areas/OTcalculate/Models/ApprovalFlowModel.cs new file mode 100644 index 0000000..51f679e --- /dev/null +++ b/Areas/OTcalculate/Models/ApprovalFlowModel.cs @@ -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; } + } + + + +} diff --git a/Areas/OTcalculate/Models/HrUserSettingModel.cs b/Areas/OTcalculate/Models/HrUserSettingModel.cs index 0b55805..7650519 100644 --- a/Areas/OTcalculate/Models/HrUserSettingModel.cs +++ b/Areas/OTcalculate/Models/HrUserSettingModel.cs @@ -25,6 +25,12 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models public int? StateId { get; set; } [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; } } } diff --git a/Areas/OTcalculate/Models/OtRegisterModel.cs b/Areas/OTcalculate/Models/OtRegisterModel.cs index c46bfaf..2e19e74 100644 --- a/Areas/OTcalculate/Models/OtRegisterModel.cs +++ b/Areas/OTcalculate/Models/OtRegisterModel.cs @@ -31,6 +31,11 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models [Required] public int UserId { get; set; } + public int? StatusId { get; set; } + + [ForeignKey("StatusId")] + public virtual OtStatusModel? Otstatus { get; set; } + // Convert string times to TimeSpan before saving public TimeSpan? GetOfficeFrom() => ParseTimeSpan(OfficeFrom?.ToString()); public TimeSpan? GetOfficeTo() => ParseTimeSpan(OfficeTo?.ToString()); diff --git a/Areas/OTcalculate/Models/OtStatusModel.cs b/Areas/OTcalculate/Models/OtStatusModel.cs index 278dd1b..a1f450b 100644 --- a/Areas/OTcalculate/Models/OtStatusModel.cs +++ b/Areas/OTcalculate/Models/OtStatusModel.cs @@ -8,29 +8,46 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models { [Key] public int StatusId { get; set; } - - [Required] public int UserId { get; set; } - - [Required] public int Month { get; set; } - - [Required] public int Year { get; set; } - public DateTime SubmitDate { get; set; } - public string HodStatus { get; set; } = "Pending"; + public string? HouStatus { get; set; } + public string? HodStatus { get; set; } + public string? ManagerStatus { get; set; } + public string? HrStatus { get; set; } - // JSON array of ApprovalUpdateLog + public string? HouUpdate { get; set; } public string? HodUpdate { get; set; } - - public string HrStatus { get; set; } = "Pending"; - - // JSON array of ApprovalUpdateLog + public string? ManagerUpdate { get; set; } public string? HrUpdate { get; set; } + public DateTime? HouSubmitDate { get; set; } + public DateTime? HodSubmitDate { get; set; } + public DateTime? ManagerSubmitDate { get; set; } + public DateTime? HrSubmitDate { get; set; } + public string? FilePath { get; set; } + + + public bool Updated => + !string.IsNullOrEmpty(HouUpdate) || + !string.IsNullOrEmpty(HodUpdate) || + !string.IsNullOrEmpty(ManagerUpdate) || + !string.IsNullOrEmpty(HrUpdate); + + } + public class StatusDto + { + public int StatusId { get; set; } } + public class UpdateStatusDto + { + public int StatusId { get; set; } + public string Decision { get; set; } + } + + } diff --git a/Areas/OTcalculate/Models/OvertimeRequestDto.cs b/Areas/OTcalculate/Models/OvertimeRequestDto.cs index 109ef5f..bcd46d4 100644 --- a/Areas/OTcalculate/Models/OvertimeRequestDto.cs +++ b/Areas/OTcalculate/Models/OvertimeRequestDto.cs @@ -13,6 +13,7 @@ public string OtDescription { get; set; } public string OtDays { get; set; } public int UserId { get; set; } + public int? StatusId { get; set; } } diff --git a/Areas/OTcalculate/Services/OvertimeExcelService.cs b/Areas/OTcalculate/Services/OvertimeExcelService.cs index f784cdc..8e7efe0 100644 --- a/Areas/OTcalculate/Services/OvertimeExcelService.cs +++ b/Areas/OTcalculate/Services/OvertimeExcelService.cs @@ -5,6 +5,7 @@ using PSTW_CentralSystem.Areas.OTcalculate.Models; using System; using System.Collections.Generic; using System.Linq; +using ClosedXML.Excel.Drawings; namespace PSTW_CentralSystem.Areas.OTcalculate.Services { @@ -19,79 +20,133 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services int weekendId, List publicHolidays, bool isAdminUser = false, - byte[]? logoImage = null // This parameter is missing in the call + byte[]? logoImage = null ) { var workbook = new XLWorkbook(); var worksheet = workbook.Worksheets.Add("Overtime Records"); int currentRow = 1; + int logoBottomRow = 3; + + if (logoImage != null) + { + using (var ms = new MemoryStream(logoImage)) + { + var picture = worksheet.AddPicture(ms) + .MoveTo(worksheet.Cell(currentRow, 1)) + .WithPlacement(XLPicturePlacement.FreeFloating); + picture.Name = "Company Logo"; + picture.Scale(0.3); + } + } + + currentRow = logoBottomRow + 1; + + int mergeCols = 10; // or set to 'col' if you want to merge all active columns - // Add Header Information if (!string.IsNullOrEmpty(userFullName)) { - worksheet.Cell(currentRow, 1).Value = $"Name: {userFullName}"; + var nameCell = worksheet.Range(currentRow, 1, currentRow, mergeCols).Merge(); + nameCell.Value = $"Name: {userFullName}"; + nameCell.Style.Font.Bold = true; + nameCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + nameCell.Style.Alignment.WrapText = true; currentRow++; } + if (!string.IsNullOrEmpty(departmentName)) { - worksheet.Cell(currentRow, 1).Value = $"Department: {departmentName}"; + var deptCell = worksheet.Range(currentRow, 1, currentRow, mergeCols).Merge(); + deptCell.Value = $"Department: {departmentName}"; + deptCell.Style.Font.Bold = true; + deptCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + deptCell.Style.Alignment.WrapText = true; currentRow++; } - currentRow++; // Add an empty row after header - // Header titles - var headers = new List - { - "Day", "Date", "Office From", "Office To", "Office Break", - "After From", "After To", "After Break", - "Total OT", "Break Minutes", "Net OT" - }; - - if (departmentId == 2 || isAdminUser) - headers.Add("Station"); - - headers.Add("Description"); - - // Set header row - int headerRow = currentRow; - for (int i = 0; i < headers.Count; i++) - { - var cell = worksheet.Cell(headerRow, i + 1); - cell.Value = headers[i]; - cell.Style.Font.Bold = true; - cell.Style.Fill.BackgroundColor = XLColor.LightGray; - cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; - cell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; - cell.Style.Border.InsideBorder = XLBorderStyleValues.Thin; - } currentRow++; - // Fill data rows + // Header setup + int headerRow1 = currentRow; + int headerRow2 = currentRow + 1; + + worksheet.Cell(headerRow1, 1).Value = "Days"; + worksheet.Cell(headerRow1, 2).Value = "Date"; + worksheet.Range(headerRow1, 1, headerRow2, 1).Merge(); // Days + worksheet.Range(headerRow1, 2, headerRow2, 2).Merge(); // Date + + worksheet.Cell(headerRow1, 3).Value = "Office Hours"; + worksheet.Range(headerRow1, 3, headerRow1, 5).Merge(); + + worksheet.Cell(headerRow2, 3).Value = "From"; + worksheet.Cell(headerRow2, 4).Value = "To"; + worksheet.Cell(headerRow2, 5).Value = "Break (min)"; + + worksheet.Cell(headerRow1, 6).Value = "After Office Hours"; + worksheet.Range(headerRow1, 6, headerRow1, 8).Merge(); + + worksheet.Cell(headerRow2, 6).Value = "From"; + worksheet.Cell(headerRow2, 7).Value = "To"; + worksheet.Cell(headerRow2, 8).Value = "Break (min)"; + + worksheet.Cell(headerRow1, 9).Value = "Total OT Hours"; + worksheet.Cell(headerRow1, 10).Value = "Break Hours (min)"; + worksheet.Cell(headerRow1, 11).Value = "Net OT Hours"; + worksheet.Range(headerRow1, 9, headerRow2, 9).Merge(); + worksheet.Range(headerRow1, 10, headerRow2, 10).Merge(); + worksheet.Range(headerRow1, 11, headerRow2, 11).Merge(); + + int col = 12; + if (departmentId == 2 || isAdminUser) + { + worksheet.Cell(headerRow1, col).Value = "Station"; + worksheet.Range(headerRow1, col, headerRow2, col).Merge(); + col++; + } + + worksheet.Cell(headerRow1, col).Value = "Description"; + worksheet.Range(headerRow1, col, headerRow2, col).Merge(); + + // Apply styling after header setup + var headerRange = worksheet.Range(headerRow1, 1, headerRow2, col); + headerRange.Style.Font.Bold = true; + headerRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + headerRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; + headerRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; + headerRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + // Background colors for header cells + worksheet.Range(headerRow1, 1, headerRow2, 2).Style.Fill.BackgroundColor = XLColor.LightGreen; + worksheet.Range(headerRow1, 3, headerRow2, 5).Style.Fill.BackgroundColor = XLColor.AliceBlue; + worksheet.Range(headerRow1, 6, headerRow2, 8).Style.Fill.BackgroundColor = XLColor.AliceBlue; + worksheet.Range(headerRow1, 9, headerRow2, 11).Style.Fill.BackgroundColor = XLColor.Peach; + if (departmentId == 2 || isAdminUser) + worksheet.Range(headerRow1, 12, headerRow2, 12).Style.Fill.BackgroundColor = XLColor.LightCyan; + + worksheet.Cell(headerRow1, col).Style.Fill.BackgroundColor = XLColor.LightBlue; + + // Update currentRow after headers + currentRow = headerRow2 + 1; + + DateTime? previousDate = null; + foreach (var r in records) { - TimeSpan totalOT = CalculateTotalOT(r); - int totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0); - TimeSpan netOT = totalOT - TimeSpan.FromMinutes(totalBreak); + int dataCol = 1; - int col = 1; - var dayCell = worksheet.Cell(currentRow, col++); - dayCell.Value = r.OtDate.ToString("ddd"); - dayCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; - dayCell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; - dayCell.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + bool isSameDateAsPrevious = previousDate == r.OtDate.Date; + previousDate = r.OtDate.Date; - var dateCell = worksheet.Cell(currentRow, col++); - dateCell.Value = r.OtDate.ToString("yyyy-MM-dd"); - dateCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; - dateCell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; - dateCell.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + var dayCell = worksheet.Cell(currentRow, dataCol++); + var dateCell = worksheet.Cell(currentRow, dataCol++); - // Apply background color for weekends and public holidays + // Check the type of day first var dayOfWeek = r.OtDate.DayOfWeek; bool isWeekend = (weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) || (weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday)); bool isPublicHoliday = publicHolidays.Any(h => h.HolidayDate.Date == r.OtDate.Date); + // Apply color regardless of whether the value is shown if (isPublicHoliday) { dayCell.Style.Fill.BackgroundColor = XLColor.Pink; @@ -103,28 +158,50 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services dateCell.Style.Fill.BackgroundColor = XLColor.LightBlue; } - worksheet.Cell(currentRow, col++).Value = FormatTime(r.OfficeFrom); - worksheet.Cell(currentRow, col++).Value = FormatTime(r.OfficeTo); - worksheet.Cell(currentRow, col++).Value = r.OfficeBreak; + // Show value only if it's not repeated + if (!isSameDateAsPrevious) + { + dayCell.Value = r.OtDate.ToString("ddd"); + dateCell.Value = r.OtDate.ToString("yyyy-MM-dd"); + } + else + { + dayCell.Value = ""; + dateCell.Value = ""; + } - worksheet.Cell(currentRow, col++).Value = FormatTime(r.AfterFrom); - worksheet.Cell(currentRow, col++).Value = FormatTime(r.AfterTo); - worksheet.Cell(currentRow, col++).Value = r.AfterBreak; + worksheet.Cell(currentRow, dataCol++).Value = FormatTime(r.OfficeFrom); + worksheet.Cell(currentRow, dataCol++).Value = FormatTime(r.OfficeTo); + worksheet.Cell(currentRow, dataCol++).Value = r.OfficeBreak; - worksheet.Cell(currentRow, col++).Value = totalOT.ToString(@"hh\:mm"); - worksheet.Cell(currentRow, col++).Value = totalBreak; - worksheet.Cell(currentRow, col++).Value = netOT.ToString(@"hh\:mm"); + worksheet.Cell(currentRow, dataCol++).Value = FormatTime(r.AfterFrom); + worksheet.Cell(currentRow, dataCol++).Value = FormatTime(r.AfterTo); + worksheet.Cell(currentRow, dataCol++).Value = r.AfterBreak; + + TimeSpan totalOT = CalculateTotalOT(r); + int totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0); + TimeSpan netOT = totalOT - TimeSpan.FromMinutes(totalBreak); + + var totalOTCell = worksheet.Cell(currentRow, dataCol++); + totalOTCell.Value = totalOT; + totalOTCell.Style.NumberFormat.Format = "hh:mm"; + + worksheet.Cell(currentRow, dataCol++).Value = totalBreak; + + var netOTCell = worksheet.Cell(currentRow, dataCol++); + netOTCell.Value = netOT; + netOTCell.Style.NumberFormat.Format = "hh:mm"; if (departmentId == 2 || isAdminUser) - worksheet.Cell(currentRow, col++).Value = r.Stations?.StationName ?? ""; + worksheet.Cell(currentRow, dataCol++).Value = r.Stations?.StationName ?? ""; - worksheet.Cell(currentRow, col++).Value = r.OtDescription ?? ""; + worksheet.Cell(currentRow, dataCol++).Value = r.OtDescription ?? ""; - // Apply border and alignment for the rest of the row - for (int i = headers.IndexOf("Office From") + 1; i <= headers.Count; i++) + for (int i = 1; i <= col; i++) { var cell = worksheet.Cell(currentRow, i); cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; cell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; cell.Style.Border.InsideBorder = XLBorderStyleValues.Thin; } @@ -132,18 +209,64 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services currentRow++; } - // Add Total row - int totalRow = currentRow; - worksheet.Cell(totalRow, 1).Value = "TOTAL"; - worksheet.Cell(totalRow, 1).Style.Font.Bold = true; - worksheet.Cell(totalRow, headers.IndexOf("Total OT") + 1).Value = $"=SUM(I{headerRow + 1}:I{totalRow - 1})"; - worksheet.Cell(totalRow, headers.IndexOf("Total OT") + 1).Style.Font.Bold = true; - worksheet.Cell(totalRow, headers.IndexOf("Break Minutes") + 1).Value = $"=SUM(J{headerRow + 1}:J{totalRow - 1})"; - worksheet.Cell(totalRow, headers.IndexOf("Break Minutes") + 1).Style.Font.Bold = true; - worksheet.Cell(totalRow, headers.IndexOf("Net OT") + 1).Value = $"=SUM(K{headerRow + 1}:K{totalRow - 1})"; - worksheet.Cell(totalRow, headers.IndexOf("Net OT") + 1).Style.Font.Bold = true; + if (records.Any()) + { + int totalRow = currentRow; + worksheet.Cell(totalRow, 1).Value = "TOTAL"; + worksheet.Cell(totalRow, 1).Style.Font.Bold = true; - worksheet.Columns().AdjustToContents(); + int colTotalOT = 9; // Column for Total OT + int colBreakMin = 10; // Column for Break Hours (min) + int colNetOT = 11; // Column for Net OT + + var totalOTSumCell = worksheet.Cell(totalRow, colTotalOT); + totalOTSumCell.FormulaA1 = $"SUM({GetColumnLetter(colTotalOT)}{headerRow1 + 1}:{GetColumnLetter(colTotalOT)}{totalRow - 1})"; + totalOTSumCell.Style.NumberFormat.Format = "hh:mm"; + + var breakMinTotalCell = worksheet.Cell(totalRow, colBreakMin); + breakMinTotalCell.FormulaA1 = $"SUM({GetColumnLetter(colBreakMin)}{headerRow1 + 1}:{GetColumnLetter(colBreakMin)}{totalRow - 1})/1440"; + breakMinTotalCell.Style.NumberFormat.Format = "hh:mm"; + + var netOTSumCell = worksheet.Cell(totalRow, colNetOT); + netOTSumCell.FormulaA1 = $"SUM({GetColumnLetter(colNetOT)}{headerRow1 + 1}:{GetColumnLetter(colNetOT)}{totalRow - 1})"; + netOTSumCell.Style.NumberFormat.Format = "hh:mm"; + + for (int i = 1; i <= col; i++) + { + var cell = worksheet.Cell(totalRow, i); + cell.Style.Font.Bold = true; + cell.Style.Fill.BackgroundColor = XLColor.Yellow; + cell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; + cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + } + } + + // Step 1: Enable wrap text on header range + headerRange.Style.Alignment.WrapText = true; + worksheet.Style.Alignment.WrapText = true; + + // Step 2: Adjust row height based on wrap text + worksheet.Rows(headerRow1, headerRow2).AdjustToContents(); + + // Step 3 (optional): Manually set column widths to ensure long headers show correctly + worksheet.Column(1).Width = 8; // Days + worksheet.Column(2).Width = 13; // Date + worksheet.Column(3).Width = 12; // Office From + worksheet.Column(4).Width = 12; // Office To + worksheet.Column(5).Width = 12; // Office Break + worksheet.Column(6).Width = 12; // After From + worksheet.Column(7).Width = 12; // After To + worksheet.Column(8).Width = 12; // After Break + worksheet.Column(9).Width = 14; // Total OT Hours + worksheet.Column(10).Width = 15.5f; // Break Hours + worksheet.Column(11).Width = 14; // Net OT Hours + + int colIndex = 12; + if (departmentId == 2 || isAdminUser) + { + worksheet.Column(colIndex++).Width = 20; // Station + } + worksheet.Column(colIndex).Width = 35; // Description var stream = new MemoryStream(); workbook.SaveAs(stream); @@ -164,5 +287,18 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services { return time == null || time == TimeSpan.Zero ? "" : time.Value.ToString(@"hh\:mm"); } + + private string GetColumnLetter(int columnNumber) + { + string columnString = ""; + while (columnNumber > 0) + { + int currentLetterNumber = (columnNumber - 1) % 26; + char currentLetter = (char)(currentLetterNumber + 65); + columnString = currentLetter + columnString; + columnNumber = (columnNumber - 1) / 26; + } + return columnString; + } } -} \ No newline at end of file +} diff --git a/Areas/OTcalculate/Views/ApprovalDashboard/Approval.cshtml b/Areas/OTcalculate/Views/ApprovalDashboard/Approval.cshtml new file mode 100644 index 0000000..43ffa8d --- /dev/null +++ b/Areas/OTcalculate/Views/ApprovalDashboard/Approval.cshtml @@ -0,0 +1,163 @@ +@{ + ViewData["Title"] = "Overtime Pending Approval"; + Layout = "~/Views/Shared/_Layout.cshtml"; +} + + + +
+
+
+ + +
+
+ + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Staff NameDate SubmitHoU StatusHoD StatusManager StatusHR StatusAction
{{ row.fullName }}{{ formatDate(row.submitDate) }}{{ row.houStatus }}{{ row.hodStatus }}{{ row.managerStatus }}{{ row.hrStatus }} + + + + +
+
+
+
+ + + + diff --git a/Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml b/Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml new file mode 100644 index 0000000..ea3b0e5 --- /dev/null +++ b/Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml @@ -0,0 +1,226 @@ +@{ + ViewData["Title"] = "OT Details Review"; + Layout = "~/Views/Shared/_Layout.cshtml"; +} + + + +
+
+
+ Employee Name: {{ userInfo.fullName }}
+ Department: {{ userInfo.departmentName || 'N/A' }}
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateOffice Hour
(8:30 - 17:30)
After Office Hour
(17:30 - 8:30)
Total OT HoursBreak (min)Net OT HoursStationDaysDescriptionAction
FromToBreakFromToBreak
{{ formatDate(record.otDate) }}{{ formatTime(record.officeFrom) }}{{ formatTime(record.officeTo) }}{{ record.officeBreak }}{{ formatTime(record.afterFrom) }}{{ formatTime(record.afterTo) }}{{ record.afterBreak }}{{ formatHourMinute(calculateTotalTime(record)) }}{{ calculateTotalBreak(record) }}{{ formatHourMinute(calculateNetTime(record)) }}{{ record.stationName || 'N/A' }}{{ record.otDays }}{{ record.otDescription }} + + +
No overtime details found for this submission.
+
+
+ + + +
+
+@section Scripts { + +} \ No newline at end of file diff --git a/Areas/OTcalculate/Views/HodDashboard/HodApproval.cshtml b/Areas/OTcalculate/Views/HodDashboard/HodApproval.cshtml deleted file mode 100644 index da357b1..0000000 --- a/Areas/OTcalculate/Views/HodDashboard/HodApproval.cshtml +++ /dev/null @@ -1,68 +0,0 @@ -@{ - ViewData["Title"] = "Overtime Approval"; - Layout = "~/Views/Shared/_Layout.cshtml"; -} - -
- - - - - - - - - - - - - - - - - - - - -
NameSubmission DateMonth/YearAction
{{ submission.userName }}{{ formatDate(submission.submissionDate) }}{{ submission.monthYear }}
No overtime records submitted for review.
-
- -@section Scripts { - - -} \ No newline at end of file diff --git a/Areas/OTcalculate/Views/HodDashboard/OtReview.cshtml b/Areas/OTcalculate/Views/HodDashboard/OtReview.cshtml deleted file mode 100644 index 6693b41..0000000 --- a/Areas/OTcalculate/Views/HodDashboard/OtReview.cshtml +++ /dev/null @@ -1,169 +0,0 @@ -@{ - ViewData["Title"] = "Overtime Review"; - Layout = "~/Views/Shared/_Layout.cshtml"; -} - -
-

Overtime Records for {{ reviewedUserName }} - {{ formatMonthYear(selectedMonth, selectedYear) }}

- -
- -@section Scripts { - - - } \ No newline at end of file diff --git a/Areas/OTcalculate/Views/HouDashboard/HouApproval.cshtml b/Areas/OTcalculate/Views/HouDashboard/HouApproval.cshtml deleted file mode 100644 index 0cc15d5..0000000 --- a/Areas/OTcalculate/Views/HouDashboard/HouApproval.cshtml +++ /dev/null @@ -1,5 +0,0 @@ -@{ - ViewData["Title"] = "Overtime Approval"; - Layout = "~/Views/Shared/_Layout.cshtml"; -} - diff --git a/Areas/OTcalculate/Views/HouDashboard/OtReview.cshtml b/Areas/OTcalculate/Views/HouDashboard/OtReview.cshtml deleted file mode 100644 index 6693b41..0000000 --- a/Areas/OTcalculate/Views/HouDashboard/OtReview.cshtml +++ /dev/null @@ -1,169 +0,0 @@ -@{ - ViewData["Title"] = "Overtime Review"; - Layout = "~/Views/Shared/_Layout.cshtml"; -} - -
-

Overtime Records for {{ reviewedUserName }} - {{ formatMonthYear(selectedMonth, selectedYear) }}

- -
- -@section Scripts { - - - } \ No newline at end of file diff --git a/Areas/OTcalculate/Views/HrDashboard/HrUserSetting.cshtml b/Areas/OTcalculate/Views/HrDashboard/HrUserSetting.cshtml index ef40697..77b98a8 100644 --- a/Areas/OTcalculate/Views/HrDashboard/HrUserSetting.cshtml +++ b/Areas/OTcalculate/Views/HrDashboard/HrUserSetting.cshtml @@ -1,10 +1,28 @@ @{ + ViewBag.Title = "User Settings"; + Layout = "~/Views/Shared/_Layout.cshtml"; + } @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + +
@@ -46,21 +64,27 @@
-
+
@@ -96,32 +120,206 @@
-
- - - - -
-
- - - - - - - - - -
Full NameDepartmentCurrent StateSelect
-
-
- - +
+
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ +
+ + + + + + + + + + +
Full NameDepartmentCurrent StateCurrent FlowSelect
+
+
+ + +
+ +
+
+
+
Create Approval Flow
+
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + +
No.Approval NameAction
No approval flows found.
{{ index + 1 }}{{ flow.approvalName }} + + + + + +
+
+
+ +
+
+
+ + +
@@ -145,14 +343,37 @@ selectedUsersState: [], stateUserList: [], userDatatable: null, - stateDatatable: null + stateDatatable: null, + approvalFlows: [], + approvalFlowList: [], + selectedApprovalFlowId: '', + approvalFlow: { + approvalName: '', + hou: '', + hod: '', + manager: '', + hr: '' + }, + editFlow: { + approvalId: '', + approvalName: '', + hou: '', + hod: '', + manager: '', + hr: '' + }, + allUsers: [] + }; }, mounted() { console.log("Vue App Mounted Successfully"); this.fetchFlexiHours(); this.fetchStates(); - this.changeTab('flexi'); // Initialize the default tab + this.changeTab('flexi'); + this.fetchAllUsers(); + this.fetchUsersState(); + this.fetchApprovalFlows(); }, methods: { changeTab(tab) { @@ -165,11 +386,13 @@ this.initiateTable(); } } else if (tab === 'state') { - this.clearStateSelection(); + this.clearAllSelectionsStateFlow(); if (!this.stateUserList.length) { - this.fetchUsersState().then(() => this.initiateStateTable()); - } - else{ + this.fetchUsersState().then(() => { + this.initiateStateTable(); + this.fetchApprovalFlows(); // Ensure approval flows are fetched on tab change + }); + } else { this.initiateStateTable(); } } @@ -177,7 +400,7 @@ async updateUserSettings(apiUrl, selectedUsers, selectedValue, successMessage, clearCallback, fetchCallback, valueKey) { if (!selectedUsers.length || !selectedValue) { - alert("Please select at least one user and a value."); + alert("Please select at least one user and a flexi hour."); return; } @@ -203,7 +426,6 @@ } }, - async fetchFlexiHours() { try { const response = await fetch("/OvertimeAPI/GetFlexiHours"); @@ -286,7 +508,7 @@ if (e.target.checked) { self.selectedUsers.push(userId); } else { - self.selectedUsers = self.selectedUsers.filter(id => id !== userId); + self.selectedUsers= self.selectedUsers.filter(id => id !== userId); } }); }); @@ -305,6 +527,7 @@ { data: "fullName" }, { data: "departmentName" }, { data: "state", defaultContent: "N/A" }, + { data: "approvalName", defaultContent: "N/A" }, { data: "id", className: "text-center", @@ -326,42 +549,72 @@ }); }, - clearForm() { - this.selectedFlexiHourId = ''; - this.selectedUsers = []; - if (this.userDatatable) { - this.userDatatable.rows().every(function () { - $(this.node()).find('input[type="checkbox"]').prop('checked', false); - }); + async handleCombinedUpdate() { + const hasUsers = this.selectedUsersState.length > 0; + const hasState = this.selectedStateAll !== ''; + const hasFlow = this.selectedApprovalFlowId !== ''; + + if (!hasUsers && !hasState && !hasFlow) { + alert("Please choose users, a state, or an approval flow to proceed."); + this.clearAllSelectionsStateFlow(); + return; } - }, - clearStateSelection() { - this.selectedUsersState = []; - if (this.stateDatatable) { - this.stateDatatable.rows().every(function () { - $(this.node()).find('input[type="checkbox"]').prop('checked', false); - }); + if (!hasUsers) { + alert("Please select at least one user."); + return; } + + if (!hasState && !hasFlow) { + alert("Please select either a State or Approval Flow."); + return; + } + + if (hasState) { + await this.updateUserSettings( + "/OvertimeAPI/UpdateUserStates", + this.selectedUsersState, + this.selectedStateAll, + "State updated successfully!", + () => {}, // Don't clear yet + () => {}, // Don't fetch yet + "StateId" + ); + } + + if (hasFlow) { + const payload = this.selectedUsersState.map(userId => ({ + UserId: userId, + ApprovalFlowId: this.selectedApprovalFlowId + })); + + try { + const response = await fetch("/OvertimeAPI/UpdateUserApprovalFlow", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const errorData = await response.json(); + alert(errorData.message || "Failed to update Approval Flow."); + return; + } + + alert("Approval Flow updated successfully!"); + + } catch (error) { + console.error("Error updating approval flow:", error); + alert("An unexpected error occurred."); + } + } + + // After updates, refresh and clear + await this.fetchUsersState(); + this.initiateStateTable(); + this.clearAllSelectionsStateFlow(); }, - async saveState() { - await this.updateUserSettings( - "/OvertimeAPI/UpdateUserStates", - this.selectedUsersState, - this.selectedStateAll, - "State updated successfully!", - this.clearStateSelection, - this.fetchUsersState, // This will fetch the updated state users after the update - "StateId" - ); - - // Fetch the updated state list to ensure the page reflects the new state data - await this.fetchUsersState(); // This fetch will make sure stateUserList is updated - this.initiateStateTable(); // Reinitialize the state table with new data - }, - - async updateFlexiHours() { await this.updateUserSettings( "/OvertimeAPI/UpdateUserFlexiHours", @@ -372,11 +625,152 @@ this.fetchUsers, "FlexiHourId" ); + }, + + async fetchAllUsers() { + try { + const response = await fetch("/OvertimeAPI/GetAllUsers"); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Fetch failed: ${response.status} ${errorText}`); + } + + const data = await response.json(); + + console.log("Fetched users:", data); + + this.allUsers = data.filter(e => e.fullName !== "MAAdmin" && e.fullName !== "SysAdmin"); + } catch (error) { + console.error("Error fetching all users:", error); + } + }, + + async fetchApprovalFlows() { + try { + const response = await fetch('/OvertimeAPI/GetApprovalFlowList'); + const data = await response.json(); + this.approvalFlowList = data; // Update the data correctly + console.log('Fetched approval flows:', this.approvalFlowList); // Verify data + } catch (error) { + console.error('Error fetching approval flows:', error); + } + }, + async submitApprovalFlow() { + try { + const payload = { ...this.approvalFlow }; + if (payload.hou === "") payload.hou = null; + if (payload.hod === "") payload.hod = null; + if (payload.manager === "") payload.manager = null; + + const response = await fetch('/OvertimeAPI/CreateApprovalFlow', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (response.ok) { + alert('Approval flow created successfully!'); + this.clearFormApproval(); + await this.fetchApprovalFlows(); + } else { + const errorData = await response.json(); + alert(errorData.message || 'Failed to create approval flow. Please try again.'); + } + } catch (error) { + console.error('Error submitting approval flow:', error); + alert('An unexpected error occurred.'); + } + }, + + clearFormApproval() { + this.approvalFlow = { + approvalName: '', + hou: '', + hod: '', + manager: '', + hr: '' + }; + }, + + async deleteApprovalFlow(approvalId) { + if (!confirm("Are you sure you want to delete this approval flow?")) return; + try { + const response = await fetch(`/OvertimeAPI/DeleteApprovalFlow/${approvalId}`, { method: "DELETE" }); + if (!response.ok) throw new Error("Failed to delete"); + alert("Approval flow deleted."); + await this.fetchApprovalFlows(); + } catch (error) { + console.error("Error deleting flow:", error); + } + }, + + clearForm() { + this.selectedFlexiHourId = ''; + this.selectedUsers = []; + if (this.userDatatable) { + this.userDatatable.rows().every(function () { + $(this.node()).find('input[type="checkbox"]').prop('checked', false); + }); + } + }, + + clearAllSelectionsStateFlow() { + this.selectedStateAll = ''; + this.selectedApprovalFlowId = ''; + this.selectedUsersState = []; + if (this.stateDatatable) { + this.stateDatatable.rows().every(function () { + $(this.node()).find('input[type="checkbox"]').prop('checked', false); + }); + } + }, + openEditModal(flow) { + // Set the editFlow to the selected flow + this.editFlow = { ...flow }; + + // Optionally, log the data to ensure it's correct + console.log(this.editFlow); + + // Show the modal + this.fetchAllUsers().then(() => { + const modal = new bootstrap.Modal(document.getElementById('editApprovalModal')); + modal.show(); + }); + }, + + + async submitEditApprovalFlow() { + try { + const response = await fetch('/OvertimeAPI/EditApprovalFlow', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.editFlow) + }); + + if (response.ok) { + alert('Approval flow updated successfully!'); + await this.fetchApprovalFlows(); + const modalElement = document.getElementById('editApprovalModal'); + const modal = bootstrap.Modal.getInstance(modalElement); + modal.hide(); + } else { + const error = await response.json(); + alert(error.message || 'Failed to update approval flow.'); + } + } catch (error) { + console.error('Error updating flow:', error); + alert('Unexpected error occurred.'); + } } + + } }); app.mount('#app'); }; -} +} \ No newline at end of file diff --git a/Areas/OTcalculate/Views/HrDashboard/Settings.cshtml b/Areas/OTcalculate/Views/HrDashboard/Settings.cshtml index f69164e..025c838 100644 --- a/Areas/OTcalculate/Views/HrDashboard/Settings.cshtml +++ b/Areas/OTcalculate/Views/HrDashboard/Settings.cshtml @@ -107,6 +107,17 @@
+
+
+
+
+
+ +
+
+
+
+
@@ -126,6 +137,7 @@ calendarUpdateDate: null, flexiHourUpdateDate: null, regionUpdateDate: null, + approvalFlowUpdateDate: null, }; }, mounted() { @@ -144,6 +156,7 @@ this.calendarUpdateDate = data.calendarUpdateDate; this.flexiHourUpdateDate = data.flexiHourUpdateDate; this.regionUpdateDate = data.regionUpdateDate; + this.approvalFlowUpdateDate = data.approvalFlowUpdateDate; } catch (error) { console.error(error); } diff --git a/Areas/OTcalculate/Views/Overtime/EditOvertime.cshtml b/Areas/OTcalculate/Views/Overtime/EditOvertime.cshtml index 024984d..d0f40a4 100644 --- a/Areas/OTcalculate/Views/Overtime/EditOvertime.cshtml +++ b/Areas/OTcalculate/Views/Overtime/EditOvertime.cshtml @@ -153,6 +153,9 @@ return { label, value: totalMinutes }; }), + + previousPage: document.referrer + }; }, @@ -434,7 +437,7 @@ if (response.ok) { alert("Overtime record updated successfully!"); - window.location.href = '/OTcalculate/Overtime/OtRecords'; + window.location.href = this.previousPage; } else { alert("Failed to update overtime record."); } @@ -445,7 +448,7 @@ }, goBack() { - window.location.href = "/OTcalculate/Overtime/OtRecords"; + window.location.href = this.previousPage; }, clearOfficeHours() { this.editForm.officeFrom = ""; diff --git a/Areas/OTcalculate/Views/Overtime/OtRecords.cshtml b/Areas/OTcalculate/Views/Overtime/OtRecords.cshtml index b607dd5..a0a8e69 100644 --- a/Areas/OTcalculate/Views/Overtime/OtRecords.cshtml +++ b/Areas/OTcalculate/Views/Overtime/OtRecords.cshtml @@ -160,8 +160,8 @@
- - + +