edited
This commit is contained in:
parent
2c9d8bc4da
commit
ffdc93a4b7
163
Areas/OTcalculate/Services/OvertimePdfService.cs
Normal file
163
Areas/OTcalculate/Services/OvertimePdfService.cs
Normal file
@ -0,0 +1,163 @@
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using PSTW_CentralSystem.Areas.OTcalculate.Models;
|
||||
using System;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
||||
{
|
||||
public class OvertimePdfService
|
||||
{
|
||||
public MemoryStream GenerateOvertimeTablePdf(
|
||||
List<OtRegisterModel> records,
|
||||
int departmentId,
|
||||
string userFullName,
|
||||
string departmentName,
|
||||
byte[]? logoImage = null // Optional logo image
|
||||
)
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
|
||||
Document.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4.Landscape());
|
||||
page.Margin(30);
|
||||
|
||||
// Header section with logo and user info
|
||||
page.Content().Column(column =>
|
||||
{
|
||||
column.Item().Row(row =>
|
||||
{
|
||||
row.RelativeItem(2).Column(col =>
|
||||
{
|
||||
|
||||
|
||||
if (logoImage != null)
|
||||
{
|
||||
col.Item().Container().Height(36).Image(logoImage, ImageScaling.FitArea);
|
||||
col.Spacing(10);
|
||||
}
|
||||
|
||||
col.Item().Text($"Name: {userFullName}").FontSize(9).SemiBold();
|
||||
col.Item().Text($"Department: {departmentName}").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
|
||||
column.Item().Table(table =>
|
||||
{
|
||||
table.ColumnsDefinition(columns =>
|
||||
{
|
||||
columns.RelativeColumn(); // Date
|
||||
columns.RelativeColumn(1); // Office From
|
||||
columns.RelativeColumn(1); // Office To
|
||||
columns.RelativeColumn(); // Office Break
|
||||
columns.RelativeColumn(1); // Outside From
|
||||
columns.RelativeColumn(1); // Outside To
|
||||
columns.RelativeColumn(); // Outside Break
|
||||
columns.RelativeColumn(); // Total OT
|
||||
columns.RelativeColumn(1); // Break Hours
|
||||
columns.RelativeColumn(); // Net OT
|
||||
if (departmentId == 2)
|
||||
columns.RelativeColumn(); // Station
|
||||
columns.RelativeColumn(1); // Day Type
|
||||
columns.RelativeColumn(3); // Description
|
||||
});
|
||||
|
||||
// Header Row
|
||||
table.Header(header =>
|
||||
{
|
||||
header.Cell().Background("#d0ead2").Padding(5).Text("Date").FontSize(9).Bold().AlignCenter();
|
||||
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("#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();
|
||||
header.Cell().Background("#edf2f7").Padding(5).Text("Break\n(Outside)").FontSize(9).Bold().AlignCenter();
|
||||
header.Cell().Background("#fdebd0").Padding(5).Text("Total OT\nHours").FontSize(9).Bold().AlignCenter();
|
||||
header.Cell().Background("#fdebd0").Padding(5).Text("Break Hours\n(min)").FontSize(9).Bold().AlignCenter();
|
||||
header.Cell().Background("#fdebd0").Padding(5).Text("Net OT").FontSize(9).Bold().AlignCenter();
|
||||
if (departmentId == 2)
|
||||
header.Cell().Background("#d0f0ef").Padding(5).Text("Station").FontSize(9).Bold().AlignCenter();
|
||||
header.Cell().Background("#e0f7da").Padding(5).Text("Days").FontSize(9).Bold().AlignCenter();
|
||||
header.Cell().Background("#e3f2fd").Padding(5).Text("Description").FontSize(9).Bold().AlignCenter();
|
||||
});
|
||||
|
||||
// Data Rows
|
||||
double totalOTSum = 0;
|
||||
int totalBreakSum = 0;
|
||||
TimeSpan totalNetOt = TimeSpan.Zero;
|
||||
|
||||
foreach (var r in records)
|
||||
{
|
||||
var totalOT = CalculateTotalOT(r);
|
||||
var totalBreak = (r.OfficeBreak ?? 0) + (r.OutsideBreak ?? 0);
|
||||
var netOT = totalOT - TimeSpan.FromMinutes(totalBreak);
|
||||
|
||||
totalOTSum += totalOT.TotalHours;
|
||||
totalBreakSum += totalBreak;
|
||||
totalNetOt += netOT;
|
||||
|
||||
table.Cell().Padding(5).Text(r.OtDate.ToString("dd/MM/yyyy")).FontSize(9);
|
||||
table.Cell().Padding(5).Text(FormatTime(r.OfficeFrom)).FontSize(9);
|
||||
table.Cell().Padding(5).Text(FormatTime(r.OfficeTo)).FontSize(9);
|
||||
table.Cell().Padding(5).Text($"{r.OfficeBreak ?? 0} min").FontSize(9);
|
||||
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);
|
||||
table.Cell().Padding(5).Text($"{totalOT.TotalHours:F2}").FontSize(9);
|
||||
table.Cell().Padding(5).Text($"{totalBreak}").FontSize(9);
|
||||
table.Cell().Padding(5).Text($"{netOT.Hours} hr {netOT.Minutes} min").FontSize(9);
|
||||
if (departmentId == 2)
|
||||
table.Cell().Padding(5).Text(r.Stations?.StationName ?? "N/A").FontSize(9);
|
||||
table.Cell().Padding(5).Text(r.OtDays).FontSize(9);
|
||||
table.Cell().Padding(5).Text(r.OtDescription ?? "-").FontSize(9).WrapAnywhere().LineHeight(1.2f);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
|
||||
|
||||
private TimeSpan CalculateTotalOT(OtRegisterModel r)
|
||||
{
|
||||
TimeSpan office = (r.OfficeTo ?? TimeSpan.Zero) - (r.OfficeFrom ?? TimeSpan.Zero);
|
||||
TimeSpan outside = (r.OutsideTo ?? TimeSpan.Zero) - (r.OutsideFrom ?? TimeSpan.Zero);
|
||||
|
||||
if (outside < TimeSpan.Zero)
|
||||
outside += TimeSpan.FromHours(24);
|
||||
|
||||
return office + outside;
|
||||
}
|
||||
|
||||
private string FormatTime(TimeSpan? time)
|
||||
{
|
||||
return time?.ToString(@"hh\:mm") ?? "-";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
@{
|
||||
ViewData["Title"] = "Records Overtime";
|
||||
ViewData["Title"] = "Edit Overtime";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
@{
|
||||
ViewData["Title"] = "My Overtime Records";
|
||||
ViewData["Title"] = "Overtime Records";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
@ -51,6 +51,14 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
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 */
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@ -80,7 +88,8 @@
|
||||
<th class="header-orange" rowspan="2">Total OT Hours</th>
|
||||
<th class="header-orange" rowspan="2">Break Hours (min)</th>
|
||||
<th class="header-orange" rowspan="2">Net OT</th>
|
||||
<th class="header-orange" rowspan="2">Station</th>
|
||||
<th class="header-orange" rowspan="2" v-if="isPSTWAIR">Station</th>
|
||||
<th class="header-green" rowspan="2">Days</th>
|
||||
<th class="header-blue" rowspan="2">Description</th>
|
||||
<th class="header-blue" rowspan="2">File</th>
|
||||
<th class="header-green" rowspan="2">Action</th>
|
||||
@ -106,8 +115,9 @@
|
||||
<td>{{ calcTotalHours(record).toFixed(2) }}</td>
|
||||
<td>{{ calcBreakTotal(record) }}</td>
|
||||
<td>{{ formatHourMinute(calcNetHours(record)) }}</td>
|
||||
<td>{{ record.stationName || 'N/A' }}</td>
|
||||
<td>{{ record.otDescription }}</td>
|
||||
<td v-if="isPSTWAIR">{{ record.stationName || 'N/A' }}</td>
|
||||
<td>{{ record.otDays}}</td>
|
||||
<td class="wrap-text">{{ record.otDescription }}</td>
|
||||
<td>
|
||||
<span v-if="record.pdfBase64">
|
||||
<button class="btn btn-light border rounded-circle" title="View PDF" v-on:click="viewPdf(record.pdfBase64)">
|
||||
@ -127,7 +137,7 @@
|
||||
|
||||
</tr>
|
||||
<tr v-if="filteredRecords.length === 0">
|
||||
<td colspan="14">No records found for selected month and year.</td>
|
||||
<td :colspan="isPSTWAIR ? 14 : 13">No records found for selected month and year.</td>
|
||||
</tr>
|
||||
<tr class="table-primary fw-bold">
|
||||
<td>TOTAL</td>
|
||||
@ -135,6 +145,7 @@
|
||||
<td>{{ totalHours.toFixed(2) }}</td>
|
||||
<td>{{ totalBreak }}</td>
|
||||
<td>{{ formatHourMinute(totalNetTime) }}</td>
|
||||
<td v-if="isPSTWAIR"></td>
|
||||
<td colspan="4"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -157,27 +168,25 @@
|
||||
|
||||
@section Scripts {
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.2/html2pdf.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
return {
|
||||
otRecords: [],
|
||||
userId: null,
|
||||
isPSTWAIR: false,
|
||||
selectedMonth: new Date().getMonth() + 1,
|
||||
selectedYear: new Date().getFullYear(),
|
||||
selectedYear: currentYear,
|
||||
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
||||
years: Array.from({ length: 10 }, (_, i) => new Date().getFullYear() - 5 + i)
|
||||
years: Array.from({ length: 10 }, (_, i) => currentYear - 5 + i)
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredRecords() {
|
||||
return this.otRecords
|
||||
.filter(record => {
|
||||
const date = new Date(record.otDate);
|
||||
return date.getMonth() + 1 === this.selectedMonth && date.getFullYear() === this.selectedYear;
|
||||
})
|
||||
.filter(r => new Date(r.otDate).getMonth() + 1 === this.selectedMonth && new Date(r.otDate).getFullYear() === this.selectedYear)
|
||||
.sort((a, b) => new Date(a.otDate) - new Date(b.otDate));
|
||||
},
|
||||
totalHours() {
|
||||
@ -186,38 +195,41 @@
|
||||
totalBreak() {
|
||||
return this.filteredRecords.reduce((sum, r) => sum + this.calcBreakTotal(r), 0);
|
||||
},
|
||||
netHours() {
|
||||
return this.totalHours - (this.totalBreak / 60);
|
||||
},
|
||||
totalNetTime() {
|
||||
const totalMinutes = (this.totalHours * 60) - this.totalBreak;
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = Math.round(totalMinutes % 60);
|
||||
return { hours, minutes };
|
||||
return {
|
||||
hours: Math.floor(totalMinutes / 60),
|
||||
minutes: Math.round(totalMinutes % 60)
|
||||
};
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.initUserAndRecords();
|
||||
},
|
||||
await this.initUserAndRecords();
|
||||
},
|
||||
methods: {
|
||||
async initUserAndRecords() {
|
||||
await this.fetchUser();
|
||||
if (this.userId) {
|
||||
await this.fetchOtRecords();
|
||||
}
|
||||
if (this.userId) await this.fetchOtRecords();
|
||||
},
|
||||
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;
|
||||
} catch (err) { console.error("User fetch error", err); }
|
||||
const deptName = department?.departmentName?.toUpperCase?.() || '';
|
||||
this.isPSTWAIR = deptName.includes("PSTW") && deptName.includes("AIR") && department?.departmentId === 2;
|
||||
} catch (err) {
|
||||
console.error("User fetch error:", err);
|
||||
}
|
||||
},
|
||||
async fetchOtRecords() {
|
||||
try {
|
||||
const res = await fetch(`/OvertimeAPI/GetUserOvertimeRecords/${this.userId}`);
|
||||
this.otRecords = await res.json();
|
||||
} catch (err) { console.error("Records fetch error", err); }
|
||||
} catch (err) {
|
||||
console.error("Records fetch error:", err);
|
||||
}
|
||||
},
|
||||
formatDate(d) {
|
||||
return new Date(d).toLocaleDateString();
|
||||
@ -225,6 +237,12 @@
|
||||
formatTime(t) {
|
||||
return t ? t.slice(0, 5) : "-";
|
||||
},
|
||||
getTimeDiff(from, to) {
|
||||
if (!from || !to) return 0;
|
||||
const [fh, fm] = from.split(":").map(Number);
|
||||
const [th, tm] = to.split(":").map(Number);
|
||||
return ((th * 60 + tm) - (fh * 60 + fm)) / 60;
|
||||
},
|
||||
calcTotalHours(r) {
|
||||
return this.getTimeDiff(r.officeFrom, r.officeTo) + this.getTimeDiff(r.outsideFrom, r.outsideTo);
|
||||
},
|
||||
@ -232,67 +250,73 @@
|
||||
return (r.officeBreak || 0) + (r.outsideBreak || 0);
|
||||
},
|
||||
calcNetHours(r) {
|
||||
const totalHours = this.calcTotalHours(r);
|
||||
const breakMinutes = this.calcBreakTotal(r);
|
||||
const netMinutes = (totalHours * 60) - breakMinutes;
|
||||
const hours = Math.floor(netMinutes / 60);
|
||||
const minutes = Math.round(netMinutes % 60);
|
||||
return { hours, minutes };
|
||||
const totalMinutes = (this.calcTotalHours(r) * 60) - this.calcBreakTotal(r);
|
||||
return {
|
||||
hours: Math.floor(totalMinutes / 60),
|
||||
minutes: Math.round(totalMinutes % 60)
|
||||
};
|
||||
},
|
||||
formatHourMinute(timeObj) {
|
||||
if (!timeObj) return '-';
|
||||
return `${timeObj.hours} hr ${timeObj.minutes} min`;
|
||||
},
|
||||
getTimeDiff(from, to) {
|
||||
if (!from || !to) return 0;
|
||||
const [fh, fm] = from.split(":").map(Number);
|
||||
const [th, tm] = to.split(":").map(Number);
|
||||
return ((th * 60 + tm) - (fh * 60 + fm)) / 60;
|
||||
return timeObj ? `${timeObj.hours} hr ${timeObj.minutes} min` : '-';
|
||||
},
|
||||
editRecord(index) {
|
||||
|
||||
const record = this.filteredRecords[index];
|
||||
window.location.href = `/OTcalculate/Overtime/EditOvertime?id=${record.overtimeId}`;
|
||||
},
|
||||
async deleteRecord(index) {
|
||||
const record = this.filteredRecords[index];
|
||||
const confirmed = confirm("Are you sure you want to delete this record?");
|
||||
if (!confirmed) return;
|
||||
|
||||
if (!confirm("Are you sure you want to delete this record?")) return;
|
||||
try {
|
||||
const res = await fetch(`/OvertimeAPI/DeleteOvertimeRecord/${record.overtimeId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
// Remove from local state after successful backend deletion
|
||||
this.otRecords.splice(this.otRecords.indexOf(record), 1);
|
||||
} else {
|
||||
const errorText = await res.text();
|
||||
alert("Failed to delete: " + errorText);
|
||||
}
|
||||
const res = await fetch(`/OvertimeAPI/DeleteOvertimeRecord/${record.overtimeId}`, { method: 'DELETE' });
|
||||
if (res.ok) this.otRecords.splice(this.otRecords.indexOf(record), 1);
|
||||
else alert("Failed to delete: " + await res.text());
|
||||
} catch (err) {
|
||||
console.error("Delete failed", err);
|
||||
alert("Error deleting record.");
|
||||
}
|
||||
},
|
||||
|
||||
printTable() {
|
||||
window.print();
|
||||
},
|
||||
downloadPdf() {
|
||||
const element = document.getElementById("print-section");
|
||||
html2pdf().from(element).save("OT_Records.pdf");
|
||||
async downloadPdf() {
|
||||
try {
|
||||
const res = await fetch(`/OvertimeAPI/GenerateOvertimePdf?month=${this.selectedMonth}&year=${this.selectedYear}`);
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `OvertimeRecords_${this.selectedYear}_${this.selectedMonth}.pdf`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
alert("Failed to generate PDF: " + await res.text());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("PDF download error:", err);
|
||||
alert("An error occurred while generating the PDF.");
|
||||
}
|
||||
},
|
||||
submitRecords() {
|
||||
alert("Submitting records...");
|
||||
async submitRecords() {
|
||||
try {
|
||||
const res = await fetch('/OvertimeAPI/SaveOvertimeRecordsWithPdf', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.otRecords)
|
||||
});
|
||||
if (res.ok) alert("Overtime records submitted successfully.");
|
||||
else alert("Submission failed.");
|
||||
} catch (err) {
|
||||
console.error("Submission error:", err);
|
||||
alert("An error occurred during submission.");
|
||||
}
|
||||
},
|
||||
viewPdf(base64) {
|
||||
const byteArray = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
|
||||
const blob = new Blob([byteArray], { type: 'application/pdf' });
|
||||
window.open(URL.createObjectURL(blob));
|
||||
const pdfWindow = window.open("");
|
||||
pdfWindow.document.write(`<iframe width='100%' height='100%' src='data:application/pdf;base64,${base64}'></iframe>`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.mount("#app");
|
||||
</script>
|
||||
}
|
||||
}
|
||||
@ -54,7 +54,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="mb-3" v-if="isPSTWAIR">
|
||||
<label for="airstationDropdown">Air Station</label>
|
||||
<select id="airstationDropdown" class="form-control" v-model="selectedAirStation">
|
||||
<option value="" disabled selected>Select Station</option>
|
||||
@ -65,11 +65,18 @@
|
||||
<small class="text-danger">*Only for PSTW AIR</small>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="otDescription">Work Brief Description</label>
|
||||
<textarea id="otDescription" class="form-control" v-model="otDescription"
|
||||
<textarea id="otDescription" class="form-control"
|
||||
v-model="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">
|
||||
@ -143,11 +150,20 @@
|
||||
uploadedFile: null,
|
||||
currentUser: null,
|
||||
userId: null,
|
||||
isPSTWAIR: false,
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
charCount() {
|
||||
return this.otDescription.length;
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.fetchStations();
|
||||
await this.fetchUser();
|
||||
if (this.isPSTWAIR) {
|
||||
this.fetchStations();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchStations() {
|
||||
@ -167,6 +183,14 @@
|
||||
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?.departmentId);
|
||||
|
||||
if (this.currentUser?.department?.departmentId === 2) {
|
||||
this.isPSTWAIR = true;
|
||||
console.log("User is PSTW AIR");
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed to fetch user: ${response.statusText}`);
|
||||
}
|
||||
@ -174,6 +198,12 @@
|
||||
console.error("Error fetching user:", error);
|
||||
}
|
||||
},
|
||||
limitCharCount(event) {
|
||||
if (this.otDescription.length > 150) {
|
||||
this.otDescription = this.otDescription.substring(0, 150);
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
calculateOTAndBreak() {
|
||||
let officeOT = this.calculateTimeDifference(this.officeFrom, this.officeTo, this.officeBreak);
|
||||
let outsideOT = this.calculateTimeDifference(this.outsideFrom, this.outsideTo, this.outsideBreak);
|
||||
@ -223,7 +253,7 @@
|
||||
return `${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}:00`; // Ensure valid HH:mm:ss format
|
||||
},
|
||||
async addOvertime() {
|
||||
if (!this.selectedDate || !this.selectedDayType) {
|
||||
if (this.isPSTWAIR && !this.selectedAirStation) {
|
||||
alert("Please fill in all required fields.");
|
||||
return;
|
||||
}
|
||||
@ -253,13 +283,14 @@
|
||||
outsideFrom: this.formatTime(this.outsideFrom) || null,
|
||||
outsideTo: this.formatTime(this.outsideTo) || null,
|
||||
outsideBreak: this.outsideBreak || 0,
|
||||
stationId: this.selectedAirStation || null,
|
||||
otDescription: this.otDescription,
|
||||
stationId: this.isPSTWAIR ? parseInt(this.selectedAirStation) : null,
|
||||
otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '),
|
||||
otDays: this.selectedDayType,
|
||||
pdfBase64: base64String,
|
||||
userId: this.userId,
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(`${window.location.origin}/OvertimeAPI/AddOvertime`, {
|
||||
method: "POST",
|
||||
|
||||
@ -19,6 +19,11 @@ using System.Reflection;
|
||||
using static System.Collections.Specialized.BitVector32;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using PSTW_CentralSystem.Areas.OTcalculate.Services;
|
||||
|
||||
|
||||
namespace PSTW_CentralSystem.Controllers.API
|
||||
@ -30,12 +35,15 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
private readonly ILogger<OvertimeAPI> _logger;
|
||||
private readonly CentralSystemContext _centralDbContext;
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly OvertimePdfService _pdfService;
|
||||
|
||||
public OvertimeAPI(ILogger<OvertimeAPI> logger, CentralSystemContext centralDbContext, UserManager<UserModel> userManager)
|
||||
|
||||
public OvertimeAPI(ILogger<OvertimeAPI> logger, CentralSystemContext centralDbContext, UserManager<UserModel> userManager, OvertimePdfService pdfService)
|
||||
{
|
||||
_logger = logger;
|
||||
_centralDbContext = centralDbContext;
|
||||
_userManager = userManager;
|
||||
_pdfService = pdfService;
|
||||
}
|
||||
|
||||
#region Settings
|
||||
@ -485,5 +493,90 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
[HttpPost("SaveOvertimeRecordsWithPdf")]
|
||||
public async Task<IActionResult> SaveOvertimeRecordsWithPdf([FromBody] List<OtRegisterModel> records)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("SaveOvertimeRecordsWithPdf called with {RecordCount} records", records.Count);
|
||||
|
||||
foreach (var record in records)
|
||||
{
|
||||
_logger.LogDebug("Processing record with OvertimeId: {OvertimeId}", record.OvertimeId);
|
||||
|
||||
var existingRecord = await _centralDbContext.Otregisters.FindAsync(record.OvertimeId);
|
||||
|
||||
if (existingRecord != null)
|
||||
{
|
||||
_logger.LogDebug("Updating existing record with OvertimeId: {OvertimeId}", record.OvertimeId);
|
||||
existingRecord.PDFBase64 = record.PDFBase64;
|
||||
_centralDbContext.Otregisters.Update(existingRecord);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Record with OvertimeId: {OvertimeId} not found, adding new", record.OvertimeId);
|
||||
_centralDbContext.Otregisters.Add(record);
|
||||
}
|
||||
}
|
||||
|
||||
await _centralDbContext.SaveChangesAsync();
|
||||
_logger.LogInformation("Successfully saved {RecordCount} overtime records with PDFs", records.Count);
|
||||
return Ok("Overtime records updated with PDFs.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error saving overtime records with PDFs");
|
||||
return StatusCode(500, "An error occurred while saving records.");
|
||||
}
|
||||
}
|
||||
|
||||
[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))
|
||||
{
|
||||
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");
|
||||
|
||||
var userFullName = $"{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();
|
||||
|
||||
// Optional: load logo image as byte array
|
||||
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);
|
||||
return File(stream, "application/pdf", $"OvertimeRecords_{year}_{month}.pdf");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -28,6 +28,7 @@
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.7" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
||||
<PackageReference Include="QuestPDF" Version="2025.4.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -37,6 +38,7 @@
|
||||
<Folder Include="Areas\Report\Models\" />
|
||||
<Folder Include="Controllers\API\Reporting\" />
|
||||
<Folder Include="Logs\" />
|
||||
<Folder Include="wwwroot\NewFolder\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -5,6 +5,9 @@ using PSTW_CentralSystem.CustomPolicy;
|
||||
using PSTW_CentralSystem.DBContext;
|
||||
using PSTW_CentralSystem.Models;
|
||||
using Serilog;
|
||||
using QuestPDF;
|
||||
using QuestPDF.Infrastructure;
|
||||
using PSTW_CentralSystem.Areas.OTcalculate.Services;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
@ -12,11 +15,14 @@ internal class Program
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var centralConnectionString = builder.Configuration.GetConnectionString("CentralConnnection");
|
||||
Settings.License = LicenseType.Community;
|
||||
//var inventoryConnectionString = builder.Configuration.GetConnectionString("InventoryConnection");
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddControllersWithViews();
|
||||
builder.Services.AddRazorPages();
|
||||
builder.Services.AddScoped<OvertimePdfService>();
|
||||
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(builder.Configuration)
|
||||
@ -94,4 +100,5 @@ internal class Program
|
||||
|
||||
app.Run();
|
||||
}
|
||||
|
||||
}
|
||||
BIN
wwwroot/images/logo.jpg
Normal file
BIN
wwwroot/images/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Loading…
Reference in New Issue
Block a user