356 lines
16 KiB
Plaintext
356 lines
16 KiB
Plaintext
@{
|
|
ViewData["Title"] = "Register Overtime";
|
|
Layout = "~/Views/Shared/_Layout.cshtml";
|
|
}
|
|
|
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
|
|
<div id="app" class="container mt-4 d-flex justify-content-center">
|
|
<div class="card shadow-sm" style="width: 1100px;">
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-7">
|
|
<div class="mb-3">
|
|
<label class="form-label" for="dateInput">Date</label>
|
|
<input type="date" id="dateInput" class="form-control" v-model="selectedDate"
|
|
v-on:input="calculateOTAndBreak">
|
|
</div>
|
|
|
|
<h6 class="fw-bold">OFFICE HOURS</h6>
|
|
<div class="row mb-3">
|
|
<div class="col-4">
|
|
<label for="officeFrom">From</label>
|
|
<input type="time" id="officeFrom" class="form-control" v-model="officeFrom"
|
|
v-on:input="calculateOTAndBreak">
|
|
</div>
|
|
<div class="col-4">
|
|
<label for="officeTo">To</label>
|
|
<input type="time" id="officeTo" class="form-control" v-model="officeTo"
|
|
v-on:input="calculateOTAndBreak">
|
|
</div>
|
|
<div class="col-4">
|
|
<label for="officeBreak">Break Hours (Minutes)</label>
|
|
<input type="number" id="officeBreak" class="form-control" v-model="officeBreak"
|
|
v-on:input="calculateOTAndBreak" placeholder="e.g. 30">
|
|
</div>
|
|
</div>
|
|
|
|
<h6 class="fw-bold text-danger">AFTER OFFICE HOURS</h6>
|
|
<div class="row mb-2">
|
|
<div class="col-4">
|
|
<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="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="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>
|
|
|
|
<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>
|
|
<option v-for="station in airstationList" :key="station.stationId" :value="station.stationId">
|
|
{{ station.stationName || 'Unnamed Station' }}
|
|
</option>
|
|
</select>
|
|
<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"
|
|
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">
|
|
<label class="mb-2">Day</label>
|
|
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" id="weekday" v-model="selectedDayType" value="Weekday">
|
|
<label class="form-check-label" for="weekday">Weekday</label>
|
|
</div>
|
|
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" id="weekend" v-model="selectedDayType" value="Weekend">
|
|
<label class="form-check-label" for="weekend">Weekend</label>
|
|
</div>
|
|
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" id="publicHoliday" v-model="selectedDayType"
|
|
value="Public Holiday">
|
|
<label class="form-check-label" for="publicHoliday">Public Holiday</label>
|
|
</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"
|
|
style="width: 200px;" readonly>
|
|
</div>
|
|
|
|
<div class="mb-3 d-flex flex-column align-items-center">
|
|
<label for="totalBreakHours">Total Break Hours</label>
|
|
<input type="text" id="totalBreakHours" class="form-control text-center" v-model="totalBreakHours"
|
|
style="width: 200px;" readonly>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-end mt-3">
|
|
<button class="btn btn-danger" v-on:click="clearForm">Clear</button>
|
|
<button class="btn btn-success ms-3" v-on:click="addOvertime">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
@{
|
|
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
|
}
|
|
<script>
|
|
const app = Vue.createApp({
|
|
data() {
|
|
return {
|
|
selectedDate: "",
|
|
officeFrom: "",
|
|
officeTo: "",
|
|
officeBreak: 0,
|
|
afterFrom: "",
|
|
afterTo: "",
|
|
afterBreak: 0,
|
|
selectedAirStation: "",
|
|
airstationList: [],
|
|
otDescription: "",
|
|
selectedDayType: "",
|
|
totalOTHours: "0 hr 0 min",
|
|
totalBreakHours: "0 hr 0 min",
|
|
currentUser: null,
|
|
userId: null,
|
|
isPSTWAIR: false,
|
|
|
|
};
|
|
},
|
|
computed: {
|
|
charCount() {
|
|
return this.otDescription.length;
|
|
}
|
|
},
|
|
async mounted() {
|
|
await this.fetchUser();
|
|
if (this.isPSTWAIR) {
|
|
this.fetchStations();
|
|
}
|
|
},
|
|
methods: {
|
|
async fetchStations() {
|
|
try {
|
|
const response = await fetch(`/OvertimeAPI/GetStationsByDepartment`);
|
|
if (!response.ok) throw new Error("Failed to fetch stations");
|
|
|
|
this.airstationList = await response.json();
|
|
} catch (error) {
|
|
console.error("Error fetching stations:", error);
|
|
}
|
|
},
|
|
async fetchUser() {
|
|
try {
|
|
const response = await fetch(`/IdentityAPI/GetUserInformation/`, { method: 'POST' });
|
|
if (response.ok) {
|
|
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}`);
|
|
}
|
|
} catch (error) {
|
|
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 afterOT = this.calculateTimeDifference(this.afterFrom, this.afterTo, this.afterBreak);
|
|
|
|
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.afterBreak || 0);
|
|
let totalBreakHours = Math.floor(totalBreakMinutes / 60);
|
|
totalBreakMinutes = totalBreakMinutes % 60;
|
|
|
|
this.totalBreakHours = `${totalBreakHours} hr ${totalBreakMinutes} min`;
|
|
},
|
|
calculateTimeDifference(startTime, endTime, breakMinutes) {
|
|
if (!startTime || !endTime) {
|
|
return { hours: 0, minutes: 0 };
|
|
}
|
|
|
|
const start = this.parseTime(startTime);
|
|
const end = this.parseTime(endTime);
|
|
|
|
let diffMinutes = (end.hours * 60 + end.minutes) - (start.hours * 60 + start.minutes);
|
|
if (diffMinutes < 0) {
|
|
diffMinutes += 24 * 60;
|
|
}
|
|
|
|
diffMinutes -= breakMinutes || 0;
|
|
|
|
const hours = Math.floor(diffMinutes / 60);
|
|
const minutes = diffMinutes % 60;
|
|
|
|
return { hours, minutes };
|
|
},
|
|
parseTime(timeString) {
|
|
const [hours, minutes] = timeString.split(':').map(Number);
|
|
return { hours, minutes };
|
|
},
|
|
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 addOvertime() {
|
|
if (this.isPSTWAIR && !this.selectedAirStation) {
|
|
alert("Please fill in all required fields.");
|
|
return;
|
|
}
|
|
|
|
if (!this.userId) {
|
|
console.error("User ID is not set!");
|
|
alert("User information is missing. Please try again.");
|
|
return;
|
|
}
|
|
|
|
// 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",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(requestData),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`HTTP error! status: ${response.status} - ${errorText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
alert(result.message);
|
|
this.clearForm();
|
|
} catch (error) {
|
|
console.error("Error adding overtime:", error);
|
|
alert("Failed to save overtime. Please check the console for errors.");
|
|
}
|
|
},
|
|
|
|
|
|
clearForm() {
|
|
this.selectedDate = "";
|
|
this.officeFrom = "";
|
|
this.officeTo = "";
|
|
this.officeBreak = 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";
|
|
},
|
|
}
|
|
});
|
|
|
|
app.mount("#app");
|
|
</script>
|
|
} |