This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
const db = require("../models");
|
||||
const Employee = db.employee;
|
||||
const ExtUserPermission = db.ext_usr_perm;
|
||||
const config = require("../config/auth.config");
|
||||
const axios = require("axios");
|
||||
|
||||
var jwt = require("jsonwebtoken");
|
||||
var bcrypt = require("bcryptjs");
|
||||
const { splitSite } = require("../middlewares");
|
||||
const HR_AUTH_LOGIN_ENDPOINT = "https://ws-hr.mayosolution.com/api/auth/login";
|
||||
const SYSTEM_ACCESS_PERMISSION = "System Access";
|
||||
|
||||
const ALL_PERMISSIONS = [
|
||||
'Dashboard',
|
||||
@@ -91,62 +95,171 @@ const isEmployeeActive = (employeeDoc) => {
|
||||
return `${rawStatus}`.trim().toLowerCase() === "active";
|
||||
};
|
||||
|
||||
// Create and Save a new User
|
||||
exports.login = (req, res) => {
|
||||
var condition = {};
|
||||
const emailUsername = req.body.emailUsername;
|
||||
console.log('emailUsername', emailUsername);
|
||||
if (emailUsername) {
|
||||
condition = { $or: [
|
||||
{ email: emailUsername },
|
||||
{ username: emailUsername }
|
||||
]};
|
||||
condition = splitSite.splitSiteGet(req, condition);
|
||||
const parseExternalTokenId = (value = "") => {
|
||||
const token = `${value || ""}`;
|
||||
if (!token.startsWith("ext:")) return null;
|
||||
const chunks = token.split(":");
|
||||
if (chunks.length < 3) return null;
|
||||
return {
|
||||
externalUserId: chunks[1],
|
||||
site: Number(chunks[2]) || null
|
||||
};
|
||||
};
|
||||
|
||||
Employee.find(condition)
|
||||
.then(data => {
|
||||
if (data && data.length > 0) {
|
||||
const activeEmployee = isEmployeeActive(data[0]);
|
||||
if (data.length === 1 && bcrypt.compareSync(
|
||||
req.body.password,
|
||||
data[0].password
|
||||
) && activeEmployee) {
|
||||
var token = jwt.sign({id: data[0].id}, config.secret, {
|
||||
const getExternalPermissionBySite = async (externalUserId, site) => {
|
||||
if (!externalUserId || !site) return null;
|
||||
return ExtUserPermission.findOne({
|
||||
external_user_id: `${externalUserId}`,
|
||||
allow_site: Number(site)
|
||||
}).sort({ updatedAt: -1 });
|
||||
};
|
||||
|
||||
const buildExternalAuthResponse = async (externalUser, site) => {
|
||||
const permissionDoc = await getExternalPermissionBySite(externalUser?.id, site);
|
||||
const permissions = Array.isArray(permissionDoc?.permissions) ? permissionDoc.permissions : [];
|
||||
const tokenPayload = {
|
||||
id: `ext:${externalUser?.id}:${site}`,
|
||||
authSource: "external",
|
||||
externalUserId: externalUser?.id,
|
||||
site: Number(site),
|
||||
username: externalUser?.username || "",
|
||||
email: externalUser?.email || "",
|
||||
name: externalUser?.name || "",
|
||||
firstname: externalUser?.firstname || "",
|
||||
lastname: externalUser?.lastname || "",
|
||||
status: externalUser?.status || ""
|
||||
};
|
||||
const accessToken = jwt.sign(tokenPayload, config.secret, {
|
||||
expiresIn: 86400
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
username: externalUser?.username || "",
|
||||
email: externalUser?.email || "",
|
||||
roles: [],
|
||||
permissions,
|
||||
id: externalUser?.id,
|
||||
name: externalUser?.name || "",
|
||||
firstname: externalUser?.firstname || "",
|
||||
lastname: externalUser?.lastname || "",
|
||||
site: Number(site),
|
||||
status: externalUser?.status || "",
|
||||
auth_source: "external"
|
||||
};
|
||||
};
|
||||
|
||||
// Create and Save a new User
|
||||
exports.login = async (req, res) => {
|
||||
const emailUsername = req.body.emailUsername;
|
||||
const password = req.body.password;
|
||||
const requestedSite = Number(req.body.site);
|
||||
const site = Number.isInteger(requestedSite) && requestedSite > 0 ? requestedSite : splitSite.findSiteNumber(req);
|
||||
|
||||
if (!emailUsername) {
|
||||
return res.status(400).send({ message: "email or username is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
let condition = {
|
||||
$or: [{ email: emailUsername }, { username: emailUsername }]
|
||||
};
|
||||
condition = splitSite.splitSiteGet(req, condition);
|
||||
const localEmployees = await Employee.find(condition);
|
||||
|
||||
if (localEmployees && localEmployees.length > 0) {
|
||||
const localEmployee = localEmployees[0];
|
||||
const activeEmployee = isEmployeeActive(localEmployee);
|
||||
const isPasswordCorrect = bcrypt.compareSync(password, localEmployee.password || "");
|
||||
if (localEmployees.length === 1 && isPasswordCorrect && activeEmployee) {
|
||||
const token = jwt.sign({ id: localEmployee.id }, config.secret, {
|
||||
expiresIn: 86400 // 24 hours
|
||||
});
|
||||
res.send({
|
||||
return res.send({
|
||||
accessToken: token,
|
||||
username: data[0].username,
|
||||
email: data[0].email,
|
||||
roles: data[0].roles,
|
||||
permissions: getEffectivePermissions(data[0]),
|
||||
id: data[0].id,
|
||||
name: data[0].name,
|
||||
name_cn: data[0].name_cn
|
||||
} );
|
||||
} else {
|
||||
username: localEmployee.username,
|
||||
email: localEmployee.email,
|
||||
roles: localEmployee.roles,
|
||||
permissions: getEffectivePermissions(localEmployee),
|
||||
id: localEmployee.id,
|
||||
name: localEmployee.name,
|
||||
name_cn: localEmployee.name_cn
|
||||
});
|
||||
}
|
||||
if (!activeEmployee) {
|
||||
throw(Error('User is not activated'));
|
||||
} else {
|
||||
throw(Error('Email or Password Is Invalid'));
|
||||
throw Error("User is not activated");
|
||||
}
|
||||
// Local account exists but credential doesn't match. Continue to external auth fallback.
|
||||
}
|
||||
} else {
|
||||
throw(Error('Email or Password Is Invalid'));
|
||||
|
||||
try {
|
||||
const externalAuthResponse = await axios.post(
|
||||
HR_AUTH_LOGIN_ENDPOINT,
|
||||
{
|
||||
emailUsername,
|
||||
password,
|
||||
site
|
||||
},
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
const externalUser = externalAuthResponse?.data;
|
||||
const isExternalUserValid =
|
||||
externalUser &&
|
||||
externalUser.id &&
|
||||
`${externalUser?.status || ""}`.trim().toLowerCase() === "active";
|
||||
if (isExternalUserValid) {
|
||||
const loginPayload = await buildExternalAuthResponse(externalUser, site);
|
||||
return res.send(loginPayload);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
res.status(500).send({
|
||||
message:
|
||||
err.message || "Email Or Password Invalid"
|
||||
} catch (_externalError) {
|
||||
// Fall through to existing invalid-login response.
|
||||
}
|
||||
|
||||
throw Error("Email or Password Is Invalid");
|
||||
} catch (err) {
|
||||
return res.status(500).send({
|
||||
message: err.message || "Email Or Password Invalid"
|
||||
});
|
||||
});
|
||||
} else {
|
||||
throw(Error('email or username is required'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.me = (req, res) => {
|
||||
const authPayload = req.authPayload || {};
|
||||
const parsedExternalToken = parseExternalTokenId(req.userId);
|
||||
const isExternalAuth =
|
||||
authPayload?.authSource === "external" || !!parsedExternalToken?.externalUserId;
|
||||
|
||||
if (isExternalAuth) {
|
||||
const site = Number(authPayload?.site) || parsedExternalToken?.site || splitSite.findSiteNumber(req);
|
||||
const externalUserId = authPayload?.externalUserId || parsedExternalToken?.externalUserId;
|
||||
return getExternalPermissionBySite(externalUserId, site)
|
||||
.then((permissionDoc) => {
|
||||
let permissions = Array.isArray(permissionDoc?.permissions) ? permissionDoc.permissions : [];
|
||||
if (permissions.includes(SYSTEM_ACCESS_PERMISSION) === false) {
|
||||
// Keep login valid; UI permission gate will lock down non-system users.
|
||||
permissions = permissions.filter(Boolean);
|
||||
}
|
||||
return res.send({
|
||||
username: authPayload?.username || "",
|
||||
email: authPayload?.email || "",
|
||||
roles: [],
|
||||
permissions,
|
||||
id: externalUserId,
|
||||
name: authPayload?.name || "",
|
||||
firstname: authPayload?.firstname || "",
|
||||
lastname: authPayload?.lastname || "",
|
||||
site,
|
||||
status: authPayload?.status || "",
|
||||
auth_source: "external"
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
return res.status(500).send({
|
||||
message: err.message || "Failed to fetch current user"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const condition = splitSite.splitSiteGet(req, { _id: req.userId });
|
||||
Employee.findOne(condition)
|
||||
.then((employee) => {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
const db = require("../models");
|
||||
const Employee = db.employee;
|
||||
const ExtUserPermission = db.ext_usr_perm;
|
||||
const axios = require("axios");
|
||||
|
||||
var bcrypt = require("bcryptjs");
|
||||
const { splitSite } = require("../middlewares");
|
||||
const SYSTEM_ACCESS_PERMISSION = "System Access";
|
||||
const HR_EMPLOYEE_LIST_ENDPOINT = "https://ws-hr.mayosolution.com/api/integration/employees/list";
|
||||
const HR_INTEGRATION_USERNAME = "vibecodingking";
|
||||
const HR_INTEGRATION_PASSWORD = "oAQC483f1jxdJdoJcd0kCAd7C";
|
||||
|
||||
const ALL_PERMISSIONS = [
|
||||
'Dashboard',
|
||||
@@ -312,3 +318,131 @@ exports.deleteEmployee = (req, res) => {
|
||||
exports.getEmployeesWithUsernameOrEmail = (req, res) => {
|
||||
|
||||
};
|
||||
|
||||
exports.getExternalEmployeesList = async (req, res) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
HR_EMPLOYEE_LIST_ENDPOINT,
|
||||
{
|
||||
username: HR_INTEGRATION_USERNAME,
|
||||
password: HR_INTEGRATION_PASSWORD,
|
||||
status: "active"
|
||||
},
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
const list = Array.isArray(response?.data) ? response.data : [];
|
||||
res.send(list);
|
||||
} catch (err) {
|
||||
res.status(500).send({
|
||||
message: err?.response?.data?.message || err.message || "Failed to fetch employees from HR system."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.getExternalUserPermission = async (req, res) => {
|
||||
try {
|
||||
const externalUserId = `${req.query.external_user_id || req.query.externalUserId || ""}`.trim();
|
||||
const requestedSite = Number(req.query.site || req.query.allow_site);
|
||||
const allowSite = Number.isInteger(requestedSite) && requestedSite > 0 ? requestedSite : splitSite.findSiteNumber(req);
|
||||
if (!externalUserId) {
|
||||
return res.status(400).send({ message: "external_user_id is required." });
|
||||
}
|
||||
const config = await ExtUserPermission.findOne({
|
||||
external_user_id: externalUserId,
|
||||
allow_site: allowSite
|
||||
}).sort({ updatedAt: -1 });
|
||||
return res.send(config || null);
|
||||
} catch (err) {
|
||||
return res.status(500).send({
|
||||
message: err.message || "Failed to fetch external user permissions."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.getExternalUserPermissionsList = async (req, res) => {
|
||||
try {
|
||||
const requestedSite = Number(req.query.site || req.query.allow_site);
|
||||
const allowSite = Number.isInteger(requestedSite) && requestedSite > 0 ? requestedSite : splitSite.findSiteNumber(req);
|
||||
const records = await ExtUserPermission.find({ allow_site: allowSite }).sort({ username: 1, updatedAt: -1 });
|
||||
return res.send(records);
|
||||
} catch (err) {
|
||||
return res.status(500).send({
|
||||
message: err.message || "Failed to fetch external user permissions list."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.saveExternalUserPermission = async (req, res) => {
|
||||
try {
|
||||
const externalUserId = `${req.body.external_user_id || req.body.externalUserId || ""}`.trim();
|
||||
const allowSite = Number(req.body.allow_site || req.body.allowSite);
|
||||
const username = req.body.username || "";
|
||||
const name = req.body.name || "";
|
||||
const email = req.body.email || "";
|
||||
if (!externalUserId) {
|
||||
return res.status(400).send({ message: "external_user_id is required." });
|
||||
}
|
||||
if (!Number.isInteger(allowSite) || allowSite <= 0) {
|
||||
return res.status(400).send({ message: "allow_site must be a positive number." });
|
||||
}
|
||||
|
||||
const rawPermissions = Array.isArray(req.body.permissions) ? req.body.permissions : [];
|
||||
const permissions = Array.from(new Set([SYSTEM_ACCESS_PERMISSION, ...rawPermissions.filter(Boolean)]));
|
||||
const editorId = req.userId ? `${req.userId}` : "";
|
||||
const existing = await ExtUserPermission.findOne({
|
||||
external_user_id: externalUserId,
|
||||
allow_site: allowSite
|
||||
});
|
||||
const payload = {
|
||||
external_user_id: externalUserId,
|
||||
allow_site: allowSite,
|
||||
username,
|
||||
name,
|
||||
email,
|
||||
permissions,
|
||||
edit_by: editorId
|
||||
};
|
||||
if (!existing) {
|
||||
payload.create_by = editorId;
|
||||
}
|
||||
|
||||
const saved = await ExtUserPermission.findOneAndUpdate(
|
||||
{
|
||||
external_user_id: externalUserId,
|
||||
allow_site: allowSite
|
||||
},
|
||||
payload,
|
||||
{
|
||||
upsert: true,
|
||||
new: true,
|
||||
runValidators: true,
|
||||
setDefaultsOnInsert: true
|
||||
}
|
||||
);
|
||||
return res.send(saved);
|
||||
} catch (err) {
|
||||
return res.status(500).send({
|
||||
message: err.message || "Failed to save external user permissions."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.revokeExternalUserPermission = async (req, res) => {
|
||||
try {
|
||||
const externalUserId = `${req.query.external_user_id || req.query.externalUserId || req.body.external_user_id || ""}`.trim();
|
||||
const requestedSite = Number(req.query.site || req.query.allow_site || req.body.allow_site);
|
||||
const allowSite = Number.isInteger(requestedSite) && requestedSite > 0 ? requestedSite : splitSite.findSiteNumber(req);
|
||||
if (!externalUserId) {
|
||||
return res.status(400).send({ message: "external_user_id is required." });
|
||||
}
|
||||
await ExtUserPermission.deleteOne({
|
||||
external_user_id: externalUserId,
|
||||
allow_site: allowSite
|
||||
});
|
||||
return res.send({ success: true });
|
||||
} catch (err) {
|
||||
return res.status(500).send({
|
||||
message: err.message || "Failed to revoke external user permissions."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ verifyToken = (req, res, next) => {
|
||||
return res.status(401).send({ message: "Unauthorized!" });
|
||||
}
|
||||
req.userId = decoded.id;
|
||||
req.authPayload = decoded;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
36
app/models/ext-usr-perm.model.js
Normal file
36
app/models/ext-usr-perm.model.js
Normal file
@@ -0,0 +1,36 @@
|
||||
module.exports = (mongoose) => {
|
||||
const schema = mongoose.Schema(
|
||||
{
|
||||
external_user_id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
allow_site: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
username: String,
|
||||
name: String,
|
||||
email: String,
|
||||
permissions: [
|
||||
{
|
||||
type: String
|
||||
}
|
||||
],
|
||||
create_by: String,
|
||||
edit_by: String
|
||||
},
|
||||
{ collection: "ext_usr_perm", timestamps: true }
|
||||
);
|
||||
|
||||
schema.method("toJSON", function () {
|
||||
const { __v, _id, ...object } = this.toObject();
|
||||
object.id = _id;
|
||||
return object;
|
||||
});
|
||||
|
||||
schema.index({ external_user_id: 1, allow_site: 1 }, { unique: true });
|
||||
|
||||
const ExtUserPermission = mongoose.model("ext_usr_perm", schema);
|
||||
return ExtUserPermission;
|
||||
};
|
||||
@@ -36,4 +36,5 @@ db.carousel = require("./carousel.model")(mongoose);
|
||||
db.fingerprint_attendance = require("./fingerprint-attendance.model")(mongoose);
|
||||
db.dailyRoutesTemplate = require("./daily-routes-template.model")(mongoose);
|
||||
db.msg_custom_template = require("./msg-custom-template.model")(mongoose);
|
||||
db.ext_usr_perm = require("./ext-usr-perm.model")(mongoose);
|
||||
module.exports = db;
|
||||
@@ -11,6 +11,16 @@ module.exports = app => {
|
||||
var router = require("express").Router();
|
||||
// Retrieve all employee
|
||||
router.get("/", employees.getAllEmployees);
|
||||
// Retrieve active employee list from HR integration service
|
||||
router.post("/external/list", [authJwt.verifyToken], employees.getExternalEmployeesList);
|
||||
// Get external user permission config for a site
|
||||
router.get("/external/permissions", [authJwt.verifyToken], employees.getExternalUserPermission);
|
||||
// List external user permission configs for a site
|
||||
router.get("/external/permissions-list", [authJwt.verifyToken], employees.getExternalUserPermissionsList);
|
||||
// Save external user permission config for a site
|
||||
router.post("/external/permissions", [authJwt.verifyToken], employees.saveExternalUserPermission);
|
||||
// Revoke external user permission config for a site
|
||||
router.delete("/external/permissions", [authJwt.verifyToken], employees.revokeExternalUserPermission);
|
||||
// Retrive employees across sites
|
||||
router.get("/all-sites", employees.getAllEmployeesAcrossSites);
|
||||
// Create a new employee
|
||||
|
||||
@@ -21,6 +21,7 @@ import CreateEmployee from "./components/employees/CreateEmployee";
|
||||
import UpdateEmployee from "./components/employees/UpdateEmployee";
|
||||
import EmployeeList from "./components/employees/EmployeeList";
|
||||
import ViewEmployee from "./components/employees/ViewEmployee";
|
||||
import ExternalEmployeesImport from "./components/employees/ExternalEmployeesImport";
|
||||
import CreateRoute from "./components/trans-routes/CreateRoute";
|
||||
import Admin from './components/admin/Admin';
|
||||
import CustomerReport from "./components/admin/CustomerReport";
|
||||
@@ -244,6 +245,7 @@ function App() {
|
||||
|
||||
<Route path="/employees" element={<CreateEmployee /> } />
|
||||
<Route path="/employees/list" element={<EmployeeList/> } />
|
||||
<Route path="/employees/external-import" element={<ExternalEmployeesImport/> } />
|
||||
<Route path="/employees/:id" element={<ViewEmployee /> } />
|
||||
<Route path="/employees/edit/:id" element={<UpdateEmployee /> } />
|
||||
<Route path="/users" element={<UsersList />} />
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import React, {useState, useEffect} from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { driverSlice } from "./../../store";
|
||||
import { employeeSlice } from "../../store/employees";
|
||||
import { AuthService, EmployeeService } from "../../services";
|
||||
import React, { useMemo, useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Modal, Spinner } from "react-bootstrap";
|
||||
import { AuthService, EmployeeService, EventsService } from "../../services";
|
||||
import { EMPLOYEE_PERMISSION_GROUPS } from "../../shared";
|
||||
|
||||
const SYSTEM_ACCESS_PERMISSION = "System Access";
|
||||
|
||||
const EmployeeList = () => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [employees, setEmployees] = useState([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [showInactive, setShowInactive] = useState(false);
|
||||
|
||||
const [hrUsers, setHrUsers] = useState([]);
|
||||
const [isHrLoading, setIsHrLoading] = useState(false);
|
||||
const [isSavingHrPermission, setIsSavingHrPermission] = useState(false);
|
||||
const [hrKeyword, setHrKeyword] = useState('');
|
||||
const [hrSiteFilter, setHrSiteFilter] = useState(EventsService.site || 3);
|
||||
const [hrPermissionMap, setHrPermissionMap] = useState({});
|
||||
const [editingHrUser, setEditingHrUser] = useState(undefined);
|
||||
const [showHrPermissionModal, setShowHrPermissionModal] = useState(false);
|
||||
const [selectedHrPermissions, setSelectedHrPermissions] = useState([SYSTEM_ACCESS_PERMISSION]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!AuthService.canViewEmployees()) {
|
||||
@@ -22,8 +30,43 @@ const EmployeeList = () => {
|
||||
EmployeeService.getAllEmployees().then((data) =>
|
||||
setEmployees(data.data)
|
||||
);
|
||||
loadHrUsers();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadHrPermissionsBySite(hrSiteFilter);
|
||||
}, [hrSiteFilter]);
|
||||
|
||||
const loadHrUsers = () => {
|
||||
setIsHrLoading(true);
|
||||
EmployeeService.getExternalEmployeesList()
|
||||
.then((response) => {
|
||||
setHrUsers(Array.isArray(response?.data) ? response.data : []);
|
||||
})
|
||||
.catch((error) => {
|
||||
window.alert(error?.response?.data?.message || 'Failed to load HR users.');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsHrLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const loadHrPermissionsBySite = (site) => {
|
||||
EmployeeService.getExternalUserPermissionsList(site)
|
||||
.then((response) => {
|
||||
const nextMap = {};
|
||||
(Array.isArray(response?.data) ? response.data : []).forEach((item) => {
|
||||
const key = item?.external_user_id;
|
||||
if (!key) return;
|
||||
nextMap[key] = Array.isArray(item?.permissions) ? item.permissions : [];
|
||||
});
|
||||
setHrPermissionMap(nextMap);
|
||||
})
|
||||
.catch(() => {
|
||||
setHrPermissionMap({});
|
||||
});
|
||||
};
|
||||
|
||||
const redirectToAdmin = () => {
|
||||
navigate(`/admin/customer-report`)
|
||||
}
|
||||
@@ -41,6 +84,93 @@ const EmployeeList = () => {
|
||||
navigate(`/employees`)
|
||||
}
|
||||
|
||||
const goToExternalImport = () => {
|
||||
navigate(`/employees/external-import`);
|
||||
}
|
||||
|
||||
const filteredHrUsers = useMemo(() => {
|
||||
const site = Number(hrSiteFilter);
|
||||
return (hrUsers || [])
|
||||
.filter((item) => Number(item?.site) === site)
|
||||
.filter((item) => {
|
||||
if (!hrKeyword) return true;
|
||||
const key = hrKeyword.toLowerCase();
|
||||
return (
|
||||
(item?.username || '').toLowerCase().includes(key) ||
|
||||
(item?.name || '').toLowerCase().includes(key) ||
|
||||
(item?.title || '').toLowerCase().includes(key)
|
||||
);
|
||||
});
|
||||
}, [hrUsers, hrKeyword, hrSiteFilter]);
|
||||
|
||||
const openHrPermissionModal = (hrUser) => {
|
||||
setEditingHrUser(hrUser);
|
||||
const existingPermissions = hrPermissionMap?.[hrUser?.employee_id] || [];
|
||||
setSelectedHrPermissions(Array.from(new Set([SYSTEM_ACCESS_PERMISSION, ...existingPermissions])));
|
||||
setShowHrPermissionModal(true);
|
||||
};
|
||||
|
||||
const closeHrPermissionModal = () => {
|
||||
if (isSavingHrPermission) return;
|
||||
setShowHrPermissionModal(false);
|
||||
setEditingHrUser(undefined);
|
||||
setSelectedHrPermissions([SYSTEM_ACCESS_PERMISSION]);
|
||||
};
|
||||
|
||||
const toggleHrPermission = (permissionKey) => {
|
||||
if (permissionKey === SYSTEM_ACCESS_PERMISSION) return;
|
||||
setSelectedHrPermissions((prev) => {
|
||||
if (prev.includes(permissionKey)) {
|
||||
return prev.filter((item) => item !== permissionKey);
|
||||
}
|
||||
return [...prev, permissionKey];
|
||||
});
|
||||
};
|
||||
|
||||
const saveHrPermissions = () => {
|
||||
if (!editingHrUser?.employee_id) return;
|
||||
setIsSavingHrPermission(true);
|
||||
EmployeeService.saveExternalUserPermission({
|
||||
external_user_id: editingHrUser.employee_id,
|
||||
username: editingHrUser.username || '',
|
||||
name: editingHrUser.name || '',
|
||||
email: editingHrUser.email || '',
|
||||
allow_site: Number(hrSiteFilter),
|
||||
permissions: selectedHrPermissions
|
||||
})
|
||||
.then(() => {
|
||||
setHrPermissionMap((prev) => ({
|
||||
...prev,
|
||||
[editingHrUser.employee_id]: Array.from(new Set([SYSTEM_ACCESS_PERMISSION, ...selectedHrPermissions]))
|
||||
}));
|
||||
closeHrPermissionModal();
|
||||
})
|
||||
.catch((error) => {
|
||||
window.alert(error?.response?.data?.message || 'Failed to save HR user permissions.');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSavingHrPermission(false);
|
||||
});
|
||||
};
|
||||
|
||||
const revokeHrPermissions = (hrUser) => {
|
||||
if (!hrUser?.employee_id) return;
|
||||
if (!window.confirm(`Revoke all permissions for ${hrUser?.username || 'this HR user'} on Site ${hrSiteFilter}?`)) {
|
||||
return;
|
||||
}
|
||||
EmployeeService.revokeExternalUserPermission(hrUser.employee_id, Number(hrSiteFilter))
|
||||
.then(() => {
|
||||
setHrPermissionMap((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[hrUser.employee_id];
|
||||
return next;
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
window.alert(error?.response?.data?.message || 'Failed to revoke HR user permissions.');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -54,11 +184,76 @@ const EmployeeList = () => {
|
||||
Add New Employee
|
||||
</button>
|
||||
)}
|
||||
{AuthService.canAddOrEditEmployees() && (
|
||||
<button className="btn btn-primary btn-sm ms-2" onClick={() => goToExternalImport()}>
|
||||
Add New Employee From HR System
|
||||
</button>
|
||||
)}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div className="list row mb-4">
|
||||
<div className="col-md-12">
|
||||
<h6 className="text-primary">HR System Users</h6>
|
||||
<div className="mb-3 d-flex align-items-center" style={{ gap: '12px', flexWrap: 'wrap' }}>
|
||||
<label>
|
||||
Site:
|
||||
<select className="ms-2" value={hrSiteFilter} onChange={(e) => setHrSiteFilter(Number(e.currentTarget.value))}>
|
||||
<option value={1}>Site 1</option>
|
||||
<option value={2}>Site 2</option>
|
||||
<option value={3}>Site 3</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Filter:
|
||||
<input className="ms-2" type="text" value={hrKeyword} onChange={(e) => setHrKeyword(e.currentTarget.value)} />
|
||||
</label>
|
||||
<button className="btn btn-primary btn-sm" onClick={() => {loadHrUsers(); loadHrPermissionsBySite(hrSiteFilter);}} disabled={isHrLoading}>
|
||||
{isHrLoading ? 'Loading...' : 'Refresh HR Users'}
|
||||
</button>
|
||||
</div>
|
||||
<table className="personnel-info-table mb-5">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Name</th>
|
||||
<th>Title</th>
|
||||
<th>Site</th>
|
||||
<th>Configured Permissions</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isHrLoading && (
|
||||
<tr>
|
||||
<td colSpan={6}><Spinner size="sm" className="me-2" />Loading HR users...</td>
|
||||
</tr>
|
||||
)}
|
||||
{!isHrLoading && filteredHrUsers.map((hrUser) => {
|
||||
const configuredPermissions = hrPermissionMap?.[hrUser?.employee_id] || [];
|
||||
return (
|
||||
<tr key={`${hrUser?.employee_id}-${hrUser?.site}`}>
|
||||
<td>{hrUser?.username}</td>
|
||||
<td>{hrUser?.name}</td>
|
||||
<td>{hrUser?.title}</td>
|
||||
<td>{hrUser?.site}</td>
|
||||
<td>{configuredPermissions.length > 0 ? configuredPermissions.join(', ') : '-'}</td>
|
||||
<td>
|
||||
<button className="btn btn-primary btn-sm me-2" onClick={() => openHrPermissionModal(hrUser)}>Edit</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={() => revokeHrPermissions(hrUser)}>Revoke</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{!isHrLoading && filteredHrUsers.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} style={{ textAlign: 'center' }}>No HR users found for selected site.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h6 className="text-primary">Internal Employees</h6>
|
||||
<div className="mb-4">Filter By Name: <input type="text" value={keyword} onChange={(e) => setKeyword(e.currentTarget.value)}/></div>
|
||||
<input className="mb-4 me-2" type="checkbox" value={showInactive} checked={showInactive === true} onChange={() => setShowInactive(!showInactive)} />
|
||||
Show Inactive Employees
|
||||
@@ -93,6 +288,43 @@ const EmployeeList = () => {
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<Modal show={showHrPermissionModal} onHide={closeHrPermissionModal} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Edit HR User Permissions - {editingHrUser?.username}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="mb-3">
|
||||
<strong>Allow Site:</strong> {hrSiteFilter}
|
||||
</div>
|
||||
{Object.entries(EMPLOYEE_PERMISSION_GROUPS).map(([groupName, permissionItems]) => (
|
||||
<div key={groupName} className="mb-3">
|
||||
<div className="text-primary mb-1">{groupName}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px 18px' }}>
|
||||
{permissionItems.map((permissionKey) => (
|
||||
<label key={permissionKey} style={{ minWidth: '280px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="me-2"
|
||||
checked={selectedHrPermissions.includes(permissionKey)}
|
||||
onChange={() => toggleHrPermission(permissionKey)}
|
||||
disabled={permissionKey === SYSTEM_ACCESS_PERMISSION}
|
||||
/>
|
||||
{permissionKey}{permissionKey === SYSTEM_ACCESS_PERMISSION ? ' (required)' : ''}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={closeHrPermissionModal} disabled={isSavingHrPermission}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={saveHrPermissions} disabled={isSavingHrPermission}>
|
||||
{isSavingHrPermission ? <><Spinner size="sm" className="me-2" />Saving...</> : 'Done'}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
237
client/src/components/employees/ExternalEmployeesImport.js
Normal file
237
client/src/components/employees/ExternalEmployeesImport.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Modal, Spinner } from "react-bootstrap";
|
||||
import { AuthService, EmployeeService, EventsService } from "../../services";
|
||||
import { EMPLOYEE_PERMISSION_GROUPS } from "../../shared";
|
||||
|
||||
const SYSTEM_ACCESS_PERMISSION = "System Access";
|
||||
|
||||
const ExternalEmployeesImport = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [employees, setEmployees] = useState([]);
|
||||
const [siteFilter, setSiteFilter] = useState(EventsService.site || 3);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [showPermissionModal, setShowPermissionModal] = useState(false);
|
||||
const [selectedEmployee, setSelectedEmployee] = useState(undefined);
|
||||
const [selectedPermissions, setSelectedPermissions] = useState([SYSTEM_ACCESS_PERMISSION]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!AuthService.canAddOrEditEmployees()) {
|
||||
window.alert("You haven't login yet OR this user does not have access to this page. Please change an admin account to login.");
|
||||
AuthService.logout();
|
||||
navigate("/login");
|
||||
return;
|
||||
}
|
||||
loadExternalEmployees();
|
||||
}, []);
|
||||
|
||||
const loadExternalEmployees = () => {
|
||||
setLoading(true);
|
||||
EmployeeService.getExternalEmployeesList()
|
||||
.then((response) => {
|
||||
setEmployees(Array.isArray(response?.data) ? response.data : []);
|
||||
})
|
||||
.catch((error) => {
|
||||
window.alert(error?.response?.data?.message || "Failed to load employees from HR system.");
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const filteredEmployees = useMemo(() => {
|
||||
const siteNumber = Number(siteFilter);
|
||||
return (employees || [])
|
||||
.filter((item) => Number(item?.site) === siteNumber)
|
||||
.filter((item) => {
|
||||
if (!keyword) return true;
|
||||
const key = keyword.toLowerCase();
|
||||
return (
|
||||
(item?.username || "").toLowerCase().includes(key) ||
|
||||
(item?.name || "").toLowerCase().includes(key) ||
|
||||
(item?.title || "").toLowerCase().includes(key)
|
||||
);
|
||||
});
|
||||
}, [employees, siteFilter, keyword]);
|
||||
|
||||
const openPermissionModal = (employee) => {
|
||||
const allowSite = Number(siteFilter);
|
||||
setSelectedEmployee(employee);
|
||||
setSelectedPermissions([SYSTEM_ACCESS_PERMISSION]);
|
||||
setShowPermissionModal(true);
|
||||
EmployeeService.getExternalUserPermission(employee?.employee_id, allowSite)
|
||||
.then((response) => {
|
||||
const existingPermissions = Array.isArray(response?.data?.permissions) ? response.data.permissions : [];
|
||||
const nextPermissions = Array.from(new Set([SYSTEM_ACCESS_PERMISSION, ...existingPermissions]));
|
||||
setSelectedPermissions(nextPermissions);
|
||||
})
|
||||
.catch(() => {
|
||||
setSelectedPermissions([SYSTEM_ACCESS_PERMISSION]);
|
||||
});
|
||||
};
|
||||
|
||||
const closePermissionModal = () => {
|
||||
if (saving) return;
|
||||
setShowPermissionModal(false);
|
||||
setSelectedEmployee(undefined);
|
||||
setSelectedPermissions([SYSTEM_ACCESS_PERMISSION]);
|
||||
};
|
||||
|
||||
const togglePermission = (permissionKey) => {
|
||||
if (permissionKey === SYSTEM_ACCESS_PERMISSION) return;
|
||||
setSelectedPermissions((prev) => {
|
||||
if (prev.includes(permissionKey)) {
|
||||
return prev.filter((item) => item !== permissionKey);
|
||||
}
|
||||
return [...prev, permissionKey];
|
||||
});
|
||||
};
|
||||
|
||||
const savePermissions = () => {
|
||||
if (!selectedEmployee?.employee_id) return;
|
||||
setSaving(true);
|
||||
EmployeeService.saveExternalUserPermission({
|
||||
external_user_id: selectedEmployee.employee_id,
|
||||
username: selectedEmployee.username || "",
|
||||
name: selectedEmployee.name || "",
|
||||
email: selectedEmployee.email || "",
|
||||
allow_site: Number(siteFilter),
|
||||
permissions: selectedPermissions
|
||||
})
|
||||
.then(() => {
|
||||
closePermissionModal();
|
||||
})
|
||||
.catch((error) => {
|
||||
window.alert(error?.response?.data?.message || "Failed to save external user permissions.");
|
||||
})
|
||||
.finally(() => {
|
||||
setSaving(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="list row mb-4">
|
||||
<div className="col-md-12 text-primary">
|
||||
<h5>
|
||||
Add New Employee From HR System
|
||||
<button className="btn btn-link btn-sm" onClick={() => navigate("/employees/list")}>Back</button>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="list row mb-4">
|
||||
<div className="col-md-12">
|
||||
<div className="mb-3 d-flex align-items-center" style={{ gap: "12px", flexWrap: "wrap" }}>
|
||||
<label>
|
||||
Filter By Site:
|
||||
<select
|
||||
className="ms-2"
|
||||
value={siteFilter}
|
||||
onChange={(e) => setSiteFilter(Number(e.currentTarget.value))}
|
||||
>
|
||||
<option value={1}>Site 1</option>
|
||||
<option value={2}>Site 2</option>
|
||||
<option value={3}>Site 3</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Filter By Username/Name:
|
||||
<input
|
||||
className="ms-2"
|
||||
type="text"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
<button className="btn btn-primary btn-sm" onClick={loadExternalEmployees} disabled={loading}>
|
||||
{loading ? "Loading..." : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="py-4"><Spinner size="sm" className="me-2" />Loading employees...</div>
|
||||
) : (
|
||||
<table className="personnel-info-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Name</th>
|
||||
<th>Title</th>
|
||||
<th>Site</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredEmployees.map((item) => (
|
||||
<tr key={`${item?.employee_id}-${item?.site}`}>
|
||||
<td>{item?.username}</td>
|
||||
<td>{item?.name}</td>
|
||||
<td>{item?.title}</td>
|
||||
<td>{item?.site}</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => openPermissionModal(item)}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredEmployees.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} style={{ textAlign: "center" }}>No active employees found for selected site.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal show={showPermissionModal} onHide={closePermissionModal} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Set Permissions - {selectedEmployee?.username}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="mb-3">
|
||||
<strong>Allow Site:</strong> {siteFilter}
|
||||
</div>
|
||||
{Object.entries(EMPLOYEE_PERMISSION_GROUPS).map(([groupName, permissionItems]) => (
|
||||
<div key={groupName} className="mb-3">
|
||||
<div className="text-primary mb-1">{groupName}</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "10px 18px" }}>
|
||||
{permissionItems.map((permissionKey) => (
|
||||
<label key={permissionKey} style={{ minWidth: "280px" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="me-2"
|
||||
checked={selectedPermissions.includes(permissionKey)}
|
||||
onChange={() => togglePermission(permissionKey)}
|
||||
disabled={permissionKey === SYSTEM_ACCESS_PERMISSION}
|
||||
/>
|
||||
{permissionKey}
|
||||
{permissionKey === SYSTEM_ACCESS_PERMISSION ? " (required)" : ""}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={closePermissionModal} disabled={saving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={savePermissions} disabled={saving}>
|
||||
{saving ? <><Spinner size="sm" className="me-2" />Saving...</> : "Done"}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExternalEmployeesImport;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {AuthService} from './../../services';
|
||||
import { EventsService } from "../../services";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const Login = ({ setMenu}) => {
|
||||
@@ -27,7 +28,8 @@ const Login = ({ setMenu}) => {
|
||||
const loginAndRedirect = () => {
|
||||
AuthService.login({
|
||||
emailUsername: username,
|
||||
password
|
||||
password,
|
||||
site: EventsService.site
|
||||
}).then(({data}) => {
|
||||
localStorage.setItem('token', data.accessToken);
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
|
||||
@@ -38,6 +38,40 @@ const getAllEmployeeFiles = (employeeId, name, fileType) => {
|
||||
return http.get(`/files/uploadedDocs/employee/${employeeId}/type/${fileType}/name/${name}`)
|
||||
}
|
||||
|
||||
const getExternalEmployeesList = () => {
|
||||
return http.post('/employees/external/list');
|
||||
};
|
||||
|
||||
const getExternalUserPermission = (externalUserId, site) => {
|
||||
return http.get('/employees/external/permissions', {
|
||||
params: {
|
||||
external_user_id: externalUserId,
|
||||
site
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getExternalUserPermissionsList = (site) => {
|
||||
return http.get('/employees/external/permissions-list', {
|
||||
params: {
|
||||
site
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveExternalUserPermission = (data) => {
|
||||
return http.post('/employees/external/permissions', data);
|
||||
};
|
||||
|
||||
const revokeExternalUserPermission = (externalUserId, site) => {
|
||||
return http.delete('/employees/external/permissions', {
|
||||
params: {
|
||||
external_user_id: externalUserId,
|
||||
site
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const validatePassword = (password = '') => {
|
||||
const lowercaseRegex = /[a-z]/;
|
||||
const uppercaseRegex = /[A-Z]/;
|
||||
@@ -69,5 +103,10 @@ export const EmployeeService = {
|
||||
getEmployee,
|
||||
validatePassword,
|
||||
uploadEmployeeFile,
|
||||
getAllEmployeeFiles
|
||||
getAllEmployeeFiles,
|
||||
getExternalEmployeesList,
|
||||
getExternalUserPermission,
|
||||
getExternalUserPermissionsList,
|
||||
saveExternalUserPermission,
|
||||
revokeExternalUserPermission
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user