PSTW_CentralizeSystem/Areas/OTcalculate/Views/Overtime/OtRegister.cshtml
2025-04-29 17:19:25 +08:00

446 lines
21 KiB
Plaintext

@{
ViewData["Title"] = "Register Overtime";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<div id="app" class="container mt-4 d-flex justify-content-center">
<div class="card shadow-sm" style="width: 1100px;">
<div class="card-body">
<div class="row">
<div class="col-md-7">
<div class="mb-3">
<label class="form-label" for="dateInput">Date</label>
<input type="date" id="dateInput" class="form-control" v-model="selectedDate"
v-on:input="handleDateChange">
</div>
<h6 class="fw-bold">OFFICE HOURS</h6>
<div class="row mb-3">
<div class="col-4">
<label for="officeFrom">From</label>
<input type="time" id="officeFrom" class="form-control" v-model="officeFrom"
v-on:change ="officeFrom = roundToNearest30(officeFrom); calculateOTAndBreak()">
</div>
<div class="col-4">
<label for="officeTo">To</label>
<input type="time" id="officeTo" class="form-control" v-model="officeTo"
v-on:change ="officeTo = roundToNearest30(officeTo); calculateOTAndBreak()">
</div>
<div class="col-4">
<label for="officeBreak">Break Hours (Minutes)</label>
<select id="officeBreak" class="form-control" v-model.number="officeBreak" v-on:change ="calculateOTAndBreak">
<option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</div>
</div>
<h6 class="fw-bold text-danger">AFTER OFFICE HOURS</h6>
<div class="row mb-2">
<div class="col-4">
<label for="afterFrom">From</label>
<input type="time" id="afterFrom" class="form-control" v-model="afterFrom"
v-on:change ="afterFrom = roundToNearest30(afterFrom); calculateOTAndBreak()">
</div>
<div class="col-4">
<label for="afterTo">To</label>
<input type="time" id="afterTo" class="form-control" v-model="afterTo"
v-on:change ="afterTo = roundToNearest30(afterTo); calculateOTAndBreak()">
</div>
<div class="col-4">
<label for="afterBreak">Break Hours (Minutes)</label>
<select id="afterBreak" class="form-control" v-model.number="afterBreak" v-on:change ="calculateOTAndBreak">
<option v-for="opt in breakOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</div>
</div>
<div class="mb-3" v-if="isPSTWAIR">
<label for="airstationDropdown">Air Station</label>
<select id="airstationDropdown" class="form-control" v-model="selectedAirStation">
<option value="" disabled selected>Select Station</option>
<option v-for="station in airstationList" :key="station.stationId" :value="station.stationId">
{{ station.stationName || 'Unnamed Station' }}
</option>
</select>
<small class="text-danger">*Only for PSTW AIR</small>
</div>
<div class="mb-3">
<label for="otDescription">Work Brief Description</label>
<textarea id="otDescription" class="form-control"
v-model="otDescription"
v-on:input="limitCharCount"
placeholder="Describe the work done..."></textarea>
<small class="text-muted">
{{ charCount }} / 150 characters
</small>
</div>
</div>
<div class="col-md-5 mt-5">
<div class="mb-3 d-flex flex-column align-items-center">
<label for="detectedDayType">Day</label>
<input type="text" class="form-control text-center" v-model="detectedDayType" readonly
style="width: 200px;">
</div>
<div class="mb-3 d-flex flex-column align-items-center">
<label for="totalOTHours">Total OT Hours</label>
<input type="text" id="totalOTHours" class="form-control text-center" v-model="totalOTHours"
style="width: 200px;" readonly>
</div>
<div class="mb-3 d-flex flex-column align-items-center">
<label for="totalBreakHours">Total Break Hours</label>
<input type="text" id="totalBreakHours" class="form-control text-center" v-model="totalBreakHours"
style="width: 200px;" readonly>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-3">
<button class="btn btn-danger" v-on:click="clearForm">Clear</button>
<button class="btn btn-success ms-3" v-on:click="addOvertime">Save</button>
</div>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
const app = Vue.createApp({
data() {
return {
selectedDate: "",
officeFrom: "",
officeTo: "",
officeBreak: 0,
afterFrom: "",
afterTo: "",
afterBreak: 0,
selectedAirStation: "",
airstationList: [],
otDescription: "",
detectedDayType: "", // To display the auto-detected day type
totalOTHours: "0 hr 0 min",
totalBreakHours: "0 hr 0 min",
currentUser: null,
userId: null,
userState: null, // To store the user's state information
publicHolidays: [], // To store public holidays for the user's state and year
isPSTWAIR: false,
breakOptions: Array.from({ length: 15 }, (_, i) => {
const totalMinutes = i * 30;
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
let label = '';
if (hours > 0) label += `${hours} hour${hours > 1 ? 's' : ''}`;
if (minutes > 0) label += `${label ? ' ' : ''}${minutes} min`;
if (!label) label = '0 min';
return { label, value: totalMinutes };
}),
};
},
computed: {
charCount() {
return this.otDescription.length;
}
},
async mounted() {
await this.fetchUser();
if (this.isPSTWAIR) {
this.fetchStations();
}
},
methods: {
async fetchStations() {
try {
const response = await fetch(`/OvertimeAPI/GetStationsByDepartment`);
if (!response.ok) throw new Error("Failed to fetch stations");
this.airstationList = await response.json();
} catch (error) {
console.error("Error fetching stations:", error);
}
},
async fetchUser() {
try {
const response = await fetch(`/IdentityAPI/GetUserInformation/`, { method: 'POST' });
if (response.ok) {
const data = await response.json();
this.currentUser = data?.userInfo || null;
this.userId = this.currentUser?.id || null;
console.log("Fetched User:", this.currentUser);
console.log("Dept ID:", this.currentUser?.department?.departmentId);
console.log("Roles:", this.currentUser?.role);
const isSuperAdmin = this.currentUser?.role?.includes("SuperAdmin");
const isSystemAdmin = this.currentUser?.role?.includes("SystemAdmin");
const isDepartmentTwo = this.currentUser?.department?.departmentId === 2;
this.isPSTWAIR = isSuperAdmin || isSystemAdmin || isDepartmentTwo;
console.log("isPSTWAIR:", this.isPSTWAIR);
if (this.isPSTWAIR) {
this.fetchStations();
}
// Fetch user's state and public holidays after fetching user info
if (this.userId) {
await this.fetchUserStateAndHolidays();
}
} else {
console.error(`Failed to fetch user: ${response.statusText}`);
}
} catch (error) {
console.error("Error fetching user:", error);
}
},
async fetchUserStateAndHolidays() {
try {
const response = await fetch(`/OvertimeAPI/GetUserStateAndHolidays/${this.userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user state and holidays: ${response.statusText}`);
}
const data = await response.json();
this.userState = data.state;
this.publicHolidays = data.publicHolidays;
this.updateDayType(); // Initial detection after loading data
} catch (error) {
console.error("Error fetching user state and holidays:", error);
this.detectedDayType = "Weekday"; // Default if fetching fails
}
},
roundToNearest30(timeStr) {
if (!timeStr) return timeStr;
const [hours, minutes] = timeStr.split(':').map(Number);
const roundedMinutes = minutes < 15 ? 0 : minutes < 45 ? 30 : 0;
const adjustedHour = minutes < 45 ? hours : (hours + 1) % 24;
return `${adjustedHour.toString().padStart(2, '0')}:${roundedMinutes.toString().padStart(2, '0')}`;
},
limitCharCount(event) {
if (this.otDescription.length > 150) {
this.otDescription = this.otDescription.substring(0, 150);
event.preventDefault();
}
},
calculateOTAndBreak() {
let officeOT = this.calculateTimeDifference(this.officeFrom, this.officeTo, this.officeBreak);
let afterOT = this.calculateTimeDifference(this.afterFrom, this.afterTo, this.afterBreak);
let totalOTMinutes = officeOT.minutes + afterOT.minutes;
let totalOTHours = officeOT.hours + afterOT.hours + Math.floor(totalOTMinutes / 60);
totalOTMinutes = totalOTMinutes % 60;
this.totalOTHours = `${totalOTHours} hr ${totalOTMinutes} min`;
let totalBreakMinutes = (this.officeBreak || 0) + (this.afterBreak || 0);
let totalBreakHours = Math.floor(totalBreakMinutes / 60);
totalBreakMinutes = totalBreakMinutes % 60;
this.totalBreakHours = `${totalBreakHours} hr ${totalBreakMinutes} min`;
},
calculateTimeDifference(startTime, endTime, breakMinutes) {
if (!startTime || !endTime) {
return { hours: 0, minutes: 0 };
}
const start = this.parseTime(startTime);
const end = this.parseTime(endTime);
let diffMinutes = (end.hours * 60 + end.minutes) - (start.hours * 60 + start.minutes);
if (diffMinutes < 0) {
diffMinutes += 24 * 60;
}
diffMinutes -= breakMinutes || 0;
const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60;
return { hours, minutes };
},
parseTime(timeString) {
const [hours, minutes] = timeString.split(':').map(Number);
return { hours, minutes };
},
formatTime(timeString) {
if (!timeString) return null;
const [hours, minutes] = timeString.split(':');
return `${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}:00`; // HH:mm:ss format
},
handleDateChange() {
this.updateDayType();
this.calculateOTAndBreak();
},
async addOvertime() {
if (this.isPSTWAIR && !this.selectedAirStation) {
alert("Please fill in all required fields.");
return;
}
if (!this.userId) {
console.error("User ID is not set!");
alert("User information is missing. Please try again.");
return;
}
// Validate office hours
if (this.officeTo && !this.officeFrom) {
alert("Please fill in the 'From' time for office hours.");
return;
}
if (this.officeFrom && !this.officeTo) {
alert("Please fill in the 'To' time for office hours.");
return;
}
if (this.officeFrom && this.officeTo) {
if (this.officeTo <= this.officeFrom) {
alert("Invalid Office Hour Time: 'To' time must be later than 'From' time (same day only).");
return;
}
}
// Validate after hours
if (this.afterTo && !this.afterFrom) {
alert("Please fill in the 'From' time for after office hours.");
return;
}
if (this.afterFrom && !this.afterTo) {
alert("Please fill in the 'To' time for after office hours.");
return;
}
if (this.afterFrom && this.afterTo) {
if (this.afterTo <= this.afterFrom) {
alert("Invalid After Office Hour Time: 'To' time must be later than 'From' time (same day only).");
return;
}
}
// Require at least one: Office or After
if ((!this.officeFrom || !this.officeTo) && (!this.afterFrom || !this.afterTo)) {
alert("Please enter either Office Hours or After Office Hours.");
return;
}
const requestData = {
otDate: this.selectedDate,
officeFrom: this.officeFrom ? this.formatTime(this.officeFrom) : null,
officeTo: this.officeTo ? this.formatTime(this.officeTo) : null,
officeBreak: this.officeBreak || null, // Make this optional
afterFrom: this.afterFrom ? this.formatTime(this.afterFrom) : null, // Set to null if empty
afterTo: this.afterTo ? this.formatTime(this.afterTo) : null, // Set to null if empty
afterBreak: this.afterBreak || null, // Make this optional
stationId: this.isPSTWAIR ? parseInt(this.selectedAirStation) : null,
otDescription: this.otDescription.trim().split(/\s+/).slice(0, 50).join(' '),
otDays: this.detectedDayType, // Use the auto-detected day type
userId: this.userId
};
try {
const response = await fetch(`${window.location.origin}/OvertimeAPI/AddOvertime`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(requestData),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP error! status: ${response.status} - ${errorText}`);
}
const result = await response.json();
alert(result.message);
this.clearForm();
} catch (error) {
console.error("Error adding overtime:", error);
alert("Failed to save overtime. Please check the console for errors.");
}
},
updateDayType() {
if (!this.selectedDate || !this.userState) {
this.detectedDayType = "";
return;
}
// Force parsing at midnight local time
const selectedDateObj = new Date(this.selectedDate + "T00:00:00");
const dayOfWeek = selectedDateObj.getDay(); // 0 (Sunday) to 6 (Saturday)
const year = selectedDateObj.getFullYear();
const month = selectedDateObj.getMonth() + 1;
const day = selectedDateObj.getDate();
const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
// 1. Check if it's a Public Holiday
if (this.publicHolidays.some(holiday => holiday.date === formattedDate)) {
this.detectedDayType = "Public Holiday";
return;
}
// 2. Check if it's a Weekend according to user's weekendId
const weekendId = this.userState.weekendId;
const isWeekend = (() => {
if (weekendId === 1) {
// WeekendId 1: Friday and Saturday
return dayOfWeek === 5 || dayOfWeek === 6;
} else if (weekendId === 2) {
// WeekendId 2: Saturday and Sunday
return dayOfWeek === 6 || dayOfWeek === 0;
} else {
return dayOfWeek === 0; // Default Sunday
}
})();
if (isWeekend) {
this.detectedDayType = "Weekend";
return;
}
// 3. Otherwise, it's a normal Weekday
this.detectedDayType = "Weekday";
},
clearForm() {
this.selectedDate = "";
this.officeFrom = "";
this.officeTo = "";
this.officeBreak = 0;
this.afterFrom = "";
this.afterTo = "";
this.afterBreak = 0;
this.selectedAirStation = "";
this.otDescription = "";
this.detectedDayType = "";
this.totalOTHours = "0 hr 0 min";
this.totalBreakHours = "0 hr 0 min";
},
}
});
app.mount("#app");
</script>
}