468 lines
23 KiB
Plaintext
468 lines
23 KiB
Plaintext
@{
|
|
ViewData["Title"] = "Edit 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">
|
|
<!-- Overtime Date Input -->
|
|
<div class="mb-3">
|
|
<label class="form-label" for="dateInput">Date</label>
|
|
<input type="date" class="form-control" v-model="editForm.otDate" v-on:input="calculateOTAndBreak">
|
|
</div>
|
|
|
|
<!-- Office Hours Section -->
|
|
<h6 class="fw-bold">OFFICE HOURS</h6>
|
|
<div class="d-flex gap-3 mb-3 align-items-end flex-wrap">
|
|
<div style="flex: 1;">
|
|
<label for="officeFrom">From</label>
|
|
<input type="time" id="officeFrom" class="form-control" v-model="editForm.officeFrom" v-on:change="updateTime('officeFrom')">
|
|
</div>
|
|
<div style="flex: 1;">
|
|
<label for="officeTo">To</label>
|
|
<input type="time" id="officeTo" class="form-control" v-model="editForm.officeTo" v-on:change="updateTime('officeTo')">
|
|
</div>
|
|
<div style="flex: 1;">
|
|
<label for="officeBreak">Break Hours (Minutes)</label>
|
|
<div class="d-flex">
|
|
<select id="officeBreak" class="form-control" v-model.number="editForm.officeBreak" v-on:change="calculateOTAndBreak">
|
|
<option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
|
</select>
|
|
<button class="btn btn-outline-danger ms-2" v-on:click="clearOfficeHours" title="Clear Office Hours">
|
|
<i class="bi bi-x-circle"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- After Office Hours Section -->
|
|
<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="afterFrom">From</label>
|
|
<input type="time" id="afterFrom" class="form-control" v-model="editForm.afterFrom" v-on:change="updateTime('afterFrom')">
|
|
</div>
|
|
<div style="flex: 1;">
|
|
<label for="afterTo">To</label>
|
|
<input type="time" id="afterTo" class="form-control" v-model="editForm.afterTo" v-on:change="updateTime('afterTo')">
|
|
</div>
|
|
<div style="flex: 1;">
|
|
<label for="afterBreak">Break Hours (Minutes)</label>
|
|
<div class="d-flex">
|
|
<select id="afterBreak" class="form-control" v-model.number="editForm.afterBreak" v-on:change="calculateOTAndBreak">
|
|
<option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
|
</select>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Air Station Dropdown (only for PSTW AIR) -->
|
|
<div class="mb-3" v-if="isPSTWAIR">
|
|
<label for="airstationDropdown">Air Station</label>
|
|
<select id="airstationDropdown" class="form-control" v-model="editForm.stationId">
|
|
<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>
|
|
|
|
<!-- Work Description Input -->
|
|
<div class="mb-3">
|
|
<label for="otDescription">Work Brief Description</label>
|
|
<textarea id="otDescription" class="form-control" v-model="editForm.otDescription" v-on:input="limitCharCount" placeholder="Describe the work done..."></textarea>
|
|
<small class="text-muted">{{ charCount }} / 150 characters</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Overtime Hours and Break Section -->
|
|
<div class="col-md-5 mt-5">
|
|
<div class="mb-3 d-flex flex-column align-items-center">
|
|
<label for="detectedDayType">Day</label>
|
|
<input type="text" class="form-control text-center" v-model="editForm.otDays" readonly style="width: 200px;">
|
|
</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>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="d-flex justify-content-end mt-3">
|
|
<button class="btn btn-danger" v-on:click="goBack">Cancel</button>
|
|
<button class="btn btn-success ms-3" v-on:click="updateRecord">Update</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
@section Scripts {
|
|
@{
|
|
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
|
}
|
|
<script>
|
|
const app = Vue.createApp({
|
|
data() {
|
|
return {
|
|
editForm: {
|
|
overtimeId: null,
|
|
otDate: "",
|
|
officeFrom: "",
|
|
officeTo: "",
|
|
officeBreak: 0,
|
|
afterFrom: "",
|
|
afterTo: "",
|
|
afterBreak: 0,
|
|
stationId: "",
|
|
otDescription: "",
|
|
otDays: "",
|
|
userId: null,
|
|
},
|
|
airstationList: [],
|
|
totalOTHours: "0 hr 0 min",
|
|
totalBreakHours: "0 hr 0 min",
|
|
currentUser: null,
|
|
isPSTWAIR: false,
|
|
userState: null, // To store the user's state information
|
|
publicHolidays: [], // To store public holidays
|
|
breakOptions: Array.from({ length: 15 }, (_, i) => {
|
|
const totalMinutes = i * 30;
|
|
const hours = Math.floor(totalMinutes / 60);
|
|
const minutes = totalMinutes % 60;
|
|
|
|
let label = '';
|
|
if (hours > 0) label += `${hours} hour${hours > 1 ? 's' : ''}`;
|
|
if (minutes > 0) label += `${label ? ' ' : ''}${minutes} min`;
|
|
if (!label) label = '0 min';
|
|
|
|
return { label, value: totalMinutes };
|
|
}),
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
charCount() {
|
|
return this.editForm.otDescription.length;
|
|
}
|
|
},
|
|
|
|
async mounted() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const overtimeId = urlParams.get('overtimeId');
|
|
if (overtimeId) {
|
|
await this.fetchOvertimeRecord(overtimeId);
|
|
}
|
|
|
|
await this.fetchUserAndRelatedData(); // Fetch user, state, and holidays
|
|
if (this.isPSTWAIR) {
|
|
await this.fetchStations();
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
async fetchOvertimeRecord(id) {
|
|
try {
|
|
const res = await fetch(`/OvertimeAPI/GetOvertimeRecordById/${id}`);
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
this.populateForm(data);
|
|
} else {
|
|
alert("Failed to fetch overtime record.");
|
|
}
|
|
} catch (err) {
|
|
console.error("Fetch error:", err);
|
|
}
|
|
},
|
|
|
|
populateForm(record) {
|
|
this.editForm = {
|
|
...record,
|
|
otDate: record.otDate ? record.otDate.slice(0, 10) : "",
|
|
// We will auto-detect the day, so we don't need to pre-fill otDays for auto-detection
|
|
};
|
|
this.calculateOTAndBreak();
|
|
this.updateDayType(); // Initial detection after loading data
|
|
},
|
|
|
|
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 fetchUserAndRelatedData() {
|
|
try {
|
|
const response = await fetch(`/IdentityAPI/GetUserInformation/`, { method: 'POST' });
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
this.currentUser = data?.userInfo || null;
|
|
this.editForm.userId = this.currentUser?.id || null;
|
|
|
|
const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin");
|
|
const isSystemAdmin = this.currentUser?.role?.includes("SystemAdmin");
|
|
const isDepartmentTwo = this.currentUser?.department?.departmentId === 2;
|
|
this.isPSTWAIR = isSuperAdmin || isSystemAdmin || isDepartmentTwo;
|
|
|
|
if (this.editForm.userId) {
|
|
await this.fetchUserStateAndHolidays();
|
|
}
|
|
} else {
|
|
console.error(`Failed to fetch user: ${response.statusText}`);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching user:", error);
|
|
}
|
|
},
|
|
|
|
async fetchUserStateAndHolidays() {
|
|
try {
|
|
const response = await fetch(`/OvertimeAPI/GetUserStateAndHolidays/${this.editForm.userId}`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch user state and holidays: ${response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
this.userState = data.state;
|
|
this.publicHolidays = data.publicHolidays;
|
|
this.updateDayType(); // Detect day type after loading data
|
|
} catch (error) {
|
|
console.error("Error fetching user state and holidays:", error);
|
|
this.editForm.otDays = "Weekday"; // Default if fetching fails
|
|
}
|
|
},
|
|
|
|
limitCharCount(event) {
|
|
if (this.editForm.otDescription && this.editForm.otDescription.length > 150) {
|
|
this.editForm.otDescription = this.editForm.otDescription.substring(0, 150);
|
|
event.preventDefault();
|
|
}
|
|
},
|
|
|
|
calculateOTAndBreak() {
|
|
let officeOT = this.calculateTimeDifference(
|
|
this.editForm.officeFrom,
|
|
this.editForm.officeTo,
|
|
this.editForm.officeBreak
|
|
);
|
|
|
|
let afterOT = this.calculateTimeDifference(
|
|
this.editForm.afterFrom,
|
|
this.editForm.afterTo,
|
|
this.editForm.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.editForm.officeBreak || 0) + (this.editForm.afterBreak || 0);
|
|
let totalBreakHours = Math.floor(totalBreakMinutes / 60);
|
|
totalBreakMinutes = totalBreakMinutes % 60;
|
|
|
|
this.totalBreakHours = `${totalBreakHours} hr ${totalBreakMinutes} min`;
|
|
this.updateDayType(); // Update day type when times or date change
|
|
},
|
|
updateTime(fieldName) {
|
|
if (fieldName === 'officeFrom') {
|
|
this.editForm.officeFrom = this.roundToNearest30(this.editForm.officeFrom);
|
|
} else if (fieldName === 'officeTo') {
|
|
this.editForm.officeTo = this.roundToNearest30(this.editForm.officeTo);
|
|
} else if (fieldName === 'afterFrom') {
|
|
this.editForm.afterFrom = this.roundToNearest30(this.editForm.afterFrom);
|
|
} else if (fieldName === 'afterTo') {
|
|
this.editForm.afterTo = this.roundToNearest30(this.editForm.afterTo);
|
|
}
|
|
// Recalculate OT and break times after rounding, if necessary
|
|
this.calculateOTAndBreak();
|
|
},
|
|
roundToNearest30(timeStr) {
|
|
if (!timeStr) return timeStr;
|
|
const [hours, minutes] = timeStr.split(':').map(Number);
|
|
const roundedMinutes = minutes < 15 ? 0 : minutes < 45 ? 30 : 0;
|
|
const adjustedHour = minutes < 45 ? hours : (hours + 1) % 24;
|
|
return `${adjustedHour.toString().padStart(2, '0')}:${roundedMinutes.toString().padStart(2, '0')}`;
|
|
},
|
|
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
|
|
},
|
|
|
|
handleDateChange() {
|
|
this.updateDayType();
|
|
this.calculateOTAndBreak();
|
|
},
|
|
|
|
updateDayType() {
|
|
if (!this.editForm.otDate || !this.userState) {
|
|
this.editForm.otDays = "";
|
|
return;
|
|
}
|
|
|
|
const selectedDateObj = new Date(this.editForm.otDate + "T00:00:00");
|
|
const dayOfWeek = selectedDateObj.getDay(); // 0 (Sunday) to 6 (Saturday)
|
|
const year = selectedDateObj.getFullYear();
|
|
const month = selectedDateObj.getMonth() + 1;
|
|
const day = selectedDateObj.getDate();
|
|
const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
|
|
|
|
if (this.publicHolidays.some(holiday => holiday.date === formattedDate)) {
|
|
this.editForm.otDays = "Public Holiday";
|
|
return;
|
|
}
|
|
|
|
const weekendId = this.userState.weekendId;
|
|
const isWeekend = (() => {
|
|
if (weekendId === 1) {
|
|
return dayOfWeek === 5 || dayOfWeek === 6; // Friday and Saturday
|
|
} else if (weekendId === 2) {
|
|
return dayOfWeek === 6 || dayOfWeek === 0; // Saturday and Sunday
|
|
} else {
|
|
return dayOfWeek === 0; // Default Sunday
|
|
}
|
|
})();
|
|
|
|
if (isWeekend) {
|
|
this.editForm.otDays = "Weekend";
|
|
return;
|
|
}
|
|
|
|
this.editForm.otDays = "Weekday";
|
|
},
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append("OvertimeId", this.editForm.overtimeId);
|
|
formData.append("OtDate", this.editForm.otDate);
|
|
formData.append("StationId", this.editForm.stationId || "");
|
|
formData.append("OtDescription", this.editForm.otDescription || "");
|
|
formData.append("OtDays", this.editForm.otDays); // Use the auto-detected day type
|
|
formData.append("OfficeFrom", this.formatTime(this.editForm.officeFrom) || "");
|
|
formData.append("OfficeTo", this.formatTime(this.editForm.officeTo) || "");
|
|
formData.append("AfterFrom", this.formatTime(this.editForm.afterFrom) || "");
|
|
formData.append("AfterTo", this.formatTime(this.editForm.afterTo) || "");
|
|
formData.append("officeBreak", this.editForm.officeBreak || 0);
|
|
formData.append("afterBreak", this.editForm.afterBreak || 0);
|
|
formData.append("userId", this.currentUser?.id);
|
|
|
|
try {
|
|
const response = await fetch(`/OvertimeAPI/UpdateOvertimeRecord`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (response.ok) {
|
|
alert("Overtime record updated successfully!");
|
|
window.location.href = '/OTcalculate/Overtime/OtRecords';
|
|
} else {
|
|
alert("Failed to update overtime record.");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error updating record:", error);
|
|
alert("An error occurred while updating the overtime record.");
|
|
}
|
|
},
|
|
|
|
goBack() {
|
|
window.location.href = "/OTcalculate/Overtime/OtRecords";
|
|
},
|
|
clearOfficeHours() {
|
|
this.editForm.officeFrom = "";
|
|
this.editForm.officeTo = "";
|
|
this.editForm.officeBreak = 0;
|
|
this.calculateOTAndBreak();
|
|
},
|
|
clearAfterHours() {
|
|
this.editForm.afterFrom = "";
|
|
this.editForm.afterTo = "";
|
|
this.editForm.afterBreak = 0;
|
|
this.calculateOTAndBreak();
|
|
},
|
|
|
|
}
|
|
});
|
|
|
|
app.mount("#app");
|
|
</script>
|
|
} |