This commit is contained in:
Naz 2025-04-14 12:19:20 +08:00
parent ffdc93a4b7
commit 872eb2363a
3 changed files with 185 additions and 70 deletions

View File

@ -18,6 +18,10 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
byte[]? logoImage = null // Optional logo image byte[]? logoImage = null // Optional logo image
) )
{ {
records = records
.OrderBy(r => r.OtDate)
.ToList();
var stream = new MemoryStream(); var stream = new MemoryStream();
Document.Create(container => Document.Create(container =>
@ -44,6 +48,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
col.Item().Text($"Name: {userFullName}").FontSize(9).SemiBold(); col.Item().Text($"Name: {userFullName}").FontSize(9).SemiBold();
col.Item().Text($"Department: {departmentName}").FontSize(9).Italic(); col.Item().Text($"Department: {departmentName}").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}") row.RelativeItem(1).AlignRight().Text($"Generated: {DateTime.Now:dd MMM yyyy HH:mm}")
@ -58,82 +63,131 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{ {
table.ColumnsDefinition(columns => table.ColumnsDefinition(columns =>
{ {
columns.RelativeColumn(); // Date columns.RelativeColumn(1.1f); // Date
columns.RelativeColumn(1); // Office From columns.RelativeColumn(0.8f); // Office From
columns.RelativeColumn(1); // Office To columns.RelativeColumn(0.8f); // Office To
columns.RelativeColumn(); // Office Break columns.RelativeColumn(0.8f); // Office Break
columns.RelativeColumn(1); // Outside From columns.RelativeColumn(0.9f); // Outside From
columns.RelativeColumn(1); // Outside To columns.RelativeColumn(0.9f); // Outside To
columns.RelativeColumn(); // Outside Break columns.RelativeColumn(0.9f); // Outside Break
columns.RelativeColumn(); // Total OT columns.RelativeColumn(); // Total OT
columns.RelativeColumn(1); // Break Hours columns.RelativeColumn(); // Break Hours
columns.RelativeColumn(); // Net OT columns.RelativeColumn(); // Net OT
if (departmentId == 2) if (departmentId == 2)
columns.RelativeColumn(); // Station columns.RelativeColumn(); // Station
columns.RelativeColumn(1); // Day Type columns.RelativeColumn(0.9f); // Day Type
columns.RelativeColumn(3); // Description columns.RelativeColumn(2.7f); // Description
}); });
// Header Row // Header Row
table.Header(header => table.Header(header =>
{ {
header.Cell().Background("#d0ead2").Padding(5).Text("Date").FontSize(9).Bold().AlignCenter(); void AddHeaderCell(string text, string bgColor)
header.Cell().Background("#dceefb").Padding(5).Text("From\n(Office)").FontSize(9).Bold().AlignCenter(); {
header.Cell().Background("#dceefb").Padding(5).Text("To\n(Office)").FontSize(9).Bold().AlignCenter(); header.Cell().Background(bgColor).Border(0.25f).Padding(5).Text(text).FontSize(9).Bold().AlignCenter();
header.Cell().Background("#dceefb").Padding(5).Text("Break\n(Office)").FontSize(9).Bold().AlignCenter(); }
header.Cell().Background("#edf2f7").Padding(5).Text("From\n(Outside)").FontSize(9).Bold().AlignCenter();
header.Cell().Background("#edf2f7").Padding(5).Text("To\n(Outside)").FontSize(9).Bold().AlignCenter(); AddHeaderCell("Date", "#d0ead2");
header.Cell().Background("#edf2f7").Padding(5).Text("Break\n(Outside)").FontSize(9).Bold().AlignCenter(); AddHeaderCell("From\n(Office)", "#dceefb");
header.Cell().Background("#fdebd0").Padding(5).Text("Total OT\nHours").FontSize(9).Bold().AlignCenter(); AddHeaderCell("To\n(Office)", "#dceefb");
header.Cell().Background("#fdebd0").Padding(5).Text("Break Hours\n(min)").FontSize(9).Bold().AlignCenter(); AddHeaderCell("Break\n(Office)", "#dceefb");
header.Cell().Background("#fdebd0").Padding(5).Text("Net OT").FontSize(9).Bold().AlignCenter(); AddHeaderCell("From\n(Outside)", "#edf2f7");
AddHeaderCell("To\n(Outside)", "#edf2f7");
AddHeaderCell("Break\n(Outside)", "#edf2f7");
AddHeaderCell("Total OT\nHours", "#fdebd0");
AddHeaderCell("Break Hours\n(min)", "#fdebd0");
AddHeaderCell("Net OT", "#fdebd0");
if (departmentId == 2) if (departmentId == 2)
header.Cell().Background("#d0f0ef").Padding(5).Text("Station").FontSize(9).Bold().AlignCenter(); AddHeaderCell("Station", "#d0f0ef");
header.Cell().Background("#e0f7da").Padding(5).Text("Days").FontSize(9).Bold().AlignCenter(); AddHeaderCell("Days", "#e0f7da");
header.Cell().Background("#e3f2fd").Padding(5).Text("Description").FontSize(9).Bold().AlignCenter(); AddHeaderCell("Description", "#e3f2fd");
}); });
// Data Rows
// Data Rows // Data Rows
double totalOTSum = 0; double totalOTSum = 0;
int totalBreakSum = 0; int totalBreakSum = 0;
TimeSpan totalNetOt = TimeSpan.Zero; TimeSpan totalNetOt = TimeSpan.Zero;
bool alternate = false;
foreach (var r in records) if (!records.Any())
{ {
var totalOT = CalculateTotalOT(r); // Show message row if no records
var totalBreak = (r.OfficeBreak ?? 0) + (r.OutsideBreak ?? 0); uint colspan = (uint)(departmentId == 2 ? 13 : 12);
var netOT = totalOT - TimeSpan.FromMinutes(totalBreak);
totalOTSum += totalOT.TotalHours; table.Cell().ColumnSpan(colspan)
totalBreakSum += totalBreak; .Border(0.5f)
totalNetOt += netOT; .Padding(10)
.AlignCenter()
table.Cell().Padding(5).Text(r.OtDate.ToString("dd/MM/yyyy")).FontSize(9); .Text("No records found for selected month and year.")
table.Cell().Padding(5).Text(FormatTime(r.OfficeFrom)).FontSize(9); .FontSize(10)
table.Cell().Padding(5).Text(FormatTime(r.OfficeTo)).FontSize(9); .FontColor(Colors.Grey.Darken2)
table.Cell().Padding(5).Text($"{r.OfficeBreak ?? 0} min").FontSize(9); .Italic();
table.Cell().Padding(5).Text(FormatTime(r.OutsideFrom)).FontSize(9); }
table.Cell().Padding(5).Text(FormatTime(r.OutsideTo)).FontSize(9);
table.Cell().Padding(5).Text($"{r.OutsideBreak ?? 0} min").FontSize(9); else
table.Cell().Padding(5).Text($"{totalOT.TotalHours:F2}").FontSize(9); {
table.Cell().Padding(5).Text($"{totalBreak}").FontSize(9); foreach (var r in records)
table.Cell().Padding(5).Text($"{netOT.Hours} hr {netOT.Minutes} min").FontSize(9); {
if (departmentId == 2) var totalOT = CalculateTotalOT(r);
table.Cell().Padding(5).Text(r.Stations?.StationName ?? "N/A").FontSize(9); var totalBreak = (r.OfficeBreak ?? 0) + (r.OutsideBreak ?? 0);
table.Cell().Padding(5).Text(r.OtDays).FontSize(9); var netOT = totalOT - TimeSpan.FromMinutes(totalBreak);
table.Cell().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(r.OtDate.ToString("dd/MM/yyyy"));
AddCell(FormatTime(r.OfficeFrom));
AddCell(FormatTime(r.OfficeTo));
AddCell($"{r.OfficeBreak ?? 0} min");
AddCell(FormatTime(r.OutsideFrom));
AddCell(FormatTime(r.OutsideTo));
AddCell($"{r.OutsideBreak ?? 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);
}
// Totals Row
var totalOTTimeSpan = TimeSpan.FromHours(totalOTSum);
var totalBreakTimeSpan = TimeSpan.FromMinutes(totalBreakSum);
int totalCols = departmentId == 2 ? 13 : 12;
int spanCols = departmentId == 2 ? 7 : 6;
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($"{(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);
else
table.Cell().Background("#d8d1f5").Border(0.80f);
table.Cell().Background("#d8d1f5").Border(0.80f);
table.Cell().Background("#d8d1f5").Border(0.80f);
} }
// Totals Row
table.Cell().ColumnSpan((uint)(departmentId == 2 ? 7 : 6)).Background("#d8d1f5").Padding(5).Text("TOTAL").Bold().FontSize(9);
table.Cell().Background("#d8d1f5").Padding(5).Text($"{totalOTSum:F2}").Bold().FontSize(9);
table.Cell().Background("#d8d1f5").Padding(5).Text($"{totalBreakSum}").Bold().FontSize(9);
table.Cell().Background("#d8d1f5").Padding(5).Text($"{(int)totalNetOt.TotalHours} hr {totalNetOt.Minutes} min").Bold().FontSize(9);
if (departmentId == 2)
table.Cell().Background("#d8d1f5");
table.Cell().Background("#d8d1f5");
table.Cell().Background("#d8d1f5");
}); });
}); });
}); });
}).GeneratePdf(stream); }).GeneratePdf(stream);
@ -158,6 +212,15 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
{ {
return time?.ToString(@"hh\:mm") ?? "-"; return time?.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}";
}
} }
} }

