-
This commit is contained in:
parent
be8084ca85
commit
8a39714a25
@ -17,9 +17,9 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
|
||||
public TimeSpan? OfficeFrom { get; set; }
|
||||
public TimeSpan? OfficeTo { get; set; }
|
||||
public int? OfficeBreak { get; set; }
|
||||
public TimeSpan? OutsideFrom { get; set; }
|
||||
public TimeSpan? OutsideTo { get; set; }
|
||||
public int? OutsideBreak { get; set; }
|
||||
public TimeSpan? AfterFrom { get; set; }
|
||||
public TimeSpan? AfterTo { get; set; }
|
||||
public int? AfterBreak { get; set; }
|
||||
public int? StationId { get; set; }
|
||||
|
||||
[ForeignKey("StationId")]
|
||||
@ -27,7 +27,6 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
|
||||
|
||||
public string OtDescription { get; set; }
|
||||
public string OtDays { get; set; }
|
||||
public string FilePath { get; set; }
|
||||
|
||||
[Required]
|
||||
public int UserId { get; set; }
|
||||
@ -35,8 +34,8 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Models
|
||||
// Convert string times to TimeSpan before saving
|
||||
public TimeSpan? GetOfficeFrom() => ParseTimeSpan(OfficeFrom?.ToString());
|
||||
public TimeSpan? GetOfficeTo() => ParseTimeSpan(OfficeTo?.ToString());
|
||||
public TimeSpan? GetOutsideFrom() => ParseTimeSpan(OutsideFrom?.ToString());
|
||||
public TimeSpan? GetOutsideTo() => ParseTimeSpan(OutsideTo?.ToString());
|
||||
public TimeSpan? GetAfterFrom() => ParseTimeSpan(AfterFrom?.ToString());
|
||||
public TimeSpan? GetAfterTo() => ParseTimeSpan(AfterTo?.ToString());
|
||||
|
||||
private TimeSpan? ParseTimeSpan(string? time)
|
||||
{
|
||||
|
||||
@ -3,17 +3,16 @@
|
||||
public class OvertimeRequestDto
|
||||
{
|
||||
public DateTime OtDate { get; set; }
|
||||
public string OfficeFrom { get; set; }
|
||||
public string OfficeTo { get; set; }
|
||||
public string? OfficeFrom { get; set; }
|
||||
public string? OfficeTo { get; set; }
|
||||
public int? OfficeBreak { get; set; }
|
||||
public string OutsideFrom { get; set; }
|
||||
public string OutsideTo { get; set; }
|
||||
public int? OutsideBreak { get; set; }
|
||||
public string? AfterFrom { get; set; }
|
||||
public string? AfterTo { get; set; }
|
||||
public int? AfterBreak { get; set; }
|
||||
public int? StationId { get; set; }
|
||||
public string OtDescription { get; set; }
|
||||
public string OtDays { get; set; }
|
||||
public int UserId { get; set; }
|
||||
public IFormFile File { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -66,9 +66,9 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
||||
columns.RelativeColumn(0.8f); // Office From
|
||||
columns.RelativeColumn(0.8f); // Office To
|
||||
columns.RelativeColumn(0.8f); // Office Break
|
||||
columns.RelativeColumn(0.9f); // Outside From
|
||||
columns.RelativeColumn(0.9f); // Outside To
|
||||
columns.RelativeColumn(0.9f); // Outside Break
|
||||
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
|
||||
@ -90,9 +90,9 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
||||
AddHeaderCell("From\n(Office)", "#dceefb");
|
||||
AddHeaderCell("To\n(Office)", "#dceefb");
|
||||
AddHeaderCell("Break\n(Office)", "#dceefb");
|
||||
AddHeaderCell("From\n(Outside)", "#edf2f7");
|
||||
AddHeaderCell("To\n(Outside)", "#edf2f7");
|
||||
AddHeaderCell("Break\n(Outside)", "#edf2f7");
|
||||
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");
|
||||
@ -126,7 +126,7 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
||||
foreach (var r in records)
|
||||
{
|
||||
var totalOT = CalculateTotalOT(r);
|
||||
var totalBreak = (r.OfficeBreak ?? 0) + (r.OutsideBreak ?? 0);
|
||||
var totalBreak = (r.OfficeBreak ?? 0) + (r.AfterBreak ?? 0);
|
||||
var netOT = totalOT - TimeSpan.FromMinutes(totalBreak);
|
||||
|
||||
totalOTSum += totalOT.TotalHours;
|
||||
@ -149,9 +149,9 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
||||
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(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");
|
||||
@ -194,12 +194,12 @@ namespace PSTW_CentralSystem.Areas.OTcalculate.Services
|
||||
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);
|
||||
TimeSpan after = (r.AfterTo ?? TimeSpan.Zero) - (r.AfterFrom ?? TimeSpan.Zero);
|
||||
|
||||
if (outside < TimeSpan.Zero)
|
||||
outside += TimeSpan.FromHours(24);
|
||||
if (after < TimeSpan.Zero)
|
||||
after += TimeSpan.FromHours(24);
|
||||
|
||||
return office + outside;
|
||||
return office + after;
|
||||
}
|
||||
|
||||
private string FormatTime(TimeSpan? time)
|
||||
|
||||
@ -15,9 +15,9 @@
|
||||
<td>{{ formatTime(record.officeFrom) }}</td>
|
||||
<td>{{ formatTime(record.officeTo) }}</td>
|
||||
<td>{{ record.officeBreak }} min</td>
|
||||
<td>{{ formatTime(record.outsideFrom) }}</td>
|
||||
<td>{{ formatTime(record.outsideTo) }}</td>
|
||||
<td>{{ record.outsideBreak }} min</td>
|
||||
<td>{{ formatTime(record.afterFrom) }}</td>
|
||||
<td>{{ formatTime(record.afterTo) }}</td>
|
||||
<td>{{ record.afterBreak }} min</td>
|
||||
<td>{{ formatHourMinute(calcTotalTime(record)) }}</td>
|
||||
<td>{{ calcBreakTotal(record) }}</td>
|
||||
<td>{{ formatHourMinute(calcNetHours(record)) }}</td>
|
||||
@ -143,10 +143,10 @@
|
||||
};
|
||||
},
|
||||
calcTotalHours(r) {
|
||||
return this.getTimeDiff(r.officeFrom, r.officeTo) + this.getTimeDiff(r.outsideFrom, r.outsideTo);
|
||||
return this.getTimeDiff(r.afterFrom, r.officeTo) + this.getTimeDiff(r.afterFrom, r.afterTo);
|
||||
},
|
||||
calcBreakTotal(r) {
|
||||
return (r.officeBreak || 0) + (r.outsideBreak || 0);
|
||||
return (r.officeBreak || 0) + (r.afterBreak || 0);
|
||||
},
|
||||
calcNetHours(r) {
|
||||
const totalMinutes = (this.calcTotalHours(r) * 60) - this.calcBreakTotal(r);
|
||||
|
||||
@ -41,26 +41,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OUTSIDE OFFICE HOURS -->
|
||||
<h6 class="fw-bold text-danger">OUTSIDE OFFICE HOURS</h6>
|
||||
<!-- AFTER OFFICE HOURS -->
|
||||
<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="outsideFrom">From</label>
|
||||
<input type="time" id="outsideFrom" class="form-control" v-model="editForm.outsideFrom"
|
||||
<label for="afterFrom">From</label>
|
||||
<input type="time" id="afterFrom" class="form-control" v-model="editForm.afterFrom"
|
||||
v-on:input="calculateOTAndBreak">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label for="outsideTo">To</label>
|
||||
<input type="time" id="outsideTo" class="form-control" v-model="editForm.outsideTo"
|
||||
<label for="afterTo">To</label>
|
||||
<input type="time" id="afterTo" class="form-control" v-model="editForm.afterTo"
|
||||
v-on:input="calculateOTAndBreak">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label for="outsideBreak">Break Hours (Minutes)</label>
|
||||
<label for="afterBreak">Break Hours (Minutes)</label>
|
||||
<div class="d-flex">
|
||||
<input type="number" id="outsideBreak" class="form-control"
|
||||
v-model="editForm.outsideBreak"
|
||||
<input type="number" id="afterBreak" class="form-control"
|
||||
v-model="editForm.afterBreak"
|
||||
v-on:input="calculateOTAndBreak" placeholder="e.g. 45">
|
||||
<button class="btn btn-outline-danger ms-2" v-on:click="clearOutsideHours" title="Clear Outside Hours">
|
||||
<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>
|
||||
</div>
|
||||
@ -113,18 +113,6 @@
|
||||
<label class="form-check-label" for="publicHoliday">Public Holiday</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 mt-3">
|
||||
<label for="fileUpload">Upload File:</label>
|
||||
<input type="file" id="fileUpload" class="form-control" accept="application/pdf"
|
||||
v-on:change="handleFileUpload" ref="fileInput">
|
||||
<small class="text-danger">*upload pdf file only</small>
|
||||
</div>
|
||||
<div v-if="editForm.filePath" class="mb-2">
|
||||
<a :href="editForm.filePath" target="_blank" class="btn btn-outline-primary btn-sm">
|
||||
View Existing PDF
|
||||
</a>
|
||||
</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"
|
||||
@ -161,20 +149,17 @@
|
||||
officeFrom: "",
|
||||
officeTo: "",
|
||||
officeBreak: 0,
|
||||
outsideFrom: "",
|
||||
outsideTo: "",
|
||||
outsideBreak: 0,
|
||||
afterFrom: "",
|
||||
afterTo: "",
|
||||
afterBreak: 0,
|
||||
stationId: "",
|
||||
otDescription: "",
|
||||
otDays: "",
|
||||
filePath: "",
|
||||
newFile: null,
|
||||
userId: null,
|
||||
},
|
||||
airstationList: [],
|
||||
totalOTHours: "0 hr 0 min",
|
||||
totalBreakHours: "0 hr 0 min",
|
||||
uploadedFile: null,
|
||||
currentUser: null,
|
||||
isPSTWAIR: false,
|
||||
};
|
||||
@ -266,19 +251,19 @@
|
||||
this.editForm.officeBreak
|
||||
);
|
||||
|
||||
let outsideOT = this.calculateTimeDifference(
|
||||
this.editForm.outsideFrom,
|
||||
this.editForm.outsideTo,
|
||||
this.editForm.outsideBreak
|
||||
let afterOT = this.calculateTimeDifference(
|
||||
this.editForm.afterFrom,
|
||||
this.editForm.afterTo,
|
||||
this.editForm.afterBreak
|
||||
);
|
||||
|
||||
let totalOTMinutes = officeOT.minutes + outsideOT.minutes;
|
||||
let totalOTHours = officeOT.hours + outsideOT.hours + Math.floor(totalOTMinutes / 60);
|
||||
let totalOTMinutes = officeOT.minutes + afterOT.minutes;
|
||||
let totalOTHours = officeOT.hours + afterOT.hours + Math.floor(totalOTMinutes / 60);
|
||||
totalOTMinutes = totalOTMinutes % 60;
|
||||
|
||||
this.totalOTHours = `${totalOTHours} hr ${totalOTMinutes} min`;
|
||||
|
||||
let totalBreakMinutes = (this.editForm.officeBreak || 0) + (this.editForm.outsideBreak || 0);
|
||||
let totalBreakMinutes = (this.editForm.officeBreak || 0) + (this.editForm.afterBreak || 0);
|
||||
let totalBreakHours = Math.floor(totalBreakMinutes / 60);
|
||||
totalBreakMinutes = totalBreakMinutes % 60;
|
||||
|
||||
@ -308,24 +293,53 @@
|
||||
const [hours, minutes] = timeString.split(':').map(Number);
|
||||
return { hours, minutes };
|
||||
},
|
||||
handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (file && file.type === "application/pdf") {
|
||||
this.editForm.newFile = file;
|
||||
} else {
|
||||
alert("Please upload a valid PDF file.");
|
||||
}
|
||||
},
|
||||
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
|
||||
},
|
||||
async updateRecord() {
|
||||
async updateRecord() {
|
||||
if (this.editForm.officeFrom && !this.editForm.officeTo) {
|
||||
alert("Please enter a 'To' time for Office Hours.");
|
||||
return;
|
||||
}
|
||||
if (this.editForm.officeTo && !this.editForm.officeFrom) {
|
||||
alert("Please enter a 'From' time for Office Hours.");
|
||||
return;
|
||||
}
|
||||
if (this.editForm.afterFrom && !this.editForm.afterTo) {
|
||||
alert("Please enter a 'To' time for After Hours.");
|
||||
return;
|
||||
}
|
||||
if (this.editForm.afterTo && !this.editForm.afterFrom) {
|
||||
alert("Please enter a 'From' time for After Hours.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if both Office and After hours are empty
|
||||
if ((!this.editForm.officeFrom || !this.editForm.officeTo) &&
|
||||
(!this.editForm.afterFrom || !this.editForm.afterTo)) {
|
||||
alert("Please enter either Office Hours or After Hours.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Existing validation for "From" < "To" logic
|
||||
if (this.editForm.officeFrom && this.editForm.officeTo) {
|
||||
if (this.editForm.officeTo <= this.editForm.officeFrom) {
|
||||
alert("Invalid Office Time: 'To' time must be later than 'From' time (same day only).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.editForm.afterFrom && this.editForm.afterTo) {
|
||||
if (this.editForm.afterTo <= this.editForm.afterFrom) {
|
||||
alert("Invalid After Time: 'To' time must be later than 'From' time (same day only).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("OvertimeId", this.editForm.overtimeId);
|
||||
formData.append("OtDate", this.editForm.otDate);
|
||||
formData.append("StationId", this.editForm.stationId || "");
|
||||
@ -333,47 +347,38 @@
|
||||
formData.append("OtDays", this.editForm.selectedDayType);
|
||||
formData.append("OfficeFrom", this.formatTime(this.editForm.officeFrom) || "");
|
||||
formData.append("OfficeTo", this.formatTime(this.editForm.officeTo) || "");
|
||||
formData.append("OutsideFrom", this.formatTime(this.editForm.outsideFrom) || "");
|
||||
formData.append("OutsideTo", this.formatTime(this.editForm.outsideTo) || "");
|
||||
formData.append("FilePath", this.editForm.filePath || "");
|
||||
|
||||
|
||||
// Conditionally include nullable fields
|
||||
if (this.editForm.outsideFrom) {
|
||||
formData.append("outsideFrom", this.formatTime(this.editForm.outsideFrom));
|
||||
}
|
||||
if (this.editForm.outsideTo) {
|
||||
formData.append("outsideTo", this.formatTime(this.editForm.outsideTo));
|
||||
}
|
||||
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("outsideBreak", this.editForm.outsideBreak || 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);
|
||||
|
||||
// File handling
|
||||
if (this.editForm.newFile) {
|
||||
formData.append("newFile", this.editForm.newFile);
|
||||
} else {
|
||||
formData.append("filePath", this.editForm.filePath || ""); // must send this
|
||||
}
|
||||
|
||||
const res = await fetch('/OvertimeAPI/UpdateOvertimeRecord', {
|
||||
const response = await fetch(`/OvertimeAPI/UpdateOvertimeRecord`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert("Record updated successfully.");
|
||||
window.location.href = "/OTcalculate/Overtime/OtRecords";
|
||||
if (response.ok) {
|
||||
alert("Overtime record updated successfully!");
|
||||
|
||||
window.location.href = '/OTcalculate/Overtime/OtRecords';
|
||||
} else {
|
||||
const errorText = await res.text();
|
||||
console.error("Server returned:", errorText);
|
||||
alert("Failed to update record.");
|
||||
alert("Failed to update overtime record.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Update error:", err);
|
||||
} catch (error) {
|
||||
console.error("Error updating record:", error);
|
||||
alert("An error occurred while updating the overtime record.");
|
||||
}
|
||||
},
|
||||
|
||||
@ -386,10 +391,10 @@
|
||||
this.editForm.officeBreak = 0;
|
||||
this.calculateOTAndBreak();
|
||||
},
|
||||
clearOutsideHours() {
|
||||
this.editForm.outsideFrom = "";
|
||||
this.editForm.outsideTo = "";
|
||||
this.editForm.outsideBreak = 0;
|
||||
clearAfterHours() {
|
||||
this.editForm.afterFrom = "";
|
||||
this.editForm.afterTo = "";
|
||||
this.editForm.afterBreak = 0;
|
||||
this.calculateOTAndBreak();
|
||||
},
|
||||
|
||||
|
||||
@ -96,15 +96,14 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header-green" rowspan="2">Date</th>
|
||||
<th class="header-blue" colspan="3">Office<br><small>(8:30 - 17:30)</small></th>
|
||||
<th class="header-blue" colspan="3">Outside<br><small>(17:30 - 8:30)</small></th>
|
||||
<th class="header-blue" colspan="3">Office Hour<br><small>(8:30 - 17:30)</small></th>
|
||||
<th class="header-blue" colspan="3">After Office Hour<br><small>(17:30 - 8:30)</small></th>
|
||||
<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 Hours</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>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -122,9 +121,9 @@
|
||||
<td>{{ formatTime(record.officeFrom) }}</td>
|
||||
<td>{{ formatTime(record.officeTo) }}</td>
|
||||
<td>{{ record.officeBreak }} min</td>
|
||||
<td>{{ formatTime(record.outsideFrom) }}</td>
|
||||
<td>{{ formatTime(record.outsideTo) }}</td>
|
||||
<td>{{ record.outsideBreak }} min</td>
|
||||
<td>{{ formatTime(record.afterFrom) }}</td>
|
||||
<td>{{ formatTime(record.afterTo) }}</td>
|
||||
<td>{{ record.afterBreak }} min</td>
|
||||
<td>{{ formatHourMinute(calcTotalTime(record)) }}</td>
|
||||
<td>{{ calcBreakTotal(record) }}</td>
|
||||
<td>{{ formatHourMinute(calcNetHours(record)) }}</td>
|
||||
@ -135,14 +134,6 @@
|
||||
{{ record.otDescription }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="record.filePath">
|
||||
<a :href="record.filePath" target="_blank" class="btn btn-light border rounded-circle" title="View PDF">
|
||||
<i class="bi bi-file-earmark-pdf-fill fs-5"></i>
|
||||
</a>
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td>
|
||||
<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>
|
||||
@ -163,7 +154,7 @@
|
||||
<td>{{ formatHourMinute(totalBreak) }}</td>
|
||||
<td>{{ formatHourMinute(totalNetTime) }}</td>
|
||||
<td v-if="isPSTWAIR"></td>
|
||||
<td colspan="4"></td>
|
||||
<td colspan="3"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -280,10 +271,10 @@
|
||||
};
|
||||
},
|
||||
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.afterFrom, r.afterTo);
|
||||
},
|
||||
calcBreakTotal(r) {
|
||||
return (r.officeBreak || 0) + (r.outsideBreak || 0);
|
||||
return (r.officeBreak || 0) + (r.afterBreak || 0);
|
||||
},
|
||||
calcNetHours(r) {
|
||||
const totalMinutes = (this.calcTotalHours(r) * 60) - this.calcBreakTotal(r);
|
||||
@ -359,9 +350,9 @@
|
||||
officeFrom: record.officeFrom,
|
||||
officeTo: record.officeTo,
|
||||
officeBreak: record.officeBreak,
|
||||
outsideFrom: record.outsideFrom,
|
||||
outsideTo: record.outsideTo,
|
||||
outsideBreak: record.outsideBreak,
|
||||
afterFrom: record.afterFrom,
|
||||
afterTo: record.afterTo,
|
||||
afterBreak: record.afterBreak,
|
||||
stationId: record.stationId,
|
||||
otDescription: record.otDescription,
|
||||
otDays: record.otDays,
|
||||
@ -377,7 +368,6 @@
|
||||
});
|
||||
if (res.ok) {
|
||||
alert("Overtime records submitted for review.");
|
||||
// Optionally, clear the local records or redirect the user
|
||||
} else {
|
||||
alert("Submission failed: " + await res.text());
|
||||
}
|
||||
|
||||
@ -35,21 +35,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="fw-bold text-danger">OUTSIDE OFFICE HOURS</h6>
|
||||
<h6 class="fw-bold text-danger">AFTER OFFICE HOURS</h6>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4">
|
||||
<label for="outsideFrom">From</label>
|
||||
<input type="time" id="outsideFrom" class="form-control" v-model="outsideFrom"
|
||||
<label for="afterFrom">From</label>
|
||||
<input type="time" id="afterFrom" class="form-control" v-model="afterFrom"
|
||||
v-on:input="calculateOTAndBreak">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label for="outsideTo">To</label>
|
||||
<input type="time" id="outsideTo" class="form-control" v-model="outsideTo"
|
||||
<label for="afterTo">To</label>
|
||||
<input type="time" id="afterTo" class="form-control" v-model="afterTo"
|
||||
v-on:input="calculateOTAndBreak">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label for="outsideBreak">Break Hours (Minutes)</label>
|
||||
<input type="number" id="outsideBreak" class="form-control" v-model="outsideBreak"
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
@ -98,13 +98,6 @@
|
||||
<label class="form-check-label" for="publicHoliday">Public Holiday</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 mt-3">
|
||||
<label for="fileUpload">Upload File:</label>
|
||||
<input type="file" id="fileUpload" class="form-control" accept="application/pdf"
|
||||
v-on:change="handleFileUpload" ref="fileInput">
|
||||
<small class="text-danger">*upload pdf file only</small>
|
||||
</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"
|
||||
@ -139,16 +132,15 @@
|
||||
officeFrom: "",
|
||||
officeTo: "",
|
||||
officeBreak: 0,
|
||||
outsideFrom: "",
|
||||
outsideTo: "",
|
||||
outsideBreak: 0,
|
||||
afterFrom: "",
|
||||
afterTo: "",
|
||||
afterBreak: 0,
|
||||
selectedAirStation: "",
|
||||
airstationList: [],
|
||||
otDescription: "",
|
||||
selectedDayType: "",
|
||||
totalOTHours: "0 hr 0 min",
|
||||
totalBreakHours: "0 hr 0 min",
|
||||
uploadedFile: null,
|
||||
currentUser: null,
|
||||
userId: null,
|
||||
isPSTWAIR: false,
|
||||
@ -207,15 +199,15 @@
|
||||
},
|
||||
calculateOTAndBreak() {
|
||||
let officeOT = this.calculateTimeDifference(this.officeFrom, this.officeTo, this.officeBreak);
|
||||
let outsideOT = this.calculateTimeDifference(this.outsideFrom, this.outsideTo, this.outsideBreak);
|
||||
let afterOT = this.calculateTimeDifference(this.afterFrom, this.afterTo, this.afterBreak);
|
||||
|
||||
let totalOTMinutes = officeOT.minutes + outsideOT.minutes;
|
||||
let totalOTHours = officeOT.hours + outsideOT.hours + Math.floor(totalOTMinutes / 60);
|
||||
let totalOTMinutes = officeOT.minutes + afterOT.minutes;
|
||||
let totalOTHours = officeOT.hours + afterOT.hours + Math.floor(totalOTMinutes / 60);
|
||||
totalOTMinutes = totalOTMinutes % 60;
|
||||
|
||||
this.totalOTHours = `${totalOTHours} hr ${totalOTMinutes} min`;
|
||||
|
||||
let totalBreakMinutes = (this.officeBreak || 0) + (this.outsideBreak || 0);
|
||||
let totalBreakMinutes = (this.officeBreak || 0) + (this.afterBreak || 0);
|
||||
let totalBreakHours = Math.floor(totalBreakMinutes / 60);
|
||||
totalBreakMinutes = totalBreakMinutes % 60;
|
||||
|
||||
@ -245,9 +237,6 @@
|
||||
const [hours, minutes] = timeString.split(':').map(Number);
|
||||
return { hours, minutes };
|
||||
},
|
||||
handleFileUpload(event) {
|
||||
this.uploadedFile = event.target.files[0];
|
||||
},
|
||||
formatTime(timeString) {
|
||||
if (!timeString) return null;
|
||||
const [hours, minutes] = timeString.split(':');
|
||||
@ -259,35 +248,75 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.uploadedFile || this.uploadedFile.type !== "application/pdf") {
|
||||
alert("Please upload a valid PDF file.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.userId) {
|
||||
console.error("User ID is not set!");
|
||||
alert("User information is missing. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", this.uploadedFile);
|
||||
formData.append("otDate", this.selectedDate);
|
||||
formData.append("officeFrom", this.officeFrom ? this.formatTime(this.officeFrom) : null);
|
||||
formData.append("officeTo", this.officeTo ? this.formatTime(this.officeTo) : null);
|
||||
formData.append("officeBreak", this.officeBreak);
|
||||
formData.append("outsideFrom", this.outsideFrom ? this.formatTime(this.outsideFrom) : null);
|
||||
formData.append("outsideTo", this.outsideTo ? this.formatTime(this.outsideTo) : null);
|
||||
formData.append("outsideBreak", this.outsideBreak);
|
||||
formData.append("stationId", this.isPSTWAIR ? parseInt(this.selectedAirStation) : "");
|
||||
formData.append("otDescription", this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '));
|
||||
formData.append("otDays", this.selectedDayType);
|
||||
formData.append("userId", this.userId);
|
||||
// Validate office hours
|
||||
if (this.officeTo && !this.officeFrom) {
|
||||
alert("Please fill in the 'From' time for office hours.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.officeFrom && !this.officeTo) {
|
||||
alert("Please fill in the 'To' time for office hours.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.officeFrom && this.officeTo) {
|
||||
if (this.officeTo <= this.officeFrom) {
|
||||
alert("Invalid Office Hour Time: 'To' time must be later than 'From' time (same day only).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate after hours
|
||||
if (this.afterTo && !this.afterFrom) {
|
||||
alert("Please fill in the 'From' time for after office hours.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.afterFrom && !this.afterTo) {
|
||||
alert("Please fill in the 'To' time for after office hours.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.afterFrom && this.afterTo) {
|
||||
if (this.afterTo <= this.afterFrom) {
|
||||
alert("Invalid After Office Hour Time: 'To' time must be later than 'From' time (same day only).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Require at least one: Office or After
|
||||
if ((!this.officeFrom || !this.officeTo) && (!this.afterFrom || !this.afterTo)) {
|
||||
alert("Please enter either Office Hours or After Office Hours.");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestData = {
|
||||
otDate: this.selectedDate,
|
||||
officeFrom: this.officeFrom ? this.formatTime(this.officeFrom) : null,
|
||||
officeTo: this.officeTo ? this.formatTime(this.officeTo) : null,
|
||||
officeBreak: this.officeBreak || null, // Make this optional
|
||||
afterFrom: this.afterFrom ? this.formatTime(this.afterFrom) : null, // Set to null if empty
|
||||
afterTo: this.afterTo ? this.formatTime(this.afterTo) : null, // Set to null if empty
|
||||
afterBreak: this.afterBreak || null, // Make this optional
|
||||
stationId: this.isPSTWAIR ? parseInt(this.selectedAirStation) : "",
|
||||
otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '),
|
||||
otDays: this.selectedDayType,
|
||||
userId: this.userId
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${window.location.origin}/OvertimeAPI/AddOvertime`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(requestData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@ -304,21 +333,20 @@
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
clearForm() {
|
||||
this.selectedDate = "";
|
||||
this.officeFrom = "";
|
||||
this.officeTo = "";
|
||||
this.officeBreak = 0;
|
||||
this.outsideFrom = "";
|
||||
this.outsideTo = "";
|
||||
this.outsideBreak = 0;
|
||||
this.afterFrom = "";
|
||||
this.afterTo = "";
|
||||
this.afterBreak = 0;
|
||||
this.selectedAirStation = "";
|
||||
this.otDescription = "";
|
||||
this.selectedDayType = "";
|
||||
this.totalOTHours = "0 hr 0 min";
|
||||
this.totalBreakHours = "0 hr 0 min";
|
||||
this.uploadedFile = null;
|
||||
this.$refs.fileInput.value = '';
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@ -391,9 +391,9 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
|
||||
|
||||
[HttpPost("AddOvertime")]
|
||||
public async Task<IActionResult> AddOvertimeAsync([FromForm] OvertimeRequestDto request)
|
||||
public async Task<IActionResult> AddOvertimeAsync([FromBody] OvertimeRequestDto request)
|
||||
{
|
||||
_logger.LogInformation("AddOvertimeAsync called (with file upload).");
|
||||
_logger.LogInformation("AddOvertimeAsync called.");
|
||||
|
||||
if (request == null)
|
||||
{
|
||||
@ -409,59 +409,35 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
return BadRequest("User ID is required.");
|
||||
}
|
||||
|
||||
// Parse times
|
||||
TimeSpan? officeFrom = TimeSpan.TryParse(request.OfficeFrom, out var of) ? of : (TimeSpan?)null;
|
||||
TimeSpan? officeTo = TimeSpan.TryParse(request.OfficeTo, out var ot) ? ot : (TimeSpan?)null;
|
||||
TimeSpan? outsideFrom = TimeSpan.TryParse(request.OutsideFrom, out var ofr) ? ofr : (TimeSpan?)null;
|
||||
TimeSpan? outsideTo = TimeSpan.TryParse(request.OutsideTo, out var otr) ? otr : (TimeSpan?)null;
|
||||
// Parse times (make them optional)
|
||||
TimeSpan? officeFrom = string.IsNullOrEmpty(request.OfficeFrom) ? (TimeSpan?)null : TimeSpan.Parse(request.OfficeFrom);
|
||||
TimeSpan? officeTo = string.IsNullOrEmpty(request.OfficeTo) ? (TimeSpan?)null : TimeSpan.Parse(request.OfficeTo);
|
||||
TimeSpan? afterFrom = string.IsNullOrEmpty(request.AfterFrom) ? (TimeSpan?)null : TimeSpan.Parse(request.AfterFrom);
|
||||
TimeSpan? afterTo = string.IsNullOrEmpty(request.AfterTo) ? (TimeSpan?)null : TimeSpan.Parse(request.AfterTo);
|
||||
|
||||
// Office time validation: if officeFrom is set, officeTo must be set too, and vice versa
|
||||
if ((officeFrom != null && officeTo == null) || (officeFrom == null && officeTo != null))
|
||||
{
|
||||
return BadRequest("Both Office From and To must be filled if one is provided.");
|
||||
}
|
||||
|
||||
if ((outsideFrom != null && outsideTo == null) || (outsideFrom == null && outsideTo != null))
|
||||
{
|
||||
return BadRequest("Both Outside From and To must be filled if one is provided.");
|
||||
}
|
||||
// No need for specific validation for AfterFrom and AfterTo being both present
|
||||
// If one is null and the other isn't, that's acceptable now.
|
||||
|
||||
// Save file
|
||||
string pdfPath = null;
|
||||
if (request.File != null && request.File.Length > 0)
|
||||
{
|
||||
var uploadsFolder = Path.Combine(_env.WebRootPath, "Media", "Overtime");
|
||||
|
||||
if (!Directory.Exists(uploadsFolder))
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
|
||||
var fileName = $"OT_{Guid.NewGuid()}{Path.GetExtension(request.File.FileName)}";
|
||||
var filePath = Path.Combine(uploadsFolder, fileName);
|
||||
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await request.File.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
// This is the relative public URL path
|
||||
pdfPath = $"/media/overtime/{fileName}";
|
||||
}
|
||||
|
||||
|
||||
// Map to DB model
|
||||
// Map to DB model (continue using null for optional fields)
|
||||
var newRecord = new OtRegisterModel
|
||||
{
|
||||
OtDate = request.OtDate,
|
||||
OfficeFrom = officeFrom,
|
||||
OfficeTo = officeTo,
|
||||
OfficeBreak = request.OfficeBreak,
|
||||
OutsideFrom = outsideFrom,
|
||||
OutsideTo = outsideTo,
|
||||
OutsideBreak = request.OutsideBreak,
|
||||
OfficeBreak = request.OfficeBreak ?? null, // Make OfficeBreak optional
|
||||
AfterFrom = afterFrom,
|
||||
AfterTo = afterTo,
|
||||
AfterBreak = request.AfterBreak ?? null, // Make AfterBreak optional
|
||||
StationId = request.StationId,
|
||||
OtDescription = request.OtDescription,
|
||||
OtDays = request.OtDays,
|
||||
UserId = request.UserId,
|
||||
FilePath = pdfPath
|
||||
};
|
||||
|
||||
_centralDbContext.Otregisters.Add(newRecord);
|
||||
@ -475,8 +451,6 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
return StatusCode(500, "An error occurred while saving overtime.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Ot Records
|
||||
@ -494,14 +468,13 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
o.OfficeFrom,
|
||||
o.OfficeTo,
|
||||
o.OfficeBreak,
|
||||
o.OutsideFrom,
|
||||
o.OutsideTo,
|
||||
o.OutsideBreak,
|
||||
o.AfterFrom,
|
||||
o.AfterTo,
|
||||
o.AfterBreak,
|
||||
o.StationId,
|
||||
StationName = o.Stations != null ? o.Stations.StationName : "N/A",
|
||||
o.OtDescription,
|
||||
o.OtDays,
|
||||
o.FilePath,
|
||||
o.UserId
|
||||
})
|
||||
.OrderByDescending(o => o.OtDate)
|
||||
@ -528,22 +501,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
return NotFound("Overtime record not found.");
|
||||
}
|
||||
|
||||
// 1. Delete the file from the server
|
||||
if (!string.IsNullOrEmpty(record.FilePath))
|
||||
{
|
||||
var filePath = Path.Combine(_env.WebRootPath, record.FilePath.TrimStart('/')); // Construct full path
|
||||
if (System.IO.File.Exists(filePath))
|
||||
{
|
||||
System.IO.File.Delete(filePath);
|
||||
_logger.LogInformation("File deleted successfully: {FilePath}", filePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("File not found, could not delete: {FilePath}", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Delete the record from the database
|
||||
// Delete the record from the database (No file handling anymore)
|
||||
_centralDbContext.Otregisters.Remove(record);
|
||||
_centralDbContext.SaveChanges();
|
||||
|
||||
@ -557,48 +515,6 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
}
|
||||
}
|
||||
|
||||
[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.FilePath = record.FilePath;
|
||||
_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)
|
||||
{
|
||||
@ -653,7 +569,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
|
||||
[HttpPost]
|
||||
[Route("UpdateOvertimeRecord")]
|
||||
public async Task<IActionResult> UpdateOvertimeRecord([FromForm] OtRegisterModel model, IFormFile? newFile)
|
||||
public IActionResult UpdateOvertimeRecord([FromForm] OtRegisterModel model)
|
||||
{
|
||||
_logger.LogInformation("UpdateOvertimeRecord called. Model: {@Model}", model);
|
||||
|
||||
@ -674,33 +590,13 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
|
||||
_logger.LogInformation("Existing record found: {@ExistingRecord}", existing);
|
||||
|
||||
string? oldFilePath = existing.FilePath; // Store the old file path
|
||||
|
||||
string? newPdfPath = null; // Initialize the new file path
|
||||
|
||||
// Handle new file upload
|
||||
if (newFile != null && newFile.Length > 0)
|
||||
{
|
||||
var fileName = $"OT_{Guid.NewGuid()}{Path.GetExtension(newFile.FileName)}";
|
||||
var savePath = Path.Combine(_env.WebRootPath, "media", "Overtime", fileName);
|
||||
newPdfPath = $"/media/Overtime/{fileName}";
|
||||
|
||||
using (var stream = new FileStream(savePath, FileMode.Create))
|
||||
{
|
||||
await newFile.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
existing.FilePath = newPdfPath; // Update the file path in the database
|
||||
}
|
||||
|
||||
// Update other fields
|
||||
existing.OtDate = model.OtDate;
|
||||
existing.OfficeFrom = model.OfficeFrom;
|
||||
existing.OfficeTo = model.OfficeTo;
|
||||
existing.OfficeBreak = model.OfficeBreak;
|
||||
existing.OutsideFrom = model.OutsideFrom;
|
||||
existing.OutsideTo = model.OutsideTo;
|
||||
existing.OutsideBreak = model.OutsideBreak;
|
||||
existing.AfterFrom = model.AfterFrom;
|
||||
existing.AfterTo = model.AfterTo;
|
||||
existing.AfterBreak = model.AfterBreak;
|
||||
existing.StationId = model.StationId;
|
||||
existing.OtDescription = model.OtDescription;
|
||||
existing.OtDays = model.OtDays;
|
||||
@ -709,21 +605,6 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
_centralDbContext.SaveChanges();
|
||||
_logger.LogInformation("Overtime record updated successfully for Id: {OvertimeId}", model.OvertimeId);
|
||||
|
||||
// Delete the old file after saving the new one (if a new file was uploaded)
|
||||
if (newFile != null && !string.IsNullOrEmpty(oldFilePath))
|
||||
{
|
||||
var fullOldFilePath = Path.Combine(_env.WebRootPath, oldFilePath.TrimStart('/'));
|
||||
if (System.IO.File.Exists(fullOldFilePath))
|
||||
{
|
||||
System.IO.File.Delete(fullOldFilePath);
|
||||
_logger.LogInformation("Old file deleted successfully: {OldFilePath}", fullOldFilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Old file not found, could not delete: {OldFilePath}", fullOldFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new { message = "Record updated successfully." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user