This commit is contained in:
Naz 2025-04-29 17:19:25 +08:00
parent 4248484877
commit 6d7dc52724
21 changed files with 1635 additions and 299 deletions

View File

@ -43,5 +43,9 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Controllers
return View();
}
public IActionResult HrUserSetting()
{
return View();
}
}
}

View File

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace PSTW_CentralSystem.Areas.OTcalculate.Models
{
public class FlexiHourModel
{
[Key]
public int FlexiHourId { get; set; }
public string FlexiHour { get; set; }
}
}

View File

@ -0,0 +1,30 @@
using PSTW_CentralSystem.Models;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PSTW_CentralSystem.Areas.OTcalculate.Models
{
public class HrUserSettingModel
{
[Key]
public int HrUserSettingId { get; set; }
public int UserId { get; set; }
[ForeignKey("UserId")]
public UserModel? User { get; set; }
public int? FlexiHourId { get; set; }
[ForeignKey("FlexiHourId")]
public FlexiHourModel? FlexiHour { get; set; }
public DateTime? FlexiHourUpdate { get; set; }
public DateTime? StateUpdate { get; set; }
public int? StateId { get; set; }
[ForeignKey("StateId")]
public StateModel? State { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace PSTW_CentralSystem.Areas.OTcalculate.Models
{
public class OvertimeSubmissionModel
{
public int Month { get; set; }
public int Year { get; set; }
public IFormFile File { get; set; }
}
}

View File

@ -0,0 +1,168 @@
using ClosedXML.Excel;
using System.IO;
using PSTW_CentralSystem.Models;
using PSTW_CentralSystem.Areas.OTcalculate.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{
public class OvertimeExcelService
{
public MemoryStream GenerateOvertimeExcel(
List<OtRegisterModel> records,
int departmentId,
string userFullName,
string departmentName,
int userStateId,
int weekendId,
List<CalendarModel> publicHolidays,
bool isAdminUser = false,
byte[]? logoImage = null // This parameter is missing in the call
)
{
var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Overtime Records");
int currentRow = 1;
// Add Header Information
if (!string.IsNullOrEmpty(userFullName))
{
worksheet.Cell(currentRow, 1).Value = $"Name: {userFullName}";
currentRow++;
}
if (!string.IsNullOrEmpty(departmentName))
{
worksheet.Cell(currentRow, 1).Value = $"Department: {departmentName}";
currentRow++;
}
currentRow++; // Add an empty row after header
// Header titles
var headers = new List<string>
{
"Day", "Date", "Office From", "Office To", "Office Break",
"After From", "After To", "After Break",
"Total OT", "Break Minutes", "Net OT"
};
if (departmentId == 2 || isAdminUser)
headers.Add("Station");
headers.Add("Description");
// Set header row
int headerRow = currentRow;
for (int i = 0; i < headers.Count; i++)
{
var cell = worksheet.Cell(headerRow, i + 1);
cell.Value = headers[i];
cell.Style.Font.Bold = true;
cell.Style.Fill.BackgroundColor = XLColor.LightGray;
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
cell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
cell.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
}
currentRow++;
// Fill data rows
foreach (var r in records)
{
TimeSpan totalOT = CalculateTotalOT(r);
int totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0);
TimeSpan netOT = totalOT - TimeSpan.FromMinutes(totalBreak);
int col = 1;
var dayCell = worksheet.Cell(currentRow, col++);
dayCell.Value = r.OtDate.ToString("ddd");
dayCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
dayCell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
dayCell.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
var dateCell = worksheet.Cell(currentRow, col++);
dateCell.Value = r.OtDate.ToString("yyyy-MM-dd");
dateCell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
dateCell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
dateCell.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
// Apply background color for weekends and public holidays
var dayOfWeek = r.OtDate.DayOfWeek;
bool isWeekend = (weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) ||
(weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday));
bool isPublicHoliday = publicHolidays.Any(h => h.HolidayDate.Date == r.OtDate.Date);
if (isPublicHoliday)
{
dayCell.Style.Fill.BackgroundColor = XLColor.Pink;
dateCell.Style.Fill.BackgroundColor = XLColor.Pink;
}
else if (isWeekend)
{
dayCell.Style.Fill.BackgroundColor = XLColor.LightBlue;
dateCell.Style.Fill.BackgroundColor = XLColor.LightBlue;
}
worksheet.Cell(currentRow, col++).Value = FormatTime(r.OfficeFrom);
worksheet.Cell(currentRow, col++).Value = FormatTime(r.OfficeTo);
worksheet.Cell(currentRow, col++).Value = r.OfficeBreak;
worksheet.Cell(currentRow, col++).Value = FormatTime(r.AfterFrom);
worksheet.Cell(currentRow, col++).Value = FormatTime(r.AfterTo);
worksheet.Cell(currentRow, col++).Value = r.AfterBreak;
worksheet.Cell(currentRow, col++).Value = totalOT.ToString(@"hh\:mm");
worksheet.Cell(currentRow, col++).Value = totalBreak;
worksheet.Cell(currentRow, col++).Value = netOT.ToString(@"hh\:mm");
if (departmentId == 2 || isAdminUser)
worksheet.Cell(currentRow, col++).Value = r.Stations?.StationName ?? "";
worksheet.Cell(currentRow, col++).Value = r.OtDescription ?? "";
// Apply border and alignment for the rest of the row
for (int i = headers.IndexOf("Office From") + 1; i <= headers.Count; i++)
{
var cell = worksheet.Cell(currentRow, i);
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
cell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
cell.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
}
currentRow++;
}
// Add Total row
int totalRow = currentRow;
worksheet.Cell(totalRow, 1).Value = "TOTAL";
worksheet.Cell(totalRow, 1).Style.Font.Bold = true;
worksheet.Cell(totalRow, headers.IndexOf("Total OT") + 1).Value = $"=SUM(I{headerRow + 1}:I{totalRow - 1})";
worksheet.Cell(totalRow, headers.IndexOf("Total OT") + 1).Style.Font.Bold = true;
worksheet.Cell(totalRow, headers.IndexOf("Break Minutes") + 1).Value = $"=SUM(J{headerRow + 1}:J{totalRow - 1})";
worksheet.Cell(totalRow, headers.IndexOf("Break Minutes") + 1).Style.Font.Bold = true;
worksheet.Cell(totalRow, headers.IndexOf("Net OT") + 1).Value = $"=SUM(K{headerRow + 1}:K{totalRow - 1})";
worksheet.Cell(totalRow, headers.IndexOf("Net OT") + 1).Style.Font.Bold = true;
worksheet.Columns().AdjustToContents();
var stream = new MemoryStream();
workbook.SaveAs(stream);
stream.Position = 0;
return stream;
}
private TimeSpan CalculateTotalOT(OtRegisterModel r)
{
TimeSpan office = (r.OfficeTo ?? TimeSpan.Zero) - (r.OfficeFrom ?? TimeSpan.Zero);
TimeSpan after = (r.AfterTo ?? TimeSpan.Zero) - (r.AfterFrom ?? TimeSpan.Zero);
if (after < TimeSpan.Zero)
after += TimeSpan.FromHours(24);
return office + after;
}
private string FormatTime(TimeSpan? time)
{
return time == null || time == TimeSpan.Zero ? "" : time.Value.ToString(@"hh\:mm");
}
}
}

View File

@ -5,22 +5,24 @@ using System.IO;
using System.Collections.Generic;
using PSTW_CentralSystem.Areas.OTcalculate.Models;
using System;
using System.Linq;
namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{
public class OvertimePdfService
{
public MemoryStream GenerateOvertimeTablePdf(
List<OtRegisterModel> records,
int departmentId,
string userFullName,
string departmentName,
byte[]? logoImage = null
)
public MemoryStream GenerateOvertimeTablePdf(
List<OtRegisterModel> records,
int departmentId,
string userFullName,
string departmentName,
int userStateId,
int weekendId,
List<CalendarModel> publicHolidays,
bool isAdminUser = false,
byte[]? logoImage = null)
{
records = records
.OrderBy(r => r.OtDate)
.ToList();
records = records.OrderBy(r => r.OtDate).ToList();
var stream = new MemoryStream();
@ -33,12 +35,11 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
page.Content().Column(column =>
{
// Header
column.Item().Row(row =>
{
row.RelativeItem(2).Column(col =>
{
if (logoImage != null)
{
col.Item().Container().Height(36).Image(logoImage, ImageScaling.FitArea);
@ -47,22 +48,22 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
col.Item().Text($"Name: {userFullName}").FontSize(9).SemiBold();
col.Item().Text($"Department: {departmentName}").FontSize(9).Italic();
col.Item().Text($"Overtime Record: { GetMonthYearString(records)}").FontSize(9).Italic();
col.Item().Text($"Overtime Record: {GetMonthYearString(records)}").FontSize(9).Italic();
});
row.RelativeItem(1).AlignRight().Text($"Generated: {DateTime.Now:dd MMM yyyy HH:mm}")
.FontSize(9).FontColor(Colors.Grey.Medium);
});
column.Item().PaddingVertical(10).LineHorizontal(0.5f).LineColor(Colors.Grey.Lighten2);
// Table section
// Table
column.Item().Table(table =>
{
// Columns
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(0.7f); // Days
columns.RelativeColumn(0.7f); // Day
columns.RelativeColumn(1.1f); // Date
columns.RelativeColumn(0.8f); // Office From
columns.RelativeColumn(0.8f); // Office To
@ -70,17 +71,17 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
columns.RelativeColumn(0.9f); // After From
columns.RelativeColumn(0.9f); // After To
columns.RelativeColumn(0.9f); // After Break
columns.RelativeColumn(); // Total OT
columns.RelativeColumn(); // Break Hours
columns.RelativeColumn(); // Net OT
if (departmentId == 2)
columns.RelativeColumn(); // Total OT
columns.RelativeColumn(); // Break Hours
columns.RelativeColumn(); // Net OT
if (departmentId == 2 || isAdminUser)
columns.RelativeColumn(); // Station
columns.RelativeColumn(2.7f); // Description
});
// Header
table.Header(header =>
{
// Row 1 — grouped headers
header.Cell().RowSpan(2).Background("#e0f7da").Border(0.25f).Padding(5).Text("Days").FontSize(9).Bold().AlignCenter();
header.Cell().RowSpan(2).Background("#d0ead2").Border(0.25f).Padding(5).Text("Date").FontSize(9).Bold().AlignCenter();
@ -89,15 +90,14 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(5).Text("Total OT\nHours").FontSize(9).Bold().AlignCenter();
header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(5).Text("Break Hours\n(min)").FontSize(9).Bold().AlignCenter();
header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(5).Text("Net OT Hours").FontSize(9).Bold().AlignCenter();
header.Cell().RowSpan(2).Background("#fdebd0").Border(0.25f).Padding(5).Text("Net OT\nHours").FontSize(9).Bold().AlignCenter();
if (departmentId == 2)
if (departmentId == 2 || isAdminUser)
header.Cell().RowSpan(2).Background("#d0f0ef").Border(0.25f).Padding(5).Text("Station").FontSize(9).Bold().AlignCenter();
header.Cell().RowSpan(2).Background("#e3f2fd").Border(0.25f).Padding(5).Text("Description").FontSize(9).Bold().AlignCenter();
// Row 2 — subheaders only for grouped columns
// Subheaders for Office/After
header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("From").FontSize(9).Bold().AlignCenter();
header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("To").FontSize(9).Bold().AlignCenter();
header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("Break (min)").FontSize(9).Bold().AlignCenter();
@ -107,8 +107,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("Break (min)").FontSize(9).Bold().AlignCenter();
});
// Data Rows
// Data
double totalOTSum = 0;
int totalBreakSum = 0;
TimeSpan totalNetOt = TimeSpan.Zero;
@ -117,89 +116,78 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
if (!records.Any())
{
uint colspan = (uint)(departmentId == 2 ? 13 : 12);
table.Cell().ColumnSpan(colspan)
.Border(0.5f)
.Padding(10)
.AlignCenter()
table.Cell().ColumnSpan(colspan).Border(0.5f).Padding(10).AlignCenter()
.Text("No records found for selected month and year.")
.FontSize(10)
.FontColor(Colors.Grey.Darken2)
.Italic();
.FontSize(10).FontColor(Colors.Grey.Darken2).Italic();
}
else
{
var groupedRecords = records.GroupBy(r => r.OtDate.Date);
foreach (var group in groupedRecords)
foreach (var r in records)
{
bool isFirstRow = true;
var totalOT = CalculateTotalOT(r);
var totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0);
var netOT = totalOT - TimeSpan.FromMinutes(totalBreak);
foreach (var r in group)
totalOTSum += totalOT.TotalHours;
totalBreakSum += totalBreak;
totalNetOt += netOT;
string rowBg = alternate ? "#f9f9f9" : "#ffffff";
alternate = !alternate;
string backgroundColor = GetDayCellBackgroundColor(r.OtDate, userStateId, publicHolidays, weekendId);
void AddCell(string value, bool center = true, string? bg = null)
{
var totalOT = CalculateTotalOT(r);
var totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0);
var netOT = totalOT - TimeSpan.FromMinutes(totalBreak);
totalOTSum += totalOT.TotalHours;
totalBreakSum += totalBreak;
totalNetOt += netOT;
string rowBg = alternate ? "#f9f9f9" : "#ffffff";
alternate = !alternate;
void AddCell(string value, bool alignLeft = false)
{
var text = table.Cell().Background(rowBg).Border(0.25f).Padding(5).Text(value).FontSize(9);
if (alignLeft)
text.AlignLeft();
else
text.AlignCenter();
}
AddCell(isFirstRow ? $"{r.OtDate:ddd}" : "");
AddCell(isFirstRow ? r.OtDate.ToString("dd/MM/yyyy") : "");
AddCell(FormatTime(r.OfficeFrom));
AddCell(FormatTime(r.OfficeTo));
AddCell($"{r.OfficeBreak ?? 0}");
AddCell(FormatTime(r.AfterFrom));
AddCell(FormatTime(r.AfterTo));
AddCell($"{r.AfterBreak ?? 0}");
AddCell($"{(int)totalOT.TotalHours} hr {totalOT.Minutes} min");
AddCell($"{totalBreak}");
AddCell($"{netOT.Hours} hr {netOT.Minutes} min");
if (departmentId == 2)
AddCell(r.Stations?.StationName ?? "N/A");
table.Cell().Background(rowBg).Border(0.25f).Padding(5).Text(r.OtDescription ?? "-").FontSize(9).WrapAnywhere().LineHeight(1.2f);
isFirstRow = false;
var text = table.Cell().Background(bg ?? rowBg).Border(0.25f).Padding(5).Text(value).FontSize(9);
if (center)
text.AlignCenter();
else
text.AlignLeft();
}
AddCell(r.OtDate.ToString("ddd"), true, backgroundColor);
AddCell(r.OtDate.ToString("dd/MM/yyyy"));
AddCell(FormatTime(r.OfficeFrom));
AddCell(FormatTime(r.OfficeTo));
AddCell(r.OfficeBreak > 0 ? $"{r.OfficeBreak}" : "");
AddCell(FormatTime(r.AfterFrom));
AddCell(FormatTime(r.AfterTo));
AddCell(r.AfterBreak > 0 ? $"{r.AfterBreak}" : "");
AddCell(totalOT > TimeSpan.Zero ? $"{totalOT:hh\\:mm}" : "");
AddCell(totalBreak > 0 ? $"{totalBreak}" : "");
AddCell(netOT > TimeSpan.Zero ? $"{netOT:hh\\:mm}" : "");
if (departmentId == 2 || isAdminUser)
AddCell(r.Stations?.StationName ?? "");
table.Cell().Background(rowBg).Border(0.25f).Padding(5)
.Text(r.OtDescription ?? "-").FontSize(9).WrapAnywhere().LineHeight(1.2f);
}
var totalOTTimeSpan = TimeSpan.FromHours(totalOTSum);
var totalBreakTimeSpan = TimeSpan.FromMinutes(totalBreakSum);
// Totals row
int totalCols = departmentId == 2 ? 13 : 12;
int spanCols = departmentId == 2 ? 7 : 6;
int spanCols = departmentId == 2 ? 9 : 8;
for (int i = 0; i < spanCols; i++)
table.Cell().Background("#d8d1f5").Border(0.80f).Padding(5).Text(i == 0 ? "TOTAL" : "").Bold().FontSize(9).AlignCenter();
table.Cell().Background("#d8d1f5").Border(0.80f).Padding(5)
.Text(i == 0 ? "TOTAL" : "").Bold().FontSize(9).AlignCenter();
table.Cell().Background("#d8d1f5").Border(0.80f).Padding(5).Text($"{(int)totalOTTimeSpan.TotalHours} hr {totalOTTimeSpan.Minutes} min").Bold().FontSize(9).AlignCenter();
table.Cell().Background("#d8d1f5").Border(0.80f).Padding(5).Text($"{(int)totalBreakTimeSpan.TotalHours} hr {totalBreakTimeSpan.Minutes} min").Bold().FontSize(9).AlignCenter();
table.Cell().Background("#d8d1f5").Border(0.80f).Padding(5).Text($"{(int)totalNetOt.TotalHours} hr {totalNetOt.Minutes} min").Bold().FontSize(9).AlignCenter();
if (departmentId == 2)
table.Cell().Background("#d8d1f5").Border(0.80f).Padding(5)
.Text($"{TimeSpan.FromHours(totalOTSum):hh\\:mm}").Bold().FontSize(9).AlignCenter();
table.Cell().Background("#d8d1f5").Border(0.80f).Padding(5)
.Text($"{TimeSpan.FromMinutes(totalBreakSum):hh\\:mm}").Bold().FontSize(9).AlignCenter();
table.Cell().Background("#d8d1f5").Border(0.80f).Padding(5)
.Text($"{totalNetOt:hh\\:mm}").Bold().FontSize(9).AlignCenter();
if (departmentId == 2 || isAdminUser)
table.Cell().Background("#d8d1f5").Border(0.80f);
else
table.Cell().Background("#d8d1f5").Border(0.80f);
table.Cell().Background("#d8d1f5").Border(0.80f);
table.Cell().Background("#d8d1f5").Border(0.80f);
}
});
});
});
}).GeneratePdf(stream);
@ -212,26 +200,38 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{
TimeSpan office = (r.OfficeTo ?? TimeSpan.Zero) - (r.OfficeFrom ?? TimeSpan.Zero);
TimeSpan after = (r.AfterTo ?? TimeSpan.Zero) - (r.AfterFrom ?? TimeSpan.Zero);
if (after < TimeSpan.Zero)
after += TimeSpan.FromHours(24);
return office + after;
}
private string FormatTime(TimeSpan? time)
{
return time?.ToString(@"hh\:mm") ?? "-";
if (time == null || time == TimeSpan.Zero)
return "";
return time.Value.ToString(@"hh\:mm");
}
private string GetMonthYearString(List<OtRegisterModel> records)
{
if (records == null || !records.Any())
return "No Data";
var firstDate = records.First().OtDate;
return $"{firstDate:MMMM yyyy}";
}
private string GetDayCellBackgroundColor(DateTime date, int userStateId, List<CalendarModel> publicHolidays, int weekendId)
{
if (publicHolidays.Any(h => h.HolidayDate.Date == date.Date))
return "#ffc0cb"; // Pink
var dayOfWeek = date.DayOfWeek;
if ((weekendId == 1 && (dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday)) ||
(weekendId == 2 && (dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday)))
return "#add8e6"; // Blue
return "#ffffff"; // Default
}
}
}

