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.
Files changed (88) hide show
  1. package/bin/cli.js +82 -0
  2. package/package.json +25 -0
  3. package/templates/HRMS_Mongodb/README.md +331 -0
  4. package/templates/HRMS_Mongodb/backend/.env.example +6 -0
  5. package/templates/HRMS_Mongodb/backend/package-lock.json +1646 -0
  6. package/templates/HRMS_Mongodb/backend/package.json +26 -0
  7. package/templates/HRMS_Mongodb/backend/src/config/db.js +9 -0
  8. package/templates/HRMS_Mongodb/backend/src/controllers/authController.js +187 -0
  9. package/templates/HRMS_Mongodb/backend/src/controllers/departmentController.js +70 -0
  10. package/templates/HRMS_Mongodb/backend/src/controllers/employeeController.js +178 -0
  11. package/templates/HRMS_Mongodb/backend/src/controllers/positionController.js +66 -0
  12. package/templates/HRMS_Mongodb/backend/src/middleware/auth.js +57 -0
  13. package/templates/HRMS_Mongodb/backend/src/middleware/errorHandler.js +32 -0
  14. package/templates/HRMS_Mongodb/backend/src/middleware/restrictToAdmin.js +5 -0
  15. package/templates/HRMS_Mongodb/backend/src/middleware/validate.js +13 -0
  16. package/templates/HRMS_Mongodb/backend/src/models/Department.js +19 -0
  17. package/templates/HRMS_Mongodb/backend/src/models/Employee.js +81 -0
  18. package/templates/HRMS_Mongodb/backend/src/models/Position.js +19 -0
  19. package/templates/HRMS_Mongodb/backend/src/models/User.js +40 -0
  20. package/templates/HRMS_Mongodb/backend/src/routes/authRoutes.js +27 -0
  21. package/templates/HRMS_Mongodb/backend/src/routes/departmentRoutes.js +33 -0
  22. package/templates/HRMS_Mongodb/backend/src/routes/employeeRoutes.js +39 -0
  23. package/templates/HRMS_Mongodb/backend/src/routes/positionRoutes.js +32 -0
  24. package/templates/HRMS_Mongodb/backend/src/server.js +74 -0
  25. package/templates/HRMS_Mongodb/backend/src/utils/roles.js +5 -0
  26. package/templates/HRMS_Mongodb/backend/src/utils/seed.js +78 -0
  27. package/templates/HRMS_Mongodb/backend/src/validators/authValidator.js +61 -0
  28. package/templates/HRMS_Mongodb/backend/src/validators/departmentValidator.js +21 -0
  29. package/templates/HRMS_Mongodb/backend/src/validators/employeeValidator.js +27 -0
  30. package/templates/HRMS_Mongodb/backend/src/validators/positionValidator.js +26 -0
  31. package/templates/HRMS_Mongodb/frontend/index.html +19 -0
  32. package/templates/HRMS_Mongodb/frontend/package-lock.json +2812 -0
  33. package/templates/HRMS_Mongodb/frontend/package.json +25 -0
  34. package/templates/HRMS_Mongodb/frontend/public/favicon.svg +4 -0
  35. package/templates/HRMS_Mongodb/frontend/src/App.jsx +50 -0
  36. package/templates/HRMS_Mongodb/frontend/src/api/axios.js +54 -0
  37. package/templates/HRMS_Mongodb/frontend/src/components/ProtectedRoute.jsx +26 -0
  38. package/templates/HRMS_Mongodb/frontend/src/components/layout/DashboardLayout.jsx +16 -0
  39. package/templates/HRMS_Mongodb/frontend/src/components/layout/Sidebar.jsx +108 -0
  40. package/templates/HRMS_Mongodb/frontend/src/components/ui/Button.jsx +33 -0
  41. package/templates/HRMS_Mongodb/frontend/src/components/ui/Input.jsx +20 -0
  42. package/templates/HRMS_Mongodb/frontend/src/components/ui/Modal.jsx +48 -0
  43. package/templates/HRMS_Mongodb/frontend/src/components/ui/Select.jsx +27 -0
  44. package/templates/HRMS_Mongodb/frontend/src/context/AuthContext.jsx +97 -0
  45. package/templates/HRMS_Mongodb/frontend/src/index.css +34 -0
  46. package/templates/HRMS_Mongodb/frontend/src/main.jsx +16 -0
  47. package/templates/HRMS_Mongodb/frontend/src/pages/Dashboard.jsx +78 -0
  48. package/templates/HRMS_Mongodb/frontend/src/pages/Departments.jsx +144 -0
  49. package/templates/HRMS_Mongodb/frontend/src/pages/Employees.jsx +297 -0
  50. package/templates/HRMS_Mongodb/frontend/src/pages/LeaveReport.jsx +113 -0
  51. package/templates/HRMS_Mongodb/frontend/src/pages/Login.jsx +92 -0
  52. package/templates/HRMS_Mongodb/frontend/src/pages/Positions.jsx +157 -0
  53. package/templates/HRMS_Mongodb/frontend/src/pages/Register.jsx +93 -0
  54. package/templates/HRMS_Mongodb/frontend/src/pages/ResetPassword.jsx +135 -0
  55. package/templates/HRMS_Mongodb/frontend/src/utils/roles.js +1 -0
  56. package/templates/HRMS_Mongodb/frontend/src/utils/session.js +5 -0
  57. package/templates/HRMS_Mongodb/frontend/src/utils/validation.js +66 -0
  58. package/templates/HRMS_Mongodb/frontend/vite.config.js +16 -0
  59. package/templates/HRMS_Mysql/backend/db.js +13 -0
  60. package/templates/HRMS_Mysql/backend/package-lock.json +1614 -0
  61. package/templates/HRMS_Mysql/backend/package.json +21 -0
  62. package/templates/HRMS_Mysql/backend/server.js +421 -0
  63. package/templates/HRMS_Mysql/frontend/dist/assets/index-CtLtQf3_.js +75 -0
  64. package/templates/HRMS_Mysql/frontend/dist/assets/index-Dq1AXlEY.css +1 -0
  65. package/templates/HRMS_Mysql/frontend/dist/index.html +14 -0
  66. package/templates/HRMS_Mysql/frontend/dist/vite.svg +1 -0
  67. package/templates/HRMS_Mysql/frontend/index.html +13 -0
  68. package/templates/HRMS_Mysql/frontend/package-lock.json +2978 -0
  69. package/templates/HRMS_Mysql/frontend/package.json +25 -0
  70. package/templates/HRMS_Mysql/frontend/postcss.config.js +6 -0
  71. package/templates/HRMS_Mysql/frontend/public/vite.svg +1 -0
  72. package/templates/HRMS_Mysql/frontend/src/App.jsx +55 -0
  73. package/templates/HRMS_Mysql/frontend/src/api.js +11 -0
  74. package/templates/HRMS_Mysql/frontend/src/components/Layout.jsx +59 -0
  75. package/templates/HRMS_Mysql/frontend/src/index.css +7 -0
  76. package/templates/HRMS_Mysql/frontend/src/main.jsx +13 -0
  77. package/templates/HRMS_Mysql/frontend/src/pages/Dashboard.jsx +45 -0
  78. package/templates/HRMS_Mysql/frontend/src/pages/Departments.jsx +108 -0
  79. package/templates/HRMS_Mysql/frontend/src/pages/EmployeeStatusReport.jsx +72 -0
  80. package/templates/HRMS_Mysql/frontend/src/pages/Employees.jsx +252 -0
  81. package/templates/HRMS_Mysql/frontend/src/pages/ForgotPassword.jsx +66 -0
  82. package/templates/HRMS_Mysql/frontend/src/pages/Login.jsx +79 -0
  83. package/templates/HRMS_Mysql/frontend/src/pages/Positions.jsx +109 -0
  84. package/templates/HRMS_Mysql/frontend/src/pages/Register.jsx +95 -0
  85. package/templates/HRMS_Mysql/frontend/src/pages/Users.jsx +133 -0
  86. package/templates/HRMS_Mysql/frontend/tailwind.config.js +26 -0
  87. package/templates/HRMS_Mysql/frontend/vite.config.js +15 -0
  88. package/templates/HRMS_Mysql/hrms_schema.sql +57 -0
