Compare commits

...

2 Commits

Author SHA1 Message Date
Naz
c0c2c59ea3 edit 2025-04-22 17:21:41 +08:00
Naz
90e56547ba - 2025-04-22 09:12:22 +08:00
17 changed files with 389 additions and 61 deletions

View File

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

View File

@ -0,0 +1,36 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PSTW_CentralSystem.Areas.OTcalculate.Models
{
[Table("otstatus")]
public class OtStatusModel
{
[Key]
public int StatusId { get; set; }
[Required]
public int UserId { get; set; }
[Required]
public int Month { get; set; }
[Required]
public int Year { get; set; }
public DateTime SubmitDate { get; set; }
public string HodStatus { get; set; } = "Pending";
// JSON array of ApprovalUpdateLog
public string? HodUpdate { get; set; }
public string HrStatus { get; set; } = "Pending";
// JSON array of ApprovalUpdateLog
public string? HrUpdate { get; set; }
public string? FilePath { get; set; }
}
}

View File

@ -78,30 +78,35 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
columns.RelativeColumn(2.7f); // Description columns.RelativeColumn(2.7f); // Description
}); });
// Header Row
table.Header(header => table.Header(header =>
{ {
void AddHeaderCell(string text, string bgColor) // Row 1 — grouped headers
{ header.Cell().RowSpan(2).Background("#d0ead2").Border(0.25f).Padding(5).Text("Date").FontSize(9).Bold().AlignCenter();
header.Cell().Background(bgColor).Border(0.25f).Padding(5).Text(text).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();
header.Cell().ColumnSpan(3).Background("#edf2f7").Border(0.25f).Padding(5).Text("After Office Hours\n(17:30 - 8:30)").FontSize(9).Bold().AlignCenter();
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();
AddHeaderCell("Date", "#d0ead2");
AddHeaderCell("From\n(Office)", "#dceefb");
AddHeaderCell("To\n(Office)", "#dceefb");
AddHeaderCell("Break\n(Office)", "#dceefb");
AddHeaderCell("From\n(After)", "#edf2f7");
AddHeaderCell("To\n(After)", "#edf2f7");
AddHeaderCell("Break\n(After)", "#edf2f7");
AddHeaderCell("Total OT\nHours", "#fdebd0");
AddHeaderCell("Break Hours\n(min)", "#fdebd0");
AddHeaderCell("Net OT Hours", "#fdebd0");
if (departmentId == 2) if (departmentId == 2)
AddHeaderCell("Station", "#d0f0ef"); header.Cell().RowSpan(2).Background("#d0f0ef").Border(0.25f).Padding(5).Text("Station").FontSize(9).Bold().AlignCenter();
AddHeaderCell("Days", "#e0f7da");
AddHeaderCell("Description", "#e3f2fd"); 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("#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();
}); });
// Data Rows // Data Rows
double totalOTSum = 0; double totalOTSum = 0;
int totalBreakSum = 0; int totalBreakSum = 0;

View File

@ -1,6 +1,4 @@
 @{
@{
ViewData["Title"] = "Rate Update"; ViewData["Title"] = "Rate Update";
Layout = "~/Views/Shared/_Layout.cshtml"; Layout = "~/Views/Shared/_Layout.cshtml";
} }

View File

@ -104,7 +104,7 @@
<th class="header-orange" rowspan="2" v-if="isPSTWAIR">Station</th> <th class="header-orange" rowspan="2" v-if="isPSTWAIR">Station</th>
<th class="header-green" rowspan="2">Days</th> <th class="header-green" rowspan="2">Days</th>
<th class="header-blue" rowspan="2">Description</th> <th class="header-blue" rowspan="2">Description</th>
<th class="header-green" rowspan="2">Action</th> <th class="header-green" rowspan="2" v-if="!isAlreadySubmitted">Action</th>
</tr> </tr>
<tr> <tr>
<th class="header-blue">From</th> <th class="header-blue">From</th>
@ -134,7 +134,7 @@
{{ record.otDescription }} {{ record.otDescription }}
</div> </div>
</td> </td>
<td> <td v-if="!isAlreadySubmitted">
<button class="btn btn-light border rounded-circle me-1" title="Edit Record" v-on:click="editRecord(index)"> <button class="btn btn-light border rounded-circle me-1" title="Edit Record" v-on:click="editRecord(index)">
<i class="bi bi-pencil-fill text-warning fs-5"></i> <i class="bi bi-pencil-fill text-warning fs-5"></i>
</button> </button>
@ -167,10 +167,33 @@
<button class="btn btn-dark btn-sm" v-on:click="downloadPdf"> <button class="btn btn-dark btn-sm" v-on:click="downloadPdf">
<i class="bi bi-download"></i> Save <i class="bi bi-download"></i> Save
</button> </button>
<button class="btn btn-success btn-sm" v-on:click="submitRecords"> <button class="btn btn-success btn-sm"
<i class="bi bi-send"></i> Submit v-on:click="openSubmitModal"
:disabled="isSubmitting || isAlreadySubmitted">
<i class="bi bi-send"></i> {{ isAlreadySubmitted ? 'Submitted' : 'Submit' }}
</button> </button>
</div> </div>
<!-- Submit Modal -->
<div v-if="showSubmitModal" class="modal show d-block" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content p-3">
<div class="modal-header">
<h5 class="modal-title">Submit OT Records</h5>
<button type="button" class="btn-close" v-on:click="showSubmitModal = false"></button>
</div>
<div class="modal-body">
<input type="file" class="form-control" v-on:change="handleFileChange" accept=".pdf" />
</div>
<div class="modal-footer">
<button class="btn btn-success" v-on:click="submitToHod">Submit</button>
<button class="btn btn-secondary" v-on:click="showSubmitModal = false">Cancel</button>
</div>
</div>
</div>
</div>
</div> </div>
@ -189,7 +212,10 @@
selectedYear: currentYear, selectedYear: currentYear,
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
years: Array.from({ length: 10 }, (_, i) => currentYear - 5 + i), years: Array.from({ length: 10 }, (_, i) => currentYear - 5 + i),
expandedDescriptions: {} expandedDescriptions: {},
showSubmitModal: false,
submitFile: null,
submittedStatus: {},
}; };
}, },
computed: { computed: {
@ -217,6 +243,10 @@
hours: Math.floor(totalMinutes / 60), hours: Math.floor(totalMinutes / 60),
minutes: Math.round(totalMinutes % 60) minutes: Math.round(totalMinutes % 60)
}; };
},
isAlreadySubmitted() {
const key = `${this.selectedYear}-${String(this.selectedMonth).padStart(2, '0')}`;
return !!this.submittedStatus[key];
} }
}, },
async mounted() { async mounted() {
@ -225,8 +255,21 @@
methods: { methods: {
async initUserAndRecords() { async initUserAndRecords() {
await this.fetchUser(); await this.fetchUser();
if (this.userId) await this.fetchOtRecords(); if (this.userId) {
await this.fetchOtRecords();
await this.fetchSubmissionStatus();
}
}, },
async fetchSubmissionStatus() {
try {
const res = await fetch(`/OvertimeAPI/GetSubmissionStatus/${this.userId}`);
const data = await res.json();
this.submittedStatus = data; // expect format like { '2025-04': true }
} catch (err) {
console.error("Submission status fetch error:", err);
}
},
async fetchUser() { async fetchUser() {
try { try {
const res = await fetch('/IdentityAPI/GetUserInformation', { method: 'POST' }); const res = await fetch('/IdentityAPI/GetUserInformation', { method: 'POST' });
@ -342,43 +385,57 @@
alert("An error occurred while generating the PDF."); alert("An error occurred while generating the PDF.");
} }
}, },
async submitRecords() { openSubmitModal() {
try { this.showSubmitModal = true;
const recordsToSubmit = this.filteredRecords.map(record => ({ },
overtimeId: record.overtimeId, // Make sure to include the ID for updates handleFileChange(event) {
otDate: record.otDate, const file = event.target.files[0];
officeFrom: record.officeFrom, if (file && file.type !== 'application/pdf') {
officeTo: record.officeTo, alert("Only PDF files are allowed.");
officeBreak: record.officeBreak, return;
afterFrom: record.afterFrom,
afterTo: record.afterTo,
afterBreak: record.afterBreak,
stationId: record.stationId,
otDescription: record.otDescription,
otDays: record.otDays,
filePath: record.filePath, // Include existing file path
userId: this.userId
// Add other relevant fields if necessary
}));
const res = await fetch('/OvertimeAPI/SubmitOvertimeRecords', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(recordsToSubmit)
});
if (res.ok) {
alert("Overtime records submitted for review.");
} else {
alert("Submission failed: " + await res.text());
}
} catch (err) {
console.error("Submission error:", err);
alert("An error occurred during submission.");
} }
this.submitFile = file;
}, },
} async submitToHod() {
}); this.isSubmitting = true;
try {
if (!this.submitFile) {
alert("Please upload a PDF file.");
return;
}
const formData = new FormData();
formData.append("file", this.submitFile);
// Add month & year selection logic if needed
formData.append("month", new Date().getMonth() + 1);
formData.append("year", new Date().getFullYear());
try {
const response = await fetch("/OvertimeAPI/SubmitOvertimeRecords", {
method: "POST",
body: formData
});
if (!response.ok) throw new Error("Submission failed");
alert("Submission successful!");
this.showSubmitModal = false;
const key = `${this.selectedYear}-${String(this.selectedMonth).padStart(2, '0')}`;
this.submittedStatus[key] = true;
this.showSubmitModal = false;
} catch (err) {
alert("Error: " + err.message);
}
} finally {
this.isSubmitting = false;
}
}
}
});
app.mount("#app"); app.mount("#app");
</script> </script>
} }

