diff --git a/Areas/OTcalculate/Controllers/HodDashboardController.cs b/Areas/OTcalculate/Controllers/HodDashboardController.cs index 0f1f920..ff10d85 100644 --- a/Areas/OTcalculate/Controllers/HodDashboardController.cs +++ b/Areas/OTcalculate/Controllers/HodDashboardController.cs @@ -12,9 +12,9 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Controllers { [Area("OTcalculate")] [Authorize] - public class HodDashboardController : Controller + public class HouDashboardController : Controller { - public IActionResult HodApproval() + public IActionResult HouApproval() { return View(); } diff --git a/Areas/OTcalculate/Controllers/HouDashboardController.cs b/Areas/OTcalculate/Controllers/HouDashboardController.cs new file mode 100644 index 0000000..0f1f920 --- /dev/null +++ b/Areas/OTcalculate/Controllers/HouDashboardController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using PSTW_CentralSystem.Areas.OTcalculate.Models; +using PSTW_CentralSystem.DBContext; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace PSTW_CentralSystem.Areas.OTcalculate.Controllers +{ + [Area("OTcalculate")] + [Authorize] + public class HodDashboardController : Controller + { + public IActionResult HodApproval() + { + return View(); + } + public IActionResult OtReview() + { + return View(); + } + } +} diff --git a/Areas/OTcalculate/Controllers/OvertimeController.cs b/Areas/OTcalculate/Controllers/OvertimeController.cs index c5596ae..1b3b5d4 100644 --- a/Areas/OTcalculate/Controllers/OvertimeController.cs +++ b/Areas/OTcalculate/Controllers/OvertimeController.cs @@ -20,7 +20,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Controllers return View(); } - public IActionResult OtSTatus() + public IActionResult OtStatus() { return View(); } diff --git a/Areas/OTcalculate/Services/OvertimePdfService.cs b/Areas/OTcalculate/Services/OvertimePdfService.cs index 173b241..6f54e82 100644 --- a/Areas/OTcalculate/Services/OvertimePdfService.cs +++ b/Areas/OTcalculate/Services/OvertimePdfService.cs @@ -62,6 +62,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services { table.ColumnsDefinition(columns => { + columns.RelativeColumn(0.7f); // Days columns.RelativeColumn(1.1f); // Date columns.RelativeColumn(0.8f); // Office From columns.RelativeColumn(0.8f); // Office To @@ -74,13 +75,13 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services columns.RelativeColumn(); // Net OT if (departmentId == 2) columns.RelativeColumn(); // Station - columns.RelativeColumn(0.9f); // Day Type columns.RelativeColumn(2.7f); // Description }); 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(); header.Cell().ColumnSpan(3).Background("#dceefb").Border(0.25f).Padding(5).Text("Office Hours\n(8:30 - 17:30)").FontSize(9).Bold().AlignCenter(); @@ -93,17 +94,17 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services if (departmentId == 2) header.Cell().RowSpan(2).Background("#d0f0ef").Border(0.25f).Padding(5).Text("Station").FontSize(9).Bold().AlignCenter(); - header.Cell().RowSpan(2).Background("#e0f7da").Border(0.25f).Padding(5).Text("Days").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 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").FontSize(9).Bold().AlignCenter(); + header.Cell().Background("#dceefb").Border(0.25f).Padding(5).Text("Break (min)").FontSize(9).Bold().AlignCenter(); header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("From").FontSize(9).Bold().AlignCenter(); header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("To").FontSize(9).Bold().AlignCenter(); - header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("Break").FontSize(9).Bold().AlignCenter(); + header.Cell().Background("#edf2f7").Border(0.25f).Padding(5).Text("Break (min)").FontSize(9).Bold().AlignCenter(); }); @@ -128,44 +129,55 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services } else { - foreach (var r in records) + var groupedRecords = records.GroupBy(r => r.OtDate.Date); + + foreach (var group in groupedRecords) { - var totalOT = CalculateTotalOT(r); - var totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0); - var netOT = totalOT - TimeSpan.FromMinutes(totalBreak); + bool isFirstRow = true; - totalOTSum += totalOT.TotalHours; - totalBreakSum += totalBreak; - totalNetOt += netOT; - - string rowBg = alternate ? "#f9f9f9" : "#ffffff"; - alternate = !alternate; - - void AddCell(string value, bool alignLeft = false) + foreach (var r in group) { - var text = table.Cell().Background(rowBg).Border(0.25f).Padding(5).Text(value).FontSize(9); - if (alignLeft) - text.AlignLeft(); - else - text.AlignCenter(); - } + var totalOT = CalculateTotalOT(r); + var totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0); + var netOT = totalOT - TimeSpan.FromMinutes(totalBreak); - AddCell(r.OtDate.ToString("dd/MM/yyyy")); - AddCell(FormatTime(r.OfficeFrom)); - AddCell(FormatTime(r.OfficeTo)); - AddCell($"{r.OfficeBreak ?? 0} min"); - AddCell(FormatTime(r.AfterFrom)); - AddCell(FormatTime(r.AfterTo)); - AddCell($"{r.AfterBreak ?? 0} min"); - 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"); - AddCell(r.OtDays); - table.Cell().Background(rowBg).Border(0.25f).Padding(5).Text(r.OtDescription ?? "-").FontSize(9).WrapAnywhere().LineHeight(1.2f); + 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 totalOTTimeSpan = TimeSpan.FromHours(totalOTSum); var totalBreakTimeSpan = TimeSpan.FromMinutes(totalBreakSum); diff --git a/Areas/OTcalculate/Views/HouDashboard/HouApproval.cshtml b/Areas/OTcalculate/Views/HouDashboard/HouApproval.cshtml new file mode 100644 index 0000000..0cc15d5 --- /dev/null +++ b/Areas/OTcalculate/Views/HouDashboard/HouApproval.cshtml @@ -0,0 +1,5 @@ +@{ + ViewData["Title"] = "Overtime Approval"; + Layout = "~/Views/Shared/_Layout.cshtml"; +} + diff --git a/Areas/OTcalculate/Views/HouDashboard/OtReview.cshtml b/Areas/OTcalculate/Views/HouDashboard/OtReview.cshtml new file mode 100644 index 0000000..6693b41 --- /dev/null +++ b/Areas/OTcalculate/Views/HouDashboard/OtReview.cshtml @@ -0,0 +1,169 @@ +@{ + ViewData["Title"] = "Overtime Review"; + Layout = "~/Views/Shared/_Layout.cshtml"; +} + +
+

Overtime Records for {{ reviewedUserName }} - {{ formatMonthYear(selectedMonth, selectedYear) }}

+ +
+ +@section Scripts { + + + } \ No newline at end of file diff --git a/Areas/OTcalculate/Views/Overtime/OtRecords.cshtml b/Areas/OTcalculate/Views/Overtime/OtRecords.cshtml index d372cba..8465111 100644 --- a/Areas/OTcalculate/Views/Overtime/OtRecords.cshtml +++ b/Areas/OTcalculate/Views/Overtime/OtRecords.cshtml @@ -30,16 +30,10 @@ background-color: #fce5cd !important; } - input.form-control { - border-radius: 10px; - } - - select.form-control { - border-radius: 10px; - } - + input.form-control, + select.form-control, .btn { - border-radius: 10px !important; + border-radius: 10px; font-size: 13px; } @@ -52,35 +46,32 @@ } td.wrap-text { - white-space: pre-wrap; /* Keep line breaks and wrap text */ - word-wrap: break-word; /* Break long words if necessary */ - max-width: 300px; /* Adjust as needed */ - text-align: left; /* Optional: left-align description */ + white-space: pre-wrap; + word-wrap: break-word; + max-width: 300px; + text-align: left; } .description-preview { - max-height: 3.6em; /* approx. 2 lines */ + max-height: 3.6em; overflow: hidden; - text-overflow: ellipsis; cursor: pointer; + transition: max-height 0.3s ease; white-space: pre-wrap; word-wrap: break-word; - transition: max-height 0.3s ease; - } - - .description-preview.expanded { - max-height: none; } + .description-preview.expanded { + max-height: none; + } -
@@ -92,19 +83,19 @@
- -@section Scripts { - - -} diff --git a/Controllers/API/OvertimeAPI.cs b/Controllers/API/OvertimeAPI.cs index 1409c15..7e4a961 100644 --- a/Controllers/API/OvertimeAPI.cs +++ b/Controllers/API/OvertimeAPI.cs @@ -454,6 +454,7 @@ namespace PSTW_CentralSystem.Controllers.API #endregion #region Ot Records + [HttpGet("GetUserOvertimeRecords/{userId}")] public IActionResult GetUserOvertimeRecords(int userId) { @@ -461,6 +462,7 @@ namespace PSTW_CentralSystem.Controllers.API { var records = _centralDbContext.Otregisters .Where(o => o.UserId == userId) + .OrderByDescending(o => o.OtDate) .Select(o => new { o.OvertimeId, @@ -477,7 +479,6 @@ namespace PSTW_CentralSystem.Controllers.API o.OtDays, o.UserId }) - .OrderByDescending(o => o.OtDate) .ToList(); return Ok(records); @@ -501,7 +502,6 @@ namespace PSTW_CentralSystem.Controllers.API return NotFound("Overtime record not found."); } - // Delete the record from the database (No file handling anymore) _centralDbContext.Otregisters.Remove(record); _centralDbContext.SaveChanges(); @@ -518,22 +518,18 @@ namespace PSTW_CentralSystem.Controllers.API [HttpGet("GenerateOvertimePdf")] public IActionResult GenerateOvertimePdf(int month, int year) { - var userIdString = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; - - if (string.IsNullOrEmpty(userIdString) || !int.TryParse(userIdString, out int userId)) - { + var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId)) return Unauthorized(); - } - // Get user and department info var user = _centralDbContext.Users .Include(u => u.Department) .FirstOrDefault(u => u.Id == userId); if (user == null) - return NotFound("User not found"); + return NotFound("User not found."); - var userFullName = $"{user.FullName}"; + var fullName = user.FullName; var departmentId = user.departmentId ?? 0; var departmentName = user.Department?.DepartmentName ?? "N/A"; @@ -542,101 +538,53 @@ namespace PSTW_CentralSystem.Controllers.API .Where(o => o.UserId == userId && o.OtDate.Month == month && o.OtDate.Year == year) .ToList(); + // Step 1: Generate all days of the month + var daysInMonth = DateTime.DaysInMonth(year, month); + var allDays = Enumerable.Range(1, daysInMonth) + .Select(day => new DateTime(year, month, day)) + .ToList(); + + // Step 2: Merge records with missing days + var mergedRecords = new List(); + + 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 = "", + }); + } + } + + 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); - } - var stream = _pdfService.GenerateOvertimeTablePdf(records, departmentId, userFullName, departmentName, logoImage); + var stream = _pdfService.GenerateOvertimeTablePdf(mergedRecords, departmentId, fullName, departmentName, logoImage); + return File(stream, "application/pdf", $"OvertimeRecords_{year}_{month}.pdf"); } - [HttpPost("SubmitOvertimeRecords")] - public async Task SubmitOvertimeRecords([FromForm] IFormFile file, [FromForm] int month, [FromForm] int year) - { - var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId)) - return Unauthorized(); - - if (file == null || file.Length == 0) - return BadRequest("No file uploaded."); - - var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Media", "Overtime"); - - if (!Directory.Exists(uploadsFolder)) - Directory.CreateDirectory(uploadsFolder); - - var fileName = $"OT_{userId}_{year}_{month}_{DateTime.Now.Ticks}.pdf"; - var filePath = Path.Combine(uploadsFolder, fileName); - - using (var stream = new FileStream(filePath, FileMode.Create)) - { - await file.CopyToAsync(stream); - } - - var statusRecord = new OtStatusModel - { - UserId = userId, - Month = month, - Year = year, - SubmitDate = DateTime.Now, - HodStatus = "Pending", - HrStatus = "Pending", - FilePath = $"/Media/Overtime/{fileName}" - }; - - _centralDbContext.OtStatus.Add(statusRecord); - await _centralDbContext.SaveChangesAsync(); - - return Ok(new { message = "Overtime records submitted successfully." }); - } - - [HttpGet("CheckSubmissionStatus")] - public IActionResult CheckSubmissionStatus(int month, int year) - { - var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId)) - return Unauthorized(); - - var isSubmitted = _centralDbContext.OtStatus - .Any(s => s.UserId == userId && s.Month == month && s.Year == year); - - return Ok(new { isSubmitted }); - } - - [HttpGet("GetSubmissionStatus/{userId}")] - public IActionResult GetSubmissionStatus(int userId) - { - try - { - var statuses = _centralDbContext.OtStatus - .Where(s => s.UserId == userId) - .OrderByDescending(s => s.SubmitDate) - .Select(s => new - { - s.UserId, - s.Month, - s.Year, - s.HodStatus, - s.HrStatus, - s.SubmitDate - }) - .ToList(); - - return Ok(statuses); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to fetch submission statuses."); - return StatusCode(500, "Error retrieving submission statuses."); - } - } - #endregion + + #region Ot Edit [HttpGet("GetOvertimeRecordById/{id}")] public async Task GetOvertimeRecordById(int id) @@ -697,20 +645,7 @@ namespace PSTW_CentralSystem.Controllers.API #endregion #region OtStatus - [HttpGet("GetUserOtStatus")] - public IActionResult GetUserOtStatus() - { - var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userIdStr) || !int.TryParse(userIdStr, out int userId)) - return Unauthorized(); - var records = _centralDbContext.OtStatus - .Where(s => s.UserId == userId) - .OrderByDescending(s => s.Year).ThenByDescending(s => s.Month) - .ToList(); - - return Ok(records); - } #endregion diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 448acb4..d5fbcc3 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -561,6 +561,20 @@ +