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,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dab-hrms-backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "DAB Enterprise HRMS API",
|
|
5
|
+
"main": "src/server.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node src/server.js",
|
|
9
|
+
"dev": "nodemon src/server.js",
|
|
10
|
+
"seed": "node src/utils/seed.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"bcryptjs": "^2.4.3",
|
|
14
|
+
"cors": "^2.8.5",
|
|
15
|
+
"dotenv": "^16.4.7",
|
|
16
|
+
"express": "^4.21.2",
|
|
17
|
+
"express-mongo-sanitize": "^2.2.0",
|
|
18
|
+
"express-rate-limit": "^7.5.0",
|
|
19
|
+
"express-validator": "^7.2.1",
|
|
20
|
+
"helmet": "^8.0.0",
|
|
21
|
+
"jsonwebtoken": "^9.0.2",
|
|
22
|
+
"mongoose": "^8.9.3",
|
|
23
|
+
"nodemon": "^3.1.14",
|
|
24
|
+
"xss-clean": "^0.1.4"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const connectDB = async () => {
|
|
4
|
+
const uri = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/dab_hrms';
|
|
5
|
+
await mongoose.connect(uri);
|
|
6
|
+
console.log(`MongoDB connected: ${mongoose.connection.host}`);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default connectDB;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import User from '../models/User.js';
|
|
3
|
+
import Employee from '../models/Employee.js';
|
|
4
|
+
import Department from '../models/Department.js';
|
|
5
|
+
import Position from '../models/Position.js';
|
|
6
|
+
|
|
7
|
+
// --- Helper Functions ---
|
|
8
|
+
|
|
9
|
+
const signToken = (id) =>
|
|
10
|
+
jwt.sign({ id }, process.env.JWT_SECRET, {
|
|
11
|
+
expiresIn: process.env.JWT_EXPIRE || '7d',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const sendTokenResponse = (user, statusCode, res) => {
|
|
15
|
+
const token = signToken(user._id);
|
|
16
|
+
const userObj = user.toObject();
|
|
17
|
+
delete userObj.password;
|
|
18
|
+
|
|
19
|
+
res.status(statusCode).json({
|
|
20
|
+
success: true,
|
|
21
|
+
token,
|
|
22
|
+
user: userObj,
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// --- Authentication Controllers ---
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @desc Authenticate user & get token
|
|
30
|
+
* @route POST /api/auth/login
|
|
31
|
+
* @access Public
|
|
32
|
+
*/
|
|
33
|
+
export const login = async (req, res, next) => {
|
|
34
|
+
try {
|
|
35
|
+
const { userName, password } = req.body;
|
|
36
|
+
|
|
37
|
+
const user = await User.findOne({ userName })
|
|
38
|
+
.select('+password')
|
|
39
|
+
.populate({
|
|
40
|
+
path: 'employee',
|
|
41
|
+
populate: [{ path: 'department' }, { path: 'position' }],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!user || !(await user.comparePassword(password))) {
|
|
45
|
+
return res.status(401).json({ success: false, message: 'Invalid username or password' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
sendTokenResponse(user, 200, res);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
next(error);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @desc Register a new user and employee profile
|
|
56
|
+
* @route POST /api/auth/register
|
|
57
|
+
* @access Public
|
|
58
|
+
*/
|
|
59
|
+
export const register = async (req, res, next) => {
|
|
60
|
+
try {
|
|
61
|
+
const { userName, password, email } = req.body;
|
|
62
|
+
const formattedEmail = email.toLowerCase().trim();
|
|
63
|
+
|
|
64
|
+
const existingUser = await User.findOne({ userName });
|
|
65
|
+
if (existingUser) {
|
|
66
|
+
return res.status(400).json({ success: false, message: 'Username is already taken' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const existingEmail = await Employee.findOne({ empEmail: formattedEmail });
|
|
70
|
+
if (existingEmail) {
|
|
71
|
+
return res.status(400).json({ success: false, message: 'An account with this email already exists' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let department = await Department.findOne().sort({ departName: 1 });
|
|
75
|
+
if (!department) {
|
|
76
|
+
department = await Department.create({
|
|
77
|
+
departName: 'General Administration',
|
|
78
|
+
departname: 'General Administration'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let position = await Position.findOne().sort({ posName: 1 });
|
|
83
|
+
if (!position) {
|
|
84
|
+
position = await Position.create({
|
|
85
|
+
posName: 'Staff Member',
|
|
86
|
+
posname: 'Staff Member',
|
|
87
|
+
RequiredQualification: 'Not Specified',
|
|
88
|
+
requiredQualification: 'Not Specified'
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const employee = await Employee.create({
|
|
93
|
+
empFirstName: userName,
|
|
94
|
+
empLastName: 'Employee',
|
|
95
|
+
empEmail: formattedEmail,
|
|
96
|
+
empTelephone: 'N/A',
|
|
97
|
+
empGender: 'Other',
|
|
98
|
+
empAddress: 'Kigali, Rwanda',
|
|
99
|
+
empDateOfBirth: new Date('1995-01-01'),
|
|
100
|
+
empHireDate: new Date(),
|
|
101
|
+
empStatus: 'Active',
|
|
102
|
+
department: department._id,
|
|
103
|
+
position: position._id,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await User.create({
|
|
107
|
+
userName,
|
|
108
|
+
password,
|
|
109
|
+
employee: employee._id,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
res.status(201).json({
|
|
113
|
+
success: true,
|
|
114
|
+
message: 'Your account has been created. You can now sign in.',
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
next(error);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @desc Custom User-defined Password Provision (Reset Flow)
|
|
123
|
+
* @route POST /api/auth/reset-password
|
|
124
|
+
* @access Public
|
|
125
|
+
*/
|
|
126
|
+
export const resetPassword = async (req, res, next) => {
|
|
127
|
+
try {
|
|
128
|
+
const { email, password, confirmPassword } = req.body;
|
|
129
|
+
|
|
130
|
+
// 1. Core Defensively Safe Parameter Checks (Prevents uncaught crashes changing to 404s)
|
|
131
|
+
if (!email) {
|
|
132
|
+
return res.status(400).json({ success: false, message: 'Email field is empty or format was corrupted' });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!password || !confirmPassword) {
|
|
136
|
+
return res.status(400).json({ success: false, message: 'Please provide and confirm your new password' });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (password !== confirmPassword) {
|
|
140
|
+
return res.status(400).json({ success: false, message: 'Passwords do not match' });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (password.length < 6) {
|
|
144
|
+
return res.status(400).json({ success: false, message: 'Password must be at least 6 characters long' });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Convert safely now that we verified the email variable string exists
|
|
148
|
+
const formattedEmail = String(email).toLowerCase().trim();
|
|
149
|
+
|
|
150
|
+
// 2. Query and link database profiles
|
|
151
|
+
const employee = await Employee.findOne({ empEmail: formattedEmail });
|
|
152
|
+
if (!employee) {
|
|
153
|
+
return res.status(404).json({
|
|
154
|
+
success: false,
|
|
155
|
+
message: 'No account found with this email address',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const user = await User.findOne({ employee: employee._id }).select('+password');
|
|
160
|
+
if (!user) {
|
|
161
|
+
return res.status(404).json({
|
|
162
|
+
success: false,
|
|
163
|
+
message: 'No user account linked to this employee email',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 3. Mutate fields (triggers pre-save password encryption inside User schema automatically)
|
|
168
|
+
user.password = password;
|
|
169
|
+
await user.save();
|
|
170
|
+
|
|
171
|
+
res.status(200).json({
|
|
172
|
+
success: true,
|
|
173
|
+
message: 'Your password has been reset successfully. You can now use your new password to sign in.',
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
next(error);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @desc Get currently logged-in user profile details
|
|
182
|
+
* @route GET /api/auth/me
|
|
183
|
+
* @access Private
|
|
184
|
+
*/
|
|
185
|
+
export const getMe = async (req, res) => {
|
|
186
|
+
res.status(200).json({ success: true, user: req.user });
|
|
187
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import Department from '../models/Department.js';
|
|
2
|
+
import Employee from '../models/Employee.js';
|
|
3
|
+
|
|
4
|
+
export const getDepartments = async (req, res, next) => {
|
|
5
|
+
try {
|
|
6
|
+
const departments = await Department.find().sort({ departName: 1 });
|
|
7
|
+
res.status(200).json({ success: true, count: departments.length, data: departments });
|
|
8
|
+
} catch (error) {
|
|
9
|
+
next(error);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const getDepartment = async (req, res, next) => {
|
|
14
|
+
try {
|
|
15
|
+
const department = await Department.findById(req.params.id);
|
|
16
|
+
if (!department) {
|
|
17
|
+
return res.status(404).json({ success: false, message: 'Department not found' });
|
|
18
|
+
}
|
|
19
|
+
const employees = await Employee.find({ department: department._id })
|
|
20
|
+
.populate('position')
|
|
21
|
+
.select('empFirstName empLastName empEmail empStatus');
|
|
22
|
+
|
|
23
|
+
res.status(200).json({ success: true, data: { ...department.toObject(), employees } });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
next(error);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const createDepartment = async (req, res, next) => {
|
|
30
|
+
try {
|
|
31
|
+
const department = await Department.create(req.body);
|
|
32
|
+
res.status(201).json({ success: true, data: department });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
next(error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const updateDepartment = async (req, res, next) => {
|
|
39
|
+
try {
|
|
40
|
+
const department = await Department.findByIdAndUpdate(req.params.id, req.body, {
|
|
41
|
+
new: true,
|
|
42
|
+
runValidators: true,
|
|
43
|
+
});
|
|
44
|
+
if (!department) {
|
|
45
|
+
return res.status(404).json({ success: false, message: 'Department not found' });
|
|
46
|
+
}
|
|
47
|
+
res.status(200).json({ success: true, data: department });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
next(error);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const deleteDepartment = async (req, res, next) => {
|
|
54
|
+
try {
|
|
55
|
+
const count = await Employee.countDocuments({ department: req.params.id });
|
|
56
|
+
if (count > 0) {
|
|
57
|
+
return res.status(400).json({
|
|
58
|
+
success: false,
|
|
59
|
+
message: `Cannot delete department with ${count} assigned employee(s)`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const department = await Department.findByIdAndDelete(req.params.id);
|
|
63
|
+
if (!department) {
|
|
64
|
+
return res.status(404).json({ success: false, message: 'Department not found' });
|
|
65
|
+
}
|
|
66
|
+
res.status(200).json({ success: true, message: 'Department deleted successfully' });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
next(error);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import Employee from '../models/Employee.js';
|
|
2
|
+
|
|
3
|
+
export const getEmployees = async (req, res, next) => {
|
|
4
|
+
try {
|
|
5
|
+
const employees = await Employee.find()
|
|
6
|
+
.populate('department')
|
|
7
|
+
.populate('position')
|
|
8
|
+
.sort({ createdAt: -1 });
|
|
9
|
+
|
|
10
|
+
res.status(200).json({ success: true, count: employees.length, data: employees });
|
|
11
|
+
} catch (error) {
|
|
12
|
+
next(error);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const getEmployee = async (req, res, next) => {
|
|
17
|
+
try {
|
|
18
|
+
const employee = await Employee.findById(req.params.id)
|
|
19
|
+
.populate('department')
|
|
20
|
+
.populate('position');
|
|
21
|
+
|
|
22
|
+
if (!employee) {
|
|
23
|
+
return res.status(404).json({ success: false, message: 'Employee not found' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
res.status(200).json({ success: true, data: employee });
|
|
27
|
+
} catch (error) {
|
|
28
|
+
next(error);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const createEmployee = async (req, res, next) => {
|
|
33
|
+
try {
|
|
34
|
+
const employee = await Employee.create(req.body);
|
|
35
|
+
const populated = await Employee.findById(employee._id)
|
|
36
|
+
.populate('department')
|
|
37
|
+
.populate('position');
|
|
38
|
+
|
|
39
|
+
res.status(201).json({ success: true, data: populated });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
next(error);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const updateEmployee = async (req, res, next) => {
|
|
46
|
+
try {
|
|
47
|
+
const employee = await Employee.findByIdAndUpdate(req.params.id, req.body, {
|
|
48
|
+
new: true,
|
|
49
|
+
runValidators: true,
|
|
50
|
+
})
|
|
51
|
+
.populate('department')
|
|
52
|
+
.populate('position');
|
|
53
|
+
|
|
54
|
+
if (!employee) {
|
|
55
|
+
return res.status(404).json({ success: false, message: 'Employee not found' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
res.status(200).json({ success: true, data: employee });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
next(error);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const deleteEmployee = async (req, res, next) => {
|
|
65
|
+
try {
|
|
66
|
+
const employee = await Employee.findByIdAndDelete(req.params.id);
|
|
67
|
+
|
|
68
|
+
if (!employee) {
|
|
69
|
+
return res.status(404).json({ success: false, message: 'Employee not found' });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
res.status(200).json({ success: true, message: 'Employee deleted successfully' });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
next(error);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// 1. FIXED & UPDATED STATS METHOD
|
|
79
|
+
export const getEmployeeStats = async (req, res, next) => {
|
|
80
|
+
try {
|
|
81
|
+
// Run separate asynchronous database counts in parallel for optimal efficiency
|
|
82
|
+
const [
|
|
83
|
+
total,
|
|
84
|
+
active,
|
|
85
|
+
onLeave,
|
|
86
|
+
left,
|
|
87
|
+
blacklisted,
|
|
88
|
+
deceased,
|
|
89
|
+
onMission
|
|
90
|
+
] = await Promise.all([
|
|
91
|
+
Employee.countDocuments(),
|
|
92
|
+
Employee.countDocuments({ empStatus: 'Active' }),
|
|
93
|
+
Employee.countDocuments({ empStatus: 'On Leave' }),
|
|
94
|
+
Employee.countDocuments({ empStatus: 'Left' }),
|
|
95
|
+
Employee.countDocuments({ empStatus: 'Blacklisted' }),
|
|
96
|
+
Employee.countDocuments({ empStatus: 'Deceased' }),
|
|
97
|
+
Employee.countDocuments({ empStatus: 'On Mission' })
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
const byDepartment = await Employee.aggregate([
|
|
101
|
+
{
|
|
102
|
+
$lookup: {
|
|
103
|
+
from: 'departments',
|
|
104
|
+
localField: 'department',
|
|
105
|
+
foreignField: '_id',
|
|
106
|
+
as: 'dept',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{ $unwind: '$dept' },
|
|
110
|
+
{ $group: { _id: '$dept.departName', count: { $sum: 1 } } },
|
|
111
|
+
{ $sort: { count: -1 } },
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
res.status(200).json({
|
|
115
|
+
success: true,
|
|
116
|
+
data: {
|
|
117
|
+
total,
|
|
118
|
+
active,
|
|
119
|
+
onLeave,
|
|
120
|
+
left,
|
|
121
|
+
blacklisted,
|
|
122
|
+
deceased,
|
|
123
|
+
onMission,
|
|
124
|
+
byDepartment
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
next(error);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Leave Report
|
|
133
|
+
export const getLeaveReport = async (req, res, next) => {
|
|
134
|
+
try {
|
|
135
|
+
const leaveReport = await Employee.aggregate([
|
|
136
|
+
// A. Match only people on leave
|
|
137
|
+
{ $match: { empStatus: 'On Leave' } },
|
|
138
|
+
// B. Lookup relationship attributes inside the departments collection
|
|
139
|
+
{
|
|
140
|
+
$lookup: {
|
|
141
|
+
from: 'departments',
|
|
142
|
+
localField: 'department',
|
|
143
|
+
foreignField: '_id',
|
|
144
|
+
as: 'deptDetails'
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{ $unwind: { path: '$deptDetails', preserveNullAndEmptyArrays: true } },
|
|
148
|
+
// C. Group documents under department designations
|
|
149
|
+
{
|
|
150
|
+
$group: {
|
|
151
|
+
_id: { $ifNull: ['$deptDetails.departName', 'Unassigned Department'] },
|
|
152
|
+
employees: {
|
|
153
|
+
$push: {
|
|
154
|
+
id: '$_id',
|
|
155
|
+
name: { $concat: ['$empFirstName', ' ', '$empLastName'] },
|
|
156
|
+
email: '$empEmail',
|
|
157
|
+
telephone: '$empTelephone',
|
|
158
|
+
status: '$empStatus'
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
totalInDepartment: { $sum: 1 }
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{ $sort: { _id: 1 } }
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
// Calculate absolute sum aggregate metrics total
|
|
168
|
+
const grandTotal = leaveReport.reduce((acc, current) => acc + current.totalInDepartment, 0);
|
|
169
|
+
|
|
170
|
+
res.status(200).json({
|
|
171
|
+
success: true,
|
|
172
|
+
totalEmployeesOnLeave: grandTotal,
|
|
173
|
+
reportData: leaveReport
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
next(error);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import Position from '../models/Position.js';
|
|
2
|
+
import Employee from '../models/Employee.js';
|
|
3
|
+
|
|
4
|
+
export const getPositions = async (req, res, next) => {
|
|
5
|
+
try {
|
|
6
|
+
const positions = await Position.find().sort({ posName: 1 });
|
|
7
|
+
res.status(200).json({ success: true, count: positions.length, data: positions });
|
|
8
|
+
} catch (error) {
|
|
9
|
+
next(error);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const getPosition = async (req, res, next) => {
|
|
14
|
+
try {
|
|
15
|
+
const position = await Position.findById(req.params.id);
|
|
16
|
+
if (!position) {
|
|
17
|
+
return res.status(404).json({ success: false, message: 'Position not found' });
|
|
18
|
+
}
|
|
19
|
+
res.status(200).json({ success: true, data: position });
|
|
20
|
+
} catch (error) {
|
|
21
|
+
next(error);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const createPosition = async (req, res, next) => {
|
|
26
|
+
try {
|
|
27
|
+
const position = await Position.create(req.body);
|
|
28
|
+
res.status(201).json({ success: true, data: position });
|
|
29
|
+
} catch (error) {
|
|
30
|
+
next(error);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const updatePosition = async (req, res, next) => {
|
|
35
|
+
try {
|
|
36
|
+
const position = await Position.findByIdAndUpdate(req.params.id, req.body, {
|
|
37
|
+
new: true,
|
|
38
|
+
runValidators: true,
|
|
39
|
+
});
|
|
40
|
+
if (!position) {
|
|
41
|
+
return res.status(404).json({ success: false, message: 'Position not found' });
|
|
42
|
+
}
|
|
43
|
+
res.status(200).json({ success: true, data: position });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
next(error);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const deletePosition = async (req, res, next) => {
|
|
50
|
+
try {
|
|
51
|
+
const count = await Employee.countDocuments({ position: req.params.id });
|
|
52
|
+
if (count > 0) {
|
|
53
|
+
return res.status(400).json({
|
|
54
|
+
success: false,
|
|
55
|
+
message: `Cannot delete position with ${count} assigned employee(s)`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const position = await Position.findByIdAndDelete(req.params.id);
|
|
59
|
+
if (!position) {
|
|
60
|
+
return res.status(404).json({ success: false, message: 'Position not found' });
|
|
61
|
+
}
|
|
62
|
+
res.status(200).json({ success: true, message: 'Position deleted successfully' });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
next(error);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import User from '../models/User.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @desc Protect routes - Verify JWT token and attach fully populated user to request
|
|
6
|
+
*/
|
|
7
|
+
export const protect = async (req, res, next) => {
|
|
8
|
+
let token;
|
|
9
|
+
|
|
10
|
+
// 1. Check for token in the Authorization header
|
|
11
|
+
if (
|
|
12
|
+
req.headers.authorization &&
|
|
13
|
+
req.headers.authorization.startsWith('Bearer')
|
|
14
|
+
) {
|
|
15
|
+
token = req.headers.authorization.split(' ')[1];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 2. Make sure token exists
|
|
19
|
+
if (!token) {
|
|
20
|
+
return res.status(401).json({
|
|
21
|
+
success: false,
|
|
22
|
+
message: 'Not authorized to access this route. Token missing.'
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// 3. Verify token encryption integrity
|
|
28
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
29
|
+
|
|
30
|
+
// 4. Fetch the user identity and deeply populate structural profile links
|
|
31
|
+
// This matches the exact payload configuration of the login controller
|
|
32
|
+
req.user = await User.findById(decoded.id).populate({
|
|
33
|
+
path: 'employee',
|
|
34
|
+
populate: [
|
|
35
|
+
{ path: 'department' },
|
|
36
|
+
{ path: 'position' }
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// 5. Ensure the database document still exists
|
|
41
|
+
if (!req.user) {
|
|
42
|
+
return res.status(401).json({
|
|
43
|
+
success: false,
|
|
44
|
+
message: 'The user belonging to this token no longer exists.'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 6. Grant access to the protected controller route
|
|
49
|
+
next();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Auth middleware verification failure:', error.message);
|
|
52
|
+
return res.status(401).json({
|
|
53
|
+
success: false,
|
|
54
|
+
message: 'Not authorized to access this route. Session expired or invalid.'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const notFound = (req, res, next) => {
|
|
2
|
+
const error = new Error(`Not found - ${req.originalUrl}`);
|
|
3
|
+
res.status(404);
|
|
4
|
+
next(error);
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const errorHandler = (err, req, res, next) => {
|
|
8
|
+
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
|
|
9
|
+
|
|
10
|
+
if (err.name === 'ValidationError') {
|
|
11
|
+
const messages = Object.values(err.errors).map((e) => e.message);
|
|
12
|
+
return res.status(400).json({ success: false, message: messages.join(', ') });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (err.code === 11000) {
|
|
16
|
+
const field = Object.keys(err.keyValue)[0];
|
|
17
|
+
return res.status(400).json({
|
|
18
|
+
success: false,
|
|
19
|
+
message: `${field} already exists.`,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (err.name === 'CastError') {
|
|
24
|
+
return res.status(400).json({ success: false, message: 'Invalid resource ID.' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
res.status(statusCode).json({
|
|
28
|
+
success: false,
|
|
29
|
+
message: err.message || 'Server error',
|
|
30
|
+
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
|
|
31
|
+
});
|
|
32
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { validationResult } from 'express-validator';
|
|
2
|
+
|
|
3
|
+
export const validate = (req, res, next) => {
|
|
4
|
+
const errors = validationResult(req);
|
|
5
|
+
if (!errors.isEmpty()) {
|
|
6
|
+
return res.status(400).json({
|
|
7
|
+
success: false,
|
|
8
|
+
message: 'Validation failed',
|
|
9
|
+
errors: errors.array().map((e) => ({ field: e.path, message: e.msg })),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
next();
|
|
13
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const departmentSchema = new mongoose.Schema(
|
|
4
|
+
{
|
|
5
|
+
departName: {
|
|
6
|
+
type: String,
|
|
7
|
+
required: [true, 'Department name is required'],
|
|
8
|
+
unique: true,
|
|
9
|
+
trim: true,
|
|
10
|
+
},
|
|
11
|
+
description: {
|
|
12
|
+
type: String,
|
|
13
|
+
trim: true,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
{ timestamps: true }
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export default mongoose.model('Department', departmentSchema);
|