create-nodemin-app 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +82 -0
- package/package.json +25 -0
- package/templates/HRMS_Mongodb/README.md +331 -0
- package/templates/HRMS_Mongodb/backend/.env.example +6 -0
- package/templates/HRMS_Mongodb/backend/package-lock.json +1646 -0
- package/templates/HRMS_Mongodb/backend/package.json +26 -0
- package/templates/HRMS_Mongodb/backend/src/config/db.js +9 -0
- package/templates/HRMS_Mongodb/backend/src/controllers/authController.js +187 -0
- package/templates/HRMS_Mongodb/backend/src/controllers/departmentController.js +70 -0
- package/templates/HRMS_Mongodb/backend/src/controllers/employeeController.js +178 -0
- package/templates/HRMS_Mongodb/backend/src/controllers/positionController.js +66 -0
- package/templates/HRMS_Mongodb/backend/src/middleware/auth.js +57 -0
- package/templates/HRMS_Mongodb/backend/src/middleware/errorHandler.js +32 -0
- package/templates/HRMS_Mongodb/backend/src/middleware/restrictToAdmin.js +5 -0
- package/templates/HRMS_Mongodb/backend/src/middleware/validate.js +13 -0
- package/templates/HRMS_Mongodb/backend/src/models/Department.js +19 -0
- package/templates/HRMS_Mongodb/backend/src/models/Employee.js +81 -0
- package/templates/HRMS_Mongodb/backend/src/models/Position.js +19 -0
- package/templates/HRMS_Mongodb/backend/src/models/User.js +40 -0
- package/templates/HRMS_Mongodb/backend/src/routes/authRoutes.js +27 -0
- package/templates/HRMS_Mongodb/backend/src/routes/departmentRoutes.js +33 -0
- package/templates/HRMS_Mongodb/backend/src/routes/employeeRoutes.js +39 -0
- package/templates/HRMS_Mongodb/backend/src/routes/positionRoutes.js +32 -0
- package/templates/HRMS_Mongodb/backend/src/server.js +74 -0
- package/templates/HRMS_Mongodb/backend/src/utils/roles.js +5 -0
- package/templates/HRMS_Mongodb/backend/src/utils/seed.js +78 -0
- package/templates/HRMS_Mongodb/backend/src/validators/authValidator.js +61 -0
- package/templates/HRMS_Mongodb/backend/src/validators/departmentValidator.js +21 -0
- package/templates/HRMS_Mongodb/backend/src/validators/employeeValidator.js +27 -0
- package/templates/HRMS_Mongodb/backend/src/validators/positionValidator.js +26 -0
- package/templates/HRMS_Mongodb/frontend/index.html +19 -0
- package/templates/HRMS_Mongodb/frontend/package-lock.json +2812 -0
- package/templates/HRMS_Mongodb/frontend/package.json +25 -0
- package/templates/HRMS_Mongodb/frontend/public/favicon.svg +4 -0
- package/templates/HRMS_Mongodb/frontend/src/App.jsx +50 -0
- package/templates/HRMS_Mongodb/frontend/src/api/axios.js +54 -0
- package/templates/HRMS_Mongodb/frontend/src/components/ProtectedRoute.jsx +26 -0
- package/templates/HRMS_Mongodb/frontend/src/components/layout/DashboardLayout.jsx +16 -0
- package/templates/HRMS_Mongodb/frontend/src/components/layout/Sidebar.jsx +108 -0
- package/templates/HRMS_Mongodb/frontend/src/components/ui/Button.jsx +33 -0
- package/templates/HRMS_Mongodb/frontend/src/components/ui/Input.jsx +20 -0
- package/templates/HRMS_Mongodb/frontend/src/components/ui/Modal.jsx +48 -0
- package/templates/HRMS_Mongodb/frontend/src/components/ui/Select.jsx +27 -0
- package/templates/HRMS_Mongodb/frontend/src/context/AuthContext.jsx +97 -0
- package/templates/HRMS_Mongodb/frontend/src/index.css +34 -0
- package/templates/HRMS_Mongodb/frontend/src/main.jsx +16 -0
- package/templates/HRMS_Mongodb/frontend/src/pages/Dashboard.jsx +78 -0
- package/templates/HRMS_Mongodb/frontend/src/pages/Departments.jsx +144 -0
- package/templates/HRMS_Mongodb/frontend/src/pages/Employees.jsx +297 -0
- package/templates/HRMS_Mongodb/frontend/src/pages/LeaveReport.jsx +113 -0
- package/templates/HRMS_Mongodb/frontend/src/pages/Login.jsx +92 -0
- package/templates/HRMS_Mongodb/frontend/src/pages/Positions.jsx +157 -0
- package/templates/HRMS_Mongodb/frontend/src/pages/Register.jsx +93 -0
- package/templates/HRMS_Mongodb/frontend/src/pages/ResetPassword.jsx +135 -0
- package/templates/HRMS_Mongodb/frontend/src/utils/roles.js +1 -0
- package/templates/HRMS_Mongodb/frontend/src/utils/session.js +5 -0
- package/templates/HRMS_Mongodb/frontend/src/utils/validation.js +66 -0
- package/templates/HRMS_Mongodb/frontend/vite.config.js +16 -0
- package/templates/HRMS_Mysql/backend/db.js +13 -0
- package/templates/HRMS_Mysql/backend/package-lock.json +1614 -0
- package/templates/HRMS_Mysql/backend/package.json +21 -0
- package/templates/HRMS_Mysql/backend/server.js +421 -0
- package/templates/HRMS_Mysql/frontend/dist/assets/index-CtLtQf3_.js +75 -0
- package/templates/HRMS_Mysql/frontend/dist/assets/index-Dq1AXlEY.css +1 -0
- package/templates/HRMS_Mysql/frontend/dist/index.html +14 -0
- package/templates/HRMS_Mysql/frontend/dist/vite.svg +1 -0
- package/templates/HRMS_Mysql/frontend/index.html +13 -0
- package/templates/HRMS_Mysql/frontend/package-lock.json +2978 -0
- package/templates/HRMS_Mysql/frontend/package.json +25 -0
- package/templates/HRMS_Mysql/frontend/postcss.config.js +6 -0
- package/templates/HRMS_Mysql/frontend/public/vite.svg +1 -0
- package/templates/HRMS_Mysql/frontend/src/App.jsx +55 -0
- package/templates/HRMS_Mysql/frontend/src/api.js +11 -0
- package/templates/HRMS_Mysql/frontend/src/components/Layout.jsx +59 -0
- package/templates/HRMS_Mysql/frontend/src/index.css +7 -0
- package/templates/HRMS_Mysql/frontend/src/main.jsx +13 -0
- package/templates/HRMS_Mysql/frontend/src/pages/Dashboard.jsx +45 -0
- package/templates/HRMS_Mysql/frontend/src/pages/Departments.jsx +108 -0
- package/templates/HRMS_Mysql/frontend/src/pages/EmployeeStatusReport.jsx +72 -0
- package/templates/HRMS_Mysql/frontend/src/pages/Employees.jsx +252 -0
- package/templates/HRMS_Mysql/frontend/src/pages/ForgotPassword.jsx +66 -0
- package/templates/HRMS_Mysql/frontend/src/pages/Login.jsx +79 -0
- package/templates/HRMS_Mysql/frontend/src/pages/Positions.jsx +109 -0
- package/templates/HRMS_Mysql/frontend/src/pages/Register.jsx +95 -0
- package/templates/HRMS_Mysql/frontend/src/pages/Users.jsx +133 -0
- package/templates/HRMS_Mysql/frontend/tailwind.config.js +26 -0
- package/templates/HRMS_Mysql/frontend/vite.config.js +15 -0
- package/templates/HRMS_Mysql/hrms_schema.sql +57 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dab-hrms-frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axios": "^1.7.9",
|
|
13
|
+
"lucide-react": "^0.469.0",
|
|
14
|
+
"react": "^18.3.1",
|
|
15
|
+
"react-dom": "^18.3.1",
|
|
16
|
+
"react-hot-toast": "^2.4.1",
|
|
17
|
+
"react-router-dom": "^7.1.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
21
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
22
|
+
"tailwindcss": "^4.0.0",
|
|
23
|
+
"vite": "^6.0.6"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
2
|
+
import { useAuth } from './context/AuthContext';
|
|
3
|
+
import ProtectedRoute from './components/ProtectedRoute';
|
|
4
|
+
import DashboardLayout from './components/layout/DashboardLayout';
|
|
5
|
+
|
|
6
|
+
// View Components
|
|
7
|
+
import Login from './pages/Login';
|
|
8
|
+
import Register from './pages/Register';
|
|
9
|
+
import ForgotPassword from './pages/ResetPassword';
|
|
10
|
+
import Dashboard from './pages/Dashboard';
|
|
11
|
+
import Employees from './pages/Employees';
|
|
12
|
+
import Departments from './pages/Departments';
|
|
13
|
+
import Positions from './pages/Positions';
|
|
14
|
+
import LeaveReport from './pages/LeaveReport';
|
|
15
|
+
|
|
16
|
+
export default function App() {
|
|
17
|
+
const { sessionKey } = useAuth();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Routes>
|
|
21
|
+
{/* Public Authentication Pipelines */}
|
|
22
|
+
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
23
|
+
<Route path="/login" element={<Login />} />
|
|
24
|
+
<Route path="/register" element={<Register />} />
|
|
25
|
+
<Route path="/forgot-password" element={<ForgotPassword />} />
|
|
26
|
+
|
|
27
|
+
{/* Protected Global Administrative Portal Clusters */}
|
|
28
|
+
<Route
|
|
29
|
+
path="/"
|
|
30
|
+
element={
|
|
31
|
+
<ProtectedRoute>
|
|
32
|
+
<DashboardLayout />
|
|
33
|
+
</ProtectedRoute>
|
|
34
|
+
}
|
|
35
|
+
>
|
|
36
|
+
{/* Core Administrative Views */}
|
|
37
|
+
<Route path="dashboard" element={<Dashboard key={`dash-${sessionKey}`} />} />
|
|
38
|
+
<Route path="employees" element={<Employees key={`emp-${sessionKey}`} />} />
|
|
39
|
+
<Route path="departments" element={<Departments key={`dept-${sessionKey}`} />} />
|
|
40
|
+
<Route path="positions" element={<Positions key={`pos-${sessionKey}`} />} />
|
|
41
|
+
|
|
42
|
+
{/* FIXED & PROTECTED: Safely nested within the layout shell tree */}
|
|
43
|
+
<Route path="reports/leave" element={<LeaveReport key={`leave-${sessionKey}`} />} />
|
|
44
|
+
</Route>
|
|
45
|
+
|
|
46
|
+
{/* Fallback Catch-All Wildcard Navigation */}
|
|
47
|
+
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
|
48
|
+
</Routes>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
// Create an instance of axios with standard configurations
|
|
4
|
+
const api = axios.create({
|
|
5
|
+
// Fallback to local server proxy url if VITE_API_BASE_URL is not explicitly configured
|
|
6
|
+
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api',
|
|
7
|
+
timeout: 10000,
|
|
8
|
+
headers: {
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
'Accept': 'application/json'
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Request Interceptor
|
|
16
|
+
* Intercepts every outgoing request before it leaves the browser,
|
|
17
|
+
* injects the JWT token from localStorage if present to prevent 401 loops.
|
|
18
|
+
*/
|
|
19
|
+
api.interceptors.request.use(
|
|
20
|
+
(config) => {
|
|
21
|
+
// Read the token saved during successful authentication
|
|
22
|
+
const token = localStorage.getItem('token');
|
|
23
|
+
|
|
24
|
+
if (token) {
|
|
25
|
+
// Standard HTTP header syntax expected by your backend protect middleware
|
|
26
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return config;
|
|
30
|
+
},
|
|
31
|
+
(error) => {
|
|
32
|
+
// Handle request setup formatting errors gracefully
|
|
33
|
+
return Promise.reject(error);
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Response Interceptor (Optional Safeguard)
|
|
39
|
+
* Instantly intercepts incoming responses. If the server sends an authorization
|
|
40
|
+
* or verification error, it purges local references to keep data fresh.
|
|
41
|
+
*/
|
|
42
|
+
api.interceptors.response.use(
|
|
43
|
+
(response) => response,
|
|
44
|
+
(error) => {
|
|
45
|
+
// If the server returns a 401 or token validation fails explicitly
|
|
46
|
+
if (error.response && error.response.status === 401) {
|
|
47
|
+
localStorage.removeItem('token');
|
|
48
|
+
localStorage.removeItem('user');
|
|
49
|
+
}
|
|
50
|
+
return Promise.reject(error);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export default api;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Navigate, useLocation } from 'react-router-dom';
|
|
2
|
+
import { useAuth } from '../context/AuthContext';
|
|
3
|
+
|
|
4
|
+
const ProtectedRoute = ({ children }) => {
|
|
5
|
+
const { isAuthenticated, loading } = useAuth();
|
|
6
|
+
const location = useLocation();
|
|
7
|
+
|
|
8
|
+
if (loading) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex min-h-screen items-center justify-center bg-brand-950">
|
|
11
|
+
<div className="flex flex-col items-center gap-4">
|
|
12
|
+
<div className="h-10 w-10 animate-spin rounded-full border-4 border-brand-800 border-t-brand-400" />
|
|
13
|
+
<p className="text-sm text-slate-400">Loading...</p>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!isAuthenticated) {
|
|
20
|
+
return <Navigate to="/login" state={{ from: location }} replace />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return children;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default ProtectedRoute;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Outlet } from 'react-router-dom';
|
|
2
|
+
import Sidebar from '../layout/Sidebar'; // Double check this path matches where your Sidebar file sits
|
|
3
|
+
|
|
4
|
+
export default function DashboardLayout() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="flex min-h-screen bg-slate-950 text-slate-100">
|
|
7
|
+
{/* 1. Sidebar stays locked on the left side */}
|
|
8
|
+
<Sidebar />
|
|
9
|
+
|
|
10
|
+
{/* 2. Main content pane wraps the switching views on the right side */}
|
|
11
|
+
<main className="flex-1 p-8 lg:p-12 overflow-y-auto">
|
|
12
|
+
<Outlet />
|
|
13
|
+
</main>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
2
|
+
import toast from 'react-hot-toast';
|
|
3
|
+
|
|
4
|
+
const mainNavigation = [
|
|
5
|
+
{ name: 'Dashboard', href: '/dashboard' },
|
|
6
|
+
{ name: 'Employees', href: '/employees' },
|
|
7
|
+
{ name: 'Departments', href: '/departments' },
|
|
8
|
+
{ name: 'Positions', href: '/positions' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const reportNavigation = [
|
|
12
|
+
{ name: 'Leave Report', href: '/reports/leave' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export default function Sidebar() {
|
|
16
|
+
const location = useLocation();
|
|
17
|
+
const navigate = useNavigate();
|
|
18
|
+
|
|
19
|
+
const handleLogout = () => {
|
|
20
|
+
// 1. Flush local authorization credentials from memory
|
|
21
|
+
localStorage.removeItem('token');
|
|
22
|
+
localStorage.removeItem('user');
|
|
23
|
+
|
|
24
|
+
// 2. Alert profile termination successfully
|
|
25
|
+
toast.success('Session closed successfully');
|
|
26
|
+
|
|
27
|
+
// 3. Kick tracking route to root login gateway outside layout shell
|
|
28
|
+
navigate('/login');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const renderLinks = (items) => (
|
|
32
|
+
<ul className="space-y-1.5">
|
|
33
|
+
{items.map((item) => {
|
|
34
|
+
const isActive = location.pathname === item.href;
|
|
35
|
+
return (
|
|
36
|
+
<li key={item.name}>
|
|
37
|
+
<Link
|
|
38
|
+
to={item.href}
|
|
39
|
+
className={`group flex items-center justify-between rounded-xl px-4 py-2.5 text-sm font-medium tracking-wide transition-all duration-200 relative ${
|
|
40
|
+
isActive
|
|
41
|
+
? 'bg-brand-500 text-white font-semibold shadow-lg shadow-brand-500/15'
|
|
42
|
+
: 'text-slate-400 hover:bg-brand-900/40 hover:text-stone-100'
|
|
43
|
+
}`}
|
|
44
|
+
>
|
|
45
|
+
<span>{item.name}</span>
|
|
46
|
+
|
|
47
|
+
{isActive ? (
|
|
48
|
+
<span className="h-1.5 w-1.5 rounded-full bg-white animate-pulse" />
|
|
49
|
+
) : (
|
|
50
|
+
<span className="text-xs opacity-0 group-hover:opacity-100 transition-opacity duration-150 text-slate-500 font-mono">
|
|
51
|
+
→
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</Link>
|
|
55
|
+
</li>
|
|
56
|
+
);
|
|
57
|
+
})}
|
|
58
|
+
</ul>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex h-screen w-64 flex-col bg-brand-950 border-r border-brand-800/60 p-4 justify-between select-none">
|
|
63
|
+
<div>
|
|
64
|
+
{/* Core Branding Platform Panel Header */}
|
|
65
|
+
<div className="mb-8 px-4 py-3 border-b border-brand-900/80 flex items-center justify-between">
|
|
66
|
+
<span className="text-base font-bold tracking-widest text-white uppercase bg-clip-text">
|
|
67
|
+
DAB Enterprise
|
|
68
|
+
</span>
|
|
69
|
+
<span className="text-[10px] bg-brand-900 text-brand-400 px-2 py-0.5 rounded-md font-mono border border-brand-800/40">
|
|
70
|
+
v1.0
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Section 1: Main Management Routing Links */}
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<span className="px-4 text-[11px] font-bold uppercase tracking-widest text-brand-500/80 block mb-2">
|
|
77
|
+
Core Directories
|
|
78
|
+
</span>
|
|
79
|
+
<nav>{renderLinks(mainNavigation)}</nav>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Section 2: Analytics & Status Reporting Modules */}
|
|
83
|
+
<div className="mt-8 space-y-2">
|
|
84
|
+
<span className="px-4 text-[11px] font-bold uppercase tracking-widest text-brand-500/80 block mb-2">
|
|
85
|
+
Analytics Matrices
|
|
86
|
+
</span>
|
|
87
|
+
<nav>{renderLinks(reportNavigation)}</nav>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Footer Branding Info & Pure Text Action Logout Trigger */}
|
|
92
|
+
<div className="space-y-3">
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={handleLogout}
|
|
96
|
+
className="w-full rounded-xl px-4 py-2.5 text-left text-sm font-medium tracking-wide text-red-400 hover:bg-red-500/10 hover:text-red-300 transition-all duration-150"
|
|
97
|
+
>
|
|
98
|
+
Logout
|
|
99
|
+
</button>
|
|
100
|
+
<div className="px-4 py-3 bg-brand-900/20 border border-brand-900/60 rounded-xl text-center">
|
|
101
|
+
<p className="text-[11px] text-slate-500 font-medium">
|
|
102
|
+
HR Administration Engine
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const variants = {
|
|
2
|
+
primary: 'bg-brand-600 text-white hover:bg-brand-500 shadow-lg shadow-brand-950/50',
|
|
3
|
+
secondary: 'bg-brand-800 text-slate-200 border border-brand-600 hover:bg-brand-700',
|
|
4
|
+
danger: 'bg-red-700 text-white hover:bg-red-600',
|
|
5
|
+
ghost: 'text-slate-300 hover:bg-brand-800',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const sizes = {
|
|
9
|
+
sm: 'px-3 py-1.5 text-sm',
|
|
10
|
+
md: 'px-4 py-2.5 text-sm',
|
|
11
|
+
lg: 'px-6 py-3 text-base',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default function Button({
|
|
15
|
+
children,
|
|
16
|
+
variant = 'primary',
|
|
17
|
+
size = 'md',
|
|
18
|
+
className = '',
|
|
19
|
+
disabled,
|
|
20
|
+
type = 'button',
|
|
21
|
+
...props
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
type={type}
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
className={`inline-flex items-center justify-center gap-2 rounded-lg font-semibold transition-all focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 focus:ring-offset-card disabled:cursor-not-allowed disabled:opacity-50 ${variants[variant]} ${sizes[size]} ${className}`}
|
|
28
|
+
{...props}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export default function Input({ label, error, id, className = '', ...props }) {
|
|
2
|
+
const inputId = id || props.name;
|
|
3
|
+
return (
|
|
4
|
+
<div className="w-full">
|
|
5
|
+
{label && (
|
|
6
|
+
<label htmlFor={inputId} className="mb-1.5 block text-sm font-medium text-slate-300">
|
|
7
|
+
{label}
|
|
8
|
+
</label>
|
|
9
|
+
)}
|
|
10
|
+
<input
|
|
11
|
+
id={inputId}
|
|
12
|
+
className={`w-full rounded-lg border bg-brand-950/50 px-4 py-2.5 text-white placeholder:text-slate-500 transition focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/25 ${
|
|
13
|
+
error ? 'border-red-500' : 'border-brand-700/60'
|
|
14
|
+
} ${className}`}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
{error && <p className="mt-1 text-sm text-red-400">{error}</p>}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Button from './Button';
|
|
2
|
+
|
|
3
|
+
export default function Modal({ isOpen, onClose, title, children, size = 'md' }) {
|
|
4
|
+
if (!isOpen) return null;
|
|
5
|
+
|
|
6
|
+
const sizes = {
|
|
7
|
+
sm: 'max-w-md',
|
|
8
|
+
md: 'max-w-lg',
|
|
9
|
+
lg: 'max-w-2xl',
|
|
10
|
+
xl: 'max-w-4xl',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
15
|
+
<div className="absolute inset-0 bg-brand-950/80 backdrop-blur-sm" onClick={onClose} aria-hidden />
|
|
16
|
+
<div
|
|
17
|
+
className={`relative w-full ${sizes[size]} max-h-[90vh] overflow-y-auto rounded-xl bg-card p-6 shadow-2xl ring-1 ring-brand-800`}
|
|
18
|
+
role="dialog"
|
|
19
|
+
aria-modal="true"
|
|
20
|
+
>
|
|
21
|
+
<div className="mb-4 flex items-center justify-between">
|
|
22
|
+
<h2 className="text-xl font-bold text-white">{title}</h2>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
onClick={onClose}
|
|
26
|
+
className="rounded-lg px-2 py-1 text-sm font-medium text-slate-400 hover:bg-brand-800 hover:text-white"
|
|
27
|
+
>
|
|
28
|
+
Close
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
{children}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ModalFooter({ onCancel, submitLabel = 'Save', loading, danger }) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="mt-6 flex justify-end gap-3 border-t border-brand-800 pt-4">
|
|
40
|
+
<Button variant="secondary" type="button" onClick={onCancel}>
|
|
41
|
+
Cancel
|
|
42
|
+
</Button>
|
|
43
|
+
<Button variant={danger ? 'danger' : 'primary'} type="submit" disabled={loading}>
|
|
44
|
+
{loading ? 'Saving...' : submitLabel}
|
|
45
|
+
</Button>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export default function Select({ label, error, id, options = [], placeholder, className = '', ...props }) {
|
|
2
|
+
const selectId = id || props.name;
|
|
3
|
+
return (
|
|
4
|
+
<div className="w-full">
|
|
5
|
+
{label && (
|
|
6
|
+
<label htmlFor={selectId} className="mb-1.5 block text-sm font-medium text-slate-300">
|
|
7
|
+
{label}
|
|
8
|
+
</label>
|
|
9
|
+
)}
|
|
10
|
+
<select
|
|
11
|
+
id={selectId}
|
|
12
|
+
className={`w-full rounded-lg border bg-brand-950/50 px-4 py-2.5 text-white transition focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/25 ${
|
|
13
|
+
error ? 'border-red-500' : 'border-brand-700/60'
|
|
14
|
+
} ${className}`}
|
|
15
|
+
{...props}
|
|
16
|
+
>
|
|
17
|
+
{placeholder && <option value="">{placeholder}</option>}
|
|
18
|
+
{options.map((opt) => (
|
|
19
|
+
<option key={opt.value} value={opt.value}>
|
|
20
|
+
{opt.label}
|
|
21
|
+
</option>
|
|
22
|
+
))}
|
|
23
|
+
</select>
|
|
24
|
+
{error && <p className="mt-1 text-sm text-red-400">{error}</p>}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import toast from 'react-hot-toast';
|
|
3
|
+
import api from '../api/axios';
|
|
4
|
+
import { clearUserSession } from '../utils/session';
|
|
5
|
+
|
|
6
|
+
const AuthContext = createContext(null);
|
|
7
|
+
|
|
8
|
+
export const AuthProvider = ({ children }) => {
|
|
9
|
+
const [user, setUser] = useState(null);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [sessionKey, setSessionKey] = useState(0);
|
|
12
|
+
|
|
13
|
+
const bumpSession = useCallback(() => {
|
|
14
|
+
setSessionKey((key) => key + 1);
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
const persistAuth = (token, userData) => {
|
|
18
|
+
clearUserSession();
|
|
19
|
+
localStorage.setItem('token', token);
|
|
20
|
+
localStorage.setItem('user', JSON.stringify(userData));
|
|
21
|
+
setUser(userData);
|
|
22
|
+
bumpSession();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const logout = useCallback(() => {
|
|
26
|
+
clearUserSession();
|
|
27
|
+
localStorage.removeItem('token');
|
|
28
|
+
localStorage.removeItem('user');
|
|
29
|
+
setUser(null);
|
|
30
|
+
toast.dismiss();
|
|
31
|
+
bumpSession();
|
|
32
|
+
}, [bumpSession]);
|
|
33
|
+
|
|
34
|
+
const login = async (userName, password) => {
|
|
35
|
+
clearUserSession();
|
|
36
|
+
const { data } = await api.post('/auth/login', { userName, password });
|
|
37
|
+
persistAuth(data.token, data.user);
|
|
38
|
+
return data;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const register = async (payload) => {
|
|
42
|
+
const { data } = await api.post('/auth/register', payload);
|
|
43
|
+
return data;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// FIXED: Accepts full payload containing email, password, and confirmPassword
|
|
47
|
+
const resetPassword = async (payload) => {
|
|
48
|
+
const { data } = await api.post('/auth/reset-password', payload);
|
|
49
|
+
return data;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const init = async () => {
|
|
54
|
+
const token = localStorage.getItem('token');
|
|
55
|
+
if (!token) {
|
|
56
|
+
setLoading(false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const { data } = await api.get('/auth/me');
|
|
61
|
+
setUser(data.user);
|
|
62
|
+
localStorage.setItem('user', JSON.stringify(data.user));
|
|
63
|
+
} catch {
|
|
64
|
+
clearUserSession();
|
|
65
|
+
localStorage.removeItem('token');
|
|
66
|
+
localStorage.removeItem('user');
|
|
67
|
+
setUser(null);
|
|
68
|
+
} finally {
|
|
69
|
+
setLoading(false);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
init();
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<AuthContext.Provider
|
|
77
|
+
value={{
|
|
78
|
+
user,
|
|
79
|
+
loading,
|
|
80
|
+
sessionKey,
|
|
81
|
+
login,
|
|
82
|
+
logout,
|
|
83
|
+
register,
|
|
84
|
+
resetPassword,
|
|
85
|
+
isAuthenticated: !!user,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</AuthContext.Provider>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const useAuth = () => {
|
|
94
|
+
const ctx = useContext(AuthContext);
|
|
95
|
+
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
96
|
+
return ctx;
|
|
97
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--font-sans: 'DM Sans', system-ui, sans-serif;
|
|
5
|
+
--color-brand-50: #eff6ff;
|
|
6
|
+
--color-brand-100: #dbeafe;
|
|
7
|
+
--color-brand-200: #bfdbfe;
|
|
8
|
+
--color-brand-300: #93c5fd;
|
|
9
|
+
--color-brand-400: #60a5fa;
|
|
10
|
+
--color-brand-500: #3b82f6;
|
|
11
|
+
--color-brand-600: #2563eb;
|
|
12
|
+
--color-brand-700: #1d4ed8;
|
|
13
|
+
--color-brand-800: #1e3a8a;
|
|
14
|
+
--color-brand-900: #172554;
|
|
15
|
+
--color-brand-950: #0f172a;
|
|
16
|
+
--color-surface: #0f172a;
|
|
17
|
+
--color-card: #1e293b;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
* {
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
margin: 0;
|
|
26
|
+
font-family: var(--font-sans);
|
|
27
|
+
background: var(--color-surface);
|
|
28
|
+
color: #e2e8f0;
|
|
29
|
+
-webkit-font-smoothing: antialiased;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#root {
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
4
|
+
import { AuthProvider } from './context/AuthContext';
|
|
5
|
+
import App from './App';
|
|
6
|
+
import './index.css';
|
|
7
|
+
|
|
8
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
9
|
+
<React.StrictMode>
|
|
10
|
+
<BrowserRouter>
|
|
11
|
+
<AuthProvider>
|
|
12
|
+
<App />
|
|
13
|
+
</AuthProvider>
|
|
14
|
+
</BrowserRouter>
|
|
15
|
+
</React.StrictMode>
|
|
16
|
+
);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import api from '../api/axios';
|
|
3
|
+
import toast from 'react-hot-toast';
|
|
4
|
+
|
|
5
|
+
const StatCard = ({ label, value }) => (
|
|
6
|
+
<div className="rounded-xl bg-card p-5 ring-1 ring-brand-800">
|
|
7
|
+
<p className="text-2xl font-bold text-white">{value}</p>
|
|
8
|
+
<p className="mt-1 text-sm text-slate-400">{label}</p>
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export default function Dashboard() {
|
|
13
|
+
const [stats, setStats] = useState(null);
|
|
14
|
+
const [counts, setCounts] = useState({ departments: 0, positions: 0 });
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const load = async () => {
|
|
18
|
+
try {
|
|
19
|
+
// Fetch global corporate HR metric objects
|
|
20
|
+
const statsRes = await api.get('/employees/stats');
|
|
21
|
+
setStats(statsRes.data.data);
|
|
22
|
+
|
|
23
|
+
// Fetches administrative management node parameters unconditionally
|
|
24
|
+
const [deptRes, posRes] = await Promise.all([
|
|
25
|
+
api.get('/departments'),
|
|
26
|
+
api.get('/positions'),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
setCounts({
|
|
30
|
+
departments: deptRes.data.count || deptRes.data.data?.length || 0,
|
|
31
|
+
positions: posRes.data.count || posRes.data.data?.length || 0
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Dashboard load failure:', error);
|
|
35
|
+
toast.error('Failed to load dashboard metrics');
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
load();
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div>
|
|
43
|
+
<header className="mb-8">
|
|
44
|
+
<h1 className="text-2xl font-bold text-white">Dashboard</h1>
|
|
45
|
+
<p className="mt-1 text-slate-400">Company-wide HR overview</p>
|
|
46
|
+
</header>
|
|
47
|
+
|
|
48
|
+
{/* Metrics Layout Grid */}
|
|
49
|
+
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
50
|
+
<StatCard label="Total employees" value={stats?.total ?? '—'} />
|
|
51
|
+
<StatCard label="Active employees" value={stats?.active ?? '—'} />
|
|
52
|
+
<StatCard label="Departments" value={counts.departments} />
|
|
53
|
+
<StatCard label="Positions" value={counts.positions} />
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Department Breakdown Progress Bar Section */}
|
|
57
|
+
{stats?.byDepartment?.length > 0 && (
|
|
58
|
+
<div className="mt-8 rounded-xl bg-card p-6 ring-1 ring-brand-800">
|
|
59
|
+
<h2 className="text-lg font-semibold text-white">Employees by department</h2>
|
|
60
|
+
<div className="mt-4 space-y-3">
|
|
61
|
+
{stats.byDepartment.map((item) => (
|
|
62
|
+
<div key={item._id} className="flex items-center gap-4">
|
|
63
|
+
<span className="w-36 shrink-0 text-sm text-slate-300 truncate">{item._id || 'Unassigned'}</span>
|
|
64
|
+
<div className="h-2 flex-1 overflow-hidden rounded-full bg-brand-950">
|
|
65
|
+
<div
|
|
66
|
+
className="h-full rounded-full bg-brand-500"
|
|
67
|
+
style={{ width: `${Math.min(100, (item.count / (stats.total || 1)) * 100)}%` }}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
<span className="w-6 text-right text-sm font-medium text-white">{item.count}</span>
|
|
71
|
+
</div>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|