662 lines
35 KiB
Plaintext
662 lines
35 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 <span class="text-danger">*</span></label>
|
|
<input type="date" id="dateInput" class="form-control" v-model="selectedDate"
|
|
v-on:input="handleDateChange">
|
|
</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:change="officeFrom = roundToNearest30(officeFrom); calculateOTAndBreak()">
|
|
</div>
|
|
<div class="col-4">
|
|
<label for="officeTo">To</label>
|
|
<input type="time" id="officeTo" class="form-control" v-model="officeTo"
|
|
v-on:change="officeTo = roundToNearest30(officeTo); calculateOTAndBreak()">
|
|
</div>
|
|
<div class="col-4">
|
|
<label for="officeBreak">Break Hours (Minutes)</label>
|
|
<select id="officeBreak" class="form-control" v-model.number="officeBreak" v-on:change="calculateOTAndBreak">
|
|
<option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">
|
|
{{ opt.label }}
|
|
</option>
|
|
</select>
|
|
</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:change="afterFrom = roundToNearest30(afterFrom); calculateOTAndBreak()">
|
|
</div>
|
|
<div class="col-4">
|
|
<label for="afterTo">To</label>
|
|
<input type="time" id="afterTo" class="form-control" v-model="afterTo"
|
|
v-on:change="afterTo = roundToNearest30(afterTo); calculateOTAndBreak()">
|
|
</div>
|
|
<div class="col-4">
|
|
<label for="afterBreak">Break Hours (Minutes)</label>
|
|
<select id="afterBreak" class="form-control" v-model.number="afterBreak" v-on:change="calculateOTAndBreak">
|
|
<option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">
|
|
{{ opt.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3" v-if="showAirDropdown">
|
|
<label for="airStationDropdown">Air Station <span class="text-danger" v-if="requiresStation">*</span></label>
|
|
<select id="airStationDropdown" class="form-control" style="width: 100%;" v-model="selectedAirStation">
|
|
<option value="">Select Air 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" v-if="showMarineDropdown">
|
|
<label for="marineStationDropdown">Marine Station <span class="text-danger" v-if="requiresStation">*</span></label>
|
|
<select id="marineStationDropdown" class="form-control" style="width: 100%;" v-model="selectedMarineStation">
|
|
<option value="">Select Marine Station</option>
|
|
<option v-for="station in marineStationList" :key="station.stationId" :value="station.stationId">
|
|
{{ station.stationName || 'Unnamed Station' }}
|
|
</option>
|
|
</select>
|
|
<small class="text-danger">*Only for PSTW MARINE</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="otDescription">Work Brief Description <span class="text-danger">*</span></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 mt-5">
|
|
<div class="mb-3 d-flex flex-column align-items-center">
|
|
<label for="flexiHourDisplay">Flexi Hour</label>
|
|
<input type="text" id="flexiHourDisplay" class="form-control text-center" v-model="userFlexiHour" readonly
|
|
style="width: 200px;">
|
|
</div>
|
|
|
|
<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="detectedDayType" 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>
|
|
|
|
<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" :disabled="!areUserSettingsComplete">Save</button>
|
|
</div>
|
|
<div v-if="!areUserSettingsComplete" class="alert alert-warning mt-3 text-center" role="alert">
|
|
Action Required: Your Flexi Hours, Approval Flow, Salary, or State settings have not been configured. Please contact the IT or HR department for assistance. You cannot save overtime until these are set.
|
|
</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,
|
|
// New properties for separate station selections
|
|
selectedAirStation: "", // Holds selected Air station ID
|
|
selectedMarineStation: "", // Holds selected Marine station ID
|
|
airStationList: [], // Stores stations for Air Department (DepartmentId = 2)
|
|
marineStationList: [], // Stores stations for Marine Department (DepartmentId = 3)
|
|
|
|
otDescription: "",
|
|
userFlexiHour: "",
|
|
detectedDayType: "",
|
|
totalOTHours: "0 hr 0 min",
|
|
totalBreakHours: "0 hr 0 min",
|
|
currentUser: null,
|
|
userId: null,
|
|
userState: null,
|
|
publicHolidays: [],
|
|
// Keep user's actual department ID and admin flag for conditional rendering
|
|
userDepartmentId: null, // The department ID from the current user's profile
|
|
isUserAdmin: false, // True if the user is a SuperAdmin or SystemAdmin
|
|
departmentName: "", // This will be dynamic based on the user's main department for hints
|
|
areUserSettingsComplete: false,
|
|
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.otDescription.length;
|
|
},
|
|
// Determines if the Air Station dropdown should be shown
|
|
showAirDropdown() {
|
|
return this.isUserAdmin || this.userDepartmentId === 2;
|
|
},
|
|
// Determines if the Marine Station dropdown should be shown
|
|
showMarineDropdown() {
|
|
return this.isUserAdmin || this.userDepartmentId === 3;
|
|
},
|
|
// This computed property selects the station ID to be sent to the backend.
|
|
// It assumes that if both are visible (for admins), only ONE should be selected.
|
|
// If both are selected by an admin, validation in addOvertime will catch it.
|
|
stationIdForSubmission() {
|
|
if (this.selectedAirStation) {
|
|
return parseInt(this.selectedAirStation);
|
|
}
|
|
if (this.selectedMarineStation) {
|
|
return parseInt(this.selectedMarineStation);
|
|
}
|
|
return null; // No station selected from either dropdown
|
|
},
|
|
// This indicates if *any* station selection is required based on visible dropdowns
|
|
requiresStation() {
|
|
return this.showAirDropdown || this.showMarineDropdown;
|
|
}
|
|
},
|
|
watch: {
|
|
// Watch for changes in airStationList to re-initialize Select2 for Air
|
|
airStationList: {
|
|
handler() {
|
|
this.$nextTick(() => {
|
|
const selectElement = $('#airStationDropdown');
|
|
if (selectElement.length) {
|
|
if (selectElement.data('select2')) {
|
|
selectElement.select2('destroy');
|
|
}
|
|
selectElement.select2({ theme: 'bootstrap4', placeholder: 'Select Air Station', allowClear: true });
|
|
// Set initial value if already selected
|
|
if (this.selectedAirStation) {
|
|
selectElement.val(this.selectedAirStation).trigger('change');
|
|
}
|
|
// Ensure event listener is set only once
|
|
selectElement.off('change.v-model-air').on('change.v-model-air', (event) => {
|
|
this.selectedAirStation = $(event.currentTarget).val();
|
|
});
|
|
}
|
|
});
|
|
},
|
|
deep: true
|
|
},
|
|
// Watch for changes in marineStationList to re-initialize Select2 for Marine
|
|
marineStationList: {
|
|
handler() {
|
|
this.$nextTick(() => {
|
|
const selectElement = $('#marineStationDropdown');
|
|
if (selectElement.length) {
|
|
if (selectElement.data('select2')) {
|
|
selectElement.select2('destroy');
|
|
}
|
|
selectElement.select2({ theme: 'bootstrap4', placeholder: 'Select Marine Station', allowClear: true });
|
|
// Set initial value if already selected
|
|
if (this.selectedMarineStation) {
|
|
selectElement.val(this.selectedMarineStation).trigger('change');
|
|
}
|
|
// Ensure event listener is set only once
|
|
selectElement.off('change.v-model-marine').on('change.v-model-marine', (event) => {
|
|
this.selectedMarineStation = $(event.currentTarget).val();
|
|
});
|
|
}
|
|
});
|
|
},
|
|
deep: true
|
|
},
|
|
// Keep selectedAirStation in sync with Select2 if it's already rendered
|
|
selectedAirStation(newVal) {
|
|
const selectElement = $('#airStationDropdown');
|
|
if (selectElement.length && selectElement.val() !== newVal) {
|
|
selectElement.val(newVal).trigger('change.select2');
|
|
}
|
|
},
|
|
// Keep selectedMarineStation in sync with Select2 if it's already rendered
|
|
selectedMarineStation(newVal) {
|
|
const selectElement = $('#marineStationDropdown');
|
|
if (selectElement.length && selectElement.val() !== newVal) {
|
|
selectElement.val(newVal).trigger('change.select2');
|
|
}
|
|
}
|
|
},
|
|
async mounted() {
|
|
await this.fetchUser();
|
|
if (this.userId) {
|
|
await this.checkUserSettings();
|
|
}
|
|
|
|
// Fetch stations for Air if the dropdown will be visible
|
|
if (this.showAirDropdown) {
|
|
await this.fetchStations(2, 'air'); // Department ID 2 for Air
|
|
}
|
|
// Fetch stations for Marine if the dropdown will be visible
|
|
if (this.showMarineDropdown) {
|
|
await this.fetchStations(3, 'marine'); // Department ID 3 for Marine
|
|
}
|
|
},
|
|
methods: {
|
|
// Modified fetchStations to populate specific lists based on listType
|
|
async fetchStations(departmentId, listType) {
|
|
try {
|
|
const response = await fetch(`/OvertimeAPI/GetStationsByDepartment?departmentId=${departmentId}`);
|
|
if (!response.ok) throw new Error(`Failed to fetch ${listType} stations`);
|
|
const data = await response.json();
|
|
if (listType === 'air') {
|
|
this.airStationList = data;
|
|
} else if (listType === 'marine') {
|
|
this.marineStationList = data;
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error fetching ${listType} stations:`, error);
|
|
if (listType === 'air') {
|
|
this.airStationList = [];
|
|
} else if (listType === 'marine') {
|
|
this.marineStationList = [];
|
|
}
|
|
}
|
|
},
|
|
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;
|
|
|
|
const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin");
|
|
const isSystemAdmin = this.currentUser?.role?.includes("SystemAdmin");
|
|
this.userDepartmentId = this.currentUser?.department?.departmentId; // Store user's actual department ID
|
|
|
|
this.isUserAdmin = isSuperAdmin || isSystemAdmin; // Set the admin flag
|
|
|
|
// Set departmentName for the generic "Only for {{ departmentName }}" hint if not admin
|
|
if (!this.isUserAdmin) {
|
|
if (this.userDepartmentId === 2) {
|
|
this.departmentName = "PSTW AIR";
|
|
} else if (this.userDepartmentId === 3) {
|
|
this.departmentName = "PSTW MARINE";
|
|
} else {
|
|
this.departmentName = "";
|
|
}
|
|
} else {
|
|
this.departmentName = ""; // Admins see both, so this specific hint is removed for them
|
|
}
|
|
|
|
console.log("Fetched User:", this.currentUser);
|
|
console.log("User Dept ID:", this.userDepartmentId);
|
|
console.log("Roles:", this.currentUser?.role);
|
|
console.log("isUserAdmin:", this.isUserAdmin);
|
|
|
|
if (this.userId) {
|
|
await this.fetchUserStateAndHolidays();
|
|
if (this.userState) {
|
|
this.statusId = this.userState.defaultStatusId;
|
|
this.userFlexiHour = this.userState?.flexiHour || "N/A";
|
|
}
|
|
}
|
|
} else {
|
|
console.error(`Failed to fetch user: ${response.statusText}`);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching user:", error);
|
|
}
|
|
},
|
|
async checkUserSettings() {
|
|
try {
|
|
const response = await fetch(`/OvertimeAPI/CheckUserSettings/${this.userId}`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to check user settings: ${response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
this.areUserSettingsComplete = data.isComplete;
|
|
|
|
if (!this.areUserSettingsComplete) {
|
|
alert("Action Required: Your Flexi Hours, Approval Flow, Salary, or State settings have not been configured. Please contact the IT or HR department for assistance.");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error checking user settings:", error);
|
|
alert("An error occurred while verifying your settings. Please try again or contact support.");
|
|
this.areUserSettingsComplete = false;
|
|
}
|
|
},
|
|
async fetchUserStateAndHolidays() {
|
|
try {
|
|
const response = await fetch(`/OvertimeAPI/GetUserStateAndHolidays/${this.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();
|
|
} catch (error) {
|
|
console.error("Error fetching user state and holidays:", error);
|
|
this.detectedDayType = "Weekday";
|
|
}
|
|
},
|
|
roundToNearest30(timeStr) {
|
|
if (!timeStr) return timeStr;
|
|
const [hours, minutes] = timeStr.split(':').map(Number);
|
|
const totalMinutes = hours * 60 + minutes;
|
|
const remainder = totalMinutes % 30;
|
|
|
|
// If closer to the lower 30-min mark, round down. Otherwise, round up.
|
|
const roundedMinutes = remainder < 15 ? totalMinutes - remainder : totalMinutes + (30 - remainder);
|
|
|
|
const adjustedHour = Math.floor(roundedMinutes / 60) % 24; // Ensure hours wrap around 24
|
|
const adjustedMinute = roundedMinutes % 60;
|
|
|
|
return `${adjustedHour.toString().padStart(2, '0')}:${adjustedMinute.toString().padStart(2, '0')}`;
|
|
},
|
|
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;
|
|
|
|
// If TO is 00:00 (midnight), calculate the duration until end of day (24:00)
|
|
if (end.hours === 0 && end.minutes === 0 && (start.hours > 0 || start.minutes > 0)) {
|
|
diffMinutes = (24 * 60) - (start.hours * 60 + start.minutes);
|
|
} else if (end.hours * 60 + end.minutes <= start.hours * 60 + start.minutes) {
|
|
// For all other cases where 'To' time is on or before 'From' time, it's invalid.
|
|
return { hours: 0, minutes: 0 };
|
|
} else {
|
|
// Standard calculation for times within the same 24-hour period on the same day.
|
|
diffMinutes = (end.hours * 60 + end.minutes) - (start.hours * 60 + start.minutes);
|
|
}
|
|
|
|
diffMinutes -= breakMinutes || 0;
|
|
if (diffMinutes < 0) diffMinutes = 0; // Ensure total hours don't go negative if break is too long
|
|
|
|
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`;
|
|
},
|
|
handleDateChange() {
|
|
this.updateDayType();
|
|
this.calculateOTAndBreak();
|
|
},
|
|
async addOvertime() {
|
|
if (!this.areUserSettingsComplete) {
|
|
alert("Cannot save overtime: Your essential user settings are incomplete. Please contact IT or HR.");
|
|
return;
|
|
}
|
|
|
|
// --- Frontend Validation ---
|
|
if (!this.selectedDate) {
|
|
alert("Please select a date for the overtime.");
|
|
return;
|
|
}
|
|
|
|
if (!this.otDescription.trim()) {
|
|
alert("Please provide a brief description of the work done.");
|
|
return;
|
|
}
|
|
|
|
let stationIdToSubmit = this.stationIdForSubmission;
|
|
if (this.requiresStation && !stationIdToSubmit) {
|
|
alert("Please select a station from the available dropdown(s).");
|
|
return;
|
|
}
|
|
|
|
if (this.isUserAdmin && this.selectedAirStation && this.selectedMarineStation) {
|
|
alert("As an administrator, please select *either* an Air Station *or* a Marine Station, but not both for a single overtime entry.");
|
|
return;
|
|
}
|
|
|
|
if (!this.userId) {
|
|
console.error("User ID is not set!");
|
|
alert("User information is missing. Please try again.");
|
|
return;
|
|
}
|
|
|
|
const hasOfficeHours = this.officeFrom && this.officeTo;
|
|
const hasAfterHours = this.afterFrom && this.afterTo;
|
|
|
|
if (!hasOfficeHours && !hasAfterHours) {
|
|
alert("Please enter either Office Hours or After Office Hours.");
|
|
return;
|
|
}
|
|
|
|
// Validate time ranges according to the new rule: TO 00:00 only if FROM is 4:30 PM - 11:30 PM
|
|
const validateTimeRangeForSubmission = (fromTime, toTime, label) => {
|
|
if (!fromTime || !toTime) return true; // Handled by outer checks
|
|
|
|
const start = this.parseTime(fromTime);
|
|
const end = this.parseTime(toTime);
|
|
|
|
const minAllowedFromMinutesForMidnightTo = 16 * 60 + 30; // 4:30 PM
|
|
const maxAllowedFromMinutesForMidnightTo = 23 * 60 + 30; // 11:30 PM
|
|
const startMinutes = start.hours * 60 + start.minutes;
|
|
|
|
if (end.hours === 0 && end.minutes === 0) { // If 'To' is 00:00 (midnight)
|
|
if (fromTime === "00:00") {
|
|
alert(`Invalid ${label} Time: 'From' and 'To' cannot both be 00:00 (midnight).`);
|
|
return false;
|
|
}
|
|
// This is the specific rule: if 'To' is 00:00, 'From' must be within 4:30 PM and 11:30 PM
|
|
if (startMinutes < minAllowedFromMinutesForMidnightTo || startMinutes > maxAllowedFromMinutesForMidnightTo) {
|
|
alert(`Invalid ${label} Time: If 'To' is 12:00 am (00:00), 'From' must start between 4:30 pm and 11:30 pm on the same day to be saved.`);
|
|
return false;
|
|
}
|
|
} else if (end.hours * 60 + end.minutes <= start.hours * 60 + start.minutes) {
|
|
alert(`Invalid ${label} Time: 'To' time must be later than 'From' time for durations within the same day.`);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
if (hasOfficeHours && !validateTimeRangeForSubmission(this.officeFrom, this.officeTo, 'Office Hour')) {
|
|
return;
|
|
}
|
|
if (hasAfterHours && !validateTimeRangeForSubmission(this.afterFrom, this.afterTo, 'After Office Hour')) {
|
|
return;
|
|
}
|
|
// --- End Frontend Validation ---
|
|
|
|
|
|
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,
|
|
afterFrom: this.afterFrom ? this.formatTime(this.afterFrom) : null,
|
|
afterTo: this.afterTo ? this.formatTime(this.afterTo) : null,
|
|
afterBreak: this.afterBreak || null,
|
|
stationId: stationIdToSubmit, // Use the selected station from either dropdown
|
|
otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '),
|
|
otDays: this.detectedDayType,
|
|
userId: this.userId,
|
|
statusId: this.statusId || null,
|
|
};
|
|
|
|
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();
|
|
|
|
await this.fetchUserStateAndHolidays();
|
|
if (this.userState) {
|
|
this.statusId = this.userState.defaultStatusId;
|
|
this.userFlexiHour = this.userState?.flexiHour || "N/A";
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error("Error adding overtime:", error);
|
|
alert(`Failed to save overtime. Error: ${error.message}`);
|
|
}
|
|
},
|
|
|
|
updateDayType() {
|
|
if (!this.selectedDate || !this.userState) {
|
|
this.detectedDayType = "";
|
|
return;
|
|
}
|
|
|
|
const selectedDateObj = new Date(this.selectedDate + "T00:00:00");
|
|
const dayOfWeek = selectedDateObj.getDay();
|
|
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.detectedDayType = "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.detectedDayType = "Weekend";
|
|
return;
|
|
}
|
|
|
|
this.detectedDayType = "Weekday";
|
|
},
|
|
|
|
clearForm() {
|
|
this.selectedDate = "";
|
|
this.officeFrom = "";
|
|
this.officeTo = "";
|
|
this.officeBreak = 0;
|
|
this.afterFrom = "";
|
|
this.afterTo = "";
|
|
this.afterBreak = 0;
|
|
this.selectedAirStation = ""; // Clear specific station selections
|
|
this.selectedMarineStation = ""; // Clear specific station selections
|
|
|
|
// Clear Select2 for both dropdowns if they exist
|
|
const airSelect = $('#airStationDropdown');
|
|
if (airSelect.length && airSelect.data('select2')) {
|
|
airSelect.val('').trigger('change.select2');
|
|
}
|
|
const marineSelect = $('#marineStationDropdown');
|
|
if (marineSelect.length && marineSelect.data('select2')) {
|
|
marineSelect.val('').trigger('change.select2');
|
|
}
|
|
|
|
this.otDescription = "";
|
|
this.userFlexiHour = "";
|
|
this.detectedDayType = "";
|
|
this.totalOTHours = "0 hr 0 min";
|
|
this.totalBreakHours = "0 hr 0 min";
|
|
},
|
|
}
|
|
});
|
|
|
|
app.mount("#app");
|
|
</script>
|
|
} |