View File

@ -58,6 +58,19 @@
text-align: left; /* Optional: left-align description */ text-align: left; /* Optional: left-align description */
} }
.description-preview {
max-height: 3.6em; /* approx. 2 lines */
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
white-space: pre-wrap;
word-wrap: break-word;
transition: max-height 0.3s ease;
}
.description-preview.expanded {
max-height: none;
}
</style> </style>
@ -112,12 +125,16 @@
<td>{{ formatTime(record.outsideFrom) }}</td> <td>{{ formatTime(record.outsideFrom) }}</td>
<td>{{ formatTime(record.outsideTo) }}</td> <td>{{ formatTime(record.outsideTo) }}</td>
<td>{{ record.outsideBreak }} min</td> <td>{{ record.outsideBreak }} min</td>
<td>{{ calcTotalHours(record).toFixed(2) }}</td> <td>{{ formatHourMinute(calcTotalTime(record)) }}</td>
<td>{{ calcBreakTotal(record) }}</td> <td>{{ calcBreakTotal(record) }}</td>
<td>{{ formatHourMinute(calcNetHours(record)) }}</td> <td>{{ formatHourMinute(calcNetHours(record)) }}</td>
<td v-if="isPSTWAIR">{{ record.stationName || 'N/A' }}</td> <td v-if="isPSTWAIR">{{ record.stationName || 'N/A' }}</td>
<td>{{ record.otDays}}</td> <td>{{ record.otDays}}</td>
<td class="wrap-text">{{ record.otDescription }}</td> <td class="wrap-text">
<div class="description-preview" v-on:click ="toggleDescription(index)" :class="{ expanded: expandedDescriptions[index] }">
{{ record.otDescription }}
</div>
</td>
<td> <td>
<span v-if="record.pdfBase64"> <span v-if="record.pdfBase64">
<button class="btn btn-light border rounded-circle" title="View PDF" v-on:click="viewPdf(record.pdfBase64)"> <button class="btn btn-light border rounded-circle" title="View PDF" v-on:click="viewPdf(record.pdfBase64)">
@ -142,8 +159,8 @@
<tr class="table-primary fw-bold"> <tr class="table-primary fw-bold">
<td>TOTAL</td> <td>TOTAL</td>
<td colspan="6"></td> <td colspan="6"></td>
<td>{{ totalHours.toFixed(2) }}</td> <td>{{ formatHourMinute(totalHours) }}</td>
<td>{{ totalBreak }}</td> <td>{{ formatHourMinute(totalBreak) }}</td>
<td>{{ formatHourMinute(totalNetTime) }}</td> <td>{{ formatHourMinute(totalNetTime) }}</td>
<td v-if="isPSTWAIR"></td> <td v-if="isPSTWAIR"></td>
<td colspan="4"></td> <td colspan="4"></td>
@ -153,7 +170,7 @@
</div> </div>
<div class="mt-3 d-flex flex-wrap gap-2"> <div class="mt-3 d-flex flex-wrap gap-2">
<button class="btn btn-primary btn-sm" v-on:click="printTable"> <button class="btn btn-primary btn-sm" v-on:click="printPdf">
<i class="bi bi-printer"></i> Print <i class="bi bi-printer"></i> Print
</button> </button>
<button class="btn btn-dark btn-sm" v-on:click="downloadPdf"> <button class="btn btn-dark btn-sm" v-on:click="downloadPdf">
@ -180,7 +197,8 @@
selectedMonth: new Date().getMonth() + 1, selectedMonth: new Date().getMonth() + 1,
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: {}
}; };
}, },
computed: { computed: {
@ -190,13 +208,20 @@
.sort((a, b) => new Date(a.otDate) - new Date(b.otDate)); .sort((a, b) => new Date(a.otDate) - new Date(b.otDate));
}, },
totalHours() { totalHours() {
return this.filteredRecords.reduce((sum, r) => sum + this.calcTotalHours(r), 0); const total = this.filteredRecords.reduce((sum, r) => sum + this.calcTotalHours(r), 0);
const hours = Math.floor(total);
const minutes = Math.round((total - hours) * 60);
return { hours, minutes };
}, },
totalBreak() { totalBreak() {
return this.filteredRecords.reduce((sum, r) => sum + this.calcBreakTotal(r), 0); const totalMin = this.filteredRecords.reduce((sum, r) => sum + this.calcBreakTotal(r), 0);
const hours = Math.floor(totalMin / 60);
const minutes = totalMin % 60;
return { hours, minutes };
}, },
totalNetTime() { totalNetTime() {
const totalMinutes = (this.totalHours * 60) - this.totalBreak; const totalMinutes = (this.totalHours.hours * 60 + this.totalHours.minutes) -
(this.totalBreak.hours * 60 + this.totalBreak.minutes);
return { return {
hours: Math.floor(totalMinutes / 60), hours: Math.floor(totalMinutes / 60),
minutes: Math.round(totalMinutes % 60) minutes: Math.round(totalMinutes % 60)
@ -231,6 +256,10 @@
console.error("Records fetch error:", err); console.error("Records fetch error:", err);
} }
}, },
toggleDescription(index) {
this.expandedDescriptions[index] = !this.expandedDescriptions[index];
},
formatDate(d) { formatDate(d) {
return new Date(d).toLocaleDateString(); return new Date(d).toLocaleDateString();
}, },
@ -243,6 +272,13 @@
const [th, tm] = to.split(":").map(Number); const [th, tm] = to.split(":").map(Number);
return ((th * 60 + tm) - (fh * 60 + fm)) / 60; return ((th * 60 + tm) - (fh * 60 + fm)) / 60;
}, },
calcTotalTime(r) {
const totalMinutes = this.calcTotalHours(r) * 60;
return {
hours: Math.floor(totalMinutes / 60),
minutes: Math.round(totalMinutes % 60)
};
},
calcTotalHours(r) { calcTotalHours(r) {
return this.getTimeDiff(r.officeFrom, r.officeTo) + this.getTimeDiff(r.outsideFrom, r.outsideTo); return this.getTimeDiff(r.officeFrom, r.officeTo) + this.getTimeDiff(r.outsideFrom, r.outsideTo);
}, },
@ -257,7 +293,7 @@
}; };
}, },
formatHourMinute(timeObj) { formatHourMinute(timeObj) {
return timeObj ? `${timeObj.hours} hr ${timeObj.minutes} min` : '-'; return timeObj ? `${timeObj.hours} h ${timeObj.minutes} m` : '-';
}, },
editRecord(index) { editRecord(index) {
const record = this.filteredRecords[index]; const record = this.filteredRecords[index];
@ -275,8 +311,26 @@
alert("Error deleting record."); alert("Error deleting record.");
} }
}, },
printTable() { printPdf() {
window.print(); const today = new Date();
const month = today.getMonth() + 1;
const year = today.getFullYear();
fetch(`/OvertimeAPI/GenerateOvertimePdf?month=${month}&year=${year}`)
.then(response => response.blob())
.then(blob => {
const blobUrl = URL.createObjectURL(blob);
const printWindow = window.open(blobUrl, '_blank');
// Automatically trigger print after window loads
printWindow.onload = () => {
printWindow.focus();
printWindow.print();
};
})
.catch(error => {
console.error("Error generating PDF:", error);
});
}, },
async downloadPdf() { async downloadPdf() {
try { try {

View File

@ -492,9 +492,6 @@ namespace PSTW_CentralSystem.Controllers.API
} }
} }
#endregion
[HttpPost("SaveOvertimeRecordsWithPdf")] [HttpPost("SaveOvertimeRecordsWithPdf")]
public async Task<IActionResult> SaveOvertimeRecordsWithPdf([FromBody] List<OtRegisterModel> records) public async Task<IActionResult> SaveOvertimeRecordsWithPdf([FromBody] List<OtRegisterModel> records)
{ {
@ -577,6 +574,7 @@ namespace PSTW_CentralSystem.Controllers.API
return File(stream, "application/pdf", $"OvertimeRecords_{year}_{month}.pdf"); return File(stream, "application/pdf", $"OvertimeRecords_{year}_{month}.pdf");
} }
#endregion
} }
} }