PSTW_CentralizeSystem/Areas/OTcalculate/Views/HrDashboard/HrUserSetting.cshtml
2025-06-09 14:41:52 +08:00

797 lines
39 KiB
Plaintext

@{
ViewBag.Title = "User Settings";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<style>
#ApprovalFlowTable {
background-color: white;
}
#ApprovalFlowTable th,
#ApprovalFlowTable td {
background-color: white !important; /* Ensure no transparency from Bootstrap */
color: #323;
}
</style>
<div class="container">
<div class="row justify-content-center">
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="Rate">
<div class="box bg-success text-center">
<h1 class="font-light text-white"><i class="mdi mdi-currency-usd"></i></h1>
<h6 class="text-white">Rate</h6>
</div>
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="Calendar">
<div class="box bg-purple text-center">
<h1 class="font-light text-white"><i class="mdi mdi-calendar"></i></h1>
<h6 class="text-white">Calendar</h6>
</div>
</a>
</div>
</div>
<div class="col-6 col-md-6 col-lg-3">
<div class="card card-hover">
<a asp-area="OTcalculate" asp-controller="HrDashboard" asp-action="HrUserSetting">
<div class="box bg-megna text-center">
<h1 class="font-light text-white"><i class="mdi mdi-account-settings"></i></h1>
<h6 class="text-white">User Setting</h6>
</div>
</a>
</div>
</div>
</div>
</div>
<div class="container mt-4" id="app">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" :class="{ 'bg-purple text-white': activeTab === 'flexi', 'bg-light text-dark': activeTab !== 'flexi' }"
style="border: 1px solid #ddd;" v-on:click="changeTab('flexi')">
Flexi Hour Settings
</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{ 'bg-purple text-white': activeTab === 'state', 'bg-light text-dark': activeTab !== 'state' }"
style="border: 1px solid #ddd;" v-on:click="changeTab('state')">
Region & Flow Update
</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{ 'bg-purple text-white': activeTab === 'approval', 'bg-light text-dark': activeTab !== 'approval' }"
style="border: 1px solid #ddd;" v-on:click="changeTab('approval')">
Approval Flow
</a>
</li>
</ul>
<div class="tab-content mt-3">
<div v-if="activeTab === 'flexi'" class="card shadow-sm">
<div class="card m-2">
<form v-on:submit.prevent="updateFlexiHours" data-aos="fade-right">
<div class="d-flex justify-content-center align-items-center mt-3">
<div class="card-body d-flex justify-content-center align-items-center gap-3">
<label for="flexiHour" class="mb-0">Flexi Hour</label>
<select id="flexiHour" class="form-select" v-model="selectedFlexiHourId" style="max-width: 150px;">
<option disabled value="">--Select--</option>
<option v-for="option in flexiHours" :value="option.flexiHourId">{{ option.flexiHour }}</option>
</select>
<button type="button" class="btn btn-danger" v-on:click="clearForm">Clear</button>
<button type="submit" class="btn btn-success">Update Flexi Hour</button>
</div>
</div>
<div class="card-body">
<table id="userDatatable" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Full Name</th>
<th>Department</th>
<th>Current Flexi Hour</th>
<th class="text-center">Select</th>
</tr>
</thead>
</table>
</div>
<div class="d-flex justify-content-center gap-2 my-3">
<button type="button" class="btn btn-danger" v-on:click="clearForm">Clear</button>
<button type="submit" class="btn btn-success">Update Flexi Hour</button>
</div>
</form>
</div>
</div>
<div v-if="activeTab === 'state'" class="card shadow-sm">
<div class="card m-3">
<div class="d-flex justify-content-center align-items-center gap-4 flex-wrap mt-4">
<div class="d-flex align-items-center gap-2 flex-nowrap">
<label class="form-label mb-0">Select State:</label>
<select class="form-select form-select-sm" v-model="selectedStateAll" style="width: 150px; font-size: 0.900rem;">
<option disabled value="">--Select--</option>
<option v-for="state in stateList" :value="state.stateId">{{ state.stateName }}</option>
</select>
</div>
<div class="d-flex align-items-center gap-2 flex-nowrap">
<label class="form-label mb-0">Select Approval Flow:</label>
<select class="form-select form-select-sm" v-model="selectedApprovalFlowId" style="width: 150px; font-size: 0.900rem;">
<option disabled value="">--Select--</option>
<option v-for="flow in approvalFlowList" :key="flow.approvalFlowId" :value="flow.approvalFlowId">
{{ flow.approvalName }}
</option>
</select>
</div>
<div class="d-flex align-items-center gap-2 flex-nowrap">
<button class="btn btn-danger" v-on:click="clearAllSelectionsStateFlow" style="font-size: 0.900rem; margin-left: 10px;">
Clear
</button>
<button class="btn btn-primary" v-on:click="handleCombinedUpdate" style="font-size: 0.900rem;">
Update State & Flow
</button>
</div>
</div>
<div class="card-body">
<table id="stateUpdateTable" class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Full Name</th>
<th>Department</th>
<th>Current State</th>
<th>Approval Flow</th>
<th class="text-center">Select</th>
</tr>
</thead>
</table>
</div>
<div class="d-flex justify-content-center gap-2 my-3">
<button class="btn btn-danger" v-on:click="clearAllSelectionsStateFlow" style="font-size: 0.900rem; margin-left: 10px;">
Clear
</button>
<button class="btn btn-primary" v-on:click="handleCombinedUpdate" style="font-size: 0.900rem;">
Update State & Flow
</button>
</div>
</div>
</div>
<div v-if="activeTab === 'approval'" class="card shadow-sm">
<div class="card m-3">
<div class="card-body">
<h5 class="card-title text-center">Create Approval Flow</h5>
<div class="d-flex justify-content-center">
<form v-on:submit.prevent="submitApprovalFlow" class="w-75">
<div class="mb-3">
<label for="approvalName" class="form-label">Approval Name</label>
<input type="text" id="approvalName" class="form-control" v-model="approvalFlow.approvalName" placeholder="Enter approval name" required />
</div>
<div class="mb-3">
<label class="form-label">Approval Flow:</label>
</div>
<div class="mb-3">
<label for="hou" class="form-label ms-3">Head of Unit (HoU) / Executive</label>
<select id="hou" class="form-select" v-model="approvalFlow.hou">
<option disabled value="">--Select--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="mb-3">
<label for="hod" class="form-label ms-3">Head of Department (HoD)</label>
<select id="hod" class="form-select" v-model="approvalFlow.hod">
<option disabled value="">--Select--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="mb-3">
<label for="manager" class="form-label ms-3">Manager</label>
<select id="manager" class="form-select" v-model="approvalFlow.manager">
<option disabled value="">--Select--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="mb-3">
<label for="hr" class="form-label ms-3">HR</label>
<select id="hr" class="form-select" v-model="approvalFlow.hr" required>
<option disabled value="">--Select--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="d-flex justify-content-end" style="gap: 10px;">
<button type="button" class="btn btn-danger" v-on:click="clearFormApproval">Clear</button>
<button type="submit" class="btn btn-success">Submit Approval Flow</button>
</div>
</form>
</div>
<div class="d-flex justify-content-center">
<div class="table-container table-responsive w-100" style="margin-top: 20px; max-width: 900px;">
<table id="ApprovalFlowTable" class="table table-bordered table-striped align-middle">
<thead class="table-light">
<tr>
<th style="width: 50px;">No.</th>
<th>Approval Name</th>
<th style="width: 120px;">Action</th>
</tr>
</thead>
<tbody>
<tr v-if="approvalFlowList.length === 0">
<td colspan="3" class="text-center text-muted">No approval flows found.</td>
</tr>
<tr v-for="(flow, index) in approvalFlowList" :key="flow.approvalFlowId">
<td>{{ index + 1 }}</td>
<td>{{ flow.approvalName }}</td>
<td>
<button class="btn btn-sm btn-primary me-2" v-on:click="openEditModal(flow)" title="Edit">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" v-on:click="deleteApprovalFlow(flow.approvalFlowId)" title="Delete">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editApprovalModal" tabindex="-1" aria-labelledby="editApprovalModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form v-on:submit.prevent="submitEditApprovalFlow">
<div class="modal-header">
<h5 class="modal-title" id="editApprovalModalLabel">Edit Approval Flow</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Approval Name</label>
<input type="text" class="form-control" v-model="editFlow.approvalName" required />
</div>
<div class="mb-3">
<label class="form-label">Head of Unit (HoU)</label>
<select class="form-select" v-model="editFlow.hou">
<option :value="null">--None--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Head of Department (HoD)</label>
<select class="form-select" v-model="editFlow.hod">
<option :value="null">--None--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Manager</label>
<select class="form-select" v-model="editFlow.manager">
<option :value="null">--None--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">HR</label>
<select class="form-select" v-model="editFlow.hr" required>
<option disabled value="">--Select--</option>
<option v-for="user in allUsers" :value="user.id">{{ user.fullName }}</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-success">Save Changes</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
<script src="https://unpkg.com/vue@3.2.37/dist/vue.global.js"></script>
<script>
window.onload = function () {
const app = Vue.createApp({
data() {
return {
activeTab: 'flexi',
flexiHours: [],
selectedFlexiHourId: '',
selectedUsers: [],
userList: [],
stateList: [],
selectedStateAll: '',
selectedUsersState: [],
stateUserList: [],
userDatatable: null,
stateDatatable: null,
approvalFlows: [],
approvalFlowList: [],
selectedApprovalFlowId: '',
approvalFlow: {
approvalName: '',
hou: '',
hod: '',
manager: '',
hr: ''
},
editFlow: {
approvalId: '',
approvalName: '',
hou: '',
hod: '',
manager: '',
hr: ''
},
allUsers: []
};
},
mounted() {
console.log("Vue App Mounted Successfully");
this.fetchFlexiHours();
this.fetchStates();
this.changeTab('flexi');
this.fetchAllUsers();
this.fetchUsersState();
this.fetchApprovalFlows();
},
methods: {
changeTab(tab) {
this.activeTab = tab;
if (tab === 'flexi') {
this.clearForm();
if (!this.userList.length) {
this.fetchUsers().then(() => this.initiateTable());
} else {
this.initiateTable();
}
} else if (tab === 'state') {
this.clearAllSelectionsStateFlow();
if (!this.stateUserList.length) {
this.fetchUsersState().then(() => {
this.initiateStateTable();
this.fetchApprovalFlows(); // Ensure approval flows are fetched on tab change
});
} else {
this.initiateStateTable();
}
}
},
async updateUserSettings(apiUrl, selectedUsers, selectedValue, successMessage, clearCallback, fetchCallback, valueKey) {
if (!selectedUsers.length || !selectedValue) {
alert("Please select at least one user and a flexi hour.");
return;
}
const payload = selectedUsers.map(userId => ({ UserId: userId, [valueKey]: selectedValue }));
try {
const response = await fetch(apiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (response.ok) {
alert(successMessage);
clearCallback(); // Clears form selections
await fetchCallback(); // Fetches the updated data
} else {
const errorData = await response.json();
alert(errorData.message || "Failed to update. Please try again.");
}
} catch (error) {
console.error("Error updating:", error);
}
},
async fetchFlexiHours() {
try {
const response = await fetch("/OvertimeAPI/GetFlexiHours");
this.flexiHours = await response.json();
console.log("Fetched flexi hours:", this.flexiHours);
} catch (error) {
console.error("Error fetching Flexi Hours:", error);
}
},
async fetchUsers() {
try {
const response = await fetch("/OvertimeAPI/GetUserFlexiHours", { method: "GET" });
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
this.userList = data.filter(e => e.fullName !== "MAAdmin" && e.fullName !== "SysAdmin");
console.log("Fetched User data", this.userList);
// Reinitialize DataTable with updated data
this.initiateTable();
} catch (error) {
console.error("Error fetching users:", error);
}
},
async fetchStates() {
try {
const response = await fetch("/OvertimeAPI/GetStatesName");
this.stateList = await response.json();
} catch (error) {
console.error("Error fetching States:", error);
}
},
async fetchUsersState() {
try {
const response = await fetch("/OvertimeAPI/GetUserStates", { method: "GET" });
const data = await response.json();
this.stateUserList = data.filter(e => e.fullName !== "MAAdmin" && e.fullName !== "SysAdmin");
console.log("Fetched state users", this.stateUserList);
// Reinitialize the state table to reflect updated data
this.initiateStateTable();
} catch (error) {
console.error("Error fetching users for State:", error);
}
},
initiateTable() {
if (this.userDatatable) {
this.userDatatable.destroy();
this.userDatatable = null;
}
const self = this;
this.$nextTick(() => {
self.userDatatable = $('#userDatatable').DataTable({
data: self.userList,
columns: [
{ data: "fullName" },
{ data: "departmentName" },
{ data: "flexiHour", defaultContent: "N/A" },
{
data: "userId",
className: "text-center",
render: data => `<input type='checkbox' class='user-checkbox' value='${data}'>`
}
],
destroy: true,
responsive: true
});
$('#userDatatable tbody').off('change').on('change', '.user-checkbox', (e) => {
const userId = $(e.target).val();
if (e.target.checked) {
self.selectedUsers.push(userId);
} else {
self.selectedUsers= self.selectedUsers.filter(id => id !== userId);
}
});
});
},
initiateStateTable() {
if (this.stateDatatable) {
this.stateDatatable.destroy();
this.stateDatatable = null;
}
const self = this;
this.$nextTick(() => {
self.stateDatatable = $('#stateUpdateTable').DataTable({
data: self.stateUserList,
columns: [
{ data: "fullName" },
{ data: "departmentName" },
{ data: "state", defaultContent: "N/A" },
{ data: "approvalName", defaultContent: "N/A" },
{
data: "id",
className: "text-center",
render: data => `<input type='checkbox' class='state-checkbox' value='${data || ""}'>`
}
],
destroy: true,
responsive: true
});
$('#stateUpdateTable tbody').off('change').on('change', '.state-checkbox', (e) => {
const userId = $(e.target).val();
if (e.target.checked) {
self.selectedUsersState.push(userId);
} else {
self.selectedUsersState = self.selectedUsersState.filter(id => id !== userId);
}
});
});
},
async handleCombinedUpdate() {
const hasUsers = this.selectedUsersState.length > 0;
const hasState = this.selectedStateAll !== '';
const hasFlow = this.selectedApprovalFlowId !== '';
if (!hasUsers && !hasState && !hasFlow) {
alert("Please choose users, a state, or an approval flow to proceed.");
this.clearAllSelectionsStateFlow();
return;
}
if (!hasUsers) {
alert("Please select at least one user.");
return;
}
if (!hasState && !hasFlow) {
alert("Please select either a State or Approval Flow.");
return;
}
if (hasState) {
await this.updateUserSettings(
"/OvertimeAPI/UpdateUserStates",
this.selectedUsersState,
this.selectedStateAll,
"State updated successfully!",
() => {}, // Don't clear yet
() => {}, // Don't fetch yet
"StateId"
);
}
if (hasFlow) {
const payload = this.selectedUsersState.map(userId => ({
UserId: userId,
ApprovalFlowId: this.selectedApprovalFlowId
}));
try {
const response = await fetch("/OvertimeAPI/UpdateUserApprovalFlow", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
alert(errorData.message || "Failed to update Approval Flow.");
return;
}
alert("Approval Flow updated successfully!");
} catch (error) {
console.error("Error updating approval flow:", error);
alert("An unexpected error occurred.");
}
}
// After updates, refresh and clear
await this.fetchUsersState();
this.initiateStateTable();
this.clearAllSelectionsStateFlow();
},
async updateFlexiHours() {
await this.updateUserSettings(
"/OvertimeAPI/UpdateUserFlexiHours",
this.selectedUsers,
this.selectedFlexiHourId,
"Flexi Hours updated successfully!",
this.clearForm,
this.fetchUsers,
"FlexiHourId"
);
},
async fetchAllUsers() {
try {
const response = await fetch("/OvertimeAPI/GetAllUsers");
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Fetch failed: ${response.status} ${errorText}`);
}
const data = await response.json();
console.log("Fetched users:", data);
this.allUsers = data.filter(e => e.fullName !== "MAAdmin" && e.fullName !== "SysAdmin");
} catch (error) {
console.error("Error fetching all users:", error);
}
},
async fetchApprovalFlows() {
try {
const response = await fetch('/OvertimeAPI/GetApprovalFlowList');
const data = await response.json();
this.approvalFlowList = data; // Update the data correctly
console.log('Fetched approval flows:', this.approvalFlowList); // Verify data
} catch (error) {
console.error('Error fetching approval flows:', error);
}
},
async submitApprovalFlow() {
try {
const payload = { ...this.approvalFlow };
if (payload.hou === "") payload.hou = null;
if (payload.hod === "") payload.hod = null;
if (payload.manager === "") payload.manager = null;
const response = await fetch('/OvertimeAPI/CreateApprovalFlow', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
alert('Approval flow created successfully!');
this.clearFormApproval();
await this.fetchApprovalFlows();
} else {
const errorData = await response.json();
alert(errorData.message || 'Failed to create approval flow. Please try again.');
}
} catch (error) {
console.error('Error submitting approval flow:', error);
alert('An unexpected error occurred.');
}
},
clearFormApproval() {
this.approvalFlow = {
approvalName: '',
hou: '',
hod: '',
manager: '',
hr: ''
};
},
async deleteApprovalFlow(approvalId) {
if (!approvalId) {
// This handles the "undefined" ID case more gracefully
console.error("No approval ID provided for deletion.");
alert("An error occurred: No approval flow selected for deletion.");
return;
}
if (!confirm("Are you sure you want to delete this approval flow?")) return;
try {
const response = await fetch(`/OvertimeAPI/DeleteApprovalFlow/${approvalId}`, {
method: "DELETE"
});
if (response.ok) {
alert("Approval flow deleted successfully.");
await this.fetchApprovalFlows();
} else {
// Parse the error response from the server
const errorData = await response.json();
const errorMessage = errorData.message || "Failed to delete approval flow.";
// Display the alert, but DON'T re-throw or console.error if it's a specific bad request
// Only log to console for unexpected server errors (e.g., 500 status codes)
if (response.status === 400) { // Check for Bad Request specifically
alert(`Error: ${errorMessage}`);
} else {
// For other errors (e.g., 500 Internal Server Error), still log to console
console.error("Error deleting flow:", errorMessage);
alert(`Error: ${errorMessage}`);
}
}
} catch (error) {
// This catch block handles network errors or errors that prevent a valid response
console.error("An unexpected error occurred during deletion:", error);
alert(`An unexpected error occurred: ${error.message || "Please try again."}`);
}
},
clearForm() {
this.selectedFlexiHourId = '';
this.selectedUsers = [];
if (this.userDatatable) {
this.userDatatable.rows().every(function () {
$(this.node()).find('input[type="checkbox"]').prop('checked', false);
});
}
},
clearAllSelectionsStateFlow() {
this.selectedStateAll = '';
this.selectedApprovalFlowId = '';
this.selectedUsersState = [];
if (this.stateDatatable) {
this.stateDatatable.rows().every(function () {
$(this.node()).find('input[type="checkbox"]').prop('checked', false);
});
}
},
openEditModal(flow) {
// Set the editFlow to the selected flow
this.editFlow = { ...flow };
// Optionally, log the data to ensure it's correct
console.log(this.editFlow);
// Show the modal
this.fetchAllUsers().then(() => {
const modal = new bootstrap.Modal(document.getElementById('editApprovalModal'));
modal.show();
});
},
async submitEditApprovalFlow() {
try {
const response = await fetch('/OvertimeAPI/EditApprovalFlow', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.editFlow)
});
if (response.ok) {
alert('Approval flow updated successfully!');
await this.fetchApprovalFlows();
const modalElement = document.getElementById('editApprovalModal');
const modal = bootstrap.Modal.getInstance(modalElement);
modal.hide();
} else {
const error = await response.json();
alert(error.message || 'Failed to update approval flow.');
}
} catch (error) {
console.error('Error updating flow:', error);
alert('Unexpected error occurred.');
}
}
}
});
app.mount('#app');
};
</script>
}