View File

@ -0,0 +1,123 @@
@{
ViewData["Title"] = "Overtime Status";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<style>
.hodstatus, .hrstatus {
text-transform: capitalize;
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.6);
display: flex;
align-items: center;
justify-content: center;
}
.modal-container {
background: white;
padding: 20px;
border-radius: 8px;
width: 500px;
}
</style>
<div id="otStatusApp" class="container mt-4">
<table class="table table-bordered">
<thead>
<tr>
<th>Month</th>
<th>Year</th>
<th>Submitted On</th>
<th>HOD Status</th>
<th>HR Status</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr v-for="status in statusList" :key="status.statusId">
<td>{{ getMonthName(status.month) }}</td>
<td>{{ status.year }}</td>
<td>{{ formatDate(status.submitDate) }}</td>
<td :class="status.hodStatus.toLowerCase()">{{ status.hodStatus }}</td>
<td :class="status.hrStatus.toLowerCase()">{{ status.hrStatus }}</td>
<td>
<button class="btn btn-sm btn-primary" v-on:click ="viewUpdates(status)">View Updates</button>
</td>
</tr>
</tbody>
</table>
<!-- Modal -->
<div v-if="selectedStatus" class="modal-mask">
<div class="modal-container">
<h5>Status History</h5>
<p><strong>HOD Updates:</strong></p>
<ul>
<li v-for="update in parseJson(selectedStatus.hodUpdate)">
{{ formatUpdate(update) }}
</li>
</ul>
<p><strong>HR Updates:</strong></p>
<ul>
<li v-for="update in parseJson(selectedStatus.hrUpdate)">
{{ formatUpdate(update) }}
</li>
</ul>
<button class="btn btn-secondary" v-on:click ="selectedStatus = null">Close</button>
</div>
</div>
</div>
@section Scripts {
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
statusList: [],
selectedStatus: null,
};
},
mounted() {
fetch('/OvertimeAPI/GetUserOtStatus')
.then(res => {
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
return res.json();
})
.then(data => this.statusList = data)
.catch(err => console.error("Fetch error:", err));
},
methods: {
formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString();
},
getMonthName(month) {
return new Date(2000, month - 1, 1).toLocaleString('default', { month: 'long' });
},
parseJson(jsonStr) {
try {
return jsonStr ? JSON.parse(jsonStr) : [];
} catch (e) {
return [];
}
},
formatUpdate(update) {
return `${update.timestamp} - ${update.updatedBy} changed ${update.field} from '${update.oldValue}' to '${update.newValue}'`;
},
viewUpdates(status) {
this.selectedStatus = status;
}
}
}).mount('#otStatusApp');
</script>
}