View File

@ -4,7 +4,6 @@
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<div class="container">
<div class="row justify-content-center">
<div class="col-6 col-md-6 col-lg-3">
@ -32,9 +31,21 @@
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="HrUserSetting">
<div class="box bg-megna text-center">
<h1 class="font-light text-white">
<i class="mdi mdi-account-settings"></i>
</h1>
<h6 class="text-white">User Setting</h6>
</div>
</a>
</div>
</div>
</div>
</div>
<div id="app">
<div class="container mt-3">

View File

@ -0,0 +1,382 @@
@{
ViewBag.Title = "User Settings";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<div class="container">
<div class="row justify-content-center">
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<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>
</div>
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="Calendar">
<div class="box bg-purple text-center">
<h1 class="font-light text-white"><i class="mdi mdi-calendar"></i></h1>
<h6 class="text-white">Calendar</h6>
</div>
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="HrUserSetting">
<div class="box bg-megna text-center">
<h1 class="font-light text-white"><i class="mdi mdi-account-settings"></i></h1>
<h6 class="text-white">User Setting</h6>
</div>
</a>
</div>
</div>
</div>
</div>
<div class="container mt-4" id="app">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" :class="{ 'bg-purple text-white': activeTab === 'flexi', 'bg-light text-dark': activeTab !== 'flexi' }"
style="border: 1px solid #ddd;" v-on:click="changeTab('flexi')">
Flexi Hour Settings
</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{ 'bg-purple text-white': activeTab === 'state', 'bg-light text-dark': activeTab !== 'state' }"
style="border: 1px solid #ddd;" v-on:click="changeTab('state')">
Region Update
</a>
</li>
</ul>
<div class="tab-content mt-3">
<div v-if="activeTab === 'flexi'" class="card shadow-sm">
<div class="card m-1">
<form v-on:submit.prevent="updateFlexiHours" data-aos="fade-right">
<div class="d-flex justify-content-center align-items-center mt-3">
<div class="card-body d-flex justify-content-center align-items-center gap-3">
<label for="flexiHour" class="mb-0">Flexi Hour</label>
<select id="flexiHour" class="form-select" v-model="selectedFlexiHourId" style="max-width: 150px;">
<option disabled value="">--Select--</option>
<option v-for="option in flexiHours" :value="option.flexiHourId">{{ option.flexiHour }}</option>
</select>
<button type="button" class="btn btn-danger" v-on:click="clearForm">Clear</button>
<button type="submit" class="btn btn-success">Update Flexi Hour</button>
</div>
</div>
<div class="card-body">
<table id="userDatatable" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Full Name</th>
<th>Department</th>
<th>Current Flexi Hour</th>
<th class="text-center">Select</th>
</tr>
</thead>
</table>
</div>
<div class="d-flex justify-content-center gap-2 my-3">
<button type="button" class="btn btn-danger" v-on:click="clearForm">Clear</button>
<button type="submit" class="btn btn-success">Update Flexi Hour</button>
</div>
</form>
</div>
</div>
<div v-if="activeTab === 'state'" class="card shadow-sm">
<div class="card-body d-flex justify-content-center align-items-center gap-3">
<label class="mb-0">Select State:</label>
<select class="form-select" v-model="selectedStateAll" style="max-width: 150px;">
<option disabled value="">--Select--</option>
<option v-for="state in stateList" :value="state.stateId">{{ state.stateName }}</option>
</select>
<button class="btn btn-danger" v-on:click="clearStateSelection">Cancel</button>
<button class="btn btn-success" v-on:click="saveState">Update State</button>
</div>
<div class="card-body">
<table id="stateUpdateTable" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Full Name</th>
<th>Department</th>
<th>Current State</th>
<th class="text-center">Select</th>
</tr>
</thead>
</table>
</div>
<div class="card-body text-center">
<button class="btn btn-danger" v-on:click="clearStateSelection">Clear Selection</button>
<button class="btn btn-success ms-3" v-on:click="saveState">Update User State</button>
</div>
</div>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
<script src="https://unpkg.com/vue@3.2.37/dist/vue.global.js"></script>
<script>
window.onload = function () {
const app = Vue.createApp({
data() {
return {
activeTab: 'flexi',
flexiHours: [],
selectedFlexiHourId: '',
selectedUsers: [],
userList: [],
stateList: [],
selectedStateAll: '',
selectedUsersState: [],
stateUserList: [],
userDatatable: null,
stateDatatable: null
};
},
mounted() {
console.log("Vue App Mounted Successfully");
this.fetchFlexiHours();
this.fetchStates();
this.changeTab('flexi'); // Initialize the default tab
},
methods: {
changeTab(tab) {
this.activeTab = tab;
if (tab === 'flexi') {
this.clearForm();
if (!this.userList.length) {
this.fetchUsers().then(() => this.initiateTable());
} else {
this.initiateTable();
}
} else if (tab === 'state') {
this.clearStateSelection();
if (!this.stateUserList.length) {
this.fetchUsersState().then(() => this.initiateStateTable());
}
else{
this.initiateStateTable();
}
}
},
async updateUserSettings(apiUrl, selectedUsers, selectedValue, successMessage, clearCallback, fetchCallback, valueKey) {
if (!selectedUsers.length || !selectedValue) {
alert("Please select at least one user and a value.");
return;
}
const payload = selectedUsers.map(userId => ({ UserId: userId, [valueKey]: selectedValue }));
try {
const response = await fetch(apiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (response.ok) {
alert(successMessage);
clearCallback(); // Clears form selections
await fetchCallback(); // Fetches the updated data
} else {
const errorData = await response.json();
alert(errorData.message || "Failed to update. Please try again.");
}
} catch (error) {
console.error("Error updating:", error);
}
},
async fetchFlexiHours() {
try {
const response = await fetch("/OvertimeAPI/GetFlexiHours");
this.flexiHours = await response.json();
console.log("Fetched flexi hours:", this.flexiHours);
} catch (error) {
console.error("Error fetching Flexi Hours:", error);
}
},
async fetchUsers() {
try {
const response = await fetch("/OvertimeAPI/GetUserFlexiHours", { method: "GET" });
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
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) {
console.error("Error fetching users:", error);
}
},
async fetchStates() {
try {
const response = await fetch("/OvertimeAPI/GetStatesName");
this.stateList = await response.json();
} catch (error) {
console.error("Error fetching States:", error);
}
},
async fetchUsersState() {
try {
const response = await fetch("/OvertimeAPI/GetUserStates", { method: "GET" });
const data = await response.json();
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) {
console.error("Error fetching users for State:", error);
}
},
initiateTable() {
if (this.userDatatable) {
this.userDatatable.destroy();
this.userDatatable = null;
}
const self = this;
this.$nextTick(() => {
self.userDatatable = $('#userDatatable').DataTable({
data: self.userList,
columns: [
{ data: "fullName" },
{ data: "departmentName" },
{ data: "flexiHour", defaultContent: "N/A" },
{
data: "userId",
className: "text-center",
render: data => `<input type='checkbox' class='user-checkbox' value='${data}'>`
}
],
destroy: true,
responsive: true
});
$('#userDatatable tbody').off('change').on('change', '.user-checkbox', (e) => {
const userId = $(e.target).val();
if (e.target.checked) {
self.selectedUsers.push(userId);
} else {
self.selectedUsers = self.selectedUsers.filter(id => id !== userId);
}
});
});
},
initiateStateTable() {
if (this.stateDatatable) {
this.stateDatatable.destroy();
this.stateDatatable = null;
}
const self = this;
this.$nextTick(() => {
self.stateDatatable = $('#stateUpdateTable').DataTable({
data: self.stateUserList,
columns: [
{ data: "fullName" },
{ data: "departmentName" },
{ data: "state", defaultContent: "N/A" },
{
data: "id",
className: "text-center",
render: data => `<input type='checkbox' class='state-checkbox' value='${data || ""}'>`
}
],
destroy: true,
responsive: true
});
$('#stateUpdateTable tbody').off('change').on('change', '.state-checkbox', (e) => {
const userId = $(e.target).val();
if (e.target.checked) {
self.selectedUsersState.push(userId);
} else {
self.selectedUsersState = self.selectedUsersState.filter(id => id !== userId);
}
});
});
},
clearForm() {
this.selectedFlexiHourId = '';
this.selectedUsers = [];
if (this.userDatatable) {
this.userDatatable.rows().every(function () {
$(this.node()).find('input[type="checkbox"]').prop('checked', false);
});
}
},
clearStateSelection() {
this.selectedUsersState = [];
if (this.stateDatatable) {
this.stateDatatable.rows().every(function () {
$(this.node()).find('input[type="checkbox"]').prop('checked', false);
});
}
},
async saveState() {
await this.updateUserSettings(
"/OvertimeAPI/UpdateUserStates",
this.selectedUsersState,
this.selectedStateAll,
"State updated successfully!",
this.clearStateSelection,
this.fetchUsersState, // This will fetch the updated state users after the update
"StateId"
);
// Fetch the updated state list to ensure the page reflects the new state data
await this.fetchUsersState(); // This fetch will make sure stateUserList is updated
this.initiateStateTable(); // Reinitialize the state table with new data
},
async updateFlexiHours() {
await this.updateUserSettings(
"/OvertimeAPI/UpdateUserFlexiHours",
this.selectedUsers,
this.selectedFlexiHourId,
"Flexi Hours updated successfully!",
this.clearForm,
this.fetchUsers,
"FlexiHourId"
);
}
}
});
app.mount('#app');
};
</script>
}

View File

@ -31,6 +31,19 @@
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="HrUserSetting">
<div class="box bg-megna text-center">
<h1 class="font-light text-white">
<i class="mdi mdi-account-settings"></i>
</h1>
<h6 class="text-white">User Setting</h6>
</div>
</a>
</div>
</div>
</div>
</div>
@ -56,7 +69,7 @@
<th>Full Name</th>
<th>Department</th>
<th>Current Rate</th>
<th class="text-center">Select Rate</th>
<th class="text-center">Select</th>
</tr>
</thead>
</table>
@ -174,7 +187,7 @@
"render": data => data ? parseFloat(data).toFixed(2) : 'N/A'
},
{
"title": "Select Rate",
"title": "Select",
"data": "id",
"className": "text-center",
"render": function (data)

View File

@ -4,7 +4,6 @@
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<div class="container">
<div class="row justify-content-center">
<div class="col-6 col-md-6 col-lg-3">
@ -32,6 +31,19 @@
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="HrUserSetting">
<div class="box bg-megna text-center">
<h1 class="font-light text-white">
<i class="mdi mdi-account-settings"></i>
</h1>
<h6 class="text-white">User Setting</h6>
</div>
</a>
</div>
</div>
</div>
</div>
@ -73,6 +85,28 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
<div class="card-header text-center" style="background-color: white;">
<label class="date-heading text-center">Flexi Hour Latest Update: {{ flexiHourUpdateDate || 'N/A' }}</label>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
<div class="card-header text-center" style="background-color: white;">
<label class="date-heading text-center">Region Latest Update: {{ regionUpdateDate || 'N/A' }}</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -90,6 +124,8 @@
return {
rateUpdateDate: null,
calendarUpdateDate: null,
flexiHourUpdateDate: null,
regionUpdateDate: null,
};
},
mounted() {
@ -106,6 +142,8 @@
const data = await response.json();
this.rateUpdateDate = data.rateUpdateDate;
this.calendarUpdateDate = data.calendarUpdateDate;
this.flexiHourUpdateDate = data.flexiHourUpdateDate;
this.regionUpdateDate = data.regionUpdateDate;
} catch (error) {
console.error(error);
}

