update OT

This commit is contained in:
Naz 2025-08-06 11:34:32 +08:00
parent 20b9f1e1fd
commit e3bf644a0b
4 changed files with 711 additions and 305 deletions

View File

@ -192,7 +192,7 @@
<td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="3">TOTAL</td> <td v-if="!isApproverRole(['HoU', 'HoD', 'Manager'])" colspan="3">TOTAL</td>
<td v-else colspan="1">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 v-else colspan="2"></td>
<td><strong>{{ formatBreakToHourMinute(totals.officeBreak) }}</strong></td> <td><strong>{{ formatBreakToHourMinute(totals.officeBreak) }}</strong></td>
@ -247,31 +247,35 @@
<div class="row"> <div class="row">
<div class="col-md-7"> <div class="col-md-7">
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="dateInput">Date</label> <label class="form-label" for="modalDateInput">Date</label>
<input type="date" class="form-control" id="dateInput" v-model="currentEditRecord.otDate"> <input type="date" class="form-control" id="modalDateInput" v-model="currentEditRecord.otDate">
</div> </div>
<h6 class="fw-bold">OFFICE HOURS</h6> <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 class="d-flex gap-3 mb-3 align-items-end flex-wrap">
<div style="flex: 1;"> <div style="flex: 1;">
<label for="officeFrom">From</label> <label for="modalOfficeFrom">From</label>
<input type="time" id="officeFrom" class="form-control" <input type="time" id="modalOfficeFrom" class="form-control"
v-model="currentEditRecord.officeFrom" v-model="currentEditRecord.officeFrom"
v-on:change="roundMinutes('officeFrom'); validateTimeFields()"> v-on:change="roundMinutes('officeFrom'); validateTimeFields()"
:disabled="isModalOfficeHoursDisabled">
</div> </div>
<div style="flex: 1;"> <div style="flex: 1;">
<label for="officeTo">To</label> <label for="modalOfficeTo">To</label>
<input type="time" id="officeTo" class="form-control" <input type="time" id="modalOfficeTo" class="form-control"
v-model="currentEditRecord.officeTo" v-model="currentEditRecord.officeTo"
v-on:change="roundMinutes('officeTo'); validateTimeFields()"> v-on:change="roundMinutes('officeTo'); validateTimeFields()"
:disabled="isModalOfficeHoursDisabled">
</div> </div>
<div style="flex: 1;"> <div style="flex: 1;">
<label for="officeBreak">Break Hours (Minutes)</label> <label for="modalOfficeBreak">Break Hours (Min)</label>
<div class="d-flex"> <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> <option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
</select> </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> <i class="bi bi-x-circle"></i>
</button> </button>
</div> </div>
@ -279,23 +283,24 @@
</div> </div>
<h6 class="fw-bold text-danger">AFTER OFFICE HOURS</h6> <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 class="d-flex gap-3 mb-3 align-items-end flex-wrap">
<div style="flex: 1;"> <div style="flex: 1;">
<label for="afterFrom">From</label> <label for="modalAfterFrom">From</label>
<input type="time" id="afterFrom" class="form-control" <input type="time" id="modalAfterFrom" class="form-control"
v-model="currentEditRecord.afterFrom" v-model="currentEditRecord.afterFrom"
v-on:change="roundMinutes('afterFrom'); validateTimeFields()"> v-on:change="roundMinutes('afterFrom'); validateTimeFields()">
</div> </div>
<div style="flex: 1;"> <div style="flex: 1;">
<label for="afterTo">To</label> <label for="modalAfterTo">To</label>
<input type="time" id="afterTo" class="form-control" <input type="time" id="modalAfterTo" class="form-control"
v-model="currentEditRecord.afterTo" v-model="currentEditRecord.afterTo"
v-on:change="roundMinutes('afterTo'); validateTimeFields()"> v-on:change="roundMinutes('afterTo'); validateTimeFields()">
</div> </div>
<div style="flex: 1;"> <div style="flex: 1;">
<label for="afterBreak">Break Hours (Minutes)</label> <label for="modalAfterBreak">Break Hours (Min)</label>
<div class="d-flex"> <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> <option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
</select> </select>
<button class="btn btn-outline-danger ms-2" v-on:click="clearAfterHours" title="Clear After Office Hours"> <button class="btn btn-outline-danger ms-2" v-on:click="clearAfterHours" title="Clear After Office Hours">
@ -306,8 +311,8 @@
</div> </div>
<div class="mb-3" v-show="showAirStationDropdown"> <div class="mb-3" v-show="showAirStationDropdown">
<label for="airstationDropdown">Air Station</label> <label for="modalAirstationDropdown">Air Station</label>
<select id="airstationDropdown" class="form-control select2-enable" style="width: 100%;"> <select id="modalAirstationDropdown" class="form-control select2-enable" style="width: 100%;">
<option value="" disabled selected>Select Air Station</option> <option value="" disabled selected>Select Air Station</option>
<option v-for="station in airStations" :key="station.stationId" :value="station.stationId"> <option v-for="station in airStations" :key="station.stationId" :value="station.stationId">
{{ station.stationName || 'Unnamed Station' }} {{ station.stationName || 'Unnamed Station' }}
@ -317,8 +322,8 @@
</div> </div>
<div class="mb-3" v-show="showMarineStationDropdown"> <div class="mb-3" v-show="showMarineStationDropdown">
<label for="marinestationDropdown">Marine Station</label> <label for="modalMarinestationDropdown">Marine Station</label>
<select id="marinestationDropdown" class="form-control select2-enable" style="width: 100%;"> <select id="modalMarinestationDropdown" class="form-control select2-enable" style="width: 100%;">
<option value="" disabled selected>Select Marine Station</option> <option value="" disabled selected>Select Marine Station</option>
<option v-for="station in marineStations" :key="station.stationId" :value="station.stationId"> <option v-for="station in marineStations" :key="station.stationId" :value="station.stationId">
{{ station.stationName || 'Unnamed Station' }} {{ station.stationName || 'Unnamed Station' }}
@ -328,33 +333,34 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="otDescription">Work Brief Description</label> <label for="modalOtDescription">Work Brief Description <span class="text-danger">*</span></label>
<textarea id="otDescription" class="form-control" v-model="currentEditRecord.otDescription" placeholder="Describe the work done..."></textarea> <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> <small class="text-muted">{{ currentEditRecord.otDescription ? currentEditRecord.otDescription.length : 0 }} / 150 characters</small>
</div> </div>
</div> </div>
<div class="col-md-5 mt-5"> <div class="col-md-5 mt-5">
<div class="mb-3 d-flex flex-column align-items-center"> <div class="mb-3 d-flex flex-column align-items-center">
<label for="userFlexiHourDisplay">Your Flexi Hour</label> <label for="modalUserFlexiHourDisplay">Your Flexi Hour</label>
<input type="text" id="userFlexiHourDisplay" class="form-control text-center" :value="userInfo.flexiHour || 'N/A'" readonly style="width: 200px;"> <input type="text" id="modalUserFlexiHourDisplay" class="form-control text-center" :value="userInfo.flexiHour || 'N/A'" readonly style="width: 200px;">
</div> </div>
<div class="mb-3 d-flex flex-column align-items-center"> <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" <input type="text" class="form-control text-center"
:value="getDayType(currentEditRecord.otDate)" :value="getDayType(currentEditRecord.otDate)"
readonly style="width: 200px;"> readonly style="width: 200px;">
</div> </div>
<div class="mb-3 d-flex flex-column align-items-center"> <div class="mb-3 d-flex flex-column align-items-center">
<label for="totalOTHours">Total OT Hours</label> <label for="modalTotalOTHours">Total OT Hours</label>
<input type="text" id="totalOTHours" class="form-control text-center" :value="calculateModalTotalOtHrs()" style="width: 200px;" readonly> <input type="text" id="modalTotalOTHours" class="form-control text-center" :value="calculateModalTotalOtHrs()" style="width: 200px;" readonly>
</div> </div>
<div class="mb-3 d-flex flex-column align-items-center"> <div class="mb-3 d-flex flex-column align-items-center">
<label for="totalBreakHours">Total Break Hours</label> <label for="modalTotalBreakHours">Total Break Hours</label>
<input type="text" id="totalBreakHours" class="form-control text-center" :value="calculateModalTotalBreakHrs()" style="width: 200px;" readonly> <input type="text" id="modalTotalBreakHours" class="form-control text-center" :value="calculateModalTotalBreakHrs()" style="width: 200px;" readonly>
</div> </div>
</div> </div>
</div> </div>
@ -387,7 +393,7 @@
weekendId: null, weekendId: null,
basicSalary: null basicSalary: null
}, },
isHoU: false, // Keep this for existing logic, but use isApproverRole for salary visibility isHoU: false,
expandedDescriptions: [], expandedDescriptions: [],
currentEditRecord: { currentEditRecord: {
overtimeId: null, overtimeId: null,
@ -401,7 +407,7 @@
stationId: null, stationId: null,
otDescription: '', otDescription: '',
statusId: null, statusId: null,
otDays: '' otDays: '' // This will store the day type for the modal's selected date
}, },
airStations: [], airStations: [],
marineStations: [], marineStations: [],
@ -520,32 +526,51 @@
default: default:
return true; 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: { watch: {
'currentEditRecord.otDate': function(newDate) { '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) { 'currentEditRecord.stationId': function(newVal) {
if (this.showAirStationDropdown) { if (this.showAirStationDropdown) {
$('#airstationDropdown').val(newVal).trigger('change.select2'); $('#modalAirstationDropdown').val(newVal).trigger('change.select2');
} else if (this.showMarineStationDropdown) { } else if (this.showMarineStationDropdown) {
$('#marinestationDropdown').val(newVal).trigger('change.select2'); $('#modalMarinestationDropdown').val(newVal).trigger('change.select2');
} }
}, },
airStations: function() { airStations: function() {
if (this.showAirStationDropdown) { if (this.showAirStationDropdown) {
this.initSelect2('#airstationDropdown'); this.initSelect2('#modalAirstationDropdown');
} }
}, },
marineStations: function() { marineStations: function() {
if (this.showMarineStationDropdown) { if (this.showMarineStationDropdown) {
this.initSelect2('#marinestationDropdown'); this.initSelect2('#modalMarinestationDropdown');
} }
} }
}, },
methods: { methods: {
// New method to check if the current approver role is in the list of roles to hide salary info
isApproverRole(rolesToHide) { isApproverRole(rolesToHide) {
return rolesToHide.includes(this.approverRole); return rolesToHide.includes(this.approverRole);
}, },
@ -566,7 +591,6 @@
...data.userInfo, ...data.userInfo,
basicSalary: data.userInfo.rate 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.isHoU = data.isHoU;
this.currentEditRecord.statusId = parseInt(statusId); this.currentEditRecord.statusId = parseInt(statusId);
@ -600,16 +624,12 @@
} }
}, },
initSelect2(selector) { initSelect2(selector) {
if ($(selector).data('select2')) { if ($(selector).data('select2')) {
$(selector).select2('destroy'); $(selector).select2('destroy');
} }
$(selector).select2({ $(selector).select2({
dropdownParent: $('#editOtModal') dropdownParent: $('#editOtModal')
}).on('change', (e) => { }).on('change', (e) => {
this.currentEditRecord.stationId = $(e.currentTarget).val(); this.currentEditRecord.stationId = $(e.currentTarget).val();
}); });
@ -728,9 +748,8 @@
break; break;
case 'Off Day': case 'Off Day':
case 'Weekend': case 'Weekend': // Assume Weekend is handled like Off Day if not explicitly categorized
if (officeHrs > 0) { if (officeHrs > 0) {
if (officeHrs <= 4) { if (officeHrs <= 4) {
result.odUnder4 = toFixedOrEmpty(officeHrs); result.odUnder4 = toFixedOrEmpty(officeHrs);
} else if (officeHrs <= 8) { } else if (officeHrs <= 8) {
@ -739,9 +758,7 @@
result.odAfter = toFixedOrEmpty(officeHrs); result.odAfter = toFixedOrEmpty(officeHrs);
} }
} }
result.odAfter = toFixedOrEmpty((parseFloat(result.odAfter) || 0) + afterHrs);
result.odAfter = toFixedOrEmpty( (parseFloat(result.odAfter) || 0) + afterHrs );
break; break;
case 'Rest Day': case 'Rest Day':
@ -754,12 +771,10 @@
result.rdAfter = toFixedOrEmpty(officeHrs); result.rdAfter = toFixedOrEmpty(officeHrs);
} }
} }
result.rdAfter = toFixedOrEmpty((parseFloat(result.rdAfter) || 0) + afterHrs);
result.rdAfter = toFixedOrEmpty( (parseFloat(result.rdAfter) || 0) + afterHrs );
break; break;
case 'Public Holiday': case 'Public Holiday':
if (officeHrs > 0) { if (officeHrs > 0) {
if (officeHrs <= 8) { if (officeHrs <= 8) {
result.phUnder8 = toFixedOrEmpty(officeHrs); result.phUnder8 = toFixedOrEmpty(officeHrs);
@ -767,8 +782,7 @@
result.phAfter = toFixedOrEmpty(officeHrs); result.phAfter = toFixedOrEmpty(officeHrs);
} }
} }
result.phAfter = toFixedOrEmpty((parseFloat(result.phAfter) || 0) + afterHrs);
result.phAfter = toFixedOrEmpty( (parseFloat(result.phAfter) || 0) + afterHrs );
break; break;
} }
@ -834,8 +848,9 @@
amountOffice = 0.5 * orp; amountOffice = 0.5 * orp;
} else if (officeHours <= 8) { } else if (officeHours <= 8) {
amountOffice = 1 * orp; amountOffice = 1 * orp;
} else {
amountOffice = 1 * orp; // For >8 hours, still based on 1 ORP for office hours
} }
} else if (otType === 'Public Holiday') { } else if (otType === 'Public Holiday') {
amountOffice = 2 * orp; amountOffice = 2 * orp;
} }
@ -860,27 +875,19 @@
return totalAmount.toFixed(2); return totalAmount.toFixed(2);
}, },
roundMinutes(fieldName) { roundMinutes(fieldName) {
const timeValue = this.currentEditRecord[fieldName]; const timeStr = this.currentEditRecord[fieldName];
if (!timeValue) return; if (!timeStr) return;
const [hours, minutes] = timeValue.split(':').map(Number); const [hours, minutes] = timeStr.split(':').map(Number);
let roundedMinutes = minutes; const totalMinutes = hours * 60 + minutes;
if (minutes >= 45 || minutes < 15) { const remainder = totalMinutes % 30;
roundedMinutes = 0; const roundedMinutes = remainder < 15 ? totalMinutes - remainder : totalMinutes + (30 - remainder);
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;
}
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(); this.validateTimeFields();
}, },
editRecord(id) { editRecord(id) {
@ -908,18 +915,22 @@
afterBreak: recordToEdit.afterBreak || 0, afterBreak: recordToEdit.afterBreak || 0,
stationId: recordToEdit.stationId, stationId: recordToEdit.stationId,
otDescription: recordToEdit.otDescription, otDescription: recordToEdit.otDescription,
statusId: this.currentEditRecord.statusId, statusId: this.currentEditRecord.statusId, // Retain the overall statusId
otDays: recordToEdit.otDays otDays: recordToEdit.dayType || this.getDayType(formattedDate) // Ensure otDays is set correctly
}; };
if (this.isModalOfficeHoursDisabled) {
this.clearOfficeHours();
}
this.$nextTick(() => { this.$nextTick(() => {
if (this.showAirStationDropdown) { if (this.showAirStationDropdown) {
this.initSelect2('#airstationDropdown'); this.initSelect2('#modalAirstationDropdown');
$('#airstationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2'); $('#modalAirstationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');
} }
if (this.showMarineStationDropdown) { if (this.showMarineStationDropdown) {
this.initSelect2('#marinestationDropdown'); this.initSelect2('#modalMarinestationDropdown');
$('#marinestationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2'); $('#modalMarinestationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');
} }
const editModal = new bootstrap.Modal(document.getElementById('editOtModal')); const editModal = new bootstrap.Modal(document.getElementById('editOtModal'));
editModal.show(); editModal.show();
@ -929,25 +940,8 @@
async submitEdit() { async submitEdit() {
try { try {
const hasOfficeHours = this.currentEditRecord.officeFrom && this.currentEditRecord.officeTo; // MODIFIED: Check for validation failure and exit early
const hasAfterHours = this.currentEditRecord.afterFrom && this.currentEditRecord.afterTo; if (!this.validateModalForm()) {
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')) {
return; return;
} }
@ -956,7 +950,12 @@
headers: { headers: {
'Content-Type': 'application/json' '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(); const data = await response.json();
@ -974,6 +973,120 @@
alert('An error occurred while updating the record.'); 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) { async fetchPublicHolidays(stateId) {
if (!stateId) { if (!stateId) {
this.publicHolidays = []; this.publicHolidays = [];
@ -990,7 +1103,7 @@
} }
}, },
getDayType(dateStr) { getDayType(dateStr) {
if (!dateStr || !this.userState || this.publicHolidays.length === 0) return ''; if (!dateStr || !this.userState) return '';
const selectedDate = new Date(dateStr + "T00:00:00"); const selectedDate = new Date(dateStr + "T00:00:00");
if (isNaN(selectedDate.getTime())) return ''; if (isNaN(selectedDate.getTime())) return '';
@ -1014,9 +1127,10 @@
if (dayOfWeek === 6) return 'Off Day'; if (dayOfWeek === 6) return 'Off Day';
if (dayOfWeek === 0) return 'Rest Day'; if (dayOfWeek === 0) return 'Rest Day';
else return 'Normal Day'; else return 'Normal Day';
} else {
if (dayOfWeek === 0) return 'Rest Day';
else return 'Normal Day';
} }
return 'Normal Day';
}, },
calculateModalTotalOtHrs() { calculateModalTotalOtHrs() {
@ -1048,6 +1162,7 @@
}, },
parseTime(timeString) { parseTime(timeString) {
if (!timeString) return { hours: -1, minutes: -1 };
const [hours, minutes] = timeString.split(':').map(Number); const [hours, minutes] = timeString.split(':').map(Number);
return { hours, minutes }; return { hours, minutes };
}, },
@ -1056,12 +1171,16 @@
this.currentEditRecord.officeFrom = ""; this.currentEditRecord.officeFrom = "";
this.currentEditRecord.officeTo = ""; this.currentEditRecord.officeTo = "";
this.currentEditRecord.officeBreak = 0; this.currentEditRecord.officeBreak = 0;
this.calculateModalTotalOtHrs();
this.calculateModalTotalBreakHrs();
}, },
clearAfterHours() { clearAfterHours() {
this.currentEditRecord.afterFrom = ""; this.currentEditRecord.afterFrom = "";
this.currentEditRecord.afterTo = ""; this.currentEditRecord.afterTo = "";
this.currentEditRecord.afterBreak = 0; this.currentEditRecord.afterBreak = 0;
this.calculateModalTotalOtHrs();
this.calculateModalTotalBreakHrs();
}, },
deleteRecord(id) { deleteRecord(id) {
@ -1202,8 +1321,8 @@
alert(`Invalid ${label} Time: 'From' and 'To' cannot both be 00:00 (midnight).`); alert(`Invalid ${label} Time: 'From' and 'To' cannot both be 00:00 (midnight).`);
return false; return false;
} }
if (startMinutes < minAllowedFromMidnightTo || startMinutes > maxAllowedFromMidnightTo) { if (startMinutes < minAllowedFromMinutesForMidnightTo || 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.`); 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; return false;
} }
} else if (end.hours * 60 + end.minutes <= start.hours * 60 + start.minutes) { } else if (end.hours * 60 + end.minutes <= start.hours * 60 + start.minutes) {
@ -1213,22 +1332,8 @@
return true; return true;
}, },
validateTimeFields() { validateTimeFields() {
const hasOfficeHours = this.currentEditRecord.officeFrom && this.currentEditRecord.officeTo; this.calculateModalTotalOtHrs();
const hasAfterHours = this.currentEditRecord.afterFrom && this.currentEditRecord.afterTo; this.calculateModalTotalBreakHrs();
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');
}
}, },
}, },
async created() { async created() {
@ -1237,23 +1342,28 @@
}, },
mounted() { mounted() {
if (this.showAirStationDropdown) { if (this.showAirStationDropdown) {
this.initSelect2('#airstationDropdown'); this.initSelect2('#modalAirstationDropdown');
} }
if (this.showMarineStationDropdown) { if (this.showMarineStationDropdown) {
this.initSelect2('#marinestationDropdown'); this.initSelect2('#modalMarinestationDropdown');
} }
var editModalElement = document.getElementById('editOtModal'); var editModalElement = document.getElementById('editOtModal');
if (editModalElement) { if (editModalElement) {
editModalElement.addEventListener('shown.bs.modal', () => { editModalElement.addEventListener('shown.bs.modal', () => {
if (this.showAirStationDropdown) { if (this.showAirStationDropdown) {
this.initSelect2('#airstationDropdown'); this.initSelect2('#modalAirstationDropdown');
$('#airstationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2'); $('#modalAirstationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');
} }
if (this.showMarineStationDropdown) { if (this.showMarineStationDropdown) {
this.initSelect2('#marinestationDropdown'); this.initSelect2('#modalMarinestationDropdown');
$('#marinestationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2'); $('#modalMarinestationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2');
} }
this.currentEditRecord.otDays = this.getDayType(this.currentEditRecord.otDate);
if (this.isModalOfficeHoursDisabled) {
this.clearOfficeHours();
}
this.validateTimeFields();
}); });
} }
} }

View File

@ -12,35 +12,41 @@
<div class="col-md-7"> <div class="col-md-7">
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="dateInput">Date <span class="text-danger">*</span></label> <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> <small class="text-danger" v-if="validationErrors.otDate">{{ validationErrors.otDate }}</small>
</div> </div>
<h6 class="fw-bold">OFFICE HOURS</h6> <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;"> <div style="flex: 1;">
<label for="officeFrom">From</label> <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>
<div style="flex: 1;"> <div style="flex: 1;">
<label for="officeTo">To</label> <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>
<div style="flex: 1;"> <div style="flex: 1;">
<label for="officeBreak">Break Hours (Minutes)</label> <label for="officeBreak">Break Hours (Minutes)</label>
<div class="d-flex"> <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> <option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
</select> </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> <i class="bi bi-x-circle"></i>
</button> </button>
</div> </div>
</div> </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> <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;"> <div style="flex: 1;">
<label for="afterFrom">From</label> <label for="afterFrom">From</label>
<input type="time" id="afterFrom" class="form-control" v-model="editForm.afterFrom" v-on:change="updateTime('afterFrom')"> <input type="time" id="afterFrom" class="form-control" v-model="editForm.afterFrom" v-on:change="updateTime('afterFrom')">
@ -61,6 +67,7 @@
</div> </div>
</div> </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"> <div class="mb-3" v-if="isPSTWAIR">
<label for="airstationDropdown">Air Station <span class="text-danger">*</span></label> <label for="airstationDropdown">Air Station <span class="text-danger">*</span></label>
@ -180,13 +187,25 @@
} }
}; };
}, },
computed: { computed: {
charCount() { charCount() {
return this.editForm.otDescription.length; return this.editForm.otDescription.length;
}, },
userFlexiHourDisplay() { userFlexiHourDisplay() {
return this.userFlexiHour ? this.userFlexiHour.flexiHour : "N/A"; 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() { async mounted() {
@ -221,11 +240,20 @@
$('#marinestationDropdown').val(this.editForm.stationId).trigger('change.select2'); $('#marinestationDropdown').val(this.editForm.stationId).trigger('change.select2');
} }
} }
if (this.isOfficeHoursDisabled) {
this.clearOfficeHours();
}
}); });
}, },
watch: {
'editForm.otDays'(newVal) {
if (newVal === "Weekday") {
this.clearOfficeHours();
}
}
},
methods: { methods: {
initializeSelect2Dropdowns() { initializeSelect2Dropdowns() {
if ($('#airstationDropdown').data('select2')) { if ($('#airstationDropdown').data('select2')) {
$('#airstationDropdown').select2('destroy'); $('#airstationDropdown').select2('destroy');
} }
@ -272,7 +300,6 @@
otDate: record.otDate ? record.otDate.slice(0, 10) : "", otDate: record.otDate ? record.otDate.slice(0, 10) : "",
}; };
this.calculateOTAndBreak(); this.calculateOTAndBreak();
this.updateDayType();
}, },
async fetchStations() { async fetchStations() {
@ -333,7 +360,7 @@
this.updateDayType(); this.updateDayType();
} catch (error) { } catch (error) {
console.error("Error fetching user state and holidays:", 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) { updateTime(fieldName) {
if (fieldName === 'officeFrom') { if (fieldName === 'officeFrom') {
this.editForm.officeFrom = this.roundToNearest30(this.editForm.officeFrom); this.editForm.officeFrom = this.roundToNearest30(this.editForm.officeFrom);
} else if (fieldName === 'officeTo') { } else if (fieldName === 'officeTo') {
@ -431,7 +457,7 @@
return false; return false;
} }
if (startMinutes < minAllowedFromMinutes || startMinutes > maxAllowedFromMinutes) { 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; return false;
} }
} else if (endMinutes <= startMinutes) { } else if (endMinutes <= startMinutes) {
@ -552,7 +578,6 @@
}, },
validateForm() { validateForm() {
this.validationErrors = { this.validationErrors = {
otDate: "", otDate: "",
stationId: "", stationId: "",
@ -563,84 +588,58 @@
let isValid = true; let isValid = true;
let errorMessages = []; let errorMessages = [];
if (!this.editForm.otDate) { if (!this.editForm.otDate) {
this.validationErrors.otDate = "Date is required."; this.validationErrors.otDate = "Date is required.";
errorMessages.push("Date is required."); errorMessages.push("Date is required.");
isValid = false; isValid = false;
} }
// Validate station (only if user is in PSTW AIR or MARINE)
if ((this.isPSTWAIR || this.isPSTWMARINE) && !this.editForm.stationId) { if ((this.isPSTWAIR || this.isPSTWMARINE) && !this.editForm.stationId) {
this.validationErrors.stationId = "Station is required."; this.validationErrors.stationId = "Station is required.";
errorMessages.push("Station is required."); errorMessages.push("Station is required.");
isValid = false; isValid = false;
} }
// Validate work brief description
if (!this.editForm.otDescription || this.editForm.otDescription.trim() === "") { if (!this.editForm.otDescription || this.editForm.otDescription.trim() === "") {
this.validationErrors.otDescription = "Work brief description is required."; this.validationErrors.otDescription = "Work brief description is required.";
errorMessages.push("Work brief description is required."); errorMessages.push("Work brief description is required.");
isValid = false; isValid = false;
} }
// Validate at least one time entry (either office hours or after hours) const officeFromFilled = !!this.editForm.officeFrom;
const hasOfficeHours = this.editForm.officeFrom && this.editForm.officeTo; const officeToFilled = !!this.editForm.officeTo;
const hasAfterHours = this.editForm.afterFrom && this.editForm.afterTo; const afterFromFilled = !!this.editForm.afterFrom;
const afterToFilled = !!this.editForm.afterTo;
if (!hasOfficeHours && !hasAfterHours) { if (!this.isOfficeHoursDisabled) {
this.validationErrors.timeEntries = "Please enter either Office Hours or After Hours."; if (officeFromFilled !== officeToFilled) {
errorMessages.push("Please enter either Office Hours or After Hours."); 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; isValid = false;
} else { } else if (officeFromFilled && officeToFilled) {
// Validate office hours if provided if (!this.validateTimeRangeForSubmission(this.editForm.officeFrom, this.editForm.officeTo, 'Office Hour')) {
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.");
isValid = false; 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; isValid = false;
} }
} }
// Validate after hours if provided const hasAnyValidTimeEntry = (!this.isOfficeHoursDisabled && officeFromFilled && officeToFilled) || (afterFromFilled && afterToFilled);
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;
if (afterTo.hours === 0 && afterTo.minutes === 0) { if (!hasAnyValidTimeEntry && errorMessages.length === 0) {
// Midnight case - FROM must be between 4:30 PM and 11:30 PM this.validationErrors.timeEntries = "Please enter either Office Hours or After Office Hours.";
const minAllowed = 16 * 60 + 30; // 4:30 PM errorMessages.push("Please enter either Office Hours or After Office Hours.");
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.");
isValid = false; 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) { if (!isValid) {
alert("Please correct the following issues:\n\n" + errorMessages.join("\n")); alert("Please correct the following issues:\n\n" + errorMessages.join("\n"));
@ -649,6 +648,33 @@
return isValid; 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() { validateAndUpdate() {
if (this.validateForm()) { if (this.validateForm()) {
this.updateRecord(); this.updateRecord();
@ -662,9 +688,9 @@
StationId: this.editForm.stationId || null, StationId: this.editForm.stationId || null,
OtDescription: this.editForm.otDescription || "", OtDescription: this.editForm.otDescription || "",
OtDays: this.editForm.otDays, OtDays: this.editForm.otDays,
OfficeFrom: this.editForm.officeFrom || null, OfficeFrom: this.isOfficeHoursDisabled ? null : (this.editForm.officeFrom || null),
OfficeTo: this.editForm.officeTo || null, OfficeTo: this.isOfficeHoursDisabled ? null : (this.editForm.officeTo || null),
OfficeBreak: this.editForm.officeBreak || 0, OfficeBreak: this.isOfficeHoursDisabled ? 0 : (this.editForm.officeBreak || 0),
AfterFrom: this.editForm.afterFrom || null, AfterFrom: this.editForm.afterFrom || null,
AfterTo: this.editForm.afterTo || null, AfterTo: this.editForm.afterTo || null,
AfterBreak: this.editForm.afterBreak || 0, AfterBreak: this.editForm.afterBreak || 0,

View File

@ -1,5 +1,4 @@
 @{
@{
ViewData["Title"] = "Register Overtime"; ViewData["Title"] = "Register Overtime";
Layout = "~/Views/Shared/_Layout.cshtml"; Layout = "~/Views/Shared/_Layout.cshtml";
} }
@ -22,21 +21,29 @@
<div class="col-4"> <div class="col-4">
<label for="officeFrom">From</label> <label for="officeFrom">From</label>
<input type="time" id="officeFrom" class="form-control" v-model="officeFrom" <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>
<div class="col-4"> <div class="col-4">
<label for="officeTo">To</label> <label for="officeTo">To</label>
<input type="time" id="officeTo" class="form-control" v-model="officeTo" <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>
<div class="col-4"> <div class="col-4">
<label for="officeBreak">Break Hours (Minutes)</label> <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"> <option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">
{{ opt.label }} {{ opt.label }}
</option> </option>
</select> </select>
</div> </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> </div>
<h6 class="fw-bold text-danger">AFTER OFFICE HOURS</h6> <h6 class="fw-bold text-danger">AFTER OFFICE HOURS</h6>
@ -59,6 +66,11 @@
</option> </option>
</select> </select>
</div> </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>
<div class="mb-3" v-if="showAirDropdown"> <div class="mb-3" v-if="showAirDropdown">
@ -166,6 +178,9 @@
isUserAdmin: false, isUserAdmin: false,
departmentName: "", departmentName: "",
areUserSettingsComplete: false, 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) => { breakOptions: Array.from({ length: 15 }, (_, i) => {
const totalMinutes = i * 30; const totalMinutes = i * 30;
const hours = Math.floor(totalMinutes / 60); const hours = Math.floor(totalMinutes / 60);
@ -201,7 +216,20 @@
}, },
requiresStation() { requiresStation() {
return this.showAirDropdown || this.showMarineDropdown; 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: { watch: {
airStationList: { airStationList: {
@ -255,6 +283,15 @@
if (selectElement.length && selectElement.val() !== newVal) { if (selectElement.length && selectElement.val() !== newVal) {
selectElement.val(newVal).trigger('change.select2'); 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() { async mounted() {
@ -442,8 +479,43 @@
}, },
handleDateChange() { handleDateChange() {
this.updateDayType(); this.updateDayType();
// Clear office hour fields if it's a weekday
if (this.isOfficeHoursDisabled) {
this.officeFrom = "";
this.officeTo = "";
this.officeBreak = 0;
}
this.calculateOTAndBreak(); 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() { async addOvertime() {
if (!this.areUserSettingsComplete) { if (!this.areUserSettingsComplete) {
alert("Cannot save overtime: Your essential user settings are incomplete. Please contact IT or HR."); alert("Cannot save overtime: Your essential user settings are incomplete. Please contact IT or HR.");
@ -485,38 +557,11 @@
return; return;
} }
// Validate time ranges according to the new rule: TO 00:00 only if FROM is 4:30 PM - 11:30 PM // Now call the shared validation method correctly using 'this'
const validateTimeRangeForSubmission = (fromTime, toTime, label) => { if (hasOfficeHours && !this.validateTimeRangeForSubmission(this.officeFrom, this.officeTo, 'Office Hour')) {
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')) {
return; return;
} }
if (hasAfterHours && !validateTimeRangeForSubmission(this.afterFrom, this.afterTo, 'After Office Hour')) { if (hasAfterHours && !this.validateTimeRangeForSubmission(this.afterFrom, this.afterTo, 'After Office Hour')) {
return; return;
} }
@ -545,8 +590,8 @@
}); });
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorMessage = await response.text();
throw new Error(`HTTP error! status: ${response.status} - ${errorText}`); throw new Error(errorMessage);
} }
const result = await response.json(); const result = await response.json();
@ -560,8 +605,8 @@
} }
} catch (error) { } catch (error) {
console.error("Error adding overtime:", error); // Display the specific error message from the backend.
alert(`Failed to save overtime. Error: ${error.message}`); alert(`${error.message}`);
} }
}, },
@ -623,7 +668,6 @@
} }
this.otDescription = ""; this.otDescription = "";
this.userFlexiHour = "";
this.detectedDayType = ""; this.detectedDayType = "";
this.totalOTHours = "0 hr 0 min"; this.totalOTHours = "0 hr 0 min";
this.totalBreakHours = "0 hr 0 min"; this.totalBreakHours = "0 hr 0 min";

View File

@ -1,32 +1,33 @@
using Azure.Core; using Azure.Core;
using DocumentFormat.OpenXml.InkML;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages;
using Mono.TextTemplating; using Mono.TextTemplating;
using Newtonsoft.Json; using Newtonsoft.Json;
using PSTW_CentralSystem.Areas.OTcalculate.Models; using PSTW_CentralSystem.Areas.OTcalculate.Models;
using PSTW_CentralSystem.Areas.OTcalculate.Services;
using PSTW_CentralSystem.Controllers.API; using PSTW_CentralSystem.Controllers.API;
using PSTW_CentralSystem.Controllers.API.Inventory; using PSTW_CentralSystem.Controllers.API.Inventory;
using PSTW_CentralSystem.DBContext; using PSTW_CentralSystem.DBContext;
using PSTW_CentralSystem.Models; 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.Fluent;
using QuestPDF.Helpers; using QuestPDF.Helpers;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
using PSTW_CentralSystem.Areas.OTcalculate.Services; using RestSharp.Extensions;
using Microsoft.AspNetCore.Hosting; using System;
using DocumentFormat.OpenXml.InkML; 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 PSTW_CentralSystem.Areas.OTcalculate.Models.OtRegisterModel;
using static System.Collections.Specialized.BitVector32;
namespace PSTW_CentralSystem.Controllers.API namespace PSTW_CentralSystem.Controllers.API
@ -863,7 +864,7 @@ namespace PSTW_CentralSystem.Controllers.API
#endregion #endregion
#region OtRegister #region Ot Register
[HttpGet("GetStationsByDepartment")] [HttpGet("GetStationsByDepartment")]
public async Task<IActionResult> GetStationsByDepartment([FromQuery] int? departmentId) public async Task<IActionResult> GetStationsByDepartment([FromQuery] int? departmentId)
{ {
@ -966,7 +967,7 @@ namespace PSTW_CentralSystem.Controllers.API
if (officeFrom.Value < minAllowedFromMidnightTo || officeFrom.Value > maxAllowedFromMidnightTo) 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) else if (officeTo <= officeFrom)
@ -986,7 +987,7 @@ namespace PSTW_CentralSystem.Controllers.API
if (afterFrom.Value < minAllowedFromMidnightTo || afterFrom.Value > maxAllowedFromMidnightTo) 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) else if (afterTo <= afterFrom)
@ -1000,6 +1001,68 @@ namespace PSTW_CentralSystem.Controllers.API
return BadRequest("Please enter either Office Hours or After Office Hours."); 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 var newRecord = new OtRegisterModel
{ {
OtDate = request.OtDate, OtDate = request.OtDate,
@ -1393,6 +1456,11 @@ namespace PSTW_CentralSystem.Controllers.API
return BadRequest(new { message = timeValidationError }); 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); var existing = _centralDbContext.Otregisters.FirstOrDefault(o => o.OvertimeId == model.OvertimeId);
if (existing == null) if (existing == null)
{ {
@ -1442,7 +1510,7 @@ namespace PSTW_CentralSystem.Controllers.API
if (officeFrom.Value < minAllowedFromMidnightTo || officeFrom.Value > maxAllowedFromMidnightTo) 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) else if (officeTo <= officeFrom)
@ -1462,7 +1530,7 @@ namespace PSTW_CentralSystem.Controllers.API
if (afterFrom.Value < minAllowedFromMidnightTo || afterFrom.Value > maxAllowedFromMidnightTo) 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) else if (afterTo <= afterFrom)
@ -1478,6 +1546,82 @@ namespace PSTW_CentralSystem.Controllers.API
return null; 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 #endregion
#region OT Status #region OT Status
@ -1993,12 +2137,21 @@ namespace PSTW_CentralSystem.Controllers.API
return NotFound(new { message = "Overtime record not found." }); 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); var otStatus = _centralDbContext.Otstatus.FirstOrDefault(s => s.StatusId == updatedRecordDto.StatusId);
if (otStatus == null) if (otStatus == null)
{ {
return NotFound("OT status not found."); return NotFound("OT status not found.");
} }
// ... (rest of the code for checking permissions and logging)
var currentLoggedInUserId = GetCurrentLoggedInUserId(); var currentLoggedInUserId = GetCurrentLoggedInUserId();
string approverRole = GetApproverRole(currentLoggedInUserId, otStatus.StatusId); string approverRole = GetApproverRole(currentLoggedInUserId, otStatus.StatusId);
@ -2029,6 +2182,7 @@ namespace PSTW_CentralSystem.Controllers.API
ReferenceLoopHandling = ReferenceLoopHandling.Ignore ReferenceLoopHandling = ReferenceLoopHandling.Ignore
}); });
// Update the existing record with the new data
existingRecord.OtDate = updatedRecordDto.OtDate; existingRecord.OtDate = updatedRecordDto.OtDate;
existingRecord.OfficeFrom = ParseTimeStringToTimeSpan(updatedRecordDto.OfficeFrom); existingRecord.OfficeFrom = ParseTimeStringToTimeSpan(updatedRecordDto.OfficeFrom);
existingRecord.OfficeTo = ParseTimeStringToTimeSpan(updatedRecordDto.OfficeTo); 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." }); 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}")] [HttpDelete("DeleteOvertimeInOtReview/{id}")]
public IActionResult DeleteOvertimeInOtReview(int id) public IActionResult DeleteOvertimeInOtReview(int id)
{ {