@@ -0,0 +1,144 @@
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 { validateDepartment } from '../utils/validation';
8
+
9
+ export default function Departments() {
10
+ const [departments, setDepartments] = useState([]);
11
+ const [modalOpen, setModalOpen] = useState(false);
12
+ const [editing, setEditing] = useState(null);
13
+ const [form, setForm] = useState({ departName: '' });
14
+ const [errors, setErrors] = useState({});
15
+ const [loading, setLoading] = useState(false);
16
+
17
+ const load = async () => {
18
+ try {
19
+ const { data } = await api.get('/departments');
20
+ setDepartments(data.data);
21
+ } catch {
22
+ toast.error('Failed to load departments roster details');
23
+ }
24
+ };
25
+
26
+ useEffect(() => {
27
+ load();
28
+ }, []);
29
+
30
+ const openCreate = () => {
31
+ setEditing(null);
32
+ setForm({ departName: '' });
33
+ setErrors({});
34
+ setModalOpen(true);
35
+ };
36
+
37
+ const openEdit = (dept) => {
38
+ setEditing(dept);
39
+ setForm({ departName: dept.departName });
40
+ setErrors({});
41
+ setModalOpen(true);
42
+ };
43
+
44
+ const handleSubmit = async (e) => {
45
+ e.preventDefault();
46
+ const validationErrors = validateDepartment(form);
47
+ if (Object.keys(validationErrors).length) {
48
+ setErrors(validationErrors);
49
+ return;
50
+ }
51
+ setLoading(true);
52
+ try {
53
+ if (editing) {
54
+ await api.put(`/departments/${editing._id}`, form);
55
+ toast.success('Department updated');
56
+ } else {
57
+ await api.post('/departments', form);
58
+ toast.success('Department created');
59
+ }
60
+ setModalOpen(false);
61
+ load();
62
+ } catch (err) {
63
+ toast.error(err.response?.data?.message || 'Operation failed');
64
+ } finally {
65
+ setLoading(false);
66
+ }
67
+ };
68
+
69
+ const handleDelete = async (id) => {
70
+ if (!window.confirm('Delete this department?')) return;
71
+ try {
72
+ await api.delete(`/departments/${id}`);
73
+ toast.success('Department deleted');
74
+ load();
75
+ } catch (err) {
76
+ toast.error(err.response?.data?.message || 'Delete failed');
77
+ }
78
+ };
79
+
80
+ return (
81
+ <div>
82
+ <div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
83
+ <div>
84
+ <h1 className="text-3xl font-bold text-stone-50">Departments</h1>
85
+ <p className="mt-1 text-brand-300">Organize employees by department clusters</p>
86
+ </div>
87
+ <Button onClick={openCreate}>Add department</Button>
88
+ </div>
89
+
90
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
91
+ {departments.map((dept) => (
92
+ <div
93
+ key={dept._id}
94
+ className="group rounded-2xl bg-card p-6 shadow-sm ring-1 ring-brand-700 transition hover:ring-brand-500"
95
+ >
96
+ <div className="flex items-start justify-between">
97
+ <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-brand-800 text-brand-200 text-lg font-bold">
98
+ {dept.departName.charAt(0)}
99
+ </div>
100
+ <div className="flex gap-2">
101
+ <button
102
+ type="button"
103
+ onClick={() => openEdit(dept)}
104
+ className="rounded-lg px-2 py-1 text-sm font-medium text-brand-300 hover:bg-brand-800 hover:text-stone-100"
105
+ >
106
+ Edit
107
+ </button>
108
+ <button
109
+ type="button"
110
+ onClick={() => handleDelete(dept._id)}
111
+ className="rounded-lg px-2 py-1 text-sm font-medium text-red-400 hover:bg-red-900/30"
112
+ >
113
+ Delete
114
+ </button>
115
+ </div>
116
+ </div>
117
+ <h3 className="mt-4 text-lg font-bold text-stone-50">{dept.departName}</h3>
118
+ </div>
119
+ ))}
120
+ </div>
121
+
122
+ {departments.length === 0 && (
123
+ <p className="py-12 text-center text-brand-400 rounded-2xl bg-card ring-1 ring-brand-700">No departments yet</p>
124
+ )}
125
+
126
+ <Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title={editing ? 'Edit department' : 'Add department'}>
127
+ <form onSubmit={handleSubmit}>
128
+ <Input
129
+ label="Department name"
130
+ name="departName"
131
+ value={form.departName}
132
+ onChange={(e) => {
133
+ setForm({ departName: e.target.value });
134
+ setErrors({});
135
+ }}
136
+ error={errors.departName}
137
+ placeholder="e.g. Sales"
138
+ />
139
+ <ModalFooter onCancel={() => setModalOpen(false)} loading={loading} />
140
+ </form>
141
+ </Modal>
142
+ </div>
143
+ );
144
+ }
@@ -0,0 +1,297 @@
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 Select from '../components/ui/Select';
7
+ import Modal, { ModalFooter } from '../components/ui/Modal';
8
+ import { validateEmployee } from '../utils/validation';
9
+
10
+ const emptyForm = {
11
+ empFirstName: '',
12
+ empLastName: '',
13
+ empEmail: '',
14
+ empTelephone: '',
15
+ empGender: '',
16
+ empAddress: '',
17
+ empDateOfBirth: '',
18
+ empHireDate: '',
19
+ empStatus: 'Active',
20
+ department: '',
21
+ position: '',
22
+ };
23
+
24
+ // Complete Operational Status Alignment Matrices
25
+ const statusOptions = [
26
+ { value: 'Active', label: 'Active' },
27
+ { value: 'On Leave', label: 'On Leave' },
28
+ { value: 'Left', label: 'Left' },
29
+ { value: 'Blacklisted', label: 'Blacklisted' },
30
+ { value: 'Deceased', label: 'Deceased' },
31
+ { value: 'On Mission', label: 'On Mission' },
32
+ ];
33
+
34
+ const genderOptions = [
35
+ { value: 'Male', label: 'Male' },
36
+ { value: 'Female', label: 'Female' },
37
+ { value: 'Other', label: 'Other' },
38
+ ];
39
+
40
+ export default function Employees() {
41
+ const [employees, setEmployees] = useState([]);
42
+ const [departments, setDepartments] = useState([]);
43
+ const [positions, setPositions] = useState([]);
44
+ const [search, setSearch] = useState('');
45
+ const [modalOpen, setModalOpen] = useState(false);
46
+ const [editing, setEditing] = useState(null);
47
+ const [form, setForm] = useState(emptyForm);
48
+ const [errors, setErrors] = useState({});
49
+ const [loading, setLoading] = useState(false);
50
+
51
+ const loadData = async () => {
52
+ try {
53
+ const token = localStorage.getItem('token');
54
+ const headers = token ? { Authorization: `Bearer ${token}` } : {};
55
+
56
+ const [empRes, deptRes, posRes] = await Promise.all([
57
+ api.get('/employees', { headers }).catch(() => ({ data: { data: [] } })),
58
+ api.get('/departments', { headers }).catch(() => ({ data: { data: [] } })),
59
+ api.get('/positions', { headers }).catch(() => ({ data: { data: [] } }))
60
+ ]);
61
+
62
+ setEmployees(empRes.data?.data || empRes.data || []);
63
+ setDepartments(deptRes.data?.data || deptRes.data || []);
64
+ setPositions(posRes.data?.data || posRes.data || []);
65
+ } catch (err) {
66
+ console.error('Data load failure:', err);
67
+ toast.error('Failed to communicate cleanly with core data models');
68
+ }
69
+ };
70
+
71
+ useEffect(() => {
72
+ loadData();
73
+ }, []);
74
+
75
+ const openCreate = () => {
76
+ setEditing(null);
77
+ setForm(emptyForm);
78
+ setErrors({});
79
+ setModalOpen(true);
80
+ };
81
+
82
+ const openEdit = (emp) => {
83
+ setEditing(emp);
84
+ setForm({
85
+ empFirstName: emp.empFirstName || '',
86
+ empLastName: emp.empLastName || '',
87
+ empEmail: emp.empEmail || '',
88
+ empTelephone: emp.empTelephone || '',
89
+ empGender: emp.empGender || '',
90
+ empAddress: emp.empAddress || '',
91
+ empDateOfBirth: emp.empDateOfBirth?.slice(0, 10) || '',
92
+ empHireDate: emp.empHireDate?.slice(0, 10) || '',
93
+ empStatus: emp.empStatus || 'Active',
94
+ department: emp.department?._id || emp.department || '',
95
+ position: emp.position?._id || emp.position || '',
96
+ });
97
+ setErrors({});
98
+ setModalOpen(true);
99
+ };
100
+
101
+ const handleChange = (e) => {
102
+ const { name, value } = e.target;
103
+ setForm((prev) => ({ ...prev, [name]: value }));
104
+ setErrors((prev) => ({ ...prev, [name]: '' }));
105
+ };
106
+
107
+ const handleSubmit = async (e) => {
108
+ e.preventDefault();
109
+
110
+ // Check client-side form configurations
111
+ const validationErrors = validateEmployee(form);
112
+ if (Object.keys(validationErrors).length) {
113
+ setErrors(validationErrors);
114
+ return;
115
+ }
116
+
117
+ setLoading(true);
118
+ try {
119
+ if (editing) {
120
+ await api.put(`/employees/${editing._id}`, form);
121
+ toast.success('Employee record modified successfully');
122
+ } else {
123
+ await api.post('/employees', form);
124
+ toast.success('Employee profile initialized');
125
+ }
126
+ setModalOpen(false);
127
+ loadData();
128
+ } catch (err) {
129
+ console.error('Submission failure error trace:', err);
130
+
131
+ // Dynamic Server Error Unpacker Logic
132
+ if (err.response?.data?.errors && Array.isArray(err.response.data.errors)) {
133
+ // Extract messages generated directly from express-validator arrays
134
+ err.response.data.errors.forEach((validationErr) => {
135
+ toast.error(`${validationErr.path || 'Field'}: ${validationErr.msg}`);
136
+ });
137
+ } else {
138
+ const msg = err.response?.data?.message || 'Operation failed running profile commit';
139
+ toast.error(msg);
140
+ }
141
+ } finally {
142
+ setLoading(false);
143
+ }
144
+ };
145
+
146
+ const handleDelete = async (id) => {
147
+ if (!window.confirm('Delete this employee directory entry permanently?')) return;
148
+ try {
149
+ await api.delete(`/employees/${id}`);
150
+ toast.success('Employee record dropped');
151
+ loadData();
152
+ } catch (err) {
153
+ toast.error(err.response?.data?.message || 'Delete operation dropped');
154
+ }
155
+ };
156
+
157
+ const filtered = employees.filter((e) => {
158
+ const q = search.toLowerCase();
159
+ const name = `${e.empFirstName || ''} ${e.empLastName || ''}`.toLowerCase();
160
+ return name.includes(q) || e.empEmail?.toLowerCase().includes(q);
161
+ });
162
+
163
+ const getStatusBadgeClass = (status) => {
164
+ switch (status) {
165
+ case 'Active':
166
+ return 'bg-green-500/10 text-green-400 ring-1 ring-green-500/20';
167
+ case 'On Leave':
168
+ return 'bg-amber-500/10 text-amber-400 ring-1 ring-amber-500/20';
169
+ case 'On Mission':
170
+ return 'bg-blue-500/10 text-blue-400 ring-1 ring-blue-500/20';
171
+ case 'Left':
172
+ return 'bg-slate-500/10 text-slate-400 ring-1 ring-slate-500/20';
173
+ case 'Blacklisted':
174
+ return 'bg-red-500/10 text-red-500 ring-1 ring-red-500/30 font-bold uppercase';
175
+ case 'Deceased':
176
+ return 'bg-purple-500/10 text-purple-400 ring-1 ring-purple-500/20';
177
+ default:
178
+ return 'bg-zinc-500/10 text-zinc-400 ring-1 ring-zinc-500/20';
179
+ }
180
+ };
181
+
182
+ return (
183
+ <div>
184
+ <div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
185
+ <div>
186
+ <h1 className="text-2xl font-bold text-white">Employees</h1>
187
+ <p className="mt-1 text-slate-400">Manage organizational employee records</p>
188
+ </div>
189
+ <Button onClick={openCreate}>Add employee</Button>
190
+ </div>
191
+
192
+ <div className="mb-6 max-w-md">
193
+ <input
194
+ type="search"
195
+ placeholder="Search by name or email..."
196
+ value={search}
197
+ onChange={(e) => setSearch(e.target.value)}
198
+ className="w-full rounded-xl border border-brand-700 bg-brand-900 py-2.5 px-4 text-stone-100 placeholder:text-brand-400 focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/30"
199
+ />
200
+ </div>
201
+
202
+ <div className="overflow-hidden rounded-xl bg-card ring-1 ring-brand-800">
203
+ <table className="w-full text-left text-sm">
204
+ <thead className="border-b border-brand-700 bg-brand-900/50">
205
+ <tr>
206
+ <th className="px-6 py-4 font-semibold text-brand-200">Name</th>
207
+ <th className="px-6 py-4 font-semibold text-brand-200">Email</th>
208
+ <th className="px-6 py-4 font-semibold text-brand-200">Department</th>
209
+ <th className="px-6 py-4 font-semibold text-brand-200">Position</th>
210
+ <th className="px-6 py-4 font-semibold text-brand-200">Status</th>
211
+ <th className="px-6 py-4 font-semibold text-brand-200">Actions</th>
212
+ </tr>
213
+ </thead>
214
+ <tbody className="divide-y divide-brand-800">
215
+ {filtered.map((emp) => (
216
+ <tr key={emp._id} className="hover:bg-brand-900/30">
217
+ <td className="px-6 py-4 font-medium text-stone-100">
218
+ {emp.empFirstName} {emp.empLastName}
219
+ </td>
220
+ <td className="px-6 py-4 text-brand-300">{emp.empEmail}</td>
221
+ <td className="px-6 py-4">{emp.department?.departName || '—'}</td>
222
+ <td className="px-6 py-4">{emp.position?.posName || '—'}</td>
223
+ <td className="px-6 py-4">
224
+ <span className={`inline-flex rounded-full px-2.5 py-0.5 text-xs font-semibold ${getStatusBadgeClass(emp.empStatus)}`}>
225
+ {emp.empStatus || 'Active'}
226
+ </span>
227
+ </td>
228
+ <td className="px-6 py-4">
229
+ <div className="flex gap-2">
230
+ <button
231
+ type="button"
232
+ onClick={() => openEdit(emp)}
233
+ className="rounded-lg px-2 py-1 text-sm font-medium text-brand-400 hover:bg-brand-800 hover:text-white"
234
+ >
235
+ Edit
236
+ </button>
237
+ <button
238
+ type="button"
239
+ onClick={() => handleDelete(emp._id)}
240
+ className="rounded-lg px-2 py-1 text-sm font-medium text-red-400 hover:bg-red-900/30"
241
+ >
242
+ Delete
243
+ </button>
244
+ </div>
245
+ </td>
246
+ </tr>
247
+ ))}
248
+ </tbody>
249
+ </table>
250
+ {filtered.length === 0 && (
251
+ <p className="py-12 text-center text-brand-400">No employees found</p>
252
+ )}
253
+ </div>
254
+
255
+ <Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title={editing ? 'Edit employee' : 'Add employee'} size="xl">
256
+ <form onSubmit={handleSubmit}>
257
+ <div className="grid gap-4 sm:grid-cols-2">
258
+ <Input label="First name" name="empFirstName" value={form.empFirstName} onChange={handleChange} error={errors.empFirstName} />
259
+ <Input label="Last name" name="empLastName" value={form.empLastName} onChange={handleChange} error={errors.empLastName} />
260
+ <Input label="Email" name="empEmail" type="email" value={form.empEmail} onChange={handleChange} error={errors.empEmail} />
261
+ <Input label="Telephone" name="empTelephone" value={form.empTelephone} onChange={handleChange} error={errors.empTelephone} />
262
+ <Select label="Gender" name="empGender" value={form.empGender} onChange={handleChange} options={genderOptions} placeholder="Select gender" error={errors.empGender} />
263
+
264
+ {/* Input Selection Connected to Custom Rules Status Matrix */}
265
+ <Select label="Status" name="empStatus" value={form.empStatus} onChange={handleChange} options={statusOptions} error={errors.empStatus} />
266
+
267
+ <Input label="Date of birth" name="empDateOfBirth" type="date" value={form.empDateOfBirth} onChange={handleChange} error={errors.empDateOfBirth} />
268
+ <Input label="Hire date" name="empHireDate" type="date" value={form.empHireDate} onChange={handleChange} error={errors.empHireDate} />
269
+
270
+ <Select
271
+ label="Department"
272
+ name="department"
273
+ value={form.department}
274
+ onChange={handleChange}
275
+ options={departments.map((d) => ({ value: d._id, label: d.departName }))}
276
+ placeholder="Select department"
277
+ error={errors.department}
278
+ />
279
+ <Select
280
+ label="Position"
281
+ name="position"
282
+ value={form.position}
283
+ onChange={handleChange}
284
+ options={positions.map((p) => ({ value: p._id, label: p.posName }))}
285
+ placeholder="Select position"
286
+ error={errors.position}
287
+ />
288
+ <div className="sm:col-span-2">
289
+ <Input label="Address" name="empAddress" value={form.empAddress} onChange={handleChange} error={errors.empAddress} />
290
+ </div>
291
+ </div>
292
+ <ModalFooter onCancel={() => setModalOpen(false)} loading={loading} submitLabel={editing ? 'Update' : 'Create'} />
293
+ </form>
294
+ </Modal>
295
+ </div>
296
+ );
297
+ }
@@ -0,0 +1,113 @@
1
+ import { useEffect, useState } from 'react';
2
+ import api from '../api/axios';
3
+ import toast from 'react-hot-toast';
4
+
5
+ export default function LeaveReport() {
6
+ const [employees, setEmployees] = useState([]);
7
+ const [loading, setLoading] = useState(true);
8
+
9
+ useEffect(() => {
10
+ const fetchEmployees = async () => {
11
+ try {
12
+ const token = localStorage.getItem('token');
13
+ const headers = token ? { Authorization: `Bearer ${token}` } : {};
14
+ const res = await api.get('/employees', { headers });
15
+ setEmployees(res.data?.data || res.data || []);
16
+ } catch (err) {
17
+ console.error('Failed to load employee records:', err);
18
+ toast.error('Could not fetch employee directory for report generation');
19
+ } finally {
20
+ setLoading(false);
21
+ }
22
+ };
23
+ fetchEmployees();
24
+ }, []);
25
+
26
+ // 1. Filter out only employees whose current status is exactly "On Leave"
27
+ const leaveEmployees = employees.filter((emp) => emp.empStatus === 'On Leave');
28
+
29
+ // 2. Reduce the list to group employees by their department name
30
+ const groupedByDepartment = leaveEmployees.reduce((groups, emp) => {
31
+ const deptName = emp.department?.departName || 'Unassigned Department';
32
+ if (!groups[deptName]) {
33
+ groups[deptName] = [];
34
+ }
35
+ groups[deptName].push(emp);
36
+ return groups;
37
+ }, {});
38
+
39
+ if (loading) {
40
+ return <div className="p-8 text-white">Generating employee status report...</div>;
41
+ }
42
+
43
+ return (
44
+ <div className="space-y-6">
45
+ {/* Report Header */}
46
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between border-b border-brand-800 pb-5">
47
+ <div>
48
+ <h1 className="text-2xl font-bold text-white tracking-wide">Employee Status Report</h1>
49
+ <p className="mt-1 text-slate-400">Comprehensive overview of active workspace leaves of absence</p>
50
+ </div>
51
+ <div className="text-sm text-slate-400 bg-brand-900/40 border border-brand-800 px-4 py-2 rounded-xl">
52
+ Report Generated: <span className="text-white font-medium">{new Date().toLocaleDateString()}</span>
53
+ </div>
54
+ </div>
55
+
56
+ {/* Summary KPI Block */}
57
+ <div className="max-w-xs rounded-xl bg-gradient-to-br from-brand-950 to-brand-900 p-5 ring-1 ring-brand-700 shadow-lg">
58
+ <p className="text-sm font-semibold uppercase tracking-wider text-brand-300">Total Employees On Leave</p>
59
+ <p className="mt-2 text-4xl font-extrabold text-white">{leaveEmployees.length}</p>
60
+ </div>
61
+
62
+ {/* Main Report Data Output Layout */}
63
+ {leaveEmployees.length === 0 ? (
64
+ <div className="rounded-xl bg-card border border-brand-800 p-12 text-center text-slate-400">
65
+ 🎉 No employee profiles are currently registered as "On Leave" across the operational matrix.
66
+ </div>
67
+ ) : (
68
+ <div className="space-y-8">
69
+ {Object.keys(groupedByDepartment).map((deptName) => (
70
+ <div key={deptName} className="overflow-hidden rounded-xl bg-card ring-1 ring-brand-800 shadow-md">
71
+
72
+ {/* Department Subsection Banner */}
73
+ <div className="flex items-center justify-between border-b border-brand-700 bg-brand-900/30 px-6 py-4">
74
+ <h2 className="text-lg font-bold text-brand-200 uppercase tracking-wide">{deptName}</h2>
75
+ <span className="rounded-full bg-brand-800 px-3 py-1 text-xs font-semibold text-white ring-1 ring-brand-600">
76
+ {groupedByDepartment[deptName].length} on leave
77
+ </span>
78
+ </div>
79
+
80
+ {/* Department Leave Grid Table View */}
81
+ <table className="w-full text-left text-sm border-collapse">
82
+ <thead>
83
+ <tr className="border-b border-brand-800 bg-brand-950/20 text-xs font-semibold uppercase tracking-wider text-slate-400">
84
+ <th className="px-6 py-3.5">Employee Name</th>
85
+ <th className="px-6 py-3.5">Email Address</th>
86
+ <th className="px-6 py-3.5">Assigned Position</th>
87
+ <th className="px-6 py-3.5">Telephone Connection</th>
88
+ <th className="px-6 py-3.5">Hire Date</th>
89
+ </tr>
90
+ </thead>
91
+ <tbody className="divide-y divide-brand-800/60 text-slate-300">
92
+ {groupedByDepartment[deptName].map((emp) => (
93
+ <tr key={emp._id} className="hover:bg-brand-900/10 transition-colors">
94
+ <td className="px-6 py-4 font-medium text-stone-100">
95
+ {emp.empFirstName} {emp.empLastName}
96
+ </td>
97
+ <td className="px-6 py-4 text-brand-300 font-mono text-xs">{emp.empEmail}</td>
98
+ <td className="px-6 py-4">{emp.position?.posName || '—'}</td>
99
+ <td className="px-6 py-4 text-slate-400">{emp.empTelephone}</td>
100
+ <td className="px-6 py-4 text-slate-400">
101
+ {emp.empHireDate ? new Date(emp.empHireDate).toLocaleDateString() : '—'}
102
+ </td>
103
+ </tr>
104
+ ))}
105
+ </tbody>
106
+ </table>
107
+ </div>
108
+ ))}
109
+ </div>
110
+ )}
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,92 @@
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 Login() {
7
+ const [formData, setFormData] = useState({ userName: '', password: '' });
8
+ const [submitting, setSubmitting] = useState(false);
9
+ const { login } = 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
+ if (!formData.userName || !formData.password) {
19
+ return toast.error('Please enter all required fields');
20
+ }
21
+ try {
22
+ setSubmitting(true);
23
+ await login(formData.userName, formData.password);
24
+ toast.success('Successfully logged into workspace');
25
+ navigate('/dashboard');
26
+ } catch (err) {
27
+ toast.error(err.response?.data?.message || 'Invalid administrative credentials');
28
+ } finally {
29
+ setSubmitting(false);
30
+ }
31
+ };
32
+
33
+ return (
34
+ <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">
35
+ <div className="w-full max-w-md rounded-2xl border border-brand-700/40 bg-card p-8 shadow-2xl shadow-black/30">
36
+ <p className="text-center text-xs font-semibold uppercase tracking-widest text-brand-400">
37
+ DAB Enterprise HRMS
38
+ </p>
39
+ <h1 className="mt-3 text-center text-2xl font-bold text-white">Sign In to Portal</h1>
40
+ <p className="mt-2 text-center text-sm text-slate-400">Access your workspace dashboard panel</p>
41
+
42
+ <form onSubmit={handleSubmit} className="mt-8 space-y-5">
43
+ <div>
44
+ <label className="block text-xs font-semibold uppercase tracking-wider text-slate-300">
45
+ Username
46
+ </label>
47
+ <input
48
+ type="text"
49
+ name="userName"
50
+ value={formData.userName}
51
+ onChange={handleInputChange}
52
+ 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"
53
+ placeholder="admin_user"
54
+ />
55
+ </div>
56
+ <div>
57
+ <div className="flex justify-between">
58
+ <label className="block text-xs font-semibold uppercase tracking-wider text-slate-300">
59
+ Password
60
+ </label>
61
+ <Link to="/forgot-password" className="text-xs font-medium text-brand-400 hover:underline">
62
+ Forgot password?
63
+ </Link>
64
+ </div>
65
+ <input
66
+ type="password"
67
+ name="password"
68
+ value={formData.password}
69
+ onChange={handleInputChange}
70
+ 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"
71
+ placeholder="••••••••"
72
+ />
73
+ </div>
74
+ <button
75
+ type="submit"
76
+ disabled={submitting}
77
+ 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"
78
+ >
79
+ {submitting ? 'Authenticating...' : 'Sign In'}
80
+ </button>
81
+ </form>
82
+
83
+ <p className="mt-6 text-center text-xs text-slate-400">
84
+ Need a portal account?{' '}
85
+ <Link to="/register" className="font-semibold text-brand-400 hover:underline">
86
+ Create an Admin Profile
87
+ </Link>
88
+ </p>
89
+ </div>
90
+ </div>
91
+ );
92
+ }