View File

@ -9,31 +9,29 @@
<div class="card-body">
<div class="row">
<div class="col-md-7">
<!-- Overtime Date Input -->
<div class="mb-3">
<label class="form-label" for="dateInput">Date</label>
<input type="date" class="form-control" v-model="editForm.otDate"
v-on:input="calculateOTAndBreak">
<input type="date" class="form-control" v-model="editForm.otDate" v-on:input="calculateOTAndBreak">
</div>
<!-- OFFICE HOURS -->
<!-- Office Hours Section -->
<h6 class="fw-bold">OFFICE HOURS</h6>
<div class="d-flex gap-3 mb-3 align-items-end flex-wrap">
<div style="flex: 1;">
<label for="officeFrom">From</label>
<input type="time" class="form-control" v-model="editForm.officeFrom"
v-on:input="calculateOTAndBreak">
<input type="time" id="officeFrom" class="form-control" v-model="editForm.officeFrom" v-on:change="updateTime('officeFrom')">
</div>
<div style="flex: 1;">
<label for="officeTo">To</label>
<input type="time" id="officeTo" class="form-control" v-model="editForm.officeTo"
v-on:input="calculateOTAndBreak">
<input type="time" id="officeTo" class="form-control" v-model="editForm.officeTo" v-on:change="updateTime('officeTo')">
</div>
<div style="flex: 1;">
<label for="officeBreak">Break Hours (Minutes)</label>
<div class="d-flex">
<input type="number" id="officeBreak" class="form-control"
v-model="editForm.officeBreak"
v-on:input="calculateOTAndBreak" placeholder="e.g. 30">
<select id="officeBreak" class="form-control" v-model.number="editForm.officeBreak" v-on:change="calculateOTAndBreak">
<option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
</select>
<button class="btn btn-outline-danger ms-2" v-on:click="clearOfficeHours" title="Clear Office Hours">
<i class="bi bi-x-circle"></i>
</button>
@ -41,25 +39,23 @@
</div>
</div>
<!-- AFTER OFFICE HOURS -->
<!-- After Office Hours Section -->
<h6 class="fw-bold text-danger">AFTER OFFICE HOURS</h6>
<div class="d-flex gap-3 mb-3 align-items-end flex-wrap">
<div style="flex: 1;">
<label for="afterFrom">From</label>
<input type="time" id="afterFrom" class="form-control" v-model="editForm.afterFrom"
v-on:input="calculateOTAndBreak">
<input type="time" id="afterFrom" class="form-control" v-model="editForm.afterFrom" v-on:change="updateTime('afterFrom')">
</div>
<div style="flex: 1;">
<label for="afterTo">To</label>
<input type="time" id="afterTo" class="form-control" v-model="editForm.afterTo"
v-on:input="calculateOTAndBreak">
<input type="time" id="afterTo" class="form-control" v-model="editForm.afterTo" v-on:change="updateTime('afterTo')">
</div>
<div style="flex: 1;">
<label for="afterBreak">Break Hours (Minutes)</label>
<div class="d-flex">
<input type="number" id="afterBreak" class="form-control"
v-model="editForm.afterBreak"
v-on:input="calculateOTAndBreak" placeholder="e.g. 45">
<select id="afterBreak" class="form-control" v-model.number="editForm.afterBreak" v-on:change="calculateOTAndBreak">
<option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
</select>
<button class="btn btn-outline-danger ms-2" v-on:click="clearAfterHours" title="Clear After Office Hours">
<i class="bi bi-x-circle"></i>
</button>
@ -67,7 +63,7 @@
</div>
</div>
<!-- Air Station Dropdown (only for PSTW AIR) -->
<div class="mb-3" v-if="isPSTWAIR">
<label for="airstationDropdown">Air Station</label>
<select id="airstationDropdown" class="form-control" v-model="editForm.stationId">
@ -79,54 +75,34 @@
<small class="text-danger">*Only for PSTW AIR</small>
</div>
<!-- Work Description Input -->
<div class="mb-3">
<label for="otDescription">Work Brief Description</label>
<textarea id="otDescription" class="form-control"
v-model="editForm.otDescription"
v-on:input="limitCharCount"
placeholder="Describe the work done..."></textarea>
<small class="text-muted">
{{ charCount }} / 150 characters
</small>
<textarea id="otDescription" class="form-control" v-model="editForm.otDescription" v-on:input="limitCharCount" placeholder="Describe the work done..."></textarea>
<small class="text-muted">{{ charCount }} / 150 characters</small>
</div>
</div>
<div class="col-md-5">
<label class="mb-2">Day</label>
<div class="form-check">
<input class="form-check-input" type="radio" id="weekday" v-model="editForm.selectedDayType" value="Weekday">
<label class="form-check-label" for="weekday">Weekday</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="weekend" v-model="editForm.selectedDayType" value="Weekend">
<label class="form-check-label" for="weekend">Weekend</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="publicHoliday" v-model="editForm.selectedDayType"
value="Public Holiday">
<label class="form-check-label" for="publicHoliday">Public Holiday</label>
<!-- Overtime Hours and Break Section -->
<div class="col-md-5 mt-5">
<div class="mb-3 d-flex flex-column align-items-center">
<label for="detectedDayType">Day</label>
<input type="text" class="form-control text-center" v-model="editForm.otDays" readonly style="width: 200px;">
</div>
<div class="mb-3 d-flex flex-column align-items-center">
<label for="totalOTHours">Total OT Hours</label>
<input type="text" id="totalOTHours" class="form-control text-center" v-model="totalOTHours"
style="width: 200px;" readonly>
<input type="text" id="totalOTHours" class="form-control text-center" v-model="totalOTHours" style="width: 200px;" readonly>
</div>
<div class="mb-3 d-flex flex-column align-items-center">
<label for="totalBreakHours">Total Break Hours</label>
<input type="text" id="totalBreakHours" class="form-control text-center" v-model="totalBreakHours"
style="width: 200px;" readonly>
<input type="text" id="totalBreakHours" class="form-control text-center" v-model="totalBreakHours" style="width: 200px;" readonly>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-end mt-3">
<button class="btn btn-danger" v-on:click="goBack">Cancel</button>
<button class="btn btn-success ms-3" v-on:click="updateRecord">Update</button>
@ -135,6 +111,7 @@
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
@ -162,6 +139,20 @@
totalBreakHours: "0 hr 0 min",
currentUser: null,
isPSTWAIR: false,
userState: null, // To store the user's state information
publicHolidays: [], // To store public holidays
breakOptions: Array.from({ length: 15 }, (_, i) => {
const totalMinutes = i * 30;
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
let label = '';
if (hours > 0) label += `${hours} hour${hours > 1 ? 's' : ''}`;
if (minutes > 0) label += `${label ? ' ' : ''}${minutes} min`;
if (!label) label = '0 min';
return { label, value: totalMinutes };
}),
};
},
@ -170,6 +161,7 @@
return this.editForm.otDescription.length;
}
},
async mounted() {
const urlParams = new URLSearchParams(window.location.search);
const overtimeId = urlParams.get('overtimeId');
@ -177,19 +169,19 @@
await this.fetchOvertimeRecord(overtimeId);
}
await this.fetchUser();
await this.fetchUserAndRelatedData(); // Fetch user, state, and holidays
if (this.isPSTWAIR) {
await this.fetchStations();
}
},
methods: {
async fetchOvertimeRecord(id) {
try {
const res = await fetch(`/OvertimeAPI/GetOvertimeRecordById/${id}`);
if (res.ok) {
const data = await res.json();
this.populateForm(data); // Fill form fields with data
this.populateForm(data);
} else {
alert("Failed to fetch overtime record.");
}
@ -197,39 +189,42 @@
console.error("Fetch error:", err);
}
},
populateForm(record) {
this.editForm = {
...record,
otDate: record.otDate.slice(0, 10),
selectedDayType: record.otDays,
otDate: record.otDate ? record.otDate.slice(0, 10) : "",
// We will auto-detect the day, so we don't need to pre-fill otDays for auto-detection
};
this.calculateOTAndBreak();
this.updateDayType(); // Initial detection after loading data
},
async fetchStations() {
try {
const response = await fetch(`/OvertimeAPI/GetStationsByDepartment`);
if (!response.ok) throw new Error("Failed to fetch stations");
this.airstationList = await response.json();
} catch (error) {
console.error("Error fetching stations:", error);
}
},
async fetchUser() {
async fetchUserAndRelatedData() {
try {
const response = await fetch(`/IdentityAPI/GetUserInformation/`, { method: 'POST' });
if (response.ok) {
const data = await response.json();
this.currentUser = data?.userInfo || null;
this.userId = this.currentUser?.id || null;
this.editForm.userId = this.currentUser?.id || null;
console.log("Fetched User:", this.currentUser);
console.log("Dept ID:", this.currentUser?.departmentId);
const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin");
const isSystemAdmin = this.currentUser?.role?.includes("SystemAdmin");
const isDepartmentTwo = this.currentUser?.department?.departmentId === 2;
this.isPSTWAIR = isSuperAdmin || isSystemAdmin || isDepartmentTwo;
if (this.currentUser?.department?.departmentId === 2) {
this.isPSTWAIR = true;
console.log("User is PSTW AIR");
if (this.editForm.userId) {
await this.fetchUserStateAndHolidays();
}
} else {
console.error(`Failed to fetch user: ${response.statusText}`);
@ -238,12 +233,30 @@
console.error("Error fetching user:", error);
}
},
async fetchUserStateAndHolidays() {
try {
const response = await fetch(`/OvertimeAPI/GetUserStateAndHolidays/${this.editForm.userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user state and holidays: ${response.statusText}`);
}
const data = await response.json();
this.userState = data.state;
this.publicHolidays = data.publicHolidays;
this.updateDayType(); // Detect day type after loading data
} catch (error) {
console.error("Error fetching user state and holidays:", error);
this.editForm.otDays = "Weekday"; // Default if fetching fails
}
},
limitCharCount(event) {
if (this.editForm.otDescription && this.editForm.otDescription.length > 150) {
this.editForm.otDescription = this.editForm.otDescription.substring(0, 150);
event.preventDefault();
}
},
calculateOTAndBreak() {
let officeOT = this.calculateTimeDifference(
this.editForm.officeFrom,
@ -268,36 +281,97 @@
totalBreakMinutes = totalBreakMinutes % 60;
this.totalBreakHours = `${totalBreakHours} hr ${totalBreakMinutes} min`;
this.updateDayType(); // Update day type when times or date change
},
updateTime(fieldName) {
if (fieldName === 'officeFrom') {
this.editForm.officeFrom = this.roundToNearest30(this.editForm.officeFrom);
} else if (fieldName === 'officeTo') {
this.editForm.officeTo = this.roundToNearest30(this.editForm.officeTo);
} else if (fieldName === 'afterFrom') {
this.editForm.afterFrom = this.roundToNearest30(this.editForm.afterFrom);
} else if (fieldName === 'afterTo') {
this.editForm.afterTo = this.roundToNearest30(this.editForm.afterTo);
}
// Recalculate OT and break times after rounding, if necessary
this.calculateOTAndBreak();
},
roundToNearest30(timeStr) {
if (!timeStr) return timeStr;
const [hours, minutes] = timeStr.split(':').map(Number);
const roundedMinutes = minutes < 15 ? 0 : minutes < 45 ? 30 : 0;
const adjustedHour = minutes < 45 ? hours : (hours + 1) % 24;
return `${adjustedHour.toString().padStart(2, '0')}:${roundedMinutes.toString().padStart(2, '0')}`;
},
calculateTimeDifference(startTime, endTime, breakMinutes) {
if (!startTime || !endTime) {
return { hours: 0, minutes: 0 };
}
const start = this.parseTime(startTime);
const end = this.parseTime(endTime);
let diffMinutes = (end.hours * 60 + end.minutes) - (start.hours * 60 + start.minutes);
if (diffMinutes < 0) {
diffMinutes += 24 * 60;
}
diffMinutes -= breakMinutes || 0;
const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60;
return { hours, minutes };
},
parseTime(timeString) {
const [hours, minutes] = timeString.split(':').map(Number);
return { hours, minutes };
},
formatTime(timeString) {
if (!timeString) return null;
const [hours, minutes] = timeString.split(':');
return `${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}:00`; //HH:mm:ss format
},
handleDateChange() {
this.updateDayType();
this.calculateOTAndBreak();
},
updateDayType() {
if (!this.editForm.otDate || !this.userState) {
this.editForm.otDays = "";
return;
}
const selectedDateObj = new Date(this.editForm.otDate + "T00:00:00");
const dayOfWeek = selectedDateObj.getDay(); // 0 (Sunday) to 6 (Saturday)
const year = selectedDateObj.getFullYear();
const month = selectedDateObj.getMonth() + 1;
const day = selectedDateObj.getDate();
const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
if (this.publicHolidays.some(holiday => holiday.date === formattedDate)) {
this.editForm.otDays = "Public Holiday";
return;
}
const weekendId = this.userState.weekendId;
const isWeekend = (() => {
if (weekendId === 1) {
return dayOfWeek === 5 || dayOfWeek === 6; // Friday and Saturday
} else if (weekendId === 2) {
return dayOfWeek === 6 || dayOfWeek === 0; // Saturday and Sunday
} else {
return dayOfWeek === 0; // Default Sunday
}
})();
if (isWeekend) {
this.editForm.otDays = "Weekend";
return;
}
this.editForm.otDays = "Weekday";
},
async updateRecord() {
if (this.editForm.officeFrom && !this.editForm.officeTo) {
alert("Please enter a 'To' time for Office Hours.");
@ -338,32 +412,21 @@
}
}
const formData = new FormData();
formData.append("OvertimeId", this.editForm.overtimeId);
formData.append("OtDate", this.editForm.otDate);
formData.append("StationId", this.editForm.stationId || "");
formData.append("OtDescription", this.editForm.otDescription || "");
formData.append("OtDays", this.editForm.otDays); // Use the auto-detected day type
formData.append("OfficeFrom", this.formatTime(this.editForm.officeFrom) || "");
formData.append("OfficeTo", this.formatTime(this.editForm.officeTo) || "");
formData.append("AfterFrom", this.formatTime(this.editForm.afterFrom) || "");
formData.append("AfterTo", this.formatTime(this.editForm.afterTo) || "");
formData.append("officeBreak", this.editForm.officeBreak || 0);
formData.append("afterBreak", this.editForm.afterBreak || 0);
formData.append("userId", this.currentUser?.id);
try {
const formData = new FormData();
formData.append("OvertimeId", this.editForm.overtimeId);
formData.append("OtDate", this.editForm.otDate);
formData.append("StationId", this.editForm.stationId || "");
formData.append("OtDescription", this.editForm.otDescription || "");
formData.append("OtDays", this.editForm.selectedDayType);
formData.append("OfficeFrom", this.formatTime(this.editForm.officeFrom) || "");
formData.append("OfficeTo", this.formatTime(this.editForm.officeTo) || "");
formData.append("After Office From", this.formatTime(this.editForm.afterFrom) || "");
formData.append("After Office To", this.formatTime(this.editForm.afterTo) || "");
formData.append("officeBreak", this.editForm.officeBreak || 0);
formData.append("afterBreak", this.editForm.afterBreak || 0);
// Conditionally include nullable fields
if (this.editForm.afterFrom) {
formData.append("afterFrom", this.formatTime(this.editForm.afterFrom));
}
if (this.editForm.afterTo) {
formData.append("afterTo", this.formatTime(this.editForm.afterTo));
}
// Required field
formData.append("userId", this.currentUser?.id);
const response = await fetch(`/OvertimeAPI/UpdateOvertimeRecord`, {
method: 'POST',
body: formData
@ -371,7 +434,6 @@
if (response.ok) {
alert("Overtime record updated successfully!");
window.location.href = '/OTcalculate/Overtime/OtRecords';
} else {
alert("Failed to update overtime record.");

View File

@ -111,10 +111,10 @@
<td>{{ formatDate(record.otDate) }}</td>
<td>{{ formatTime(record.officeFrom) }}</td>
<td>{{ formatTime(record.officeTo) }}</td>
<td>{{ record.officeBreak }} min</td>
<td>{{ record.officeBreak }}</td>
<td>{{ formatTime(record.afterFrom) }}</td>
<td>{{ formatTime(record.afterTo) }}</td>
<td>{{ record.afterBreak }} min</td>
<td>{{ record.afterBreak }}</td>
<td>{{ formatHourMinute(calcTotalTime(record)) }}</td>
<td>{{ calcBreakTotal(record) }}</td>
<td>{{ formatHourMinute(calcNetHours(record)) }}</td>
@ -126,13 +126,20 @@
</div>
</td>
<td>
<button class="btn btn-light border rounded-circle me-1" title="Edit" v-on:click ="editRecord(index)">
<button class="btn btn-light border rounded-circle me-1"
title="Edit"
:disabled="hasSubmitted"
v-on:click="editRecord(index)">
<i class="bi bi-pencil-fill text-warning fs-5"></i>
</button>
<button class="btn btn-light border rounded-circle" title="Delete" v-on:click="deleteRecord(index)">
<button class="btn btn-light border rounded-circle"
title="Delete"
:disabled="hasSubmitted"
v-on:click="deleteRecord(index)">
<i class="bi bi-trash-fill text-danger fs-5"></i>
</button>
</td>
</tr>
<tr v-if="filteredRecords.length === 0">
<td :colspan="isPSTWAIR ? 14 : 13">No records found.</td>
@ -152,9 +159,26 @@
<div class="mt-3 d-flex flex-wrap gap-2">
<button class="btn btn-primary btn-sm" v-on:click="printPdf"><i class="bi bi-printer"></i> Print</button>
<button class="btn btn-dark btn-sm" v-on:click="downloadPdf"><i class="bi bi-download"></i> Save</button>
<button class="btn btn-success btn-sm" v-on:click=""><i class="bi bi-send"></i> Submit
</button>
<button class="btn btn-dark btn-sm" v-on:click="downloadPdf"><i class="bi bi-file-pdf"></i> Save</button>
<button class="btn btn-success btn-sm" v-on:click="downloadExcel(selectedMonth, selectedYear)"><i class="bi bi-file-earmark-excel"></i>Excel</button>
<button class="btn btn-success btn-sm" :disabled="hasSubmitted" v-on:click="openSubmitModal"><i class="bi bi-send"></i>{{ hasSubmitted ? 'Submitted' : 'Submit' }}</button>
</div>
<div class="modal fade" id="submitModal" tabindex="-1" aria-labelledby="submitModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="submitModalLabel">Submit Overtime</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="file" class="form-control" v-on:change ="handleFileUpload">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" v-on:click ="submitOvertime">Submit</button>
</div>
</div>
</div>
</div>
</div>
@ -162,6 +186,8 @@
@section Scripts {
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
const app = Vue.createApp({
data() {
@ -176,8 +202,18 @@
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
years: Array.from({ length: 10 }, (_, i) => currentYear - 5 + i),
expandedDescriptions: {},
selectedFile: null,
hasSubmitted: false,
};
},
watch: {
selectedMonth() {
this.checkSubmissionStatus();
},
selectedYear() {
this.checkSubmissionStatus();
}
},
computed: {
filteredRecords() {
return this.otRecords
@ -207,6 +243,7 @@
},
async mounted() {
await this.initUserAndRecords();
await this.checkSubmissionStatus();
},
methods: {
async initUserAndRecords() {
@ -217,16 +254,32 @@
},
async fetchUser() {
try {
const res = await fetch('/IdentityAPI/GetUserInformation', { method: 'POST' });
const data = await res.json();
const department = data.userInfo?.department;
this.userId = data.userInfo?.id;
const deptName = department?.departmentName?.toUpperCase?.() || '';
this.isPSTWAIR = deptName.includes("PSTW") && deptName.includes("AIR") && department?.departmentId === 2;
} catch (err) {
console.error("User fetch error:", err);
const response = await fetch(`/IdentityAPI/GetUserInformation/`, { method: 'POST' });
if (response.ok) {
const data = await response.json();
this.currentUser = data?.userInfo || null;
this.userId = this.currentUser?.id || null;
console.log("Fetched User:", this.currentUser);
console.log("Dept ID:", this.currentUser?.department?.departmentId);
console.log("Roles:", this.currentUser?.role);
const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin");
const isSystemAdmin = this.currentUser?.role?.includes("SystemAdmin");
const isDepartmentTwo = this.currentUser?.department?.departmentId === 2;
this.isPSTWAIR = isSuperAdmin || isSystemAdmin || isDepartmentTwo;
console.log("isPSTWAIR:", this.isPSTWAIR);
} else {
console.error(`Failed to fetch user: ${response.statusText}`);
}
} catch (error) {
console.error("Error fetching user:", error);
}
},
async fetchOtRecords() {
try {
const res = await fetch(`/OvertimeAPI/GetUserOvertimeRecords/${this.userId}`);
@ -235,6 +288,14 @@
console.error("Records fetch error:", err);
}
},
async checkSubmissionStatus() {
try {
const res = await fetch(`/OvertimeAPI/CheckOvertimeSubmitted/${this.userId}/${this.selectedMonth}/${this.selectedYear}`);
this.hasSubmitted = await res.json();
} catch (err) {
console.error("Failed to check submission status", err);
}
},
toggleDescription(index) {
this.expandedDescriptions[index] = !this.expandedDescriptions[index];
},
@ -243,7 +304,9 @@
return new Date(d).toLocaleDateString();
},
formatTime(t) {
return t ? t.slice(0, 5) : "-";
if (!t) return "-";
const [hours, minutes] = t.split(':').map(Number);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
},
getTimeDiff(from, to) {
if (!from || !to) return 0;
@ -272,7 +335,10 @@
};
},
formatHourMinute(timeObj) {
return timeObj ? `${timeObj.hours} h ${timeObj.minutes} m` : '-';
if (!timeObj) return "-";
const hours = timeObj.hours.toString().padStart(2, '0');
const minutes = timeObj.minutes.toString().padStart(2, '0');
return `${hours}:${minutes}`;
},
editRecord(index) {
const record = this.filteredRecords[index];
@ -290,26 +356,19 @@
alert("Error deleting record.");
}
},
printPdf() {
const today = new Date();
const month = this.selectedMonth;
const year = this.selectedYear;
fetch(`/OvertimeAPI/GenerateOvertimePdf?month=${month}&year=${year}`)
.then(response => response.blob())
.then(blob => {
const blobUrl = URL.createObjectURL(blob);
const printWindow = window.open(blobUrl, '_blank');
// Trigger print after window loads
printWindow.onload = () => {
printWindow.focus();
printWindow.print();
};
})
.catch(error => {
console.error("Error generating PDF:", error);
});
async printPdf() {
try {
const response = await fetch(`/OvertimeAPI/GenerateOvertimePdf?month=${this.selectedMonth}&year=${this.selectedYear}`);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const printWindow = window.open(blobUrl, '_blank');
printWindow.onload = () => {
printWindow.focus();
printWindow.print();
};
} catch (error) {
console.error("Error generating PDF:", error);
}
},
async downloadPdf() {
try {
@ -330,6 +389,51 @@
alert("An error occurred while generating the PDF.");
}
},
downloadExcel(month, year) {
window.open(`/OvertimeAPI/GenerateOvertimeExcel?month=${month}&year=${year}`, '_blank');
},
openSubmitModal() {
const modal = new bootstrap.Modal(document.getElementById('submitModal'));
modal.show();
},
handleFileUpload(event) {
this.selectedFile = event.target.files[0];
},
async submitOvertime() {
if (!this.selectedFile) {
alert("Please select a file to upload.");
return;
}
const formData = new FormData();
formData.append('Month', this.selectedMonth);
formData.append('Year', this.selectedYear);
formData.append('File', this.selectedFile);
try {
const res = await axios.post('/OvertimeAPI/SubmitOvertime', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
if (res.status === 200) {
alert('Overtime submitted successfully!');
const modalEl = document.getElementById('submitModal');
const modalInstance = bootstrap.Modal.getInstance(modalEl);
modalInstance.hide();
} else {
alert('Submission failed.');
}
} catch (error) {
console.error(error);
alert('An error occurred during submission.');
}
}
}
});
app.mount("#app");

View File

@ -13,7 +13,8 @@
<div class="mb-3">
<label class="form-label" for="dateInput">Date</label>
<input type="date" id="dateInput" class="form-control" v-model="selectedDate"
v-on:input="calculateOTAndBreak">
v-on:input="handleDateChange">
</div>
<h6 class="fw-bold">OFFICE HOURS</h6>
@ -21,17 +22,22 @@
<div class="col-4">
<label for="officeFrom">From</label>
<input type="time" id="officeFrom" class="form-control" v-model="officeFrom"
v-on:input="calculateOTAndBreak">
v-on:change ="officeFrom = roundToNearest30(officeFrom); calculateOTAndBreak()">
</div>
<div class="col-4">
<label for="officeTo">To</label>
<input type="time" id="officeTo" class="form-control" v-model="officeTo"
v-on:input="calculateOTAndBreak">
v-on:change ="officeTo = roundToNearest30(officeTo); calculateOTAndBreak()">
</div>
<div class="col-4">
<label for="officeBreak">Break Hours (Minutes)</label>
<input type="number" id="officeBreak" class="form-control" v-model="officeBreak"
v-on:input="calculateOTAndBreak" placeholder="e.g. 30">
<select id="officeBreak" class="form-control" v-model.number="officeBreak" v-on:change ="calculateOTAndBreak">
<option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</div>
</div>
@ -40,17 +46,20 @@
<div class="col-4">
<label for="afterFrom">From</label>
<input type="time" id="afterFrom" class="form-control" v-model="afterFrom"
v-on:input="calculateOTAndBreak">
v-on:change ="afterFrom = roundToNearest30(afterFrom); calculateOTAndBreak()">
</div>
<div class="col-4">
<label for="afterTo">To</label>
<input type="time" id="afterTo" class="form-control" v-model="afterTo"
v-on:input="calculateOTAndBreak">
v-on:change ="afterTo = roundToNearest30(afterTo); calculateOTAndBreak()">
</div>
<div class="col-4">
<label for="afterBreak">Break Hours (Minutes)</label>
<input type="number" id="afterBreak" class="form-control" v-model="afterBreak"
v-on:input="calculateOTAndBreak" placeholder="e.g. 45">
<select id="afterBreak" class="form-control" v-model.number="afterBreak" v-on:change ="calculateOTAndBreak">
<option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</div>
</div>
@ -79,23 +88,11 @@
</div>
<div class="col-md-5">
<label class="mb-2">Day</label>
<div class="form-check">
<input class="form-check-input" type="radio" id="weekday" v-model="selectedDayType" value="Weekday">
<label class="form-check-label" for="weekday">Weekday</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="weekend" v-model="selectedDayType" value="Weekend">
<label class="form-check-label" for="weekend">Weekend</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="publicHoliday" v-model="selectedDayType"
value="Public Holiday">
<label class="form-check-label" for="publicHoliday">Public Holiday</label>
<div class="col-md-5 mt-5">
<div class="mb-3 d-flex flex-column align-items-center">
<label for="detectedDayType">Day</label>
<input type="text" class="form-control text-center" v-model="detectedDayType" readonly
style="width: 200px;">
</div>
<div class="mb-3 d-flex flex-column align-items-center">
@ -138,14 +135,27 @@
selectedAirStation: "",
airstationList: [],
otDescription: "",
selectedDayType: "",
detectedDayType: "", // To display the auto-detected day type
totalOTHours: "0 hr 0 min",
totalBreakHours: "0 hr 0 min",
currentUser: null,
userId: null,
userState: null, // To store the user's state information
publicHolidays: [], // To store public holidays for the user's state and year
isPSTWAIR: false,
breakOptions: Array.from({ length: 15 }, (_, i) => {
const totalMinutes = i * 30;
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
};
let label = '';
if (hours > 0) label += `${hours} hour${hours > 1 ? 's' : ''}`;
if (minutes > 0) label += `${label ? ' ' : ''}${minutes} min`;
if (!label) label = '0 min';
return { label, value: totalMinutes };
}),
};
},
computed: {
charCount() {
@ -178,12 +188,25 @@
this.userId = this.currentUser?.id || null;
console.log("Fetched User:", this.currentUser);
console.log("Dept ID:", this.currentUser?.departmentId);
console.log("Dept ID:", this.currentUser?.department?.departmentId);
console.log("Roles:", this.currentUser?.role);
if (this.currentUser?.department?.departmentId === 2) {
this.isPSTWAIR = true;
console.log("User is PSTW AIR");
const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin");
const isSystemAdmin = this.currentUser?.role?.includes("SystemAdmin");
const isDepartmentTwo = this.currentUser?.department?.departmentId === 2;
this.isPSTWAIR = isSuperAdmin || isSystemAdmin || isDepartmentTwo;
console.log("isPSTWAIR:", this.isPSTWAIR);
if (this.isPSTWAIR) {
this.fetchStations();
}
// Fetch user's state and public holidays after fetching user info
if (this.userId) {
await this.fetchUserStateAndHolidays();
}
} else {
console.error(`Failed to fetch user: ${response.statusText}`);
}
@ -191,6 +214,28 @@
console.error("Error fetching user:", error);
}
},
async fetchUserStateAndHolidays() {
try {
const response = await fetch(`/OvertimeAPI/GetUserStateAndHolidays/${this.userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user state and holidays: ${response.statusText}`);
}
const data = await response.json();
this.userState = data.state;
this.publicHolidays = data.publicHolidays;
this.updateDayType(); // Initial detection after loading data
} catch (error) {
console.error("Error fetching user state and holidays:", error);
this.detectedDayType = "Weekday"; // Default if fetching fails
}
},
roundToNearest30(timeStr) {
if (!timeStr) return timeStr;
const [hours, minutes] = timeStr.split(':').map(Number);
const roundedMinutes = minutes < 15 ? 0 : minutes < 45 ? 30 : 0;
const adjustedHour = minutes < 45 ? hours : (hours + 1) % 24;
return `${adjustedHour.toString().padStart(2, '0')}:${roundedMinutes.toString().padStart(2, '0')}`;
},
limitCharCount(event) {
if (this.otDescription.length > 150) {
this.otDescription = this.otDescription.substring(0, 150);
@ -242,6 +287,10 @@
const [hours, minutes] = timeString.split(':');
return `${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}:00`; // HH:mm:ss format
},
handleDateChange() {
this.updateDayType();
this.calculateOTAndBreak();
},
async addOvertime() {
if (this.isPSTWAIR && !this.selectedAirStation) {
alert("Please fill in all required fields.");
@ -306,7 +355,7 @@
afterBreak: this.afterBreak || null, // Make this optional
stationId: this.isPSTWAIR ? parseInt(this.selectedAirStation) : null,
otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '),
otDays: this.selectedDayType,
otDays: this.detectedDayType, // Use the auto-detected day type
userId: this.userId
};
@ -332,7 +381,48 @@
alert("Failed to save overtime. Please check the console for errors.");
}
},
updateDayType() {
if (!this.selectedDate || !this.userState) {
this.detectedDayType = "";
return;
}
// Force parsing at midnight local time
const selectedDateObj = new Date(this.selectedDate + "T00:00:00");
const dayOfWeek = selectedDateObj.getDay(); // 0 (Sunday) to 6 (Saturday)
const year = selectedDateObj.getFullYear();
const month = selectedDateObj.getMonth() + 1;
const day = selectedDateObj.getDate();
const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
// 1. Check if it's a Public Holiday
if (this.publicHolidays.some(holiday => holiday.date === formattedDate)) {
this.detectedDayType = "Public Holiday";
return;
}
// 2. Check if it's a Weekend according to user's weekendId
const weekendId = this.userState.weekendId;
const isWeekend = (() => {
if (weekendId === 1) {
// WeekendId 1: Friday and Saturday
return dayOfWeek === 5 || dayOfWeek === 6;
} else if (weekendId === 2) {
// WeekendId 2: Saturday and Sunday
return dayOfWeek === 6 || dayOfWeek === 0;
} else {
return dayOfWeek === 0; // Default Sunday
}
})();
if (isWeekend) {
this.detectedDayType = "Weekend";
return;
}
// 3. Otherwise, it's a normal Weekday
this.detectedDayType = "Weekday";
},
clearForm() {
this.selectedDate = "";
@ -344,7 +434,7 @@
this.afterBreak = 0;
this.selectedAirStation = "";
this.otDescription = "";
this.selectedDayType = "";
this.detectedDayType = "";
this.totalOTHours = "0 hr 0 min";
this.totalBreakHours = "0 hr 0 min";
},

View File

@ -24,6 +24,7 @@ using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using PSTW_CentralSystem.Areas.OTcalculate.Services;
using Microsoft.AspNetCore.Hosting;
namespace PSTW_CentralSystem.Controllers.API
@ -37,14 +38,16 @@ namespace PSTW_CentralSystem.Controllers.API
private readonly UserManager<UserModel> _userManager;
private readonly OvertimePdfService _pdfService;
private readonly IWebHostEnvironment _env;
private readonly OvertimeExcelService _excelService;
public OvertimeAPI(ILogger<OvertimeAPI> logger, CentralSystemContext centralDbContext, UserManager<UserModel> userManager, OvertimePdfService pdfService, IWebHostEnvironment env)
public OvertimeAPI(ILogger<OvertimeAPI> logger, CentralSystemContext centralDbContext, UserManager<UserModel> userManager, OvertimePdfService pdfService, IWebHostEnvironment env, OvertimeExcelService excelService)
{
_logger = logger;
_centralDbContext = centralDbContext;
_userManager = userManager;
_pdfService = pdfService;
_env = env;
_excelService = excelService;
}
#region Settings
@ -56,11 +59,15 @@ namespace PSTW_CentralSystem.Controllers.API
var latestRateUpdate = _centralDbContext.Rates.OrderByDescending(r => r.LastUpdated).FirstOrDefault()?.LastUpdated;
var latestCalendarUpdate = _centralDbContext.Holidays.OrderByDescending(c => c.LastUpdated).FirstOrDefault()?.LastUpdated;
var latestFlexiHourUpdate = _centralDbContext.Hrusersetting.OrderByDescending(r => r.FlexiHourUpdate).FirstOrDefault()?.FlexiHourUpdate;
var latestRegionUpdate = _centralDbContext.Hrusersetting.OrderByDescending(c => c.StateUpdate).FirstOrDefault()?.StateUpdate;
var updateDates = new
{
rateUpdateDate = latestRateUpdate.HasValue ? latestRateUpdate.Value.ToString("dd MMMM yyyy") : null,
calendarUpdateDate = latestCalendarUpdate.HasValue ? latestCalendarUpdate.Value.ToString("dd MMMM yyyy") : null
calendarUpdateDate = latestCalendarUpdate.HasValue ? latestCalendarUpdate.Value.ToString("dd MMMM yyyy") : null,
flexiHourUpdateDate = latestFlexiHourUpdate.HasValue ? latestFlexiHourUpdate.Value.ToString("dd MMMM yyyy") : null,
regionUpdateDate = latestRegionUpdate.HasValue ? latestRegionUpdate.Value.ToString("dd MMMM yyyy") : null
};
return Json(updateDates);
@ -154,6 +161,179 @@ namespace PSTW_CentralSystem.Controllers.API
}
#endregion
#region FlexiHour State
private async Task UpdateOrInsertUserSettingAsync(int userId, int? flexiHourId = null, int? stateId = null)
{
var setting = await _centralDbContext.Hrusersetting
.FirstOrDefaultAsync(h => h.UserId == userId);
if (setting != null)
{
if (flexiHourId.HasValue)
{
setting.FlexiHourId = flexiHourId;
setting.FlexiHourUpdate = DateTime.Now;
}
if (stateId.HasValue)
{
setting.StateId = stateId;
setting.StateUpdate = DateTime.Now;
}
_centralDbContext.Hrusersetting.Update(setting);
}
else
{
var newSetting = new HrUserSettingModel
{
UserId = userId,
FlexiHourId = flexiHourId,
FlexiHourUpdate = flexiHourId.HasValue ? DateTime.Now : null,
StateId = stateId,
StateUpdate = stateId.HasValue ? DateTime.Now : null
};
_centralDbContext.Hrusersetting.Add(newSetting);
}
}
#endregion
#region FlexiHour
[HttpGet("GetFlexiHours")]
public IActionResult GetAllFlexiHours()
{
var flexiHours = _centralDbContext.Flexihour
.Select(f => new { f.FlexiHourId, f.FlexiHour })
.ToList();
return Ok(flexiHours);
}
[HttpGet("GetUserFlexiHours")]
public async Task<IActionResult> GetUserFlexiHours()
{
try
{
var users = await _centralDbContext.Users
.Include(u => u.Department)
.ToListAsync();
var hrUserSettings = await _centralDbContext.Hrusersetting
.Include(hr => hr.FlexiHour)
.ToListAsync();
var result = users.Select(u => new
{
UserId = u.Id,
FullName = u.FullName,
DepartmentName = u.Department != null ? u.Department.DepartmentName : "N/A",
FlexiHour = hrUserSettings
.Where(hr => hr.UserId == u.Id)
.Select(hr => hr.FlexiHour != null ? hr.FlexiHour.FlexiHour : "N/A")
.FirstOrDefault() ?? "N/A",
State = hrUserSettings
.Where(hr => hr.UserId == u.Id)
.Select(hr => hr.State != null ? hr.State.StateName : "N/A")
.FirstOrDefault() ?? "N/A"
}).ToList();
// Log this data to inspect the response
Console.WriteLine(JsonConvert.SerializeObject(result)); // Debugging log
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(new { message = ex.Message });
}
}
[HttpPost("UpdateUserFlexiHours")]
public async Task<IActionResult> UpdateUserFlexiHours([FromBody] List<HrUserSettingModel> updates)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
try
{
foreach (var update in updates)
{
await UpdateOrInsertUserSettingAsync(update.UserId, flexiHourId: update.FlexiHourId);
}
await _centralDbContext.SaveChangesAsync();
return Ok();
}
catch (Exception ex)
{
return BadRequest(new { message = ex.Message });
}
}
#endregion
#region State
[HttpGet("GetUserStates")]
public async Task<IActionResult> GetUserStates()
{
try
{
var users = await _centralDbContext.Users
.Include(u => u.Department)
.ToListAsync();
var hrUserSettings = await _centralDbContext.Hrusersetting
.Include(h => h.State)
.ToListAsync();
var result = users.Select(u =>
{
var hrSetting = hrUserSettings.FirstOrDefault(h => h.UserId == u.Id);
return new
{
u.Id,
u.FullName,
DepartmentName = u.Department != null ? u.Department.DepartmentName : "N/A",
State = hrSetting != null && hrSetting.State != null ? hrSetting.State.StateName : "N/A"
};
}).ToList();
return Ok(result);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpPost("UpdateUserStates")]
public async Task<IActionResult> UpdateUserStates([FromBody] List<HrUserSettingModel> updates)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
try
{
foreach (var update in updates)
{
await UpdateOrInsertUserSettingAsync(update.UserId, stateId: update.StateId);
}
await _centralDbContext.SaveChangesAsync();
return Ok();
}
catch (Exception ex)
{
return BadRequest(new { message = ex.Message });
}
}
#endregion
#region Calendar
[HttpGet("GetStatesName")]
public async Task<IActionResult> GetStatesName()
@ -451,6 +631,47 @@ namespace PSTW_CentralSystem.Controllers.API
return StatusCode(500, "An error occurred while saving overtime.");
}
}
[HttpGet("GetUserStateAndHolidays/{userId}")]
public async Task<IActionResult> GetUserStateAndHolidaysAsync(int userId)
{
try
{
var hrSettings = await _centralDbContext.Hrusersetting
.Include(h => h.State)
.ThenInclude(s => s.Weekends)
.Where(h => h.UserId == userId)
.FirstOrDefaultAsync();
if (hrSettings?.State == null)
{
return Ok(new { state = (object)null, publicHolidays = new List<object>() }); // Or handle no state differently
}
// Fetch public holidays for the user's state and the current year (example)
var publicHolidays = await _centralDbContext.Holidays
.Where(ph => ph.StateId == hrSettings.StateId && ph.HolidayDate.Year == DateTime.Now.Year)
.Select(ph => new { Date = ph.HolidayDate.ToString("yyyy-MM-dd") })
.ToListAsync();
return Ok(new
{
state = new
{
stateId = hrSettings.StateId,
stateName = hrSettings.State?.StateName,
weekendDay = hrSettings.State?.Weekends?.Day,
weekendId = hrSettings.State?.WeekendId
},
publicHolidays
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching user state and public holidays.");
return StatusCode(500, "An error occurred while fetching user state and public holidays.");
}
}
#endregion
#region Ot Records
@ -538,6 +759,22 @@ namespace PSTW_CentralSystem.Controllers.API
.Where(o => o.UserId == userId && o.OtDate.Month == month && o.OtDate.Year == year)
.ToList();
var hrUserSetting = _centralDbContext.Hrusersetting.FirstOrDefault(h => h.UserId == userId);
// StateId
var userStateId = hrUserSetting?.StateId ?? 0;
// WeekendId
var weekendId = _centralDbContext.States
.Where(s => s.StateId == userStateId)
.Select(s => s.WeekendId)
.FirstOrDefault() ?? 2; // Default Sat-Sun if null
// Public Holidays
var publicHolidays = _centralDbContext.Holidays
.Where(c => c.StateId == userStateId)
.ToList();
// Step 1: Generate all days of the month
var daysInMonth = DateTime.DaysInMonth(year, month);
var allDays = Enumerable.Range(1, daysInMonth)
@ -573,18 +810,188 @@ namespace PSTW_CentralSystem.Controllers.API
byte[]? logoImage = null;
var logoPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images", "logo.jpg");
if (System.IO.File.Exists(logoPath))
logoImage = System.IO.File.ReadAllBytes(logoPath);
logoImage = System.IO.File.Exists(logoPath) ? System.IO.File.ReadAllBytes(logoPath) : null;
var stream = _pdfService.GenerateOvertimeTablePdf(
mergedRecords,
departmentId,
fullName,
departmentName,
userStateId,
weekendId,
publicHolidays,
isAdminUser: IsAdmin(userId),
logoImage
);
var stream = _pdfService.GenerateOvertimeTablePdf(mergedRecords, departmentId, fullName, departmentName, logoImage);
return File(stream, "application/pdf", $"OvertimeRecords_{year}_{month}.pdf");
}
private bool IsAdmin(int userId)
{
var userRoles = _centralDbContext.UserRoles
.Where(ur => ur.UserId == userId)
.Join(_centralDbContext.Roles, ur => ur.RoleId, r => r.Id, (ur, r) => r.Name)
.ToList();
return userRoles.Any(role => role.Contains("SuperAdmin") || role.Contains("SystemAdmin"));
}
[HttpGet("GenerateOvertimeExcel")]
public IActionResult GenerateOvertimeExcel(int month, int year)
{
var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId))
return Unauthorized();
var user = _centralDbContext.Users
.Include(u => u.Department)
.FirstOrDefault(u => u.Id == userId);
if (user == null)
return NotFound("User not found.");
var fullName = user.FullName;
var departmentId = user.departmentId ?? 0;
var departmentName = user.Department?.DepartmentName ?? "N/A";
var records = _centralDbContext.Otregisters
.Include(o => o.Stations)
.Where(o => o.UserId == userId && o.OtDate.Month == month && o.OtDate.Year == year)
.ToList();
var hrUserSetting = _centralDbContext.Hrusersetting.FirstOrDefault(h => h.UserId == userId);
var userStateId = hrUserSetting?.StateId ?? 0;
var weekendId = _centralDbContext.States
.Where(s => s.StateId == userStateId)
.Select(s => s.WeekendId)
.FirstOrDefault() ?? 2;
var publicHolidays = _centralDbContext.Holidays
.Where(c => c.StateId == userStateId)
.ToList();
var daysInMonth = DateTime.DaysInMonth(year, month);
var allDays = Enumerable.Range(1, daysInMonth)
.Select(day => new DateTime(year, month, day))
.ToList();
var mergedRecords = new List<OtRegisterModel>();
foreach (var date in allDays)
{
var dayRecords = records
.Where(r => r.OtDate.Date == date.Date)
.OrderBy(r => r.OfficeFrom)
.ToList();
if (dayRecords.Any())
{
mergedRecords.AddRange(dayRecords);
}
else
{
mergedRecords.Add(new OtRegisterModel
{
OtDate = date,
OtDays = date.DayOfWeek.ToString(),
OtDescription = "",
});
}
}
var stream = _excelService.GenerateOvertimeExcel(
mergedRecords,
departmentId,
fullName,
departmentName,
userStateId,
weekendId,
publicHolidays,
isAdminUser: IsAdmin(userId),
logoImage: null
);
return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"OvertimeRecords_{year}_{month}.xlsx");
}
[HttpPost("SubmitOvertime")]
public async Task<IActionResult> SubmitOvertime([FromForm] OvertimeSubmissionModel model)
{
if (model.File == null || model.File.Length == 0)
return BadRequest("No file uploaded.");
// Get userId from the login token
var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId))
return Unauthorized();
try
{
var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Media", "Overtime");
if (!Directory.Exists(uploadsFolder))
Directory.CreateDirectory(uploadsFolder);
var uniqueFileName = $"{Guid.NewGuid()}_{model.File.FileName}";
var filePath = Path.Combine(uploadsFolder, uniqueFileName);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await model.File.CopyToAsync(fileStream);
}
var relativePath = Path.Combine("Media", "Overtime", uniqueFileName).Replace("\\", "/");
// Check if record exists for same month/year/user
var existingStatus = _centralDbContext.Otstatus.FirstOrDefault(x => x.UserId == userId && x.Month == model.Month && x.Year == model.Year);
if (existingStatus != null)
{
existingStatus.FilePath = relativePath;
existingStatus.SubmitDate = DateTime.Now;
_centralDbContext.Otstatus.Update(existingStatus);
}
else
{
var newStatus = new OtStatusModel
{
UserId = userId,
Month = model.Month,
Year = model.Year,
FilePath = relativePath,
SubmitDate = DateTime.Now,
HodStatus = "Pending",
HrStatus = "Pending"
};
_centralDbContext.Otstatus.Add(newStatus);
}
await _centralDbContext.SaveChangesAsync();
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to submit overtime.");
return StatusCode(500, "An error occurred while submitting overtime.");
}
}
[HttpGet("CheckOvertimeSubmitted/{userId}/{month}/{year}")]
public async Task<IActionResult> CheckOvertimeSubmitted(string userId, int month, int year)
{
if (!int.TryParse(userId, out int parsedUserId))
return BadRequest("Invalid userId.");
var isSubmitted = await _centralDbContext.Otstatus
.AnyAsync(s => s.UserId == parsedUserId && s.Month == month && s.Year == year);
return Ok(isSubmitted);
}
#endregion
#region Ot Edit
[HttpGet("GetOvertimeRecordById/{id}")]
public async Task<IActionResult> GetOvertimeRecordById(int id)
@ -646,7 +1053,6 @@ namespace PSTW_CentralSystem.Controllers.API
#region OtStatus
#endregion
}

