update OT
This commit is contained in:
parent
20b9f1e1fd
commit
e3bf644a0b
@ -192,7 +192,7 @@
|
||||
<td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="3">TOTAL</td>
|
||||
<td v-else colspan="1">TOTAL</td>
|
||||
|
||||
<td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="3"></td>
|
||||
<td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="2"></td>
|
||||
<td v-else colspan="2"></td>
|
||||
<td><strong>{{ formatBreakToHourMinute(totals.officeBreak) }}</strong></td>
|
||||
|
||||
@ -247,31 +247,35 @@
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="dateInput">Date</label>
|
||||
<input type="date" class="form-control" id="dateInput" v-model="currentEditRecord.otDate">
|
||||
<label class="form-label" for="modalDateInput">Date</label>
|
||||
<input type="date" class="form-control" id="modalDateInput" v-model="currentEditRecord.otDate">
|
||||
</div>
|
||||
|
||||
<h6 class="fw-bold">OFFICE HOURS</h6>
|
||||
<small class="text-muted mb-3 d-block" v-if="flexiTimes.start && flexiTimes.end">Valid Office Hours ranges: {{ flexiTimes.start }} to {{ flexiTimes.end }}</small>
|
||||
<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"
|
||||
<label for="modalOfficeFrom">From</label>
|
||||
<input type="time" id="modalOfficeFrom" class="form-control"
|
||||
v-model="currentEditRecord.officeFrom"
|
||||
v-on:change="roundMinutes('officeFrom'); validateTimeFields()">
|
||||
v-on:change="roundMinutes('officeFrom'); validateTimeFields()"
|
||||
:disabled="isModalOfficeHoursDisabled">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label for="officeTo">To</label>
|
||||
<input type="time" id="officeTo" class="form-control"
|
||||
<label for="modalOfficeTo">To</label>
|
||||
<input type="time" id="modalOfficeTo" class="form-control"
|
||||
v-model="currentEditRecord.officeTo"
|
||||
v-on:change="roundMinutes('officeTo'); validateTimeFields()">
|
||||
v-on:change="roundMinutes('officeTo'); validateTimeFields()"
|
||||
:disabled="isModalOfficeHoursDisabled">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label for="officeBreak">Break Hours (Minutes)</label>
|
||||
<label for="modalOfficeBreak">Break Hours (Min)</label>
|
||||
<div class="d-flex">
|
||||
<select id="officeBreak" class="form-control" v-model.number="currentEditRecord.officeBreak">
|
||||
<select id="modalOfficeBreak" class="form-control" v-model.number="currentEditRecord.officeBreak"
|
||||
:disabled="isModalOfficeHoursDisabled">
|
||||
<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">
|
||||
<button class="btn btn-outline-danger ms-2" v-on:click="clearOfficeHours"
|
||||
title="Clear Office Hours" :disabled="isModalOfficeHoursDisabled">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
@ -279,23 +283,24 @@
|
||||
</div>
|
||||
|
||||
<h6 class="fw-bold text-danger">AFTER OFFICE HOURS</h6>
|
||||
<small class="text-muted mb-3 d-block" v-if="flexiTimes.start && flexiTimes.end">Valid After Office Hours ranges: 00:00 - {{ flexiTimes.start }} or {{ flexiTimes.end }} - 00:00</small>
|
||||
<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"
|
||||
<label for="modalAfterFrom">From</label>
|
||||
<input type="time" id="modalAfterFrom" class="form-control"
|
||||
v-model="currentEditRecord.afterFrom"
|
||||
v-on:change="roundMinutes('afterFrom'); validateTimeFields()">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label for="afterTo">To</label>
|
||||
<input type="time" id="afterTo" class="form-control"
|
||||
<label for="modalAfterTo">To</label>
|
||||
<input type="time" id="modalAfterTo" class="form-control"
|
||||
v-model="currentEditRecord.afterTo"
|
||||
v-on:change="roundMinutes('afterTo'); validateTimeFields()">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label for="afterBreak">Break Hours (Minutes)</label>
|
||||
<label for="modalAfterBreak">Break Hours (Min)</label>
|
||||
<div class="d-flex">
|
||||
<select id="afterBreak" class="form-control" v-model.number="currentEditRecord.afterBreak">
|
||||
<select id="modalAfterBreak" class="form-control" v-model.number="currentEditRecord.afterBreak">
|
||||
<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">
|
||||
@ -306,8 +311,8 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3" v-show="showAirStationDropdown">
|
||||
<label for="airstationDropdown">Air Station</label>
|
||||
<select id="airstationDropdown" class="form-control select2-enable" style="width: 100%;">
|
||||
<label for="modalAirstationDropdown">Air Station</label>
|
||||
<select id="modalAirstationDropdown" class="form-control select2-enable" style="width: 100%;">
|
||||
<option value="" disabled selected>Select Air Station</option>
|
||||
<option v-for="station in airStations" :key="station.stationId" :value="station.stationId">
|
||||
{{ station.stationName || 'Unnamed Station' }}
|
||||
@ -317,8 +322,8 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3" v-show="showMarineStationDropdown">
|
||||
<label for="marinestationDropdown">Marine Station</label>
|
||||
<select id="marinestationDropdown" class="form-control select2-enable" style="width: 100%;">
|
||||
<label for="modalMarinestationDropdown">Marine Station</label>
|
||||
<select id="modalMarinestationDropdown" class="form-control select2-enable" style="width: 100%;">
|
||||
<option value="" disabled selected>Select Marine Station</option>
|
||||
<option v-for="station in marineStations" :key="station.stationId" :value="station.stationId">
|
||||
{{ station.stationName || 'Unnamed Station' }}
|
||||
@ -328,33 +333,34 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="otDescription">Work Brief Description</label>
|
||||
<textarea id="otDescription" class="form-control" v-model="currentEditRecord.otDescription" placeholder="Describe the work done..."></textarea>
|
||||
<label for="modalOtDescription">Work Brief Description <span class="text-danger">*</span></label>
|
||||
<textarea id="modalOtDescription" class="form-control" v-model="currentEditRecord.otDescription" placeholder="Describe the work done..." required></textarea>
|
||||
<small class="text-danger" v-if="descriptionError">{{ descriptionError }}</small>
|
||||
<small class="text-muted">{{ currentEditRecord.otDescription ? currentEditRecord.otDescription.length : 0 }} / 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="userFlexiHourDisplay">Your Flexi Hour</label>
|
||||
<input type="text" id="userFlexiHourDisplay" class="form-control text-center" :value="userInfo.flexiHour || 'N/A'" readonly style="width: 200px;">
|
||||
<label for="modalUserFlexiHourDisplay">Your Flexi Hour</label>
|
||||
<input type="text" id="modalUserFlexiHourDisplay" class="form-control text-center" :value="userInfo.flexiHour || 'N/A'" readonly style="width: 200px;">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 d-flex flex-column align-items-center">
|
||||
<label for="detectedDayType">Day</label>
|
||||
<label for="modalDetectedDayType">Day</label>
|
||||
<input type="text" class="form-control text-center"
|
||||
:value="getDayType(currentEditRecord.otDate)"
|
||||
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" :value="calculateModalTotalOtHrs()" style="width: 200px;" readonly>
|
||||
<label for="modalTotalOTHours">Total OT Hours</label>
|
||||
<input type="text" id="modalTotalOTHours" class="form-control text-center" :value="calculateModalTotalOtHrs()" 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" :value="calculateModalTotalBreakHrs()" style="width: 200px;" readonly>
|
||||
<label for="modalTotalBreakHours">Total Break Hours</label>
|
||||
<input type="text" id="modalTotalBreakHours" class="form-control text-center" :value="calculateModalTotalBreakHrs()" style="width: 200px;" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -387,7 +393,7 @@
|
||||
weekendId: null,
|
||||
basicSalary: null
|
||||
},
|
||||
isHoU: false, // Keep this for existing logic, but use isApproverRole for salary visibility
|
||||
isHoU: false,
|
||||
expandedDescriptions: [],
|
||||
currentEditRecord: {
|
||||
overtimeId: null,
|
||||
@ -401,7 +407,7 @@
|
||||
stationId: null,
|
||||
otDescription: '',
|
||||
statusId: null,
|
||||
otDays: ''
|
||||
otDays: '' // This will store the day type for the modal's selected date
|
||||
},
|
||||
airStations: [],
|
||||
marineStations: [],
|
||||
@ -520,32 +526,51 @@
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
isModalOfficeHoursDisabled() {
|
||||
return this.getDayType(this.currentEditRecord.otDate) === "Normal Day";
|
||||
},
|
||||
flexiTimes() {
|
||||
if (!this.userInfo || !this.userInfo.flexiHour) {
|
||||
return { start: 'N/A', end: 'N/A' };
|
||||
}
|
||||
const times = this.userInfo.flexiHour.split(' - ');
|
||||
return {
|
||||
start: times[0],
|
||||
end: times[1]
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'currentEditRecord.otDate': function(newDate) {
|
||||
this.currentEditRecord.otDays = this.getDayType(newDate);
|
||||
if (this.isModalOfficeHoursDisabled) {
|
||||
this.clearOfficeHours();
|
||||
}
|
||||
this.validateTimeFields();
|
||||
this.calculateModalTotalOtHrs();
|
||||
this.calculateModalTotalBreakHrs();
|
||||
},
|
||||
'currentEditRecord.stationId': function(newVal) {
|
||||
if (this.showAirStationDropdown) {
|
||||
$('#airstationDropdown').val(newVal).trigger('change.select2');
|
||||
$('#modalAirstationDropdown').val(newVal).trigger('change.select2');
|
||||
} else if (this.showMarineStationDropdown) {
|
||||
$('#marinestationDropdown').val(newVal).trigger('change.select2');
|
||||
$('#modalMarinestationDropdown').val(newVal).trigger('change.select2');
|
||||
}
|
||||
},
|
||||
|
||||
airStations: function() {
|
||||
if (this.showAirStationDropdown) {
|
||||
this.initSelect2('#airstationDropdown');
|
||||
this.initSelect2('#modalAirstationDropdown');
|
||||
}
|
||||
},
|
||||
marineStations: function() {
|
||||
if (this.showMarineStationDropdown) {
|
||||
this.initSelect2('#marinestationDropdown');
|
||||
this.initSelect2('#modalMarinestationDropdown');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// New method to check if the current approver role is in the list of roles to hide salary info
|
||||
isApproverRole(rolesToHide) {
|
||||
return rolesToHide.includes(this.approverRole);
|
||||
},
|
||||
@ -566,7 +591,6 @@
|
||||
...data.userInfo,
|
||||
basicSalary: data.userInfo.rate
|
||||
};
|
||||
// No change here for isHoU, as it's used for other logic (like edit/delete buttons)
|
||||
this.isHoU = data.isHoU;
|
||||
this.currentEditRecord.statusId = parseInt(statusId);
|
||||
|
||||
@ -600,16 +624,12 @@
|
||||
}
|
||||
},
|
||||
initSelect2(selector) {
|
||||
|
||||
if ($(selector).data('select2')) {
|
||||
$(selector).select2('destroy');
|
||||
}
|
||||
|
||||
|
||||
$(selector).select2({
|
||||
dropdownParent: $('#editOtModal')
|
||||
}).on('change', (e) => {
|
||||
|
||||
this.currentEditRecord.stationId = $(e.currentTarget).val();
|
||||
});
|
||||
|
||||
@ -728,9 +748,8 @@
|
||||
break;
|
||||
|
||||
case 'Off Day':
|
||||
case 'Weekend':
|
||||
case 'Weekend': // Assume Weekend is handled like Off Day if not explicitly categorized
|
||||
if (officeHrs > 0) {
|
||||
|
||||
if (officeHrs <= 4) {
|
||||
result.odUnder4 = toFixedOrEmpty(officeHrs);
|
||||
} else if (officeHrs <= 8) {
|
||||
@ -739,9 +758,7 @@
|
||||
result.odAfter = toFixedOrEmpty(officeHrs);
|
||||
}
|
||||
}
|
||||
|
||||
result.odAfter = toFixedOrEmpty( (parseFloat(result.odAfter) || 0) + afterHrs );
|
||||
|
||||
result.odAfter = toFixedOrEmpty((parseFloat(result.odAfter) || 0) + afterHrs);
|
||||
break;
|
||||
|
||||
case 'Rest Day':
|
||||
@ -754,12 +771,10 @@
|
||||
result.rdAfter = toFixedOrEmpty(officeHrs);
|
||||
}
|
||||
}
|
||||
|
||||
result.rdAfter = toFixedOrEmpty( (parseFloat(result.rdAfter) || 0) + afterHrs );
|
||||
result.rdAfter = toFixedOrEmpty((parseFloat(result.rdAfter) || 0) + afterHrs);
|
||||
break;
|
||||
|
||||
case 'Public Holiday':
|
||||
|
||||
if (officeHrs > 0) {
|
||||
if (officeHrs <= 8) {
|
||||
result.phUnder8 = toFixedOrEmpty(officeHrs);
|
||||
@ -767,8 +782,7 @@
|
||||
result.phAfter = toFixedOrEmpty(officeHrs);
|
||||
}
|
||||
}
|
||||
|
||||
result.phAfter = toFixedOrEmpty( (parseFloat(result.phAfter) || 0) + afterHrs );
|
||||
result.phAfter = toFixedOrEmpty((parseFloat(result.phAfter) || 0) + afterHrs);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -834,8 +848,9 @@
|
||||
amountOffice = 0.5 * orp;
|
||||
} else if (officeHours <= 8) {
|
||||
amountOffice = 1 * orp;
|
||||
} else {
|
||||
amountOffice = 1 * orp; // For >8 hours, still based on 1 ORP for office hours
|
||||
}
|
||||
|
||||
} else if (otType === 'Public Holiday') {
|
||||
amountOffice = 2 * orp;
|
||||
}
|
||||
@ -860,27 +875,19 @@
|
||||
return totalAmount.toFixed(2);
|
||||
},
|
||||
roundMinutes(fieldName) {
|
||||
const timeValue = this.currentEditRecord[fieldName];
|
||||
if (!timeValue) return;
|
||||
const timeStr = this.currentEditRecord[fieldName];
|
||||
if (!timeStr) return;
|
||||
|
||||
const [hours, minutes] = timeValue.split(':').map(Number);
|
||||
let roundedMinutes = minutes;
|
||||
const [hours, minutes] = timeStr.split(':').map(Number);
|
||||
const totalMinutes = hours * 60 + minutes;
|
||||
|
||||
if (minutes >= 45 || minutes < 15) {
|
||||
roundedMinutes = 0;
|
||||
if (minutes >= 45) {
|
||||
let currentHour = hours;
|
||||
currentHour = (currentHour + 1) % 24;
|
||||
this.currentEditRecord[fieldName] = `${String(currentHour).padStart(2, '0')}:00`;
|
||||
this.validateTimeFields();
|
||||
return;
|
||||
}
|
||||
} else if (minutes >= 15 && minutes < 45) {
|
||||
roundedMinutes = 30;
|
||||
}
|
||||
const remainder = totalMinutes % 30;
|
||||
const roundedMinutes = remainder < 15 ? totalMinutes - remainder : totalMinutes + (30 - remainder);
|
||||
|
||||
this.currentEditRecord[fieldName] = `${String(hours).padStart(2, '0')}:${String(roundedMinutes).padStart(2, '0')}`;
|
||||
const adjustedHour = Math.floor(roundedMinutes / 60) % 24;
|
||||
const adjustedMinute = roundedMinutes % 60;
|
||||
|
||||
this.currentEditRecord[fieldName] = `${adjustedHour.toString().padStart(2, '0')}:${adjustedMinute.toString().padStart(2, '0')}`;
|
||||
this.validateTimeFields();
|
||||
},
|
||||
editRecord(id) {
|
||||
@ -908,18 +915,22 @@
|
||||
afterBreak: recordToEdit.afterBreak || 0,
|
||||
stationId: recordToEdit.stationId,
|
||||
otDescription: recordToEdit.otDescription,
|
||||
statusId: this.currentEditRecord.statusId,
|
||||
otDays: recordToEdit.otDays
|
||||
statusId: this.currentEditRecord.statusId, // Retain the overall statusId
|
||||
otDays: recordToEdit.dayType || this.getDayType(formattedDate) // Ensure otDays is set correctly
|
||||
};
|
||||
|
||||
if (this.isModalOfficeHoursDisabled) {
|
||||
this.clearOfficeHours();
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.showAirStationDropdown) {
|
||||
this.initSelect2('#airstationDropdown');
|
||||
$('#airstationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');
|
||||
this.initSelect2('#modalAirstationDropdown');
|
||||
$('#modalAirstationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');
|
||||
}
|
||||
if (this.showMarineStationDropdown) {
|
||||
this.initSelect2('#marinestationDropdown');
|
||||
$('#marinestationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');
|
||||
this.initSelect2('#modalMarinestationDropdown');
|
||||
$('#modalMarinestationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');
|
||||
}
|
||||
const editModal = new bootstrap.Modal(document.getElementById('editOtModal'));
|
||||
editModal.show();
|
||||
@ -929,25 +940,8 @@
|
||||
|
||||
async submitEdit() {
|
||||
try {
|
||||
const hasOfficeHours = this.currentEditRecord.officeFrom && this.currentEditRecord.officeTo;
|
||||
const hasAfterHours = this.currentEditRecord.afterFrom && this.currentEditRecord.afterTo;
|
||||
|
||||
if (!hasOfficeHours && !hasAfterHours) {
|
||||
alert("Please enter either Office Hours or After Office Hours.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasOfficeHours && !this.validateTimeRangeForSubmission(
|
||||
this.currentEditRecord.officeFrom,
|
||||
this.currentEditRecord.officeTo,
|
||||
'Office Hour')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAfterHours && !this.validateTimeRangeForSubmission(
|
||||
this.currentEditRecord.afterFrom,
|
||||
this.currentEditRecord.afterTo,
|
||||
'After Office Hour')) {
|
||||
// MODIFIED: Check for validation failure and exit early
|
||||
if (!this.validateModalForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -956,7 +950,12 @@
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(this.currentEditRecord)
|
||||
body: JSON.stringify({
|
||||
...this.currentEditRecord,
|
||||
OfficeFrom: this.isModalOfficeHoursDisabled ? null : this.currentEditRecord.officeFrom,
|
||||
OfficeTo: this.isModalOfficeHoursDisabled ? null : this.currentEditRecord.officeTo,
|
||||
OfficeBreak: this.isModalOfficeHoursDisabled ? 0 : this.currentEditRecord.officeBreak
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
@ -974,6 +973,120 @@
|
||||
alert('An error occurred while updating the record.');
|
||||
}
|
||||
},
|
||||
|
||||
// MODIFIED: validateModalForm to return early on first error
|
||||
validateModalForm() {
|
||||
// Reset validation errors
|
||||
this.descriptionError = "";
|
||||
|
||||
// 1. Check for Work Brief Description
|
||||
if (!this.currentEditRecord.otDescription || this.currentEditRecord.otDescription.trim() === "") {
|
||||
this.descriptionError = "Work brief description is required.";
|
||||
alert(this.descriptionError);
|
||||
return false;
|
||||
}
|
||||
|
||||
let errorMessages = [];
|
||||
|
||||
if ((this.showAirStationDropdown || this.showMarineStationDropdown) && !this.currentEditRecord.stationId) {
|
||||
errorMessages.push("Station is required.");
|
||||
}
|
||||
|
||||
const officeFromFilled = !!this.currentEditRecord.officeFrom;
|
||||
const officeToFilled = !!this.currentEditRecord.officeTo;
|
||||
const afterFromFilled = !!this.currentEditRecord.afterFrom;
|
||||
const afterToFilled = !!this.currentEditRecord.afterTo;
|
||||
|
||||
if (!this.isModalOfficeHoursDisabled) {
|
||||
if (officeFromFilled !== officeToFilled) {
|
||||
errorMessages.push("Both 'From' and 'To' times must be provided for Office Hours, or leave both empty.");
|
||||
} else if (officeFromFilled && officeToFilled) {
|
||||
if (!this.validateTimeRangeForSubmission(this.currentEditRecord.officeFrom, this.currentEditRecord.officeTo, 'Office Hour')) {
|
||||
// validateTimeRangeForSubmission already shows an alert, so we just return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (afterFromFilled !== afterToFilled) {
|
||||
errorMessages.push("Both 'From' and 'To' times must be provided for After Office Hours, or leave both empty.");
|
||||
} else if (afterFromFilled && afterToFilled) {
|
||||
if (!this.validateTimeRangeForSubmission(this.currentEditRecord.afterFrom, this.currentEditRecord.afterTo, 'After Office Hour')) {
|
||||
// validateTimeRangeForSubmission already shows an alert, so we just return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const hasAnyCompleteTimeEntry = (
|
||||
(!this.isModalOfficeHoursDisabled && officeFromFilled && officeToFilled) ||
|
||||
(afterFromFilled && afterToFilled)
|
||||
);
|
||||
|
||||
if (!hasAnyCompleteTimeEntry && errorMessages.length === 0) {
|
||||
errorMessages.push("Please enter either Office Hours or After Office Hours.");
|
||||
}
|
||||
|
||||
// Call the overlap check here. It will show its own alert.
|
||||
if (!this.checkOverlaps(this.currentEditRecord)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there are any other validation errors, show them and return false.
|
||||
if (errorMessages.length > 0) {
|
||||
alert("Please correct the following issues:\n\n" + errorMessages.join("\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// NEW METHOD: checkOverlaps
|
||||
checkOverlaps(recordToValidate) {
|
||||
const existingRecordsOnSameDate = this.otRecords.filter(
|
||||
r => r.overtimeId !== recordToValidate.overtimeId &&
|
||||
r.otDate.slice(0, 10) === recordToValidate.otDate
|
||||
);
|
||||
|
||||
const overlaps = existingRecordsOnSameDate.some(existingRecord => {
|
||||
const newRanges = [];
|
||||
if (recordToValidate.officeFrom && recordToValidate.officeTo) newRanges.push({ start: this.parseTime(recordToValidate.officeFrom), end: this.parseTime(recordToValidate.officeTo) });
|
||||
if (recordToValidate.afterFrom && recordToValidate.afterTo) newRanges.push({ start: this.parseTime(recordToValidate.afterFrom), end: this.parseTime(recordToValidate.afterTo) });
|
||||
|
||||
const existingRanges = [];
|
||||
if (existingRecord.officeFrom && existingRecord.officeTo) existingRanges.push({ start: this.parseTime(this.formatTime(existingRecord.officeFrom)), end: this.parseTime(this.formatTime(existingRecord.officeTo)) });
|
||||
if (existingRecord.afterFrom && existingRecord.afterTo) existingRanges.push({ start: this.parseTime(this.formatTime(existingRecord.afterFrom)), end: this.parseTime(this.formatTime(existingRecord.afterTo)) });
|
||||
|
||||
for (const newRange of newRanges) {
|
||||
for (const existingRange of existingRanges) {
|
||||
if (this.areTimeRangesOverlapping(newRange, existingRange)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (overlaps) {
|
||||
alert("An overtime entry for this date overlaps with an existing entry. Please adjust the time ranges.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// NEW HELPER METHOD: areTimeRangesOverlapping
|
||||
areTimeRangesOverlapping(range1, range2) {
|
||||
const start1 = range1.start.hours * 60 + range1.start.minutes;
|
||||
let end1 = range1.end.hours * 60 + range1.end.minutes;
|
||||
const start2 = range2.start.hours * 60 + range2.start.minutes;
|
||||
let end2 = range2.end.hours * 60 + range2.end.minutes;
|
||||
|
||||
if (end1 <= start1) end1 += 24 * 60;
|
||||
if (end2 <= start2) end2 += 24 * 60;
|
||||
|
||||
return !(end1 <= start2 || end2 <= start1);
|
||||
},
|
||||
|
||||
async fetchPublicHolidays(stateId) {
|
||||
if (!stateId) {
|
||||
this.publicHolidays = [];
|
||||
@ -990,7 +1103,7 @@
|
||||
}
|
||||
},
|
||||
getDayType(dateStr) {
|
||||
if (!dateStr || !this.userState || this.publicHolidays.length === 0) return '';
|
||||
if (!dateStr || !this.userState) return '';
|
||||
|
||||
const selectedDate = new Date(dateStr + "T00:00:00");
|
||||
if (isNaN(selectedDate.getTime())) return '';
|
||||
@ -1014,9 +1127,10 @@
|
||||
if (dayOfWeek === 6) return 'Off Day';
|
||||
if (dayOfWeek === 0) return 'Rest Day';
|
||||
else return 'Normal Day';
|
||||
} else {
|
||||
if (dayOfWeek === 0) return 'Rest Day';
|
||||
else return 'Normal Day';
|
||||
}
|
||||
|
||||
return 'Normal Day';
|
||||
},
|
||||
|
||||
calculateModalTotalOtHrs() {
|
||||
@ -1048,6 +1162,7 @@
|
||||
},
|
||||
|
||||
parseTime(timeString) {
|
||||
if (!timeString) return { hours: -1, minutes: -1 };
|
||||
const [hours, minutes] = timeString.split(':').map(Number);
|
||||
return { hours, minutes };
|
||||
},
|
||||
@ -1056,12 +1171,16 @@
|
||||
this.currentEditRecord.officeFrom = "";
|
||||
this.currentEditRecord.officeTo = "";
|
||||
this.currentEditRecord.officeBreak = 0;
|
||||
this.calculateModalTotalOtHrs();
|
||||
this.calculateModalTotalBreakHrs();
|
||||
},
|
||||
|
||||
clearAfterHours() {
|
||||
this.currentEditRecord.afterFrom = "";
|
||||
this.currentEditRecord.afterTo = "";
|
||||
this.currentEditRecord.afterBreak = 0;
|
||||
this.calculateModalTotalOtHrs();
|
||||
this.calculateModalTotalBreakHrs();
|
||||
},
|
||||
|
||||
deleteRecord(id) {
|
||||
@ -1202,8 +1321,8 @@
|
||||
alert(`Invalid ${label} Time: 'From' and 'To' cannot both be 00:00 (midnight).`);
|
||||
return false;
|
||||
}
|
||||
if (startMinutes < minAllowedFromMidnightTo || startMinutes > maxAllowedFromMidnightTo) {
|
||||
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.`);
|
||||
if (startMinutes < minAllowedFromMinutesForMidnightTo || startMinutes > maxAllowedFromMidnightTo) {
|
||||
alert(`Invalid ${label} Time: If 'To' is 12:00 am (00:00), 'From' must start between end of your flexi hour to 11:30 pm on the same day.`);
|
||||
return false;
|
||||
}
|
||||
} else if (end.hours * 60 + end.minutes <= start.hours * 60 + start.minutes) {
|
||||
@ -1213,22 +1332,8 @@
|
||||
return true;
|
||||
},
|
||||
validateTimeFields() {
|
||||
const hasOfficeHours = this.currentEditRecord.officeFrom && this.currentEditRecord.officeTo;
|
||||
const hasAfterHours = this.currentEditRecord.afterFrom && this.currentEditRecord.afterTo;
|
||||
|
||||
if (hasOfficeHours) {
|
||||
this.validateTimeRangeForSubmission(
|
||||
this.currentEditRecord.officeFrom,
|
||||
this.currentEditRecord.officeTo,
|
||||
'Office Hour');
|
||||
}
|
||||
|
||||
if (hasAfterHours) {
|
||||
this.validateTimeRangeForSubmission(
|
||||
this.currentEditRecord.afterFrom,
|
||||
this.currentEditRecord.afterTo,
|
||||
'After Office Hour');
|
||||
}
|
||||
this.calculateModalTotalOtHrs();
|
||||
this.calculateModalTotalBreakHrs();
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
@ -1237,23 +1342,28 @@
|
||||
},
|
||||
mounted() {
|
||||
if (this.showAirStationDropdown) {
|
||||
this.initSelect2('#airstationDropdown');
|
||||
this.initSelect2('#modalAirstationDropdown');
|
||||
}
|
||||
if (this.showMarineStationDropdown) {
|
||||
this.initSelect2('#marinestationDropdown');
|
||||
this.initSelect2('#modalMarinestationDropdown');
|
||||
}
|
||||
|
||||
var editModalElement = document.getElementById('editOtModal');
|
||||
if (editModalElement) {
|
||||
editModalElement.addEventListener('shown.bs.modal', () => {
|
||||
if (this.showAirStationDropdown) {
|
||||
this.initSelect2('#airstationDropdown');
|
||||
$('#airstationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');
|
||||
this.initSelect2('#modalAirstationDropdown');
|
||||
$('#modalAirstationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');
|
||||
}
|
||||
if (this.showMarineStationDropdown) {
|
||||
this.initSelect2('#marinestationDropdown');
|
||||
$('#marinestationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');
|
||||
this.initSelect2('#modalMarinestationDropdown');
|
||||
$('#modalMarinestationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');
|
||||
}
|
||||
this.currentEditRecord.otDays = this.getDayType(this.currentEditRecord.otDate);
|
||||
if (this.isModalOfficeHoursDisabled) {
|
||||
this.clearOfficeHours();
|
||||
}
|
||||
this.validateTimeFields();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,35 +12,41 @@
|
||||
<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" class="form-control" v-model="editForm.otDate" v-on:input="calculateOTAndBreak" required>
|
||||
<input type="date" class="form-control" v-model="editForm.otDate" v-on:input="handleDateChange" required>
|
||||
<small class="text-danger" v-if="validationErrors.otDate">{{ validationErrors.otDate }}</small>
|
||||
</div>
|
||||
|
||||
<h6 class="fw-bold">OFFICE HOURS</h6>
|
||||
<div class="d-flex gap-3 mb-3 align-items-end flex-wrap">
|
||||
<div class="d-flex gap-3 mb-1 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')">
|
||||
<input type="time" id="officeFrom" class="form-control" v-model="editForm.officeFrom"
|
||||
v-on:change="updateTime('officeFrom')" :disabled="isOfficeHoursDisabled">
|
||||
</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')">
|
||||
<input type="time" id="officeTo" class="form-control" v-model="editForm.officeTo"
|
||||
v-on:change="updateTime('officeTo')" :disabled="isOfficeHoursDisabled">
|
||||
</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">
|
||||
<select id="officeBreak" class="form-control" v-model.number="editForm.officeBreak"
|
||||
v-on:change="calculateOTAndBreak" :disabled="isOfficeHoursDisabled">
|
||||
<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">
|
||||
<button class="btn btn-outline-danger ms-2" v-on:click="clearOfficeHours"
|
||||
title="Clear Office Hours" :disabled="isOfficeHoursDisabled">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted mb-3 d-block" v-if="flexiTimes.start && flexiTimes.end">Valid Office Hours ranges: {{ flexiTimes.start }} to {{ flexiTimes.end }}</small>
|
||||
|
||||
|
||||
<h6 class="fw-bold text-danger">AFTER OFFICE HOURS</h6>
|
||||
<div class="d-flex gap-3 mb-3 align-items-end flex-wrap">
|
||||
<div class="d-flex gap-3 mb-1 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')">
|
||||
@ -61,6 +67,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted mb-3 d-block" v-if="flexiTimes.start && flexiTimes.end">Valid After Office Hours ranges: 00:00 - {{ flexiTimes.start }} or {{ flexiTimes.end }} - 00:00</small>
|
||||
|
||||
<div class="mb-3" v-if="isPSTWAIR">
|
||||
<label for="airstationDropdown">Air Station <span class="text-danger">*</span></label>
|
||||
@ -180,13 +187,25 @@
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
charCount() {
|
||||
return this.editForm.otDescription.length;
|
||||
},
|
||||
userFlexiHourDisplay() {
|
||||
return this.userFlexiHour ? this.userFlexiHour.flexiHour : "N/A";
|
||||
},
|
||||
isOfficeHoursDisabled() {
|
||||
return this.editForm.otDays === "Weekday";
|
||||
},
|
||||
flexiTimes() {
|
||||
if (!this.userFlexiHour || !this.userFlexiHour.flexiHour) {
|
||||
return { start: 'N/A', end: 'N/A' };
|
||||
}
|
||||
const times = this.userFlexiHour.flexiHour.split(' - ');
|
||||
return {
|
||||
start: times[0],
|
||||
end: times[1]
|
||||
};
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@ -221,11 +240,20 @@
|
||||
$('#marinestationDropdown').val(this.editForm.stationId).trigger('change.select2');
|
||||
}
|
||||
}
|
||||
if (this.isOfficeHoursDisabled) {
|
||||
this.clearOfficeHours();
|
||||
}
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
'editForm.otDays'(newVal) {
|
||||
if (newVal === "Weekday") {
|
||||
this.clearOfficeHours();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initializeSelect2Dropdowns() {
|
||||
|
||||
if ($('#airstationDropdown').data('select2')) {
|
||||
$('#airstationDropdown').select2('destroy');
|
||||
}
|
||||
@ -272,7 +300,6 @@
|
||||
otDate: record.otDate ? record.otDate.slice(0, 10) : "",
|
||||
};
|
||||
this.calculateOTAndBreak();
|
||||
this.updateDayType();
|
||||
},
|
||||
|
||||
async fetchStations() {
|
||||
@ -333,7 +360,7 @@
|
||||
this.updateDayType();
|
||||
} catch (error) {
|
||||
console.error("Error fetching user state and holidays:", error);
|
||||
this.editForm.otDays = "Weekday";
|
||||
this.editForm.otDays = "Weekday"; // Default to weekday on error
|
||||
}
|
||||
},
|
||||
|
||||
@ -398,7 +425,6 @@
|
||||
},
|
||||
|
||||
updateTime(fieldName) {
|
||||
|
||||
if (fieldName === 'officeFrom') {
|
||||
this.editForm.officeFrom = this.roundToNearest30(this.editForm.officeFrom);
|
||||
} else if (fieldName === 'officeTo') {
|
||||
@ -431,7 +457,7 @@
|
||||
return false;
|
||||
}
|
||||
if (startMinutes < minAllowedFromMinutes || startMinutes > maxAllowedFromMinutes) {
|
||||
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.`);
|
||||
alert(`Invalid ${label} Time: If 'To' is 12:00 am (00:00), 'From' must start between end of your flexi hour to 11:30 pm on the same day.`);
|
||||
return false;
|
||||
}
|
||||
} else if (endMinutes <= startMinutes) {
|
||||
@ -552,7 +578,6 @@
|
||||
},
|
||||
|
||||
validateForm() {
|
||||
|
||||
this.validationErrors = {
|
||||
otDate: "",
|
||||
stationId: "",
|
||||
@ -563,84 +588,58 @@
|
||||
let isValid = true;
|
||||
let errorMessages = [];
|
||||
|
||||
|
||||
if (!this.editForm.otDate) {
|
||||
this.validationErrors.otDate = "Date is required.";
|
||||
errorMessages.push("Date is required.");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Validate station (only if user is in PSTW AIR or MARINE)
|
||||
if ((this.isPSTWAIR || this.isPSTWMARINE) && !this.editForm.stationId) {
|
||||
this.validationErrors.stationId = "Station is required.";
|
||||
errorMessages.push("Station is required.");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Validate work brief description
|
||||
if (!this.editForm.otDescription || this.editForm.otDescription.trim() === "") {
|
||||
this.validationErrors.otDescription = "Work brief description is required.";
|
||||
errorMessages.push("Work brief description is required.");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Validate at least one time entry (either office hours or after hours)
|
||||
const hasOfficeHours = this.editForm.officeFrom && this.editForm.officeTo;
|
||||
const hasAfterHours = this.editForm.afterFrom && this.editForm.afterTo;
|
||||
const officeFromFilled = !!this.editForm.officeFrom;
|
||||
const officeToFilled = !!this.editForm.officeTo;
|
||||
const afterFromFilled = !!this.editForm.afterFrom;
|
||||
const afterToFilled = !!this.editForm.afterTo;
|
||||
|
||||
if (!hasOfficeHours && !hasAfterHours) {
|
||||
this.validationErrors.timeEntries = "Please enter either Office Hours or After Hours.";
|
||||
errorMessages.push("Please enter either Office Hours or After Hours.");
|
||||
if (!this.isOfficeHoursDisabled) {
|
||||
if (officeFromFilled !== officeToFilled) {
|
||||
this.validationErrors.timeEntries = "Both 'From' and 'To' times must be provided for Office Hours, or leave both empty.";
|
||||
errorMessages.push("Both 'From' and 'To' times must be provided for Office Hours, or leave both empty.");
|
||||
isValid = false;
|
||||
} else {
|
||||
// Validate office hours if provided
|
||||
if (hasOfficeHours) {
|
||||
const officeFrom = this.parseTime(this.editForm.officeFrom);
|
||||
const officeTo = this.parseTime(this.editForm.officeTo);
|
||||
const officeFromMinutes = officeFrom.hours * 60 + officeFrom.minutes;
|
||||
const officeToMinutes = officeTo.hours * 60 + officeTo.minutes;
|
||||
|
||||
if (officeTo.hours === 0 && officeTo.minutes === 0) {
|
||||
// Midnight case - FROM must be between 4:30 PM and 11:30 PM
|
||||
const minAllowed = 16 * 60 + 30; // 4:30 PM
|
||||
const maxAllowed = 23 * 60 + 30; // 11:30 PM
|
||||
|
||||
if (officeFromMinutes < minAllowed || officeFromMinutes > maxAllowed) {
|
||||
this.validationErrors.timeEntries = "Invalid Office Time: If 'To' is 00:00, 'From' must be between 4:30 PM and 11:30 PM.";
|
||||
errorMessages.push("Invalid Office Time: If 'To' is 00:00, 'From' must be between 4:30 PM and 11:30 PM.");
|
||||
} else if (officeFromFilled && officeToFilled) {
|
||||
if (!this.validateTimeRangeForSubmission(this.editForm.officeFrom, this.editForm.officeTo, 'Office Hour')) {
|
||||
isValid = false;
|
||||
}
|
||||
} else if (officeToMinutes <= officeFromMinutes) {
|
||||
this.validationErrors.timeEntries = "Invalid Office Time: 'To' time must be later than 'From' time.";
|
||||
errorMessages.push("Invalid Office Time: 'To' time must be later than 'From' time.");
|
||||
}
|
||||
}
|
||||
|
||||
if (afterFromFilled !== afterToFilled) {
|
||||
this.validationErrors.timeEntries = (this.validationErrors.timeEntries ? this.validationErrors.timeEntries + "\n" : "") + "Both 'From' and 'To' times must be provided for After Office Hours, or leave both empty.";
|
||||
errorMessages.push("Both 'From' and 'To' times must be provided for After Office Hours, or leave both empty.");
|
||||
isValid = false;
|
||||
} else if (afterFromFilled && afterToFilled) {
|
||||
if (!this.validateTimeRangeForSubmission(this.editForm.afterFrom, this.editForm.afterTo, 'After Office Hour')) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate after hours if provided
|
||||
if (hasAfterHours) {
|
||||
const afterFrom = this.parseTime(this.editForm.afterFrom);
|
||||
const afterTo = this.parseTime(this.editForm.afterTo);
|
||||
const afterFromMinutes = afterFrom.hours * 60 + afterFrom.minutes;
|
||||
const afterToMinutes = afterTo.hours * 60 + afterTo.minutes;
|
||||
const hasAnyValidTimeEntry = (!this.isOfficeHoursDisabled && officeFromFilled && officeToFilled) || (afterFromFilled && afterToFilled);
|
||||
|
||||
if (afterTo.hours === 0 && afterTo.minutes === 0) {
|
||||
// Midnight case - FROM must be between 4:30 PM and 11:30 PM
|
||||
const minAllowed = 16 * 60 + 30; // 4:30 PM
|
||||
const maxAllowed = 23 * 60 + 30; // 11:30 PM
|
||||
|
||||
if (afterFromMinutes < minAllowed || afterFromMinutes > maxAllowed) {
|
||||
this.validationErrors.timeEntries = "Invalid After Hours Time: If 'To' is 00:00, 'From' must be between 4:30 PM and 11:30 PM.";
|
||||
errorMessages.push("Invalid After Hours Time: If 'To' is 00:00, 'From' must be between 4:30 PM and 11:30 PM.");
|
||||
if (!hasAnyValidTimeEntry && errorMessages.length === 0) {
|
||||
this.validationErrors.timeEntries = "Please enter either Office Hours or After Office Hours.";
|
||||
errorMessages.push("Please enter either Office Hours or After Office Hours.");
|
||||
isValid = false;
|
||||
}
|
||||
} else if (afterToMinutes <= afterFromMinutes) {
|
||||
this.validationErrors.timeEntries = "Invalid After Hours Time: 'To' time must be later than 'From' time.";
|
||||
errorMessages.push("Invalid After Hours Time: 'To' time must be later than 'From' time.");
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
alert("Please correct the following issues:\n\n" + errorMessages.join("\n"));
|
||||
@ -649,6 +648,33 @@
|
||||
return isValid;
|
||||
},
|
||||
|
||||
validateTimeRangeForSubmission(fromTime, toTime, label) {
|
||||
if (!fromTime || !toTime) return true;
|
||||
|
||||
const start = this.parseTime(fromTime);
|
||||
const end = this.parseTime(toTime);
|
||||
|
||||
const minAllowedFromMinutes = 16 * 60 + 30;
|
||||
const maxAllowedFromMinutes = 23 * 60 + 30;
|
||||
const startMinutes = start.hours * 60 + start.minutes;
|
||||
const endMinutes = end.hours * 60 + end.minutes;
|
||||
|
||||
if (endMinutes === 0) {
|
||||
if (fromTime === "00:00") {
|
||||
alert(`Invalid ${label} Time: 'From' and 'To' cannot both be 00:00 (midnight).`);
|
||||
return false;
|
||||
}
|
||||
if (startMinutes < minAllowedFromMinutes || startMinutes > maxAllowedFromMinutes) {
|
||||
alert(`Invalid ${label} Time: If 'To' is 12:00 am (00:00), 'From' must start between end of your flexi hour to 11:30 pm on the same day..`);
|
||||
return false;
|
||||
}
|
||||
} else if (endMinutes <= startMinutes) {
|
||||
alert(`Invalid ${label} Time: 'To' time must be later than 'From' time for durations within the same day.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
validateAndUpdate() {
|
||||
if (this.validateForm()) {
|
||||
this.updateRecord();
|
||||
@ -662,9 +688,9 @@
|
||||
StationId: this.editForm.stationId || null,
|
||||
OtDescription: this.editForm.otDescription || "",
|
||||
OtDays: this.editForm.otDays,
|
||||
OfficeFrom: this.editForm.officeFrom || null,
|
||||
OfficeTo: this.editForm.officeTo || null,
|
||||
OfficeBreak: this.editForm.officeBreak || 0,
|
||||
OfficeFrom: this.isOfficeHoursDisabled ? null : (this.editForm.officeFrom || null),
|
||||
OfficeTo: this.isOfficeHoursDisabled ? null : (this.editForm.officeTo || null),
|
||||
OfficeBreak: this.isOfficeHoursDisabled ? 0 : (this.editForm.officeBreak || 0),
|
||||
AfterFrom: this.editForm.afterFrom || null,
|
||||
AfterTo: this.editForm.afterTo || null,
|
||||
AfterBreak: this.editForm.afterBreak || 0,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
|
||||
@{
|
||||
@{
|
||||
ViewData["Title"] = "Register Overtime";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
@ -22,21 +21,29 @@
|
||||
<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()">
|
||||
v-on:change="officeFrom = roundToNearest30(officeFrom); calculateOTAndBreak()"
|
||||
:disabled="isOfficeHoursDisabled">
|
||||
</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()">
|
||||
v-on:change="officeTo = roundToNearest30(officeTo); calculateOTAndBreak()"
|
||||
:disabled="isOfficeHoursDisabled">
|
||||
</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">
|
||||
<select id="officeBreak" class="form-control" v-model.number="officeBreak" v-on:change="calculateOTAndBreak"
|
||||
:disabled="isOfficeHoursDisabled">
|
||||
<option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 mt-2" v-if="userFlexiHour && userFlexiHour !== 'N/A'">
|
||||
<small class="text-muted">
|
||||
Valid Office Hours ranges: {{ flexiTimes.start }} to {{ flexiTimes.end }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="fw-bold text-danger">AFTER OFFICE HOURS</h6>
|
||||
@ -59,6 +66,11 @@
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 mt-2" v-if="userFlexiHour && userFlexiHour !== 'N/A'">
|
||||
<small class="text-muted">
|
||||
Valid After Office Hours ranges: 00:00 - {{ flexiTimes.start }} or {{ flexiTimes.end }} - 00:00
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" v-if="showAirDropdown">
|
||||
@ -166,6 +178,9 @@
|
||||
isUserAdmin: false,
|
||||
departmentName: "",
|
||||
areUserSettingsComplete: false,
|
||||
// Define these constants in data for access via `this`
|
||||
minAllowedFromMidnightTo: "16:30", // 4:30 PM
|
||||
maxAllowedFromMidnightTo: "23:30", // 11:30 PM
|
||||
breakOptions: Array.from({ length: 15 }, (_, i) => {
|
||||
const totalMinutes = i * 30;
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
@ -201,7 +216,20 @@
|
||||
},
|
||||
requiresStation() {
|
||||
return this.showAirDropdown || this.showMarineDropdown;
|
||||
},
|
||||
isOfficeHoursDisabled() {
|
||||
return this.detectedDayType === "Weekday";
|
||||
},
|
||||
flexiTimes() {
|
||||
if (this.userFlexiHour === 'N/A') {
|
||||
return { start: null, end: null };
|
||||
}
|
||||
const parts = this.userFlexiHour.split(' - ');
|
||||
return {
|
||||
start: parts[0],
|
||||
end: parts[1]
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
airStationList: {
|
||||
@ -255,6 +283,15 @@
|
||||
if (selectElement.length && selectElement.val() !== newVal) {
|
||||
selectElement.val(newVal).trigger('change.select2');
|
||||
}
|
||||
},
|
||||
detectedDayType(newVal) {
|
||||
// When the day type changes, clear office hours if it becomes a weekday
|
||||
if (newVal === "Weekday") {
|
||||
this.officeFrom = "";
|
||||
this.officeTo = "";
|
||||
this.officeBreak = 0;
|
||||
}
|
||||
this.calculateOTAndBreak(); // Recalculate OT and break when day type changes
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@ -442,8 +479,43 @@
|
||||
},
|
||||
handleDateChange() {
|
||||
this.updateDayType();
|
||||
// Clear office hour fields if it's a weekday
|
||||
if (this.isOfficeHoursDisabled) {
|
||||
this.officeFrom = "";
|
||||
this.officeTo = "";
|
||||
this.officeBreak = 0;
|
||||
}
|
||||
this.calculateOTAndBreak();
|
||||
},
|
||||
// Moved and updated validateTimeRangeForSubmission to be a method
|
||||
validateTimeRangeForSubmission(fromTime, toTime, label) {
|
||||
if (!fromTime || !toTime) return true;
|
||||
|
||||
const start = this.parseTime(fromTime);
|
||||
const end = this.parseTime(toTime);
|
||||
|
||||
// Use 'this' to access data properties
|
||||
const minAllowedFromMinutesForMidnightTo = this.parseTime(this.minAllowedFromMidnightTo).hours * 60 + this.parseTime(this.minAllowedFromMidnightTo).minutes;
|
||||
const maxAllowedFromMidnightTo = this.parseTime(this.maxAllowedFromMidnightTo).hours * 60 + this.parseTime(this.maxAllowedFromMidnightTo).minutes;
|
||||
|
||||
const startMinutes = start.hours * 60 + start.minutes;
|
||||
const endMinutes = end.hours * 60 + end.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;
|
||||
}
|
||||
if (startMinutes < minAllowedFromMinutesForMidnightTo || startMinutes > maxAllowedFromMidnightTo) {
|
||||
alert(`Invalid ${label} Time: If 'To' is 12:00 am (00:00), 'From' must start between end of your flexi hour to 11:30 pm on the same day.`);
|
||||
return false;
|
||||
}
|
||||
} else if (endMinutes <= startMinutes) {
|
||||
alert(`Invalid ${label} Time: 'To' time must be later than 'From' time for durations within the same day.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
async addOvertime() {
|
||||
if (!this.areUserSettingsComplete) {
|
||||
alert("Cannot save overtime: Your essential user settings are incomplete. Please contact IT or HR.");
|
||||
@ -485,38 +557,11 @@
|
||||
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;
|
||||
|
||||
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')) {
|
||||
// Now call the shared validation method correctly using 'this'
|
||||
if (hasOfficeHours && !this.validateTimeRangeForSubmission(this.officeFrom, this.officeTo, 'Office Hour')) {
|
||||
return;
|
||||
}
|
||||
if (hasAfterHours && !validateTimeRangeForSubmission(this.afterFrom, this.afterTo, 'After Office Hour')) {
|
||||
if (hasAfterHours && !this.validateTimeRangeForSubmission(this.afterFrom, this.afterTo, 'After Office Hour')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -545,8 +590,8 @@
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP error! status: ${response.status} - ${errorText}`);
|
||||
const errorMessage = await response.text();
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
@ -560,8 +605,8 @@
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error adding overtime:", error);
|
||||
alert(`Failed to save overtime. Error: ${error.message}`);
|
||||
// Display the specific error message from the backend.
|
||||
alert(`${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
@ -623,7 +668,6 @@
|
||||
}
|
||||
|
||||
this.otDescription = "";
|
||||
this.userFlexiHour = "";
|
||||
this.detectedDayType = "";
|
||||
this.totalOTHours = "0 hr 0 min";
|
||||
this.totalBreakHours = "0 hr 0 min";
|
||||
|
||||
@ -1,32 +1,33 @@
|
||||
using Azure.Core;
|
||||
using DocumentFormat.OpenXml.InkML;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages;
|
||||
using Mono.TextTemplating;
|
||||
using Newtonsoft.Json;
|
||||
using PSTW_CentralSystem.Areas.OTcalculate.Models;
|
||||
using PSTW_CentralSystem.Areas.OTcalculate.Services;
|
||||
using PSTW_CentralSystem.Controllers.API;
|
||||
using PSTW_CentralSystem.Controllers.API.Inventory;
|
||||
using PSTW_CentralSystem.DBContext;
|
||||
using PSTW_CentralSystem.Models;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Data;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
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;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using DocumentFormat.OpenXml.InkML;
|
||||
using RestSharp.Extensions;
|
||||
using System;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using static PSTW_CentralSystem.Areas.OTcalculate.Models.OtRegisterModel;
|
||||
using static System.Collections.Specialized.BitVector32;
|
||||
|
||||
|
||||
namespace PSTW_CentralSystem.Controllers.API
|
||||
@ -863,7 +864,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
|
||||
#endregion
|
||||
|
||||
#region OtRegister
|
||||
#region Ot Register
|
||||
[HttpGet("GetStationsByDepartment")]
|
||||
public async Task<IActionResult> GetStationsByDepartment([FromQuery] int? departmentId)
|
||||
{
|
||||
@ -966,7 +967,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
|
||||
if (officeFrom.Value < minAllowedFromMidnightTo || officeFrom.Value > maxAllowedFromMidnightTo)
|
||||
{
|
||||
return BadRequest("Invalid Office Hour 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 BadRequest("Invalid Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between end of your flexi hour to 11:30 pm on the same day.");
|
||||
}
|
||||
}
|
||||
else if (officeTo <= officeFrom)
|
||||
@ -986,7 +987,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
|
||||
if (afterFrom.Value < minAllowedFromMidnightTo || afterFrom.Value > maxAllowedFromMidnightTo)
|
||||
{
|
||||
return BadRequest("Invalid After Office Hour 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 BadRequest("Invalid After Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between end of your flexi hour to 11:30 pm on the same day.");
|
||||
}
|
||||
}
|
||||
else if (afterTo <= afterFrom)
|
||||
@ -1000,6 +1001,68 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
return BadRequest("Please enter either Office Hours or After Office Hours.");
|
||||
}
|
||||
|
||||
// Convert the request date to a DateOnly for comparison
|
||||
DateOnly requestDate = DateOnly.FromDateTime(request.OtDate);
|
||||
|
||||
// Fetch existing overtime records for the same user and date
|
||||
var existingOvertimeRecords = await _centralDbContext.Otregisters
|
||||
.Where(o => o.UserId == request.UserId && DateOnly.FromDateTime(o.OtDate) == requestDate)
|
||||
.ToListAsync();
|
||||
|
||||
// Check for overlaps with existing records
|
||||
foreach (var existingRecord in existingOvertimeRecords)
|
||||
{
|
||||
// Convert existing record times to actual TimeSpan objects
|
||||
var existingOfficeFrom = existingRecord.OfficeFrom;
|
||||
var existingOfficeTo = existingRecord.OfficeTo;
|
||||
var existingAfterFrom = existingRecord.AfterFrom;
|
||||
var existingAfterTo = existingRecord.AfterTo;
|
||||
|
||||
// Function to check for overlap between two time ranges
|
||||
Func<TimeSpan?, TimeSpan?, TimeSpan?, TimeSpan?, bool> CheckOverlap =
|
||||
(newStart, newEnd, existingStart, existingEnd) =>
|
||||
{
|
||||
if (!newStart.HasValue || !newEnd.HasValue || !existingStart.HasValue || !existingEnd.HasValue)
|
||||
{
|
||||
return false; // No overlap if either range is incomplete
|
||||
}
|
||||
|
||||
// Handle midnight (00:00) as end of day (24:00) for comparison purposes
|
||||
TimeSpan adjustedNewEnd = (newEnd == TimeSpan.Zero && newStart != TimeSpan.Zero) ? TimeSpan.FromHours(24) : newEnd.Value;
|
||||
TimeSpan adjustedExistingEnd = (existingEnd == TimeSpan.Zero && existingStart != TimeSpan.Zero) ? TimeSpan.FromHours(24) : existingEnd.Value;
|
||||
|
||||
|
||||
// Check for overlap:
|
||||
// New range starts before existing range ends AND
|
||||
// New range ends after existing range starts
|
||||
return (newStart.Value < adjustedExistingEnd && adjustedNewEnd > existingStart.Value);
|
||||
};
|
||||
|
||||
// Check for overlap between new Office Hours and existing Office Hours
|
||||
if (CheckOverlap(officeFrom, officeTo, existingOfficeFrom, existingOfficeTo))
|
||||
{
|
||||
return BadRequest("Your Office Hours entry overlaps with another record on this date. Kindly adjust your time.");
|
||||
}
|
||||
|
||||
// Check for overlap between new After Office Hours and existing After Office Hours
|
||||
if (CheckOverlap(afterFrom, afterTo, existingAfterFrom, existingAfterTo))
|
||||
{
|
||||
return BadRequest("Your Office Hours entry overlaps with another record on this date. Kindly adjust your time.");
|
||||
}
|
||||
|
||||
// Check for overlap between new Office Hours and existing After Office Hours
|
||||
if (CheckOverlap(officeFrom, officeTo, existingAfterFrom, existingAfterTo))
|
||||
{
|
||||
return BadRequest("Your Office Hours entry overlaps with another record on this date. Kindly adjust your time.");
|
||||
}
|
||||
|
||||
// Check for overlap between new After Office Hours and existing Office Hours
|
||||
if (CheckOverlap(afterFrom, afterTo, existingOfficeFrom, existingOfficeTo))
|
||||
{
|
||||
return BadRequest("Your Office Hours entry overlaps with another record on this date. Kindly adjust your time.");
|
||||
}
|
||||
}
|
||||
|
||||
var newRecord = new OtRegisterModel
|
||||
{
|
||||
OtDate = request.OtDate,
|
||||
@ -1393,6 +1456,11 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
return BadRequest(new { message = timeValidationError });
|
||||
}
|
||||
|
||||
if (HasOverlappingRecords(model, model.UserId))
|
||||
{
|
||||
return BadRequest(new { message = "Your Office Hours entry overlaps with another record on this date. Kindly adjust your time." });
|
||||
}
|
||||
|
||||
var existing = _centralDbContext.Otregisters.FirstOrDefault(o => o.OvertimeId == model.OvertimeId);
|
||||
if (existing == null)
|
||||
{
|
||||
@ -1442,7 +1510,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
|
||||
if (officeFrom.Value < minAllowedFromMidnightTo || officeFrom.Value > maxAllowedFromMidnightTo)
|
||||
{
|
||||
return "Invalid Office Hour 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 "Invalid Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between end of your flexi hour to 11:30 pm on the same day.";
|
||||
}
|
||||
}
|
||||
else if (officeTo <= officeFrom)
|
||||
@ -1462,7 +1530,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
|
||||
if (afterFrom.Value < minAllowedFromMidnightTo || afterFrom.Value > maxAllowedFromMidnightTo)
|
||||
{
|
||||
return "Invalid After Office Hour 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 "Invalid After Office Hour Time: If 'To' is 12:00 am (00:00), 'From' must start between end of your flexi hour to 11:30 pm on the same day.";
|
||||
}
|
||||
}
|
||||
else if (afterTo <= afterFrom)
|
||||
@ -1478,6 +1546,82 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool HasOverlappingRecords(OtRegisterUpdateDto model, int userId)
|
||||
{
|
||||
// Retrieve all other overtime records for the same user and date,
|
||||
// excluding the current record being updated.
|
||||
var existingRecords = _centralDbContext.Otregisters
|
||||
.Where(o => o.UserId == userId && o.OtDate.Date == model.OtDate.Date && o.OvertimeId != model.OvertimeId)
|
||||
.ToList();
|
||||
|
||||
// The logic to check for overlap between two time ranges A and B is:
|
||||
// A_start < B_end AND B_start < A_end
|
||||
// This handles all scenarios, including one range being entirely inside another.
|
||||
|
||||
// Check for overlap with the Office Hours block
|
||||
if (!string.IsNullOrEmpty(model.OfficeFrom) && !string.IsNullOrEmpty(model.OfficeTo))
|
||||
{
|
||||
var newOfficeStart = TimeSpan.Parse(model.OfficeFrom);
|
||||
var newOfficeEnd = TimeSpan.Parse(model.OfficeTo);
|
||||
foreach (var record in existingRecords)
|
||||
{
|
||||
TimeSpan? existingOfficeStart = record.OfficeFrom;
|
||||
TimeSpan? existingOfficeEnd = record.OfficeTo;
|
||||
TimeSpan? existingAfterStart = record.AfterFrom;
|
||||
TimeSpan? existingAfterEnd = record.AfterTo;
|
||||
|
||||
// Check against existing Office Hours
|
||||
if (existingOfficeStart.HasValue && existingOfficeEnd.HasValue)
|
||||
{
|
||||
if (newOfficeStart < existingOfficeEnd.Value && existingOfficeStart.Value < newOfficeEnd)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Check against existing After Office Hours
|
||||
if (existingAfterStart.HasValue && existingAfterEnd.HasValue)
|
||||
{
|
||||
if (newOfficeStart < existingAfterEnd.Value && existingAfterStart.Value < newOfficeEnd)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for overlap with the After Office Hours block
|
||||
if (!string.IsNullOrEmpty(model.AfterFrom) && !string.IsNullOrEmpty(model.AfterTo))
|
||||
{
|
||||
var newAfterStart = TimeSpan.Parse(model.AfterFrom);
|
||||
var newAfterEnd = TimeSpan.Parse(model.AfterTo);
|
||||
foreach (var record in existingRecords)
|
||||
{
|
||||
TimeSpan? existingOfficeStart = record.OfficeFrom;
|
||||
TimeSpan? existingOfficeEnd = record.OfficeTo;
|
||||
TimeSpan? existingAfterStart = record.AfterFrom;
|
||||
TimeSpan? existingAfterEnd = record.AfterTo;
|
||||
|
||||
// Check against existing Office Hours
|
||||
if (existingOfficeStart.HasValue && existingOfficeEnd.HasValue)
|
||||
{
|
||||
if (newAfterStart < existingOfficeEnd.Value && existingOfficeStart.Value < newAfterEnd)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Check against existing After Office Hours
|
||||
if (existingAfterStart.HasValue && existingAfterEnd.HasValue)
|
||||
{
|
||||
if (newAfterStart < existingAfterEnd.Value && existingAfterStart.Value < newAfterEnd)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region OT Status
|
||||
@ -1993,12 +2137,21 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
return NotFound(new { message = "Overtime record not found." });
|
||||
}
|
||||
|
||||
// **NEW LOGIC: Check for overlapping entries**
|
||||
if (IsOverlapping(updatedRecordDto, existingRecord.UserId, updatedRecordDto.OvertimeId))
|
||||
{
|
||||
return BadRequest(new { message = "The new overtime time range overlaps with an existing entry for the same date." });
|
||||
}
|
||||
|
||||
// ... (rest of your existing logic for finding otStatus, permissions, and logging)
|
||||
// The previous code for validation will be moved to a helper method or retained if it's still needed
|
||||
var otStatus = _centralDbContext.Otstatus.FirstOrDefault(s => s.StatusId == updatedRecordDto.StatusId);
|
||||
if (otStatus == null)
|
||||
{
|
||||
return NotFound("OT status not found.");
|
||||
}
|
||||
|
||||
// ... (rest of the code for checking permissions and logging)
|
||||
var currentLoggedInUserId = GetCurrentLoggedInUserId();
|
||||
string approverRole = GetApproverRole(currentLoggedInUserId, otStatus.StatusId);
|
||||
|
||||
@ -2029,6 +2182,7 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
||||
});
|
||||
|
||||
// Update the existing record with the new data
|
||||
existingRecord.OtDate = updatedRecordDto.OtDate;
|
||||
existingRecord.OfficeFrom = ParseTimeStringToTimeSpan(updatedRecordDto.OfficeFrom);
|
||||
existingRecord.OfficeTo = ParseTimeStringToTimeSpan(updatedRecordDto.OfficeTo);
|
||||
@ -2075,6 +2229,78 @@ namespace PSTW_CentralSystem.Controllers.API
|
||||
return Ok(new { message = "Overtime record updated successfully and changes logged." });
|
||||
}
|
||||
|
||||
// **NEW HELPER METHOD: IsOverlapping**
|
||||
private bool IsOverlapping(OtRegisterEditDto updatedRecord, int userId, int currentRecordId)
|
||||
{
|
||||
// Filter existing records for the same date, excluding the one being edited
|
||||
var existingRecords = _centralDbContext.Otregisters
|
||||
.Where(o => o.UserId == userId && o.OtDate.Date == updatedRecord.OtDate.Date && o.OvertimeId != currentRecordId)
|
||||
.ToList();
|
||||
|
||||
// Check for overlaps with the new time ranges
|
||||
TimeSpan? newOfficeFrom = ParseTimeStringToTimeSpan(updatedRecord.OfficeFrom);
|
||||
TimeSpan? newOfficeTo = ParseTimeStringToTimeSpan(updatedRecord.OfficeTo);
|
||||
TimeSpan? newAfterFrom = ParseTimeStringToTimeSpan(updatedRecord.AfterFrom);
|
||||
TimeSpan? newAfterTo = ParseTimeStringToTimeSpan(updatedRecord.AfterTo);
|
||||
|
||||
foreach (var existing in existingRecords)
|
||||
{
|
||||
// Check if the new office hours overlap with any existing time range
|
||||
if (newOfficeFrom.HasValue && newOfficeTo.HasValue)
|
||||
{
|
||||
if (CheckOverlapBetween(newOfficeFrom.Value, newOfficeTo.Value, existing.OfficeFrom, existing.OfficeTo) ||
|
||||
CheckOverlapBetween(newOfficeFrom.Value, newOfficeTo.Value, existing.AfterFrom, existing.AfterTo))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the new after office hours overlap with any existing time range
|
||||
if (newAfterFrom.HasValue && newAfterTo.HasValue)
|
||||
{
|
||||
if (CheckOverlapBetween(newAfterFrom.Value, newAfterTo.Value, existing.OfficeFrom, existing.OfficeTo) ||
|
||||
CheckOverlapBetween(newAfterFrom.Value, newAfterTo.Value, existing.AfterFrom, existing.AfterTo))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// **NEW HELPER METHOD: CheckOverlapBetween**
|
||||
private bool CheckOverlapBetween(TimeSpan start1, TimeSpan end1, TimeSpan? start2, TimeSpan? end2)
|
||||
{
|
||||
// If either of the second time range's values is missing, there can be no overlap.
|
||||
if (!start2.HasValue || !end2.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create local, non-nullable variables to work with.
|
||||
TimeSpan s1 = start1;
|
||||
TimeSpan e1 = end1;
|
||||
TimeSpan s2 = start2.Value;
|
||||
TimeSpan e2 = end2.Value;
|
||||
|
||||
// Handle overnight cases for both ranges by adding 24 hours to the end time
|
||||
// if it's on or before the start time.
|
||||
// TimeSpan.FromHours(24) is a cleaner way to express this.
|
||||
if (e1 <= s1)
|
||||
{
|
||||
e1 = e1.Add(TimeSpan.FromHours(24));
|
||||
}
|
||||
if (e2 <= s2)
|
||||
{
|
||||
e2 = e2.Add(TimeSpan.FromHours(24));
|
||||
}
|
||||
|
||||
// Overlap exists if the start of one range is before the end of the other,
|
||||
// AND the start of the other is before the end of the first.
|
||||
return s1 < e2 && s2 < e1;
|
||||
}
|
||||
|
||||
[HttpDelete("DeleteOvertimeInOtReview/{id}")]
|
||||
public IActionResult DeleteOvertimeInOtReview(int id)
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user