From e3bf644a0b6ec4e5a20117f9ed32a26533bc7945 Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 6 Aug 2025 11:34:32 +0800 Subject: [PATCH] update OT --- .../Views/ApprovalDashboard/OtReview.cshtml | 404 +++++++++++------- .../Views/Overtime/EditOvertime.cshtml | 184 ++++---- .../Views/Overtime/OtRegister.cshtml | 148 ++++--- Controllers/API/OvertimeAPI.cs | 280 ++++++++++-- 4 files changed, 711 insertions(+), 305 deletions(-) diff --git a/Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml b/Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml index a902886..dbb85c9 100644 --- a/Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml +++ b/Areas/OTcalculate/Views/ApprovalDashboard/OtReview.cshtml @@ -192,7 +192,7 @@ TOTAL TOTAL - + {{ formatBreakToHourMinute(totals.officeBreak) }} @@ -247,31 +247,35 @@
- - + +
-
OFFICE HOURS
+ Valid Office Hours ranges: {{ flexiTimes.start }} to {{ flexiTimes.end }}
- - From + + v-on:change="roundMinutes('officeFrom'); validateTimeFields()" + :disabled="isModalOfficeHoursDisabled">
- - To + + v-on:change="roundMinutes('officeTo'); validateTimeFields()" + :disabled="isModalOfficeHoursDisabled">
- +
- -
@@ -279,23 +283,24 @@
AFTER OFFICE HOURS
+ Valid After Office Hours ranges: 00:00 - {{ flexiTimes.start }} or {{ flexiTimes.end }} - 00:00
- - From +
- - To +
- +
-
- -
- -
- - + + + {{ descriptionError }} {{ currentEditRecord.otDescription ? currentEditRecord.otDescription.length : 0 }} / 150 characters
- - + +
- +
- - + +
- - + +
@@ -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(); @@ -927,27 +938,10 @@ } }, - async submitEdit() { + 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,51 +1332,42 @@ 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() { - await this.fetchOtRecords(); - await this.fetchStations(); - }, - mounted() { - if (this.showAirStationDropdown) { - this.initSelect2('#airstationDropdown'); - } - if (this.showMarineStationDropdown) { - this.initSelect2('#marinestationDropdown'); - } + }, + async created() { + await this.fetchOtRecords(); + await this.fetchStations(); + }, + mounted() { + if (this.showAirStationDropdown) { + this.initSelect2('#modalAirstationDropdown'); + } + if (this.showMarineStationDropdown) { + 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'); - } - if (this.showMarineStationDropdown) { - this.initSelect2('#marinestationDropdown'); - $('#marinestationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2'); - } - }); + var editModalElement = document.getElementById('editOtModal'); + if (editModalElement) { + editModalElement.addEventListener('shown.bs.modal', () => { + if (this.showAirStationDropdown) { + this.initSelect2('#modalAirstationDropdown'); + $('#modalAirstationDropdown').val(this.currentEditRecord.stationId).trigger('change.select2'); + } + if (this.showMarineStationDropdown) { + 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(); + }); + } } - } - }); - reviewApp.mount("#reviewApp"); + }); + reviewApp.mount("#reviewApp"); } \ No newline at end of file diff --git a/Areas/OTcalculate/Views/Overtime/EditOvertime.cshtml b/Areas/OTcalculate/Views/Overtime/EditOvertime.cshtml index e72c3be..2b79f25 100644 --- a/Areas/OTcalculate/Views/Overtime/EditOvertime.cshtml +++ b/Areas/OTcalculate/Views/Overtime/EditOvertime.cshtml @@ -12,35 +12,41 @@
- + {{ validationErrors.otDate }}
OFFICE HOURS
-
+
- +
- +
- -
+ Valid Office Hours ranges: {{ flexiTimes.start }} to {{ flexiTimes.end }} +
AFTER OFFICE HOURS
-
+
@@ -61,6 +67,7 @@
+ Valid After Office Hours ranges: 00:00 - {{ flexiTimes.start }} or {{ flexiTimes.end }} - 00:00
@@ -170,8 +177,8 @@ return { label, value: totalMinutes }; }), previousPage: document.referrer, - returnMonth: null, - returnYear: null, + returnMonth: null, + returnYear: null, validationErrors: { otDate: "", stationId: "", @@ -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') { @@ -420,18 +446,18 @@ const start = this.parseTime(fromTime); const end = this.parseTime(toTime); - const minAllowedFromMinutes = 16 * 60 + 30; + 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 (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 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) { @@ -463,7 +489,7 @@ const roundedMinutes = remainder < 15 ? totalMinutes - remainder : totalMinutes + (30 - remainder); - const adjustedHour = Math.floor(roundedMinutes / 60) % 24; + const adjustedHour = Math.floor(roundedMinutes / 60) % 24; const adjustedMinute = roundedMinutes % 60; return `${adjustedHour.toString().padStart(2, '0')}:${adjustedMinute.toString().padStart(2, '0')}`; @@ -482,7 +508,7 @@ if (end.hours === 0 && end.minutes === 0 && (start.hours > 0 || start.minutes > 0)) { diffMinutes = (24 * 60) - (start.hours * 60 + start.minutes); } else if (end.hours * 60 + end.minutes <= start.hours * 60 + start.minutes) { - + return { hours: 0, minutes: 0 }; } else { @@ -491,7 +517,7 @@ } diffMinutes -= breakMinutes || 0; - if (diffMinutes < 0) diffMinutes = 0; + if (diffMinutes < 0) diffMinutes = 0; const hours = Math.floor(diffMinutes / 60); const minutes = diffMinutes % 60; @@ -551,8 +577,7 @@ this.editForm.otDays = "Weekday"; }, - validateForm() { - + validateForm() { this.validationErrors = { otDate: "", stationId: "", @@ -563,83 +588,57 @@ 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 if (officeFromFilled && officeToFilled) { + if (!this.validateTimeRangeForSubmission(this.editForm.officeFrom, this.editForm.officeTo, 'Office Hour')) { + isValid = false; + } + } + } + + 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 { - // 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."); - 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."); - 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."); - 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 (!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; } if (!isValid) { @@ -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, diff --git a/Areas/OTcalculate/Views/Overtime/OtRegister.cshtml b/Areas/OTcalculate/Views/Overtime/OtRegister.cshtml index 713acbc..24eb502 100644 --- a/Areas/OTcalculate/Views/Overtime/OtRegister.cshtml +++ b/Areas/OTcalculate/Views/Overtime/OtRegister.cshtml @@ -1,5 +1,4 @@ - -@{ +@{ ViewData["Title"] = "Register Overtime"; Layout = "~/Views/Shared/_Layout.cshtml"; } @@ -22,21 +21,29 @@
+ v-on:change="officeFrom = roundToNearest30(officeFrom); calculateOTAndBreak()" + :disabled="isOfficeHoursDisabled">
+ v-on:change="officeTo = roundToNearest30(officeTo); calculateOTAndBreak()" + :disabled="isOfficeHoursDisabled">
-
+
+ + Valid Office Hours ranges: {{ flexiTimes.start }} to {{ flexiTimes.end }} + +
AFTER OFFICE HOURS
@@ -59,6 +66,11 @@
+
+ + Valid After Office Hours ranges: 00:00 - {{ flexiTimes.start }} or {{ flexiTimes.end }} - 00:00 + +
@@ -163,9 +175,12 @@ userState: null, publicHolidays: [], userDepartmentId: null, // The department ID from the current user's profile - isUserAdmin: false, - departmentName: "", + 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); @@ -197,11 +212,24 @@ if (this.selectedMarineStation) { return parseInt(this.selectedMarineStation); } - return null; + return null; }, 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() { @@ -299,9 +336,9 @@ const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin"); const isSystemAdmin = this.currentUser?.role?.includes("SystemAdmin"); - this.userDepartmentId = this.currentUser?.department?.departmentId; + this.userDepartmentId = this.currentUser?.department?.departmentId; - this.isUserAdmin = isSuperAdmin || isSystemAdmin; + this.isUserAdmin = isSuperAdmin || isSystemAdmin; if (!this.isUserAdmin) { if (this.userDepartmentId === 2) { @@ -375,7 +412,7 @@ const roundedMinutes = remainder < 15 ? totalMinutes - remainder : totalMinutes + (30 - remainder); - const adjustedHour = Math.floor(roundedMinutes / 60) % 24; + const adjustedHour = Math.floor(roundedMinutes / 60) % 24; const adjustedMinute = roundedMinutes % 60; return `${adjustedHour.toString().padStart(2, '0')}:${adjustedMinute.toString().padStart(2, '0')}`; @@ -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; } @@ -528,7 +573,7 @@ afterFrom: this.afterFrom ? this.formatTime(this.afterFrom) : null, afterTo: this.afterTo ? this.formatTime(this.afterTo) : null, afterBreak: this.afterBreak || null, - stationId: stationIdToSubmit, + stationId: stationIdToSubmit, otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '), otDays: this.detectedDayType, userId: this.userId, @@ -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}`); } }, @@ -610,20 +655,19 @@ this.afterFrom = ""; this.afterTo = ""; this.afterBreak = 0; - this.selectedAirStation = ""; - this.selectedMarineStation = ""; + this.selectedAirStation = ""; + this.selectedMarineStation = ""; const airSelect = $('#airStationDropdown'); if (airSelect.length && airSelect.data('select2')) { - airSelect.val('').trigger('change.select2'); + airSelect.val('').trigger('change.select2'); } const marineSelect = $('#marineStationDropdown'); if (marineSelect.length && marineSelect.data('select2')) { - marineSelect.val('').trigger('change.select2'); + marineSelect.val('').trigger('change.select2'); } this.otDescription = ""; - this.userFlexiHour = ""; this.detectedDayType = ""; this.totalOTHours = "0 hr 0 min"; this.totalBreakHours = "0 hr 0 min"; diff --git a/Controllers/API/OvertimeAPI.cs b/Controllers/API/OvertimeAPI.cs index 12c7166..a1c5899 100644 --- a/Controllers/API/OvertimeAPI.cs +++ b/Controllers/API/OvertimeAPI.cs @@ -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 GetStationsByDepartment([FromQuery] int? departmentId) { @@ -924,11 +925,11 @@ namespace PSTW_CentralSystem.Controllers.API int? userDepartmentId = userWithDepartment?.Department?.DepartmentId; bool stationRequired = false; - if (userDepartmentId == 2 || userDepartmentId == 3) + if (userDepartmentId == 2 || userDepartmentId == 3) { stationRequired = true; } - else if (isSuperAdmin || isSystemAdmin) + else if (isSuperAdmin || isSystemAdmin) { stationRequired = true; } @@ -953,11 +954,11 @@ namespace PSTW_CentralSystem.Controllers.API } TimeSpan minAllowedFromMidnightTo = new TimeSpan(16, 30, 0); - TimeSpan maxAllowedFromMidnightTo = new TimeSpan(23, 30, 0); + TimeSpan maxAllowedFromMidnightTo = new TimeSpan(23, 30, 0); if (officeFrom.HasValue && officeTo.HasValue) { - if (officeTo == TimeSpan.Zero) + if (officeTo == TimeSpan.Zero) { if (officeFrom == TimeSpan.Zero) { @@ -966,10 +967,10 @@ 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) + else if (officeTo <= officeFrom) { return BadRequest("Invalid Office Hour Time: 'To' time must be later than 'From' time (same day only)."); } @@ -977,7 +978,7 @@ namespace PSTW_CentralSystem.Controllers.API if (afterFrom.HasValue && afterTo.HasValue) { - if (afterTo == TimeSpan.Zero) + if (afterTo == TimeSpan.Zero) { if (afterFrom == TimeSpan.Zero) { @@ -986,10 +987,10 @@ 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) + else if (afterTo <= afterFrom) { return BadRequest("Invalid After Office Hour Time: 'To' time must be later than 'From' time (same day only)."); } @@ -1000,15 +1001,77 @@ 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 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, OfficeFrom = officeFrom, OfficeTo = officeTo, - OfficeBreak = request.OfficeBreak, + OfficeBreak = request.OfficeBreak, AfterFrom = afterFrom, AfterTo = afterTo, - AfterBreak = request.AfterBreak, + AfterBreak = request.AfterBreak, StationId = request.StationId, OtDescription = request.OtDescription, OtDays = request.OtDays, @@ -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) {