From a500bef32d2c2f894b61a96d0f60c26479865f85 Mon Sep 17 00:00:00 2001 From: Lixian Zhou Date: Mon, 16 Mar 2026 14:52:20 -0400 Subject: [PATCH] fix --- app/controllers/auth.controller.js | 215 +++++++++++---- app/controllers/employee.controller.js | 134 ++++++++++ app/middlewares/authJwt.js | 1 + app/models/ext-usr-perm.model.js | 36 +++ app/models/index.js | 1 + app/routes/employee.routes.js | 10 + client/src/App.js | 2 + .../src/components/employees/EmployeeList.js | 248 +++++++++++++++++- .../employees/ExternalEmployeesImport.js | 237 +++++++++++++++++ client/src/components/login/Login.js | 4 +- client/src/services/EmployeeService.js | 41 ++- 11 files changed, 868 insertions(+), 61 deletions(-) create mode 100644 app/models/ext-usr-perm.model.js create mode 100644 client/src/components/employees/ExternalEmployeesImport.js diff --git a/app/controllers/auth.controller.js b/app/controllers/auth.controller.js index c2cafc7..058a35e 100644 --- a/app/controllers/auth.controller.js +++ b/app/controllers/auth.controller.js @@ -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"; }; +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 + }; +}; + +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 = (req, res) => { - var condition = {}; +exports.login = async (req, res) => { const emailUsername = req.body.emailUsername; - console.log('emailUsername', emailUsername); - if (emailUsername) { - condition = { $or: [ - { email: emailUsername }, - { username: emailUsername } - ]}; - condition = splitSite.splitSiteGet(req, condition); - - 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, { - expiresIn: 86400 // 24 hours - }); - 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 { - if (!activeEmployee) { - throw(Error('User is not activated')); - } else { - throw(Error('Email or Password Is Invalid')); - } - } - } else { - throw(Error('Email or Password Is Invalid')); - } - }) - .catch(err => { - res.status(500).send({ - message: - err.message || "Email Or Password Invalid" - }); - }); - } else { - throw(Error('email or username is required')); + 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 + }); + return res.send({ + accessToken: token, + 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"); + } + // Local account exists but credential doesn't match. Continue to external auth fallback. + } + + 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 (_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" + }); + } +}; 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) => { diff --git a/app/controllers/employee.controller.js b/app/controllers/employee.controller.js index 8ab1947..aa5ee76 100644 --- a/app/controllers/employee.controller.js +++ b/app/controllers/employee.controller.js @@ -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." + }); + } +}; diff --git a/app/middlewares/authJwt.js b/app/middlewares/authJwt.js index e6fb90a..9332fa1 100644 --- a/app/middlewares/authJwt.js +++ b/app/middlewares/authJwt.js @@ -13,6 +13,7 @@ verifyToken = (req, res, next) => { return res.status(401).send({ message: "Unauthorized!" }); } req.userId = decoded.id; + req.authPayload = decoded; next(); }); }; diff --git a/app/models/ext-usr-perm.model.js b/app/models/ext-usr-perm.model.js new file mode 100644 index 0000000..8b91a8b --- /dev/null +++ b/app/models/ext-usr-perm.model.js @@ -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; +}; diff --git a/app/models/index.js b/app/models/index.js index 65ac962..dda8f5c 100644 --- a/app/models/index.js +++ b/app/models/index.js @@ -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; \ No newline at end of file diff --git a/app/routes/employee.routes.js b/app/routes/employee.routes.js index 62e1212..bb200a5 100644 --- a/app/routes/employee.routes.js +++ b/app/routes/employee.routes.js @@ -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 diff --git a/client/src/App.js b/client/src/App.js index 4c0220e..5bf4fb3 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -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() { } /> } /> + } /> } /> } /> } /> diff --git a/client/src/components/employees/EmployeeList.js b/client/src/components/employees/EmployeeList.js index c4b07ff..526eaa7 100644 --- a/client/src/components/employees/EmployeeList.js +++ b/client/src/components/employees/EmployeeList.js @@ -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 )} + {AuthService.canAddOrEditEmployees() && ( + + )}
+
HR System Users
+
+ + + +
+ + + + + + + + + + + + + {isHrLoading && ( + + + + )} + {!isHrLoading && filteredHrUsers.map((hrUser) => { + const configuredPermissions = hrPermissionMap?.[hrUser?.employee_id] || []; + return ( + + + + + + + + + ); + })} + {!isHrLoading && filteredHrUsers.length === 0 && ( + + + + )} + +
UsernameNameTitleSiteConfigured Permissions
Loading HR users...
{hrUser?.username}{hrUser?.name}{hrUser?.title}{hrUser?.site}{configuredPermissions.length > 0 ? configuredPermissions.join(', ') : '-'} + + +
No HR users found for selected site.
+ +
Internal Employees
Filter By Name: setKeyword(e.currentTarget.value)}/>
setShowInactive(!showInactive)} /> Show Inactive Employees @@ -93,6 +288,43 @@ const EmployeeList = () => {
+ + + Edit HR User Permissions - {editingHrUser?.username} + + +
+ Allow Site: {hrSiteFilter} +
+ {Object.entries(EMPLOYEE_PERMISSION_GROUPS).map(([groupName, permissionItems]) => ( +
+
{groupName}
+
+ {permissionItems.map((permissionKey) => ( + + ))} +
+
+ ))} +
+ + + + +
) }; diff --git a/client/src/components/employees/ExternalEmployeesImport.js b/client/src/components/employees/ExternalEmployeesImport.js new file mode 100644 index 0000000..60c6c53 --- /dev/null +++ b/client/src/components/employees/ExternalEmployeesImport.js @@ -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 ( + <> +
+
+
+ Add New Employee From HR System + +
+
+
+ +
+
+
+ + + +
+ + {loading ? ( +
Loading employees...
+ ) : ( + + + + + + + + + + + + {filteredEmployees.map((item) => ( + + + + + + + + ))} + {filteredEmployees.length === 0 && ( + + + + )} + +
UsernameNameTitleSite
{item?.username}{item?.name}{item?.title}{item?.site} + +
No active employees found for selected site.
+ )} +
+
+ + + + Set Permissions - {selectedEmployee?.username} + + +
+ Allow Site: {siteFilter} +
+ {Object.entries(EMPLOYEE_PERMISSION_GROUPS).map(([groupName, permissionItems]) => ( +
+
{groupName}
+
+ {permissionItems.map((permissionKey) => ( + + ))} +
+
+ ))} +
+ + + + +
+ + ); +}; + +export default ExternalEmployeesImport; diff --git a/client/src/components/login/Login.js b/client/src/components/login/Login.js index e902930..aa0f730 100644 --- a/client/src/components/login/Login.js +++ b/client/src/components/login/Login.js @@ -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)); diff --git a/client/src/services/EmployeeService.js b/client/src/services/EmployeeService.js index 0030149..cfa5ea9 100644 --- a/client/src/services/EmployeeService.js +++ b/client/src/services/EmployeeService.js @@ -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 };