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,157 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import toast from 'react-hot-toast';
|
|
3
|
+
import api from '../api/axios';
|
|
4
|
+
import Button from '../components/ui/Button';
|
|
5
|
+
import Input from '../components/ui/Input';
|
|
6
|
+
import Modal, { ModalFooter } from '../components/ui/Modal';
|
|
7
|
+
import { validatePosition } from '../utils/validation';
|
|
8
|
+
|
|
9
|
+
export default function Positions() {
|
|
10
|
+
const [positions, setPositions] = useState([]);
|
|
11
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
12
|
+
const [editing, setEditing] = useState(null);
|
|
13
|
+
const [form, setForm] = useState({ posName: '', requiredQualification: '' });
|
|
14
|
+
const [errors, setErrors] = useState({});
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
|
|
17
|
+
const load = async () => {
|
|
18
|
+
try {
|
|
19
|
+
const { data } = await api.get('/positions');
|
|
20
|
+
setPositions(data.data);
|
|
21
|
+
} catch {
|
|
22
|
+
toast.error('Failed to load structural positions metadata');
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
load();
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const openCreate = () => {
|
|
31
|
+
setEditing(null);
|
|
32
|
+
setForm({ posName: '', requiredQualification: '' });
|
|
33
|
+
setErrors({});
|
|
34
|
+
setModalOpen(true);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const openEdit = (pos) => {
|
|
38
|
+
setEditing(pos);
|
|
39
|
+
setForm({ posName: pos.posName, requiredQualification: pos.requiredQualification });
|
|
40
|
+
setErrors({});
|
|
41
|
+
setModalOpen(true);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleChange = (e) => {
|
|
45
|
+
const { name, value } = e.target;
|
|
46
|
+
setForm((prev) => ({ ...prev, [name]: value }));
|
|
47
|
+
setErrors((prev) => ({ ...prev, [name]: '' }));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleSubmit = async (e) => {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
const validationErrors = validatePosition(form);
|
|
53
|
+
if (Object.keys(validationErrors).length) {
|
|
54
|
+
setErrors(validationErrors);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
setLoading(true);
|
|
58
|
+
try {
|
|
59
|
+
if (editing) {
|
|
60
|
+
await api.put(`/positions/${editing._id}`, form);
|
|
61
|
+
toast.success('Position updated');
|
|
62
|
+
} else {
|
|
63
|
+
await api.post('/positions', form);
|
|
64
|
+
toast.success('Position created');
|
|
65
|
+
}
|
|
66
|
+
setModalOpen(false);
|
|
67
|
+
load();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
toast.error(err.response?.data?.message || 'Operation failed');
|
|
70
|
+
} finally {
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleDelete = async (id) => {
|
|
76
|
+
if (!window.confirm('Delete this position?')) return;
|
|
77
|
+
try {
|
|
78
|
+
await api.delete(`/positions/${id}`);
|
|
79
|
+
toast.success('Position deleted');
|
|
80
|
+
load();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
toast.error(err.response?.data?.message || 'Delete failed');
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div>
|
|
88
|
+
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
89
|
+
<div>
|
|
90
|
+
<h1 className="text-3xl font-bold text-stone-50">Positions</h1>
|
|
91
|
+
<p className="mt-1 text-brand-300">Job titles and required qualifications</p>
|
|
92
|
+
</div>
|
|
93
|
+
<Button onClick={openCreate}>Add position</Button>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
97
|
+
{positions.map((pos) => (
|
|
98
|
+
<div key={pos._id} className="group rounded-2xl bg-card p-6 shadow-sm ring-1 ring-brand-700 hover:ring-brand-500">
|
|
99
|
+
<div className="flex items-start justify-between gap-4">
|
|
100
|
+
<div>
|
|
101
|
+
<h3 className="text-lg font-bold text-stone-50">{pos.posName}</h3>
|
|
102
|
+
<p className="mt-2 text-sm text-brand-300">
|
|
103
|
+
<span className="font-medium text-brand-200">Qualification: </span>
|
|
104
|
+
{pos.requiredQualification}
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="flex shrink-0 gap-2">
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
onClick={() => openEdit(pos)}
|
|
111
|
+
className="rounded-lg px-2 py-1 text-sm font-medium text-brand-300 hover:bg-brand-800 hover:text-stone-100"
|
|
112
|
+
>
|
|
113
|
+
Edit
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={() => handleDelete(pos._id)}
|
|
118
|
+
className="rounded-lg px-2 py-1 text-sm font-medium text-red-400 hover:bg-red-900/30"
|
|
119
|
+
>
|
|
120
|
+
Delete
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{positions.length === 0 && (
|
|
129
|
+
<p className="py-12 text-center text-brand-400 rounded-2xl bg-card ring-1 ring-brand-700">No positions yet</p>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
<Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title={editing ? 'Edit position' : 'Add position'} size="lg">
|
|
133
|
+
<form onSubmit={handleSubmit}>
|
|
134
|
+
<div className="space-y-4">
|
|
135
|
+
<Input label="Position name" name="posName" value={form.posName} onChange={handleChange} error={errors.posName} />
|
|
136
|
+
<div>
|
|
137
|
+
<label className="mb-1.5 block text-sm font-medium text-brand-200">Required qualification</label>
|
|
138
|
+
<textarea
|
|
139
|
+
name="requiredQualification"
|
|
140
|
+
value={form.requiredQualification}
|
|
141
|
+
onChange={handleChange}
|
|
142
|
+
rows={3}
|
|
143
|
+
className={`w-full rounded-xl border bg-brand-900 px-4 py-2.5 text-stone-100 focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/30 ${
|
|
144
|
+
errors.requiredQualification ? 'border-red-500' : 'border-brand-700'
|
|
145
|
+
}`}
|
|
146
|
+
/>
|
|
147
|
+
{errors.requiredQualification && (
|
|
148
|
+
<p className="mt-1 text-sm text-red-400">{errors.requiredQualification}</p>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<ModalFooter onCancel={() => setModalOpen(false)} loading={loading} />
|
|
153
|
+
</form>
|
|
154
|
+
</Modal>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useNavigate, Link } from 'react-router-dom';
|
|
3
|
+
import toast from 'react-hot-toast';
|
|
4
|
+
import { useAuth } from '../context/AuthContext';
|
|
5
|
+
|
|
6
|
+
export default function Register() {
|
|
7
|
+
const [formData, setFormData] = useState({ userName: '', email: '', password: '' });
|
|
8
|
+
const [submitting, setSubmitting] = useState(false);
|
|
9
|
+
const { register } = useAuth();
|
|
10
|
+
const navigate = useNavigate();
|
|
11
|
+
|
|
12
|
+
const handleInputChange = (e) => {
|
|
13
|
+
setFormData({ ...formData, [e.target.name]: e.target.value });
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const handleSubmit = async (e) => {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
const { userName, email, password } = formData;
|
|
19
|
+
if (!userName || !email || !password) {
|
|
20
|
+
return toast.error('All profile initialization properties are required');
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
setSubmitting(true);
|
|
24
|
+
await register(formData);
|
|
25
|
+
toast.success('Registration complete! Please log in.');
|
|
26
|
+
navigate('/login');
|
|
27
|
+
} catch (err) {
|
|
28
|
+
toast.error(err.response?.data?.message || 'Failed to initialize master profile configuration');
|
|
29
|
+
} finally {
|
|
30
|
+
setSubmitting(false);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex min-h-screen items-center justify-center bg-gradient-to-b from-brand-950 via-brand-900 to-brand-950 px-4 py-10">
|
|
36
|
+
<div className="w-full max-w-md rounded-2xl border border-brand-700/40 bg-card p-8 shadow-2xl shadow-black/30">
|
|
37
|
+
<p className="text-center text-xs font-semibold uppercase tracking-widest text-brand-400">
|
|
38
|
+
DAB Enterprise HRMS
|
|
39
|
+
</p>
|
|
40
|
+
<h1 className="mt-3 text-center text-2xl font-bold text-white">Initialize Administrator</h1>
|
|
41
|
+
<p className="mt-2 text-center text-sm text-slate-400">Configure system administrative profile node</p>
|
|
42
|
+
|
|
43
|
+
<form onSubmit={handleSubmit} className="mt-8 space-y-5">
|
|
44
|
+
<div>
|
|
45
|
+
<label className="block text-xs font-semibold uppercase tracking-wider text-slate-300">Username</label>
|
|
46
|
+
<input
|
|
47
|
+
type="text"
|
|
48
|
+
name="userName"
|
|
49
|
+
value={formData.userName}
|
|
50
|
+
onChange={handleInputChange}
|
|
51
|
+
className="mt-2 w-full rounded-lg border border-slate-800 bg-slate-900/50 px-4 py-2.5 text-white focus:border-brand-500 focus:outline-none"
|
|
52
|
+
placeholder="master_admin"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<label className="block text-xs font-semibold uppercase tracking-wider text-slate-300">Email Address</label>
|
|
57
|
+
<input
|
|
58
|
+
type="email"
|
|
59
|
+
name="email"
|
|
60
|
+
value={formData.email}
|
|
61
|
+
onChange={handleInputChange}
|
|
62
|
+
className="mt-2 w-full rounded-lg border border-slate-800 bg-slate-900/50 px-4 py-2.5 text-white focus:border-brand-500 focus:outline-none"
|
|
63
|
+
placeholder="admin@enterprise.com"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
<div>
|
|
67
|
+
<label className="block text-xs font-semibold uppercase tracking-wider text-slate-300">Password</label>
|
|
68
|
+
<input
|
|
69
|
+
type="password"
|
|
70
|
+
name="password"
|
|
71
|
+
value={formData.password}
|
|
72
|
+
onChange={handleInputChange}
|
|
73
|
+
className="mt-2 w-full rounded-lg border border-slate-800 bg-slate-900/50 px-4 py-2.5 text-white focus:border-brand-500 focus:outline-none"
|
|
74
|
+
placeholder="••••••••"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
<button
|
|
78
|
+
type="submit"
|
|
79
|
+
disabled={submitting}
|
|
80
|
+
className="w-full rounded-lg bg-brand-600 py-3 text-sm font-semibold text-white transition-colors duration-200 hover:bg-brand-500 disabled:opacity-50"
|
|
81
|
+
>
|
|
82
|
+
{submitting ? 'Configuring System...' : 'Create Account'}
|
|
83
|
+
</button>
|
|
84
|
+
</form>
|
|
85
|
+
|
|
86
|
+
<p className="mt-6 text-center text-xs text-slate-400">
|
|
87
|
+
Already registered?{' '}
|
|
88
|
+
<Link to="/login" className="font-semibold text-brand-400 hover:underline">Log In</Link>
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import toast from 'react-hot-toast';
|
|
4
|
+
import { useAuth } from '../context/AuthContext';
|
|
5
|
+
import Input from '../components/ui/Input';
|
|
6
|
+
import Button from '../components/ui/Button';
|
|
7
|
+
import { validateEmail } from '../utils/validation';
|
|
8
|
+
|
|
9
|
+
export default function ResetPassword() {
|
|
10
|
+
const [form, setForm] = useState({
|
|
11
|
+
email: '',
|
|
12
|
+
password: '',
|
|
13
|
+
confirmPassword: '',
|
|
14
|
+
});
|
|
15
|
+
const [errors, setErrors] = useState({});
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
const [result, setResult] = useState(null);
|
|
18
|
+
const { resetPassword } = useAuth();
|
|
19
|
+
|
|
20
|
+
const handleChange = (e) => {
|
|
21
|
+
const { name, value } = e.target;
|
|
22
|
+
setForm((prev) => ({ ...prev, [name]: value }));
|
|
23
|
+
setErrors((prev) => ({ ...prev, [name]: '' }));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleSubmit = async (e) => {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
const localErrors = {};
|
|
29
|
+
|
|
30
|
+
// 1. Email Format Validation
|
|
31
|
+
const emailError = validateEmail(form.email);
|
|
32
|
+
if (emailError) localErrors.email = emailError;
|
|
33
|
+
|
|
34
|
+
// 2. Password Completeness Validations
|
|
35
|
+
if (!form.password) {
|
|
36
|
+
localErrors.password = 'Password is required';
|
|
37
|
+
} else if (form.password.length < 6) {
|
|
38
|
+
localErrors.password = 'Password must be at least 6 characters';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (form.password !== form.confirmPassword) {
|
|
42
|
+
localErrors.confirmPassword = 'Passwords do not match';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Object.keys(localErrors).length > 0) {
|
|
46
|
+
setErrors(localErrors);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setErrors({});
|
|
51
|
+
setLoading(true);
|
|
52
|
+
try {
|
|
53
|
+
// Sends email along with structural configuration variables to auth endpoints
|
|
54
|
+
const responseData = await resetPassword({
|
|
55
|
+
email: form.email.trim().toLowerCase(),
|
|
56
|
+
password: form.password,
|
|
57
|
+
confirmPassword: form.confirmPassword,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
setResult(responseData || { message: 'Password has been adjusted cleanly.' });
|
|
61
|
+
toast.success('Password updated successfully');
|
|
62
|
+
} catch (err) {
|
|
63
|
+
toast.error(err.response?.data?.message || 'Failed to authorize password modification rules');
|
|
64
|
+
} finally {
|
|
65
|
+
setLoading(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="flex min-h-screen items-center justify-center bg-gradient-to-b from-brand-950 via-brand-900 to-brand-950 px-4 py-10">
|
|
71
|
+
<div className="w-full max-w-md rounded-2xl border border-brand-700/40 bg-card p-8 shadow-2xl shadow-black/30">
|
|
72
|
+
<p className="text-center text-xs font-semibold uppercase tracking-widest text-brand-400">
|
|
73
|
+
DAB Enterprise HRMS
|
|
74
|
+
</p>
|
|
75
|
+
<h1 className="mt-3 text-center text-2xl font-bold text-white">Reset password</h1>
|
|
76
|
+
<p className="mt-2 text-center text-sm text-slate-400">Update your account credentials</p>
|
|
77
|
+
|
|
78
|
+
<div className="mt-4 mb-4">
|
|
79
|
+
<Link to="/login" className="text-sm font-medium text-brand-400 hover:text-white">
|
|
80
|
+
← Back to sign in
|
|
81
|
+
</Link>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{result ? (
|
|
85
|
+
<div className="mt-6 rounded-lg border border-brand-700/60 bg-brand-950/50 p-4 text-sm text-slate-300">
|
|
86
|
+
<p className="font-semibold text-white">Password Change Complete</p>
|
|
87
|
+
<p className="mt-2 text-slate-400">{result.message || 'You can now sign in using your new credentials.'}</p>
|
|
88
|
+
<Link to="/login" className="mt-5 inline-block w-full">
|
|
89
|
+
<Button className="w-full">Go to sign in</Button>
|
|
90
|
+
</Link>
|
|
91
|
+
</div>
|
|
92
|
+
) : (
|
|
93
|
+
<form onSubmit={handleSubmit} className="mt-6 space-y-4" noValidate autoComplete="off">
|
|
94
|
+
<Input
|
|
95
|
+
label="Employee email"
|
|
96
|
+
name="email"
|
|
97
|
+
type="email"
|
|
98
|
+
value={form.email}
|
|
99
|
+
onChange={handleChange}
|
|
100
|
+
error={errors.email}
|
|
101
|
+
placeholder="you@dabenterprise.rw"
|
|
102
|
+
autoComplete="off"
|
|
103
|
+
/>
|
|
104
|
+
|
|
105
|
+
<Input
|
|
106
|
+
label="New Password"
|
|
107
|
+
name="password"
|
|
108
|
+
type="password"
|
|
109
|
+
value={form.password}
|
|
110
|
+
onChange={handleChange}
|
|
111
|
+
error={errors.password}
|
|
112
|
+
placeholder="••••••••"
|
|
113
|
+
autoComplete="new-password"
|
|
114
|
+
/>
|
|
115
|
+
|
|
116
|
+
<Input
|
|
117
|
+
label="Confirm New Password"
|
|
118
|
+
name="confirmPassword"
|
|
119
|
+
type="password"
|
|
120
|
+
value={form.confirmPassword}
|
|
121
|
+
onChange={handleChange}
|
|
122
|
+
error={errors.confirmPassword}
|
|
123
|
+
placeholder="••••••••"
|
|
124
|
+
autoComplete="new-password"
|
|
125
|
+
/>
|
|
126
|
+
|
|
127
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
128
|
+
{loading ? 'Processing Reset...' : 'Update Password'}
|
|
129
|
+
</Button>
|
|
130
|
+
</form>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isAdmin = (user) => user?.role === 'admin' || user?.userName === 'admin';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const validateEmail = (email) => {
|
|
2
|
+
if (!email?.trim()) return 'Email is required';
|
|
3
|
+
if (!/^\S+@\S+\.\S+$/.test(email)) return 'Invalid email format';
|
|
4
|
+
return '';
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const validateRequired = (value, fieldName) => {
|
|
8
|
+
if (!value?.toString().trim()) return `${fieldName} is required`;
|
|
9
|
+
return '';
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const validateLogin = ({ userName, password }) => {
|
|
13
|
+
const errors = {};
|
|
14
|
+
if (!userName?.trim()) errors.userName = 'Username is required';
|
|
15
|
+
else if (userName.length < 3) errors.userName = 'Username must be at least 3 characters';
|
|
16
|
+
else if (!/^[a-zA-Z0-9_]+$/.test(userName)) errors.userName = 'Only letters, numbers, and underscores';
|
|
17
|
+
if (!password) errors.password = 'Password is required';
|
|
18
|
+
return errors;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const validateRegister = (form) => {
|
|
22
|
+
const errors = {};
|
|
23
|
+
if (!form.empFirstName?.trim()) errors.empFirstName = 'First name is required';
|
|
24
|
+
if (!form.empLastName?.trim()) errors.empLastName = 'Last name is required';
|
|
25
|
+
const emailErr = validateEmail(form.empEmail);
|
|
26
|
+
if (emailErr) errors.empEmail = emailErr;
|
|
27
|
+
if (!form.empTelephone?.trim()) errors.empTelephone = 'Telephone is required';
|
|
28
|
+
if (!form.userName?.trim()) errors.userName = 'Username is required';
|
|
29
|
+
else if (form.userName.length < 3) errors.userName = 'Username must be at least 3 characters';
|
|
30
|
+
else if (!/^[a-zA-Z0-9_]+$/.test(form.userName)) errors.userName = 'Only letters, numbers, and underscores';
|
|
31
|
+
if (!form.password) errors.password = 'Password is required';
|
|
32
|
+
else if (form.password.length < 6) errors.password = 'Password must be at least 6 characters';
|
|
33
|
+
if (!form.confirmPassword) errors.confirmPassword = 'Please confirm your password';
|
|
34
|
+
else if (form.password !== form.confirmPassword) errors.confirmPassword = 'Passwords do not match';
|
|
35
|
+
return errors;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const validateEmployee = (form) => {
|
|
39
|
+
const errors = {};
|
|
40
|
+
if (!form.empFirstName?.trim()) errors.empFirstName = 'First name is required';
|
|
41
|
+
if (!form.empLastName?.trim()) errors.empLastName = 'Last name is required';
|
|
42
|
+
const emailErr = validateEmail(form.empEmail);
|
|
43
|
+
if (emailErr) errors.empEmail = emailErr;
|
|
44
|
+
if (!form.empTelephone?.trim()) errors.empTelephone = 'Telephone is required';
|
|
45
|
+
if (!form.empGender) errors.empGender = 'Gender is required';
|
|
46
|
+
if (!form.empAddress?.trim()) errors.empAddress = 'Address is required';
|
|
47
|
+
if (!form.empDateOfBirth) errors.empDateOfBirth = 'Date of birth is required';
|
|
48
|
+
if (!form.empHireDate) errors.empHireDate = 'Hire date is required';
|
|
49
|
+
if (!form.empStatus) errors.empStatus = 'Status is required';
|
|
50
|
+
if (!form.department) errors.department = 'Department is required';
|
|
51
|
+
if (!form.position) errors.position = 'Position is required';
|
|
52
|
+
return errors;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const validateDepartment = (form) => {
|
|
56
|
+
const errors = {};
|
|
57
|
+
if (!form.departName?.trim()) errors.departName = 'Department name is required';
|
|
58
|
+
return errors;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const validatePosition = (form) => {
|
|
62
|
+
const errors = {};
|
|
63
|
+
if (!form.posName?.trim()) errors.posName = 'Position name is required';
|
|
64
|
+
if (!form.requiredQualification?.trim()) errors.requiredQualification = 'Qualification is required';
|
|
65
|
+
return errors;
|
|
66
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react(), tailwindcss()],
|
|
7
|
+
server: {
|
|
8
|
+
port: 5173,
|
|
9
|
+
proxy: {
|
|
10
|
+
'/api': {
|
|
11
|
+
target: 'http://localhost:5000',
|
|
12
|
+
changeOrigin: true,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const mysql = require('mysql2');
|
|
2
|
+
|
|
3
|
+
const pool = mysql.createPool({
|
|
4
|
+
host: process.env.DB_HOST || 'localhost',
|
|
5
|
+
user: process.env.DB_USER || 'root',
|
|
6
|
+
password: process.env.DB_PASSWORD || '',
|
|
7
|
+
database: process.env.DB_NAME || 'HRMS',
|
|
8
|
+
waitForConnections: true,
|
|
9
|
+
connectionLimit: 10,
|
|
10
|
+
queueLimit: 0
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
module.exports = pool.promise();
|