From 5efe8e13c49cf0339f8990bdb1ee04711db68e69 Mon Sep 17 00:00:00 2001 From: Naz <2022755409@student.uitm.edu.my> Date: Mon, 9 Jun 2025 14:41:52 +0800 Subject: [PATCH] updated --- Areas/OTcalculate/Models/ApprovalFlowModel.cs | 3 - Areas/OTcalculate/Models/OtRecordsModel.cs | 7 - Areas/OTcalculate/Models/OtRegisterModel.cs | 49 + Areas/OTcalculate/Models/OtStatusModel.cs | 1 + Areas/OTcalculate/Models/StaffSignModel.cs | 28 + Areas/OTcalculate/Services/OvertimeExcel.cs | 2195 +++++++++++++++++ .../Services/OvertimeExcelService.cs | 304 --- Areas/OTcalculate/Services/OvertimePDF.cs | 1294 ++++++++++ .../Services/OvertimePdfService.cs | 237 -- .../Views/ApprovalDashboard/Approval.cshtml | 373 ++- .../Views/ApprovalDashboard/OtReview.cshtml | 1238 +++++++++- .../Views/HrDashboard/HrUserSetting.cshtml | 55 +- .../Views/HrDashboard/OtApproval.cshtml | 4 - .../OTcalculate/Views/HrDashboard/Rate.cshtml | 32 +- .../Views/HrDashboard/Settings.cshtml | 27 +- .../Views/Overtime/EditOvertime.cshtml | 488 +++- .../Views/Overtime/OtRecords.cshtml | 119 +- .../Views/Overtime/OtRegister.cshtml | 449 +++- .../Views/Overtime/OtStatus.cshtml | 782 +++++- Controllers/API/OvertimeAPI.cs | 1673 ++++++++++--- DBContext/CentralSystemContext.cs | 1 + PSTW_CentralSystem.csproj | 2 + Program.cs | 4 +- ...4949-89b4-35a414acadee_Overtime_2025_4.pdf | Bin 0 -> 82519 bytes ...e68-493c-a167-472836655cd4_TESTING PDF.pdf | Bin 0 -> 15358 bytes ...25c-4154-aef9-05bb238e9b35_TESTING PDF.pdf | Bin 0 -> 15358 bytes ...f9-3902596dc24d_OvertimeRecords_2025_5.pdf | Bin 0 -> 80717 bytes ...c1-048a9d845599_OvertimeRecords_2025_6.pdf | Bin 0 -> 74401 bytes ...4e02-b015-9d670ff6fbea_Overtime_2025_6.pdf | Bin 0 -> 79040 bytes ...7e-8ec42cbfd1e6_OvertimeRecords_2025_6.pdf | Bin 0 -> 79823 bytes ...3db-4322-8bae-3d594b6de2ec_TESTING PDF.pdf | Bin 0 -> 15358 bytes 31 files changed, 7945 insertions(+), 1420 deletions(-) delete mode 100644 Areas/OTcalculate/Models/OtRecordsModel.cs create mode 100644 Areas/OTcalculate/Models/StaffSignModel.cs create mode 100644 Areas/OTcalculate/Services/OvertimeExcel.cs delete mode 100644 Areas/OTcalculate/Services/OvertimeExcelService.cs create mode 100644 Areas/OTcalculate/Services/OvertimePDF.cs delete mode 100644 Areas/OTcalculate/Services/OvertimePdfService.cs delete mode 100644 Areas/OTcalculate/Views/HrDashboard/OtApproval.cshtml create mode 100644 wwwroot/Media/Overtime/16e38f37-6cc5-4949-89b4-35a414acadee_Overtime_2025_4.pdf create mode 100644 wwwroot/Media/Overtime/2b2c9633-6e68-493c-a167-472836655cd4_TESTING PDF.pdf create mode 100644 wwwroot/Media/Overtime/6dcd97e6-225c-4154-aef9-05bb238e9b35_TESTING PDF.pdf create mode 100644 wwwroot/Media/Overtime/733dc024-ab3f-4453-8ff9-3902596dc24d_OvertimeRecords_2025_5.pdf create mode 100644 wwwroot/Media/Overtime/8cfb1ae2-e6e9-4191-b5c1-048a9d845599_OvertimeRecords_2025_6.pdf create mode 100644 wwwroot/Media/Overtime/9cc124cc-9800-4e02-b015-9d670ff6fbea_Overtime_2025_6.pdf create mode 100644 wwwroot/Media/Overtime/cd653384-c93f-4d21-b77e-8ec42cbfd1e6_OvertimeRecords_2025_6.pdf create mode 100644 wwwroot/Media/Overtime/f7477bc1-73db-4322-8bae-3d594b6de2ec_TESTING PDF.pdf diff --git a/Areas/OTcalculate/Models/ApprovalFlowModel.cs b/Areas/OTcalculate/Models/ApprovalFlowModel.cs index 51f679e..f03fd80 100644 --- a/Areas/OTcalculate/Models/ApprovalFlowModel.cs +++ b/Areas/OTcalculate/Models/ApprovalFlowModel.cs @@ -26,7 +26,4 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models [ForeignKey("HR")] public virtual UserModel? HRUser { get; set; } } - - - } diff --git a/Areas/OTcalculate/Models/OtRecordsModel.cs b/Areas/OTcalculate/Models/OtRecordsModel.cs deleted file mode 100644 index 46eb9e6..0000000 --- a/Areas/OTcalculate/Models/OtRecordsModel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace PSTW_CentralSystem.Areas.OTcalculate.Models -{ - public class OtRecordsModel - { - - } -} diff --git a/Areas/OTcalculate/Models/OtRegisterModel.cs b/Areas/OTcalculate/Models/OtRegisterModel.cs index 2e19e74..8d3e950 100644 --- a/Areas/OTcalculate/Models/OtRegisterModel.cs +++ b/Areas/OTcalculate/Models/OtRegisterModel.cs @@ -46,5 +46,54 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models { return TimeSpan.TryParse(time, out TimeSpan result) ? result : null; } + + // OtRegisterEditDto.cs + public class OtRegisterEditDto + { + public int OvertimeId { get; set; } + public DateTime OtDate { get; set; } + public string? OfficeFrom { get; set; } // Use string to match input type (e.g., "09:00") + public string? OfficeTo { get; set; } // Use string to match input type + public int? OfficeBreak { get; set; } + public string? AfterFrom { get; set; } // Use string to match input type + public string? AfterTo { get; set; } // Use string to match input type + public int? AfterBreak { get; set; } + public int? StationId { get; set; } + public string? OtDescription { get; set; } + // You might also need to send the StatusId if it's relevant for the update logic + public int StatusId { get; set; } + public string? OtDays { get; set; } + } + + public class OtUpdateLog + { + public string ApproverRole { get; set; } // e.g., "HoU", "HoD", "Manager", "HR" + public int ApproverUserId { get; set; } + public DateTime UpdateTimestamp { get; set; } + public string ChangeType { get; set; } // New: "Edit" or "Delete" + + // For "Edit" type + public OtRegisterModel? BeforeEdit { get; set; } + public OtRegisterEditDto? AfterEdit { get; set; } + + // For "Delete" type + public OtRegisterModel? DeletedRecord { get; set; } + } + + public class OtRegisterUpdateDto + { + public int OvertimeId { get; set; } + public DateTime OtDate { get; set; } + public string? OfficeFrom { get; set; } + public string? OfficeTo { get; set; } + public int? OfficeBreak { get; set; } + public string? AfterFrom { get; set; } + public string? AfterTo { get; set; } + public int? AfterBreak { get; set; } + public int? StationId { get; set; } + public string? OtDescription { get; set; } + public string? OtDays { get; set; } + public int UserId { get; set; } + } } } diff --git a/Areas/OTcalculate/Models/OtStatusModel.cs b/Areas/OTcalculate/Models/OtStatusModel.cs index a1f450b..2a6d8ac 100644 --- a/Areas/OTcalculate/Models/OtStatusModel.cs +++ b/Areas/OTcalculate/Models/OtStatusModel.cs @@ -9,6 +9,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models [Key] public int StatusId { get; set; } public int UserId { get; set; } + public int Month { get; set; } public int Year { get; set; } public DateTime SubmitDate { get; set; } diff --git a/Areas/OTcalculate/Models/StaffSignModel.cs b/Areas/OTcalculate/Models/StaffSignModel.cs new file mode 100644 index 0000000..999cf9b --- /dev/null +++ b/Areas/OTcalculate/Models/StaffSignModel.cs @@ -0,0 +1,28 @@ +using PSTW_CentralSystem.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace PSTW_CentralSystem.Areas.OTcalculate.Models +{ + public class StaffSignModel + { + [Key] + public int StaffSignId { get; set; } + + public int UserId { get; set; } + + [ForeignKey("UserId")] + public UserModel? User { get; set; } + + public string? ImagePath { get; set; } + + public DateTime? UpdateDate { get; set; } + } + + public class ApprovalSignatureData + { + public string ApproverName { get; set; } + public byte[]? SignatureImage { get; set; } + public DateTime? ApprovedDate { get; set; } // New property for the approval date + } +} diff --git a/Areas/OTcalculate/Services/OvertimeExcel.cs b/Areas/OTcalculate/Services/OvertimeExcel.cs new file mode 100644 index 0000000..26a5054 --- /dev/null +++ b/Areas/OTcalculate/Services/OvertimeExcel.cs @@ -0,0 +1,2195 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using ClosedXML.Excel; +using ClosedXML.Excel.Drawings; +using Microsoft.AspNetCore.Hosting; // Added for IWebHostEnvironment + +using PSTW_CentralSystem.Areas.OTcalculate.Models; +using PSTW_CentralSystem.Models; +using PSTW_CentralSystem.DBContext; + + +namespace PSTW_CentralSystem.Areas.OTcalculate.Services +{ + public class OvertimeExcel + { + private readonly CentralSystemContext _centralDbContext; + private readonly IWebHostEnvironment _env; // Added for IWebHostEnvironment + + public OvertimeExcel(CentralSystemContext centralDbContext, IWebHostEnvironment env) // Modified constructor + { + _centralDbContext = centralDbContext; + _env = env; // Initialize IWebHostEnvironment + } + + public MemoryStream GenerateOvertimeExcel( + List records, + UserModel user, + decimal userRate, + DateTime? selectedMonth = null, + bool isHoU = false, + string? flexiHour = null, + byte[]? logoImage = null, + bool isSimplifiedExport = false, + OtStatusModel? otStatus = null) + { + bool isAdminUser = IsAdmin(user.Id); + bool showStationColumn = user.Department?.DepartmentId == 3 || user.Department?.DepartmentId == 2 || isAdminUser; + + var stream = new MemoryStream(); + + using (var workbook = new XLWorkbook()) + { + var worksheet = workbook.Worksheets.Add("Overtime Report"); + worksheet.ShowGridLines = true; + + int currentRow = 1; + int logoBottomRow = 0; + + 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); + logoBottomRow = currentRow + 2; + } + } + + currentRow = logoBottomRow + 1; + + DateTime displayMonth = selectedMonth ?? + (records.Any() ? records.First().OtDate : DateTime.Now); + + var allDatesInMonth = GetAllDatesInMonth(displayMonth); + records = records.OrderBy(r => r.OtDate).ToList(); + + // Add user information + worksheet.Cell(currentRow, 1).Value = $"Name: {user.FullName}"; + worksheet.Cell(currentRow, 1).Style.Font.SetBold(); + currentRow++; + + worksheet.Cell(currentRow, 1).Value = $"Department: {user.Department?.DepartmentName ?? "N/A"}"; + worksheet.Cell(currentRow, 1).Style.Font.SetBold(); + currentRow++; + + if (!string.IsNullOrEmpty(flexiHour)) + { + worksheet.Cell(currentRow, 1).Value = $"Flexi Hour: {flexiHour}"; + worksheet.Cell(currentRow, 1).Style.Font.SetBold(); + currentRow++; + } + + worksheet.Cell(currentRow, 1).Value = $"Overtime Report: {displayMonth:MMMM yyyy}"; + worksheet.Cell(currentRow, 1).Style.Font.SetBold(); + currentRow++; + + currentRow++; // Add an empty row for spacing + + + // --- Conditional Header Generation --- + if (isSimplifiedExport) + { + AddSimplifiedHeaders(worksheet, ref currentRow, showStationColumn); + } + else + { + // Existing logic for full headers (for approvers or other reports) + if (isHoU) + { + if (showStationColumn) + { + AddHoUPSTWAirHeaders(worksheet, ref currentRow); + } + else + { + AddHoUNonPSTWAirHeaders(worksheet, ref currentRow); + } + } + else // !isHoU path + { + if (showStationColumn) + { + AddNonHoUPSTWAirHeaders(worksheet, ref currentRow); + } + else + { + AddNonHoUNonPSTWAirHeaders(worksheet, ref currentRow); + } + } + } + // --- End Conditional Header Generation --- + + + var userSetting = _centralDbContext.Hrusersetting + .Include(us => us.State) + .FirstOrDefault(us => us.UserId == user.Id); + + var publicHolidays = _centralDbContext.Holidays + .Where(h => userSetting != null && h.StateId == userSetting.State.StateId) + .ToList(); + + var publicHolidayDates = publicHolidays.Select(h => h.HolidayDate.Date).ToList(); + + + var recordsGroupedByDate = records + .GroupBy(r => r.OtDate.Date) + .ToDictionary(g => g.Key, g => g.ToList()); + + decimal totalAllBreaksMinutes = 0; + decimal totalAllOtMinutes = 0; + + decimal totalOfficeBreak = 0; + decimal totalAfterBreak = 0; + decimal totalOtHrsOffice = 0; + decimal totalOtHrsAfterOffice = 0; + decimal totalNdOt = 0; + decimal totalOdUnder4 = 0; + decimal totalOd4to8 = 0; + decimal totalOdAfter = 0; + decimal totalRdUnder4 = 0; + decimal totalRd4to8 = 0; + decimal totalRdAfter = 0; + decimal totalPhUnder8 = 0; + decimal totalPhAfter = 0; + decimal grandTotalOt = 0; + decimal grandTotalNdOd = 0; + decimal grandTotalRd = 0; + decimal grandTotalPh = 0; + decimal grandTotalOtAmount = 0; + + // UserRate is now interpreted as Basic Salary + var basicSalary = userRate; + var orp = CalculateOrp(basicSalary); // ORP = Basic Salary / 26 + var hrp = CalculateHrp(orp); // HRP = ORP / 8 + bool hasPrintedSalaryDetails = false; + + DateTime? previousDate = null; + + foreach (var date in allDatesInMonth) + { + recordsGroupedByDate.TryGetValue(date, out var dateRecords); + var recordsToShow = dateRecords ?? new List { new OtRegisterModel { OtDate = date } }; + recordsToShow = recordsToShow.OrderBy(r => r.OfficeFrom ?? r.AfterFrom).ToList(); + + foreach (var record in recordsToShow) + { + int col = 1; + + if (isSimplifiedExport) + { + var dayCell = worksheet.Cell(currentRow, col); + var dateCell = worksheet.Cell(currentRow, col + 1); + + if (record.OtDate.Date != previousDate?.Date) + { + dayCell.Value = record.OtDate.ToString("ddd"); + dateCell.Value = record.OtDate.ToString("dd-MM-yyyy"); + } + else + { + dayCell.Value = ""; + dateCell.Value = ""; + } + ApplyDayTypeStyle(dayCell, record.OtDate, userSetting?.State?.WeekendId, publicHolidayDates); + ApplyDayTypeStyle(dateCell, record.OtDate, userSetting?.State?.WeekendId, publicHolidayDates); + col += 2; + + worksheet.Cell(currentRow, col++).Value = record.OfficeFrom?.ToString(@"hh\:mm") ?? ""; + worksheet.Cell(currentRow, col++).Value = record.OfficeTo?.ToString(@"hh\:mm") ?? ""; + worksheet.Cell(currentRow, col++).Value = FormatAsHourMinute(record.OfficeBreak, isMinutes: true); + + worksheet.Cell(currentRow, col++).Value = record.AfterFrom?.ToString(@"hh\:mm") ?? ""; + worksheet.Cell(currentRow, col++).Value = record.AfterTo?.ToString(@"hh\:mm") ?? ""; + worksheet.Cell(currentRow, col++).Value = FormatAsHourMinute(record.AfterBreak, isMinutes: true); + + var currentRecordTotalOtMinutes = (CalculateTimeDifferenceInMinutes(record.OfficeFrom, record.OfficeTo) - record.OfficeBreak.GetValueOrDefault(0)) + + (CalculateTimeDifferenceInMinutes(record.AfterFrom, record.AfterTo) - record.AfterBreak.GetValueOrDefault(0)); + currentRecordTotalOtMinutes = Math.Max(0, currentRecordTotalOtMinutes); + worksheet.Cell(currentRow, col++).Value = FormatAsHourMinute(currentRecordTotalOtMinutes, isMinutes: true); + totalAllOtMinutes += currentRecordTotalOtMinutes; + + var currentRecordTotalBreakMinutes = record.OfficeBreak.GetValueOrDefault(0) + record.AfterBreak.GetValueOrDefault(0); + worksheet.Cell(currentRow, col++).Value = FormatAsHourMinute(currentRecordTotalBreakMinutes, isMinutes: true); + totalAllBreaksMinutes += currentRecordTotalBreakMinutes; + + if (showStationColumn) + { + worksheet.Cell(currentRow, col++).Value = record.Stations?.StationName ?? ""; + } + + var descriptionCell = worksheet.Cell(currentRow, col); + descriptionCell.Value = record.OtDescription ?? ""; + descriptionCell.Style.Alignment.WrapText = true; + descriptionCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + descriptionCell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Top; + col++; + + var rowRange = worksheet.Range(currentRow, 1, currentRow, col - 1); + rowRange.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + rowRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + rowRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + rowRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; + descriptionCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + + } + else + { + var dayType = GetDayType(date, userSetting?.State?.WeekendId, publicHolidayDates); + var classified = ClassifyOt(record, hrp, publicHolidayDates, userSetting?.State?.WeekendId); + + decimal currentNdOt = decimal.TryParse(classified["ndAfter"], out decimal ndOtVal) ? ndOtVal : 0; + decimal currentOdUnder4 = decimal.TryParse(classified["odUnder4"], out decimal odUnder4Val) ? odUnder4Val : 0; + decimal currentOd4to8 = decimal.TryParse(classified["odBetween4And8"], out decimal od4to8Val) ? od4to8Val : 0; + decimal currentOdAfter = decimal.TryParse(classified["odAfter"], out decimal odAfterVal) ? odAfterVal : 0; + decimal currentRdUnder4 = decimal.TryParse(classified["rdUnder4"], out decimal rdUnder4Val) ? rdUnder4Val : 0; + decimal currentRd4to8 = decimal.TryParse(classified["rdBetween4And8"], out decimal rd4to8Val) ? rd4to8Val : 0; + decimal currentRdAfter = decimal.TryParse(classified["rdAfter"], out decimal rdAfterVal) ? rdAfterVal : 0; + decimal currentPhUnder8 = decimal.TryParse(classified["phUnder8"], out decimal phUnder8Val) ? phUnder8Val : 0; + decimal currentPhAfter = decimal.TryParse(classified["phAfter"], out decimal phAfterVal) ? phAfterVal : 0; + + decimal currentRowTotalOt = currentNdOt + currentOdUnder4 + currentOd4to8 + currentOdAfter + + currentRdUnder4 + currentRd4to8 + currentRdAfter + + currentPhUnder8 + currentPhAfter; + + decimal currentRowTotalNdOd = currentNdOt + currentOdUnder4 + currentOd4to8 + currentOdAfter; + decimal currentRowTotalRd = currentRdUnder4 + currentRd4to8 + currentRdAfter; + decimal currentRowTotalPh = currentPhUnder8 + currentPhAfter; + + var otAmt = CalculateOtAmount(record, hrp, publicHolidayDates, userSetting?.State?.WeekendId); + + totalOfficeBreak += (decimal)record.OfficeBreak.GetValueOrDefault(0); + totalAfterBreak += (decimal)record.AfterBreak.GetValueOrDefault(0); + totalOtHrsOffice += ConvertTimeToDecimal(CalculateOfficeOtHours(record)); + totalOtHrsAfterOffice += ConvertTimeToDecimal(CalculateAfterOfficeOtHours(record)); + + totalNdOt += currentNdOt; + totalOdUnder4 += currentOdUnder4; + totalOd4to8 += currentOd4to8; + totalOdAfter += currentOdAfter; + totalRdUnder4 += currentRdUnder4; + totalRd4to8 += currentRd4to8; + totalRdAfter += currentRdAfter; + totalPhUnder8 += currentPhUnder8; + totalPhAfter += currentPhAfter; + + grandTotalOt += currentRowTotalOt; + grandTotalNdOd += currentRowTotalNdOd; + grandTotalRd += currentRowTotalRd; + grandTotalPh += currentRowTotalPh; + grandTotalOtAmount += decimal.TryParse(otAmt, out decimal otAmtVal) ? otAmtVal : 0; + + if (!isHoU) + { + // Basic Salary + worksheet.Cell(currentRow, col).Value = !hasPrintedSalaryDetails ? basicSalary.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + // ORP + worksheet.Cell(currentRow, col).Value = !hasPrintedSalaryDetails ? orp.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + // HRP + worksheet.Cell(currentRow, col).Value = !hasPrintedSalaryDetails ? hrp.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + } + hasPrintedSalaryDetails = true; + + var dayCell = worksheet.Cell(currentRow, col); + var dateCell = worksheet.Cell(currentRow, col + 1); + + if (record.OtDate.Date != previousDate?.Date) + { + dayCell.Value = record.OtDate.ToString("ddd"); + dateCell.Value = record.OtDate.ToString("dd-MM-yyyy"); + } + else + { + dayCell.Value = ""; + dateCell.Value = ""; + } + ApplyDayTypeStyle(dayCell, record.OtDate, userSetting?.State?.WeekendId, publicHolidayDates); + ApplyDayTypeStyle(dateCell, record.OtDate, userSetting?.State?.WeekendId, publicHolidayDates); + col += 2; + + worksheet.Cell(currentRow, col++).Value = record.OfficeFrom?.ToString(@"hh\:mm") ?? ""; + worksheet.Cell(currentRow, col++).Value = record.OfficeTo?.ToString(@"hh\:mm") ?? ""; + worksheet.Cell(currentRow, col++).Value = FormatAsHourMinute(record.OfficeBreak, isMinutes: true); + + worksheet.Cell(currentRow, col++).Value = record.AfterFrom?.ToString(@"hh\:mm") ?? ""; + worksheet.Cell(currentRow, col++).Value = record.AfterTo?.ToString(@"hh\:mm") ?? ""; + worksheet.Cell(currentRow, col++).Value = FormatAsHourMinute(record.AfterBreak, isMinutes: true); + + worksheet.Cell(currentRow, col++).Value = CalculateOfficeOtHours(record); + worksheet.Cell(currentRow, col++).Value = CalculateAfterOfficeOtHours(record); + + worksheet.Cell(currentRow, col++).Value = currentNdOt > 0 ? currentNdOt.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Value = currentOdUnder4 > 0 ? currentOdUnder4.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Value = currentOd4to8 > 0 ? currentOd4to8.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Value = currentOdAfter > 0 ? currentOdAfter.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Value = currentRdUnder4 > 0 ? currentRdUnder4.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Value = currentRd4to8 > 0 ? currentRd4to8.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Value = currentRdAfter > 0 ? currentRdAfter.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Value = currentPhUnder8 > 0 ? currentPhUnder8.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Value = currentPhAfter > 0 ? currentPhAfter.ToString("N2") : ""; + + worksheet.Cell(currentRow, col++).Value = currentRowTotalOt > 0 ? currentRowTotalOt.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Value = currentRowTotalNdOd > 0 ? currentRowTotalNdOd.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Value = currentRowTotalRd > 0 ? currentRowTotalRd.ToString("N2") : ""; + worksheet.Cell(currentRow, col++).Value = currentRowTotalPh > 0 ? currentRowTotalPh.ToString("N2") : ""; + + if (!isHoU) + { + worksheet.Cell(currentRow, col++).Value = otAmt == "0.00" ? "" : otAmt; + } + + if (showStationColumn) + { + worksheet.Cell(currentRow, col++).Value = record.Stations?.StationName ?? ""; + } + + var descriptionCell = worksheet.Cell(currentRow, col); + descriptionCell.Value = record.OtDescription ?? ""; + descriptionCell.Style.Alignment.WrapText = true; + descriptionCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + descriptionCell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Top; + col++; + + var rowRange = worksheet.Range(currentRow, 1, currentRow, col - 1); + rowRange.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + rowRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + rowRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + rowRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; + descriptionCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + } + + currentRow++; + previousDate = record.OtDate.Date; + } + } + + var totalsRowRange = worksheet.Range(currentRow, 1, currentRow, worksheet.LastColumnUsed().ColumnNumber()); + totalsRowRange.Style.Font.SetBold(); + foreach (var cell in totalsRowRange.Cells()) + { + ApplyHeaderStyle(cell, 5); + } + totalsRowRange.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + totalsRowRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + + if (isSimplifiedExport) + { + int totalLabelStartColumnIndex = 1; + int totalLabelEndColumnIndex = 2; + + worksheet.Range(currentRow, totalLabelStartColumnIndex, currentRow, totalLabelEndColumnIndex).Merge(); + worksheet.Cell(currentRow, totalLabelStartColumnIndex).Value = "TOTAL"; + worksheet.Cell(currentRow, totalLabelStartColumnIndex).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + + int currentTotalCol = totalLabelEndColumnIndex + 1; + + currentTotalCol += 6; + + worksheet.Cell(currentRow, currentTotalCol++).Value = FormatAsHourMinute(totalAllOtMinutes, isMinutes: true); + + worksheet.Cell(currentRow, currentTotalCol++).Value = FormatAsHourMinute(totalAllBreaksMinutes, isMinutes: true); + + if (showStationColumn) + { + currentTotalCol++; + } + } + else + { + int totalLabelStartColumnIndex = 1; + int totalLabelEndColumnIndex; + + if (!isHoU) + { + totalLabelEndColumnIndex = 3; // Basic Salary, ORP, HRP are new first three columns + } + else + { + totalLabelEndColumnIndex = 2; + } + + worksheet.Range(currentRow, totalLabelStartColumnIndex, currentRow, totalLabelEndColumnIndex).Merge(); + worksheet.Cell(currentRow, totalLabelStartColumnIndex).Value = "TOTAL"; + worksheet.Cell(currentRow, totalLabelStartColumnIndex).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + + int colOfficeBreakIndex = 0; + int colAfterBreakIndex = 0; + int colNdOtIndex = 0; + int colOdUnder4Index = 0; + int colOd4to8Index = 0; + int colOdAfterIndex = 0; + int colRdUnder4Index = 0; + int colRd4to8Index = 0; + int colRdAfterIndex = 0; + int colPhUnder8Index = 0; + int colPhAfterIndex = 0; + int colTotalOtIndex = 0; + int colTotalNdOdIndex = 0; + int colTotalRdIndex = 0; + int colTotalPhIndex = 0; + int colOtAmtIndex = 0; + + if (!isHoU) + { + // Adjusted column indices due to Basic Salary, ORP, HRP being added at the beginning + colOfficeBreakIndex = 8; + colAfterBreakIndex = 11; + colNdOtIndex = 14; + colOdUnder4Index = 15; + colOd4to8Index = 16; + colOdAfterIndex = 17; + colRdUnder4Index = 18; + colRd4to8Index = 19; + colRdAfterIndex = 20; + colPhUnder8Index = 21; + colPhAfterIndex = 22; + colTotalOtIndex = 23; + colTotalNdOdIndex = 24; + colTotalRdIndex = 25; + colTotalPhIndex = 26; + colOtAmtIndex = 27; + } + else + { + colOfficeBreakIndex = 5; + colAfterBreakIndex = 8; + colNdOtIndex = 11; + colOdUnder4Index = 12; + colOd4to8Index = 13; + colOdAfterIndex = 14; + colRdUnder4Index = 15; + colRd4to8Index = 16; + colRdAfterIndex = 17; + colPhUnder8Index = 18; + colPhAfterIndex = 19; + colTotalOtIndex = 20; + colTotalNdOdIndex = 21; + colTotalRdIndex = 22; + colTotalPhIndex = 23; + } + + worksheet.Cell(currentRow, colOfficeBreakIndex).Value = string.IsNullOrEmpty(FormatAsHourMinute(totalOfficeBreak, isMinutes: true)) ? "0:00" : FormatAsHourMinute(totalOfficeBreak, isMinutes: true); + worksheet.Cell(currentRow, colAfterBreakIndex).Value = string.IsNullOrEmpty(FormatAsHourMinute(totalAfterBreak, isMinutes: true)) ? "0:00" : FormatAsHourMinute(totalAfterBreak, isMinutes: true); + + worksheet.Cell(currentRow, colNdOtIndex).Value = totalNdOt.ToString("N2"); + worksheet.Cell(currentRow, colOdUnder4Index).Value = totalOdUnder4.ToString("N2"); + worksheet.Cell(currentRow, colOd4to8Index).Value = totalOd4to8.ToString("N2"); + worksheet.Cell(currentRow, colOdAfterIndex).Value = totalOdAfter.ToString("N2"); + worksheet.Cell(currentRow, colRdUnder4Index).Value = totalRdUnder4.ToString("N2"); + worksheet.Cell(currentRow, colRd4to8Index).Value = totalRd4to8.ToString("N2"); + worksheet.Cell(currentRow, colRdAfterIndex).Value = totalRdAfter.ToString("N2"); + worksheet.Cell(currentRow, colPhUnder8Index).Value = totalPhUnder8.ToString("N2"); + worksheet.Cell(currentRow, colPhAfterIndex).Value = totalPhAfter.ToString("N2"); + worksheet.Cell(currentRow, colTotalOtIndex).Value = grandTotalOt.ToString("N2"); + worksheet.Cell(currentRow, colTotalNdOdIndex).Value = grandTotalNdOd.ToString("N2"); + worksheet.Cell(currentRow, colTotalRdIndex).Value = grandTotalRd.ToString("N2"); + worksheet.Cell(currentRow, colTotalPhIndex).Value = grandTotalPh.ToString("N2"); + + if (!isHoU) + { + worksheet.Cell(currentRow, colOtAmtIndex).Value = grandTotalOtAmount.ToString("N2"); + } + } + + currentRow++; + + // --- Approval Signatures and Remarks Section (Combined) --- + if (!isSimplifiedExport && otStatus != null) + { + currentRow++; // Add a blank line before approval section + + // Signature section header (left side) + worksheet.Cell(currentRow, 1).Value = "Approval Summary:"; + worksheet.Cell(currentRow, 1).Style.Font.SetBold(); + currentRow++; + + int signatureStartCol = 1; + int signatureColWidth = 5; + + // Calculate remarks start column (same as before) + int remarksStartColumn = worksheet.LastColumnUsed().ColumnNumber() - 2; + + void AddApproverSignature(int column, string role, string status, DateTime? submitDate, int? approverUserId) + { + if (status == "Approved") + { + var approverUser = _centralDbContext.Users.FirstOrDefault(u => u.Id == approverUserId); + if (approverUser != null) + { + var approvedByCell = worksheet.Cell(currentRow, column); + approvedByCell.Value = $"Approved by:"; + approvedByCell.Style.Font.SetBold(); + approvedByCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + + worksheet.Cell(currentRow + 1, column).Value = approverUser.FullName; + worksheet.Cell(currentRow + 1, column).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + worksheet.Cell(currentRow + 1, column).Style.Font.SetFontName("Brush Script MT"); + worksheet.Cell(currentRow + 1, column).Style.Font.SetFontSize(18); + + worksheet.Cell(currentRow + 2, column).Value = approverUser.FullName; + worksheet.Cell(currentRow + 2, column).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + + worksheet.Cell(currentRow + 3, column).Value = submitDate?.ToString("dd MMMM yyyy") ?? "N/A"; + worksheet.Cell(currentRow + 3, column).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + } + } + } + + // Loop to add all approved signatures + var approvalFlow = _centralDbContext.Hrusersetting + .Include(us => us.Approvalflow) + .ThenInclude(af => af.HeadOfUnit) + .Include(us => us.Approvalflow) + .ThenInclude(af => af.HeadOfDepartment) + .Include(us => us.Approvalflow) + .ThenInclude(af => af.ManagerUser) + .Include(us => us.Approvalflow) + .ThenInclude(af => af.HRUser) + .Where(us => us.UserId == user.Id) + .Select(us => us.Approvalflow) + .FirstOrDefault(); + + if (approvalFlow != null) + { + int currentSignatureCol = signatureStartCol; + bool hasAnyApprovedSignature = false; + + if (otStatus.HouStatus == "Approved") + { + AddApproverSignature(currentSignatureCol, "Head of Unit", otStatus.HouStatus, otStatus.HouSubmitDate, approvalFlow.HoU); + currentSignatureCol += signatureColWidth; + hasAnyApprovedSignature = true; + } + + if (otStatus.HodStatus == "Approved") + { + AddApproverSignature(currentSignatureCol, "Head of Department", otStatus.HodStatus, otStatus.HodSubmitDate, approvalFlow.HoD); + currentSignatureCol += signatureColWidth; + hasAnyApprovedSignature = true; + } + + if (otStatus.ManagerStatus == "Approved") + { + AddApproverSignature(currentSignatureCol, "Manager", otStatus.ManagerStatus, otStatus.ManagerSubmitDate, approvalFlow.Manager); + currentSignatureCol += signatureColWidth; + hasAnyApprovedSignature = true; + } + + if (otStatus.HrStatus == "Approved") + { + AddApproverSignature(currentSignatureCol, "HR", otStatus.HrStatus, otStatus.HrSubmitDate, approvalFlow.HR); + currentSignatureCol += signatureColWidth; + hasAnyApprovedSignature = true; + } + + // Add public holidays list below the color indicators + int remarksColorStartRow = currentRow - 1; // Adjust as needed for spacing from signatures + var phColorCell = worksheet.Cell(remarksColorStartRow, remarksStartColumn); + phColorCell.Value = " "; // Small cell for color swatch + phColorCell.Style.Fill.BackgroundColor = XLColor.FromHtml("FFC0CB"); // Pink color + phColorCell.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + + worksheet.Cell(remarksColorStartRow, remarksStartColumn + 1).Value = " Public Holiday"; + worksheet.Cell(remarksColorStartRow, remarksStartColumn + 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + + // Weekend/Off Day Color (right side) - on next row + var weekendColorCell = worksheet.Cell(remarksColorStartRow + 1, remarksStartColumn); + weekendColorCell.Value = " "; // Small cell for color swatch + weekendColorCell.Style.Fill.BackgroundColor = XLColor.FromHtml("ADD8E6"); // Light Blue color + weekendColorCell.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + + worksheet.Cell(remarksColorStartRow + 1, remarksStartColumn + 1).Value = " Weekend"; + worksheet.Cell(remarksColorStartRow + 1, remarksStartColumn + 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + + + // Add public holidays list below the color indicators + int holidayListRow = remarksColorStartRow + 3; // Start below the color indicators + worksheet.Cell(holidayListRow, remarksStartColumn).Value = $"Public Holidays in {displayMonth:MMMM yyyy}:"; + worksheet.Cell(holidayListRow, remarksStartColumn).Style.Font.SetBold(); + holidayListRow++; + + if (publicHolidays != null && publicHolidays.Any()) + { + var monthHolidays = publicHolidays + .Where(h => h.HolidayDate.Year == displayMonth.Year && h.HolidayDate.Month == displayMonth.Month) + .OrderBy(h => h.HolidayDate.Day) + .ToList(); + + if (monthHolidays.Any()) + { + foreach (var holiday in monthHolidays) + { + worksheet.Cell(holidayListRow, remarksStartColumn).Value = $"- {holiday.HolidayDate:dd MMMM}: {holiday.HolidayName}"; + worksheet.Cell(holidayListRow, remarksStartColumn).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + holidayListRow++; + } + } + else + { + worksheet.Cell(holidayListRow, remarksStartColumn).Value = "No public holidays recorded for this month."; + worksheet.Cell(holidayListRow, remarksStartColumn).Style.Font.SetItalic(); + holidayListRow++; + } + } + else + { + worksheet.Cell(holidayListRow, remarksStartColumn).Value = "No public holiday data found."; + worksheet.Cell(holidayListRow, remarksStartColumn).Style.Font.SetItalic(); + holidayListRow++; + } + + // --- START ADDITION --- + // Display the message only if at least one signature was approved and displayed. + if (hasAnyApprovedSignature) + { + int messageRow = holidayListRow + 2; // 2 rows below the last public holiday detail for spacing + + // Merge cells for the message to span across the relevant width + int lastContentColumn = worksheet.LastColumnUsed().ColumnNumber(); + var messageCellRange = worksheet.Range(messageRow, 1, messageRow, lastContentColumn); + messageCellRange.Merge(); + messageCellRange.Value = "This is an automatically generated document. No signature is required for validation."; + messageCellRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + messageCellRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; + messageCellRange.Style.Font.SetItalic(); + messageCellRange.Style.Font.SetFontSize(10); + messageCellRange.Style.Font.FontColor = XLColor.Gray; + + // Update currentRow to be after the message for subsequent content (if any) + currentRow = messageRow + 1; + } + // --- END ADDITION --- + + // Update currentRow to the bottom of the entire remarks section + // This line is now outside the hasAnyApprovedSignature block, and uses the updated currentRow + currentRow = Math.Max(currentRow, holidayListRow); + } + } + else if (isSimplifiedExport) // This is the new section for simplified export remarks + { + // Call the AddRemarksSection here for simplified export + AddRemarksSection(worksheet, ref currentRow, displayMonth, userSetting?.State?.WeekendId, publicHolidays.Select(h => new CalendarModel { HolidayDate = h.HolidayDate, HolidayName = h.HolidayName }).ToList()); + } + // --- End Combined Approval Signatures and Remarks Section --- + + + // --- Column Sizing Logic (Conditional) --- + worksheet.Columns().AdjustToContents(); + + if (isSimplifiedExport) + { + worksheet.Column(1).Width = 10; + worksheet.Column(2).Width = 12; + worksheet.Column(3).Width = 9; + worksheet.Column(4).Width = 9; + worksheet.Column(5).Width = 9; + worksheet.Column(6).Width = 9; + worksheet.Column(7).Width = 9; + worksheet.Column(8).Width = 9; + worksheet.Column(9).Width = 15; + worksheet.Column(10).Width = 10; + + int descCol = 11; + if (showStationColumn) + { + worksheet.Column(11).Width = 15; + descCol = 12; + } + worksheet.Column(descCol).Width = 60; + } + else + { + if (!isHoU) + { + worksheet.Column(1).Width = 15; // Basic Salary + worksheet.Column(2).Width = 10; // ORP + worksheet.Column(3).Width = 10; // HRP + worksheet.Column(4).Width = 10; // Day + worksheet.Column(5).Width = 12; // Date + + worksheet.Column(6).Width = 9; // Office From + worksheet.Column(7).Width = 9; // Office To + worksheet.Column(8).Width = 9; // Office Break + worksheet.Column(9).Width = 9; // After From + worksheet.Column(10).Width = 9; // After To + worksheet.Column(11).Width = 9; // After Break + + worksheet.Column(12).Width = 15; // OT Hrs (Office Hour) + worksheet.Column(13).Width = 18; // OT Hrs (After Office Hour) + + for (int i = 14; i <= 27; i++) + { + worksheet.Column(i).Width = 12; + } + + int descColIndex = showStationColumn ? 29 : 28; + worksheet.Column(descColIndex).Width = 60; + } + else + { + worksheet.Column(1).Width = 10; + worksheet.Column(2).Width = 12; + + worksheet.Column(3).Width = 9; + worksheet.Column(4).Width = 9; + worksheet.Column(5).Width = 9; + worksheet.Column(6).Width = 9; + worksheet.Column(7).Width = 9; + worksheet.Column(8).Width = 9; + + worksheet.Column(9).Width = 15; + worksheet.Column(10).Width = 18; + + for (int i = 11; i <= 23; i++) + { + worksheet.Column(i).Width = 12; + } + + int descColIndex = showStationColumn ? 25 : 24; + worksheet.Column(descColIndex).Width = 60; + } + } + // --- End Column Sizing Logic --- + + workbook.SaveAs(stream); + } + + stream.Position = 0; + return stream; + } + + // Add the new simplified header method (remains the same as previous response) + private void AddSimplifiedHeaders(IXLWorksheet worksheet, ref int rowIndex, bool showStationColumn) + { + int startRowIndex = rowIndex; + int currentCol = 1; + + // Row 1 Headers + var cellDay = worksheet.Cell(rowIndex, currentCol); + cellDay.Value = "Day"; + ApplyHeaderStyle(cellDay, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); + currentCol++; + + var cellDate = worksheet.Cell(rowIndex, currentCol); + cellDate.Value = "Date"; + ApplyHeaderStyle(cellDate, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); + currentCol++; + + var cellOfficeHour = worksheet.Cell(rowIndex, currentCol); + cellOfficeHour.Value = "Office Hour"; + ApplyHeaderStyle(cellOfficeHour, 2); + var mergedRangeOfficeHour = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); + mergedRangeOfficeHour.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeOfficeHour.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellAfterOfficeHour = worksheet.Cell(rowIndex, currentCol); + cellAfterOfficeHour.Value = "After Office Hour"; + ApplyHeaderStyle(cellAfterOfficeHour, 2); + var mergedRangeAfterOfficeHour = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); + mergedRangeAfterOfficeHour.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeAfterOfficeHour.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellTotalOTHrs = worksheet.Cell(rowIndex, currentCol); + cellTotalOTHrs.Value = "Total OT Hours"; + ApplyHeaderStyle(cellTotalOTHrs, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); + currentCol++; + + var cellBreakMin = worksheet.Cell(rowIndex, currentCol); + cellBreakMin.Value = "Break (min)"; + ApplyHeaderStyle(cellBreakMin, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); + currentCol++; + + if (showStationColumn) + { + var cellStation = worksheet.Cell(rowIndex, currentCol); + cellStation.Value = "Station"; + ApplyHeaderStyle(cellStation, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); + currentCol++; + } + + var cellDescription = worksheet.Cell(rowIndex, currentCol); + cellDescription.Value = "Description"; + ApplyHeaderStyle(cellDescription, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); + currentCol++; + + // Apply borders to the full header range for Row 1 + var headerRow1Range = worksheet.Range(startRowIndex, 1, startRowIndex, worksheet.LastColumnUsed().ColumnNumber()); + headerRow1Range.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + headerRow1Range.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + rowIndex++; // Move to the next row for sub-headers + + // Row 2 Sub-Headers + currentCol = 1; + currentCol += 2; // Skip Day and Date (vertically merged) + + var cellOfficeFrom = worksheet.Cell(rowIndex, currentCol++); + cellOfficeFrom.Value = "From"; + ApplyHeaderStyle(cellOfficeFrom, 3); + + var cellOfficeTo = worksheet.Cell(rowIndex, currentCol++); + cellOfficeTo.Value = "To"; + ApplyHeaderStyle(cellOfficeTo, 3); + + var cellOfficeBreak = worksheet.Cell(rowIndex, currentCol++); + cellOfficeBreak.Value = "Break"; + ApplyHeaderStyle(cellOfficeBreak, 3); + + var cellAfterFrom = worksheet.Cell(rowIndex, currentCol++); + cellAfterFrom.Value = "From"; + ApplyHeaderStyle(cellAfterFrom, 3); + + var cellAfterTo = worksheet.Cell(rowIndex, currentCol++); + cellAfterTo.Value = "To"; + ApplyHeaderStyle(cellAfterTo, 3); + + var cellAfterBreak = worksheet.Cell(rowIndex, currentCol++); + cellAfterBreak.Value = "Break"; + ApplyHeaderStyle(cellAfterBreak, 3); + + // Skip Total OT Hours and Break (min) (vertically merged) + currentCol += 2; + + if (showStationColumn) + { + currentCol++; // Skip Station (vertically merged) + } + + // Skip Description (vertically merged) + currentCol++; + + + // Apply borders to the full header range for Row 2 + var headerRow2Range = worksheet.Range(rowIndex, 1, rowIndex, worksheet.LastColumnUsed().ColumnNumber()); + headerRow2Range.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + headerRow2Range.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + rowIndex++; // Move to the next row for data + } + + + // --- Helper methods (AddNonHoUPSTWAirHeaders, AddNonHoUNonPSTWAirHeaders, AddHoUPSTWAirHeaders, AddHoUNonPSTWAirHeaders, + // ApplyHeaderStyle, ApplyDayTypeStyle, GetDayCellColorStyleIndex, IsAdmin, GetAllDatesInMonth, GetDayType, CalculateOrp, + // FormatAsHourMinute, ConvertTimeToDecimal, CalculateTimeDifferenceInMinutes, CalculateOfficeOtHours, CalculateAfterOfficeOtHours, + // ClassifyOt, CalculateTotalOtHrs, CalculateNdOdTotal, CalculateRdTotal, CalculatePhTotal, CalculateOtAmount) + // defined only once below this line. + + private void ApplyHeaderStyle(IXLCell cell, int styleType) + { + cell.Style.Font.SetBold(); + cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; + cell.Style.Alignment.WrapText = true; + cell.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + cell.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + switch (styleType) + { + case 1: // Primary Headers (e.g., "Day", "OT Hrs", "Total OT") - Darker Blue + cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#78aafa"); // A standard blue + cell.Style.Font.FontColor = XLColor.Black; // Changed to Black + break; + case 2: // Grouping Headers (e.g., "Office Hour", "After Office Hour", "Off Day", "Rest Day", "Public Holiday") - Light Blue + cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#DDEBF7"); // Lighter blue + cell.Style.Font.FontColor = XLColor.Black; + break; + case 3: // Sub-Headers (e.g., "From", "To", "Break", "ND OT", "OD < 4") - Light Green + cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#C6E0B4"); // Light green + cell.Style.Font.FontColor = XLColor.Black; + break; + case 4: // Specific headers that need a different color (like "Description" if it needs unique) - Not used in this update, but kept as an option. + cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#F8CBAD"); // Light orange + cell.Style.Font.FontColor = XLColor.Black; + break; + case 5: // Totals row background - Purple + cell.Style.Fill.BackgroundColor = XLColor.FromHtml("D8D1F5"); // Original purple for totals + cell.Style.Font.FontColor = XLColor.Black; + break; + case 6: // Pink for Basic Salary, ORP, HRP, OT Amt + cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#FF69B4"); // Hot Pink + cell.Style.Font.FontColor = XLColor.Black; // Changed to Black + break; + default: // Fallback + cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#A9D08E"); + cell.Style.Font.FontColor = XLColor.Black; + break; + } + } + private void ApplyDayTypeStyle(IXLCell cell, DateTime date, int? weekendId, List publicHolidays) + { + uint styleIndex = GetDayCellColorStyleIndex(date, weekendId, publicHolidays); + cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; + cell.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + cell.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + + switch (styleIndex) + { + case 18: // Pink for public holidays (FFC0CB) + cell.Style.Fill.BackgroundColor = XLColor.FromHtml("FFC0CB"); + break; + case 19: // Light Blue for weekends/off days (ADD8E6) + cell.Style.Fill.BackgroundColor = XLColor.FromHtml("ADD8E6"); + break; + default: // Default (no specific background fill) + cell.Style.Fill.BackgroundColor = XLColor.NoColor; + break; + } + } + + // Method to add headers for Non-HoU, PSTWAIR (includes Basic Salary, ORP, HRP, Station) + private void AddNonHoUPSTWAirHeaders(IXLWorksheet worksheet, ref int rowIndex) + { + int startRowIndex = rowIndex; + int currentCol = 1; + + // Row 1 Headers + currentCol = 1; + var cellA = worksheet.Cell(rowIndex, currentCol); + cellA.Value = "Basic Salary (RM)"; + ApplyHeaderStyle(cellA, 6); // Set to pink + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellB = worksheet.Cell(rowIndex, currentCol); + cellB.Value = "ORP"; + ApplyHeaderStyle(cellB, 6); // Set to pink + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellC = worksheet.Cell(rowIndex, currentCol); + cellC.Value = "HRP"; + ApplyHeaderStyle(cellC, 6); // Set to pink + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellD = worksheet.Cell(rowIndex, currentCol); + cellD.Value = "Day"; + ApplyHeaderStyle(cellD, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellE = worksheet.Cell(rowIndex, currentCol); + cellE.Value = "Date"; + ApplyHeaderStyle(cellE, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellF = worksheet.Cell(rowIndex, currentCol); + cellF.Value = "Office Hour"; + ApplyHeaderStyle(cellF, 2); + var mergedRangeF = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge F,G,H for Office Hour + mergedRangeF.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeF.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellI = worksheet.Cell(rowIndex, currentCol); + cellI.Value = "After Office Hour"; + ApplyHeaderStyle(cellI, 2); + var mergedRangeI = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge I,J,K for After Office Hour + mergedRangeI.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeI.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellL = worksheet.Cell(rowIndex, currentCol); + cellL.Value = "OT Hrs (Office Hour)"; + ApplyHeaderStyle(cellL, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellM = worksheet.Cell(rowIndex, currentCol); + cellM.Value = "OT Hrs (After Office Hour)"; + ApplyHeaderStyle(cellM, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellN = worksheet.Cell(rowIndex, currentCol); + cellN.Value = "Normal Day (After Office)"; + ApplyHeaderStyle(cellN, 2); // Changed to 2 to match 'Off Day' color + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellO = worksheet.Cell(rowIndex, currentCol); + cellO.Value = "Off Day"; + ApplyHeaderStyle(cellO, 2); // Already using 2 + var mergedRangeO = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge O,P,Q for Off Day + mergedRangeO.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeO.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellR = worksheet.Cell(rowIndex, currentCol); + cellR.Value = "Rest Day"; + ApplyHeaderStyle(cellR, 2); + var mergedRangeR = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge R,S,T for Rest Day + mergedRangeR.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeR.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellU = worksheet.Cell(rowIndex, currentCol); + cellU.Value = "Public Holiday"; + ApplyHeaderStyle(cellU, 2); + var mergedRangeU = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 1).Merge(); // Merge U,V for Public Holiday + mergedRangeU.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeU.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 2; + + var cellW = worksheet.Cell(rowIndex, currentCol); + cellW.Value = "Total OT"; + ApplyHeaderStyle(cellW, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellX = worksheet.Cell(rowIndex, currentCol); + cellX.Value = "Total ND & OD"; + ApplyHeaderStyle(cellX, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellY = worksheet.Cell(rowIndex, currentCol); + cellY.Value = "Total RD"; + ApplyHeaderStyle(cellY, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellZ = worksheet.Cell(rowIndex, currentCol); + cellZ.Value = "Total PH"; + ApplyHeaderStyle(cellZ, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellAA = worksheet.Cell(rowIndex, currentCol); + cellAA.Value = "OT Amt (RM)"; + ApplyHeaderStyle(cellAA, 6); // Set to pink + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellAB = worksheet.Cell(rowIndex, currentCol); + cellAB.Value = "Station"; + ApplyHeaderStyle(cellAB, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellAC = worksheet.Cell(rowIndex, currentCol); + cellAC.Value = "Description"; + ApplyHeaderStyle(cellAC, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var headerRow1Range = worksheet.Range(startRowIndex, 1, startRowIndex, worksheet.LastColumnUsed().ColumnNumber()); + headerRow1Range.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + headerRow1Range.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + + rowIndex++; // Move to the next row for sub-headers + + // Row 2 Sub-Headers + currentCol = 1; + // These cells are part of vertical merges from Row 1, so they are conceptually empty, + // but we need to ensure borders are drawn for the entire merged block. + // We just advance the column counter past the merged cells from Row 1. + currentCol += 5; // Basic Salary, ORP, HRP, Day, Date + + var cellF_sub = worksheet.Cell(rowIndex, currentCol++); + cellF_sub.Value = "From"; + ApplyHeaderStyle(cellF_sub, 3); + + var cellG_sub = worksheet.Cell(rowIndex, currentCol++); + cellG_sub.Value = "To"; + ApplyHeaderStyle(cellG_sub, 3); + + var cellH_sub = worksheet.Cell(rowIndex, currentCol++); + cellH_sub.Value = "Break"; + ApplyHeaderStyle(cellH_sub, 3); + + var cellI_sub = worksheet.Cell(rowIndex, currentCol++); + cellI_sub.Value = "From"; + ApplyHeaderStyle(cellI_sub, 3); + + var cellJ_sub = worksheet.Cell(rowIndex, currentCol++); + cellJ_sub.Value = "To"; + ApplyHeaderStyle(cellJ_sub, 3); + + var cellK_sub = worksheet.Cell(rowIndex, currentCol++); + cellK_sub.Value = "Break"; + ApplyHeaderStyle(cellK_sub, 3); + + currentCol += 2; // Skip OT Hrs (Office Hour) and OT Hrs (After Office Hour) (vertically merged) + + var cellN_sub = worksheet.Cell(rowIndex, currentCol++); + cellN_sub.Value = "ND OT"; + ApplyHeaderStyle(cellN_sub, 3); + + var cellO_sub = worksheet.Cell(rowIndex, currentCol++); + cellO_sub.Value = "OD < 4"; + ApplyHeaderStyle(cellO_sub, 3); + + var cellP_sub = worksheet.Cell(rowIndex, currentCol++); + cellP_sub.Value = "OD 4-8"; + ApplyHeaderStyle(cellP_sub, 3); + + var cellQ_sub = worksheet.Cell(rowIndex, currentCol++); + cellQ_sub.Value = "OD After"; + ApplyHeaderStyle(cellQ_sub, 3); + + var cellR_sub = worksheet.Cell(rowIndex, currentCol++); + cellR_sub.Value = "RD < 4"; + ApplyHeaderStyle(cellR_sub, 3); + + var cellS_sub = worksheet.Cell(rowIndex, currentCol++); + cellS_sub.Value = "RD 4-8"; + ApplyHeaderStyle(cellS_sub, 3); + + var cellT_sub = worksheet.Cell(rowIndex, currentCol++); + cellT_sub.Value = "RD After"; + ApplyHeaderStyle(cellT_sub, 3); + + var cellU_sub = worksheet.Cell(rowIndex, currentCol++); + cellU_sub.Value = "PH < 8"; + ApplyHeaderStyle(cellU_sub, 3); + + var cellV_sub = worksheet.Cell(rowIndex, currentCol++); + cellV_sub.Value = "PH After"; + ApplyHeaderStyle(cellV_sub, 3); + + // Skip Total OT, Total ND & OD, Total RD, Total PH, OT Amt (RM), Station, Description (vertically merged) + currentCol += 7; + + // Apply borders to the full header range for Row 2 + var headerRow2Range = worksheet.Range(rowIndex, 1, rowIndex, worksheet.LastColumnUsed().ColumnNumber()); + headerRow2Range.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + headerRow2Range.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + rowIndex++; // Move to the next row for data + } + + // Method to add headers for Non-HoU, Non-PSTWAIR (includes Basic Salary, ORP, HRP, NO Station) + private void AddNonHoUNonPSTWAirHeaders(IXLWorksheet worksheet, ref int rowIndex) + { + int startRowIndex = rowIndex; + int currentCol; + + // Row 1 Headers + currentCol = 1; + var cellA = worksheet.Cell(rowIndex, currentCol); + cellA.Value = "Basic Salary (RM)"; + ApplyHeaderStyle(cellA, 6); // Set to pink + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellB = worksheet.Cell(rowIndex, currentCol); + cellB.Value = "ORP"; + ApplyHeaderStyle(cellB, 6); // Set to pink + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellC = worksheet.Cell(rowIndex, currentCol); + cellC.Value = "HRP"; + ApplyHeaderStyle(cellC, 6); // Set to pink + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellD = worksheet.Cell(rowIndex, currentCol); + cellD.Value = "Day"; + ApplyHeaderStyle(cellD, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellE = worksheet.Cell(rowIndex, currentCol); + cellE.Value = "Date"; + ApplyHeaderStyle(cellE, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellF = worksheet.Cell(rowIndex, currentCol); + cellF.Value = "Office Hour"; + ApplyHeaderStyle(cellF, 2); + var mergedRangeF = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge F,G,H + mergedRangeF.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeF.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellI = worksheet.Cell(rowIndex, currentCol); + cellI.Value = "After Office Hour"; + ApplyHeaderStyle(cellI, 2); + var mergedRangeI = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge I,J,K + mergedRangeI.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeI.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellL = worksheet.Cell(rowIndex, currentCol); + cellL.Value = "OT Hrs (Office Hour)"; + ApplyHeaderStyle(cellL, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellM = worksheet.Cell(rowIndex, currentCol); + cellM.Value = "OT Hrs (After Office Hour)"; + ApplyHeaderStyle(cellM, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellN = worksheet.Cell(rowIndex, currentCol); + cellN.Value = "Normal Day (After Office)"; + ApplyHeaderStyle(cellN, 2); // Changed to 2 to match 'Off Day' color + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellO = worksheet.Cell(rowIndex, currentCol); + cellO.Value = "Off Day"; + ApplyHeaderStyle(cellO, 2); // Already using 2 + var mergedRangeO = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge O,P,Q + mergedRangeO.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeO.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellR = worksheet.Cell(rowIndex, currentCol); + cellR.Value = "Rest Day"; + ApplyHeaderStyle(cellR, 2); + var mergedRangeR = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge R,S,T + mergedRangeR.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeR.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellU = worksheet.Cell(rowIndex, currentCol); + cellU.Value = "Public Holiday"; + ApplyHeaderStyle(cellU, 2); + var mergedRangeU = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 1).Merge(); // Merge U,V + mergedRangeU.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeU.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 2; + + var cellW = worksheet.Cell(rowIndex, currentCol); + cellW.Value = "Total OT"; + ApplyHeaderStyle(cellW, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellX = worksheet.Cell(rowIndex, currentCol); + cellX.Value = "Total ND & OD"; + ApplyHeaderStyle(cellX, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellY = worksheet.Cell(rowIndex, currentCol); + cellY.Value = "Total RD"; + ApplyHeaderStyle(cellY, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellZ = worksheet.Cell(rowIndex, currentCol); + cellZ.Value = "Total PH"; + ApplyHeaderStyle(cellZ, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellAA = worksheet.Cell(rowIndex, currentCol); + cellAA.Value = "OT Amt (RM)"; + ApplyHeaderStyle(cellAA, 6); // Set to pink + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellAB = worksheet.Cell(rowIndex, currentCol); + cellAB.Value = "Description"; // AB (No Station Column) + ApplyHeaderStyle(cellAB, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var headerRow1Range = worksheet.Range(startRowIndex, 1, startRowIndex, worksheet.LastColumnUsed().ColumnNumber()); + headerRow1Range.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + headerRow1Range.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + rowIndex++; // Move to the next row for sub-headers + + // Row 2 Sub-Headers + currentCol = 1; + currentCol += 5; // Basic Salary, ORP, HRP, Day, Date + + var cellF_sub = worksheet.Cell(rowIndex, currentCol++); + cellF_sub.Value = "From"; + ApplyHeaderStyle(cellF_sub, 3); + + var cellG_sub = worksheet.Cell(rowIndex, currentCol++); + cellG_sub.Value = "To"; + ApplyHeaderStyle(cellG_sub, 3); + + var cellH_sub = worksheet.Cell(rowIndex, currentCol++); + cellH_sub.Value = "Break"; + ApplyHeaderStyle(cellH_sub, 3); + + var cellI_sub = worksheet.Cell(rowIndex, currentCol++); + cellI_sub.Value = "From"; + ApplyHeaderStyle(cellI_sub, 3); + + var cellJ_sub = worksheet.Cell(rowIndex, currentCol++); + cellJ_sub.Value = "To"; + ApplyHeaderStyle(cellJ_sub, 3); + + var cellK_sub = worksheet.Cell(rowIndex, currentCol++); + cellK_sub.Value = "Break"; + ApplyHeaderStyle(cellK_sub, 3); + + currentCol += 2; // Skip OT Hrs (Office Hour) and OT Hrs (After Office Hour) (vertically merged) + + var cellN_sub = worksheet.Cell(rowIndex, currentCol++); + cellN_sub.Value = "ND OT"; + ApplyHeaderStyle(cellN_sub, 3); + + var cellO_sub = worksheet.Cell(rowIndex, currentCol++); + cellO_sub.Value = "OD < 4"; + ApplyHeaderStyle(cellO_sub, 3); + + var cellP_sub = worksheet.Cell(rowIndex, currentCol++); + cellP_sub.Value = "OD 4-8"; + ApplyHeaderStyle(cellP_sub, 3); + + var cellQ_sub = worksheet.Cell(rowIndex, currentCol++); + cellQ_sub.Value = "OD After"; + ApplyHeaderStyle(cellQ_sub, 3); + + var cellR_sub = worksheet.Cell(rowIndex, currentCol++); + cellR_sub.Value = "RD < 4"; + ApplyHeaderStyle(cellR_sub, 3); + + var cellS_sub = worksheet.Cell(rowIndex, currentCol++); + cellS_sub.Value = "RD 4-8"; + ApplyHeaderStyle(cellS_sub, 3); + + var cellT_sub = worksheet.Cell(rowIndex, currentCol++); + cellT_sub.Value = "RD After"; + ApplyHeaderStyle(cellT_sub, 3); + + var cellU_sub = worksheet.Cell(rowIndex, currentCol++); + cellU_sub.Value = "PH < 8"; + ApplyHeaderStyle(cellU_sub, 3); + + var cellV_sub = worksheet.Cell(rowIndex, currentCol++); + cellV_sub.Value = "PH After"; + ApplyHeaderStyle(cellV_sub, 3); + + currentCol += 6; // Skip Total OT, Total ND & OD, Total RD, Total PH, OT Amt (RM), Description (vertically merged) + + // Apply borders to the full header range for Row 2 + var headerRow2Range = worksheet.Range(rowIndex, 1, rowIndex, worksheet.LastColumnUsed().ColumnNumber()); + headerRow2Range.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + headerRow2Range.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + rowIndex++; // Move to the next row for data + } + + // Method to add headers for HoU, PSTWAIR (NO Basic Salary, ORP, HRP, includes Station) + private void AddHoUPSTWAirHeaders(IXLWorksheet worksheet, ref int rowIndex) + { + int startRowIndex = rowIndex; + int currentCol = 1; + + // Row 1 Headers + currentCol = 1; + var cellA = worksheet.Cell(rowIndex, currentCol); + cellA.Value = "Day"; + ApplyHeaderStyle(cellA, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellB = worksheet.Cell(rowIndex, currentCol); + cellB.Value = "Date"; + ApplyHeaderStyle(cellB, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellC = worksheet.Cell(rowIndex, currentCol); + cellC.Value = "Office Hour"; + ApplyHeaderStyle(cellC, 2); + var mergedRangeC = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge C,D,E + mergedRangeC.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeC.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellF = worksheet.Cell(rowIndex, currentCol); + cellF.Value = "After Office Hour"; + ApplyHeaderStyle(cellF, 2); + var mergedRangeF = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge F,G,H + mergedRangeF.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeF.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellI = worksheet.Cell(rowIndex, currentCol); + cellI.Value = "OT Hrs (Office Hour)"; + ApplyHeaderStyle(cellI, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellJ = worksheet.Cell(rowIndex, currentCol); + cellJ.Value = "OT Hrs (After Office Hour)"; + ApplyHeaderStyle(cellJ, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellK = worksheet.Cell(rowIndex, currentCol); + cellK.Value = "Normal Day (After Office)"; + ApplyHeaderStyle(cellK, 2); // Changed to 2 to match 'Off Day' color + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellL = worksheet.Cell(rowIndex, currentCol); + cellL.Value = "Off Day"; + ApplyHeaderStyle(cellL, 2); // Already using 2 + var mergedRangeL = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge L,M,N + mergedRangeL.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeL.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellO = worksheet.Cell(rowIndex, currentCol); + cellO.Value = "Rest Day"; + ApplyHeaderStyle(cellO, 2); + var mergedRangeO = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge O,P,Q + mergedRangeO.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeO.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellR = worksheet.Cell(rowIndex, currentCol); + cellR.Value = "Public Holiday"; + ApplyHeaderStyle(cellR, 2); + var mergedRangeR = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 1).Merge(); // Merge R,S + mergedRangeR.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeR.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 2; + + var cellT = worksheet.Cell(rowIndex, currentCol); + cellT.Value = "Total OT"; + ApplyHeaderStyle(cellT, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellU = worksheet.Cell(rowIndex, currentCol); + cellU.Value = "Total ND & OD"; + ApplyHeaderStyle(cellU, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellV = worksheet.Cell(rowIndex, currentCol); + cellV.Value = "Total RD"; + ApplyHeaderStyle(cellV, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellW = worksheet.Cell(rowIndex, currentCol); + cellW.Value = "Total PH"; + ApplyHeaderStyle(cellW, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellX = worksheet.Cell(rowIndex, currentCol); + cellX.Value = "Station"; + ApplyHeaderStyle(cellX, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellY = worksheet.Cell(rowIndex, currentCol); + cellY.Value = "Description"; + ApplyHeaderStyle(cellY, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var headerRow1Range = worksheet.Range(startRowIndex, 1, startRowIndex, worksheet.LastColumnUsed().ColumnNumber()); + headerRow1Range.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + headerRow1Range.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + rowIndex++; // Move to the next row for sub-headers + + // Row 2 Sub-Headers + currentCol = 1; + currentCol += 2; // Day, Date + + var cellC_sub = worksheet.Cell(rowIndex, currentCol++); + cellC_sub.Value = "From"; + ApplyHeaderStyle(cellC_sub, 3); + + var cellD_sub = worksheet.Cell(rowIndex, currentCol++); + cellD_sub.Value = "To"; + ApplyHeaderStyle(cellD_sub, 3); + + var cellE_sub = worksheet.Cell(rowIndex, currentCol++); + cellE_sub.Value = "Break"; + ApplyHeaderStyle(cellE_sub, 3); + + var cellF_sub = worksheet.Cell(rowIndex, currentCol++); + cellF_sub.Value = "From"; + ApplyHeaderStyle(cellF_sub, 3); + + var cellG_sub = worksheet.Cell(rowIndex, currentCol++); + cellG_sub.Value = "To"; + ApplyHeaderStyle(cellG_sub, 3); + + var cellH_sub = worksheet.Cell(rowIndex, currentCol++); + cellH_sub.Value = "Break"; + ApplyHeaderStyle(cellH_sub, 3); + + currentCol += 2; // Skip OT Hrs (Office Hour) and OT Hrs (After Office Hour) (vertically merged) + + var cellK_sub = worksheet.Cell(rowIndex, currentCol++); + cellK_sub.Value = "ND OT"; + ApplyHeaderStyle(cellK_sub, 3); + + var cellL_sub = worksheet.Cell(rowIndex, currentCol++); + cellL_sub.Value = "OD < 4"; + ApplyHeaderStyle(cellL_sub, 3); + + var cellM_sub = worksheet.Cell(rowIndex, currentCol++); + cellM_sub.Value = "OD 4-8"; + ApplyHeaderStyle(cellM_sub, 3); + + var cellN_sub = worksheet.Cell(rowIndex, currentCol++); + cellN_sub.Value = "OD After"; + ApplyHeaderStyle(cellN_sub, 3); + + var cellO_sub = worksheet.Cell(rowIndex, currentCol++); + cellO_sub.Value = "RD < 4"; + ApplyHeaderStyle(cellO_sub, 3); + + var cellP_sub = worksheet.Cell(rowIndex, currentCol++); + cellP_sub.Value = "RD 4-8"; + ApplyHeaderStyle(cellP_sub, 3); + + var cellQ_sub = worksheet.Cell(rowIndex, currentCol++); + cellQ_sub.Value = "RD After"; + ApplyHeaderStyle(cellQ_sub, 3); + + var cellR_sub = worksheet.Cell(rowIndex, currentCol++); + cellR_sub.Value = "PH < 8"; + ApplyHeaderStyle(cellR_sub, 3); + + var cellS_sub = worksheet.Cell(rowIndex, currentCol++); + cellS_sub.Value = "PH After"; + ApplyHeaderStyle(cellS_sub, 3); + + currentCol += 5; // Skip Total OT, Total ND & OD, Total RD, Total PH, Station, Description (vertically merged) + + // Apply borders to the full header range for Row 2 + var headerRow2Range = worksheet.Range(rowIndex, 1, rowIndex, worksheet.LastColumnUsed().ColumnNumber()); + headerRow2Range.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + headerRow2Range.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + rowIndex++; // Move to the next row for data + } + + // Method to add headers for HoU, Non-PSTWAIR (NO Basic Salary, ORP, HRP, NO Station) + private void AddHoUNonPSTWAirHeaders(IXLWorksheet worksheet, ref int rowIndex) + { + int startRowIndex = rowIndex; + int currentCol; + + // Row 1 Headers + currentCol = 1; + var cellA = worksheet.Cell(rowIndex, currentCol); + cellA.Value = "Day"; + ApplyHeaderStyle(cellA, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellB = worksheet.Cell(rowIndex, currentCol); + cellB.Value = "Date"; + ApplyHeaderStyle(cellB, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellC = worksheet.Cell(rowIndex, currentCol); + cellC.Value = "Office Hour"; + ApplyHeaderStyle(cellC, 2); + var mergedRangeC = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge C,D,E + mergedRangeC.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeC.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellF = worksheet.Cell(rowIndex, currentCol); + cellF.Value = "After Office Hour"; + ApplyHeaderStyle(cellF, 2); + var mergedRangeF = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge F,G,H + mergedRangeF.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeF.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellI = worksheet.Cell(rowIndex, currentCol); + cellI.Value = "OT Hrs (Office Hour)"; + ApplyHeaderStyle(cellI, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellJ = worksheet.Cell(rowIndex, currentCol); + cellJ.Value = "OT Hrs (After Office Hour)"; + ApplyHeaderStyle(cellJ, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellK = worksheet.Cell(rowIndex, currentCol); + cellK.Value = "Normal Day (After Office)"; + ApplyHeaderStyle(cellK, 2); // Changed to 2 to match 'Off Day' color + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellL = worksheet.Cell(rowIndex, currentCol); + cellL.Value = "Off Day"; + ApplyHeaderStyle(cellL, 2); // Already using 2 + var mergedRangeL = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge L,M,N + mergedRangeL.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeL.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellO = worksheet.Cell(rowIndex, currentCol); + cellO.Value = "Rest Day"; + ApplyHeaderStyle(cellO, 2); + var mergedRangeO = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 2).Merge(); // Merge O,P,Q + mergedRangeO.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeO.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 3; + + var cellR = worksheet.Cell(rowIndex, currentCol); + cellR.Value = "Public Holiday"; + ApplyHeaderStyle(cellR, 2); + var mergedRangeR = worksheet.Range(rowIndex, currentCol, rowIndex, currentCol + 1).Merge(); // Merge R,S + mergedRangeR.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + mergedRangeR.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + currentCol += 2; + + var cellT = worksheet.Cell(rowIndex, currentCol); + cellT.Value = "Total OT"; + ApplyHeaderStyle(cellT, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellU = worksheet.Cell(rowIndex, currentCol); + cellU.Value = "Total ND & OD"; + ApplyHeaderStyle(cellU, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellV = worksheet.Cell(rowIndex, currentCol); + cellV.Value = "Total RD"; + ApplyHeaderStyle(cellV, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellW = worksheet.Cell(rowIndex, currentCol); + cellW.Value = "Total PH"; + ApplyHeaderStyle(cellW, 1); // Changed to style 1 (Darker blue) + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var cellX = worksheet.Cell(rowIndex, currentCol); + cellX.Value = "Description"; // X (No Station Column) + ApplyHeaderStyle(cellX, 1); + worksheet.Range(rowIndex, currentCol, rowIndex + 1, currentCol).Merge(); // Merge vertically + currentCol++; + + var headerRow1Range = worksheet.Range(startRowIndex, 1, startRowIndex, worksheet.LastColumnUsed().ColumnNumber()); + headerRow1Range.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + headerRow1Range.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + rowIndex++; // Move to the next row for sub-headers + + // Row 2 Sub-Headers + currentCol = 1; + currentCol += 2; // Day, Date + + var cellC_sub = worksheet.Cell(rowIndex, currentCol++); + cellC_sub.Value = "From"; + ApplyHeaderStyle(cellC_sub, 3); + + var cellD_sub = worksheet.Cell(rowIndex, currentCol++); + cellD_sub.Value = "To"; + ApplyHeaderStyle(cellD_sub, 3); + + var cellE_sub = worksheet.Cell(rowIndex, currentCol++); + cellE_sub.Value = "Break"; + ApplyHeaderStyle(cellE_sub, 3); + + var cellF_sub = worksheet.Cell(rowIndex, currentCol++); + cellF_sub.Value = "From"; + ApplyHeaderStyle(cellF_sub, 3); + + var cellG_sub = worksheet.Cell(rowIndex, currentCol++); + cellG_sub.Value = "To"; + ApplyHeaderStyle(cellG_sub, 3); + + var cellH_sub = worksheet.Cell(rowIndex, currentCol++); + cellH_sub.Value = "Break"; + ApplyHeaderStyle(cellH_sub, 3); + + currentCol += 2; // Skip OT Hrs (Office Hour) and OT Hrs (After Office Hour) (vertically merged) + + var cellK_sub = worksheet.Cell(rowIndex, currentCol++); + cellK_sub.Value = "ND OT"; + ApplyHeaderStyle(cellK_sub, 3); + + var cellL_sub = worksheet.Cell(rowIndex, currentCol++); + cellL_sub.Value = "OD < 4"; + ApplyHeaderStyle(cellL_sub, 3); + + var cellM_sub = worksheet.Cell(rowIndex, currentCol++); + cellM_sub.Value = "OD 4-8"; + ApplyHeaderStyle(cellM_sub, 3); + + var cellN_sub = worksheet.Cell(rowIndex, currentCol++); + cellN_sub.Value = "OD After"; + ApplyHeaderStyle(cellN_sub, 3); + + var cellO_sub = worksheet.Cell(rowIndex, currentCol++); + cellO_sub.Value = "RD < 4"; + ApplyHeaderStyle(cellO_sub, 3); + + var cellP_sub = worksheet.Cell(rowIndex, currentCol++); + cellP_sub.Value = "RD 4-8"; + ApplyHeaderStyle(cellP_sub, 3); + + var cellQ_sub = worksheet.Cell(rowIndex, currentCol++); + cellQ_sub.Value = "RD After"; + ApplyHeaderStyle(cellQ_sub, 3); + + var cellR_sub = worksheet.Cell(rowIndex, currentCol++); + cellR_sub.Value = "PH < 8"; + ApplyHeaderStyle(cellR_sub, 3); + + var cellS_sub = worksheet.Cell(rowIndex, currentCol++); + cellS_sub.Value = "PH After"; + ApplyHeaderStyle(cellS_sub, 3); + + currentCol += 4; // Skip Total OT, Total ND & OD, Total RD, Total PH, Description (vertically merged) + + // Apply borders to the full header range for Row 2 + var headerRow2Range = worksheet.Range(rowIndex, 1, rowIndex, worksheet.LastColumnUsed().ColumnNumber()); + headerRow2Range.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + headerRow2Range.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + + rowIndex++; // Move to the next row for data + } + + + private uint GetDayCellColorStyleIndex(DateTime date, int? weekendId, List publicHolidays) + { + if (publicHolidays.Contains(date.Date)) + return 18; // Style 18: Pink for public holidays + + DayOfWeek dayOfWeek = date.DayOfWeek; + if ((weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) || + (weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday))) + return 19; // Style 19: Light Blue for weekends/off days (Fri/Sat or Sat/Sun) + + return 11; // Style 11: Default normal text center aligned + } + + private bool IsAdmin(int userId) + { + var userRoles = _centralDbContext.UserRoles + .Where(ur => ur.UserId == userId) + .Join(_centralDbContext.Roles, ur => ur.RoleId, r => r.Id, (ur, r) => r.Name) + .ToList(); + + return userRoles.Any(role => role.Contains("SuperAdmin") || role.Contains("SystemAdmin")); + } + + private List GetAllDatesInMonth(DateTime month) + { + return Enumerable.Range(1, DateTime.DaysInMonth(month.Year, month.Month)) + .Select(day => new DateTime(month.Year, month.Month, day)) + .ToList(); + } + + private string GetDayType(DateTime date, int? weekendId, List publicHolidays) + { + if (publicHolidays.Contains(date.Date)) + { + return "Public Holiday"; + } + + DayOfWeek dayOfWeek = date.DayOfWeek; + + if ((weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) || + (weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday))) + { + if ((weekendId == 1 && dayOfWeek == DayOfWeek.Friday) || + (weekendId == 2 && dayOfWeek == DayOfWeek.Saturday)) + { + return "Off Day"; + } + if ((weekendId == 1 && dayOfWeek == DayOfWeek.Saturday) || + (weekendId == 2 && dayOfWeek == DayOfWeek.Sunday)) + { + return "Rest Day"; + } + } + + return "Normal Day"; + } + + // Modified: ORP = Basic Salary / 26 + private decimal CalculateOrp(decimal basicSalary) + { + return basicSalary / 26m; + } + + // New: HRP = ORP / 8 + private decimal CalculateHrp(decimal orp) + { + return orp / 8m; + } + + + // Corrected FormatAsHourMinute method + private string FormatAsHourMinute(decimal? totalMinutes, bool isMinutes = false) + { + if (totalMinutes == null || totalMinutes == 0) + return ""; + + TimeSpan ts; + if (isMinutes) + { + // If isMinutes is true, input is total minutes + ts = TimeSpan.FromMinutes((double)totalMinutes.Value); + } + else + { + // If isMinutes is false, input is decimal hours (e.g., 1.5 for 1 hour 30 mins) + // Convert decimal hours to minutes for TimeSpan.FromMinutes + ts = TimeSpan.FromMinutes((double)(totalMinutes.Value * 60)); + } + + // Handle negative times if durations can ever be negative, though usually they are clamped at 0 + if (ts.TotalMinutes < 0) + { + return $"-{Math.Abs((int)ts.TotalHours)}:{Math.Abs(ts.Minutes):D2}"; + } + + int hours = (int)ts.TotalHours; + int minutes = ts.Minutes; // TimeSpan.Minutes property gives the minute component (0-59) + + return $"{hours}:{minutes:D2}"; + } + + + private decimal ConvertTimeToDecimal(string time) + { + if (string.IsNullOrEmpty(time)) return 0; + + var parts = time.Split(':'); + if (parts.Length != 2) return 0; + + if (int.TryParse(parts[0], out int hours) && int.TryParse(parts[1], out int minutes)) + { + return hours + (minutes / 60m); + } + + return 0; + } + + // --- Missing Helper Method: CalculateTimeDifferenceInMinutes --- + private decimal CalculateTimeDifferenceInMinutes(TimeSpan? from, TimeSpan? to) + { + if (!from.HasValue || !to.HasValue) return 0; + + double fromTotalMinutes = from.Value.TotalMinutes; + double toTotalMinutes = to.Value.TotalMinutes; + + double totalMinutes = toTotalMinutes - fromTotalMinutes; + + // If the 'to' time is earlier than the 'from' time, it means the period crosses midnight. + // Add 24 hours (1440 minutes) to account for the next day. + if (totalMinutes < 0) + { + totalMinutes += 24 * 60; // Add 24 hours in minutes + } + + return (decimal)Math.Max(0, totalMinutes); // Ensure the result is non-negative + } + // --- End Missing Helper Method --- + + + private string CalculateOfficeOtHours(OtRegisterModel record) + { + if (!record.OfficeFrom.HasValue || !record.OfficeTo.HasValue) return ""; + + var duration = record.OfficeTo.Value - record.OfficeFrom.Value; + var totalMinutes = duration.TotalMinutes - (record.OfficeBreak ?? 0); + totalMinutes = Math.Max(0, totalMinutes); + + int hours = (int)(totalMinutes / 60); + int minutes = (int)(totalMinutes % 60); + + return $"{hours}:{minutes:D2}"; + } + + private string CalculateAfterOfficeOtHours(OtRegisterModel record) + { + if (!record.AfterFrom.HasValue || !record.AfterTo.HasValue) return ""; + + var duration = record.AfterTo.Value - record.AfterFrom.Value; + var totalMinutes = duration.TotalMinutes - (record.AfterBreak ?? 0); + totalMinutes = Math.Max(0, totalMinutes); + + int hours = (int)(totalMinutes / 60); + int minutes = (int)(totalMinutes % 60); + + return $"{hours}:{minutes:D2}"; + } + + private Dictionary ClassifyOt(OtRegisterModel record, decimal hrp, List publicHolidays, int? weekendId) + { + var officeRaw = CalculateOfficeOtHours(record); + var afterRaw = CalculateAfterOfficeOtHours(record); + + var officeHrs = ConvertTimeToDecimal(officeRaw); + var afterHrs = ConvertTimeToDecimal(afterRaw); + + var toFixedOrEmpty = (decimal num) => num > 0 ? num.ToString("N2") : ""; + + var dayType = GetDayType(record.OtDate, weekendId, publicHolidays); + + var result = new Dictionary + { + ["ndAfter"] = "", + ["odUnder4"] = "", + ["odBetween4And8"] = "", + ["odAfter"] = "", + ["rdUnder4"] = "", + ["rdBetween4And8"] = "", + ["rdAfter"] = "", + ["phUnder8"] = "", + ["phAfter"] = "" + }; + + switch (dayType) + { + case "Normal Day": + result["ndAfter"] = toFixedOrEmpty(afterHrs); + break; + case "Off Day": + if (officeHrs > 0) + { + if (officeHrs <= 4) result["odUnder4"] = toFixedOrEmpty(officeHrs); + else if (officeHrs > 4 && officeHrs <= 8) result["odBetween4And8"] = toFixedOrEmpty(officeHrs); + } + result["odAfter"] = toFixedOrEmpty(afterHrs); + break; + case "Rest Day": + if (officeHrs > 0) + { + if (officeHrs <= 4) result["rdUnder4"] = toFixedOrEmpty(officeHrs); + else if (officeHrs > 4 && officeHrs <= 8) result["rdBetween4And8"] = toFixedOrEmpty(officeHrs); + } + result["rdAfter"] = toFixedOrEmpty(afterHrs); + break; + case "Public Holiday": + if (officeHrs > 0) + { + if (officeHrs <= 8) result["phUnder8"] = toFixedOrEmpty(officeHrs); + } + result["phAfter"] = toFixedOrEmpty(afterHrs); + break; + } + + return result; + } + + private string CalculateTotalOtHrs(OtRegisterModel record, decimal hrp, List publicHolidays, int? weekendId) + { + var classified = ClassifyOt(record, hrp, publicHolidays, weekendId); + decimal total = 0; + + foreach (var value in classified.Values) + { + if (decimal.TryParse(value, out decimal num)) + { + total += num; + } + } + + return total > 0 ? total.ToString("N2") : ""; + } + + private string CalculateNdOdTotal(OtRegisterModel record, decimal hrp, List publicHolidays, int? weekendId) + { + var classified = ClassifyOt(record, hrp, publicHolidays, weekendId); + + var nd = decimal.TryParse(classified["ndAfter"], out decimal ndVal) ? ndVal : 0; + var od1 = decimal.TryParse(classified["odUnder4"], out decimal od1Val) ? od1Val : 0; + var od2 = decimal.TryParse(classified["odBetween4And8"], out decimal od2Val) ? od2Val : 0; + var od3 = decimal.TryParse(classified["odAfter"], out decimal od3Val) ? od3Val : 0; + + var total = nd + od1 + od2 + od3; + return total > 0 ? total.ToString("N2") : ""; + } + + private string CalculateRdTotal(OtRegisterModel record, decimal hrp, List publicHolidays, int? weekendId) + { + var classified = ClassifyOt(record, hrp, publicHolidays, weekendId); + var total = + (decimal.TryParse(classified["rdUnder4"], out decimal rd1) ? rd1 : 0) + + (decimal.TryParse(classified["rdBetween4And8"], out decimal rd2) ? rd2 : 0) + + (decimal.TryParse(classified["rdAfter"], out decimal rd3) ? rd3 : 0); + + return total > 0 ? total.ToString("N2") : ""; + } + + private string CalculatePhTotal(OtRegisterModel record, decimal hrp, List publicHolidays, int? weekendId) + { + var classified = ClassifyOt(record, hrp, publicHolidays, weekendId); + var total = + (decimal.TryParse(classified["phUnder8"], out decimal ph1) ? ph1 : 0) + + (decimal.TryParse(classified["phAfter"], out decimal ph2) ? ph2 : 0); + + return total > 0 ? total.ToString("N2") : ""; + } + + private string CalculateOtAmount(OtRegisterModel record, decimal hrp, List publicHolidays, int? weekendId) + { + var orp = CalculateOrp(hrp * 208); // Recalculate ORP based on an assumed 208 hours in a month to get back to basic salary, then divide by 26 + var dayType = GetDayType(record.OtDate, weekendId, publicHolidays); + + var officeRaw = CalculateOfficeOtHours(record); + var afterRaw = CalculateAfterOfficeOtHours(record); + + var officeHours = ConvertTimeToDecimal(officeRaw); + var afterOfficeHours = ConvertTimeToDecimal(afterRaw); + + decimal amountOffice = 0; + decimal amountAfter = 0; + + if (officeHours > 0) + { + if (dayType == "Off Day" || dayType == "Rest Day") + { + if (officeHours <= 4) + { + amountOffice = 0.5m * orp; + } + else if (officeHours > 4 && officeHours <= 8) + { + amountOffice = 1 * orp; + } + } + else if (dayType == "Public Holiday") + { + amountOffice = 2 * orp; + } + } + + if (afterOfficeHours > 0) + { + switch (dayType) + { + case "Normal Day": + case "Off Day": + amountAfter = 1.5m * hrp * afterOfficeHours; + break; + case "Rest Day": + amountAfter = 2 * hrp * afterOfficeHours; + break; + case "Public Holiday": + amountAfter = 3 * hrp * afterOfficeHours; + break; + } + } + + var totalAmount = amountOffice + amountAfter; + return totalAmount.ToString("N2"); + } + + // Modified AddRemarksSection to start at column 1 + private void AddRemarksSection(IXLWorksheet worksheet, ref int currentRow, DateTime displayMonth, int? weekendId, List publicHolidays) + { + // Start remarks at column 1 for left alignment + int remarksStartColumn = 1; + int remarksRow = currentRow + 1; // Start remarks below the total table + + // Public Holiday Color + var phColorCell = worksheet.Cell(remarksRow, remarksStartColumn); + phColorCell.Value = " "; // Small cell for color swatch + phColorCell.Style.Fill.BackgroundColor = XLColor.FromHtml("FFC0CB"); // Pink color + phColorCell.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + + worksheet.Cell(remarksRow, remarksStartColumn + 1).Value = " Public Holiday"; + worksheet.Cell(remarksRow, remarksStartColumn + 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + remarksRow++; + + // Weekend/Off Day Color + var weekendColorCell = worksheet.Cell(remarksRow, remarksStartColumn); + weekendColorCell.Value = " "; // Small cell for color swatch + weekendColorCell.Style.Fill.BackgroundColor = XLColor.FromHtml("ADD8E6"); // Light Blue color + weekendColorCell.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); + + worksheet.Cell(remarksRow, remarksStartColumn + 1).Value = " Weekend"; + worksheet.Cell(remarksRow, remarksStartColumn + 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + remarksRow++; + + remarksRow++; // Blank line for spacing + + // --- Public Holidays List for the Month --- + worksheet.Cell(remarksRow, remarksStartColumn).Value = $"Public Holidays in {displayMonth:MMMM yyyy}:"; + worksheet.Cell(remarksRow, remarksStartColumn).Style.Font.SetBold(); + remarksRow++; + + if (publicHolidays != null && publicHolidays.Any()) + { + var monthHolidays = publicHolidays + .Where(h => h.HolidayDate.Year == displayMonth.Year && h.HolidayDate.Month == displayMonth.Month) + .OrderBy(h => h.HolidayDate.Day) + .ToList(); + + if (monthHolidays.Any()) + { + foreach (var holiday in monthHolidays) + { + worksheet.Cell(remarksRow, remarksStartColumn).Value = $"- {holiday.HolidayDate:dd MMMM}: {holiday.HolidayName}"; + worksheet.Cell(remarksRow, remarksStartColumn).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Left; + remarksRow++; + } + } + else + { + worksheet.Cell(remarksRow, remarksStartColumn).Value = "No public holidays recorded for this month."; + worksheet.Cell(remarksRow, remarksStartColumn).Style.Font.SetItalic(); + remarksRow++; + } + } + else + { + worksheet.Cell(remarksRow, remarksStartColumn).Value = "No public holiday data found."; + worksheet.Cell(remarksRow, remarksStartColumn).Style.Font.SetItalic(); + remarksRow++; + } + + // Update currentRow to the bottom of the remarks section + currentRow = remarksRow; + } + + private string GetWeekendDefinition(int? weekendId) + { + if (!weekendId.HasValue) return "N/A"; + + switch (weekendId) + { + case 1: return "Friday & Saturday"; + case 2: return "Saturday & Sunday"; + default: return "Unknown"; + } + } + } +} \ No newline at end of file diff --git a/Areas/OTcalculate/Services/OvertimeExcelService.cs b/Areas/OTcalculate/Services/OvertimeExcelService.cs deleted file mode 100644 index 8e7efe0..0000000 --- a/Areas/OTcalculate/Services/OvertimeExcelService.cs +++ /dev/null @@ -1,304 +0,0 @@ -using ClosedXML.Excel; -using System.IO; -using PSTW_CentralSystem.Models; -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 -{ - public class OvertimeExcelService - { - public MemoryStream GenerateOvertimeExcel( - List records, - int departmentId, - string userFullName, - string departmentName, - int userStateId, - int weekendId, - List publicHolidays, - bool isAdminUser = false, - 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 - - if (!string.IsNullOrEmpty(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)) - { - 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++; - - // 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) - { - int dataCol = 1; - - bool isSameDateAsPrevious = previousDate == r.OtDate.Date; - previousDate = r.OtDate.Date; - - var dayCell = worksheet.Cell(currentRow, dataCol++); - var dateCell = worksheet.Cell(currentRow, dataCol++); - - // 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; - dateCell.Style.Fill.BackgroundColor = XLColor.Pink; - } - else if (isWeekend) - { - dayCell.Style.Fill.BackgroundColor = XLColor.LightBlue; - dateCell.Style.Fill.BackgroundColor = XLColor.LightBlue; - } - - // 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, dataCol++).Value = FormatTime(r.OfficeFrom); - worksheet.Cell(currentRow, dataCol++).Value = FormatTime(r.OfficeTo); - worksheet.Cell(currentRow, dataCol++).Value = r.OfficeBreak; - - 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, dataCol++).Value = r.Stations?.StationName ?? ""; - - worksheet.Cell(currentRow, dataCol++).Value = r.OtDescription ?? ""; - - 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; - } - - currentRow++; - } - - if (records.Any()) - { - int totalRow = currentRow; - worksheet.Cell(totalRow, 1).Value = "TOTAL"; - worksheet.Cell(totalRow, 1).Style.Font.Bold = true; - - 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); - stream.Position = 0; - return stream; - } - - private TimeSpan CalculateTotalOT(OtRegisterModel r) - { - TimeSpan office = (r.OfficeTo ?? TimeSpan.Zero) - (r.OfficeFrom ?? TimeSpan.Zero); - TimeSpan after = (r.AfterTo ?? TimeSpan.Zero) - (r.AfterFrom ?? TimeSpan.Zero); - if (after < TimeSpan.Zero) - after += TimeSpan.FromHours(24); - return office + after; - } - - private string FormatTime(TimeSpan? time) - { - 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; - } - } -} diff --git a/Areas/OTcalculate/Services/OvertimePDF.cs b/Areas/OTcalculate/Services/OvertimePDF.cs new file mode 100644 index 0000000..d864634 --- /dev/null +++ b/Areas/OTcalculate/Services/OvertimePDF.cs @@ -0,0 +1,1294 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using PSTW_CentralSystem.Areas.OTcalculate.Models; +using PSTW_CentralSystem.Models; // Ensure this is included for CalendarModel, StateModel etc. +using Microsoft.EntityFrameworkCore; +using PSTW_CentralSystem.DBContext; + +namespace PSTW_CentralSystem.Areas.OTcalculate.Services +{ + public class OvertimePDF + { + private readonly CentralSystemContext _centralDbContext; + + public OvertimePDF(CentralSystemContext centralDbContext) + { + _centralDbContext = centralDbContext; + } + + public MemoryStream GenerateOvertimeTablePdf( + List records, + UserModel user, + decimal userRate, + DateTime? selectedMonth = null, + byte[]? logoImage = null, + bool isHoU = false, + string? flexiHour = null, + List? approvedSignatures = null) + { + bool isAdminUser = IsAdmin(user.Id); + bool showStationColumn = user.departmentId == 2 || user.departmentId == 3 || isAdminUser; + + DateTime displayMonth = selectedMonth ?? + (records.Any() ? records.First().OtDate : DateTime.Now); + + var allDatesInMonth = GetAllDatesInMonth(displayMonth); + + // Fetch user setting here once + var userSetting = _centralDbContext.Hrusersetting + .Include(us => us.State) + .FirstOrDefault(us => us.UserId == user.Id); + + // Fetch actual public holiday objects including their names for the current month/year and user's state + var publicHolidaysForUser = _centralDbContext.Holidays + .Where(h => userSetting != null && h.StateId == userSetting.State.StateId && h.HolidayDate.Month == displayMonth.Month && h.HolidayDate.Year == displayMonth.Year) + .OrderBy(h => h.HolidayDate) // Order by date for display + .ToList(); + + records = records.OrderBy(r => r.OtDate).ToList(); + var stream = new MemoryStream(); + + Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4.Landscape()); + page.Margin(30); + + page.Content().Column(column => + { + // Top Section: Logo, User Info, Generated Date + column.Item().Row(row => + { + row.RelativeItem(2).Column(col => + { + if (logoImage != null) + { + col.Item().PaddingBottom(10).Container().Height(25).Image(logoImage, ImageScaling.FitArea); + } + + // Option 1: Inline with Clear Labels for user details + col.Item().PaddingBottom(2).Text(text => + { + text.Span("Name: ").SemiBold().FontSize(9); + text.Span($"{user.FullName} |").FontSize(9); + + text.Span(" Department: ").SemiBold().FontSize(9); + text.Span($"{user.Department?.DepartmentName ?? "N/A"} |").FontSize(9); + + if (!string.IsNullOrEmpty(flexiHour)) + { + text.Span(" Flexi Hour: ").SemiBold().FontSize(9); + text.Span($"{flexiHour}").FontSize(9); + } + }); + }); + + row.RelativeItem(1).AlignRight().Text($"Generated: {DateTime.Now:dd MMM yyyy HH:mm}") + .FontSize(9).FontColor(Colors.Grey.Medium); + }); + + // Overtime Record title remains separate for clarity + column.Item().PaddingTop(3).Row(row => + { + row.RelativeItem(2).Text($"Overtime Record: {displayMonth:MMMM yyyy}").FontSize(9).Italic(); + + // Legend (remains on the same line as Overtime Record) + row.RelativeItem(1).AlignRight().Column(legendCol => + { + legendCol.Item().Element(e => e.ShowEntire().Row(subRow => + { + subRow.AutoItem().Text(text => + { + text.Span(" ").BackgroundColor("#add8e6").FontSize(9); + text.Span(" Weekend").FontSize(8).FontColor(Colors.Black); + }); + })); + legendCol.Item().Element(e => e.ShowEntire().Row(subRow => + { + subRow.AutoItem().Text(text => + { + text.Span(" ").BackgroundColor("#ffc0cb").FontSize(9); + text.Span(" Public Holiday").FontSize(8).FontColor(Colors.Black); + }); + })); + }); + }); + + + column.Item().PaddingVertical(10).LineHorizontal(0.5f).LineColor(Colors.Grey.Lighten2); + + // Pass the fetched publicHolidays to ComposeTable + column.Item().Element(container => ComposeTable(container, records, user, userRate, showStationColumn, allDatesInMonth, isHoU, publicHolidaysForUser.Select(h => h.HolidayDate.Date).ToList())); + + // Approval Signatures and Remarks section + column.Item().PaddingTop(20).Element(container => + { + container.ShowEntire().Row(row => + { + // Left side - Approval Signatures + row.RelativeItem().Element(e => e.ShowEntire().Column(approvalCol => + { + if (approvedSignatures != null && approvedSignatures.Any()) + { + approvalCol.Item().Element(e => e.ShowEntire().Row(approvalsRow => + { + approvalsRow.Spacing(20); + + foreach (var approval in approvedSignatures) + { + approvalsRow.RelativeItem().Element(e => e.ShowEntire().Column(individualApprovalColumn => + { + individualApprovalColumn.Item().Text("Approved by:").FontSize(9).SemiBold().AlignCenter(); + + individualApprovalColumn.Item().PaddingTop(1) + .Height(50) + .AlignCenter() + .AlignMiddle() + .Text(approval.ApproverName) + .FontSize(15) + .FontColor(Colors.Black) + .FontFamily("Brush Script MT"); + + individualApprovalColumn.Item().PaddingTop(1) + .Text(approval.ApproverName).FontSize(9).SemiBold().AlignCenter(); + + if (approval.ApprovedDate.HasValue) + { + individualApprovalColumn.Item().PaddingTop(2) + .Text(approval.ApprovedDate.Value.ToString("dd MMMM yyyy")) // Changed format to avoid special characters + .FontSize(8).AlignCenter(); + } + })); + } + })); + } + })); + + // Right side - Remarks and Public Holidays + row.RelativeItem().Element(e => e.ShowEntire().Column(remarksCol => + { + remarksCol.Item().Element(e => e.ShowEntire().Row(remarksRow => + { + // Public Holidays List as a mini-table + remarksRow.RelativeItem(2).Element(e => e.ShowEntire().AlignRight().Column(holidayListCol => + { + holidayListCol.Item().Text("Public Holidays:").FontSize(9).SemiBold(); + holidayListCol.Item().Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(1); // For Date + columns.RelativeColumn(2); // For Holiday Name + }); + + table.Header(header => + { + header.Cell().Border(0.25f).Padding(2).Text("Date").FontSize(7).Bold().AlignCenter(); + header.Cell().Border(0.25f).Padding(2).Text("Holiday Name").FontSize(7).Bold().AlignCenter(); + }); + + if (publicHolidaysForUser.Any()) + { + foreach (var holiday in publicHolidaysForUser) + { + table.Cell().Border(0.25f).Padding(2).Text($"{holiday.HolidayDate:dd-MM-yyyy}").FontSize(7).AlignCenter(); + table.Cell().Border(0.25f).Padding(2).Text(holiday.HolidayName).FontSize(7).AlignLeft(); + } + } + else + { + table.Cell().ColumnSpan(2).Border(0.25f).Padding(2) + .Text($"No public holidays found for {userSetting?.State?.StateName ?? "this state"} in {displayMonth:MMMM yyyy}.") // Changed format + .FontSize(7).Italic().AlignCenter(); + } + }); + })); + })); + })); + }); + }); + // Add the automatically generated document text only if signatures are present + if (approvedSignatures != null && approvedSignatures.Any()) + { + column.Item().PaddingTop(20).AlignCenter().Text("This is an automatically generated document. No signature is required for validation.").FontSize(8).Italic().FontColor(Colors.Grey.Darken2); + } + }); + }); + }).GeneratePdf(stream); + + stream.Position = 0; + return stream; + } + + private void ComposeTable(IContainer container, List records, UserModel user, decimal userRate, bool showStationColumn, List allDatesInMonth, bool isHoU, List publicHolidays) + { + var recordsGroupedByDate = records + .GroupBy(r => r.OtDate.Date) + .ToDictionary(g => g.Key, g => g.ToList()); + + var userSetting = _centralDbContext.Hrusersetting + .Include(us => us.State) + .FirstOrDefault(us => us.UserId == user.Id); + + container.Table(table => + { + table.ColumnsDefinition(columns => + { + if (!isHoU) + { + columns.RelativeColumn(0.25f); // Basic Salary + columns.RelativeColumn(0.2f); // ORP + columns.RelativeColumn(0.2f); // HRP + } + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.4f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.25f); + columns.RelativeColumn(0.25f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.25f); + columns.RelativeColumn(0.3f); + columns.RelativeColumn(0.2f); + columns.RelativeColumn(0.2f); + if (!isHoU) + { + columns.RelativeColumn(0.25f); + } + + if (showStationColumn) + { + columns.RelativeColumn(0.3f); + } + + columns.RelativeColumn(0.7f); + }); + + table.Header(header => + { + if (!isHoU) + { + header.Cell().RowSpan(2).Background("#fce5cd").Border(0.25f).Padding(3).Text("Basic Salary\n(RM)").FontSize(6).Bold().AlignCenter(); + header.Cell().RowSpan(2).Background("#fce5cd").Border(0.25f).Padding(3).Text("ORP").FontSize(6).Bold().AlignCenter(); + header.Cell().RowSpan(2).Background("#fce5cd").Border(0.25f).Padding(3).Text("HRP").FontSize(6).Bold().AlignCenter(); + } + header.Cell().RowSpan(2).Background("#e0f7da").Border(0.25f).Padding(3).Text("Day").FontSize(6).Bold().AlignCenter(); + header.Cell().RowSpan(2).Background("#d0ead2").Border(0.25f).Padding(3).Text("Date").FontSize(6).Bold().AlignCenter(); + + header.Cell().ColumnSpan(3).Background("#dceefb").Border(0.25f).Padding(3).Text("Office Hours").FontSize(6).Bold().AlignCenter(); + header.Cell().ColumnSpan(3).Background("#edf2f7").Border(0.25f).Padding(3).Text("After Office Hours").FontSize(6).Bold().AlignCenter(); + + header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(3).Text("OT Hrs\n(Office Hour)").FontSize(6).Bold().AlignCenter(); + header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(3).Text("OT Hrs\n(After Office Hour)").FontSize(6).Bold().AlignCenter(); + + header.Cell().ColumnSpan(1).Background("#deebf7").Border(0.25f).Padding(3).Text("Normal Day").FontSize(5).Bold().AlignCenter(); + header.Cell().ColumnSpan(3).Background("#deebf7").Border(0.25f).Padding(3).Text("Off Day").FontSize(5).Bold().AlignCenter(); + header.Cell().ColumnSpan(3).Background("#deebf7").Border(0.25f).Padding(3).Text("Rest Day").FontSize(5).Bold().AlignCenter(); + header.Cell().ColumnSpan(2).Background("#deebf7").Border(0.25f).Padding(3).Text("Public Holiday").FontSize(5).Bold().AlignCenter(); + + header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(3).Text("Total OT").FontSize(6).Bold().AlignCenter(); + header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(3).Text("Total\nND & OD").FontSize(6).Bold().AlignCenter(); + header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(3).Text("Total\nRD").FontSize(6).Bold().AlignCenter(); + header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(3).Text("Total\nPH").FontSize(6).Bold().AlignCenter(); + + if (!isHoU) + { + header.Cell().RowSpan(2).Background("#fce5cd").Border(0.25f).Padding(3).Text("OT Amt\n(RM)").FontSize(6).Bold().AlignCenter(); + } + + if (showStationColumn) + { + header.Cell().RowSpan(2).Background("#d0f0ef").Border(0.25f).Padding(3).Text("Station").FontSize(6).Bold().AlignCenter(); + } + + header.Cell().RowSpan(2).Background("#e3f2fd").Border(0.25f).Padding(3).Text("Description").FontSize(6).Bold().AlignCenter(); + + header.Cell().Background("#dceefb").Border(0.25f).Padding(3).Text("From").FontSize(5).Bold().AlignCenter(); + header.Cell().Background("#dceefb").Border(0.25f).Padding(3).Text("To").FontSize(5).Bold().AlignCenter(); + header.Cell().Background("#dceefb").Border(0.25f).Padding(3).Text("Break\n(min)").FontSize(5).Bold().AlignCenter(); + + header.Cell().Background("#edf2f7").Border(0.25f).Padding(3).Text("From").FontSize(5).Bold().AlignCenter(); + header.Cell().Background("#edf2f7").Border(0.25f).Padding(3).Text("To").FontSize(5).Bold().AlignCenter(); + header.Cell().Background("#edf2f7").Border(0.25f).Padding(3).Text("Break\n(min)").FontSize(5).Bold().AlignCenter(); + + header.Cell().Background("#deebf7").Border(0.25f).Padding(3).Text("ND OT").FontSize(5).Bold().AlignCenter(); + header.Cell().Background("#deebf7").Border(0.25f).Padding(3).Text("OD < 4").FontSize(5).Bold().AlignCenter(); + header.Cell().Background("#deebf7").Border(0.25f).Padding(3).Text("OD 4-8").FontSize(5).Bold().AlignCenter(); + header.Cell().Background("#deebf7").Border(0.25f).Padding(3).Text("OD After").FontSize(5).Bold().AlignCenter(); + header.Cell().Background("#deebf7").Border(0.25f).Padding(3).Text("RD < 4").FontSize(5).Bold().AlignCenter(); + header.Cell().Background("#deebf7").Border(0.25f).Padding(3).Text("RD 4-8").FontSize(5).Bold().AlignCenter(); + header.Cell().Background("#deebf7").Border(0.25f).Padding(3).Text("RD After").FontSize(5).Bold().AlignCenter(); + header.Cell().Background("#deebf7").Border(0.25f).Padding(3).Text("PH < 8").FontSize(5).Bold().AlignCenter(); + header.Cell().Background("#deebf7").Border(0.25f).Padding(3).Text("PH After").FontSize(5).Bold().AlignCenter(); + }); + + + decimal totalOfficeBreak = 0; + decimal totalAfterBreak = 0; + decimal totalOtHrsOffice = 0; + decimal totalOtHrsAfterOffice = 0; + decimal totalNdOt = 0; + decimal totalOdUnder4 = 0; + decimal totalOd4to8 = 0; + decimal totalOdAfter = 0; + decimal totalRdUnder4 = 0; + decimal totalRd4to8 = 0; + decimal totalRdAfter = 0; + decimal totalPhUnder8 = 0; + decimal totalPhAfter = 0; + decimal grandTotalOt = 0; + decimal grandTotalNdOd = 0; + decimal grandTotalRd = 0; + decimal grandTotalPh = 0; + decimal grandTotalOtAmount = 0; + + + bool alternate = false; + + // Changed calculations as per your request + var basicSalary = userRate; // userRate is now treated as basic salary + var orp = basicSalary / 26m; // Calculate ORP from basicSalary + var hrp = orp / 8m; // Calculate HRP from ORP + + + bool hasPrintedSalaryDetails = false; + DateTime? previousDate = null; + + foreach (var date in allDatesInMonth) + { + recordsGroupedByDate.TryGetValue(date, out var dateRecords); + var recordsToShow = dateRecords ?? new List { new OtRegisterModel { OtDate = date } }; + recordsToShow = recordsToShow.OrderBy(r => r.OfficeFrom ?? r.AfterFrom).ToList(); + + foreach (var record in recordsToShow) + { + var dayType = GetDayType(date, userSetting?.State?.WeekendId, publicHolidays); + var backgroundColor = GetDayCellBackgroundColor(date, userSetting?.State?.WeekendId, publicHolidays); + + var classified = ClassifyOt(record, hrp, publicHolidays, userSetting?.State?.WeekendId); + var totalOt = CalculateTotalOtHrs(record, hrp, publicHolidays, userSetting?.State?.WeekendId); + var totalNdOd = CalculateNdOdTotal(record, hrp, publicHolidays, userSetting?.State?.WeekendId); + var totalRd = CalculateRdTotal(record, hrp, publicHolidays, userSetting?.State?.WeekendId); + var totalPh = CalculatePhTotal(record, hrp, publicHolidays, userSetting?.State?.WeekendId); + var otAmt = CalculateOtAmount(record, hrp, publicHolidays, userSetting?.State?.WeekendId); + + totalOfficeBreak += (decimal)record.OfficeBreak.GetValueOrDefault(0); + totalAfterBreak += (decimal)record.AfterBreak.GetValueOrDefault(0); + totalOtHrsOffice += ConvertTimeToDecimal(CalculateOfficeOtHours(record)); + totalOtHrsAfterOffice += ConvertTimeToDecimal(CalculateAfterOfficeOtHours(record)); + + totalNdOt += decimal.TryParse(classified["ndAfter"], out decimal ndOtVal) ? ndOtVal : 0; + totalOdUnder4 += decimal.TryParse(classified["odUnder4"], out decimal odUnder4Val) ? odUnder4Val : 0; + totalOd4to8 += decimal.TryParse(classified["odBetween4And8"], out decimal od4to8Val) ? od4to8Val : 0; + totalOdAfter += decimal.TryParse(classified["odAfter"], out decimal odAfterVal) ? odAfterVal : 0; + totalRdUnder4 += decimal.TryParse(classified["rdUnder4"], out decimal rdUnder4Val) ? rdUnder4Val : 0; + totalRd4to8 += decimal.TryParse(classified["rdBetween4And8"], out decimal rd4to8Val) ? rd4to8Val : 0; + totalRdAfter += decimal.TryParse(classified["rdAfter"], out decimal rdAfterVal) ? rdAfterVal : 0; + totalPhUnder8 += decimal.TryParse(classified["phUnder8"], out decimal phUnder8Val) ? phUnder8Val : 0; + totalPhAfter += decimal.TryParse(classified["phAfter"], out decimal phAfterVal) ? phAfterVal : 0; + + grandTotalOt += decimal.TryParse(totalOt, out decimal totalOtVal) ? totalOtVal : 0; + grandTotalNdOd += decimal.TryParse(totalNdOd, out decimal totalNdOdVal) ? totalNdOdVal : 0; + grandTotalRd += decimal.TryParse(totalRd, out decimal totalRdVal) ? totalRdVal : 0; + grandTotalPh += decimal.TryParse(totalPh, out decimal totalPhVal) ? totalPhVal : 0; + grandTotalOtAmount += decimal.TryParse(otAmt, out decimal otAmtVal) ? otAmtVal : 0; + + + string rowBg = alternate ? "#f9f9f9" : "#ffffff"; + if (dateRecords == null || !dateRecords.Any()) + rowBg = alternate ? Colors.Grey.Lighten5 : Colors.White; + + + void AddCell(string value, bool center = true, string? bg = null) + { + var cell = table.Cell().Background(bg ?? rowBg).Border(0.25f).Padding(2); + var text = cell.Text(value).FontSize(6); + if (center) text.AlignCenter(); + } + + if (!isHoU) + { + // Display the calculated values here + AddCell(!hasPrintedSalaryDetails ? $"{basicSalary:F2}" : ""); + AddCell(!hasPrintedSalaryDetails ? $"{orp:F2}" : ""); + AddCell(!hasPrintedSalaryDetails ? $"{hrp:F2}" : ""); + } + hasPrintedSalaryDetails = true; // Ensure these values are printed only once + + if (date.Date != previousDate?.Date) + { + AddCell(date.ToString("ddd"), true, backgroundColor); + AddCell(date.ToString("dd-MM-yyyy"), true, backgroundColor); + } + else + { + AddCell("", true, backgroundColor); + AddCell("", true, backgroundColor); + } + previousDate = date; + + AddCell(record.OfficeFrom?.ToString(@"hh\:mm") ?? ""); + AddCell(record.OfficeTo?.ToString(@"hh\:mm") ?? ""); + AddCell(FormatAsHourMinute(record.OfficeBreak, isMinutes: true)); + AddCell(record.AfterFrom?.ToString(@"hh\:mm") ?? ""); + AddCell(record.AfterTo?.ToString(@"hh\:mm") ?? ""); + AddCell(FormatAsHourMinute(record.AfterBreak, isMinutes: true)); + AddCell(CalculateOfficeOtHours(record)); + AddCell(CalculateAfterOfficeOtHours(record)); + AddCell(classified["ndAfter"]); + AddCell(classified["odUnder4"]); + AddCell(classified["odBetween4And8"]); + AddCell(classified["odAfter"]); + AddCell(classified["rdUnder4"]); + AddCell(classified["rdBetween4And8"]); + AddCell(classified["rdAfter"]); + AddCell(classified["phUnder8"]); + AddCell(classified["phAfter"]); + AddCell(totalOt); + AddCell(totalNdOd); + AddCell(totalRd); + AddCell(totalPh); + if (!isHoU) + { + AddCell(otAmt == "0.00" ? "" : otAmt); + } + + + if (showStationColumn) + { + AddCell(record.Stations?.StationName ?? ""); + } + + table.Cell().Background(rowBg).Border(0.25f).Padding(2) + .Text(record.OtDescription ?? "").FontSize(6).AlignLeft(); + + alternate = !alternate; + } + } + + if (!isHoU) + { + table.Cell().ColumnSpan(5).Background("#d8d1f5").Border(0.80f).Padding(3) + .Text("TOTAL").Bold().FontSize(6).AlignCenter(); + } + else + { + table.Cell().ColumnSpan(2).Background("#d8d1f5").Border(0.80f).Padding(3) + .Text("TOTAL").Bold().FontSize(6).AlignCenter(); + } + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter(); + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text(totalOfficeBreak == 0 ? "0.00" : FormatAsHourMinute(totalOfficeBreak, isMinutes: true)) + .Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter(); + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text(totalAfterBreak == 0 ? "0.00" : FormatAsHourMinute(totalAfterBreak, isMinutes: true)) + .Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter(); + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{totalNdOt:N2}").Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{totalOdUnder4:N2}").Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{totalOd4to8:N2}").Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{totalOdAfter:N2}").Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{totalRdUnder4:N2}").Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{totalRd4to8:N2}").Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{totalRdAfter:N2}").Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{totalPhUnder8:N2}").Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{totalPhAfter:N2}").Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{grandTotalOt:N2}").Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{grandTotalNdOd:N2}").Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{grandTotalRd:N2}").Bold().FontSize(6).AlignCenter(); + + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{grandTotalPh:N2}").Bold().FontSize(6).AlignCenter(); + + if (!isHoU) + { + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text($"{grandTotalOtAmount:N2}").Bold().FontSize(6).AlignCenter(); + } + + if (showStationColumn) + { + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter(); + } + table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter(); + }); + } + + public MemoryStream GenerateSimpleOvertimeTablePdf( + List records, + UserModel user, // User parameter added here + decimal userRate, + DateTime? selectedMonth = null, + byte[]? logoImage = null, + string? flexiHour = null) + { + bool isAdminUser = IsAdmin(user.Id); + bool showStationColumn = user.departmentId == 2 || user.departmentId == 3 || isAdminUser; + + DateTime displayMonth = selectedMonth ?? + (records.Any() ? records.First().OtDate : DateTime.Now); + + var allDatesInMonth = GetAllDatesInMonth(displayMonth); + + // Fetch user setting here once + var userSetting = _centralDbContext.Hrusersetting + .Include(us => us.State) + .FirstOrDefault(us => us.UserId == user.Id); + + // Fetch actual public holiday objects including their names for the current month/year and user's state + var publicHolidaysForUser = _centralDbContext.Holidays + .Where(h => userSetting != null && h.StateId == userSetting.State.StateId && h.HolidayDate.Month == displayMonth.Month && h.HolidayDate.Year == displayMonth.Year) + .OrderBy(h => h.HolidayDate) // Order by date for display + .ToList(); + + records = records.OrderBy(r => r.OtDate).ToList(); + var stream = new MemoryStream(); + + Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4.Landscape()); + page.Margin(30); + + page.Content().Column(column => + { + // Top Section: Logo, User Info, Generated Date + column.Item().Row(row => + { + row.RelativeItem(2).Column(col => + { + if (logoImage != null) + { + col.Item().PaddingBottom(10).Container().Height(25).Image(logoImage, ImageScaling.FitArea); + } + + // Option 1: Inline with Clear Labels for user details + col.Item().PaddingBottom(2).Text(text => + { + text.Span("Name: ").SemiBold().FontSize(9); + text.Span($"{user.FullName} |").FontSize(9); + + text.Span(" Department: ").SemiBold().FontSize(9); + text.Span($"{user.Department?.DepartmentName ?? "N/A"} |").FontSize(9); + + if (!string.IsNullOrEmpty(flexiHour)) + { + text.Span(" Flexi Hour: ").SemiBold().FontSize(9); + text.Span($"{flexiHour}").FontSize(9); + } + }); + }); + + row.RelativeItem(1).AlignRight().Text($"Generated: {DateTime.Now:dd MMM yyyy HH:mm}") + .FontSize(9).FontColor(Colors.Grey.Medium); + }); + + // Overtime Record title remains separate for clarity + column.Item().PaddingTop(3).Row(row => + { + row.RelativeItem(2).Text($"Overtime Record: {displayMonth:MMMM yyyy}").SemiBold().FontSize(9).Italic(); + + // Legend (remains on the same line as Overtime Record) + row.RelativeItem(1).AlignRight().Column(legendCol => + { + legendCol.Item().Element(e => e.ShowEntire().Row(subRow => + { + subRow.AutoItem().Text(text => + { + text.Span(" ").BackgroundColor("#add8e6").FontSize(9); + text.Span(" Weekend").FontSize(8).FontColor(Colors.Black); + }); + })); + legendCol.Item().Element(e => e.ShowEntire().Row(subRow => + { + subRow.AutoItem().Text(text => + { + text.Span(" ").BackgroundColor("#ffc0cb").FontSize(9); + text.Span(" Public Holiday").FontSize(8).FontColor(Colors.Black); + }); + })); + }); + }); + + column.Item().PaddingVertical(10).LineHorizontal(0.5f).LineColor(Colors.Grey.Lighten2); + + // Pass the fetched publicHolidays to ComposeSimpleTable + column.Item().Element(container => ComposeSimpleTable(container, records, user, showStationColumn, allDatesInMonth, publicHolidaysForUser.Select(h => h.HolidayDate.Date).ToList())); + + // Public Holidays List (this section remains as is for now, below the table) + column.Item().PaddingTop(20).Element(container => + { + container.ShowEntire().Row(row => + { + row.RelativeItem().Column(legendCol => + { + // Removed the legend remarks from here as they are now above the table + }); + + // Public Holidays List (on the right) + row.RelativeItem(2).AlignRight().Column(holidayListCol => + { + holidayListCol.Item().Element(e => e.ShowEntire().Text("Public Holidays:").FontSize(9).SemiBold()); + + holidayListCol.Item().Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(1); // For Date + columns.RelativeColumn(2); // For Holiday Name + }); + + table.Header(header => + { + header.Cell().Border(0.25f).Padding(2).Text("Date").FontSize(7).Bold().AlignCenter(); + header.Cell().Border(0.25f).Padding(2).Text("Holiday Name").FontSize(7).Bold().AlignCenter(); + }); + + if (publicHolidaysForUser.Any()) + { + foreach (var holiday in publicHolidaysForUser) + { + table.Cell().Border(0.25f).Padding(2).Text($"{holiday.HolidayDate:dd-MM-yyyy}").FontSize(7).AlignCenter(); + table.Cell().Border(0.25f).Padding(2).Text(holiday.HolidayName).FontSize(7).AlignLeft(); + } + } + else + { + table.Cell().ColumnSpan(2).Border(0.25f).Padding(2) + .Text($"No public holidays found for {userSetting?.State?.StateName ?? "this state"} in {displayMonth:MMMM yyyy}.") // Changed format + .FontSize(7).Italic().AlignCenter(); + } + }); + }); + }); + }); + }); + }); + }).GeneratePdf(stream); + + stream.Position = 0; + return stream; + } + + // IMPORTANT: The ComposeSimpleTable method signature now accepts List for public holidays. + private void ComposeSimpleTable(IContainer container, List records, UserModel user, bool showStationColumn, List allDatesInMonth, List publicHolidays) + { + var recordsGroupedByDate = records + .GroupBy(r => r.OtDate.Date) + .ToDictionary(g => g.Key, g => g.ToList()); + + // Fetch user settings and public holidays within ComposeSimpleTable + var userSetting = _centralDbContext.Hrusersetting + .Include(us => us.State) + .FirstOrDefault(us => us.UserId == user.Id); + + container.Table(table => + { + // Define columns + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(0.8f); // Day (ddd) + columns.RelativeColumn(1); // Date + + // Office Hour group + columns.RelativeColumn(1); // From + columns.RelativeColumn(1); // To + columns.RelativeColumn(0.8f); // Break + + // After Office Hour group + columns.RelativeColumn(1); // From + columns.RelativeColumn(1); // To + columns.RelativeColumn(0.8f); // Break + + columns.RelativeColumn(1); // Total OT Hours + columns.RelativeColumn(0.8f); // Break (min) + + if (showStationColumn) + { + columns.RelativeColumn(1.5f); // Station + } + + columns.RelativeColumn(2); // Description + }); + + // Header + table.Header(header => + { + // First row of the header + header.Cell().RowSpan(2).Background("#d9ead3").Border(0.25f).Padding(3) + .Text("Day").FontSize(6).Bold().AlignCenter(); + + header.Cell().RowSpan(2).Background("#d9ead3").Border(0.25f).Padding(3) + .Text("Date").FontSize(6).Bold().AlignCenter(); + + header.Cell().ColumnSpan(3).Background("#cfe2f3").Border(0.25f).Padding(3) + .Text("Office Hour").FontSize(6).Bold().AlignCenter(); + + header.Cell().ColumnSpan(3).Background("#cfe2f3").Border(0.25f).Padding(3) + .Text("After Office Hour").FontSize(6).Bold().AlignCenter(); + + header.Cell().RowSpan(2).Background("#fce5cd").Border(0.25f).Padding(3) + .Text("Total OT Hours").FontSize(6).Bold().AlignCenter(); + header.Cell().RowSpan(2).Background("#fce5cd").Border(0.25f).Padding(3) + .Text("Break (min)").FontSize(6).Bold().AlignCenter(); + + if (showStationColumn) + { + header.Cell().RowSpan(2).Background("#d0f0ef").Border(0.25f).Padding(3) + .Text("Station").FontSize(6).Bold().AlignCenter(); + } + + header.Cell().RowSpan(2).Background("#e3f2fd").Border(0.25f).Padding(3) + .Text("Description").FontSize(6).Bold().AlignCenter(); + + // Second row of the header (sub-headers) + header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3) + .Text("From").FontSize(6).Bold().AlignCenter(); + header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3) + .Text("To").FontSize(6).Bold().AlignCenter(); + header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3) + .Text("Break").FontSize(6).Bold().AlignCenter(); + + header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3) + .Text("From").FontSize(6).Bold().AlignCenter(); + header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3) + .Text("To").FontSize(6).Bold().AlignCenter(); + header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3) + .Text("Break").FontSize(6).Bold().AlignCenter(); + }); + + decimal totalHours = 0; + decimal totalBreak = 0; + bool alternate = false; + DateTime? previousDate = null; // To avoid repeating Day/Date for multiple records on the same day + + foreach (var date in allDatesInMonth) + { + recordsGroupedByDate.TryGetValue(date, out var dateRecords); + var recordsToShow = dateRecords ?? new List { new OtRegisterModel { OtDate = date } }; + recordsToShow = recordsToShow.OrderBy(r => r.OfficeFrom ?? r.AfterFrom).ToList(); + + foreach (var record in recordsToShow) + { + // Get background color for the current date based on user's state settings + var backgroundColor = GetDayCellBackgroundColor(date, userSetting?.State?.WeekendId, publicHolidays); + + string rowBg = alternate ? "#f9f9f9" : "#ffffff"; + if (dateRecords == null || !dateRecords.Any()) + rowBg = alternate ? Colors.Grey.Lighten5 : Colors.White; + + void AddCell(string value, bool center = true, string? bg = null) + { + var cell = table.Cell().Background(bg ?? rowBg).Border(0.25f).Padding(2); + var text = cell.Text(value).FontSize(6); + if (center) text.AlignCenter(); + } + + // Apply background color to Day and Date cells + if (date.Date != previousDate?.Date) + { + AddCell(date.ToString("ddd"), true, backgroundColor); + AddCell(date.ToString("dd-MM-yyyy"), true, backgroundColor); + } + else + { + // If multiple OT entries for the same day, don't repeat Day and Date, but still apply background + AddCell("", true, backgroundColor); + AddCell("", true, backgroundColor); + } + previousDate = date; // Update previousDate for the next iteration + + + // Calculate values + var officeHours = CalculateOfficeOtHours(record); + var afterHours = CalculateAfterOfficeOtHours(record); + var totalTime = ConvertTimeToDecimal(officeHours) + ConvertTimeToDecimal(afterHours); + var breakTime = (record.OfficeBreak ?? 0) + (record.AfterBreak ?? 0); + + totalHours += totalTime; + totalBreak += breakTime; + + // Office Hour + AddCell(record.OfficeFrom?.ToString(@"hh\:mm") ?? ""); + AddCell(record.OfficeTo?.ToString(@"hh\:mm") ?? ""); + AddCell(FormatAsHourMinute(record.OfficeBreak, isMinutes: true)); + + // After Office Hour + AddCell(record.AfterFrom?.ToString(@"hh\:mm") ?? ""); + AddCell(record.AfterTo?.ToString(@"hh\:mm") ?? ""); + AddCell(FormatAsHourMinute(record.AfterBreak, isMinutes: true)); + + // Totals + AddCell(FormatAsHourMinute(totalTime, isMinutes: false)); + AddCell(FormatAsHourMinute(breakTime, isMinutes: true)); + + // Station (if applicable) + if (showStationColumn) + { + AddCell(record.Stations?.StationName ?? ""); + } + + // Description + table.Cell().Background(rowBg).Border(0.25f).Padding(2) + .Text(record.OtDescription ?? "").FontSize(6).AlignLeft(); + + alternate = !alternate; + } + } + + // Footer with totals + table.Footer(footer => + { + // "TOTAL" will span only the first two columns (Day and Date) + footer.Cell().ColumnSpan(2).Background("#d8d1f5").Border(0.80f).Padding(3) + .Text("TOTAL").Bold().FontSize(6).AlignCenter(); + + // Create an empty cell that spans the remaining columns before "Total OT Hours" + // Remaining columns: Office Hour (3) + After Office Hour (3) = 6 columns + footer.Cell().ColumnSpan(6).Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter(); + + + // These remain aligned with their respective columns + footer.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text(FormatAsHourMinute(totalHours, isMinutes: false)).Bold().FontSize(6).AlignCenter(); + + footer.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) + .Text(FormatAsHourMinute(totalBreak, isMinutes: true)).Bold().FontSize(6).AlignCenter(); + + if (showStationColumn) + { + footer.Cell().Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter(); + } + + footer.Cell().Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter(); + }); + }); + } + private bool IsAdmin(int userId) + { + var userRoles = _centralDbContext.UserRoles + .Where(ur => ur.UserId == userId) + .Join(_centralDbContext.Roles, ur => ur.RoleId, r => r.Id, (ur, r) => r.Name) + .ToList(); + + return userRoles.Any(role => role.Contains("SuperAdmin") || role.Contains("SystemAdmin")); + } + + private string GetMonthYearString(List records) + { + if (records == null || !records.Any()) + return "No Records"; + + var firstDate = records.First().OtDate; + var lastDate = records.Last().OtDate; + + if (firstDate.Month == lastDate.Month && firstDate.Year == lastDate.Year) + return firstDate.ToString("MMMM yyyy"); // Fixed formatting to yyyy + + return $"{firstDate:MMM yyyy} - {lastDate:MMM yyyy}"; // Fixed formatting to yyyy + } + + private static IContainer CellStyle(IContainer container) + { + return container + .Padding(5) + .Border(1) + .BorderColor(QuestPDF.Helpers.Colors.Grey.Lighten2) + .AlignMiddle() + .AlignLeft(); + } + + private string FormatAsHourMinute(decimal? hoursOrMinutes, bool isMinutes = false) + { + if (hoursOrMinutes == null || hoursOrMinutes == 0) + return ""; + + TimeSpan ts = isMinutes + ? TimeSpan.FromMinutes((double)hoursOrMinutes.Value) + : TimeSpan.FromHours((double)hoursOrMinutes.Value); + + int totalHours = (int)(ts.TotalHours); + int minutes = (int)(ts.Minutes); + + return $"{totalHours}:{minutes:D2}"; + } + + private string GetDayType(DateTime date, int? weekendId, List publicHolidays) + { + if (publicHolidays.Contains(date.Date)) + { + return "Public Holiday"; + } + + DayOfWeek dayOfWeek = date.DayOfWeek; + + if ((weekendId == 1 && dayOfWeek == DayOfWeek.Saturday) || // Assuming weekendId 1 means Saturday is Rest Day + (weekendId == 2 && dayOfWeek == DayOfWeek.Sunday)) // Assuming weekendId 2 means Sunday is Rest Day + { + return "Rest Day"; + } + + if ((weekendId == 1 && dayOfWeek == DayOfWeek.Friday) || // Assuming weekendId 1 means Friday is Off Day + (weekendId == 2 && dayOfWeek == DayOfWeek.Saturday)) // Assuming weekendId 2 means Saturday is Off Day + { + return "Off Day"; + } + + return "Normal Day"; + } + + // Updated these methods to reflect the new calculation logic and accept the correct input. + private decimal CalculateOrp(decimal basicSalary) + { + return basicSalary / 26m; + } + + private decimal CalculateHrp(decimal orp) + { + return orp / 8m; + } + + private decimal ConvertTimeToDecimal(string time) + { + if (string.IsNullOrEmpty(time)) return 0; + + var parts = time.Split(':'); + if (parts.Length != 2) return 0; + + if (int.TryParse(parts[0], out int hours) && int.TryParse(parts[1], out int minutes)) + { + return hours + (minutes / 60m); + } + + return 0; + } + + private string CalculateOfficeOtHours(OtRegisterModel record) + { + if (!record.OfficeFrom.HasValue || !record.OfficeTo.HasValue) return ""; + + // Convert TimeSpan to minutes from start of the day + double fromMinutes = record.OfficeFrom.Value.TotalMinutes; + double toMinutes = record.OfficeTo.Value.TotalMinutes; + + double totalMinutes = toMinutes - fromMinutes; + + // Handle overnight shifts (e.g., 21:00 to 02:00) + if (totalMinutes < 0) + { + totalMinutes += 24 * 60; // Add 24 hours in minutes + } + + totalMinutes -= (record.OfficeBreak ?? 0); // Subtract break + totalMinutes = Math.Max(0, totalMinutes); // Ensure total hours are not negative + + int hours = (int)(totalMinutes / 60); + int minutes = (int)(totalMinutes % 60); + + return $"{hours}:{minutes:D2}"; + } + + private string CalculateAfterOfficeOtHours(OtRegisterModel record) + { + if (!record.AfterFrom.HasValue || !record.AfterTo.HasValue) return ""; + + // Convert TimeSpan to minutes from start of the day + double fromMinutes = record.AfterFrom.Value.TotalMinutes; + double toMinutes = record.AfterTo.Value.TotalMinutes; + + double totalMinutes = toMinutes - fromMinutes; + + // Handle overnight shifts (e.g., 21:00 to 02:00) + if (totalMinutes < 0) + { + totalMinutes += 24 * 60; // Add 24 hours in minutes + } + + totalMinutes -= (record.AfterBreak ?? 0); // Subtract break + totalMinutes = Math.Max(0, totalMinutes); // Ensure total hours are not negative + + int hours = (int)(totalMinutes / 60); + int minutes = (int)(totalMinutes % 60); + + return $"{hours}:{minutes:D2}"; + } + + private Dictionary ClassifyOt(OtRegisterModel record, decimal hrp, List publicHolidays, int? weekendId) + { + var officeRaw = CalculateOfficeOtHours(record); + var afterRaw = CalculateAfterOfficeOtHours(record); + + var officeHrs = ConvertTimeToDecimal(officeRaw); + var afterHrs = ConvertTimeToDecimal(afterRaw); + + // Helper to format numbers. Will return "" if num is 0 or less. + var toFixedOrEmpty = (decimal num) => num > 0 ? num.ToString("N2") : ""; + + var dayType = GetDayType(record.OtDate, weekendId, publicHolidays); + + var result = new Dictionary + { + ["ndAfter"] = "", + ["odUnder4"] = "", + ["odBetween4And8"] = "", + ["odAfter"] = "", + ["rdUnder4"] = "", + ["rdBetween4And8"] = "", + ["rdAfter"] = "", + ["phUnder8"] = "", + ["phAfter"] = "" + }; + + switch (dayType) + { + case "Normal Day": + result["ndAfter"] = toFixedOrEmpty(afterHrs); // Only after-office OT on normal days + break; + + case "Off Day": + if (officeHrs > 0) // Only process if there are office hours + { + if (officeHrs <= 4) // If total office hours are 4 or less + { + result["odUnder4"] = toFixedOrEmpty(officeHrs); // Assign all to 'under 4' + } + else if (officeHrs <= 8) // If total office hours are more than 4, but 8 or less + { + result["odBetween4And8"] = toFixedOrEmpty(officeHrs); // Assign all to '4-8' + } + else // If total office hours are more than 8 + { + result["odAfter"] = toFixedOrEmpty(officeHrs); // Assign all to 'OD After' (for office hours beyond 8) + } + } + // Add 'afterHrs' to 'odAfter' as it's separate "After Office" time + // If 'odAfter' only represents office hours > 8, you would need a new key like 'odSeparateAfterOffice'. + result["odAfter"] = toFixedOrEmpty(ConvertTimeToDecimal(result["odAfter"]) + afterHrs); + break; + + case "Rest Day": + if (officeHrs > 0) // Only process if there are office hours + { + if (officeHrs <= 4) // If total office hours are 4 or less + { + result["rdUnder4"] = toFixedOrEmpty(officeHrs); // Assign all to 'under 4' + } + else if (officeHrs <= 8) // If total office hours are more than 4, but 8 or less + { + result["rdBetween4And8"] = toFixedOrEmpty(officeHrs); // Assign all to '4-8' + } + else // If total office hours are more than 8 + { + result["rdAfter"] = toFixedOrEmpty(officeHrs); // Assign all to 'RD After' (for office hours beyond 8) + } + } + // Add 'afterHrs' to 'rdAfter' + result["rdAfter"] = toFixedOrEmpty(ConvertTimeToDecimal(result["rdAfter"]) + afterHrs); + break; + + case "Public Holiday": + if (officeHrs > 0) // Only process if there are office hours + { + if (officeHrs <= 8) // If total office hours are 8 or less + { + result["phUnder8"] = toFixedOrEmpty(officeHrs); // Assign all to 'under 8' + } + else // If total office hours are more than 8 + { + result["phAfter"] = toFixedOrEmpty(officeHrs); // Assign all to 'PH After' (for office hours beyond 8) + } + } + // Add 'afterHrs' to 'phAfter' + result["phAfter"] = toFixedOrEmpty(ConvertTimeToDecimal(result["phAfter"]) + afterHrs); + break; + } + + return result; + } + + private string CalculateTotalOtHrs(OtRegisterModel record, decimal hrp, List publicHolidays, int? weekendId) + { + var classified = ClassifyOt(record, hrp, publicHolidays, weekendId); + decimal total = 0; + + foreach (var value in classified.Values) + { + if (decimal.TryParse(value, out decimal num)) + { + total += num; + } + } + + return total > 0 ? total.ToString("N2") : ""; + } + + private string CalculateNdOdTotal(OtRegisterModel record, decimal hrp, List publicHolidays, int? weekendId) + { + var classified = ClassifyOt(record, hrp, publicHolidays, weekendId); + + var nd = decimal.TryParse(classified["ndAfter"], out decimal ndVal) ? ndVal : 0; + var od1 = decimal.TryParse(classified["odUnder4"], out decimal od1Val) ? od1Val : 0; + var od2 = decimal.TryParse(classified["odBetween4And8"], out decimal od2Val) ? od2Val : 0; + var od3 = decimal.TryParse(classified["odAfter"], out decimal od3Val) ? od3Val : 0; + + var total = nd + od1 + od2 + od3; + return total > 0 ? total.ToString("N2") : ""; + } + + private string CalculateRdTotal(OtRegisterModel record, decimal hrp, List publicHolidays, int? weekendId) + { + var classified = ClassifyOt(record, hrp, publicHolidays, weekendId); + var total = + (decimal.TryParse(classified["rdUnder4"], out decimal rd1) ? rd1 : 0) + + (decimal.TryParse(classified["rdBetween4And8"], out decimal rd2) ? rd2 : 0) + + (decimal.TryParse(classified["rdAfter"], out decimal rd3) ? rd3 : 0); + + return total > 0 ? total.ToString("N2") : ""; + } + + private string CalculatePhTotal(OtRegisterModel record, decimal hrp, List publicHolidays, int? weekendId) + { + var classified = ClassifyOt(record, hrp, publicHolidays, weekendId); + var total = + (decimal.TryParse(classified["phUnder8"], out decimal ph1) ? ph1 : 0) + + (decimal.TryParse(classified["phAfter"], out decimal ph2) ? ph2 : 0); + + return total > 0 ? total.ToString("N2") : ""; + } + + private string CalculateOtAmount(OtRegisterModel record, decimal hrp, List publicHolidays, int? weekendId) + { + // Now, orp and hrp are already correctly calculated based on the incoming userRate (which is now Basic Salary) + // in the ComposeTable method, so we pass hrp directly. + // However, the existing logic in this method relies on 'orp' and 'hrp' being derived + // in a specific way for the calculations. + // If the incoming 'hrp' to this method is indeed the HRP value, we need to calculate ORP from it. + // Re-calculating ORP and HRP here based on the 'hrp' received by this method to align with existing calculation logic. + decimal orpFromHrp = hrp * 8m; // This is the ORP used in calculations within this method + decimal basicSalaryFromOrp = orpFromHrp * 26m; // This is the Basic Salary derived from orpFromHrp + + var orp = orpFromHrp; // Use the ORP derived from the HRP passed to this function + var dayType = GetDayType(record.OtDate, weekendId, publicHolidays); + + var officeRaw = CalculateOfficeOtHours(record); + var afterRaw = CalculateAfterOfficeOtHours(record); + + var officeHours = ConvertTimeToDecimal(officeRaw); + var afterOfficeHours = ConvertTimeToDecimal(afterRaw); + + decimal amountOffice = 0; + decimal amountAfter = 0; + + if (officeHours > 0) + { + if (dayType == "Off Day" || dayType == "Rest Day") + { + if (officeHours <= 4) + { + amountOffice = 0.5m * orp; + } + else if (officeHours > 4 && officeHours <= 8) + { + amountOffice = 1 * orp; + } + } + else if (dayType == "Public Holiday") + { + amountOffice = 2 * orp; + } + } + + if (afterOfficeHours > 0) + { + switch (dayType) + { + case "Normal Day": + case "Off Day": + amountAfter = 1.5m * hrp * afterOfficeHours; + break; + case "Rest Day": + amountAfter = 2 * hrp * afterOfficeHours; + break; + case "Public Holiday": + amountAfter = 3 * hrp * afterOfficeHours; + break; + } + } + + var totalAmount = amountOffice + amountAfter; + return totalAmount.ToString("N2"); + } + private List GetAllDatesInMonth(DateTime month) + { + return Enumerable.Range(1, DateTime.DaysInMonth(month.Year, month.Month)) + .Select(day => new DateTime(month.Year, month.Month, day)) + .ToList(); + } + + private string GetDayCellBackgroundColor(DateTime date, int? weekendId, List publicHolidays) + { + if (publicHolidays.Contains(date.Date)) + return "#ffc0cb"; // Light red for Public Holiday + + var dayOfWeek = date.DayOfWeek; + // Assuming weekendId 1 for Friday/Saturday weekend, 2 for Saturday/Sunday + if ((weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) || + (weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday))) + return "#add8e6"; // Light blue for Weekend + + return "#ffffff"; // White for Normal Day + } + } +} \ No newline at end of file diff --git a/Areas/OTcalculate/Services/OvertimePdfService.cs b/Areas/OTcalculate/Services/OvertimePdfService.cs deleted file mode 100644 index 8da0c80..0000000 --- a/Areas/OTcalculate/Services/OvertimePdfService.cs +++ /dev/null @@ -1,237 +0,0 @@ -using QuestPDF.Fluent; -using QuestPDF.Helpers; -using QuestPDF.Infrastructure; -using System.IO; -using System.Collections.Generic; -using PSTW_CentralSystem.Areas.OTcalculate.Models; -using System; -using System.Linq; - -namespace PSTW_CentralSystem.Areas.OTcalculate.Services -{ - public class OvertimePdfService - { - public MemoryStream GenerateOvertimeTablePdf( - List records, - int departmentId, - string userFullName, - string departmentName, - int userStateId, - int weekendId, - List publicHolidays, - bool isAdminUser = false, - byte[]? logoImage = null) - { - records = records.OrderBy(r => r.OtDate).ToList(); - - var stream = new MemoryStream(); - - Document.Create(container => - { - container.Page(page => - { - page.Size(PageSizes.A4.Landscape()); - page.Margin(30); - - page.Content().Column(column => - { - // Header - column.Item().Row(row => - { - row.RelativeItem(2).Column(col => - { - if (logoImage != null) - { - col.Item().Container().Height(36).Image(logoImage, ImageScaling.FitArea); - col.Spacing(10); - } - - col.Item().Text($"Name: {userFullName}").FontSize(9).SemiBold(); - col.Item().Text($"Department: {departmentName}").FontSize(9).Italic(); - col.Item().Text($"Overtime Record: {GetMonthYearString(records)}").FontSize(9).Italic(); - }); - - row.RelativeItem(1).AlignRight().Text($"Generated: {DateTime.Now:dd MMM yyyy HH:mm}") - .FontSize(9).FontColor(Colors.Grey.Medium); - }); - - column.Item().PaddingVertical(10).LineHorizontal(0.5f).LineColor(Colors.Grey.Lighten2); - - // Table - column.Item().Table(table => - { - // Columns - table.ColumnsDefinition(columns => - { - columns.RelativeColumn(0.7f); // Day - columns.RelativeColumn(1.1f); // Date - columns.RelativeColumn(0.8f); // Office From - columns.RelativeColumn(0.8f); // Office To - columns.RelativeColumn(0.8f); // Office Break - columns.RelativeColumn(0.9f); // After From - columns.RelativeColumn(0.9f); // After To - columns.RelativeColumn(0.9f); // After Break - columns.RelativeColumn(); // Total OT - columns.RelativeColumn(); // Break Hours - columns.RelativeColumn(); // Net OT - if (departmentId == 2 || isAdminUser) - columns.RelativeColumn(); // Station - columns.RelativeColumn(2.7f); // Description - }); - - // Header - table.Header(header => - { - header.Cell().RowSpan(2).Background("#e0f7da").Border(0.25f).Padding(5).Text("Days").FontSize(9).Bold().AlignCenter(); - header.Cell().RowSpan(2).Background("#d0ead2").Border(0.25f).Padding(5).Text("Date").FontSize(9).Bold().AlignCenter(); - - header.Cell().ColumnSpan(3).Background("#dceefb").Border(0.25f).Padding(5).Text("Office Hours\n(8:30 - 17:30)").FontSize(9).Bold().AlignCenter(); - header.Cell().ColumnSpan(3).Background("#edf2f7").Border(0.25f).Padding(5).Text("After Office Hours\n(17:30 - 8:30)").FontSize(9).Bold().AlignCenter(); - - header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(5).Text("Total OT\nHours").FontSize(9).Bold().AlignCenter(); - header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(5).Text("Break Hours\n(min)").FontSize(9).Bold().AlignCenter(); - header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(5).Text("Net OT\nHours").FontSize(9).Bold().AlignCenter(); - - if (departmentId == 2 || isAdminUser) - header.Cell().RowSpan(2).Background("#d0f0ef").Border(0.25f).Padding(5).Text("Station").FontSize(9).Bold().AlignCenter(); - - header.Cell().RowSpan(2).Background("#e3f2fd").Border(0.25f).Padding(5).Text("Description").FontSize(9).Bold().AlignCenter(); - - // Subheaders for Office/After - header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("From").FontSize(9).Bold().AlignCenter(); - header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("To").FontSize(9).Bold().AlignCenter(); - header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("Break (min)").FontSize(9).Bold().AlignCenter(); - - header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("From").FontSize(9).Bold().AlignCenter(); - header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("To").FontSize(9).Bold().AlignCenter(); - header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("Break (min)").FontSize(9).Bold().AlignCenter(); - }); - - // Data - double totalOTSum = 0; - int totalBreakSum = 0; - TimeSpan totalNetOt = TimeSpan.Zero; - bool alternate = false; - - if (!records.Any()) - { - uint colspan = (uint)(departmentId == 2 ? 13 : 12); - table.Cell().ColumnSpan(colspan).Border(0.5f).Padding(10).AlignCenter() - .Text("No records found for selected month and year.") - .FontSize(10).FontColor(Colors.Grey.Darken2).Italic(); - } - else - { - foreach (var r in records) - { - var totalOT = CalculateTotalOT(r); - var totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0); - var netOT = totalOT - TimeSpan.FromMinutes(totalBreak); - - totalOTSum += totalOT.TotalHours; - totalBreakSum += totalBreak; - totalNetOt += netOT; - - string rowBg = alternate ? "#f9f9f9" : "#ffffff"; - alternate = !alternate; - - string backgroundColor = GetDayCellBackgroundColor(r.OtDate, userStateId, publicHolidays, weekendId); - - void AddCell(string value, bool center = true, string? bg = null) - { - var text = table.Cell().Background(bg ?? rowBg).Border(0.25f).Padding(5).Text(value).FontSize(9); - if (center) - text.AlignCenter(); - else - text.AlignLeft(); - } - - AddCell(r.OtDate.ToString("ddd"), true, backgroundColor); - AddCell(r.OtDate.ToString("dd/MM/yyyy")); - - AddCell(FormatTime(r.OfficeFrom)); - AddCell(FormatTime(r.OfficeTo)); - AddCell(r.OfficeBreak > 0 ? $"{r.OfficeBreak}" : ""); - - AddCell(FormatTime(r.AfterFrom)); - AddCell(FormatTime(r.AfterTo)); - AddCell(r.AfterBreak > 0 ? $"{r.AfterBreak}" : ""); - - AddCell(totalOT > TimeSpan.Zero ? $"{totalOT:hh\\:mm}" : ""); - AddCell(totalBreak > 0 ? $"{totalBreak}" : ""); - AddCell(netOT > TimeSpan.Zero ? $"{netOT:hh\\:mm}" : ""); - - if (departmentId == 2 || isAdminUser) - AddCell(r.Stations?.StationName ?? ""); - - table.Cell().Background(rowBg).Border(0.25f).Padding(5) - .Text(r.OtDescription ?? "-").FontSize(9).WrapAnywhere().LineHeight(1.2f); - } - - // Totals row - int totalCols = departmentId == 2 ? 13 : 12; - int spanCols = departmentId == 2 ? 9 : 8; - - for (int i = 0; i < spanCols; i++) - table.Cell().Background("#d8d1f5").Border(0.80f).Padding(5) - .Text(i == 0 ? "TOTAL" : "").Bold().FontSize(9).AlignCenter(); - - table.Cell().Background("#d8d1f5").Border(0.80f).Padding(5) - .Text($"{TimeSpan.FromHours(totalOTSum):hh\\:mm}").Bold().FontSize(9).AlignCenter(); - table.Cell().Background("#d8d1f5").Border(0.80f).Padding(5) - .Text($"{TimeSpan.FromMinutes(totalBreakSum):hh\\:mm}").Bold().FontSize(9).AlignCenter(); - table.Cell().Background("#d8d1f5").Border(0.80f).Padding(5) - .Text($"{totalNetOt:hh\\:mm}").Bold().FontSize(9).AlignCenter(); - - if (departmentId == 2 || isAdminUser) - table.Cell().Background("#d8d1f5").Border(0.80f); - table.Cell().Background("#d8d1f5").Border(0.80f); - } - }); - }); - }); - }).GeneratePdf(stream); - - stream.Position = 0; - return stream; - } - - private TimeSpan CalculateTotalOT(OtRegisterModel r) - { - TimeSpan office = (r.OfficeTo ?? TimeSpan.Zero) - (r.OfficeFrom ?? TimeSpan.Zero); - TimeSpan after = (r.AfterTo ?? TimeSpan.Zero) - (r.AfterFrom ?? TimeSpan.Zero); - if (after < TimeSpan.Zero) - after += TimeSpan.FromHours(24); - return office + after; - } - - private string FormatTime(TimeSpan? time) - { - if (time == null || time == TimeSpan.Zero) - return ""; - return time.Value.ToString(@"hh\:mm"); - } - - private string GetMonthYearString(List records) - { - if (records == null || !records.Any()) - return "No Data"; - var firstDate = records.First().OtDate; - return $"{firstDate:MMMM yyyy}"; - } - - private string GetDayCellBackgroundColor(DateTime date, int userStateId, List publicHolidays, int weekendId) - { - if (publicHolidays.Any(h => h.HolidayDate.Date == date.Date)) - return "#ffc0cb"; // Pink - - var dayOfWeek = date.DayOfWeek; - if ((weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) || - (weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday))) - return "#add8e6"; // Blue - - return "#ffffff"; // Default - } - - } -} diff --git a/Areas/OTcalculate/Views/ApprovalDashboard/Approval.cshtml b/Areas/OTcalculate/Views/ApprovalDashboard/Approval.cshtml index 43ffa8d..aa26987 100644 --- a/Areas/OTcalculate/Views/ApprovalDashboard/Approval.cshtml +++ b/Areas/OTcalculate/Views/ApprovalDashboard/Approval.cshtml @@ -4,6 +4,7 @@ }
-
-
- -
-
- -
+
+ Pending Action : {{ overallPendingMonths.join(', ') }} +
+ + +
+
+ +
+
- - - - - - + + + - + - - - - + + + + -
Staff NameDate SubmitHoU StatusHoD StatusManager StatusHR Status + Staff Name + + + + + Date Submit + + + + + Status + + + + Action
{{ row.fullName }} {{ formatDate(row.submitDate) }}{{ row.houStatus }}{{ row.hodStatus }}{{ row.managerStatus }}{{ row.hrStatus }} - - - - +
HoU: {{ row.houStatus }}
+
HoD: {{ row.hodStatus }}
+
Manager: {{ row.managerStatus }}
+
HR: {{ row.hrStatus }}
+
+ Rejected by a previous approver +
+
+ + + + + +
+
No {{ activeTab === 'pending' ? 'pending' : 'completed' }} actions found for your current filters.
+ +
+
+ Showing {{ (currentPage - 1) * itemsPerPage + 1 }} to {{ Math.min(currentPage * itemsPerPage, filteredAndSortedOtStatusList.length) }} of {{ filteredAndSortedOtStatusList.length }} entries +
+ +
+ + +
+
@@ -92,12 +274,105 @@ 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(), + selectedMonth: parseInt(sessionStorage.getItem('approvalSelectedMonth')) || (new Date().getMonth() + 1), + selectedYear: parseInt(sessionStorage.getItem('approvalSelectedYear')) || new Date().getFullYear(), otStatusList: [], - userRoles: [] + activeTab: 'pending', + userRoles: [], + sortByColumn: 'submitDate', + sortDirection: 'desc', + searchQuery: '', + currentPage: 1, + itemsPerPage: 10, + overallPendingMonths: [] // To store the list of "MM/YYYY" strings }; }, + watch: { + activeTab() { + this.currentPage = 1; + this.searchQuery = ''; + }, + searchQuery() { + this.currentPage = 1; + }, + itemsPerPage() { + this.currentPage = 1; + } + }, + computed: { + filteredByTabOtStatusList() { + if (this.activeTab === 'pending') { + return this.otStatusList.filter(row => row.canApprove || (row.IsOverallRejected && (row.currentUserStatus !== 'Approved' && row.currentUserStatus !== 'Rejected'))); + } else if (this.activeTab === 'completed') { + return this.otStatusList.filter(row => row.currentUserStatus === 'Approved' || row.currentUserStatus === 'Rejected'); + } + return []; + }, + searchedOtStatusList() { + if (!this.searchQuery) { + return this.filteredByTabOtStatusList; + } + const query = this.searchQuery.toLowerCase(); + return this.filteredByTabOtStatusList.filter(row => { + if (row.fullName && row.fullName.toLowerCase().includes(query)) return true; + if (row.currentUserStatus && row.currentUserStatus.toLowerCase().includes(query)) return true; + if (row.houStatus && row.houStatus.toLowerCase().includes(query)) return true; + if (row.hodStatus && row.hodStatus.toLowerCase().includes(query)) return true; + if (row.managerStatus && row.managerStatus.toLowerCase().includes(query)) return true; + if (row.hrStatus && row.hrStatus.toLowerCase().includes(query)) return true; + return false; + }); + }, + filteredAndSortedOtStatusList() { + let sortedList = [...this.searchedOtStatusList]; + if (this.sortByColumn) { + sortedList.sort((a, b) => { + let valA = a[this.sortByColumn]; + let valB = b[this.sortByColumn]; + + if (this.sortByColumn === 'currentUserStatus') { + const statusOrder = { 'Pending': 1, 'Approved': 2, 'Rejected': 3, 'N/A': 4, null: 5, undefined: 6, '': 7 }; + const orderA = statusOrder[valA] || 99; + const orderB = statusOrder[valB] || 99; + if (this.sortDirection === 'asc') return orderA - orderB; + else return orderB - orderA; + } + else if (this.sortByColumn === 'submitDate') { + valA = valA ? new Date(valA) : new Date(0); + valB = valB ? new Date(valB) : new Date(0); + if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1; + if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1; + } + else if (typeof valA === 'string' && typeof valB === 'string') { + valA = valA.toLowerCase(); + valB = valB.toLowerCase(); + if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1; + if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1; + } + else { + if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1; + if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1; + } + return 0; + }); + } + return sortedList; + }, + totalPages() { + return Math.ceil(this.filteredAndSortedOtStatusList.length / this.itemsPerPage); + }, + paginatedData() { + const start = (this.currentPage - 1) * this.itemsPerPage; + const end = start + this.itemsPerPage; + return this.filteredAndSortedOtStatusList.slice(start, end); + }, + pendingActionsCount() { + return this.otStatusList.filter(row => row.canApprove).length; + }, + completedActionsCount() { + return this.otStatusList.filter(row => row.currentUserStatus === 'Approved' || row.currentUserStatus === 'Rejected').length; + } + }, methods: { loadData() { fetch(`/OvertimeAPI/GetPendingApproval?month=${this.selectedMonth}&year=${this.selectedYear}`) @@ -108,19 +383,31 @@ .then(result => { this.userRoles = result.roles; this.otStatusList = result.data; + this.overallPendingMonths = result.overallPendingMonths || []; + this.currentPage = 1; }) .catch(err => { console.error("Error loading data:", err); + alert("Error loading data: " + err.message); }); }, formatDate(dateStr) { + if (!dateStr) return ''; const d = new Date(dateStr); return d.toLocaleDateString(); }, + getStatusBadgeClass(status) { + switch (status) { + case 'Approved': return 'badge badge-approved'; + case 'Rejected': return 'badge badge-rejected'; + case 'Pending': return 'badge badge-pending'; + default: return 'badge bg-secondary'; + } + }, updateStatus(statusId, decision) { - console.log('statusId received:', statusId); // Add this for immediate inspection if (!statusId) { console.error("Invalid statusId passed to updateStatus."); + alert("Error: Invalid request ID."); return; } @@ -135,29 +422,43 @@ body: JSON.stringify({ statusId: statusId, decision: decision }) }) .then(res => { - if (!res.ok) throw new Error("Failed to update status"); + if (!res.ok) { + return res.json().then(err => { throw new Error(err.message || "Failed to update status"); }); + } return res.json(); }) .then(() => { - this.loadData(); // Refresh table + alert(`Request ${decision.toLowerCase()} successfully.`); + this.loadData(); }) .catch(err => { console.error("Error updating status:", err); + alert("Error: " + err.message); }); }, viewOtData(statusId) { - // Navigate to another page with the statusId in query string + sessionStorage.setItem('approvalSelectedMonth', this.selectedMonth); + sessionStorage.setItem('approvalSelectedYear', this.selectedYear); window.location.href = `/OTcalculate/ApprovalDashboard/OtReview?statusId=${statusId}`; - + }, + sortBy(column) { + if (this.sortByColumn === column) { + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.sortByColumn = column; + this.sortDirection = 'asc'; + } + this.currentPage = 1; } - }, mounted() { this.loadData(); + }, + beforeUnmount() { + sessionStorage.setItem('approvalSelectedMonth', this.selectedMonth); + sessionStorage.setItem('approvalSelectedYear', this.selectedYear); } }); app.mount('#app'); - - - + \ No newline at end of file diff --git a/Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml b/Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml index ea3b0e5..c6d54f0 100644 --- a/Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml +++ b/Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml @@ -4,6 +4,7 @@ }
@@ -55,6 +74,7 @@
Employee Name: {{ userInfo.fullName }}
Department: {{ userInfo.departmentName || 'N/A' }}
+ Flexi Hour: {{ userInfo.flexiHour || 'N/A' }}
-
- - - - - - - - + + + + + + + + + + + + + + + + + + @@ -86,47 +115,263 @@ + + + + + + + + + - - + + + + - + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateOffice Hour
(8:30 - 17:30)
After Office Hour
(17:30 - 8:30)
Total OT HoursBreak (min)Net OT HoursStationDaysBasic Salary
(RM)
ORPHRPDateOffice HourAfter Office HourOT Hrs
(Office Hour)
OT Hrs
(After Office Hour)
Normal DayOff DayRest DayPublic HolidayTotal OT HrsTotal OT Hrs
ND & OD
Total OT Hrs
RD
Total OT Hrs
PH
OT Amt (RM)Station Description Action
From To BreakND OT after office hrsOD Within office hrs < 4hrsOD Within office hrs > 4hrs < 8 hrsOD After office hrsRD Within office hrs < 4hrsRD Within office hrs > 4hrs < 8 hrsRD After office hrsPH Within office hrs < 8 hrsPH After office hrs
{{ formatDate(record.otDate) }}
{{ formatDate(record.otDate) }} {{ formatTime(record.officeFrom) }} {{ formatTime(record.officeTo) }}{{ record.officeBreak }}{{ formatBreakToHourMinute(record.officeBreak, false) }} {{ formatTime(record.afterFrom) }} {{ formatTime(record.afterTo) }}{{ record.afterBreak }}{{ formatHourMinute(calculateTotalTime(record)) }}{{ calculateTotalBreak(record) }}{{ formatHourMinute(calculateNetTime(record)) }}{{ record.stationName || 'N/A' }}{{ record.otDays }}{{ record.otDescription }}{{ formatBreakToHourMinute(record.afterBreak, false) }}{{ formatTimeFromDecimal(calculateRawDuration(record.officeFrom, record.officeTo, record.officeBreak)) }}{{ formatTimeFromDecimal(calculateRawDuration(record.afterFrom, record.afterTo, record.afterBreak)) }}{{ classifyOt(record).ndAfter }}{{ classifyOt(record).odUnder4 }}{{ classifyOt(record).odBetween4And8 }}{{ classifyOt(record).odAfter }}{{ classifyOt(record).rdUnder4 }}{{ classifyOt(record).rdBetween4And8 }}{{ classifyOt(record).rdAfter }}{{ classifyOt(record).phUnder8 }}{{ classifyOt(record).phAfter }}{{ calculateTotalOtHrs(record) }}{{ calculateNdOdTotal(record) }}{{ calculateRdTotal(record) }}{{ calculatePhTotal(record) }}{{ calculateOtAmount(record) }}{{ record.stationName || 'N/A' }} +
+ {{ record.otDescription }} +
+
No overtime details found for this submission.No overtime details found for this submission.
TOTALTOTAL{{ formatBreakToHourMinute(totals.officeBreak) }}{{ formatBreakToHourMinute(totals.afterBreak) }}{{ totals.ndAfter }}{{ totals.odUnder4 }}{{ totals.odBetween4And8 }}{{ totals.odAfter }}{{ totals.rdUnder4 }}{{ totals.rdBetween4And8 }}{{ totals.rdAfter }}{{ totals.phUnder8 }}{{ totals.phAfter }}{{ totals.totalOtHrs }}{{ totals.totalNdOd }}{{ totals.totalRd }}{{ totals.totalPh }}{{ totals.otAmt }}
- - - + + + +
+ + + @section Scripts { + -} +} \ No newline at end of file diff --git a/Areas/OTcalculate/Views/Overtime/OtRegister.cshtml b/Areas/OTcalculate/Views/Overtime/OtRegister.cshtml index c940ea3..c013deb 100644 --- a/Areas/OTcalculate/Views/Overtime/OtRegister.cshtml +++ b/Areas/OTcalculate/Views/Overtime/OtRegister.cshtml @@ -1,4 +1,5 @@ -@{ + +@{ ViewData["Title"] = "Register Overtime"; Layout = "~/Views/Shared/_Layout.cshtml"; } @@ -11,10 +12,9 @@
- + -
OFFICE HOURS
@@ -22,18 +22,16 @@
- + v-on:change="officeFrom = roundToNearest30(officeFrom); calculateOTAndBreak()">
- + v-on:change="officeTo = roundToNearest30(officeTo); calculateOTAndBreak()">
- @@ -46,16 +44,16 @@
+ v-on:change="afterFrom = roundToNearest30(afterFrom); calculateOTAndBreak()">
+ v-on:change="afterTo = roundToNearest30(afterTo); calculateOTAndBreak()">
- @@ -63,20 +61,29 @@
-
- - + + *Only for PSTW AIR
- +
+ + + *Only for PSTW MARINE +
- +