This commit is contained in:
Naz 2025-06-11 10:30:45 +08:00
parent 5efe8e13c4
commit efd69601ec
17 changed files with 469 additions and 789 deletions

View File

@ -20,7 +20,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Controllers
} }
public IActionResult OtReview(int statusId) public IActionResult OtReview(int statusId)
{ {
ViewBag.StatusId = statusId; // If needed in the view ViewBag.StatusId = statusId;
return View(); return View();
} }

View File

@ -47,36 +47,34 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
return TimeSpan.TryParse(time, out TimeSpan result) ? result : null; return TimeSpan.TryParse(time, out TimeSpan result) ? result : null;
} }
// OtRegisterEditDto.cs
public class OtRegisterEditDto public class OtRegisterEditDto
{ {
public int OvertimeId { get; set; } public int OvertimeId { get; set; }
public DateTime OtDate { get; set; } public DateTime OtDate { get; set; }
public string? OfficeFrom { get; set; } // Use string to match input type (e.g., "09:00") public string? OfficeFrom { get; set; }
public string? OfficeTo { get; set; } // Use string to match input type public string? OfficeTo { get; set; }
public int? OfficeBreak { get; set; } public int? OfficeBreak { get; set; }
public string? AfterFrom { get; set; } // Use string to match input type public string? AfterFrom { get; set; }
public string? AfterTo { get; set; } // Use string to match input type public string? AfterTo { get; set; }
public int? AfterBreak { get; set; } public int? AfterBreak { get; set; }
public int? StationId { get; set; } public int? StationId { get; set; }
public string? OtDescription { 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 int StatusId { get; set; }
public string? OtDays { get; set; } public string? OtDays { get; set; }
} }
public class OtUpdateLog public class OtUpdateLog
{ {
public string ApproverRole { get; set; } // e.g., "HoU", "HoD", "Manager", "HR" public string ApproverRole { get; set; }
public int ApproverUserId { get; set; } public int ApproverUserId { get; set; }
public DateTime UpdateTimestamp { get; set; } public DateTime UpdateTimestamp { get; set; }
public string ChangeType { get; set; } // New: "Edit" or "Delete" public string ChangeType { get; set; }
// For "Edit" type
public OtRegisterModel? BeforeEdit { get; set; } public OtRegisterModel? BeforeEdit { get; set; }
public OtRegisterEditDto? AfterEdit { get; set; } public OtRegisterEditDto? AfterEdit { get; set; }
// For "Delete" type
public OtRegisterModel? DeletedRecord { get; set; } public OtRegisterModel? DeletedRecord { get; set; }
} }

View File

@ -23,6 +23,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
{ {
public string ApproverName { get; set; } public string ApproverName { get; set; }
public byte[]? SignatureImage { get; set; } public byte[]? SignatureImage { get; set; }
public DateTime? ApprovedDate { get; set; } // New property for the approval date public DateTime? ApprovedDate { get; set; }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using PSTW_CentralSystem.Areas.OTcalculate.Models; using PSTW_CentralSystem.Areas.OTcalculate.Models;
using PSTW_CentralSystem.Models; // Ensure this is included for CalendarModel, StateModel etc. using PSTW_CentralSystem.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PSTW_CentralSystem.DBContext; using PSTW_CentralSystem.DBContext;
@ -22,14 +22,14 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
} }
public MemoryStream GenerateOvertimeTablePdf( public MemoryStream GenerateOvertimeTablePdf(
List<OtRegisterModel> records, List<OtRegisterModel> records,
UserModel user, UserModel user,
decimal userRate, decimal userRate,
DateTime? selectedMonth = null, DateTime? selectedMonth = null,
byte[]? logoImage = null, byte[]? logoImage = null,
bool isHoU = false, bool isHoU = false,
string? flexiHour = null, string? flexiHour = null,
List<ApprovalSignatureData>? approvedSignatures = null) List<ApprovalSignatureData>? approvedSignatures = null)
{ {
bool isAdminUser = IsAdmin(user.Id); bool isAdminUser = IsAdmin(user.Id);
bool showStationColumn = user.departmentId == 2 || user.departmentId == 3 || isAdminUser; bool showStationColumn = user.departmentId == 2 || user.departmentId == 3 || isAdminUser;
@ -39,15 +39,13 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
var allDatesInMonth = GetAllDatesInMonth(displayMonth); var allDatesInMonth = GetAllDatesInMonth(displayMonth);
// Fetch user setting here once
var userSetting = _centralDbContext.Hrusersetting var userSetting = _centralDbContext.Hrusersetting
.Include(us => us.State) .Include(us => us.State)
.FirstOrDefault(us => us.UserId == user.Id); .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 var publicHolidaysForUser = _centralDbContext.Holidays
.Where(h => userSetting != null && h.StateId == userSetting.State.StateId && h.HolidayDate.Month == displayMonth.Month && h.HolidayDate.Year == displayMonth.Year) .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 .OrderBy(h => h.HolidayDate)
.ToList(); .ToList();
records = records.OrderBy(r => r.OtDate).ToList(); records = records.OrderBy(r => r.OtDate).ToList();
@ -62,7 +60,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
page.Content().Column(column => page.Content().Column(column =>
{ {
// Top Section: Logo, User Info, Generated Date
column.Item().Row(row => column.Item().Row(row =>
{ {
row.RelativeItem(2).Column(col => row.RelativeItem(2).Column(col =>
@ -72,7 +69,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
col.Item().PaddingBottom(10).Container().Height(25).Image(logoImage, ImageScaling.FitArea); 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 => col.Item().PaddingBottom(2).Text(text =>
{ {
text.Span("Name: ").SemiBold().FontSize(9); text.Span("Name: ").SemiBold().FontSize(9);
@ -93,12 +89,10 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
.FontSize(9).FontColor(Colors.Grey.Medium); .FontSize(9).FontColor(Colors.Grey.Medium);
}); });
// Overtime Record title remains separate for clarity
column.Item().PaddingTop(3).Row(row => column.Item().PaddingTop(3).Row(row =>
{ {
row.RelativeItem(2).Text($"Overtime Record: {displayMonth:MMMM yyyy}").FontSize(9).Italic(); 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 => row.RelativeItem(1).AlignRight().Column(legendCol =>
{ {
legendCol.Item().Element(e => e.ShowEntire().Row(subRow => legendCol.Item().Element(e => e.ShowEntire().Row(subRow =>
@ -123,15 +117,13 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
column.Item().PaddingVertical(10).LineHorizontal(0.5f).LineColor(Colors.Grey.Lighten2); 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())); 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 => column.Item().PaddingTop(20).Element(container =>
{ {
container.ShowEntire().Row(row => container.ShowEntire().Row(row =>
{ {
// Left side - Approval Signatures
row.RelativeItem().Element(e => e.ShowEntire().Column(approvalCol => row.RelativeItem().Element(e => e.ShowEntire().Column(approvalCol =>
{ {
if (approvedSignatures != null && approvedSignatures.Any()) if (approvedSignatures != null && approvedSignatures.Any())
@ -161,8 +153,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
if (approval.ApprovedDate.HasValue) if (approval.ApprovedDate.HasValue)
{ {
individualApprovalColumn.Item().PaddingTop(2) individualApprovalColumn.Item().PaddingTop(2)
.Text(approval.ApprovedDate.Value.ToString("dd MMMM yyyy")) // Changed format to avoid special characters .Text(approval.ApprovedDate.Value.ToString("dd MMMM yyyy"))
.FontSize(8).AlignCenter(); .FontSize(8).AlignCenter();
} }
})); }));
} }
@ -170,12 +162,10 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
} }
})); }));
// Right side - Remarks and Public Holidays
row.RelativeItem().Element(e => e.ShowEntire().Column(remarksCol => row.RelativeItem().Element(e => e.ShowEntire().Column(remarksCol =>
{ {
remarksCol.Item().Element(e => e.ShowEntire().Row(remarksRow => 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 => remarksRow.RelativeItem(2).Element(e => e.ShowEntire().AlignRight().Column(holidayListCol =>
{ {
holidayListCol.Item().Text("Public Holidays:").FontSize(9).SemiBold(); holidayListCol.Item().Text("Public Holidays:").FontSize(9).SemiBold();
@ -183,8 +173,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{ {
table.ColumnsDefinition(columns => table.ColumnsDefinition(columns =>
{ {
columns.RelativeColumn(1); // For Date columns.RelativeColumn(1);
columns.RelativeColumn(2); // For Holiday Name columns.RelativeColumn(2);
}); });
table.Header(header => table.Header(header =>
@ -204,8 +194,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
else else
{ {
table.Cell().ColumnSpan(2).Border(0.25f).Padding(2) 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 .Text($"No public holidays found for {userSetting?.State?.StateName ?? "this state"} in {displayMonth:MMMM yyyy}.")
.FontSize(7).Italic().AlignCenter(); .FontSize(7).Italic().AlignCenter();
} }
}); });
})); }));
@ -213,7 +203,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
})); }));
}); });
}); });
// Add the automatically generated document text only if signatures are present
if (approvedSignatures != null && approvedSignatures.Any()) 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); 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);
@ -363,7 +353,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
bool alternate = false; bool alternate = false;
// Changed calculations as per your request
var basicSalary = userRate; // userRate is now treated as basic salary var basicSalary = userRate; // userRate is now treated as basic salary
var orp = basicSalary / 26m; // Calculate ORP from basicSalary var orp = basicSalary / 26m; // Calculate ORP from basicSalary
var hrp = orp / 8m; // Calculate HRP from ORP var hrp = orp / 8m; // Calculate HRP from ORP
@ -431,7 +420,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
AddCell(!hasPrintedSalaryDetails ? $"{orp:F2}" : ""); AddCell(!hasPrintedSalaryDetails ? $"{orp:F2}" : "");
AddCell(!hasPrintedSalaryDetails ? $"{hrp:F2}" : ""); AddCell(!hasPrintedSalaryDetails ? $"{hrp:F2}" : "");
} }
hasPrintedSalaryDetails = true; // Ensure these values are printed only once hasPrintedSalaryDetails = true;
if (date.Date != previousDate?.Date) if (date.Date != previousDate?.Date)
{ {
@ -554,7 +543,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
if (!isHoU) if (!isHoU)
{ {
table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) table.Cell().Background("#d8d1f5").Border(0.80f).Padding(3)
.Text($"{grandTotalOtAmount:N2}").Bold().FontSize(6).AlignCenter(); .Text($"{Math.Round(grandTotalOtAmount, MidpointRounding.AwayFromZero):F2}").Bold().FontSize(6).AlignCenter();
} }
if (showStationColumn) if (showStationColumn)
@ -566,12 +555,12 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
} }
public MemoryStream GenerateSimpleOvertimeTablePdf( public MemoryStream GenerateSimpleOvertimeTablePdf(
List<OtRegisterModel> records, List<OtRegisterModel> records,
UserModel user, // User parameter added here UserModel user,
decimal userRate, decimal userRate,
DateTime? selectedMonth = null, DateTime? selectedMonth = null,
byte[]? logoImage = null, byte[]? logoImage = null,
string? flexiHour = null) string? flexiHour = null)
{ {
bool isAdminUser = IsAdmin(user.Id); bool isAdminUser = IsAdmin(user.Id);
bool showStationColumn = user.departmentId == 2 || user.departmentId == 3 || isAdminUser; bool showStationColumn = user.departmentId == 2 || user.departmentId == 3 || isAdminUser;
@ -581,15 +570,13 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
var allDatesInMonth = GetAllDatesInMonth(displayMonth); var allDatesInMonth = GetAllDatesInMonth(displayMonth);
// Fetch user setting here once
var userSetting = _centralDbContext.Hrusersetting var userSetting = _centralDbContext.Hrusersetting
.Include(us => us.State) .Include(us => us.State)
.FirstOrDefault(us => us.UserId == user.Id); .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 var publicHolidaysForUser = _centralDbContext.Holidays
.Where(h => userSetting != null && h.StateId == userSetting.State.StateId && h.HolidayDate.Month == displayMonth.Month && h.HolidayDate.Year == displayMonth.Year) .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 .OrderBy(h => h.HolidayDate)
.ToList(); .ToList();
records = records.OrderBy(r => r.OtDate).ToList(); records = records.OrderBy(r => r.OtDate).ToList();
@ -604,7 +591,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
page.Content().Column(column => page.Content().Column(column =>
{ {
// Top Section: Logo, User Info, Generated Date
column.Item().Row(row => column.Item().Row(row =>
{ {
row.RelativeItem(2).Column(col => row.RelativeItem(2).Column(col =>
@ -614,7 +600,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
col.Item().PaddingBottom(10).Container().Height(25).Image(logoImage, ImageScaling.FitArea); 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 => col.Item().PaddingBottom(2).Text(text =>
{ {
text.Span("Name: ").SemiBold().FontSize(9); text.Span("Name: ").SemiBold().FontSize(9);
@ -635,12 +620,10 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
.FontSize(9).FontColor(Colors.Grey.Medium); .FontSize(9).FontColor(Colors.Grey.Medium);
}); });
// Overtime Record title remains separate for clarity
column.Item().PaddingTop(3).Row(row => column.Item().PaddingTop(3).Row(row =>
{ {
row.RelativeItem(2).Text($"Overtime Record: {displayMonth:MMMM yyyy}").SemiBold().FontSize(9).Italic(); 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 => row.RelativeItem(1).AlignRight().Column(legendCol =>
{ {
legendCol.Item().Element(e => e.ShowEntire().Row(subRow => legendCol.Item().Element(e => e.ShowEntire().Row(subRow =>
@ -664,20 +647,17 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
column.Item().PaddingVertical(10).LineHorizontal(0.5f).LineColor(Colors.Grey.Lighten2); 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())); 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 => column.Item().PaddingTop(20).Element(container =>
{ {
container.ShowEntire().Row(row => container.ShowEntire().Row(row =>
{ {
row.RelativeItem().Column(legendCol => 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 => row.RelativeItem(2).AlignRight().Column(holidayListCol =>
{ {
holidayListCol.Item().Element(e => e.ShowEntire().Text("Public Holidays:").FontSize(9).SemiBold()); holidayListCol.Item().Element(e => e.ShowEntire().Text("Public Holidays:").FontSize(9).SemiBold());
@ -686,8 +666,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{ {
table.ColumnsDefinition(columns => table.ColumnsDefinition(columns =>
{ {
columns.RelativeColumn(1); // For Date columns.RelativeColumn(1);
columns.RelativeColumn(2); // For Holiday Name columns.RelativeColumn(2);
}); });
table.Header(header => table.Header(header =>
@ -708,7 +688,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{ {
table.Cell().ColumnSpan(2).Border(0.25f).Padding(2) 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 .Text($"No public holidays found for {userSetting?.State?.StateName ?? "this state"} in {displayMonth:MMMM yyyy}.") // Changed format
.FontSize(7).Italic().AlignCenter(); .FontSize(7).Italic().AlignCenter();
} }
}); });
}); });
@ -722,21 +702,18 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
return stream; return stream;
} }
// IMPORTANT: The ComposeSimpleTable method signature now accepts List<DateTime> for public holidays.
private void ComposeSimpleTable(IContainer container, List<OtRegisterModel> records, UserModel user, bool showStationColumn, List<DateTime> allDatesInMonth, List<DateTime> publicHolidays) private void ComposeSimpleTable(IContainer container, List<OtRegisterModel> records, UserModel user, bool showStationColumn, List<DateTime> allDatesInMonth, List<DateTime> publicHolidays)
{ {
var recordsGroupedByDate = records var recordsGroupedByDate = records
.GroupBy(r => r.OtDate.Date) .GroupBy(r => r.OtDate.Date)
.ToDictionary(g => g.Key, g => g.ToList()); .ToDictionary(g => g.Key, g => g.ToList());
// Fetch user settings and public holidays within ComposeSimpleTable
var userSetting = _centralDbContext.Hrusersetting var userSetting = _centralDbContext.Hrusersetting
.Include(us => us.State) .Include(us => us.State)
.FirstOrDefault(us => us.UserId == user.Id); .FirstOrDefault(us => us.UserId == user.Id);
container.Table(table => container.Table(table =>
{ {
// Define columns
table.ColumnsDefinition(columns => table.ColumnsDefinition(columns =>
{ {
columns.RelativeColumn(0.8f); // Day (ddd) columns.RelativeColumn(0.8f); // Day (ddd)
@ -766,53 +743,52 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
// Header // Header
table.Header(header => table.Header(header =>
{ {
// First row of the header
header.Cell().RowSpan(2).Background("#d9ead3").Border(0.25f).Padding(3) header.Cell().RowSpan(2).Background("#d9ead3").Border(0.25f).Padding(3)
.Text("Day").FontSize(6).Bold().AlignCenter(); .Text("Day").FontSize(6).Bold().AlignCenter();
header.Cell().RowSpan(2).Background("#d9ead3").Border(0.25f).Padding(3) header.Cell().RowSpan(2).Background("#d9ead3").Border(0.25f).Padding(3)
.Text("Date").FontSize(6).Bold().AlignCenter(); .Text("Date").FontSize(6).Bold().AlignCenter();
header.Cell().ColumnSpan(3).Background("#cfe2f3").Border(0.25f).Padding(3) header.Cell().ColumnSpan(3).Background("#cfe2f3").Border(0.25f).Padding(3)
.Text("Office Hour").FontSize(6).Bold().AlignCenter(); .Text("Office Hour").FontSize(6).Bold().AlignCenter();
header.Cell().ColumnSpan(3).Background("#cfe2f3").Border(0.25f).Padding(3) header.Cell().ColumnSpan(3).Background("#cfe2f3").Border(0.25f).Padding(3)
.Text("After Office Hour").FontSize(6).Bold().AlignCenter(); .Text("After Office Hour").FontSize(6).Bold().AlignCenter();
header.Cell().RowSpan(2).Background("#fce5cd").Border(0.25f).Padding(3) header.Cell().RowSpan(2).Background("#fce5cd").Border(0.25f).Padding(3)
.Text("Total OT Hours").FontSize(6).Bold().AlignCenter(); .Text("Total OT Hours").FontSize(6).Bold().AlignCenter();
header.Cell().RowSpan(2).Background("#fce5cd").Border(0.25f).Padding(3) header.Cell().RowSpan(2).Background("#fce5cd").Border(0.25f).Padding(3)
.Text("Break (min)").FontSize(6).Bold().AlignCenter(); .Text("Break (min)").FontSize(6).Bold().AlignCenter();
if (showStationColumn) if (showStationColumn)
{ {
header.Cell().RowSpan(2).Background("#d0f0ef").Border(0.25f).Padding(3) header.Cell().RowSpan(2).Background("#d0f0ef").Border(0.25f).Padding(3)
.Text("Station").FontSize(6).Bold().AlignCenter(); .Text("Station").FontSize(6).Bold().AlignCenter();
} }
header.Cell().RowSpan(2).Background("#e3f2fd").Border(0.25f).Padding(3) header.Cell().RowSpan(2).Background("#e3f2fd").Border(0.25f).Padding(3)
.Text("Description").FontSize(6).Bold().AlignCenter(); .Text("Description").FontSize(6).Bold().AlignCenter();
// Second row of the header (sub-headers) // Second row of the header (sub-headers)
header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3) header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3)
.Text("From").FontSize(6).Bold().AlignCenter(); .Text("From").FontSize(6).Bold().AlignCenter();
header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3) header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3)
.Text("To").FontSize(6).Bold().AlignCenter(); .Text("To").FontSize(6).Bold().AlignCenter();
header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3) header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3)
.Text("Break").FontSize(6).Bold().AlignCenter(); .Text("Break").FontSize(6).Bold().AlignCenter();
header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3) header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3)
.Text("From").FontSize(6).Bold().AlignCenter(); .Text("From").FontSize(6).Bold().AlignCenter();
header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3) header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3)
.Text("To").FontSize(6).Bold().AlignCenter(); .Text("To").FontSize(6).Bold().AlignCenter();
header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3) header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3)
.Text("Break").FontSize(6).Bold().AlignCenter(); .Text("Break").FontSize(6).Bold().AlignCenter();
}); });
decimal totalHours = 0; decimal totalHours = 0;
decimal totalBreak = 0; decimal totalBreak = 0;
bool alternate = false; bool alternate = false;
DateTime? previousDate = null; // To avoid repeating Day/Date for multiple records on the same day DateTime? previousDate = null;
foreach (var date in allDatesInMonth) foreach (var date in allDatesInMonth)
{ {
@ -822,7 +798,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
foreach (var record in recordsToShow) 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); var backgroundColor = GetDayCellBackgroundColor(date, userSetting?.State?.WeekendId, publicHolidays);
string rowBg = alternate ? "#f9f9f9" : "#ffffff"; string rowBg = alternate ? "#f9f9f9" : "#ffffff";
@ -836,7 +811,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
if (center) text.AlignCenter(); if (center) text.AlignCenter();
} }
// Apply background color to Day and Date cells
if (date.Date != previousDate?.Date) if (date.Date != previousDate?.Date)
{ {
AddCell(date.ToString("ddd"), true, backgroundColor); AddCell(date.ToString("ddd"), true, backgroundColor);
@ -844,14 +818,11 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
} }
else 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);
AddCell("", true, backgroundColor); AddCell("", true, backgroundColor);
} }
previousDate = date; // Update previousDate for the next iteration previousDate = date;
// Calculate values
var officeHours = CalculateOfficeOtHours(record); var officeHours = CalculateOfficeOtHours(record);
var afterHours = CalculateAfterOfficeOtHours(record); var afterHours = CalculateAfterOfficeOtHours(record);
var totalTime = ConvertTimeToDecimal(officeHours) + ConvertTimeToDecimal(afterHours); var totalTime = ConvertTimeToDecimal(officeHours) + ConvertTimeToDecimal(afterHours);
@ -874,7 +845,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
AddCell(FormatAsHourMinute(totalTime, isMinutes: false)); AddCell(FormatAsHourMinute(totalTime, isMinutes: false));
AddCell(FormatAsHourMinute(breakTime, isMinutes: true)); AddCell(FormatAsHourMinute(breakTime, isMinutes: true));
// Station (if applicable) // Station
if (showStationColumn) if (showStationColumn)
{ {
AddCell(record.Stations?.StationName ?? ""); AddCell(record.Stations?.StationName ?? "");
@ -882,7 +853,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
// Description // Description
table.Cell().Background(rowBg).Border(0.25f).Padding(2) table.Cell().Background(rowBg).Border(0.25f).Padding(2)
.Text(record.OtDescription ?? "").FontSize(6).AlignLeft(); .Text(record.OtDescription ?? "").FontSize(6).AlignLeft();
alternate = !alternate; alternate = !alternate;
} }
@ -891,21 +862,17 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
// Footer with totals // Footer with totals
table.Footer(footer => table.Footer(footer =>
{ {
// "TOTAL" will span only the first two columns (Day and Date) // TOTAL
footer.Cell().ColumnSpan(2).Background("#d8d1f5").Border(0.80f).Padding(3) footer.Cell().ColumnSpan(2).Background("#d8d1f5").Border(0.80f).Padding(3)
.Text("TOTAL").Bold().FontSize(6).AlignCenter(); .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(); 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) footer.Cell().Background("#d8d1f5").Border(0.80f).Padding(3)
.Text(FormatAsHourMinute(totalHours, isMinutes: false)).Bold().FontSize(6).AlignCenter(); .Text(FormatAsHourMinute(totalHours, isMinutes: false)).Bold().FontSize(6).AlignCenter();
footer.Cell().Background("#d8d1f5").Border(0.80f).Padding(3) footer.Cell().Background("#d8d1f5").Border(0.80f).Padding(3)
.Text(FormatAsHourMinute(totalBreak, isMinutes: true)).Bold().FontSize(6).AlignCenter(); .Text(FormatAsHourMinute(totalBreak, isMinutes: true)).Bold().FontSize(6).AlignCenter();
if (showStationColumn) if (showStationColumn)
{ {
@ -935,9 +902,9 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
var lastDate = records.Last().OtDate; var lastDate = records.Last().OtDate;
if (firstDate.Month == lastDate.Month && firstDate.Year == lastDate.Year) if (firstDate.Month == lastDate.Month && firstDate.Year == lastDate.Year)
return firstDate.ToString("MMMM yyyy"); // Fixed formatting to yyyy return firstDate.ToString("MMMM yyyy");
return $"{firstDate:MMM yyyy} - {lastDate:MMM yyyy}"; // Fixed formatting to yyyy return $"{firstDate:MMM yyyy} - {lastDate:MMM yyyy}";
} }
private static IContainer CellStyle(IContainer container) private static IContainer CellStyle(IContainer container)
@ -956,8 +923,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
return ""; return "";
TimeSpan ts = isMinutes TimeSpan ts = isMinutes
? TimeSpan.FromMinutes((double)hoursOrMinutes.Value) ? TimeSpan.FromMinutes((double)hoursOrMinutes.Value)
: TimeSpan.FromHours((double)hoursOrMinutes.Value); : TimeSpan.FromHours((double)hoursOrMinutes.Value);
int totalHours = (int)(ts.TotalHours); int totalHours = (int)(ts.TotalHours);
int minutes = (int)(ts.Minutes); int minutes = (int)(ts.Minutes);
@ -974,14 +941,14 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
DayOfWeek dayOfWeek = date.DayOfWeek; DayOfWeek dayOfWeek = date.DayOfWeek;
if ((weekendId == 1 && dayOfWeek == DayOfWeek.Saturday) || // Assuming weekendId 1 means Saturday is Rest Day if ((weekendId == 1 && dayOfWeek == DayOfWeek.Saturday) ||
(weekendId == 2 && dayOfWeek == DayOfWeek.Sunday)) // Assuming weekendId 2 means Sunday is Rest Day (weekendId == 2 && dayOfWeek == DayOfWeek.Sunday))
{ {
return "Rest Day"; return "Rest Day";
} }
if ((weekendId == 1 && dayOfWeek == DayOfWeek.Friday) || // Assuming weekendId 1 means Friday is Off Day if ((weekendId == 1 && dayOfWeek == DayOfWeek.Friday) ||
(weekendId == 2 && dayOfWeek == DayOfWeek.Saturday)) // Assuming weekendId 2 means Saturday is Off Day (weekendId == 2 && dayOfWeek == DayOfWeek.Saturday))
{ {
return "Off Day"; return "Off Day";
} }
@ -989,7 +956,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
return "Normal Day"; return "Normal Day";
} }
// Updated these methods to reflect the new calculation logic and accept the correct input.
private decimal CalculateOrp(decimal basicSalary) private decimal CalculateOrp(decimal basicSalary)
{ {
return basicSalary / 26m; return basicSalary / 26m;
@ -1019,20 +985,18 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{ {
if (!record.OfficeFrom.HasValue || !record.OfficeTo.HasValue) return ""; if (!record.OfficeFrom.HasValue || !record.OfficeTo.HasValue) return "";
// Convert TimeSpan to minutes from start of the day
double fromMinutes = record.OfficeFrom.Value.TotalMinutes; double fromMinutes = record.OfficeFrom.Value.TotalMinutes;
double toMinutes = record.OfficeTo.Value.TotalMinutes; double toMinutes = record.OfficeTo.Value.TotalMinutes;
double totalMinutes = toMinutes - fromMinutes; double totalMinutes = toMinutes - fromMinutes;
// Handle overnight shifts (e.g., 21:00 to 02:00)
if (totalMinutes < 0) if (totalMinutes < 0)
{ {
totalMinutes += 24 * 60; // Add 24 hours in minutes totalMinutes += 24 * 60;
} }
totalMinutes -= (record.OfficeBreak ?? 0); // Subtract break totalMinutes -= (record.OfficeBreak ?? 0);
totalMinutes = Math.Max(0, totalMinutes); // Ensure total hours are not negative totalMinutes = Math.Max(0, totalMinutes);
int hours = (int)(totalMinutes / 60); int hours = (int)(totalMinutes / 60);
int minutes = (int)(totalMinutes % 60); int minutes = (int)(totalMinutes % 60);
@ -1044,20 +1008,18 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{ {
if (!record.AfterFrom.HasValue || !record.AfterTo.HasValue) return ""; if (!record.AfterFrom.HasValue || !record.AfterTo.HasValue) return "";
// Convert TimeSpan to minutes from start of the day
double fromMinutes = record.AfterFrom.Value.TotalMinutes; double fromMinutes = record.AfterFrom.Value.TotalMinutes;
double toMinutes = record.AfterTo.Value.TotalMinutes; double toMinutes = record.AfterTo.Value.TotalMinutes;
double totalMinutes = toMinutes - fromMinutes; double totalMinutes = toMinutes - fromMinutes;
// Handle overnight shifts (e.g., 21:00 to 02:00)
if (totalMinutes < 0) if (totalMinutes < 0)
{ {
totalMinutes += 24 * 60; // Add 24 hours in minutes totalMinutes += 24 * 60;
} }
totalMinutes -= (record.AfterBreak ?? 0); // Subtract break totalMinutes -= (record.AfterBreak ?? 0);
totalMinutes = Math.Max(0, totalMinutes); // Ensure total hours are not negative totalMinutes = Math.Max(0, totalMinutes);
int hours = (int)(totalMinutes / 60); int hours = (int)(totalMinutes / 60);
int minutes = (int)(totalMinutes % 60); int minutes = (int)(totalMinutes % 60);
@ -1073,7 +1035,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
var officeHrs = ConvertTimeToDecimal(officeRaw); var officeHrs = ConvertTimeToDecimal(officeRaw);
var afterHrs = ConvertTimeToDecimal(afterRaw); 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 toFixedOrEmpty = (decimal num) => num > 0 ? num.ToString("N2") : "";
var dayType = GetDayType(record.OtDate, weekendId, publicHolidays); var dayType = GetDayType(record.OtDate, weekendId, publicHolidays);
@ -1094,7 +1055,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
switch (dayType) switch (dayType)
{ {
case "Normal Day": case "Normal Day":
result["ndAfter"] = toFixedOrEmpty(afterHrs); // Only after-office OT on normal days result["ndAfter"] = toFixedOrEmpty(afterHrs);
break; break;
case "Off Day": case "Off Day":
@ -1113,8 +1074,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
result["odAfter"] = toFixedOrEmpty(officeHrs); // Assign all to 'OD After' (for office hours beyond 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); result["odAfter"] = toFixedOrEmpty(ConvertTimeToDecimal(result["odAfter"]) + afterHrs);
break; break;
@ -1134,7 +1094,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
result["rdAfter"] = toFixedOrEmpty(officeHrs); // Assign all to 'RD After' (for office hours beyond 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); result["rdAfter"] = toFixedOrEmpty(ConvertTimeToDecimal(result["rdAfter"]) + afterHrs);
break; break;
@ -1150,7 +1110,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
result["phAfter"] = toFixedOrEmpty(officeHrs); // Assign all to 'PH After' (for office hours beyond 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); result["phAfter"] = toFixedOrEmpty(ConvertTimeToDecimal(result["phAfter"]) + afterHrs);
break; break;
} }
@ -1210,16 +1170,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
private string CalculateOtAmount(OtRegisterModel record, decimal hrp, List<DateTime> publicHolidays, int? weekendId) private string CalculateOtAmount(OtRegisterModel record, decimal hrp, List<DateTime> publicHolidays, int? weekendId)
{ {
// Now, orp and hrp are already correctly calculated based on the incoming userRate (which is now Basic Salary) decimal orp = hrp * 8m;
// 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 dayType = GetDayType(record.OtDate, weekendId, publicHolidays);
var officeRaw = CalculateOfficeOtHours(record); var officeRaw = CalculateOfficeOtHours(record);
@ -1273,22 +1225,22 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
private List<DateTime> GetAllDatesInMonth(DateTime month) private List<DateTime> GetAllDatesInMonth(DateTime month)
{ {
return Enumerable.Range(1, DateTime.DaysInMonth(month.Year, month.Month)) return Enumerable.Range(1, DateTime.DaysInMonth(month.Year, month.Month))
.Select(day => new DateTime(month.Year, month.Month, day)) .Select(day => new DateTime(month.Year, month.Month, day))
.ToList(); .ToList();
} }
private string GetDayCellBackgroundColor(DateTime date, int? weekendId, List<DateTime> publicHolidays) private string GetDayCellBackgroundColor(DateTime date, int? weekendId, List<DateTime> publicHolidays)
{ {
if (publicHolidays.Contains(date.Date)) if (publicHolidays.Contains(date.Date))
return "#ffc0cb"; // Light red for Public Holiday return "#ffc0cb";
var dayOfWeek = date.DayOfWeek; var dayOfWeek = date.DayOfWeek;
// Assuming weekendId 1 for Friday/Saturday weekend, 2 for Saturday/Sunday
if ((weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) || if ((weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) ||
(weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday))) (weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday)))
return "#add8e6"; // Light blue for Weekend return "#add8e6";
return "#ffffff"; // White for Normal Day return "#ffffff";
} }
} }
} }

View File

@ -4,7 +4,6 @@
} }
<style> <style>
/* Your existing CSS styles remain here */
body { body {
background-color: #f3f4f6; background-color: #f3f4f6;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
@ -15,27 +14,26 @@
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 25px; padding: 25px;
margin-top: 20px; /* Adjusted margin-top for table-layer */ margin-top: 20px;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
} }
.table-container table { .table-container table {
width: 100%; width: 100%;
margin-bottom: 0; /* Remove default table margin */ margin-bottom: 0;
} }
.header { .header {
background-color: #007bff; background-color: #007bff;
color: white; color: white;
text-align: center; text-align: center;
vertical-align: middle; /* Center header text vertically */ vertical-align: middle;
} }
.btn-sm { .btn-sm {
font-size: 0.75rem; font-size: 0.75rem;
} }
/* Styles for status badges */
.badge-pending { .badge-pending {
background-color: #ffc107; background-color: #ffc107;
color: #343a40; color: #343a40;
@ -56,7 +54,6 @@
color: white; color: white;
} }
/* Custom style for action column buttons */
.action-buttons button { .action-buttons button {
margin-right: 5px; margin-right: 5px;
} }
@ -65,7 +62,6 @@
margin-right: 0; margin-right: 0;
} }
/* Sorting indicator styles */
.sortable-header { .sortable-header {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
@ -83,42 +79,40 @@
.pagination-controls { .pagination-controls {
display: flex; display: flex;
flex-wrap: wrap; /* Allow wrapping on smaller screens */ flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 10px 0; /* Add padding for separation */ padding: 10px 0;
border-top: 1px solid #e9ecef; /* Light border to separate from table */ border-top: 1px solid #e9ecef;
margin-top: 15px; /* Margin above pagination controls */ margin-top: 15px;
} }
.pagination-info { .pagination-info {
font-size: 0.85em; font-size: 0.85em;
color: #555; color: #555;
margin-right: 15px; /* Space between info and pagination links */ margin-right: 15px;
} }
/* Adjustments for "Items per page" dropdown */
.pagination-items-per-page { .pagination-items-per-page {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.pagination-items-per-page label { .pagination-items-per-page label {
white-space: nowrap; /* Prevent label from wrapping */ white-space: nowrap;
} }
/* New style for the formal overall pending notice */
.formal-pending-notice { .formal-pending-notice {
margin-top: 15px; margin-top: 15px;
margin-bottom: 20px; margin-bottom: 20px;
padding: 12px 20px; /* Slightly more padding for formal look */ padding: 12px 20px;
border-radius: 8px; /* More rounded corners */ border-radius: 8px;
background-color: #f8d7da; /* Light red/danger background for attention */ background-color: #f8d7da;
border: 1px solid #dc3545; /* Deeper red border */ border: 1px solid #dc3545;
color: #721c24; /* Dark red text for formal warning */ color: #721c24;
font-weight: bold; font-weight: bold;
text-align: left; /* Align text to the left */ text-align: left;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); /* Subtle shadow */ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
} }
</style> </style>
@ -284,7 +278,7 @@
searchQuery: '', searchQuery: '',
currentPage: 1, currentPage: 1,
itemsPerPage: 10, itemsPerPage: 10,
overallPendingMonths: [] // To store the list of "MM/YYYY" strings overallPendingMonths: []
}; };
}, },
watch: { watch: {

View File

@ -4,7 +4,6 @@
} }
<style> <style>
/* Your existing styles (excluding the previously added .date-column specific styles) */
.table-container { .table-container {
background-color: white; background-color: white;
border-radius: 15px; border-radius: 15px;
@ -386,7 +385,7 @@
flexiHour: '', flexiHour: '',
stateId: null, stateId: null,
weekendId: null, weekendId: null,
basicSalary: null // This will now hold the Basic Salary retrieved from API basicSalary: null
}, },
isHoU: false, isHoU: false,
expandedDescriptions: [], expandedDescriptions: [],
@ -468,7 +467,6 @@
this.otRecords.forEach(record => { this.otRecords.forEach(record => {
const classified = this.classifyOt(record); const classified = this.classifyOt(record);
// Breaks are already in minutes, no need for parseFloat and then toFixed for summing
totals.officeBreak += record.officeBreak || 0; totals.officeBreak += record.officeBreak || 0;
totals.afterBreak += record.afterBreak || 0; totals.afterBreak += record.afterBreak || 0;
@ -484,7 +482,6 @@
totals.phUnder8 += parseFloat(classified.phUnder8) || 0; totals.phUnder8 += parseFloat(classified.phUnder8) || 0;
totals.phAfter += parseFloat(classified.phAfter) || 0; totals.phAfter += parseFloat(classified.phAfter) || 0;
// Ensure these values are numeric before summing
totals.totalOtHrs += parseFloat(this.calculateTotalOtHrs(record)) || 0; totals.totalOtHrs += parseFloat(this.calculateTotalOtHrs(record)) || 0;
totals.totalNdOd += parseFloat(this.calculateNdOdTotal(record)) || 0; totals.totalNdOd += parseFloat(this.calculateNdOdTotal(record)) || 0;
totals.totalRd += parseFloat(this.calculateRdTotal(record)) || 0; totals.totalRd += parseFloat(this.calculateRdTotal(record)) || 0;
@ -494,13 +491,16 @@
}); });
for (let key in totals) { for (let key in totals) {
// Apply standard rounding (toNearestEven, like C# decimal.ToString("N2")) if (key.endsWith('Hrs') || key.endsWith('Amt')) {
// Only apply to total amounts, individual cell values are formatted as needed
if (key.endsWith('Hrs') || key.endsWith('Amt')) { // Target hour and amount totals if (key === 'otAmt') {
totals[key] = totals[key].toFixed(2); totals[key] = Math.round(totals[key]).toFixed(2);
} else if (key.endsWith('Break')) { // Break totals remain in minutes } else {
// Already summed as integers/floats, format later if needed for display totals[key] = totals[key].toFixed(2);
} else { // Classified hours (ndAfter, odUnder4 etc.) should be toFixed(2) }
} else if (key.endsWith('Break')) {
} else {
totals[key] = totals[key].toFixed(2); totals[key] = totals[key].toFixed(2);
} }
} }
@ -524,11 +524,7 @@
}, },
watch: { watch: {
'currentEditRecord.otDate': function(newDate) { 'currentEditRecord.otDate': function(newDate) {
// This watcher will trigger the getDayType computed property indirectly
// ensuring the 'Day' input in the modal updates reactively.
// No explicit action needed here, as the computed property handles it.
}, },
// Watch for changes in stationId to update Select2
'currentEditRecord.stationId': function(newVal) { 'currentEditRecord.stationId': function(newVal) {
if (this.showAirStationDropdown) { if (this.showAirStationDropdown) {
$('#airstationDropdown').val(newVal).trigger('change.select2'); $('#airstationDropdown').val(newVal).trigger('change.select2');
@ -536,7 +532,7 @@
$('#marinestationDropdown').val(newVal).trigger('change.select2'); $('#marinestationDropdown').val(newVal).trigger('change.select2');
} }
}, },
// Watch for changes in airStations and marineStations to update Select2 options
airStations: function() { airStations: function() {
if (this.showAirStationDropdown) { if (this.showAirStationDropdown) {
this.initSelect2('#airstationDropdown'); this.initSelect2('#airstationDropdown');
@ -549,9 +545,6 @@
} }
}, },
methods: { methods: {
// REMOVED: Custom rounding function (roundUpToTwoDecimals) as C# uses standard rounding
// and we'll use toFixed(2) directly for consistency.
toggleDescription(index) { toggleDescription(index) {
this.expandedDescriptions[index] = !this.expandedDescriptions[index]; this.expandedDescriptions[index] = !this.expandedDescriptions[index];
}, },
@ -564,10 +557,10 @@
const data = await res.json(); const data = await res.json();
this.otRecords = data.records; this.otRecords = data.records;
// Assuming data.userInfo.rate now holds the Basic Salary
this.userInfo = { this.userInfo = {
...data.userInfo, ...data.userInfo,
basicSalary: data.userInfo.rate // Assign the API's 'rate' (which is now Basic Salary) to basicSalary basicSalary: data.userInfo.rate
}; };
this.isHoU = data.isHoU; this.isHoU = data.isHoU;
this.currentEditRecord.statusId = parseInt(statusId); this.currentEditRecord.statusId = parseInt(statusId);
@ -602,38 +595,33 @@
} }
}, },
initSelect2(selector) { initSelect2(selector) {
// Destroy existing Select2 instance if any
if ($(selector).data('select2')) { if ($(selector).data('select2')) {
$(selector).select2('destroy'); $(selector).select2('destroy');
} }
// Initialize Select2
$(selector).select2({ $(selector).select2({
dropdownParent: $('#editOtModal') // Ensure dropdown is within the modal boundaries dropdownParent: $('#editOtModal')
}).on('change', (e) => { }).on('change', (e) => {
// Update Vue data when Select2 value changes
this.currentEditRecord.stationId = $(e.currentTarget).val(); this.currentEditRecord.stationId = $(e.currentTarget).val();
}); });
// Set the initial value if currentEditRecord.stationId is already set
// This needs to be called after options are populated and Select2 is initialized
if (this.currentEditRecord.stationId) { if (this.currentEditRecord.stationId) {
$(selector).val(this.currentEditRecord.stationId).trigger('change.select2'); $(selector).val(this.currentEditRecord.stationId).trigger('change.select2');
} }
}, },
// HRP = ORP / 8
calculateHrp() { calculateHrp() {
const orp = parseFloat(this.calculateOrp()); // Get ORP from the calculated value const orp = parseFloat(this.calculateOrp());
if (isNaN(orp) || orp <= 0) return 'N/A'; if (isNaN(orp) || orp <= 0) return 'N/A';
return (orp / 8).toFixed(2); return (orp / 8).toFixed(2);
}, },
// ORP = Basic Salary / 26
calculateOrp() { calculateOrp() {
const basicSalary = parseFloat(this.userInfo.basicSalary); const basicSalary = parseFloat(this.userInfo.basicSalary);
if (isNaN(basicSalary) || basicSalary <= 0) return 'N/A'; if (isNaN(basicSalary) || basicSalary <= 0) return 'N/A';
return (basicSalary / 26).toFixed(2); return (basicSalary / 26).toFixed(2);
}, },
// Basic Salary will now directly come from userInfo.basicSalary
calculateBasicSalary() { calculateBasicSalary() {
const basicSalary = parseFloat(this.userInfo.basicSalary); const basicSalary = parseFloat(this.userInfo.basicSalary);
if (isNaN(basicSalary) || basicSalary <= 0) return 'N/A'; if (isNaN(basicSalary) || basicSalary <= 0) return 'N/A';
@ -661,20 +649,18 @@
const minutes = parseFloat(breakValue || 0); const minutes = parseFloat(breakValue || 0);
if (isNaN(minutes) || minutes <= 0) return showZero ? '0:00' : ''; if (isNaN(minutes) || minutes <= 0) return showZero ? '0:00' : '';
const hrs = Math.floor(minutes / 60); const hrs = Math.floor(minutes / 60);
const mins = Math.round(minutes % 60); // Round minutes to nearest integer const mins = Math.round(minutes % 60);
return `${hrs.toString().padStart(1, '0')}:${mins.toString().padStart(2, '0')}`; return `${hrs.toString().padStart(1, '0')}:${mins.toString().padStart(2, '0')}`;
}, },
// New helper to format decimal hours into H:MM string for display columns
formatTimeFromDecimal(decimalHours) { formatTimeFromDecimal(decimalHours) {
if (decimalHours === null || isNaN(decimalHours) || decimalHours <= 0) return ''; if (decimalHours === null || isNaN(decimalHours) || decimalHours <= 0) return '';
const totalMinutes = Math.round(decimalHours * 60); // Convert to minutes and round const totalMinutes = Math.round(decimalHours * 60);
const hours = Math.floor(totalMinutes / 60); const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60; const minutes = totalMinutes % 60;
return `${hours}:${minutes.toString().padStart(2, '0')}`; return `${hours}:${minutes.toString().padStart(2, '0')}`;
}, },
// Renamed from convertTimeToDecimal to better reflect its purpose: parsing "HH:MM" to decimal hours
parseTimeSpanToDecimalHours(timeStr) { parseTimeSpanToDecimalHours(timeStr) {
if (!timeStr || typeof timeStr !== 'string') return 0; // Return 0 for empty/invalid if (!timeStr || typeof timeStr !== 'string') return 0;
const parts = timeStr.split(':'); const parts = timeStr.split(':');
if (parts.length !== 2) return 0; if (parts.length !== 2) return 0;
@ -685,7 +671,6 @@
return hours + (minutes / 60); return hours + (minutes / 60);
}, },
// This function now returns decimal hours, consistent with C# `TotalHours`
calculateRawDuration(fromTime, toTime, breakMins) { calculateRawDuration(fromTime, toTime, breakMins) {
if (!fromTime || !toTime) return 0; if (!fromTime || !toTime) return 0;
@ -699,25 +684,13 @@
const breakMinutes = parseFloat(breakMins || 0); const breakMinutes = parseFloat(breakMins || 0);
let totalMinutes = to - from - breakMinutes; let totalMinutes = to - from - breakMinutes;
if (totalMinutes < 0) totalMinutes += 24 * 60; // Handle overnight OT if (totalMinutes < 0) totalMinutes += 24 * 60;
return Math.max(0, totalMinutes / 60); // Return hours as decimal return Math.max(0, totalMinutes / 60);
}, },
// calculateOfficeOtHours and calculateAfterOfficeOtHours now call the new formatTimeFromDecimal
// to display H:MM format, but internally use calculateRawDuration which returns decimal hours.
// The C# version also returns H:MM strings.
// This also ensures 'OT Hrs (Office Hour)' column is blank for Normal Day.
// (Original C# `CalculateOfficeOtHours` also had the `totalMinutes = Math.Max(0, totalMinutes);` safeguard.)
// (Original C# `CalculateAfterOfficeOtHours` also had the `totalMinutes = Math.Max(0, totalMinutes);` safeguard.)
// No changes needed for these methods themselves, as they now correctly use `formatTimeFromDecimal`
// and `calculateRawDuration`.
// The `if (record.dayType === 'Normal Day') { return ''; }` part remains for display purposes,
// matching the C# PDF's decision to show empty for normal day office OT.
calculateOfficeOtHours(record) { calculateOfficeOtHours(record) {
if (!record.officeFrom || !record.officeTo) return ''; if (!record.officeFrom || !record.officeTo) return '';
if (record.dayType === 'Normal Day') { // Added as per PDF logic if (record.dayType === 'Normal Day') {
return ''; return '';
} }
const totalHours = this.calculateRawDuration(record.officeFrom, record.officeTo, record.officeBreak); const totalHours = this.calculateRawDuration(record.officeFrom, record.officeTo, record.officeBreak);
@ -733,9 +706,6 @@
classifyOt(record) { classifyOt(record) {
const officeHrs = this.calculateRawDuration(record.officeFrom, record.officeTo, record.officeBreak); const officeHrs = this.calculateRawDuration(record.officeFrom, record.officeTo, record.officeBreak);
const afterHrs = this.calculateRawDuration(record.afterFrom, record.afterTo, record.afterBreak); const afterHrs = this.calculateRawDuration(record.afterFrom, record.afterTo, record.afterBreak);
// Changed toFixed(2) to directly return number if num > 0, then toFixed(2) when used in table cells
// This aligns with C# returning decimal and then formatting later.
const toFixedOrEmpty = (num) => num > 0 ? num.toFixed(2) : ''; const toFixedOrEmpty = (num) => num > 0 ? num.toFixed(2) : '';
const result = { const result = {
@ -753,21 +723,19 @@
break; break;
case 'Off Day': case 'Off Day':
case 'Weekend': // Keep 'Weekend' here, it correctly maps to Off Day/Rest Day via getDayType case 'Weekend':
if (officeHrs > 0) { if (officeHrs > 0) {
// C# puts entire officeHrs into odAfter/rdAfter if > 8.
// It seems the column mapping implies odUnder4 (<=4), odBetween4And8 (>4, <=8), odAfter (>8) for office hrs,
// AND also after office hrs go to odAfter.
if (officeHrs <= 4) { if (officeHrs <= 4) {
result.odUnder4 = toFixedOrEmpty(officeHrs); result.odUnder4 = toFixedOrEmpty(officeHrs);
} else if (officeHrs <= 8) { // C# uses else if here, covering > 4 and <= 8 } else if (officeHrs <= 8) {
result.odBetween4And8 = toFixedOrEmpty(officeHrs); result.odBetween4And8 = toFixedOrEmpty(officeHrs);
} else { // For officeHrs > 8. C# puts all officeHrs into odAfter } else {
result.odAfter = toFixedOrEmpty(officeHrs); // Changed to match C# logic directly result.odAfter = toFixedOrEmpty(officeHrs);
} }
} }
// C# then adds afterHrs to odAfter.
result.odAfter = toFixedOrEmpty( (parseFloat(result.odAfter) || 0) + afterHrs ); // Ensure numeric addition result.odAfter = toFixedOrEmpty( (parseFloat(result.odAfter) || 0) + afterHrs );
break; break;
@ -775,28 +743,27 @@
if (officeHrs > 0) { if (officeHrs > 0) {
if (officeHrs <= 4) { if (officeHrs <= 4) {
result.rdUnder4 = toFixedOrEmpty(officeHrs); result.rdUnder4 = toFixedOrEmpty(officeHrs);
} else if (officeHrs <= 8) { // C# uses else if here, covering > 4 and <= 8 } else if (officeHrs <= 8) {
result.rdBetween4And8 = toFixedOrEmpty(officeHrs); result.rdBetween4And8 = toFixedOrEmpty(officeHrs);
} else { // For officeHrs > 8. C# puts all officeHrs into rdAfter } else {
result.rdAfter = toFixedOrEmpty(officeHrs); // Changed to match C# logic directly result.rdAfter = toFixedOrEmpty(officeHrs);
} }
} }
// C# then adds afterHrs to rdAfter.
result.rdAfter = toFixedOrEmpty( (parseFloat(result.rdAfter) || 0) + afterHrs ); // Ensure numeric addition result.rdAfter = toFixedOrEmpty( (parseFloat(result.rdAfter) || 0) + afterHrs );
break; break;
case 'Public Holiday': case 'Public Holiday':
// C# logic: if officeHrs <= 8, it's phUnder8. ELSE (if officeHrs > 8) it's phAfter.
// Then afterHrs are added to phAfter.
if (officeHrs > 0) { if (officeHrs > 0) {
if (officeHrs <= 8) { if (officeHrs <= 8) {
result.phUnder8 = toFixedOrEmpty(officeHrs); result.phUnder8 = toFixedOrEmpty(officeHrs);
} else { // officeHrs > 8 } else {
result.phAfter = toFixedOrEmpty(officeHrs); // Changed to match C# logic result.phAfter = toFixedOrEmpty(officeHrs);
} }
} }
// C# then adds afterHrs to phAfter.
result.phAfter = toFixedOrEmpty( (parseFloat(result.phAfter) || 0) + afterHrs ); // Ensure numeric addition result.phAfter = toFixedOrEmpty( (parseFloat(result.phAfter) || 0) + afterHrs );
break; break;
} }
@ -811,7 +778,7 @@
if (!isNaN(value)) total += value; if (!isNaN(value)) total += value;
} }
return total.toFixed(2); // Keep .toFixed(2) for display return total.toFixed(2);
}, },
calculateNdOdTotal(record) { calculateNdOdTotal(record) {
const classified = this.classifyOt(record); const classified = this.classifyOt(record);
@ -857,34 +824,22 @@
let amountAfter = 0; let amountAfter = 0;
if (officeHours > 0) { if (officeHours > 0) {
if (otType === 'Off Day' || otType === 'Rest Day') { // Removed 'Weekend' here, GetDayType handles it if (otType === 'Off Day' || otType === 'Rest Day') {
if (officeHours <= 4) { if (officeHours <= 4) {
amountOffice = 0.5 * orp; amountOffice = 0.5 * orp;
} else if (officeHours <= 8) { } else if (officeHours <= 8) {
amountOffice = 1 * orp; amountOffice = 1 * orp;
} }
// C# code logic does NOT have an else if (officeHours > 8) here.
// If officeHours > 8, it would fall through and use the last matching condition, which for Off/Rest day is `1 * orp;`
// So, this is the C# logic for office hours on Off/Rest days:
// The C# code doesn't explicitly state what happens for officeHours > 8 on Off/Rest Day.
// Based on the C# code, the `CalculateOtAmount` method for 'Off Day' and 'Rest Day' has:
// `if (officeHours <= 4) { amountOffice = 0.5m * orp; }`
// `else if (officeHours > 4 && officeHours <= 8) { amountOffice = 1 * orp; }`
// There is *no `else` branch* for `officeHours > 8`. This means if `officeHours` is 9, it would still get `1 * orp`.
// This indicates that the PDF's logic currently only defines fixed amounts for <=4h and >4h<=8h.
// To perfectly match PDF, the JS should also stop calculating `amountOffice` after `1 * orp` for >8 hours on Off/Rest.
// This confirms the previous analysis: C# does not have a special rule for `officeHours > 8` on Off/Rest days.
} else if (otType === 'Public Holiday') { } else if (otType === 'Public Holiday') {
// C# logic: `amountOffice = 2 * orp;` without conditions for PH Office Hours. amountOffice = 2 * orp;
// This means it's a fixed 2 * ORP regardless of hours.
amountOffice = 2 * orp; // Match C# logic directly
} }
} }
if (afterOfficeHours > 0) { if (afterOfficeHours > 0) {
switch (otType) { switch (otType) {
case 'Normal Day': case 'Normal Day':
case 'Off Day': // Changed to match C# logic (single case for ND and Off Day after-office hours) case 'Off Day':
amountAfter = 1.5 * hrp * afterOfficeHours; amountAfter = 1.5 * hrp * afterOfficeHours;
break; break;
case 'Rest Day': case 'Rest Day':
@ -897,9 +852,8 @@
} }
const totalAmount = amountOffice + amountAfter; const totalAmount = amountOffice + amountAfter;
return totalAmount.toFixed(2); // Use toFixed(2) for standard rounding return totalAmount.toFixed(2);
}, },
// --- NEW METHOD FOR ROUNDING MINUTES ---
roundMinutes(fieldName) { roundMinutes(fieldName) {
const timeValue = this.currentEditRecord[fieldName]; const timeValue = this.currentEditRecord[fieldName];
if (!timeValue) return; if (!timeValue) return;
@ -909,14 +863,12 @@
if (minutes >= 45 || minutes < 15) { if (minutes >= 45 || minutes < 15) {
roundedMinutes = 0; roundedMinutes = 0;
if (minutes >= 45) { // If minutes are 45-59, round up to next hour (00 minutes) if (minutes >= 45) {
let currentHour = hours; let currentHour = hours;
currentHour = (currentHour + 1) % 24; // Handle 23:xx rounding to 00:00 currentHour = (currentHour + 1) % 24;
this.currentEditRecord[fieldName] = `${String(currentHour).padStart(2, '0')}:00`; this.currentEditRecord[fieldName] = `${String(currentHour).padStart(2, '0')}:00`;
// No need to call validateTimeFields here, as it's called after the if condition this.validateTimeFields();
// and it will be called for all cases after rounding. return;
this.validateTimeFields(); // Still call here to update immediately if input changes
return; // Exit after setting
} }
} else if (minutes >= 15 && minutes < 45) { } else if (minutes >= 15 && minutes < 45) {
roundedMinutes = 30; roundedMinutes = 30;
@ -924,10 +876,8 @@
this.currentEditRecord[fieldName] = `${String(hours).padStart(2, '0')}:${String(roundedMinutes).padStart(2, '0')}`; this.currentEditRecord[fieldName] = `${String(hours).padStart(2, '0')}:${String(roundedMinutes).padStart(2, '0')}`;
// Trigger validation after rounding
this.validateTimeFields(); this.validateTimeFields();
}, },
// --- END NEW METHOD ---
editRecord(id) { editRecord(id) {
if (this.hasApproverActedLocal) { if (this.hasApproverActedLocal) {
alert("You cannot edit this record as you have already approved/rejected this OT submission."); alert("You cannot edit this record as you have already approved/rejected this OT submission.");
@ -957,7 +907,6 @@
otDays: recordToEdit.otDays otDays: recordToEdit.otDays
}; };
// Initialize Select2 after Vue has updated the DOM with the new data
this.$nextTick(() => { this.$nextTick(() => {
if (this.showAirStationDropdown) { if (this.showAirStationDropdown) {
this.initSelect2('#airstationDropdown'); this.initSelect2('#airstationDropdown');
@ -975,7 +924,6 @@
async submitEdit() { async submitEdit() {
try { try {
// Validate time ranges
const hasOfficeHours = this.currentEditRecord.officeFrom && this.currentEditRecord.officeTo; const hasOfficeHours = this.currentEditRecord.officeFrom && this.currentEditRecord.officeTo;
const hasAfterHours = this.currentEditRecord.afterFrom && this.currentEditRecord.afterTo; const hasAfterHours = this.currentEditRecord.afterFrom && this.currentEditRecord.afterTo;
@ -1053,23 +1001,20 @@
return 'Public Holiday'; return 'Public Holiday';
} }
// WeekendId 1: Friday (5) & Saturday (6)
// WeekendId 2: Saturday (6) & Sunday (0)
if (this.userState.weekendId === 1) { if (this.userState.weekendId === 1) {
if (dayOfWeek === 5) return 'Off Day'; // Friday if (dayOfWeek === 5) return 'Off Day';
if (dayOfWeek === 6) return 'Rest Day'; // Saturday if (dayOfWeek === 6) return 'Rest Day';
else return 'Normal Day'; else return 'Normal Day';
} else if (this.userState.weekendId === 2) { } else if (this.userState.weekendId === 2) {
if (dayOfWeek === 6) return 'Off Day'; // Saturday if (dayOfWeek === 6) return 'Off Day';
if (dayOfWeek === 0) return 'Rest Day'; // Sunday if (dayOfWeek === 0) return 'Rest Day';
else return 'Normal Day'; else return 'Normal Day';
} }
return 'Normal Day'; // Default if not a public holiday or weekend return 'Normal Day';
}, },
calculateModalTotalOtHrs() { calculateModalTotalOtHrs() {
// Use calculateRawDuration which returns decimal hours
const officeHrsDecimal = this.calculateRawDuration( const officeHrsDecimal = this.calculateRawDuration(
this.currentEditRecord.officeFrom, this.currentEditRecord.officeFrom,
this.currentEditRecord.officeTo, this.currentEditRecord.officeTo,
@ -1082,7 +1027,7 @@
this.currentEditRecord.afterBreak this.currentEditRecord.afterBreak
); );
const totalMinutes = Math.round((officeHrsDecimal + afterHrsDecimal) * 60); // Total minutes from decimal hours const totalMinutes = Math.round((officeHrsDecimal + afterHrsDecimal) * 60);
const totalHours = Math.floor(totalMinutes / 60); const totalHours = Math.floor(totalMinutes / 60);
const remainingMinutes = totalMinutes % 60; const remainingMinutes = totalMinutes % 60;
@ -1238,21 +1183,20 @@
}); });
}, },
validateTimeRangeForSubmission(fromTime, toTime, label) { validateTimeRangeForSubmission(fromTime, toTime, label) {
if (!fromTime || !toTime) return true; // Handled by outer checks if (!fromTime || !toTime) return true;
const start = this.parseTime(fromTime); const start = this.parseTime(fromTime);
const end = this.parseTime(toTime); const end = this.parseTime(toTime);
const minAllowedFromMinutesForMidnightTo = 16 * 60 + 30; // 4:30 PM const minAllowedFromMinutesForMidnightTo = 16 * 60 + 30;
const maxAllowedFromMinutesForMidnightTo = 23 * 60 + 30; // 11:30 PM const maxAllowedFromMinutesForMidnightTo = 23 * 60 + 30;
const startMinutes = start.hours * 60 + start.minutes; const startMinutes = start.hours * 60 + start.minutes;
if (end.hours === 0 && end.minutes === 0) { // If 'To' is 00:00 (midnight) if (end.hours === 0 && end.minutes === 0) {
if (fromTime === "00:00") { if (fromTime === "00:00") {
alert(`Invalid ${label} Time: 'From' and 'To' cannot both be 00:00 (midnight).`); alert(`Invalid ${label} Time: 'From' and 'To' cannot both be 00:00 (midnight).`);
return false; return false;
} }
// This is the specific rule: if 'To' is 00:00, 'From' must be within 4:30 PM and 11:30 PM
if (startMinutes < minAllowedFromMinutesForMidnightTo || startMinutes > maxAllowedFromMinutesForMidnightTo) { if (startMinutes < minAllowedFromMinutesForMidnightTo || startMinutes > maxAllowedFromMinutesForMidnightTo) {
alert(`Invalid ${label} Time: If 'To' is 12:00 am (00:00), 'From' must start between 4:30 pm and 11:30 pm on the same day to be saved.`); alert(`Invalid ${label} Time: If 'To' is 12:00 am (00:00), 'From' must start between 4:30 pm and 11:30 pm on the same day to be saved.`);
return false; return false;
@ -1264,7 +1208,6 @@
return true; return true;
}, },
validateTimeFields() { validateTimeFields() {
// This will validate the fields whenever they change
const hasOfficeHours = this.currentEditRecord.officeFrom && this.currentEditRecord.officeTo; const hasOfficeHours = this.currentEditRecord.officeFrom && this.currentEditRecord.officeTo;
const hasAfterHours = this.currentEditRecord.afterFrom && this.currentEditRecord.afterTo; const hasAfterHours = this.currentEditRecord.afterFrom && this.currentEditRecord.afterTo;
@ -1288,8 +1231,6 @@
await this.fetchStations(); await this.fetchStations();
}, },
mounted() { mounted() {
// Initialize Select2 when the component is mounted
// These will apply to the elements if they are visible initially based on userInfo
if (this.showAirStationDropdown) { if (this.showAirStationDropdown) {
this.initSelect2('#airstationDropdown'); this.initSelect2('#airstationDropdown');
} }
@ -1297,12 +1238,9 @@
this.initSelect2('#marinestationDropdown'); this.initSelect2('#marinestationDropdown');
} }
// Re-initialize Select2 when the modal is shown
var editModalElement = document.getElementById('editOtModal'); var editModalElement = document.getElementById('editOtModal');
if (editModalElement) { if (editModalElement) {
editModalElement.addEventListener('shown.bs.modal', () => { editModalElement.addEventListener('shown.bs.modal', () => {
// Re-initialize Select2 and set its value when the modal becomes visible
// This ensures Select2 is active and displays the correct value when the modal opens
if (this.showAirStationDropdown) { if (this.showAirStationDropdown) {
this.initSelect2('#airstationDropdown'); this.initSelect2('#airstationDropdown');
$('#airstationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2'); $('#airstationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');

View File

@ -13,7 +13,7 @@
<h1 class="font-light text-white"> <h1 class="font-light text-white">
<i class="mdi mdi-currency-usd"></i> <i class="mdi mdi-currency-usd"></i>
</h1> </h1>
<h6 class="text-white">Rate</h6> <h6 class="text-white">Salary</h6>
</div> </div>
</a> </a>
</div> </div>

View File

@ -14,7 +14,7 @@
} }
#ApprovalFlowTable th, #ApprovalFlowTable th,
#ApprovalFlowTable td { #ApprovalFlowTable td {
background-color: white !important; /* Ensure no transparency from Bootstrap */ background-color: white !important;
color: #323; color: #323;
} }
</style> </style>
@ -26,7 +26,7 @@
<a asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="Rate"> <a asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="Rate">
<div class="box bg-success text-center"> <div class="box bg-success text-center">
<h1 class="font-light text-white"><i class="mdi mdi-currency-usd"></i></h1> <h1 class="font-light text-white"><i class="mdi mdi-currency-usd"></i></h1>
<h6 class="text-white">Rate</h6> <h6 class="text-white">Salary</h6>
</div> </div>
</a> </a>
</div> </div>
@ -383,7 +383,7 @@
if (!this.stateUserList.length) { if (!this.stateUserList.length) {
this.fetchUsersState().then(() => { this.fetchUsersState().then(() => {
this.initiateStateTable(); this.initiateStateTable();
this.fetchApprovalFlows(); // Ensure approval flows are fetched on tab change this.fetchApprovalFlows();
}); });
} else { } else {
this.initiateStateTable(); this.initiateStateTable();
@ -408,8 +408,8 @@
if (response.ok) { if (response.ok) {
alert(successMessage); alert(successMessage);
clearCallback(); // Clears form selections clearCallback();
await fetchCallback(); // Fetches the updated data await fetchCallback();
} else { } else {
const errorData = await response.json(); const errorData = await response.json();
alert(errorData.message || "Failed to update. Please try again."); alert(errorData.message || "Failed to update. Please try again.");
@ -439,7 +439,6 @@
this.userList = data.filter(e => e.fullName !== "MAAdmin" && e.fullName !== "SysAdmin"); this.userList = data.filter(e => e.fullName !== "MAAdmin" && e.fullName !== "SysAdmin");
console.log("Fetched User data", this.userList); console.log("Fetched User data", this.userList);
// Reinitialize DataTable with updated data
this.initiateTable(); this.initiateTable();
} catch (error) { } catch (error) {
@ -464,7 +463,6 @@
this.stateUserList = data.filter(e => e.fullName !== "MAAdmin" && e.fullName !== "SysAdmin"); this.stateUserList = data.filter(e => e.fullName !== "MAAdmin" && e.fullName !== "SysAdmin");
console.log("Fetched state users", this.stateUserList); console.log("Fetched state users", this.stateUserList);
// Reinitialize the state table to reflect updated data
this.initiateStateTable(); this.initiateStateTable();
} catch (error) { } catch (error) {
@ -569,8 +567,8 @@
this.selectedUsersState, this.selectedUsersState,
this.selectedStateAll, this.selectedStateAll,
"State updated successfully!", "State updated successfully!",
() => {}, // Don't clear yet () => {},
() => {}, // Don't fetch yet () => {},
"StateId" "StateId"
); );
} }
@ -602,7 +600,6 @@
} }
} }
// After updates, refresh and clear
await this.fetchUsersState(); await this.fetchUsersState();
this.initiateStateTable(); this.initiateStateTable();
this.clearAllSelectionsStateFlow(); this.clearAllSelectionsStateFlow();
@ -643,8 +640,8 @@
try { try {
const response = await fetch('/OvertimeAPI/GetApprovalFlowList'); const response = await fetch('/OvertimeAPI/GetApprovalFlowList');
const data = await response.json(); const data = await response.json();
this.approvalFlowList = data; // Update the data correctly this.approvalFlowList = data;
console.log('Fetched approval flows:', this.approvalFlowList); // Verify data console.log('Fetched approval flows:', this.approvalFlowList);
} catch (error) { } catch (error) {
console.error('Error fetching approval flows:', error); console.error('Error fetching approval flows:', error);
} }
@ -690,7 +687,6 @@
async deleteApprovalFlow(approvalId) { async deleteApprovalFlow(approvalId) {
if (!approvalId) { if (!approvalId) {
// This handles the "undefined" ID case more gracefully
console.error("No approval ID provided for deletion."); console.error("No approval ID provided for deletion.");
alert("An error occurred: No approval flow selected for deletion."); alert("An error occurred: No approval flow selected for deletion.");
return; return;
@ -707,22 +703,17 @@
alert("Approval flow deleted successfully."); alert("Approval flow deleted successfully.");
await this.fetchApprovalFlows(); await this.fetchApprovalFlows();
} else { } else {
// Parse the error response from the server
const errorData = await response.json(); const errorData = await response.json();
const errorMessage = errorData.message || "Failed to delete approval flow."; const errorMessage = errorData.message || "Failed to delete approval flow.";
// Display the alert, but DON'T re-throw or console.error if it's a specific bad request if (response.status === 400) {
// Only log to console for unexpected server errors (e.g., 500 status codes)
if (response.status === 400) { // Check for Bad Request specifically
alert(`Error: ${errorMessage}`); alert(`Error: ${errorMessage}`);
} else { } else {
// For other errors (e.g., 500 Internal Server Error), still log to console
console.error("Error deleting flow:", errorMessage); console.error("Error deleting flow:", errorMessage);
alert(`Error: ${errorMessage}`); alert(`Error: ${errorMessage}`);
} }
} }
} catch (error) { } catch (error) {
// This catch block handles network errors or errors that prevent a valid response
console.error("An unexpected error occurred during deletion:", error); console.error("An unexpected error occurred during deletion:", error);
alert(`An unexpected error occurred: ${error.message || "Please try again."}`); alert(`An unexpected error occurred: ${error.message || "Please try again."}`);
} }
@ -749,13 +740,10 @@
} }
}, },
openEditModal(flow) { openEditModal(flow) {
// Set the editFlow to the selected flow
this.editFlow = { ...flow }; this.editFlow = { ...flow };
// Optionally, log the data to ensure it's correct
console.log(this.editFlow); console.log(this.editFlow);
// Show the modal
this.fetchAllUsers().then(() => { this.fetchAllUsers().then(() => {
const modal = new bootstrap.Modal(document.getElementById('editApprovalModal')); const modal = new bootstrap.Modal(document.getElementById('editApprovalModal'));
modal.show(); modal.show();

View File

@ -53,7 +53,7 @@
<div class="card-header bg-white text-center"> <div class="card-header bg-white text-center">
<h3 class="rate-heading">UPDATE SALARY</h3> <h3 class="rate-heading">UPDATE SALARY</h3>
</div> </div>
@* Enter Rate *@ @* Enter Salary *@
<div class="d-flex justify-content-center align-items-center"> <div class="d-flex justify-content-center align-items-center">
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<label for="rate" class="mb-0">Salary</label> <label for="rate" class="mb-0">Salary</label>

View File

@ -142,7 +142,7 @@
}, },
mounted() { mounted() {
this.fetchUpdateDates(); this.fetchUpdateDates();
this.checkIncompleteSettings(); // Call the new method on mount this.checkIncompleteSettings();
}, },
methods: { methods: {
async fetchUpdateDates() { async fetchUpdateDates() {
@ -172,7 +172,6 @@
const data = await response.json(); const data = await response.json();
if (data.hasIncompleteSettings) { if (data.hasIncompleteSettings) {
// Access the new property 'numberOfIncompleteUsers'
const numberOfStaff = data.numberOfIncompleteUsers; const numberOfStaff = data.numberOfIncompleteUsers;
let alertMessage = `Action Required!\n\nThere are ${numberOfStaff} staff with incomplete Rate / Flexi Hour / Approval Flow / State.`; let alertMessage = `Action Required!\n\nThere are ${numberOfStaff} staff with incomplete Rate / Flexi Hour / Approval Flow / State.`;
alert(alertMessage); alert(alertMessage);

View File

@ -170,8 +170,8 @@
return { label, value: totalMinutes }; return { label, value: totalMinutes };
}), }),
previousPage: document.referrer, previousPage: document.referrer,
returnMonth: null, // Add this returnMonth: null,
returnYear: null, // Add this returnYear: null,
validationErrors: { validationErrors: {
otDate: "", otDate: "",
stationId: "", stationId: "",
@ -192,7 +192,7 @@
async mounted() { async mounted() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const overtimeId = urlParams.get('overtimeId'); const overtimeId = urlParams.get('overtimeId');
// Capture month and year from URL parameters
this.returnMonth = urlParams.get('month'); this.returnMonth = urlParams.get('month');
this.returnYear = urlParams.get('year'); this.returnYear = urlParams.get('year');
@ -225,7 +225,7 @@
}, },
methods: { methods: {
initializeSelect2Dropdowns() { initializeSelect2Dropdowns() {
// Destroy any existing Select2 instances to prevent issues on re-initialization
if ($('#airstationDropdown').data('select2')) { if ($('#airstationDropdown').data('select2')) {
$('#airstationDropdown').select2('destroy'); $('#airstationDropdown').select2('destroy');
} }
@ -396,9 +396,9 @@
this.totalBreakHours = `${totalBreakHours} hr ${totalBreakMinutes} min`; this.totalBreakHours = `${totalBreakHours} hr ${totalBreakMinutes} min`;
this.updateDayType(); this.updateDayType();
}, },
// Update the updateTime method to include midnight validation
updateTime(fieldName) { updateTime(fieldName) {
// Round time first
if (fieldName === 'officeFrom') { if (fieldName === 'officeFrom') {
this.editForm.officeFrom = this.roundToNearest30(this.editForm.officeFrom); this.editForm.officeFrom = this.roundToNearest30(this.editForm.officeFrom);
} else if (fieldName === 'officeTo') { } else if (fieldName === 'officeTo') {
@ -409,12 +409,10 @@
this.editForm.afterTo = this.roundToNearest30(this.editForm.afterTo); this.editForm.afterTo = this.roundToNearest30(this.editForm.afterTo);
} }
// Validate time ranges
this.validateTimeRanges(); this.validateTimeRanges();
this.calculateOTAndBreak(); this.calculateOTAndBreak();
}, },
// Add new validation method
validateTimeRanges() { validateTimeRanges() {
const validateRange = (fromTime, toTime, label) => { const validateRange = (fromTime, toTime, label) => {
if (!fromTime || !toTime) return true; if (!fromTime || !toTime) return true;
@ -422,12 +420,12 @@
const start = this.parseTime(fromTime); const start = this.parseTime(fromTime);
const end = this.parseTime(toTime); const end = this.parseTime(toTime);
const minAllowedFromMinutes = 16 * 60 + 30; // 4:30 PM const minAllowedFromMinutes = 16 * 60 + 30;
const maxAllowedFromMinutes = 23 * 60 + 30; // 11:30 PM const maxAllowedFromMinutes = 23 * 60 + 30;
const startMinutes = start.hours * 60 + start.minutes; const startMinutes = start.hours * 60 + start.minutes;
const endMinutes = end.hours * 60 + end.minutes; const endMinutes = end.hours * 60 + end.minutes;
if (endMinutes === 0) { // If 'To' is 00:00 (midnight) if (endMinutes === 0) {
if (fromTime === "00:00") { if (fromTime === "00:00") {
alert(`Invalid ${label} Time: 'From' and 'To' cannot both be 00:00 (midnight).`); alert(`Invalid ${label} Time: 'From' and 'To' cannot both be 00:00 (midnight).`);
return false; return false;
@ -443,14 +441,13 @@
return true; return true;
}; };
// Validate office hours
if (this.editForm.officeFrom && this.editForm.officeTo) { if (this.editForm.officeFrom && this.editForm.officeTo) {
if (!validateRange(this.editForm.officeFrom, this.editForm.officeTo, 'Office Hour')) { if (!validateRange(this.editForm.officeFrom, this.editForm.officeTo, 'Office Hour')) {
this.editForm.officeTo = ''; this.editForm.officeTo = '';
} }
} }
// Validate after hours
if (this.editForm.afterFrom && this.editForm.afterTo) { if (this.editForm.afterFrom && this.editForm.afterTo) {
if (!validateRange(this.editForm.afterFrom, this.editForm.afterTo, 'After Office Hour')) { if (!validateRange(this.editForm.afterFrom, this.editForm.afterTo, 'After Office Hour')) {
this.editForm.afterTo = ''; this.editForm.afterTo = '';
@ -458,23 +455,20 @@
} }
}, },
// Update the roundToNearest30 method to match OtRegister
roundToNearest30(timeStr) { roundToNearest30(timeStr) {
if (!timeStr) return timeStr; if (!timeStr) return timeStr;
const [hours, minutes] = timeStr.split(':').map(Number); const [hours, minutes] = timeStr.split(':').map(Number);
const totalMinutes = hours * 60 + minutes; const totalMinutes = hours * 60 + minutes;
const remainder = totalMinutes % 30; const remainder = totalMinutes % 30;
// If closer to the lower 30-min mark, round down. Otherwise, round up.
const roundedMinutes = remainder < 15 ? totalMinutes - remainder : totalMinutes + (30 - remainder); const roundedMinutes = remainder < 15 ? totalMinutes - remainder : totalMinutes + (30 - remainder);
const adjustedHour = Math.floor(roundedMinutes / 60) % 24; // Ensure hours wrap around 24 const adjustedHour = Math.floor(roundedMinutes / 60) % 24;
const adjustedMinute = roundedMinutes % 60; const adjustedMinute = roundedMinutes % 60;
return `${adjustedHour.toString().padStart(2, '0')}:${adjustedMinute.toString().padStart(2, '0')}`; return `${adjustedHour.toString().padStart(2, '0')}:${adjustedMinute.toString().padStart(2, '0')}`;
}, },
// Update calculateTimeDifference to handle midnight case
calculateTimeDifference(startTime, endTime, breakMinutes) { calculateTimeDifference(startTime, endTime, breakMinutes) {
if (!startTime || !endTime) { if (!startTime || !endTime) {
return { hours: 0, minutes: 0 }; return { hours: 0, minutes: 0 };
@ -485,19 +479,19 @@
let diffMinutes; let diffMinutes;
// If TO is 00:00 (midnight), calculate the duration until end of day (24:00)
if (end.hours === 0 && end.minutes === 0 && (start.hours > 0 || start.minutes > 0)) { if (end.hours === 0 && end.minutes === 0 && (start.hours > 0 || start.minutes > 0)) {
diffMinutes = (24 * 60) - (start.hours * 60 + start.minutes); diffMinutes = (24 * 60) - (start.hours * 60 + start.minutes);
} else if (end.hours * 60 + end.minutes <= start.hours * 60 + start.minutes) { } else if (end.hours * 60 + end.minutes <= start.hours * 60 + start.minutes) {
// For all other cases where 'To' time is on or before 'From' time, it's invalid.
return { hours: 0, minutes: 0 }; return { hours: 0, minutes: 0 };
} else { } else {
// Standard calculation for times within the same 24-hour period on the same day.
diffMinutes = (end.hours * 60 + end.minutes) - (start.hours * 60 + start.minutes); diffMinutes = (end.hours * 60 + end.minutes) - (start.hours * 60 + start.minutes);
} }
diffMinutes -= breakMinutes || 0; diffMinutes -= breakMinutes || 0;
if (diffMinutes < 0) diffMinutes = 0; // Ensure total hours don't go negative if break is too long if (diffMinutes < 0) diffMinutes = 0;
const hours = Math.floor(diffMinutes / 60); const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60; const minutes = diffMinutes % 60;
@ -558,7 +552,7 @@
}, },
validateForm() { validateForm() {
// Reset validation errors
this.validationErrors = { this.validationErrors = {
otDate: "", otDate: "",
stationId: "", stationId: "",
@ -569,7 +563,7 @@
let isValid = true; let isValid = true;
let errorMessages = []; let errorMessages = [];
// Validate date
if (!this.editForm.otDate) { if (!this.editForm.otDate) {
this.validationErrors.otDate = "Date is required."; this.validationErrors.otDate = "Date is required.";
errorMessages.push("Date is required."); errorMessages.push("Date is required.");
@ -648,7 +642,6 @@
} }
} }
// Display alert if not valid
if (!isValid) { if (!isValid) {
alert("Please correct the following issues:\n\n" + errorMessages.join("\n")); alert("Please correct the following issues:\n\n" + errorMessages.join("\n"));
} }
@ -709,11 +702,9 @@
}, },
goBack() { goBack() {
// If we have stored month and year, use them to return to the specific view
if (this.returnMonth && this.returnYear) { if (this.returnMonth && this.returnYear) {
window.location.href = `/OTcalculate/Overtime/OtRecords?month=${this.returnMonth}&year=${this.returnYear}`; window.location.href = `/OTcalculate/Overtime/OtRecords?month=${this.returnMonth}&year=${this.returnYear}`;
} else { } else {
// Fallback to previous page if month/year are not available
window.location.href = this.previousPage; window.location.href = this.previousPage;
} }
}, },

View File

@ -212,8 +212,8 @@
otRecords: [], otRecords: [],
userId: null, userId: null,
isPSTWAIR: false, isPSTWAIR: false,
selectedMonth: initialMonth, // Use initialMonth selectedMonth: initialMonth,
selectedYear: initialYear, // Use initialYear selectedYear: initialYear,
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
years: Array.from({ length: 10 }, (_, i) => currentYear - 5 + i), years: Array.from({ length: 10 }, (_, i) => currentYear - 5 + i),
expandedDescriptions: {}, expandedDescriptions: {},
@ -238,7 +238,6 @@
.sort((a, b) => new Date(a.otDate) - new Date(b.otDate)); .sort((a, b) => new Date(a.otDate) - new Date(b.otDate));
}, },
noRecordsFound() { noRecordsFound() {
// This new computed property checks if filteredRecords is empty
return this.filteredRecords.length === 0; return this.filteredRecords.length === 0;
}, },
totalHours() { totalHours() {
@ -291,7 +290,6 @@
const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin"); const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin");
const isSystemAdmin = this.currentUser?.role?.includes("SystemAdmin"); const isSystemAdmin = this.currentUser?.role?.includes("SystemAdmin");
const isDepartmentTwo = this.currentUser?.department?.departmentId === 2; const isDepartmentTwo = this.currentUser?.department?.departmentId === 2;
// ADD THIS LINE TO INCLUDE DEPARTMENT ID 3 (PSTWMARINE)
const isDepartmentThree = this.currentUser?.department?.departmentId === 3; const isDepartmentThree = this.currentUser?.department?.departmentId === 3;
this.isPSTWAIR = isSuperAdmin || isSystemAdmin || isDepartmentTwo || isDepartmentThree; this.isPSTWAIR = isSuperAdmin || isSystemAdmin || isDepartmentTwo || isDepartmentThree;
@ -320,14 +318,14 @@
const errorText = await res.text(); const errorText = await res.text();
console.error("Failed to check submission status:", errorText); console.error("Failed to check submission status:", errorText);
alert("Error checking submission status. Please try again."); alert("Error checking submission status. Please try again.");
this.hasSubmitted = false; // Or handle as appropriate this.hasSubmitted = false;
return; return;
} }
this.hasSubmitted = await res.json(); this.hasSubmitted = await res.json();
} catch (err) { } catch (err) {
console.error("Failed to parse submission status:", err); console.error("Failed to parse submission status:", err);
alert("An unexpected error occurred while checking submission status."); alert("An unexpected error occurred while checking submission status.");
this.hasSubmitted = false; // Or handle as appropriate this.hasSubmitted = false;
} }
}, },
toggleDescription(index) { toggleDescription(index) {
@ -357,13 +355,11 @@
let totalMinutes = (th * 60 + tm) - (fh * 60 + fm); let totalMinutes = (th * 60 + tm) - (fh * 60 + fm);
// If the 'to' time is earlier than the 'from' time, it means it's an overnight span.
// Add 24 hours (1440 minutes) to account for the next day.
if (totalMinutes < 0) { if (totalMinutes < 0) {
totalMinutes += 24 * 60; // Add 24 hours in minutes totalMinutes += 24 * 60;
} }
return totalMinutes / 60; // Return total hours as a decimal return totalMinutes / 60;
}, },
calcTotalTime(r) { calcTotalTime(r) {
const totalMinutes = this.calcTotalHours(r) * 60; const totalMinutes = this.calcTotalHours(r) * 60;
@ -393,7 +389,6 @@
}, },
editRecord(index) { editRecord(index) {
const record = this.filteredRecords[index]; const record = this.filteredRecords[index];
// Pass current month and year along with overtimeId
window.location.href = `/OTcalculate/Overtime/EditOvertime?overtimeId=${record.overtimeId}&month=${this.selectedMonth}&year=${this.selectedYear}`; window.location.href = `/OTcalculate/Overtime/EditOvertime?overtimeId=${record.overtimeId}&month=${this.selectedMonth}&year=${this.selectedYear}`;
}, },
async deleteRecord(index) { async deleteRecord(index) {
@ -443,7 +438,6 @@
} }
}, },
downloadExcel(month, year) { downloadExcel(month, year) {
// Use the new API endpoint
window.open(`/OvertimeAPI/GenerateUserOvertimeExcel/${this.userId}/${month}/${year}`, '_blank'); window.open(`/OvertimeAPI/GenerateUserOvertimeExcel/${this.userId}/${month}/${year}`, '_blank');
}, },
openSubmitModal() { openSubmitModal() {
@ -478,7 +472,7 @@
const modalInstance = bootstrap.Modal.getInstance(modalEl); const modalInstance = bootstrap.Modal.getInstance(modalEl);
modalInstance.hide(); modalInstance.hide();
await this.getSubmissionStatus(); // Call this to refresh the hasSubmitted status await this.getSubmissionStatus();
} else { } else {
alert('Submission failed.'); alert('Submission failed.');

View File

@ -148,7 +148,6 @@
afterFrom: "", afterFrom: "",
afterTo: "", afterTo: "",
afterBreak: 0, afterBreak: 0,
// New properties for separate station selections
selectedAirStation: "", // Holds selected Air station ID selectedAirStation: "", // Holds selected Air station ID
selectedMarineStation: "", // Holds selected Marine station ID selectedMarineStation: "", // Holds selected Marine station ID
airStationList: [], // Stores stations for Air Department (DepartmentId = 2) airStationList: [], // Stores stations for Air Department (DepartmentId = 2)
@ -163,10 +162,9 @@
userId: null, userId: null,
userState: null, userState: null,
publicHolidays: [], publicHolidays: [],
// Keep user's actual department ID and admin flag for conditional rendering
userDepartmentId: null, // The department ID from the current user's profile userDepartmentId: null, // The department ID from the current user's profile
isUserAdmin: false, // True if the user is a SuperAdmin or SystemAdmin isUserAdmin: false,
departmentName: "", // This will be dynamic based on the user's main department for hints departmentName: "",
areUserSettingsComplete: false, areUserSettingsComplete: false,
breakOptions: Array.from({ length: 15 }, (_, i) => { breakOptions: Array.from({ length: 15 }, (_, i) => {
const totalMinutes = i * 30; const totalMinutes = i * 30;
@ -186,17 +184,12 @@
charCount() { charCount() {
return this.otDescription.length; return this.otDescription.length;
}, },
// Determines if the Air Station dropdown should be shown
showAirDropdown() { showAirDropdown() {
return this.isUserAdmin || this.userDepartmentId === 2; return this.isUserAdmin || this.userDepartmentId === 2;
}, },
// Determines if the Marine Station dropdown should be shown
showMarineDropdown() { showMarineDropdown() {
return this.isUserAdmin || this.userDepartmentId === 3; return this.isUserAdmin || this.userDepartmentId === 3;
}, },
// This computed property selects the station ID to be sent to the backend.
// It assumes that if both are visible (for admins), only ONE should be selected.
// If both are selected by an admin, validation in addOvertime will catch it.
stationIdForSubmission() { stationIdForSubmission() {
if (this.selectedAirStation) { if (this.selectedAirStation) {
return parseInt(this.selectedAirStation); return parseInt(this.selectedAirStation);
@ -204,15 +197,13 @@
if (this.selectedMarineStation) { if (this.selectedMarineStation) {
return parseInt(this.selectedMarineStation); return parseInt(this.selectedMarineStation);
} }
return null; // No station selected from either dropdown return null;
}, },
// This indicates if *any* station selection is required based on visible dropdowns
requiresStation() { requiresStation() {
return this.showAirDropdown || this.showMarineDropdown; return this.showAirDropdown || this.showMarineDropdown;
} }
}, },
watch: { watch: {
// Watch for changes in airStationList to re-initialize Select2 for Air
airStationList: { airStationList: {
handler() { handler() {
this.$nextTick(() => { this.$nextTick(() => {
@ -222,11 +213,9 @@
selectElement.select2('destroy'); selectElement.select2('destroy');
} }
selectElement.select2({ theme: 'bootstrap4', placeholder: 'Select Air Station', allowClear: true }); selectElement.select2({ theme: 'bootstrap4', placeholder: 'Select Air Station', allowClear: true });
// Set initial value if already selected
if (this.selectedAirStation) { if (this.selectedAirStation) {
selectElement.val(this.selectedAirStation).trigger('change'); selectElement.val(this.selectedAirStation).trigger('change');
} }
// Ensure event listener is set only once
selectElement.off('change.v-model-air').on('change.v-model-air', (event) => { selectElement.off('change.v-model-air').on('change.v-model-air', (event) => {
this.selectedAirStation = $(event.currentTarget).val(); this.selectedAirStation = $(event.currentTarget).val();
}); });
@ -235,7 +224,6 @@
}, },
deep: true deep: true
}, },
// Watch for changes in marineStationList to re-initialize Select2 for Marine
marineStationList: { marineStationList: {
handler() { handler() {
this.$nextTick(() => { this.$nextTick(() => {
@ -245,11 +233,9 @@
selectElement.select2('destroy'); selectElement.select2('destroy');
} }
selectElement.select2({ theme: 'bootstrap4', placeholder: 'Select Marine Station', allowClear: true }); selectElement.select2({ theme: 'bootstrap4', placeholder: 'Select Marine Station', allowClear: true });
// Set initial value if already selected
if (this.selectedMarineStation) { if (this.selectedMarineStation) {
selectElement.val(this.selectedMarineStation).trigger('change'); selectElement.val(this.selectedMarineStation).trigger('change');
} }
// Ensure event listener is set only once
selectElement.off('change.v-model-marine').on('change.v-model-marine', (event) => { selectElement.off('change.v-model-marine').on('change.v-model-marine', (event) => {
this.selectedMarineStation = $(event.currentTarget).val(); this.selectedMarineStation = $(event.currentTarget).val();
}); });
@ -258,14 +244,12 @@
}, },
deep: true deep: true
}, },
// Keep selectedAirStation in sync with Select2 if it's already rendered
selectedAirStation(newVal) { selectedAirStation(newVal) {
const selectElement = $('#airStationDropdown'); const selectElement = $('#airStationDropdown');
if (selectElement.length && selectElement.val() !== newVal) { if (selectElement.length && selectElement.val() !== newVal) {
selectElement.val(newVal).trigger('change.select2'); selectElement.val(newVal).trigger('change.select2');
} }
}, },
// Keep selectedMarineStation in sync with Select2 if it's already rendered
selectedMarineStation(newVal) { selectedMarineStation(newVal) {
const selectElement = $('#marineStationDropdown'); const selectElement = $('#marineStationDropdown');
if (selectElement.length && selectElement.val() !== newVal) { if (selectElement.length && selectElement.val() !== newVal) {
@ -278,18 +262,14 @@
if (this.userId) { if (this.userId) {
await this.checkUserSettings(); await this.checkUserSettings();
} }
// Fetch stations for Air if the dropdown will be visible
if (this.showAirDropdown) { if (this.showAirDropdown) {
await this.fetchStations(2, 'air'); // Department ID 2 for Air await this.fetchStations(2, 'air'); // Department ID 2 for Air
} }
// Fetch stations for Marine if the dropdown will be visible
if (this.showMarineDropdown) { if (this.showMarineDropdown) {
await this.fetchStations(3, 'marine'); // Department ID 3 for Marine await this.fetchStations(3, 'marine'); // Department ID 3 for Marine
} }
}, },
methods: { methods: {
// Modified fetchStations to populate specific lists based on listType
async fetchStations(departmentId, listType) { async fetchStations(departmentId, listType) {
try { try {
const response = await fetch(`/OvertimeAPI/GetStationsByDepartment?departmentId=${departmentId}`); const response = await fetch(`/OvertimeAPI/GetStationsByDepartment?departmentId=${departmentId}`);
@ -319,11 +299,10 @@
const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin"); const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin");
const isSystemAdmin = this.currentUser?.role?.includes("SystemAdmin"); const isSystemAdmin = this.currentUser?.role?.includes("SystemAdmin");
this.userDepartmentId = this.currentUser?.department?.departmentId; // Store user's actual department ID this.userDepartmentId = this.currentUser?.department?.departmentId;
this.isUserAdmin = isSuperAdmin || isSystemAdmin; // Set the admin flag this.isUserAdmin = isSuperAdmin || isSystemAdmin;
// Set departmentName for the generic "Only for {{ departmentName }}" hint if not admin
if (!this.isUserAdmin) { if (!this.isUserAdmin) {
if (this.userDepartmentId === 2) { if (this.userDepartmentId === 2) {
this.departmentName = "PSTW AIR"; this.departmentName = "PSTW AIR";
@ -333,7 +312,7 @@
this.departmentName = ""; this.departmentName = "";
} }
} else { } else {
this.departmentName = ""; // Admins see both, so this specific hint is removed for them this.departmentName = "";
} }
console.log("Fetched User:", this.currentUser); console.log("Fetched User:", this.currentUser);
@ -394,10 +373,9 @@
const totalMinutes = hours * 60 + minutes; const totalMinutes = hours * 60 + minutes;
const remainder = totalMinutes % 30; const remainder = totalMinutes % 30;
// If closer to the lower 30-min mark, round down. Otherwise, round up.
const roundedMinutes = remainder < 15 ? totalMinutes - remainder : totalMinutes + (30 - remainder); const roundedMinutes = remainder < 15 ? totalMinutes - remainder : totalMinutes + (30 - remainder);
const adjustedHour = Math.floor(roundedMinutes / 60) % 24; // Ensure hours wrap around 24 const adjustedHour = Math.floor(roundedMinutes / 60) % 24;
const adjustedMinute = roundedMinutes % 60; const adjustedMinute = roundedMinutes % 60;
return `${adjustedHour.toString().padStart(2, '0')}:${adjustedMinute.toString().padStart(2, '0')}`; return `${adjustedHour.toString().padStart(2, '0')}:${adjustedMinute.toString().padStart(2, '0')}`;
@ -446,7 +424,7 @@
} }
diffMinutes -= breakMinutes || 0; diffMinutes -= breakMinutes || 0;
if (diffMinutes < 0) diffMinutes = 0; // Ensure total hours don't go negative if break is too long if (diffMinutes < 0) diffMinutes = 0;
const hours = Math.floor(diffMinutes / 60); const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60; const minutes = diffMinutes % 60;
@ -472,7 +450,6 @@
return; return;
} }
// --- Frontend Validation ---
if (!this.selectedDate) { if (!this.selectedDate) {
alert("Please select a date for the overtime."); alert("Please select a date for the overtime.");
return; return;
@ -510,7 +487,7 @@
// Validate time ranges according to the new rule: TO 00:00 only if FROM is 4:30 PM - 11:30 PM // Validate time ranges according to the new rule: TO 00:00 only if FROM is 4:30 PM - 11:30 PM
const validateTimeRangeForSubmission = (fromTime, toTime, label) => { const validateTimeRangeForSubmission = (fromTime, toTime, label) => {
if (!fromTime || !toTime) return true; // Handled by outer checks if (!fromTime || !toTime) return true;
const start = this.parseTime(fromTime); const start = this.parseTime(fromTime);
const end = this.parseTime(toTime); const end = this.parseTime(toTime);
@ -542,8 +519,6 @@
if (hasAfterHours && !validateTimeRangeForSubmission(this.afterFrom, this.afterTo, 'After Office Hour')) { if (hasAfterHours && !validateTimeRangeForSubmission(this.afterFrom, this.afterTo, 'After Office Hour')) {
return; return;
} }
// --- End Frontend Validation ---
const requestData = { const requestData = {
otDate: this.selectedDate, otDate: this.selectedDate,
@ -553,7 +528,7 @@
afterFrom: this.afterFrom ? this.formatTime(this.afterFrom) : null, afterFrom: this.afterFrom ? this.formatTime(this.afterFrom) : null,
afterTo: this.afterTo ? this.formatTime(this.afterTo) : null, afterTo: this.afterTo ? this.formatTime(this.afterTo) : null,
afterBreak: this.afterBreak || null, afterBreak: this.afterBreak || null,
stationId: stationIdToSubmit, // Use the selected station from either dropdown stationId: stationIdToSubmit,
otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '), otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '),
otDays: this.detectedDayType, otDays: this.detectedDayType,
userId: this.userId, userId: this.userId,
@ -635,10 +610,9 @@
this.afterFrom = ""; this.afterFrom = "";
this.afterTo = ""; this.afterTo = "";
this.afterBreak = 0; this.afterBreak = 0;
this.selectedAirStation = ""; // Clear specific station selections this.selectedAirStation = "";
this.selectedMarineStation = ""; // Clear specific station selections this.selectedMarineStation = "";
// Clear Select2 for both dropdowns if they exist
const airSelect = $('#airStationDropdown'); const airSelect = $('#airStationDropdown');
if (airSelect.length && airSelect.data('select2')) { if (airSelect.length && airSelect.data('select2')) {
airSelect.val('').trigger('change.select2'); airSelect.val('').trigger('change.select2');

View File

@ -112,6 +112,7 @@
<div id="app" style="max-width: 1300px; margin: auto; font-size: 13px;"> <div id="app" style="max-width: 1300px; margin: auto; font-size: 13px;">
<div class="table-layer"> <div class="table-layer">
<div class="white-box"> <div class="white-box">
<!-- Simplified Filter Section --> <!-- Simplified Filter Section -->
<div class="filter-container"> <div class="filter-container">
<div class="row mb-3"> <div class="row mb-3">
@ -344,12 +345,12 @@
computed: { computed: {
columnCount() { columnCount() {
let count = 3; // Month/Year, SubmitDate, Edit History let count = 3;
if (this.includeHou) count++; if (this.includeHou) count++;
if (this.includeHod) count++; if (this.includeHod) count++;
if (this.includeManager) count++; if (this.includeManager) count++;
if (this.includeHr) count++; if (this.includeHr) count++;
return count + 1; // File column return count + 1;
}, },
monthYearOptions() { monthYearOptions() {
@ -374,7 +375,6 @@
{ value: 'Rejected', text: 'Rejected' } { value: 'Rejected', text: 'Rejected' }
]; ];
// Add combined status options if multiple approval levels exist
if ((this.includeHou && this.includeHod) || if ((this.includeHou && this.includeHod) ||
(this.includeHou && this.includeManager) || (this.includeHou && this.includeManager) ||
(this.includeHod && this.includeManager)) { (this.includeHod && this.includeManager)) {
@ -390,7 +390,6 @@
filteredRecords() { filteredRecords() {
let filtered = [...this.otRecords]; let filtered = [...this.otRecords];
// Apply filters
if (this.filters.monthYear) { if (this.filters.monthYear) {
const [month, year] = this.filters.monthYear.split('/').map(Number); const [month, year] = this.filters.monthYear.split('/').map(Number);
filtered = filtered.filter(item => filtered = filtered.filter(item =>
@ -434,7 +433,6 @@
} }
} }
// Apply sorting
filtered.sort((a, b) => { filtered.sort((a, b) => {
let aValue, bValue; let aValue, bValue;
@ -472,7 +470,6 @@
return 0; return 0;
}); });
// Update pagination
this.pagination.totalPages = Math.ceil(filtered.length / this.pagination.itemsPerPage); this.pagination.totalPages = Math.ceil(filtered.length / this.pagination.itemsPerPage);
this.pagination.currentPage = Math.min(this.pagination.currentPage, this.pagination.totalPages || 1); this.pagination.currentPage = Math.min(this.pagination.currentPage, this.pagination.totalPages || 1);
@ -727,7 +724,6 @@
this.parsedHistory = []; this.parsedHistory = [];
}, },
// Sorting methods
sortBy(field) { sortBy(field) {
if (this.sort.field === field) { if (this.sort.field === field) {
this.sort.order = this.sort.order === 'asc' ? 'desc' : 'asc'; this.sort.order = this.sort.order === 'asc' ? 'desc' : 'asc';
@ -735,10 +731,9 @@
this.sort.field = field; this.sort.field = field;
this.sort.order = 'asc'; this.sort.order = 'asc';
} }
this.pagination.currentPage = 1; // Reset to first page when sorting changes this.pagination.currentPage = 1;
}, },
// Filter methods
resetFilters() { resetFilters() {
this.filters = { this.filters = {
monthYear: '', monthYear: '',
@ -747,7 +742,6 @@
this.pagination.currentPage = 1; this.pagination.currentPage = 1;
}, },
// Pagination methods
prevPage() { prevPage() {
if (this.pagination.currentPage > 1) { if (this.pagination.currentPage > 1) {
this.pagination.currentPage--; this.pagination.currentPage--;

View File

@ -85,29 +85,24 @@ namespace PSTW_CentralSystem.Controllers.API
{ {
try try
{ {
// Use a list to hold IDs of users with incomplete settings
var incompleteUserIds = new List<int>(); var incompleteUserIds = new List<int>();
// Get all user IDs
var allUserIds = await _userManager.Users.Select(u => u.Id).ToListAsync(); var allUserIds = await _userManager.Users.Select(u => u.Id).ToListAsync();
foreach (var userId in allUserIds) foreach (var userId in allUserIds)
{ {
bool isIncomplete = false; bool isIncomplete = false;
// Check HrUserSettingModel
var hrUserSetting = await _centralDbContext.Hrusersetting var hrUserSetting = await _centralDbContext.Hrusersetting
.Where(h => h.UserId == userId) .Where(h => h.UserId == userId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (hrUserSetting == null) if (hrUserSetting == null)
{ {
// No HR user setting found for this user
isIncomplete = true; isIncomplete = true;
} }
else else
{ {
// Check for null/empty fields in HrUserSettingModel
if (hrUserSetting.FlexiHourId == null || hrUserSetting.FlexiHourId == 0) if (hrUserSetting.FlexiHourId == null || hrUserSetting.FlexiHourId == 0)
{ {
isIncomplete = true; isIncomplete = true;
@ -122,19 +117,16 @@ namespace PSTW_CentralSystem.Controllers.API
} }
} }
// Check RateModel
var rateSetting = await _centralDbContext.Rates var rateSetting = await _centralDbContext.Rates
.Where(r => r.UserId == userId) .Where(r => r.UserId == userId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (rateSetting == null) if (rateSetting == null)
{ {
// No Rate setting found for this user
isIncomplete = true; isIncomplete = true;
} }
else else
{ {
// Check for default/empty RateValue (assuming 0.00 is considered empty)
if (rateSetting.RateValue == 0.00m) if (rateSetting.RateValue == 0.00m)
{ {
isIncomplete = true; isIncomplete = true;
@ -143,13 +135,12 @@ namespace PSTW_CentralSystem.Controllers.API
if (isIncomplete) if (isIncomplete)
{ {
incompleteUserIds.Add(userId); // Add only the ID incompleteUserIds.Add(userId);
} }
} }
if (incompleteUserIds.Any()) if (incompleteUserIds.Any())
{ {
// Return just the count of incomplete users
return Ok(new { hasIncompleteSettings = true, numberOfIncompleteUsers = incompleteUserIds.Count }); return Ok(new { hasIncompleteSettings = true, numberOfIncompleteUsers = incompleteUserIds.Count });
} }
else else
@ -330,8 +321,7 @@ namespace PSTW_CentralSystem.Controllers.API
.FirstOrDefault() ?? "N/A" .FirstOrDefault() ?? "N/A"
}).ToList(); }).ToList();
// Log this data to inspect the response Console.WriteLine(JsonConvert.SerializeObject(result));
Console.WriteLine(JsonConvert.SerializeObject(result)); // Debugging log
return Ok(result); return Ok(result);
@ -418,7 +408,7 @@ namespace PSTW_CentralSystem.Controllers.API
if (existingSetting != null) if (existingSetting != null)
{ {
existingSetting.StateId = update.StateId; existingSetting.StateId = update.StateId;
existingSetting.StateUpdate = DateTime.Now; // <-- ADD THIS LINE existingSetting.StateUpdate = DateTime.Now;
} }
else else
{ {
@ -426,8 +416,7 @@ namespace PSTW_CentralSystem.Controllers.API
{ {
UserId = update.UserId, UserId = update.UserId,
StateId = update.StateId, StateId = update.StateId,
StateUpdate = DateTime.Now, // <-- ADD THIS LINE for new records StateUpdate = DateTime.Now,
// Consider setting default for FlexiHourId and ApprovalFlowId if they are not nullable or have defaults
}); });
} }
} }
@ -580,7 +569,6 @@ namespace PSTW_CentralSystem.Controllers.API
return NotFound("Approval flow not found."); return NotFound("Approval flow not found.");
} }
// Update fields
existingFlow.ApprovalName = model.ApprovalName; existingFlow.ApprovalName = model.ApprovalName;
existingFlow.HoU = model.HoU; existingFlow.HoU = model.HoU;
existingFlow.HoD = model.HoD; existingFlow.HoD = model.HoD;
@ -594,7 +582,6 @@ namespace PSTW_CentralSystem.Controllers.API
} }
catch (Exception ex) catch (Exception ex)
{ {
// Log the exception
return StatusCode(500, new { message = "Failed to update approval flow.", detail = ex.Message }); return StatusCode(500, new { message = "Failed to update approval flow.", detail = ex.Message });
} }
} }
@ -610,7 +597,6 @@ namespace PSTW_CentralSystem.Controllers.API
return NotFound(new { message = "Approval flow not found." }); return NotFound(new { message = "Approval flow not found." });
} }
// Check if any users are using this approval flow
var usersWithThisFlow = await _centralDbContext.Hrusersetting var usersWithThisFlow = await _centralDbContext.Hrusersetting
.AnyAsync(u => u.ApprovalFlowId == id); .AnyAsync(u => u.ApprovalFlowId == id);
@ -802,13 +788,11 @@ namespace PSTW_CentralSystem.Controllers.API
if (existingState != null) if (existingState != null)
{ {
// Corrected: Updating WeekendId
existingState.WeekendId = state.WeekendId; existingState.WeekendId = state.WeekendId;
_centralDbContext.States.Update(existingState); _centralDbContext.States.Update(existingState);
} }
else else
{ {
// Ensure new states are added correctly
_centralDbContext.States.Add(new StateModel _centralDbContext.States.Add(new StateModel
{ {
StateId = state.StateId, StateId = state.StateId,
@ -874,15 +858,13 @@ namespace PSTW_CentralSystem.Controllers.API
#endregion #endregion
#region OtRegister #region OtRegister
// Modified to accept departmentId as a query parameter
[HttpGet("GetStationsByDepartment")] [HttpGet("GetStationsByDepartment")]
public async Task<IActionResult> GetStationsByDepartment([FromQuery] int? departmentId) public async Task<IActionResult> GetStationsByDepartment([FromQuery] int? departmentId)
{ {
if (!departmentId.HasValue) if (!departmentId.HasValue)
{ {
_logger.LogWarning("GetStationsByDepartment called without a departmentId."); _logger.LogWarning("GetStationsByDepartment called without a departmentId.");
return Ok(new List<object>()); // Return empty list if no department is specified return Ok(new List<object>());
} }
var stations = await _centralDbContext.Stations var stations = await _centralDbContext.Stations
@ -918,7 +900,6 @@ namespace PSTW_CentralSystem.Controllers.API
return BadRequest("User ID is required."); return BadRequest("User ID is required.");
} }
// **Backend Validation for StationId based on user roles and department**
var user = await _userManager.FindByIdAsync(request.UserId.ToString()); var user = await _userManager.FindByIdAsync(request.UserId.ToString());
if (user == null) if (user == null)
{ {
@ -930,22 +911,20 @@ namespace PSTW_CentralSystem.Controllers.API
var isSuperAdmin = userRoles.Contains("SuperAdmin"); var isSuperAdmin = userRoles.Contains("SuperAdmin");
var isSystemAdmin = userRoles.Contains("SystemAdmin"); var isSystemAdmin = userRoles.Contains("SystemAdmin");
// Get user's department info from the database (assuming it's eager loaded or can be fetched)
var userWithDepartment = await _centralDbContext.Users var userWithDepartment = await _centralDbContext.Users
.Include(u => u.Department) .Include(u => u.Department)
.FirstOrDefaultAsync(u => u.Id == request.UserId); .FirstOrDefaultAsync(u => u.Id == request.UserId);
int? userDepartmentId = userWithDepartment?.Department?.DepartmentId; int? userDepartmentId = userWithDepartment?.Department?.DepartmentId;
// Determine if a station is required and validate it
bool stationRequired = false; bool stationRequired = false;
if (userDepartmentId == 2 || userDepartmentId == 3) // For regular Air/Marine users if (userDepartmentId == 2 || userDepartmentId == 3)
{ {
stationRequired = true; stationRequired = true;
} }
else if (isSuperAdmin || isSystemAdmin) // For Admins, they see both, and must select one else if (isSuperAdmin || isSystemAdmin)
{ {
stationRequired = true; // Admins also must select a station if they submit overtime stationRequired = true;
} }
if (stationRequired && (!request.StationId.HasValue || request.StationId.Value <= 0)) if (stationRequired && (!request.StationId.HasValue || request.StationId.Value <= 0))
@ -958,7 +937,6 @@ namespace PSTW_CentralSystem.Controllers.API
TimeSpan? afterFrom = string.IsNullOrEmpty(request.AfterFrom) ? (TimeSpan?)null : TimeSpan.Parse(request.AfterFrom); TimeSpan? afterFrom = string.IsNullOrEmpty(request.AfterFrom) ? (TimeSpan?)null : TimeSpan.Parse(request.AfterFrom);
TimeSpan? afterTo = string.IsNullOrEmpty(request.AfterTo) ? (TimeSpan?)null : TimeSpan.Parse(request.AfterTo); TimeSpan? afterTo = string.IsNullOrEmpty(request.AfterTo) ? (TimeSpan?)null : TimeSpan.Parse(request.AfterTo);
// Validation for time ranges (consolidated)
if ((officeFrom != null && officeTo == null) || (officeFrom == null && officeTo != null)) if ((officeFrom != null && officeTo == null) || (officeFrom == null && officeTo != null))
{ {
return BadRequest("Both Office From and To times must be provided if one is entered."); return BadRequest("Both Office From and To times must be provided if one is entered.");
@ -968,47 +946,44 @@ namespace PSTW_CentralSystem.Controllers.API
return BadRequest("Both After Office From and To times must be provided if one is entered."); return BadRequest("Both After Office From and To times must be provided if one is entered.");
} }
// Define allowed range for FROM when TO is 00:00 (midnight) TimeSpan minAllowedFromMidnightTo = new TimeSpan(16, 30, 0);
TimeSpan minAllowedFromMidnightTo = new TimeSpan(16, 30, 0); // 4:30 PM TimeSpan maxAllowedFromMidnightTo = new TimeSpan(23, 30, 0);
TimeSpan maxAllowedFromMidnightTo = new TimeSpan(23, 30, 0); // 11:30 PM
// Backend Validation for Office Hours
if (officeFrom.HasValue && officeTo.HasValue) if (officeFrom.HasValue && officeTo.HasValue)
{ {
if (officeTo == TimeSpan.Zero) // If OfficeTo is exactly midnight (00:00:00) if (officeTo == TimeSpan.Zero)
{ {
if (officeFrom == TimeSpan.Zero) if (officeFrom == TimeSpan.Zero)
{ {
return BadRequest("Invalid Office Hour Time: 'From' and 'To' cannot both be 00:00 (midnight)."); return BadRequest("Invalid Office Hour Time: 'From' and 'To' cannot both be 00:00 (midnight).");
} }
// Check if OfficeFrom is within the specified range (4:30 PM to 11:30 PM)
if (officeFrom.Value < minAllowedFromMidnightTo || officeFrom.Value > maxAllowedFromMidnightTo) if (officeFrom.Value < minAllowedFromMidnightTo || officeFrom.Value > maxAllowedFromMidnightTo)
{ {
return BadRequest("Invalid Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between 4:30 pm and 11:30 pm on the same day to be saved."); return BadRequest("Invalid Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between 4:30 pm and 11:30 pm on the same day to be saved.");
} }
} }
else if (officeTo <= officeFrom) // For all other cases, "To" must be strictly greater than "From" else if (officeTo <= officeFrom)
{ {
return BadRequest("Invalid Office Hour Time: 'To' time must be later than 'From' time (same day only)."); return BadRequest("Invalid Office Hour Time: 'To' time must be later than 'From' time (same day only).");
} }
} }
// Backend Validation for After Office Hours
if (afterFrom.HasValue && afterTo.HasValue) if (afterFrom.HasValue && afterTo.HasValue)
{ {
if (afterTo == TimeSpan.Zero) // If AfterTo is exactly midnight (00:00:00) if (afterTo == TimeSpan.Zero)
{ {
if (afterFrom == TimeSpan.Zero) if (afterFrom == TimeSpan.Zero)
{ {
return BadRequest("Invalid After Office Hour Time: 'From' and 'To' cannot both be 00:00 (midnight)."); return BadRequest("Invalid After Office Hour Time: 'From' and 'To' cannot both be 00:00 (midnight).");
} }
// Check if AfterFrom is within the specified range (4:30 PM to 11:30 PM)
if (afterFrom.Value < minAllowedFromMidnightTo || afterFrom.Value > maxAllowedFromMidnightTo) if (afterFrom.Value < minAllowedFromMidnightTo || afterFrom.Value > maxAllowedFromMidnightTo)
{ {
return BadRequest("Invalid After Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between 4:30 pm and 11:30 pm on the same day to be saved."); return BadRequest("Invalid After Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between 4:30 pm and 11:30 pm on the same day to be saved.");
} }
} }
else if (afterTo <= afterFrom) // For all other cases, "To" must be strictly greater than "From" else if (afterTo <= afterFrom)
{ {
return BadRequest("Invalid After Office Hour Time: 'To' time must be later than 'From' time (same day only)."); return BadRequest("Invalid After Office Hour Time: 'To' time must be later than 'From' time (same day only).");
} }
@ -1024,10 +999,10 @@ namespace PSTW_CentralSystem.Controllers.API
OtDate = request.OtDate, OtDate = request.OtDate,
OfficeFrom = officeFrom, OfficeFrom = officeFrom,
OfficeTo = officeTo, OfficeTo = officeTo,
OfficeBreak = request.OfficeBreak, // Assuming it's nullable or default to 0 OfficeBreak = request.OfficeBreak,
AfterFrom = afterFrom, AfterFrom = afterFrom,
AfterTo = afterTo, AfterTo = afterTo,
AfterBreak = request.AfterBreak, // Assuming it's nullable or default to 0 AfterBreak = request.AfterBreak,
StationId = request.StationId, StationId = request.StationId,
OtDescription = request.OtDescription, OtDescription = request.OtDescription,
OtDays = request.OtDays, OtDays = request.OtDays,
@ -1142,7 +1117,7 @@ namespace PSTW_CentralSystem.Controllers.API
try try
{ {
var records = _centralDbContext.Otregisters var records = _centralDbContext.Otregisters
.Include(o => o.Stations) // <--- ADD THIS LINE .Include(o => o.Stations)
.Where(o => o.UserId == userId) .Where(o => o.UserId == userId)
.OrderByDescending(o => o.OtDate) .OrderByDescending(o => o.OtDate)
.Select(o => new .Select(o => new
@ -1233,7 +1208,6 @@ namespace PSTW_CentralSystem.Controllers.API
var relativePath = Path.Combine("Media", "Overtime", uniqueFileName).Replace("\\", "/"); var relativePath = Path.Combine("Media", "Overtime", uniqueFileName).Replace("\\", "/");
// Create a NEW OtStatusModel for the resubmission
var statusModel = new OtStatusModel var statusModel = new OtStatusModel
{ {
UserId = userId, UserId = userId,
@ -1248,9 +1222,8 @@ namespace PSTW_CentralSystem.Controllers.API
}; };
_centralDbContext.Otstatus.Add(statusModel); _centralDbContext.Otstatus.Add(statusModel);
await _centralDbContext.SaveChangesAsync(); // Save the new OtStatus record to get its StatusId await _centralDbContext.SaveChangesAsync();
// Update StatusId in OtRegister records for the current month/year
var monthStart = new DateTime(model.Year, model.Month, 1); var monthStart = new DateTime(model.Year, model.Month, 1);
var monthEnd = monthStart.AddMonths(1); var monthEnd = monthStart.AddMonths(1);
@ -1266,7 +1239,6 @@ namespace PSTW_CentralSystem.Controllers.API
_centralDbContext.Otregisters.UpdateRange(otRecords); _centralDbContext.Otregisters.UpdateRange(otRecords);
await _centralDbContext.SaveChangesAsync(); await _centralDbContext.SaveChangesAsync();
// Update HrUserSetting with the NEW StatusId
var userSetting = _centralDbContext.Hrusersetting.FirstOrDefault(s => s.UserId == userId); var userSetting = _centralDbContext.Hrusersetting.FirstOrDefault(s => s.UserId == userId);
if (userSetting != null) if (userSetting != null)
{ {
@ -1290,7 +1262,6 @@ namespace PSTW_CentralSystem.Controllers.API
{ {
try try
{ {
// Get the latest OtStatus record for the user, month, and year
var latestStatus = _centralDbContext.Otstatus var latestStatus = _centralDbContext.Otstatus
.Where(s => s.UserId == userId && s.Month == month && s.Year == year) .Where(s => s.UserId == userId && s.Month == month && s.Year == year)
.OrderByDescending(s => s.SubmitDate) .OrderByDescending(s => s.SubmitDate)
@ -1298,29 +1269,26 @@ namespace PSTW_CentralSystem.Controllers.API
if (latestStatus == null) if (latestStatus == null)
{ {
return Ok(false); // Not submitted yet return Ok(false);
} }
// Check if the latest submission has been rejected at any level
if (latestStatus.HouStatus?.ToLower() == "rejected" || if (latestStatus.HouStatus?.ToLower() == "rejected" ||
latestStatus.HodStatus?.ToLower() == "rejected" || latestStatus.HodStatus?.ToLower() == "rejected" ||
latestStatus.ManagerStatus?.ToLower() == "rejected" || latestStatus.ManagerStatus?.ToLower() == "rejected" ||
latestStatus.HrStatus?.ToLower() == "rejected") latestStatus.HrStatus?.ToLower() == "rejected")
{ {
return Ok(false); // Latest submission was rejected, enable submit return Ok(false);
} }
// If not rejected, check if it's in a pending state (new submission)
if (latestStatus.HouStatus?.ToLower() == "pending" || if (latestStatus.HouStatus?.ToLower() == "pending" ||
latestStatus.HodStatus?.ToLower() == "pending" || latestStatus.HodStatus?.ToLower() == "pending" ||
latestStatus.ManagerStatus?.ToLower() == "pending" || latestStatus.ManagerStatus?.ToLower() == "pending" ||
latestStatus.HrStatus?.ToLower() == "pending") latestStatus.HrStatus?.ToLower() == "pending")
{ {
return Ok(true); // Newly submitted or resubmitted, disable submit return Ok(true);
} }
// If not pending and not rejected, it implies it's fully approved or in a final rejected state return Ok(true);
return Ok(true); // Disable submit
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -1376,14 +1344,13 @@ namespace PSTW_CentralSystem.Controllers.API
return Ok(record); return Ok(record);
} }
// New API Endpoint to Get User's Flexi Hour
[HttpGet("GetUserFlexiHour/{userId}")] [HttpGet("GetUserFlexiHour/{userId}")]
public async Task<IActionResult> GetUserFlexiHour(int userId) public async Task<IActionResult> GetUserFlexiHour(int userId)
{ {
try try
{ {
var userSetting = await _centralDbContext.Hrusersetting var userSetting = await _centralDbContext.Hrusersetting
.Include(hs => hs.FlexiHour) // Include the FlexiHour navigation property .Include(hs => hs.FlexiHour)
.FirstOrDefaultAsync(hs => hs.UserId == userId); .FirstOrDefaultAsync(hs => hs.UserId == userId);
if (userSetting == null || userSetting.FlexiHour == null) if (userSetting == null || userSetting.FlexiHour == null)
@ -1392,7 +1359,6 @@ namespace PSTW_CentralSystem.Controllers.API
return NotFound(new { message = "Flexi hour not found for this user." }); return NotFound(new { message = "Flexi hour not found for this user." });
} }
// Return the FlexiHourModel object
return Ok(new { flexiHour = userSetting.FlexiHour }); return Ok(new { flexiHour = userSetting.FlexiHour });
} }
catch (Exception ex) catch (Exception ex)
@ -1401,6 +1367,7 @@ namespace PSTW_CentralSystem.Controllers.API
return StatusCode(500, new { message = "An error occurred while fetching flexi hour." }); return StatusCode(500, new { message = "An error occurred while fetching flexi hour." });
} }
} }
[HttpPost] [HttpPost]
[Route("UpdateOvertimeRecord")] [Route("UpdateOvertimeRecord")]
public IActionResult UpdateOvertimeRecord([FromBody] OtRegisterUpdateDto model) public IActionResult UpdateOvertimeRecord([FromBody] OtRegisterUpdateDto model)
@ -1414,7 +1381,6 @@ namespace PSTW_CentralSystem.Controllers.API
return BadRequest(ModelState); return BadRequest(ModelState);
} }
// Validate time ranges
var timeValidationError = ValidateTimeRanges(model); var timeValidationError = ValidateTimeRanges(model);
if (timeValidationError != null) if (timeValidationError != null)
{ {
@ -1427,7 +1393,6 @@ namespace PSTW_CentralSystem.Controllers.API
return NotFound(new { message = "Overtime record not found." }); return NotFound(new { message = "Overtime record not found." });
} }
// Update properties
existing.OtDate = model.OtDate; existing.OtDate = model.OtDate;
existing.OfficeFrom = TimeSpan.TryParse(model.OfficeFrom, out var officeFrom) ? officeFrom : null; existing.OfficeFrom = TimeSpan.TryParse(model.OfficeFrom, out var officeFrom) ? officeFrom : null;
existing.OfficeTo = TimeSpan.TryParse(model.OfficeTo, out var officeTo) ? officeTo : null; existing.OfficeTo = TimeSpan.TryParse(model.OfficeTo, out var officeTo) ? officeTo : null;
@ -1457,47 +1422,44 @@ namespace PSTW_CentralSystem.Controllers.API
TimeSpan? afterFrom = string.IsNullOrEmpty(model.AfterFrom) ? (TimeSpan?)null : TimeSpan.Parse(model.AfterFrom); TimeSpan? afterFrom = string.IsNullOrEmpty(model.AfterFrom) ? (TimeSpan?)null : TimeSpan.Parse(model.AfterFrom);
TimeSpan? afterTo = string.IsNullOrEmpty(model.AfterTo) ? (TimeSpan?)null : TimeSpan.Parse(model.AfterTo); TimeSpan? afterTo = string.IsNullOrEmpty(model.AfterTo) ? (TimeSpan?)null : TimeSpan.Parse(model.AfterTo);
// Define allowed range for FROM when TO is 00:00 (midnight) TimeSpan minAllowedFromMidnightTo = new TimeSpan(16, 30, 0);
TimeSpan minAllowedFromMidnightTo = new TimeSpan(16, 30, 0); // 4:30 PM TimeSpan maxAllowedFromMidnightTo = new TimeSpan(23, 30, 0);
TimeSpan maxAllowedFromMidnightTo = new TimeSpan(23, 30, 0); // 11:30 PM
// Validate office hours
if (officeFrom.HasValue && officeTo.HasValue) if (officeFrom.HasValue && officeTo.HasValue)
{ {
if (officeTo == TimeSpan.Zero) // If OfficeTo is exactly midnight (00:00:00) if (officeTo == TimeSpan.Zero)
{ {
if (officeFrom == TimeSpan.Zero) if (officeFrom == TimeSpan.Zero)
{ {
return "Invalid Office Hour Time: 'From' and 'To' cannot both be 00:00 (midnight)."; return "Invalid Office Hour Time: 'From' and 'To' cannot both be 00:00 (midnight).";
} }
// Check if OfficeFrom is within the specified range (4:30 PM to 11:30 PM)
if (officeFrom.Value < minAllowedFromMidnightTo || officeFrom.Value > maxAllowedFromMidnightTo) if (officeFrom.Value < minAllowedFromMidnightTo || officeFrom.Value > maxAllowedFromMidnightTo)
{ {
return "Invalid Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between 4:30 pm and 11:30 pm on the same day to be saved."; return "Invalid Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between 4:30 pm and 11:30 pm on the same day to be saved.";
} }
} }
else if (officeTo <= officeFrom) // For all other cases, "To" must be strictly greater than "From" else if (officeTo <= officeFrom)
{ {
return "Invalid Office Hour Time: 'To' time must be later than 'From' time (same day only)."; return "Invalid Office Hour Time: 'To' time must be later than 'From' time (same day only).";
} }
} }
// Validate after hours
if (afterFrom.HasValue && afterTo.HasValue) if (afterFrom.HasValue && afterTo.HasValue)
{ {
if (afterTo == TimeSpan.Zero) // If AfterTo is exactly midnight (00:00:00) if (afterTo == TimeSpan.Zero)
{ {
if (afterFrom == TimeSpan.Zero) if (afterFrom == TimeSpan.Zero)
{ {
return "Invalid After Office Hour Time: 'From' and 'To' cannot both be 00:00 (midnight)."; return "Invalid After Office Hour Time: 'From' and 'To' cannot both be 00:00 (midnight).";
} }
// Check if AfterFrom is within the specified range (4:30 PM to 11:30 PM)
if (afterFrom.Value < minAllowedFromMidnightTo || afterFrom.Value > maxAllowedFromMidnightTo) if (afterFrom.Value < minAllowedFromMidnightTo || afterFrom.Value > maxAllowedFromMidnightTo)
{ {
return "Invalid After Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between 4:30 pm and 11:30 pm on the same day to be saved."; return "Invalid After Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between 4:30 pm and 11:30 pm on the same day to be saved.";
} }
} }
else if (afterTo <= afterFrom) // For all other cases, "To" must be strictly greater than "From" else if (afterTo <= afterFrom)
{ {
return "Invalid After Office Hour Time: 'To' time must be later than 'From' time (same day only)."; return "Invalid After Office Hour Time: 'To' time must be later than 'From' time (same day only).";
} }
@ -1529,7 +1491,6 @@ namespace PSTW_CentralSystem.Controllers.API
.Select(x => x.ApprovalFlowId) .Select(x => x.ApprovalFlowId)
.FirstOrDefault(); .FirstOrDefault();
// Default values when no approval flow exists
bool includeHou = false; bool includeHou = false;
bool includeHod = false; bool includeHod = false;
bool includeManager = false; bool includeManager = false;
@ -1594,7 +1555,6 @@ namespace PSTW_CentralSystem.Controllers.API
.ToList(); .ToList();
if (!flows.Any()) if (!flows.Any())
// Ensure OverallPendingMonths is always returned, even if empty
return Json(new { Roles = new List<string>(), Data = new List<object>(), OverallPendingMonths = new List<string>() }); return Json(new { Roles = new List<string>(), Data = new List<object>(), OverallPendingMonths = new List<string>() });
var flowRoleMap = new Dictionary<int, string>(); var flowRoleMap = new Dictionary<int, string>();
@ -1606,8 +1566,6 @@ namespace PSTW_CentralSystem.Controllers.API
if (flow.HR == currentUserId) flowRoleMap[flow.ApprovalFlowId] = "HR"; if (flow.HR == currentUserId) flowRoleMap[flow.ApprovalFlowId] = "HR";
} }
// --- Modified: Load ALL relevant OT entries for the current user for ALL months/years ---
// This is to populate the 'OverallPendingMonths' list accurately.
var allRelevantOtEntries = (from status in _centralDbContext.Otstatus var allRelevantOtEntries = (from status in _centralDbContext.Otstatus
join setting in _centralDbContext.Hrusersetting on status.UserId equals setting.UserId join setting in _centralDbContext.Hrusersetting on status.UserId equals setting.UserId
where setting.ApprovalFlowId.HasValue && flowRoleMap.Keys.Contains(setting.ApprovalFlowId.Value) where setting.ApprovalFlowId.HasValue && flowRoleMap.Keys.Contains(setting.ApprovalFlowId.Value)
@ -1624,19 +1582,16 @@ namespace PSTW_CentralSystem.Controllers.API
var pendingMonthsAndYears = new HashSet<string>(); var pendingMonthsAndYears = new HashSet<string>();
// We need to fetch the flows again here to get the full flow structure var allFlows = _centralDbContext.Approvalflow.ToList();
// as `allRelevantOtEntries` only brings back ApprovalFlowId, not the full flow object
var allFlows = _centralDbContext.Approvalflow.ToList(); // Fetch all flows once
foreach (var entry in allRelevantOtEntries) foreach (var entry in allRelevantOtEntries)
{ {
var role = flowRoleMap[entry.ApprovalFlowId.Value]; var role = flowRoleMap[entry.ApprovalFlowId.Value];
var flow = allFlows.FirstOrDefault(f => f.ApprovalFlowId == entry.ApprovalFlowId); // Get the full flow object var flow = allFlows.FirstOrDefault(f => f.ApprovalFlowId == entry.ApprovalFlowId);
if (flow == null) continue; if (flow == null) continue;
bool isPendingForCurrentUser = false; bool isPendingForCurrentUser = false;
// Determine if the current user has a pending action for this specific entry
if (role == "HoU" && (entry.HouStatus == null || entry.HouStatus == "Pending") && if (role == "HoU" && (entry.HouStatus == null || entry.HouStatus == "Pending") &&
!(entry.HodStatus == "Rejected" || entry.ManagerStatus == "Rejected" || entry.HrStatus == "Rejected")) !(entry.HodStatus == "Rejected" || entry.ManagerStatus == "Rejected" || entry.HrStatus == "Rejected"))
{ {
@ -1666,14 +1621,10 @@ namespace PSTW_CentralSystem.Controllers.API
if (isPendingForCurrentUser) if (isPendingForCurrentUser)
{ {
// Format as MM/YYYY and add to the set
pendingMonthsAndYears.Add($"{entry.Month:D2}/{entry.Year}"); pendingMonthsAndYears.Add($"{entry.Month:D2}/{entry.Year}");
} }
} }
// --- End of Modified Logic for Overall Pending Months ---
// This part remains the same: load OT status entries for the *selected* month/year
var otEntriesForSelectedMonth = (from status in _centralDbContext.Otstatus var otEntriesForSelectedMonth = (from status in _centralDbContext.Otstatus
join user in _centralDbContext.Users on status.UserId equals user.Id join user in _centralDbContext.Users on status.UserId equals user.Id
join setting in _centralDbContext.Hrusersetting on status.UserId equals setting.UserId join setting in _centralDbContext.Hrusersetting on status.UserId equals setting.UserId
@ -1702,7 +1653,7 @@ namespace PSTW_CentralSystem.Controllers.API
var role = flowRoleMap[entry.ApprovalFlowId.Value]; var role = flowRoleMap[entry.ApprovalFlowId.Value];
distinctRoles.Add(role); distinctRoles.Add(role);
var flow = allFlows.FirstOrDefault(f => f.ApprovalFlowId == entry.ApprovalFlowId); // Use the already fetched allFlows var flow = allFlows.FirstOrDefault(f => f.ApprovalFlowId == entry.ApprovalFlowId);
if (flow == null) continue; if (flow == null) continue;
bool canApprove = false; bool canApprove = false;
@ -1765,7 +1716,7 @@ namespace PSTW_CentralSystem.Controllers.API
{ {
Roles = distinctRoles.ToList(), Roles = distinctRoles.ToList(),
Data = processedList, Data = processedList,
OverallPendingMonths = pendingMonthsAndYears.OrderByDescending(m => m).ToList() // Return sorted list of "MM/YYYY" OverallPendingMonths = pendingMonthsAndYears.OrderByDescending(m => m).ToList()
}); });
} }
@ -1881,10 +1832,9 @@ namespace PSTW_CentralSystem.Controllers.API
.Select(d => d.DepartmentName) .Select(d => d.DepartmentName)
.FirstOrDefault(); .FirstOrDefault();
// Assuming 'RateValue' in your 'Rates' table actually stores the Basic Salary
var userBasicSalary = _centralDbContext.Rates var userBasicSalary = _centralDbContext.Rates
.Where(r => r.UserId == user.Id) .Where(r => r.UserId == user.Id)
.Select(r => r.RateValue) // Now this `RateValue` is intended to be the Basic Salary .Select(r => r.RateValue)
.FirstOrDefault(); .FirstOrDefault();
var userSetting = _centralDbContext.Hrusersetting var userSetting = _centralDbContext.Hrusersetting
@ -1900,10 +1850,9 @@ namespace PSTW_CentralSystem.Controllers.API
if (userSetting?.Approvalflow == null) if (userSetting?.Approvalflow == null)
return NotFound("Approval flow information not found for the user."); return NotFound("Approval flow information not found for the user.");
// Public holidays for the table display (already fetched and formatted)
var publicHolidaysForTable = _centralDbContext.Holidays var publicHolidaysForTable = _centralDbContext.Holidays
.Where(h => h.StateId == userSetting.State.StateId) .Where(h => h.StateId == userSetting.State.StateId)
.Select(h => h.HolidayDate.Date.ToString("yyyy-MM-dd")) // Format to YYYY-MM-DD string .Select(h => h.HolidayDate.Date.ToString("yyyy-MM-dd"))
.ToList(); .ToList();
var otRecords = _centralDbContext.Otregisters var otRecords = _centralDbContext.Otregisters
@ -1916,17 +1865,17 @@ namespace PSTW_CentralSystem.Controllers.API
DateTime otDate = o.OtDate.Date; DateTime otDate = o.OtDate.Date;
DayOfWeek dayOfWeek = otDate.DayOfWeek; DayOfWeek dayOfWeek = otDate.DayOfWeek;
if (publicHolidaysForTable.Contains(otDate.ToString("yyyy-MM-dd"))) // Use formatted date for check if (publicHolidaysForTable.Contains(otDate.ToString("yyyy-MM-dd")))
{ {
dayType = "Public Holiday"; dayType = "Public Holiday";
} }
else if (userSetting.State.WeekendId == 1) // Friday/Saturday weekend (for specific states like Johor, Kedah, Kelantan, Terengganu) else if (userSetting.State.WeekendId == 1)
{ {
if (dayOfWeek == DayOfWeek.Friday) dayType = "Off Day"; if (dayOfWeek == DayOfWeek.Friday) dayType = "Off Day";
else if (dayOfWeek == DayOfWeek.Saturday) dayType = "Rest Day"; else if (dayOfWeek == DayOfWeek.Saturday) dayType = "Rest Day";
else dayType = "Normal Day"; else dayType = "Normal Day";
} }
else if (userSetting.State.WeekendId == 2) // Saturday/Sunday weekend (most other states) else if (userSetting.State.WeekendId == 2)
{ {
if (dayOfWeek == DayOfWeek.Saturday) dayType = "Off Day"; if (dayOfWeek == DayOfWeek.Saturday) dayType = "Off Day";
else if (dayOfWeek == DayOfWeek.Sunday) dayType = "Rest Day"; else if (dayOfWeek == DayOfWeek.Sunday) dayType = "Rest Day";
@ -1934,7 +1883,7 @@ namespace PSTW_CentralSystem.Controllers.API
} }
else else
{ {
dayType = "Normal Day"; // Default if WeekendId is neither 1 nor 2 dayType = "Normal Day";
} }
return new return new
@ -1953,7 +1902,7 @@ namespace PSTW_CentralSystem.Controllers.API
o.OtDays, o.OtDays,
o.UserId, o.UserId,
Rate = userBasicSalary, // Pass the Basic Salary as 'Rate' Rate = userBasicSalary, // Pass the Basic Salary as 'Rate'
DayType = dayType, // This is for the table display DayType = dayType,
}; };
}) })
.ToList(); .ToList();
@ -1977,7 +1926,7 @@ namespace PSTW_CentralSystem.Controllers.API
hasApproverActed = !string.IsNullOrEmpty(otStatus.HrStatus) && otStatus.HrStatus != "Pending"; hasApproverActed = !string.IsNullOrEmpty(otStatus.HrStatus) && otStatus.HrStatus != "Pending";
break; break;
default: default:
hasApproverActed = true; // Default to disabled if role not matched or already acted hasApproverActed = true;
break; break;
} }
@ -1991,9 +1940,9 @@ namespace PSTW_CentralSystem.Controllers.API
filePath = otStatus.FilePath, filePath = otStatus.FilePath,
fullNameLower = user.FullName?.ToLower(), fullNameLower = user.FullName?.ToLower(),
flexiHour = userSetting.FlexiHour?.FlexiHour, flexiHour = userSetting.FlexiHour?.FlexiHour,
stateId = userSetting.State.StateId, // Ensure StateId is passed stateId = userSetting.State.StateId,
weekendId = userSetting.State.WeekendId, // Ensure WeekendId is passed weekendId = userSetting.State.WeekendId,
rate = userBasicSalary // Send Basic Salary as 'rate' in userInfo as well for overall calculations rate = userBasicSalary
}, },
records = otRecords, records = otRecords,
isHoU = userSetting.Approvalflow?.HoU == currentLoggedInUserId, isHoU = userSetting.Approvalflow?.HoU == currentLoggedInUserId,
@ -2003,7 +1952,7 @@ namespace PSTW_CentralSystem.Controllers.API
otStatus.HrStatus, otStatus.HrStatus,
approverRole, approverRole,
hasApproverActed hasApproverActed
// public holidays are now fetched separately in the frontend for the modal
}; };
return Ok(result); return Ok(result);
@ -2333,7 +2282,7 @@ namespace PSTW_CentralSystem.Controllers.API
var currentLoggedInUserId = GetCurrentLoggedInUserId(); var currentLoggedInUserId = GetCurrentLoggedInUserId();
var userSetting = _centralDbContext.Hrusersetting var userSetting = _centralDbContext.Hrusersetting
.Include(us => us.Approvalflow) // Include the ApprovalFlow to get approver IDs .Include(us => us.Approvalflow)
.Include(us => us.FlexiHour) .Include(us => us.FlexiHour)
.FirstOrDefault(us => us.UserId == user.Id); .FirstOrDefault(us => us.UserId == user.Id);
@ -2345,33 +2294,27 @@ namespace PSTW_CentralSystem.Controllers.API
string? flexiHour = userSetting?.FlexiHour?.FlexiHour; string? flexiHour = userSetting?.FlexiHour?.FlexiHour;
// --- Logic for collecting approved signatures ---
var approvedSignatures = new List<ApprovalSignatureData>(); var approvedSignatures = new List<ApprovalSignatureData>();
// Get the approval flow for the user whose OT is being viewed
var approvalFlow = userSetting?.Approvalflow; var approvalFlow = userSetting?.Approvalflow;
if (approvalFlow != null) if (approvalFlow != null)
{ {
// Define the approval sequence and their corresponding status fields
// We'll also include the UserModel directly here for FullName
var approvalStages = new List<(int? approverId, string statusField, DateTime? submitDate, UserModel? approverUser)>
{
(approvalFlow.HoU, otStatus.HouStatus, otStatus.HouSubmitDate, _centralDbContext.Users.FirstOrDefault(u => u.Id == approvalFlow.HoU)),
(approvalFlow.HoD, otStatus.HodStatus, otStatus.HodSubmitDate, _centralDbContext.Users.FirstOrDefault(u => u.Id == approvalFlow.HoD)),
(approvalFlow.Manager, otStatus.ManagerStatus, otStatus.ManagerSubmitDate, _centralDbContext.Users.FirstOrDefault(u => u.Id == approvalFlow.Manager)),
(approvalFlow.HR, otStatus.HrStatus, otStatus.HrSubmitDate, _centralDbContext.Users.FirstOrDefault(u => u.Id == approvalFlow.HR))
};
// Order by submit date to maintain flow, ensuring non-null submitDate are sorted var approvalStages = new List<(int? approverId, string statusField, DateTime? submitDate, UserModel? approverUser)>
// Approvals without a submit date (null) would appear first if not handled with care. {
// For a proper flow, we only consider stages with an Approved status and a valid submit date. (approvalFlow.HoU, otStatus.HouStatus, otStatus.HouSubmitDate, _centralDbContext.Users.FirstOrDefault(u => u.Id == approvalFlow.HoU)),
(approvalFlow.HoD, otStatus.HodStatus, otStatus.HodSubmitDate, _centralDbContext.Users.FirstOrDefault(u => u.Id == approvalFlow.HoD)),
(approvalFlow.Manager, otStatus.ManagerStatus, otStatus.ManagerSubmitDate, _centralDbContext.Users.FirstOrDefault(u => u.Id == approvalFlow.Manager)),
(approvalFlow.HR, otStatus.HrStatus, otStatus.HrSubmitDate, _centralDbContext.Users.FirstOrDefault(u => u.Id == approvalFlow.HR))
};
foreach (var stage in approvalStages foreach (var stage in approvalStages
.Where(s => s.approverUser != null && s.statusField == "Approved" && s.submitDate.HasValue) .Where(s => s.approverUser != null && s.statusField == "Approved" && s.submitDate.HasValue)
.OrderBy(s => s.submitDate)) .OrderBy(s => s.submitDate))
{ {
byte[]? signatureImageBytes = null; byte[]? signatureImageBytes = null;
// Directly query StaffSign using approver's UserId
var staffSign = _centralDbContext.Staffsign var staffSign = _centralDbContext.Staffsign
.FirstOrDefault(ss => ss.UserId == stage.approverId.Value); .FirstOrDefault(ss => ss.UserId == stage.approverId.Value);
@ -2379,11 +2322,10 @@ namespace PSTW_CentralSystem.Controllers.API
{ {
ApproverName = stage.approverUser.FullName, ApproverName = stage.approverUser.FullName,
SignatureImage = signatureImageBytes, SignatureImage = signatureImageBytes,
ApprovedDate = stage.submitDate // Pass the submit date here ApprovedDate = stage.submitDate
}); });
} }
} }
// --- End logic for collecting approved signatures ---
var pdfGenerator = new OvertimePDF(_centralDbContext); var pdfGenerator = new OvertimePDF(_centralDbContext);
var stream = pdfGenerator.GenerateOvertimeTablePdf(otRecords, user, userRate, selectedMonth, logoImage, isHoU, flexiHour, approvedSignatures); var stream = pdfGenerator.GenerateOvertimeTablePdf(otRecords, user, userRate, selectedMonth, logoImage, isHoU, flexiHour, approvedSignatures);
@ -2402,7 +2344,6 @@ namespace PSTW_CentralSystem.Controllers.API
if (user == null) if (user == null)
return NotFound("User not found."); return NotFound("User not found.");
// Get the user's rate
var userRate = _centralDbContext.Rates var userRate = _centralDbContext.Rates
.Where(r => r.UserId == user.Id) .Where(r => r.UserId == user.Id)
.OrderByDescending(r => r.LastUpdated) .OrderByDescending(r => r.LastUpdated)
@ -2436,7 +2377,6 @@ namespace PSTW_CentralSystem.Controllers.API
logoImage = System.IO.File.ReadAllBytes(logoPath); logoImage = System.IO.File.ReadAllBytes(logoPath);
} }
// Get flexi hour if exists
var flexiHour = _centralDbContext.Hrusersetting var flexiHour = _centralDbContext.Hrusersetting
.Include(us => us.FlexiHour) .Include(us => us.FlexiHour)
.FirstOrDefault(us => us.UserId == userId)?.FlexiHour?.FlexiHour; .FirstOrDefault(us => us.UserId == userId)?.FlexiHour?.FlexiHour;
@ -2461,7 +2401,6 @@ namespace PSTW_CentralSystem.Controllers.API
if (user == null) if (user == null)
return NotFound("User not found."); return NotFound("User not found.");
// Get the user's rate
var userRate = _centralDbContext.Rates var userRate = _centralDbContext.Rates
.Where(r => r.UserId == user.Id) .Where(r => r.UserId == user.Id)
.OrderByDescending(r => r.LastUpdated) .OrderByDescending(r => r.LastUpdated)
@ -2476,7 +2415,7 @@ namespace PSTW_CentralSystem.Controllers.API
? otRecords.First().OtDate ? otRecords.First().OtDate
: DateTime.Now; : DateTime.Now;
var currentLoggedInUserId = GetCurrentLoggedInUserId(); // Assuming this is defined elsewhere in your API controller var currentLoggedInUserId = GetCurrentLoggedInUserId();
var userSetting = _centralDbContext.Hrusersetting var userSetting = _centralDbContext.Hrusersetting
.Include(us => us.Approvalflow) .Include(us => us.Approvalflow)
@ -2491,13 +2430,11 @@ namespace PSTW_CentralSystem.Controllers.API
string? flexiHour = userSetting?.FlexiHour?.FlexiHour; string? flexiHour = userSetting?.FlexiHour?.FlexiHour;
// Pass _env to the OvertimeExcel constructor
var excelGenerator = new OvertimeExcel(_centralDbContext, _env); var excelGenerator = new OvertimeExcel(_centralDbContext, _env);
var logoPath = Path.Combine(_env.WebRootPath, "images", "logo.jpg"); var logoPath = Path.Combine(_env.WebRootPath, "images", "logo.jpg");
byte[]? logoImage = System.IO.File.Exists(logoPath) ? System.IO.File.ReadAllBytes(logoPath) : null; byte[]? logoImage = System.IO.File.Exists(logoPath) ? System.IO.File.ReadAllBytes(logoPath) : null;
// Pass the otStatus object to the GenerateOvertimeExcel method
var stream = excelGenerator.GenerateOvertimeExcel(otRecords, user, userRate, selectedMonth, isHoU, flexiHour, logoImage, isSimplifiedExport: false, otStatus); var stream = excelGenerator.GenerateOvertimeExcel(otRecords, user, userRate, selectedMonth, isHoU, flexiHour, logoImage, isSimplifiedExport: false, otStatus);
string fileName = $"OvertimeReport_{user.FullName}_{DateTime.Now:yyyyMMdd}.xlsx"; string fileName = $"OvertimeReport_{user.FullName}_{DateTime.Now:yyyyMMdd}.xlsx";
@ -2516,20 +2453,17 @@ namespace PSTW_CentralSystem.Controllers.API
if (user == null) if (user == null)
return NotFound("User not found."); return NotFound("User not found.");
// Get the user's rate
var userRate = _centralDbContext.Rates var userRate = _centralDbContext.Rates
.Where(r => r.UserId == user.Id) .Where(r => r.UserId == user.Id)
.OrderByDescending(r => r.LastUpdated) .OrderByDescending(r => r.LastUpdated)
.FirstOrDefault()?.RateValue ?? 0m; .FirstOrDefault()?.RateValue ?? 0m;
// Get the user's flexi hour setting
var userSetting = _centralDbContext.Hrusersetting var userSetting = _centralDbContext.Hrusersetting
.Include(us => us.FlexiHour) .Include(us => us.FlexiHour)
.FirstOrDefault(us => us.UserId == userId); .FirstOrDefault(us => us.UserId == userId);
string flexiHour = userSetting?.FlexiHour?.FlexiHour; string flexiHour = userSetting?.FlexiHour?.FlexiHour;
// Get records for the selected month/year
var startDate = new DateTime(year, month, 1); var startDate = new DateTime(year, month, 1);
var endDate = startDate.AddMonths(1); var endDate = startDate.AddMonths(1);
@ -2538,28 +2472,22 @@ namespace PSTW_CentralSystem.Controllers.API
.Where(o => o.UserId == userId && o.OtDate >= startDate && o.OtDate < endDate) .Where(o => o.UserId == userId && o.OtDate >= startDate && o.OtDate < endDate)
.ToList(); .ToList();
// Check if user is admin/PSTWAIR to show station column (logic remains same) bool isAdminUser = IsAdmin(user.Id);
bool isAdminUser = IsAdmin(user.Id); // Assuming IsAdmin is defined elsewhere in your API controller
// bool isPSTWAIR = user.Department?.DepartmentId == 2 || isAdminUser; // This variable is not used after declaration.
// Corrected line: Pass _env to the OvertimeExcel constructor
var excelGenerator = new OvertimeExcel(_centralDbContext, _env); var excelGenerator = new OvertimeExcel(_centralDbContext, _env);
// Get logo
var logoPath = Path.Combine(_env.WebRootPath, "images", "logo.jpg"); var logoPath = Path.Combine(_env.WebRootPath, "images", "logo.jpg");
byte[]? logoImage = System.IO.File.Exists(logoPath) ? System.IO.File.ReadAllBytes(logoPath) : null; byte[]? logoImage = System.IO.File.Exists(logoPath) ? System.IO.File.ReadAllBytes(logoPath) : null;
// Call GenerateOvertimeExcel with isSimplifiedExport = true
// For regular users, isHoU is typically false as they are not generating approver-specific reports.
var stream = excelGenerator.GenerateOvertimeExcel( var stream = excelGenerator.GenerateOvertimeExcel(
otRecords, otRecords,
user, user,
userRate, userRate,
startDate, startDate,
isHoU: false, // Set to false for regular user report isHoU: false,
flexiHour: flexiHour, flexiHour: flexiHour,
logoImage: logoImage, logoImage: logoImage,
isSimplifiedExport: true // Set to true for the simplified report from OtRecords isSimplifiedExport: true
); );
string fileName = $"OvertimeReport_{user.FullName}_{month}_{year}.xlsx"; string fileName = $"OvertimeReport_{user.FullName}_{month}_{year}.xlsx";

View File

@ -40,6 +40,7 @@ internal class Program
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}")) outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}"))
.WriteTo.Console() .WriteTo.Console()
.CreateLogger(); .CreateLogger();
// Set default session to 30 minutes // Set default session to 30 minutes
builder.Services.AddSession(options => builder.Services.AddSession(options =>
{ {
@ -54,6 +55,7 @@ internal class Program
mysqlOptions => mysqlOptions.CommandTimeout(120) mysqlOptions => mysqlOptions.CommandTimeout(120)
); );
}); });
//builder.Services.AddDbContext<InventoryDBContext>(options => //builder.Services.AddDbContext<InventoryDBContext>(options =>
//{ //{
// options.UseMySql(inventoryConnectionString, new MySqlServerVersion(new Version(8, 0, 39)), // options.UseMySql(inventoryConnectionString, new MySqlServerVersion(new Version(8, 0, 39)),