fix
All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 36s

This commit is contained in:
2026-03-16 14:52:20 -04:00
parent 2664177463
commit a500bef32d
11 changed files with 868 additions and 61 deletions

View File

@@ -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 />} />

View File

@@ -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>
</>
)
};

View 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;

View File

@@ -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));

View File

@@ -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
};