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; 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 isRestrictedUser = 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); var userSetting = _centralDbContext.Hrusersetting .Include(us => us.State) .FirstOrDefault(us => us.UserId == user.Id); 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) .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 => { column.Item().Row(row => { row.RelativeItem(2).Column(col => { if (logoImage != null) { col.Item().PaddingBottom(10).Container().Height(25).Image(logoImage, ImageScaling.FitArea); } 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); }); column.Item().PaddingTop(3).Row(row => { row.RelativeItem(2).Text($"Overtime Record: {displayMonth:MMMM yyyy}").FontSize(9).Italic(); 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 new isRestrictedUser flag to ComposeTable column.Item().Element(container => ComposeTable(container, records, user, userRate, showStationColumn, allDatesInMonth, isRestrictedUser, publicHolidaysForUser.Select(h => h.HolidayDate.Date).ToList())); column.Item().PaddingTop(20).Element(container => { container.ShowEntire().Row(row => { 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")) .FontSize(8).AlignCenter(); } })); } })); } })); row.RelativeItem().Element(e => e.ShowEntire().Column(remarksCol => { remarksCol.Item().Element(e => e.ShowEntire().Row(remarksRow => { 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); columns.RelativeColumn(2); }); 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}.") .FontSize(7).Italic().AlignCenter(); } }); })); })); })); }); }); 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; } // Update the signature of ComposeTable to accept the new flag private void ComposeTable(IContainer container, List records, UserModel user, decimal userRate, bool showStationColumn, List allDatesInMonth, bool hideSalaryDetails, 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 => { // Conditionally add salary columns based on hideSalaryDetails if (!hideSalaryDetails) { 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 (!hideSalaryDetails) // Conditionally add Total Amt column { columns.RelativeColumn(0.25f); } if (showStationColumn) { columns.RelativeColumn(0.3f); } columns.RelativeColumn(0.7f); }); table.Header(header => { // Conditionally add salary headers based on hideSalaryDetails if (!hideSalaryDetails) { 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 (!hideSalaryDetails) // Conditionally add Total Amt header { 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; 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(); } // Conditionally render salary details if (!hideSalaryDetails) { // Display the calculated values here AddCell(!hasPrintedSalaryDetails ? $"{basicSalary:F2}" : ""); AddCell(!hasPrintedSalaryDetails ? $"{orp:F2}" : ""); AddCell(!hasPrintedSalaryDetails ? $"{hrp:F2}" : ""); } hasPrintedSalaryDetails = true; 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 (!hideSalaryDetails) // Conditionally render Total Amt column { 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; } } // Adjust column span for the TOTAL row based on visibility if (!hideSalaryDetails) { 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 (!hideSalaryDetails) // Conditionally render Total Amt in footer { table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) .Text($"{Math.Round(grandTotalOtAmount, MidpointRounding.AwayFromZero):F2}").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, 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); var userSetting = _centralDbContext.Hrusersetting .Include(us => us.State) .FirstOrDefault(us => us.UserId == user.Id); 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) .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 => { column.Item().Row(row => { row.RelativeItem(2).Column(col => { if (logoImage != null) { col.Item().PaddingBottom(10).Container().Height(25).Image(logoImage, ImageScaling.FitArea); } 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); }); column.Item().PaddingTop(3).Row(row => { row.RelativeItem(2).Text($"Overtime Record: {displayMonth:MMMM yyyy}").SemiBold().FontSize(9).Italic(); 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); column.Item().Element(container => ComposeSimpleTable(container, records, user, showStationColumn, allDatesInMonth, publicHolidaysForUser.Select(h => h.HolidayDate.Date).ToList())); column.Item().PaddingTop(20).Element(container => { container.ShowEntire().Row(row => { row.RelativeItem().Column(legendCol => { }); 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); columns.RelativeColumn(2); }); 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}.") .FontSize(7).Italic().AlignCenter(); } }); }); }); }); }); }); }).GeneratePdf(stream); stream.Position = 0; return stream; } 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()); var userSetting = _centralDbContext.Hrusersetting .Include(us => us.State) .FirstOrDefault(us => us.UserId == user.Id); container.Table(table => { 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 => { 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; 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 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(); } 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; 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 (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 footer.Cell().ColumnSpan(2).Background("#d8d1f5").Border(0.80f).Padding(3) .Text("TOTAL").Bold().FontSize(6).AlignCenter(); footer.Cell().ColumnSpan(6).Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter(); 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"); return $"{firstDate:MMM yyyy} - {lastDate:MMM 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) || (weekendId == 2 && dayOfWeek == DayOfWeek.Sunday)) { return "Rest Day"; } if ((weekendId == 1 && dayOfWeek == DayOfWeek.Friday) || (weekendId == 2 && dayOfWeek == DayOfWeek.Saturday)) { return "Off Day"; } return "Normal Day"; } 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 ""; double fromMinutes = record.OfficeFrom.Value.TotalMinutes; double toMinutes = record.OfficeTo.Value.TotalMinutes; double totalMinutes = toMinutes - fromMinutes; if (totalMinutes < 0) { totalMinutes += 24 * 60; } 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 ""; double fromMinutes = record.AfterFrom.Value.TotalMinutes; double toMinutes = record.AfterTo.Value.TotalMinutes; double totalMinutes = toMinutes - fromMinutes; if (totalMinutes < 0) { totalMinutes += 24 * 60; } 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) // 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) } } 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) } } 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 { result["phAfter"] = toFixedOrEmpty(officeHrs); // Assign all to 'PH After' (for office hours beyond 8) } } 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) { decimal orp = hrp * 8m; 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"; var dayOfWeek = date.DayOfWeek; if ((weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) || (weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday))) return "#add8e6"; return "#ffffff"; } } }