Latest
This commit is contained in:
parent
5efe8e13c4
commit
efd69601ec
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@ -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
@ -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;
|
||||
|
||||
@ -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,7 +153,7 @@ 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
|
||||
.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,7 +194,7 @@ 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
|
||||
.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)
|
||||
@ -567,7 +556,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
||||
|
||||
public MemoryStream GenerateSimpleOvertimeTablePdf(
|
||||
List<OtRegisterModel> records,
|
||||
UserModel user, // User parameter added here
|
||||
UserModel user,
|
||||
decimal userRate,
|
||||
DateTime? selectedMonth = null,
|
||||
byte[]? logoImage = null,
|
||||
@ -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 =>
|
||||
@ -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,7 +743,6 @@ 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();
|
||||
|
||||
@ -812,7 +788,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
||||
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 ?? "");
|
||||
@ -891,16 +862,12 @@ 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();
|
||||
|
||||
@ -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)
|
||||
@ -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);
|
||||
@ -1280,15 +1232,15 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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: {
|
||||
|
||||
@ -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
|
||||
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')) { // 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)
|
||||
}
|
||||
} 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');
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@ -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.');
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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--;
|
||||
|
||||
@ -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,16 +2294,13 @@ 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)),
|
||||
@ -2363,15 +2309,12 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
(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.
|
||||
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";
|
||||
|
||||
@ -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)),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user