View File

@ -554,6 +554,87 @@ namespace PSTW_CentralSystem.Controllers.API
return File(stream, "application/pdf", $"OvertimeRecords_{year}_{month}.pdf"); return File(stream, "application/pdf", $"OvertimeRecords_{year}_{month}.pdf");
} }
[HttpPost("SubmitOvertimeRecords")]
public async Task<IActionResult> 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 #endregion
#region Ot Edit #region Ot Edit
@ -615,6 +696,23 @@ namespace PSTW_CentralSystem.Controllers.API
} }
#endregion #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
} }
} }

View File

@ -104,5 +104,7 @@ namespace PSTW_CentralSystem.DBContext
public DbSet<StateModel> States { get; set; } public DbSet<StateModel> States { get; set; }
public DbSet<WeekendModel> Weekends { get; set; } public DbSet<WeekendModel> Weekends { get; set; }
public DbSet<OtRegisterModel> Otregisters { get; set; } public DbSet<OtRegisterModel> Otregisters { get; set; }
public DbSet<OtStatusModel> OtStatus { get; set; }
} }
} }

View File

@ -539,6 +539,11 @@
<i class="mdi mdi-view-dashboard"></i><span class="hide-menu">OT Records</span> <i class="mdi mdi-view-dashboard"></i><span class="hide-menu">OT Records</span>
</a> </a>
</li> </li>
<li class="sidebar-item">
<a class="sidebar-link waves-effect waves-dark sidebar-link" asp-area="OTcalculate" asp-controller="Overtime" asp-action="OtStatus" aria-expanded="false">
<i class="mdi mdi-view-dashboard"></i><span class="hide-menu">OT Status</span>
</a>
</li>
</ul> </ul>
</li> </li>