View File

@ -104,7 +104,9 @@ namespace PSTW_CentralSystem.DBContext
public DbSet<StateModel> States { get; set; }
public DbSet<WeekendModel> Weekends { get; set; }
public DbSet<OtRegisterModel> Otregisters { get; set; }
public DbSet<OtStatusModel> OtStatus { get; set; }
public DbSet<OtStatusModel> Otstatus { get; set; }
public DbSet<HrUserSettingModel> Hrusersetting { get; set; }
public DbSet<FlexiHourModel> Flexihour { get; set; }
}
}

View File

@ -12,6 +12,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.104.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />

View File

@ -22,6 +22,8 @@ internal class Program
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddScoped<OvertimePdfService>();
builder.Services.AddTransient<OvertimeExcelService>();
Log.Logger = new LoggerConfiguration()

View File

@ -502,26 +502,6 @@
</ul>
</li>
<li class="sidebar-item">
<a class="sidebar-link has-arrow waves-effect waves-dark"
href="javascript:void(0)"
aria-expanded="false">
<i class="mdi mdi-receipt"></i><span class="hide-menu">HR Dashboard</span>
</a>
<ul aria-expanded="false" class="collapse first-level">
<li class="sidebar-item">
<a class="sidebar-link waves-effect waves-dark sidebar-link" asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="OtApproval" aria-expanded="false">
<i class="mdi mdi-view-dashboard"></i><span class="hide-menu">OT Approval</span>
</a>
</li>
<li class="sidebar-item">
<a class="sidebar-link waves-effect waves-dark sidebar-link" asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="Settings" aria-expanded="false">
<i class="mdi mdi-view-dashboard"></i><span class="hide-menu">Settings</span>
</a>
</li>
</ul>
</li>
<li class="sidebar-item">
<a class="sidebar-link has-arrow waves-effect waves-dark"
href="javascript:void(0)"
@ -575,6 +555,26 @@
</li>
</ul>
</li>
<li class="sidebar-item">
<a class="sidebar-link has-arrow waves-effect waves-dark"
href="javascript:void(0)"
aria-expanded="false">
<i class="mdi mdi-receipt"></i><span class="hide-menu">HR Dashboard</span>
</a>
<ul aria-expanded="false" class="collapse first-level">
<li class="sidebar-item">
<a class="sidebar-link waves-effect waves-dark sidebar-link" asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="OtApproval" aria-expanded="false">
<i class="mdi mdi-view-dashboard"></i><span class="hide-menu">OT Approval</span>
</a>
</li>
<li class="sidebar-item">
<a class="sidebar-link waves-effect waves-dark sidebar-link" asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="Settings" aria-expanded="false">
<i class="mdi mdi-view-dashboard"></i><span class="hide-menu">Settings</span>
</a>
</li>
</ul>
</li>
<!-- <li class="sidebar-item">
<a class="sidebar-link waves-effect waves-dark sidebar-link"
href="charts.html"