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)
{
ViewBag.StatusId = statusId; // If needed in the view
ViewBag.StatusId = statusId;
return View();
}

View File

@ -47,36 +47,34 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
return TimeSpan.TryParse(time, out TimeSpan result) ? result : null;
}
// OtRegisterEditDto.cs
public class OtRegisterEditDto
{
public int OvertimeId { get; set; }
public DateTime OtDate { get; set; }
public string? OfficeFrom { get; set; } // Use string to match input type (e.g., "09:00")
public string? OfficeTo { get; set; } // Use string to match input type
public string? OfficeFrom { get; set; }
public string? OfficeTo { get; set; }
public int? OfficeBreak { get; set; }
public string? AfterFrom { get; set; } // Use string to match input type
public string? AfterTo { get; set; } // Use string to match input type
public string? AfterFrom { get; set; }
public string? AfterTo { get; set; }
public int? AfterBreak { get; set; }
public int? StationId { get; set; }
public string? OtDescription { get; set; }
// You might also need to send the StatusId if it's relevant for the update logic
public int StatusId { get; set; }
public string? OtDays { get; set; }
}
public class OtUpdateLog
{
public string ApproverRole { get; set; } // e.g., "HoU", "HoD", "Manager", "HR"
public string ApproverRole { get; set; }
public int ApproverUserId { 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 OtRegisterEditDto? AfterEdit { get; set; }
// For "Delete" type
public OtRegisterModel? DeletedRecord { get; set; }
}

View File

@ -23,6 +23,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
{
public string ApproverName { 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.Linq;
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 PSTW_CentralSystem.DBContext;
@ -22,14 +22,14 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
}
public MemoryStream GenerateOvertimeTablePdf(
List<OtRegisterModel> records,
UserModel user,
decimal userRate,
DateTime? selectedMonth = null,
byte[]? logoImage = null,
bool isHoU = false,
string? flexiHour = null,
List<ApprovalSignatureData>? approvedSignatures = null)
List<OtRegisterModel> records,
UserModel user,
decimal userRate,
DateTime? selectedMonth = null,
byte[]? logoImage = null,
bool isHoU = false,
string? flexiHour = null,
List<ApprovalSignatureData>? approvedSignatures = null)
{
bool isAdminUser = IsAdmin(user.Id);
bool showStationColumn = user.departmentId == 2 || user.departmentId == 3 || isAdminUser;
@ -39,15 +39,13 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
var allDatesInMonth = GetAllDatesInMonth(displayMonth);
// Fetch user setting here once
var userSetting = _centralDbContext.Hrusersetting
.Include(us => us.State)
.FirstOrDefault(us => us.UserId == user.Id);
// Fetch actual public holiday objects including their names for the current month/year and user's state
var publicHolidaysForUser = _centralDbContext.Holidays
.Where(h => userSetting != null && h.StateId == userSetting.State.StateId && h.HolidayDate.Month == displayMonth.Month && h.HolidayDate.Year == displayMonth.Year)
.OrderBy(h => h.HolidayDate) // Order by date for display
.OrderBy(h => h.HolidayDate)
.ToList();
records = records.OrderBy(r => r.OtDate).ToList();
@ -62,7 +60,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
page.Content().Column(column =>
{
// Top Section: Logo, User Info, Generated Date
column.Item().Row(row =>
{
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);
}
// Option 1: Inline with Clear Labels for user details
col.Item().PaddingBottom(2).Text(text =>
{
text.Span("Name: ").SemiBold().FontSize(9);
@ -93,12 +89,10 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
.FontSize(9).FontColor(Colors.Grey.Medium);
});
// Overtime Record title remains separate for clarity
column.Item().PaddingTop(3).Row(row =>
{
row.RelativeItem(2).Text($"Overtime Record: {displayMonth:MMMM yyyy}").FontSize(9).Italic();
// Legend (remains on the same line as Overtime Record)
row.RelativeItem(1).AlignRight().Column(legendCol =>
{
legendCol.Item().Element(e => e.ShowEntire().Row(subRow =>
@ -123,15 +117,13 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
column.Item().PaddingVertical(10).LineHorizontal(0.5f).LineColor(Colors.Grey.Lighten2);
// Pass the fetched publicHolidays to ComposeTable
column.Item().Element(container => ComposeTable(container, records, user, userRate, showStationColumn, allDatesInMonth, isHoU, publicHolidaysForUser.Select(h => h.HolidayDate.Date).ToList()));
// Approval Signatures and Remarks section
column.Item().PaddingTop(20).Element(container =>
{
container.ShowEntire().Row(row =>
{
// Left side - Approval Signatures
row.RelativeItem().Element(e => e.ShowEntire().Column(approvalCol =>
{
if (approvedSignatures != null && approvedSignatures.Any())
@ -161,8 +153,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
if (approval.ApprovedDate.HasValue)
{
individualApprovalColumn.Item().PaddingTop(2)
.Text(approval.ApprovedDate.Value.ToString("dd MMMM yyyy")) // Changed format to avoid special characters
.FontSize(8).AlignCenter();
.Text(approval.ApprovedDate.Value.ToString("dd MMMM yyyy"))
.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 =>
{
remarksCol.Item().Element(e => e.ShowEntire().Row(remarksRow =>
{
// Public Holidays List as a mini-table
remarksRow.RelativeItem(2).Element(e => e.ShowEntire().AlignRight().Column(holidayListCol =>
{
holidayListCol.Item().Text("Public Holidays:").FontSize(9).SemiBold();
@ -183,8 +173,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(1); // For Date
columns.RelativeColumn(2); // For Holiday Name
columns.RelativeColumn(1);
columns.RelativeColumn(2);
});
table.Header(header =>
@ -204,8 +194,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
else
{
table.Cell().ColumnSpan(2).Border(0.25f).Padding(2)
.Text($"No public holidays found for {userSetting?.State?.StateName ?? "this state"} in {displayMonth:MMMM yyyy}.") // Changed format
.FontSize(7).Italic().AlignCenter();
.Text($"No public holidays found for {userSetting?.State?.StateName ?? "this state"} in {displayMonth:MMMM yyyy}.")
.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())
{
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;
// Changed calculations as per your request
var basicSalary = userRate; // userRate is now treated as basic salary
var orp = basicSalary / 26m; // Calculate ORP from basicSalary
var hrp = orp / 8m; // Calculate HRP from ORP
@ -431,7 +420,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
AddCell(!hasPrintedSalaryDetails ? $"{orp:F2}" : "");
AddCell(!hasPrintedSalaryDetails ? $"{hrp:F2}" : "");
}
hasPrintedSalaryDetails = true; // Ensure these values are printed only once
hasPrintedSalaryDetails = true;
if (date.Date != previousDate?.Date)
{
@ -554,7 +543,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
if (!isHoU)
{
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)
@ -566,12 +555,12 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
}
public MemoryStream GenerateSimpleOvertimeTablePdf(
List<OtRegisterModel> records,
UserModel user, // User parameter added here
decimal userRate,
DateTime? selectedMonth = null,
byte[]? logoImage = null,
string? flexiHour = null)
List<OtRegisterModel> records,
UserModel user,
decimal userRate,
DateTime? selectedMonth = null,
byte[]? logoImage = null,
string? flexiHour = null)
{
bool isAdminUser = IsAdmin(user.Id);
bool showStationColumn = user.departmentId == 2 || user.departmentId == 3 || isAdminUser;
@ -581,15 +570,13 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
var allDatesInMonth = GetAllDatesInMonth(displayMonth);
// Fetch user setting here once
var userSetting = _centralDbContext.Hrusersetting
.Include(us => us.State)
.FirstOrDefault(us => us.UserId == user.Id);
// Fetch actual public holiday objects including their names for the current month/year and user's state
var publicHolidaysForUser = _centralDbContext.Holidays
.Where(h => userSetting != null && h.StateId == userSetting.State.StateId && h.HolidayDate.Month == displayMonth.Month && h.HolidayDate.Year == displayMonth.Year)
.OrderBy(h => h.HolidayDate) // Order by date for display
.OrderBy(h => h.HolidayDate)
.ToList();
records = records.OrderBy(r => r.OtDate).ToList();
@ -604,7 +591,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
page.Content().Column(column =>
{
// Top Section: Logo, User Info, Generated Date
column.Item().Row(row =>
{
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);
}
// Option 1: Inline with Clear Labels for user details
col.Item().PaddingBottom(2).Text(text =>
{
text.Span("Name: ").SemiBold().FontSize(9);
@ -635,12 +620,10 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
.FontSize(9).FontColor(Colors.Grey.Medium);
});
// Overtime Record title remains separate for clarity
column.Item().PaddingTop(3).Row(row =>
{
row.RelativeItem(2).Text($"Overtime Record: {displayMonth:MMMM yyyy}").SemiBold().FontSize(9).Italic();
// Legend (remains on the same line as Overtime Record)
row.RelativeItem(1).AlignRight().Column(legendCol =>
{
legendCol.Item().Element(e => e.ShowEntire().Row(subRow =>
@ -664,20 +647,17 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
column.Item().PaddingVertical(10).LineHorizontal(0.5f).LineColor(Colors.Grey.Lighten2);
// Pass the fetched publicHolidays to ComposeSimpleTable
column.Item().Element(container => ComposeSimpleTable(container, records, user, showStationColumn, allDatesInMonth, publicHolidaysForUser.Select(h => h.HolidayDate.Date).ToList()));
// Public Holidays List (this section remains as is for now, below the table)
column.Item().PaddingTop(20).Element(container =>
{
container.ShowEntire().Row(row =>
{
row.RelativeItem().Column(legendCol =>
{
// Removed the legend remarks from here as they are now above the table
});
// Public Holidays List (on the right)
row.RelativeItem(2).AlignRight().Column(holidayListCol =>
{
holidayListCol.Item().Element(e => e.ShowEntire().Text("Public Holidays:").FontSize(9).SemiBold());
@ -686,8 +666,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(1); // For Date
columns.RelativeColumn(2); // For Holiday Name
columns.RelativeColumn(1);
columns.RelativeColumn(2);
});
table.Header(header =>
@ -708,7 +688,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{
table.Cell().ColumnSpan(2).Border(0.25f).Padding(2)
.Text($"No public holidays found for {userSetting?.State?.StateName ?? "this state"} in {displayMonth:MMMM yyyy}.") // Changed format
.FontSize(7).Italic().AlignCenter();
.FontSize(7).Italic().AlignCenter();
}
});
});
@ -722,21 +702,18 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
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)
{
var recordsGroupedByDate = records
.GroupBy(r => r.OtDate.Date)
.ToDictionary(g => g.Key, g => g.ToList());
// Fetch user settings and public holidays within ComposeSimpleTable
var userSetting = _centralDbContext.Hrusersetting
.Include(us => us.State)
.FirstOrDefault(us => us.UserId == user.Id);
container.Table(table =>
{
// Define columns
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(0.8f); // Day (ddd)
@ -766,53 +743,52 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
// Header
table.Header(header =>
{
// First row of the header
header.Cell().RowSpan(2).Background("#d9ead3").Border(0.25f).Padding(3)
.Text("Day").FontSize(6).Bold().AlignCenter();
header.Cell().RowSpan(2).Background("#d9ead3").Border(0.25f).Padding(3)
.Text("Date").FontSize(6).Bold().AlignCenter();
.Text("Date").FontSize(6).Bold().AlignCenter();
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)
.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)
.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)
.Text("Break (min)").FontSize(6).Bold().AlignCenter();
.Text("Break (min)").FontSize(6).Bold().AlignCenter();
if (showStationColumn)
{
header.Cell().RowSpan(2).Background("#d0f0ef").Border(0.25f).Padding(3)
.Text("Station").FontSize(6).Bold().AlignCenter();
.Text("Station").FontSize(6).Bold().AlignCenter();
}
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)
header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3)
.Text("From").FontSize(6).Bold().AlignCenter();
header.Cell().Background("#cfe2f3").Border(0.25f).Padding(3)
.Text("To").FontSize(6).Bold().AlignCenter();
.Text("To").FontSize(6).Bold().AlignCenter();
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)
.Text("From").FontSize(6).Bold().AlignCenter();
.Text("From").FontSize(6).Bold().AlignCenter();
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)
.Text("Break").FontSize(6).Bold().AlignCenter();
.Text("Break").FontSize(6).Bold().AlignCenter();
});
decimal totalHours = 0;
decimal totalBreak = 0;
bool alternate = false;
DateTime? previousDate = null; // To avoid repeating Day/Date for multiple records on the same day
DateTime? previousDate = null;
foreach (var date in allDatesInMonth)
{
@ -822,7 +798,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
foreach (var record in recordsToShow)
{
// Get background color for the current date based on user's state settings
var backgroundColor = GetDayCellBackgroundColor(date, userSetting?.State?.WeekendId, publicHolidays);
string rowBg = alternate ? "#f9f9f9" : "#ffffff";
@ -836,7 +811,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
if (center) text.AlignCenter();
}
// Apply background color to Day and Date cells
if (date.Date != previousDate?.Date)
{
AddCell(date.ToString("ddd"), true, backgroundColor);
@ -844,14 +818,11 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
}
else
{
// If multiple OT entries for the same day, don't repeat Day and Date, but still apply background
AddCell("", true, backgroundColor);
AddCell("", true, backgroundColor);
}
previousDate = date; // Update previousDate for the next iteration
previousDate = date;
// Calculate values
var officeHours = CalculateOfficeOtHours(record);
var afterHours = CalculateAfterOfficeOtHours(record);
var totalTime = ConvertTimeToDecimal(officeHours) + ConvertTimeToDecimal(afterHours);
@ -874,7 +845,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
AddCell(FormatAsHourMinute(totalTime, isMinutes: false));
AddCell(FormatAsHourMinute(breakTime, isMinutes: true));
// Station (if applicable)
// Station
if (showStationColumn)
{
AddCell(record.Stations?.StationName ?? "");
@ -882,7 +853,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
// Description
table.Cell().Background(rowBg).Border(0.25f).Padding(2)
.Text(record.OtDescription ?? "").FontSize(6).AlignLeft();
.Text(record.OtDescription ?? "").FontSize(6).AlignLeft();
alternate = !alternate;
}
@ -891,21 +862,17 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
// Footer with totals
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)
.Text("TOTAL").Bold().FontSize(6).AlignCenter();
// Create an empty cell that spans the remaining columns before "Total OT Hours"
// Remaining columns: Office Hour (3) + After Office Hour (3) = 6 columns
footer.Cell().ColumnSpan(6).Background("#d8d1f5").Border(0.80f).Padding(3).Text("").FontSize(6).AlignCenter();
// These remain aligned with their respective columns
footer.Cell().Background("#d8d1f5").Border(0.80f).Padding(3)
.Text(FormatAsHourMinute(totalHours, isMinutes: false)).Bold().FontSize(6).AlignCenter();
footer.Cell().Background("#d8d1f5").Border(0.80f).Padding(3)
.Text(FormatAsHourMinute(totalBreak, isMinutes: true)).Bold().FontSize(6).AlignCenter();
.Text(FormatAsHourMinute(totalBreak, isMinutes: true)).Bold().FontSize(6).AlignCenter();
if (showStationColumn)
{
@ -935,9 +902,9 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
var lastDate = records.Last().OtDate;
if (firstDate.Month == lastDate.Month && firstDate.Year == lastDate.Year)
return firstDate.ToString("MMMM yyyy"); // Fixed formatting to yyyy
return firstDate.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)
@ -956,8 +923,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
return "";
TimeSpan ts = isMinutes
? TimeSpan.FromMinutes((double)hoursOrMinutes.Value)
: TimeSpan.FromHours((double)hoursOrMinutes.Value);
? TimeSpan.FromMinutes((double)hoursOrMinutes.Value)
: TimeSpan.FromHours((double)hoursOrMinutes.Value);
int totalHours = (int)(ts.TotalHours);
int minutes = (int)(ts.Minutes);
@ -974,14 +941,14 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
DayOfWeek dayOfWeek = date.DayOfWeek;
if ((weekendId == 1 && dayOfWeek == DayOfWeek.Saturday) || // Assuming weekendId 1 means Saturday is Rest Day
(weekendId == 2 && dayOfWeek == DayOfWeek.Sunday)) // Assuming weekendId 2 means Sunday is Rest Day
if ((weekendId == 1 && dayOfWeek == DayOfWeek.Saturday) ||
(weekendId == 2 && dayOfWeek == DayOfWeek.Sunday))
{
return "Rest Day";
}
if ((weekendId == 1 && dayOfWeek == DayOfWeek.Friday) || // Assuming weekendId 1 means Friday is Off Day
(weekendId == 2 && dayOfWeek == DayOfWeek.Saturday)) // Assuming weekendId 2 means Saturday is Off Day
if ((weekendId == 1 && dayOfWeek == DayOfWeek.Friday) ||
(weekendId == 2 && dayOfWeek == DayOfWeek.Saturday))
{
return "Off Day";
}
@ -989,7 +956,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
return "Normal Day";
}
// Updated these methods to reflect the new calculation logic and accept the correct input.
private decimal CalculateOrp(decimal basicSalary)
{
return basicSalary / 26m;
@ -1019,20 +985,18 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{
if (!record.OfficeFrom.HasValue || !record.OfficeTo.HasValue) return "";
// Convert TimeSpan to minutes from start of the day
double fromMinutes = record.OfficeFrom.Value.TotalMinutes;
double toMinutes = record.OfficeTo.Value.TotalMinutes;
double totalMinutes = toMinutes - fromMinutes;
// Handle overnight shifts (e.g., 21:00 to 02:00)
if (totalMinutes < 0)
{
totalMinutes += 24 * 60; // Add 24 hours in minutes
totalMinutes += 24 * 60;
}
totalMinutes -= (record.OfficeBreak ?? 0); // Subtract break
totalMinutes = Math.Max(0, totalMinutes); // Ensure total hours are not negative
totalMinutes -= (record.OfficeBreak ?? 0);
totalMinutes = Math.Max(0, totalMinutes);
int hours = (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 "";
// Convert TimeSpan to minutes from start of the day
double fromMinutes = record.AfterFrom.Value.TotalMinutes;
double toMinutes = record.AfterTo.Value.TotalMinutes;
double totalMinutes = toMinutes - fromMinutes;
// Handle overnight shifts (e.g., 21:00 to 02:00)
if (totalMinutes < 0)
{
totalMinutes += 24 * 60; // Add 24 hours in minutes
totalMinutes += 24 * 60;
}
totalMinutes -= (record.AfterBreak ?? 0); // Subtract break
totalMinutes = Math.Max(0, totalMinutes); // Ensure total hours are not negative
totalMinutes -= (record.AfterBreak ?? 0);
totalMinutes = Math.Max(0, totalMinutes);
int hours = (int)(totalMinutes / 60);
int minutes = (int)(totalMinutes % 60);
@ -1073,7 +1035,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
var officeHrs = ConvertTimeToDecimal(officeRaw);
var afterHrs = ConvertTimeToDecimal(afterRaw);
// Helper to format numbers. Will return "" if num is 0 or less.
var toFixedOrEmpty = (decimal num) => num > 0 ? num.ToString("N2") : "";
var dayType = GetDayType(record.OtDate, weekendId, publicHolidays);
@ -1094,7 +1055,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
switch (dayType)
{
case "Normal Day":
result["ndAfter"] = toFixedOrEmpty(afterHrs); // Only after-office OT on normal days
result["ndAfter"] = toFixedOrEmpty(afterHrs);
break;
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)
}
}
// Add 'afterHrs' to 'odAfter' as it's separate "After Office" time
// If 'odAfter' only represents office hours > 8, you would need a new key like 'odSeparateAfterOffice'.
result["odAfter"] = toFixedOrEmpty(ConvertTimeToDecimal(result["odAfter"]) + afterHrs);
break;
@ -1134,7 +1094,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
result["rdAfter"] = toFixedOrEmpty(officeHrs); // Assign all to 'RD After' (for office hours beyond 8)
}
}
// Add 'afterHrs' to 'rdAfter'
result["rdAfter"] = toFixedOrEmpty(ConvertTimeToDecimal(result["rdAfter"]) + afterHrs);
break;
@ -1150,7 +1110,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
result["phAfter"] = toFixedOrEmpty(officeHrs); // Assign all to 'PH After' (for office hours beyond 8)
}
}
// Add 'afterHrs' to 'phAfter'
result["phAfter"] = toFixedOrEmpty(ConvertTimeToDecimal(result["phAfter"]) + afterHrs);
break;
}
@ -1210,16 +1170,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
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)
// 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
decimal orp = hrp * 8m;
var orp = orpFromHrp; // Use the ORP derived from the HRP passed to this function
var dayType = GetDayType(record.OtDate, weekendId, publicHolidays);
var officeRaw = CalculateOfficeOtHours(record);
@ -1273,22 +1225,22 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
private List<DateTime> GetAllDatesInMonth(DateTime month)
{
return Enumerable.Range(1, DateTime.DaysInMonth(month.Year, month.Month))
.Select(day => new DateTime(month.Year, month.Month, day))
.ToList();
.Select(day => new DateTime(month.Year, month.Month, day))
.ToList();
}
private string GetDayCellBackgroundColor(DateTime date, int? weekendId, List<DateTime> publicHolidays)
{
if (publicHolidays.Contains(date.Date))
return "#ffc0cb"; // Light red for Public Holiday
return "#ffc0cb";
var dayOfWeek = date.DayOfWeek;
// Assuming weekendId 1 for Friday/Saturday weekend, 2 for Saturday/Sunday
if ((weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) ||
(weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday)))
return "#add8e6"; // Light blue for Weekend
return "#add8e6";
return "#ffffff"; // White for Normal Day
return "#ffffff";
}
}
}

View File

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

View File

@ -4,7 +4,6 @@
}
<style>
/* Your existing styles (excluding the previously added .date-column specific styles) */
.table-container {
background-color: white;
border-radius: 15px;
@ -386,7 +385,7 @@
flexiHour: '',
stateId: null,
weekendId: null,
basicSalary: null // This will now hold the Basic Salary retrieved from API
basicSalary: null
},
isHoU: false,
expandedDescriptions: [],
@ -468,7 +467,6 @@
this.otRecords.forEach(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.afterBreak += record.afterBreak || 0;
@ -484,7 +482,6 @@
totals.phUnder8 += parseFloat(classified.phUnder8) || 0;
totals.phAfter += parseFloat(classified.phAfter) || 0;
// Ensure these values are numeric before summing
totals.totalOtHrs += parseFloat(this.calculateTotalOtHrs(record)) || 0;
totals.totalNdOd += parseFloat(this.calculateNdOdTotal(record)) || 0;
totals.totalRd += parseFloat(this.calculateRdTotal(record)) || 0;
@ -494,13 +491,16 @@
});
for (let key in totals) {
// Apply standard rounding (toNearestEven, like C# decimal.ToString("N2"))
// Only apply to total amounts, individual cell values are formatted as needed
if (key.endsWith('Hrs') || key.endsWith('Amt')) { // Target hour and amount totals
totals[key] = totals[key].toFixed(2);
} else if (key.endsWith('Break')) { // Break totals remain in minutes
// Already summed as integers/floats, format later if needed for display
} else { // Classified hours (ndAfter, odUnder4 etc.) should be toFixed(2)
if (key.endsWith('Hrs') || key.endsWith('Amt')) {
if (key === 'otAmt') {
totals[key] = Math.round(totals[key]).toFixed(2);
} else {
totals[key] = totals[key].toFixed(2);
}
} else if (key.endsWith('Break')) {
} else {
totals[key] = totals[key].toFixed(2);
}
}
@ -524,11 +524,7 @@
},
watch: {
'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) {
if (this.showAirStationDropdown) {
$('#airstationDropdown').val(newVal).trigger('change.select2');
@ -536,7 +532,7 @@
$('#marinestationDropdown').val(newVal).trigger('change.select2');
}
},
// Watch for changes in airStations and marineStations to update Select2 options
airStations: function() {
if (this.showAirStationDropdown) {
this.initSelect2('#airstationDropdown');
@ -549,9 +545,6 @@
}
},
methods: {
// REMOVED: Custom rounding function (roundUpToTwoDecimals) as C# uses standard rounding
// and we'll use toFixed(2) directly for consistency.
toggleDescription(index) {
this.expandedDescriptions[index] = !this.expandedDescriptions[index];
},
@ -564,10 +557,10 @@
const data = await res.json();
this.otRecords = data.records;
// Assuming data.userInfo.rate now holds the Basic Salary
this.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.currentEditRecord.statusId = parseInt(statusId);
@ -602,38 +595,33 @@
}
},
initSelect2(selector) {
// Destroy existing Select2 instance if any
if ($(selector).data('select2')) {
$(selector).select2('destroy');
}
// Initialize Select2
$(selector).select2({
dropdownParent: $('#editOtModal') // Ensure dropdown is within the modal boundaries
dropdownParent: $('#editOtModal')
}).on('change', (e) => {
// Update Vue data when Select2 value changes
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) {
$(selector).val(this.currentEditRecord.stationId).trigger('change.select2');
}
},
// HRP = ORP / 8
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';
return (orp / 8).toFixed(2);
},
// ORP = Basic Salary / 26
calculateOrp() {
const basicSalary = parseFloat(this.userInfo.basicSalary);
if (isNaN(basicSalary) || basicSalary <= 0) return 'N/A';
return (basicSalary / 26).toFixed(2);
},
// Basic Salary will now directly come from userInfo.basicSalary
calculateBasicSalary() {
const basicSalary = parseFloat(this.userInfo.basicSalary);
if (isNaN(basicSalary) || basicSalary <= 0) return 'N/A';
@ -661,20 +649,18 @@
const minutes = parseFloat(breakValue || 0);
if (isNaN(minutes) || minutes <= 0) return showZero ? '0:00' : '';
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')}`;
},
// New helper to format decimal hours into H:MM string for display columns
formatTimeFromDecimal(decimalHours) {
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 minutes = totalMinutes % 60;
return `${hours}:${minutes.toString().padStart(2, '0')}`;
},
// Renamed from convertTimeToDecimal to better reflect its purpose: parsing "HH:MM" to decimal hours
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(':');
if (parts.length !== 2) return 0;
@ -685,7 +671,6 @@
return hours + (minutes / 60);
},
// This function now returns decimal hours, consistent with C# `TotalHours`
calculateRawDuration(fromTime, toTime, breakMins) {
if (!fromTime || !toTime) return 0;
@ -699,25 +684,13 @@
const breakMinutes = parseFloat(breakMins || 0);
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) {
if (!record.officeFrom || !record.officeTo) return '';
if (record.dayType === 'Normal Day') { // Added as per PDF logic
if (record.dayType === 'Normal Day') {
return '';
}
const totalHours = this.calculateRawDuration(record.officeFrom, record.officeTo, record.officeBreak);
@ -733,9 +706,6 @@
classifyOt(record) {
const officeHrs = this.calculateRawDuration(record.officeFrom, record.officeTo, record.officeBreak);
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 result = {
@ -753,21 +723,19 @@
break;
case 'Off Day':
case 'Weekend': // Keep 'Weekend' here, it correctly maps to Off Day/Rest Day via getDayType
case 'Weekend':
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) {
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);
} else { // For officeHrs > 8. C# puts all officeHrs into odAfter
result.odAfter = toFixedOrEmpty(officeHrs); // Changed to match C# logic directly
} else {
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;
@ -775,28 +743,27 @@
if (officeHrs > 0) {
if (officeHrs <= 4) {
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);
} else { // For officeHrs > 8. C# puts all officeHrs into rdAfter
result.rdAfter = toFixedOrEmpty(officeHrs); // Changed to match C# logic directly
} else {
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;
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 <= 8) {
result.phUnder8 = toFixedOrEmpty(officeHrs);
} else { // officeHrs > 8
result.phAfter = toFixedOrEmpty(officeHrs); // Changed to match C# logic
} else {
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;
}
@ -811,7 +778,7 @@
if (!isNaN(value)) total += value;
}
return total.toFixed(2); // Keep .toFixed(2) for display
return total.toFixed(2);
},
calculateNdOdTotal(record) {
const classified = this.classifyOt(record);
@ -857,34 +824,22 @@
let amountAfter = 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) {
amountOffice = 0.5 * orp;
} else if (officeHours <= 8) {
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') {
// C# logic: `amountOffice = 2 * orp;` without conditions for PH Office Hours.
// This means it's a fixed 2 * ORP regardless of hours.
amountOffice = 2 * orp; // Match C# logic directly
amountOffice = 2 * orp;
}
}
if (afterOfficeHours > 0) {
switch (otType) {
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;
break;
case 'Rest Day':
@ -897,9 +852,8 @@
}
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) {
const timeValue = this.currentEditRecord[fieldName];
if (!timeValue) return;
@ -909,14 +863,12 @@
if (minutes >= 45 || minutes < 15) {
roundedMinutes = 0;
if (minutes >= 45) { // If minutes are 45-59, round up to next hour (00 minutes)
if (minutes >= 45) {
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`;
// No need to call validateTimeFields here, as it's called after the if condition
// and it will be called for all cases after rounding.
this.validateTimeFields(); // Still call here to update immediately if input changes
return; // Exit after setting
this.validateTimeFields();
return;
}
} else if (minutes >= 15 && minutes < 45) {
roundedMinutes = 30;
@ -924,10 +876,8 @@
this.currentEditRecord[fieldName] = `${String(hours).padStart(2, '0')}:${String(roundedMinutes).padStart(2, '0')}`;
// Trigger validation after rounding
this.validateTimeFields();
},
// --- END NEW METHOD ---
editRecord(id) {
if (this.hasApproverActedLocal) {
alert("You cannot edit this record as you have already approved/rejected this OT submission.");
@ -957,7 +907,6 @@
otDays: recordToEdit.otDays
};
// Initialize Select2 after Vue has updated the DOM with the new data
this.$nextTick(() => {
if (this.showAirStationDropdown) {
this.initSelect2('#airstationDropdown');
@ -975,7 +924,6 @@
async submitEdit() {
try {
// Validate time ranges
const hasOfficeHours = this.currentEditRecord.officeFrom && this.currentEditRecord.officeTo;
const hasAfterHours = this.currentEditRecord.afterFrom && this.currentEditRecord.afterTo;
@ -1053,23 +1001,20 @@
return 'Public Holiday';
}
// WeekendId 1: Friday (5) & Saturday (6)
// WeekendId 2: Saturday (6) & Sunday (0)
if (this.userState.weekendId === 1) {
if (dayOfWeek === 5) return 'Off Day'; // Friday
if (dayOfWeek === 6) return 'Rest Day'; // Saturday
if (dayOfWeek === 5) return 'Off Day';
if (dayOfWeek === 6) return 'Rest Day';
else return 'Normal Day';
} else if (this.userState.weekendId === 2) {
if (dayOfWeek === 6) return 'Off Day'; // Saturday
if (dayOfWeek === 0) return 'Rest Day'; // Sunday
if (dayOfWeek === 6) return 'Off Day';
if (dayOfWeek === 0) return 'Rest Day';
else return 'Normal Day';
}
return 'Normal Day'; // Default if not a public holiday or weekend
return 'Normal Day';
},
calculateModalTotalOtHrs() {
// Use calculateRawDuration which returns decimal hours
const officeHrsDecimal = this.calculateRawDuration(
this.currentEditRecord.officeFrom,
this.currentEditRecord.officeTo,
@ -1082,7 +1027,7 @@
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 remainingMinutes = totalMinutes % 60;
@ -1238,21 +1183,20 @@
});
},
validateTimeRangeForSubmission(fromTime, toTime, label) {
if (!fromTime || !toTime) return true; // Handled by outer checks
if (!fromTime || !toTime) return true;
const start = this.parseTime(fromTime);
const end = this.parseTime(toTime);
const minAllowedFromMinutesForMidnightTo = 16 * 60 + 30; // 4:30 PM
const maxAllowedFromMinutesForMidnightTo = 23 * 60 + 30; // 11:30 PM
const minAllowedFromMinutesForMidnightTo = 16 * 60 + 30;
const maxAllowedFromMinutesForMidnightTo = 23 * 60 + 30;
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") {
alert(`Invalid ${label} Time: 'From' and 'To' cannot both be 00:00 (midnight).`);
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) {
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;
@ -1264,7 +1208,6 @@
return true;
},
validateTimeFields() {
// This will validate the fields whenever they change
const hasOfficeHours = this.currentEditRecord.officeFrom && this.currentEditRecord.officeTo;
const hasAfterHours = this.currentEditRecord.afterFrom && this.currentEditRecord.afterTo;
@ -1288,8 +1231,6 @@
await this.fetchStations();
},
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) {
this.initSelect2('#airstationDropdown');
}
@ -1297,12 +1238,9 @@
this.initSelect2('#marinestationDropdown');
}
// Re-initialize Select2 when the modal is shown
var editModalElement = document.getElementById('editOtModal');
if (editModalElement) {
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) {
this.initSelect2('#airstationDropdown');
$('#airstationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');

View File

@ -13,7 +13,7 @@
<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>
</a>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -148,7 +148,6 @@
afterFrom: "",
afterTo: "",
afterBreak: 0,
// New properties for separate station selections
selectedAirStation: "", // Holds selected Air station ID
selectedMarineStation: "", // Holds selected Marine station ID
airStationList: [], // Stores stations for Air Department (DepartmentId = 2)
@ -163,10 +162,9 @@
userId: null,
userState: null,
publicHolidays: [],
// Keep user's actual department ID and admin flag for conditional rendering
userDepartmentId: null, // The department ID from the current user's profile
isUserAdmin: false, // True if the user is a SuperAdmin or SystemAdmin
departmentName: "", // This will be dynamic based on the user's main department for hints
isUserAdmin: false,
departmentName: "",
areUserSettingsComplete: false,
breakOptions: Array.from({ length: 15 }, (_, i) => {
const totalMinutes = i * 30;
@ -186,17 +184,12 @@
charCount() {
return this.otDescription.length;
},
// Determines if the Air Station dropdown should be shown
showAirDropdown() {
return this.isUserAdmin || this.userDepartmentId === 2;
},
// Determines if the Marine Station dropdown should be shown
showMarineDropdown() {
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() {
if (this.selectedAirStation) {
return parseInt(this.selectedAirStation);
@ -204,15 +197,13 @@
if (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() {
return this.showAirDropdown || this.showMarineDropdown;
}
},
watch: {
// Watch for changes in airStationList to re-initialize Select2 for Air
airStationList: {
handler() {
this.$nextTick(() => {
@ -222,11 +213,9 @@
selectElement.select2('destroy');
}
selectElement.select2({ theme: 'bootstrap4', placeholder: 'Select Air Station', allowClear: true });
// Set initial value if already selected
if (this.selectedAirStation) {
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) => {
this.selectedAirStation = $(event.currentTarget).val();
});
@ -235,7 +224,6 @@
},
deep: true
},
// Watch for changes in marineStationList to re-initialize Select2 for Marine
marineStationList: {
handler() {
this.$nextTick(() => {
@ -245,11 +233,9 @@
selectElement.select2('destroy');
}
selectElement.select2({ theme: 'bootstrap4', placeholder: 'Select Marine Station', allowClear: true });
// Set initial value if already selected
if (this.selectedMarineStation) {
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) => {
this.selectedMarineStation = $(event.currentTarget).val();
});
@ -258,14 +244,12 @@
},
deep: true
},
// Keep selectedAirStation in sync with Select2 if it's already rendered
selectedAirStation(newVal) {
const selectElement = $('#airStationDropdown');
if (selectElement.length && selectElement.val() !== newVal) {
selectElement.val(newVal).trigger('change.select2');
}
},
// Keep selectedMarineStation in sync with Select2 if it's already rendered
selectedMarineStation(newVal) {
const selectElement = $('#marineStationDropdown');
if (selectElement.length && selectElement.val() !== newVal) {
@ -278,18 +262,14 @@
if (this.userId) {
await this.checkUserSettings();
}
// Fetch stations for Air if the dropdown will be visible
if (this.showAirDropdown) {
await this.fetchStations(2, 'air'); // Department ID 2 for Air
}
// Fetch stations for Marine if the dropdown will be visible
if (this.showMarineDropdown) {
await this.fetchStations(3, 'marine'); // Department ID 3 for Marine
}
},
methods: {
// Modified fetchStations to populate specific lists based on listType
async fetchStations(departmentId, listType) {
try {
const response = await fetch(`/OvertimeAPI/GetStationsByDepartment?departmentId=${departmentId}`);
@ -319,11 +299,10 @@
const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin");
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.userDepartmentId === 2) {
this.departmentName = "PSTW AIR";
@ -333,7 +312,7 @@
this.departmentName = "";
}
} else {
this.departmentName = ""; // Admins see both, so this specific hint is removed for them
this.departmentName = "";
}
console.log("Fetched User:", this.currentUser);
@ -394,10 +373,9 @@
const totalMinutes = hours * 60 + minutes;
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 adjustedHour = Math.floor(roundedMinutes / 60) % 24; // Ensure hours wrap around 24
const adjustedHour = Math.floor(roundedMinutes / 60) % 24;
const adjustedMinute = roundedMinutes % 60;
return `${adjustedHour.toString().padStart(2, '0')}:${adjustedMinute.toString().padStart(2, '0')}`;
@ -446,7 +424,7 @@
}
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 minutes = diffMinutes % 60;
@ -472,7 +450,6 @@
return;
}
// --- Frontend Validation ---
if (!this.selectedDate) {
alert("Please select a date for the overtime.");
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
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 end = this.parseTime(toTime);
@ -542,8 +519,6 @@
if (hasAfterHours && !validateTimeRangeForSubmission(this.afterFrom, this.afterTo, 'After Office Hour')) {
return;
}
// --- End Frontend Validation ---
const requestData = {
otDate: this.selectedDate,
@ -553,7 +528,7 @@
afterFrom: this.afterFrom ? this.formatTime(this.afterFrom) : null,
afterTo: this.afterTo ? this.formatTime(this.afterTo) : 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(' '),
otDays: this.detectedDayType,
userId: this.userId,
@ -635,10 +610,9 @@
this.afterFrom = "";
this.afterTo = "";
this.afterBreak = 0;
this.selectedAirStation = ""; // Clear specific station selections
this.selectedMarineStation = ""; // Clear specific station selections
this.selectedAirStation = "";
this.selectedMarineStation = "";
// Clear Select2 for both dropdowns if they exist
const airSelect = $('#airStationDropdown');
if (airSelect.length && airSelect.data('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 class="table-layer">
<div class="white-box">
<!-- Simplified Filter Section -->
<div class="filter-container">
<div class="row mb-3">
@ -344,12 +345,12 @@
computed: {
columnCount() {
let count = 3; // Month/Year, SubmitDate, Edit History
let count = 3;
if (this.includeHou) count++;
if (this.includeHod) count++;
if (this.includeManager) count++;
if (this.includeHr) count++;
return count + 1; // File column
return count + 1;
},
monthYearOptions() {
@ -374,7 +375,6 @@
{ value: 'Rejected', text: 'Rejected' }
];
// Add combined status options if multiple approval levels exist
if ((this.includeHou && this.includeHod) ||
(this.includeHou && this.includeManager) ||
(this.includeHod && this.includeManager)) {
@ -390,7 +390,6 @@
filteredRecords() {
let filtered = [...this.otRecords];
// Apply filters
if (this.filters.monthYear) {
const [month, year] = this.filters.monthYear.split('/').map(Number);
filtered = filtered.filter(item =>
@ -434,7 +433,6 @@
}
}
// Apply sorting
filtered.sort((a, b) => {
let aValue, bValue;
@ -472,7 +470,6 @@
return 0;
});
// Update pagination
this.pagination.totalPages = Math.ceil(filtered.length / this.pagination.itemsPerPage);
this.pagination.currentPage = Math.min(this.pagination.currentPage, this.pagination.totalPages || 1);
@ -727,7 +724,6 @@
this.parsedHistory = [];
},
// Sorting methods
sortBy(field) {
if (this.sort.field === field) {
this.sort.order = this.sort.order === 'asc' ? 'desc' : 'asc';
@ -735,10 +731,9 @@
this.sort.field = field;
this.sort.order = 'asc';
}
this.pagination.currentPage = 1; // Reset to first page when sorting changes
this.pagination.currentPage = 1;
},
// Filter methods
resetFilters() {
this.filters = {
monthYear: '',
@ -747,7 +742,6 @@
this.pagination.currentPage = 1;
},
// Pagination methods
prevPage() {
if (this.pagination.currentPage > 1) {
this.pagination.currentPage--;

View File

@ -85,29 +85,24 @@ namespace PSTW_CentralSystem.Controllers.API
{
try
{
// Use a list to hold IDs of users with incomplete settings
var incompleteUserIds = new List<int>();
// Get all user IDs
var allUserIds = await _userManager.Users.Select(u => u.Id).ToListAsync();
foreach (var userId in allUserIds)
{
bool isIncomplete = false;
// Check HrUserSettingModel
var hrUserSetting = await _centralDbContext.Hrusersetting
.Where(h => h.UserId == userId)
.FirstOrDefaultAsync();
if (hrUserSetting == null)
{
// No HR user setting found for this user
isIncomplete = true;
}
else
{
// Check for null/empty fields in HrUserSettingModel
if (hrUserSetting.FlexiHourId == null || hrUserSetting.FlexiHourId == 0)
{
isIncomplete = true;
@ -122,19 +117,16 @@ namespace PSTW_CentralSystem.Controllers.API
}
}
// Check RateModel
var rateSetting = await _centralDbContext.Rates
.Where(r => r.UserId == userId)
.FirstOrDefaultAsync();
if (rateSetting == null)
{
// No Rate setting found for this user
isIncomplete = true;
}
else
{
// Check for default/empty RateValue (assuming 0.00 is considered empty)
if (rateSetting.RateValue == 0.00m)
{
isIncomplete = true;
@ -143,13 +135,12 @@ namespace PSTW_CentralSystem.Controllers.API
if (isIncomplete)
{
incompleteUserIds.Add(userId); // Add only the ID
incompleteUserIds.Add(userId);
}
}
if (incompleteUserIds.Any())
{
// Return just the count of incomplete users
return Ok(new { hasIncompleteSettings = true, numberOfIncompleteUsers = incompleteUserIds.Count });
}
else
@ -330,8 +321,7 @@ namespace PSTW_CentralSystem.Controllers.API
.FirstOrDefault() ?? "N/A"
}).ToList();
// Log this data to inspect the response
Console.WriteLine(JsonConvert.SerializeObject(result)); // Debugging log
Console.WriteLine(JsonConvert.SerializeObject(result));
return Ok(result);
@ -418,7 +408,7 @@ namespace PSTW_CentralSystem.Controllers.API
if (existingSetting != null)
{
existingSetting.StateId = update.StateId;
existingSetting.StateUpdate = DateTime.Now; // <-- ADD THIS LINE
existingSetting.StateUpdate = DateTime.Now;
}
else
{
@ -426,8 +416,7 @@ namespace PSTW_CentralSystem.Controllers.API
{
UserId = update.UserId,
StateId = update.StateId,
StateUpdate = DateTime.Now, // <-- ADD THIS LINE for new records
// Consider setting default for FlexiHourId and ApprovalFlowId if they are not nullable or have defaults
StateUpdate = DateTime.Now,
});
}
}
@ -580,7 +569,6 @@ namespace PSTW_CentralSystem.Controllers.API
return NotFound("Approval flow not found.");
}
// Update fields
existingFlow.ApprovalName = model.ApprovalName;
existingFlow.HoU = model.HoU;
existingFlow.HoD = model.HoD;
@ -594,7 +582,6 @@ namespace PSTW_CentralSystem.Controllers.API
}
catch (Exception ex)
{
// Log the exception
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." });
}
// Check if any users are using this approval flow
var usersWithThisFlow = await _centralDbContext.Hrusersetting
.AnyAsync(u => u.ApprovalFlowId == id);
@ -802,13 +788,11 @@ namespace PSTW_CentralSystem.Controllers.API
if (existingState != null)
{
// Corrected: Updating WeekendId
existingState.WeekendId = state.WeekendId;
_centralDbContext.States.Update(existingState);
}
else
{
// Ensure new states are added correctly
_centralDbContext.States.Add(new StateModel
{
StateId = state.StateId,
@ -874,15 +858,13 @@ namespace PSTW_CentralSystem.Controllers.API
#endregion
#region OtRegister
// Modified to accept departmentId as a query parameter
[HttpGet("GetStationsByDepartment")]
public async Task<IActionResult> GetStationsByDepartment([FromQuery] int? departmentId)
{
if (!departmentId.HasValue)
{
_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
@ -918,7 +900,6 @@ namespace PSTW_CentralSystem.Controllers.API
return BadRequest("User ID is required.");
}
// **Backend Validation for StationId based on user roles and department**
var user = await _userManager.FindByIdAsync(request.UserId.ToString());
if (user == null)
{
@ -930,22 +911,20 @@ namespace PSTW_CentralSystem.Controllers.API
var isSuperAdmin = userRoles.Contains("SuperAdmin");
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
.Include(u => u.Department)
.FirstOrDefaultAsync(u => u.Id == request.UserId);
int? userDepartmentId = userWithDepartment?.Department?.DepartmentId;
// Determine if a station is required and validate it
bool stationRequired = false;
if (userDepartmentId == 2 || userDepartmentId == 3) // For regular Air/Marine users
if (userDepartmentId == 2 || userDepartmentId == 3)
{
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))
@ -958,7 +937,6 @@ namespace PSTW_CentralSystem.Controllers.API
TimeSpan? afterFrom = string.IsNullOrEmpty(request.AfterFrom) ? (TimeSpan?)null : TimeSpan.Parse(request.AfterFrom);
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))
{
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.");
}
// Define allowed range for FROM when TO is 00:00 (midnight)
TimeSpan minAllowedFromMidnightTo = new TimeSpan(16, 30, 0); // 4:30 PM
TimeSpan maxAllowedFromMidnightTo = new TimeSpan(23, 30, 0); // 11:30 PM
TimeSpan minAllowedFromMidnightTo = new TimeSpan(16, 30, 0);
TimeSpan maxAllowedFromMidnightTo = new TimeSpan(23, 30, 0);
// Backend Validation for Office Hours
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)
{
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)
{
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).");
}
}
// Backend Validation for After Office Hours
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)
{
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)
{
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).");
}
@ -1024,10 +999,10 @@ namespace PSTW_CentralSystem.Controllers.API
OtDate = request.OtDate,
OfficeFrom = officeFrom,
OfficeTo = officeTo,
OfficeBreak = request.OfficeBreak, // Assuming it's nullable or default to 0
OfficeBreak = request.OfficeBreak,
AfterFrom = afterFrom,
AfterTo = afterTo,
AfterBreak = request.AfterBreak, // Assuming it's nullable or default to 0
AfterBreak = request.AfterBreak,
StationId = request.StationId,
OtDescription = request.OtDescription,
OtDays = request.OtDays,
@ -1142,7 +1117,7 @@ namespace PSTW_CentralSystem.Controllers.API
try
{
var records = _centralDbContext.Otregisters
.Include(o => o.Stations) // <--- ADD THIS LINE
.Include(o => o.Stations)
.Where(o => o.UserId == userId)
.OrderByDescending(o => o.OtDate)
.Select(o => new
@ -1233,7 +1208,6 @@ namespace PSTW_CentralSystem.Controllers.API
var relativePath = Path.Combine("Media", "Overtime", uniqueFileName).Replace("\\", "/");
// Create a NEW OtStatusModel for the resubmission
var statusModel = new OtStatusModel
{
UserId = userId,
@ -1248,9 +1222,8 @@ namespace PSTW_CentralSystem.Controllers.API
};
_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 monthEnd = monthStart.AddMonths(1);
@ -1266,7 +1239,6 @@ namespace PSTW_CentralSystem.Controllers.API
_centralDbContext.Otregisters.UpdateRange(otRecords);
await _centralDbContext.SaveChangesAsync();
// Update HrUserSetting with the NEW StatusId
var userSetting = _centralDbContext.Hrusersetting.FirstOrDefault(s => s.UserId == userId);
if (userSetting != null)
{
@ -1290,7 +1262,6 @@ namespace PSTW_CentralSystem.Controllers.API
{
try
{
// Get the latest OtStatus record for the user, month, and year
var latestStatus = _centralDbContext.Otstatus
.Where(s => s.UserId == userId && s.Month == month && s.Year == year)
.OrderByDescending(s => s.SubmitDate)
@ -1298,29 +1269,26 @@ namespace PSTW_CentralSystem.Controllers.API
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" ||
latestStatus.HodStatus?.ToLower() == "rejected" ||
latestStatus.ManagerStatus?.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" ||
latestStatus.HodStatus?.ToLower() == "pending" ||
latestStatus.ManagerStatus?.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); // Disable submit
return Ok(true);
}
catch (Exception ex)
{
@ -1376,14 +1344,13 @@ namespace PSTW_CentralSystem.Controllers.API
return Ok(record);
}
// New API Endpoint to Get User's Flexi Hour
[HttpGet("GetUserFlexiHour/{userId}")]
public async Task<IActionResult> GetUserFlexiHour(int userId)
{
try
{
var userSetting = await _centralDbContext.Hrusersetting
.Include(hs => hs.FlexiHour) // Include the FlexiHour navigation property
.Include(hs => hs.FlexiHour)
.FirstOrDefaultAsync(hs => hs.UserId == userId);
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 the FlexiHourModel object
return Ok(new { flexiHour = userSetting.FlexiHour });
}
catch (Exception ex)
@ -1401,6 +1367,7 @@ namespace PSTW_CentralSystem.Controllers.API
return StatusCode(500, new { message = "An error occurred while fetching flexi hour." });
}
}
[HttpPost]
[Route("UpdateOvertimeRecord")]
public IActionResult UpdateOvertimeRecord([FromBody] OtRegisterUpdateDto model)
@ -1414,7 +1381,6 @@ namespace PSTW_CentralSystem.Controllers.API
return BadRequest(ModelState);
}
// Validate time ranges
var timeValidationError = ValidateTimeRanges(model);
if (timeValidationError != null)
{
@ -1427,7 +1393,6 @@ namespace PSTW_CentralSystem.Controllers.API
return NotFound(new { message = "Overtime record not found." });
}
// Update properties
existing.OtDate = model.OtDate;
existing.OfficeFrom = TimeSpan.TryParse(model.OfficeFrom, out var officeFrom) ? officeFrom : 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? 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); // 4:30 PM
TimeSpan maxAllowedFromMidnightTo = new TimeSpan(23, 30, 0); // 11:30 PM
TimeSpan minAllowedFromMidnightTo = new TimeSpan(16, 30, 0);
TimeSpan maxAllowedFromMidnightTo = new TimeSpan(23, 30, 0);
// Validate office hours
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)
{
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)
{
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).";
}
}
// Validate after hours
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)
{
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)
{
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).";
}
@ -1529,7 +1491,6 @@ namespace PSTW_CentralSystem.Controllers.API
.Select(x => x.ApprovalFlowId)
.FirstOrDefault();
// Default values when no approval flow exists
bool includeHou = false;
bool includeHod = false;
bool includeManager = false;
@ -1594,7 +1555,6 @@ namespace PSTW_CentralSystem.Controllers.API
.ToList();
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>() });
var flowRoleMap = new Dictionary<int, string>();
@ -1606,8 +1566,6 @@ namespace PSTW_CentralSystem.Controllers.API
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
join setting in _centralDbContext.Hrusersetting on status.UserId equals setting.UserId
where setting.ApprovalFlowId.HasValue && flowRoleMap.Keys.Contains(setting.ApprovalFlowId.Value)
@ -1624,19 +1582,16 @@ namespace PSTW_CentralSystem.Controllers.API
var pendingMonthsAndYears = new HashSet<string>();
// We need to fetch the flows again here to get the full flow structure
// as `allRelevantOtEntries` only brings back ApprovalFlowId, not the full flow object
var allFlows = _centralDbContext.Approvalflow.ToList(); // Fetch all flows once
var allFlows = _centralDbContext.Approvalflow.ToList();
foreach (var entry in allRelevantOtEntries)
{
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;
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") &&
!(entry.HodStatus == "Rejected" || entry.ManagerStatus == "Rejected" || entry.HrStatus == "Rejected"))
{
@ -1666,14 +1621,10 @@ namespace PSTW_CentralSystem.Controllers.API
if (isPendingForCurrentUser)
{
// Format as MM/YYYY and add to the set
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
join user in _centralDbContext.Users on status.UserId equals user.Id
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];
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;
bool canApprove = false;
@ -1765,7 +1716,7 @@ namespace PSTW_CentralSystem.Controllers.API
{
Roles = distinctRoles.ToList(),
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)
.FirstOrDefault();
// Assuming 'RateValue' in your 'Rates' table actually stores the Basic Salary
var userBasicSalary = _centralDbContext.Rates
.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();
var userSetting = _centralDbContext.Hrusersetting
@ -1900,10 +1850,9 @@ namespace PSTW_CentralSystem.Controllers.API
if (userSetting?.Approvalflow == null)
return NotFound("Approval flow information not found for the user.");
// Public holidays for the table display (already fetched and formatted)
var publicHolidaysForTable = _centralDbContext.Holidays
.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();
var otRecords = _centralDbContext.Otregisters
@ -1916,17 +1865,17 @@ namespace PSTW_CentralSystem.Controllers.API
DateTime otDate = o.OtDate.Date;
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";
}
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";
else if (dayOfWeek == DayOfWeek.Saturday) dayType = "Rest 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";
else if (dayOfWeek == DayOfWeek.Sunday) dayType = "Rest Day";
@ -1934,7 +1883,7 @@ namespace PSTW_CentralSystem.Controllers.API
}
else
{
dayType = "Normal Day"; // Default if WeekendId is neither 1 nor 2
dayType = "Normal Day";
}
return new
@ -1953,7 +1902,7 @@ namespace PSTW_CentralSystem.Controllers.API
o.OtDays,
o.UserId,
Rate = userBasicSalary, // Pass the Basic Salary as 'Rate'
DayType = dayType, // This is for the table display
DayType = dayType,
};
})
.ToList();
@ -1977,7 +1926,7 @@ namespace PSTW_CentralSystem.Controllers.API
hasApproverActed = !string.IsNullOrEmpty(otStatus.HrStatus) && otStatus.HrStatus != "Pending";
break;
default:
hasApproverActed = true; // Default to disabled if role not matched or already acted
hasApproverActed = true;
break;
}
@ -1991,9 +1940,9 @@ namespace PSTW_CentralSystem.Controllers.API
filePath = otStatus.FilePath,
fullNameLower = user.FullName?.ToLower(),
flexiHour = userSetting.FlexiHour?.FlexiHour,
stateId = userSetting.State.StateId, // Ensure StateId is passed
weekendId = userSetting.State.WeekendId, // Ensure WeekendId is passed
rate = userBasicSalary // Send Basic Salary as 'rate' in userInfo as well for overall calculations
stateId = userSetting.State.StateId,
weekendId = userSetting.State.WeekendId,
rate = userBasicSalary
},
records = otRecords,
isHoU = userSetting.Approvalflow?.HoU == currentLoggedInUserId,
@ -2003,7 +1952,7 @@ namespace PSTW_CentralSystem.Controllers.API
otStatus.HrStatus,
approverRole,
hasApproverActed
// public holidays are now fetched separately in the frontend for the modal
};
return Ok(result);
@ -2333,7 +2282,7 @@ namespace PSTW_CentralSystem.Controllers.API
var currentLoggedInUserId = GetCurrentLoggedInUserId();
var userSetting = _centralDbContext.Hrusersetting
.Include(us => us.Approvalflow) // Include the ApprovalFlow to get approver IDs
.Include(us => us.Approvalflow)
.Include(us => us.FlexiHour)
.FirstOrDefault(us => us.UserId == user.Id);
@ -2345,33 +2294,27 @@ namespace PSTW_CentralSystem.Controllers.API
string? flexiHour = userSetting?.FlexiHour?.FlexiHour;
// --- Logic for collecting approved signatures ---
var approvedSignatures = new List<ApprovalSignatureData>();
// Get the approval flow for the user whose OT is being viewed
var approvalFlow = userSetting?.Approvalflow;
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
// 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.
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))
};
foreach (var stage in approvalStages
.Where(s => s.approverUser != null && s.statusField == "Approved" && s.submitDate.HasValue)
.OrderBy(s => s.submitDate))
{
byte[]? signatureImageBytes = null;
// Directly query StaffSign using approver's UserId
var staffSign = _centralDbContext.Staffsign
.FirstOrDefault(ss => ss.UserId == stage.approverId.Value);
@ -2379,11 +2322,10 @@ namespace PSTW_CentralSystem.Controllers.API
{
ApproverName = stage.approverUser.FullName,
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 stream = pdfGenerator.GenerateOvertimeTablePdf(otRecords, user, userRate, selectedMonth, logoImage, isHoU, flexiHour, approvedSignatures);
@ -2402,7 +2344,6 @@ namespace PSTW_CentralSystem.Controllers.API
if (user == null)
return NotFound("User not found.");
// Get the user's rate
var userRate = _centralDbContext.Rates
.Where(r => r.UserId == user.Id)
.OrderByDescending(r => r.LastUpdated)
@ -2436,7 +2377,6 @@ namespace PSTW_CentralSystem.Controllers.API
logoImage = System.IO.File.ReadAllBytes(logoPath);
}
// Get flexi hour if exists
var flexiHour = _centralDbContext.Hrusersetting
.Include(us => us.FlexiHour)
.FirstOrDefault(us => us.UserId == userId)?.FlexiHour?.FlexiHour;
@ -2461,7 +2401,6 @@ namespace PSTW_CentralSystem.Controllers.API
if (user == null)
return NotFound("User not found.");
// Get the user's rate
var userRate = _centralDbContext.Rates
.Where(r => r.UserId == user.Id)
.OrderByDescending(r => r.LastUpdated)
@ -2476,7 +2415,7 @@ namespace PSTW_CentralSystem.Controllers.API
? otRecords.First().OtDate
: DateTime.Now;
var currentLoggedInUserId = GetCurrentLoggedInUserId(); // Assuming this is defined elsewhere in your API controller
var currentLoggedInUserId = GetCurrentLoggedInUserId();
var userSetting = _centralDbContext.Hrusersetting
.Include(us => us.Approvalflow)
@ -2491,13 +2430,11 @@ namespace PSTW_CentralSystem.Controllers.API
string? flexiHour = userSetting?.FlexiHour?.FlexiHour;
// Pass _env to the OvertimeExcel constructor
var excelGenerator = new OvertimeExcel(_centralDbContext, _env);
var logoPath = Path.Combine(_env.WebRootPath, "images", "logo.jpg");
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);
string fileName = $"OvertimeReport_{user.FullName}_{DateTime.Now:yyyyMMdd}.xlsx";
@ -2516,20 +2453,17 @@ namespace PSTW_CentralSystem.Controllers.API
if (user == null)
return NotFound("User not found.");
// Get the user's rate
var userRate = _centralDbContext.Rates
.Where(r => r.UserId == user.Id)
.OrderByDescending(r => r.LastUpdated)
.FirstOrDefault()?.RateValue ?? 0m;
// Get the user's flexi hour setting
var userSetting = _centralDbContext.Hrusersetting
.Include(us => us.FlexiHour)
.FirstOrDefault(us => us.UserId == userId);
string flexiHour = userSetting?.FlexiHour?.FlexiHour;
// Get records for the selected month/year
var startDate = new DateTime(year, month, 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)
.ToList();
// Check if user is admin/PSTWAIR to show station column (logic remains same)
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.
bool isAdminUser = IsAdmin(user.Id);
// Corrected line: Pass _env to the OvertimeExcel constructor
var excelGenerator = new OvertimeExcel(_centralDbContext, _env);
// Get logo
var logoPath = Path.Combine(_env.WebRootPath, "images", "logo.jpg");
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(
otRecords,
user,
userRate,
startDate,
isHoU: false, // Set to false for regular user report
isHoU: false,
flexiHour: flexiHour,
logoImage: logoImage,
isSimplifiedExport: true // Set to true for the simplified report from OtRecords
isSimplifiedExport: true
);
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}"))
.WriteTo.Console()
.CreateLogger();
// Set default session to 30 minutes
builder.Services.AddSession(options =>
{
@ -54,6 +55,7 @@ internal class Program
mysqlOptions => mysqlOptions.CommandTimeout(120)
);
});
//builder.Services.AddDbContext<InventoryDBContext>(options =>
//{
// options.UseMySql(inventoryConnectionString, new MySqlServerVersion(new Version(8, 0, 39)),