alpe-temp 1.0.2 → 1.0.3
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/backend-project/package-lock.json +131 -0
- package/backend-project/package.json +3 -1
- package/backend-project/src/app.js +33 -55
- package/backend-project/src/config/app.config.js +1 -49
- package/backend-project/src/config/env.js +2 -10
- package/backend-project/src/middleware/auth.middleware.js +3 -26
- package/backend-project/src/modules/auth/auth.controller.js +15 -19
- package/backend-project/src/modules/auth/auth.routes.js +4 -8
- package/backend-project/src/modules/auth/auth.service.js +9 -31
- package/backend-project/src/modules/auth/user.model.js +10 -33
- package/backend-project/src/modules/department/department.controller.js +0 -4
- package/backend-project/src/modules/department/department.model.js +1 -4
- package/backend-project/src/modules/department/department.routes.js +0 -1
- package/backend-project/src/modules/department/department.service.js +1 -9
- package/backend-project/src/modules/employee/employee.controller.js +2 -10
- package/backend-project/src/modules/employee/employee.model.js +15 -9
- package/backend-project/src/modules/employee/employee.routes.js +4 -6
- package/backend-project/src/modules/employee/employee.service.js +20 -5
- package/backend-project/src/modules/position/position.controller.js +50 -0
- package/backend-project/src/modules/position/position.model.js +8 -0
- package/backend-project/src/modules/position/position.routes.js +14 -0
- package/backend-project/src/modules/position/position.service.js +21 -0
- package/backend-project/src/modules/reports/reports.controller.js +16 -28
- package/backend-project/src/modules/reports/reports.routes.js +2 -2
- package/backend-project/src/seed.js +69 -15
- package/backend-project/src/utils/token.js +1 -27
- package/frontend-project/dist/assets/index-BXwcQ8Za.css +1 -0
- package/frontend-project/dist/assets/index-Bo0aORq7.js +20 -0
- package/frontend-project/dist/index.html +3 -3
- package/frontend-project/index.html +1 -1
- package/frontend-project/src/Auth/Login.jsx +15 -25
- package/frontend-project/src/Auth/Register.jsx +92 -183
- package/frontend-project/src/Intro.jsx +4 -9
- package/frontend-project/src/LayOut.jsx +10 -23
- package/frontend-project/src/api/ApiClient.js +19 -60
- package/frontend-project/src/layouts/BottomNav.jsx +22 -105
- package/frontend-project/src/layouts/TopNav.jsx +19 -98
- package/frontend-project/src/layouts/useShell.js +30 -44
- package/frontend-project/src/main.jsx +2 -3
- package/frontend-project/src/pages/Department.jsx +21 -58
- package/frontend-project/src/pages/Employee.jsx +131 -113
- package/frontend-project/src/pages/Home.jsx +36 -36
- package/frontend-project/src/pages/Position.jsx +161 -0
- package/frontend-project/src/pages/Reports.jsx +81 -68
- package/package.json +4 -2
- package/server-test-err.txt +0 -0
- package/server-test-out.txt +0 -0
- package/backend-project/src/modules/_example/example.controller.js +0 -82
- package/backend-project/src/modules/_example/example.model.js +0 -47
- package/backend-project/src/modules/_example/example.routes.js +0 -43
- package/backend-project/src/modules/_example/example.service.js +0 -58
- package/backend-project/src/modules/excel/excel.controller.js +0 -61
- package/backend-project/src/modules/excel/excel.routes.js +0 -13
- package/backend-project/src/modules/excel/excel.service.js +0 -303
- package/backend-project/src/modules/salary/salary.controller.js +0 -70
- package/backend-project/src/modules/salary/salary.model.js +0 -23
- package/backend-project/src/modules/salary/salary.routes.js +0 -16
- package/backend-project/src/modules/salary/salary.service.js +0 -44
- package/frontend-project/dist/assets/index-B08ICGra.js +0 -20
- package/frontend-project/dist/assets/index-D_cqT2Z6.css +0 -1
|
@@ -1,78 +1,77 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
-
import { Plus,
|
|
2
|
+
import { Plus, Pencil, Trash2, Search } from 'lucide-react';
|
|
3
3
|
import Table from '../components/Table';
|
|
4
4
|
import Modal from '../components/Modal';
|
|
5
5
|
import Button from '../components/Button';
|
|
6
6
|
import FormField from '../components/FormField';
|
|
7
7
|
import { useToast } from '../components/Toast';
|
|
8
|
-
import { employeeApi, departmentApi,
|
|
8
|
+
import { employeeApi, departmentApi, positionApi } from '../api/ApiClient';
|
|
9
|
+
|
|
10
|
+
const STATUS_OPTIONS = [
|
|
11
|
+
{ value: 'on mission', label: 'On Mission' },
|
|
12
|
+
{ value: 'on leave', label: 'On Leave' },
|
|
13
|
+
{ value: 'left', label: 'Left' },
|
|
14
|
+
{ value: 'blacklisted', label: 'Blacklisted' },
|
|
15
|
+
{ value: 'deceased', label: 'Deceased' },
|
|
16
|
+
];
|
|
9
17
|
|
|
10
18
|
const formatDate = (iso) => {
|
|
11
19
|
if (!iso) return '—';
|
|
12
20
|
try { return new Date(iso).toISOString().split('T')[0]; } catch { return '—'; }
|
|
13
21
|
};
|
|
14
22
|
|
|
23
|
+
function StatusBadge({ status }) {
|
|
24
|
+
const colors = {
|
|
25
|
+
'on leave': 'bg-yellow-100 text-yellow-800',
|
|
26
|
+
'on mission': 'bg-blue-100 text-blue-800',
|
|
27
|
+
left: 'bg-gray-100 text-gray-800',
|
|
28
|
+
blacklisted: 'bg-red-100 text-red-800',
|
|
29
|
+
deceased: 'bg-gray-200 text-gray-600',
|
|
30
|
+
};
|
|
31
|
+
return <span className={`px-2 py-0.5 text-[11px] font-semibold ${colors[status] || 'bg-gray-100 text-gray-800'}`}>{status}</span>;
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
export default function Employee() {
|
|
16
35
|
const toast = useToast();
|
|
17
36
|
const [employees, setEmployees] = useState([]);
|
|
18
37
|
const [departments, setDepartments] = useState([]);
|
|
38
|
+
const [positions, setPositions] = useState([]);
|
|
19
39
|
const [loading, setLoading] = useState(true);
|
|
20
|
-
const [
|
|
40
|
+
const [search, setSearch] = useState('');
|
|
21
41
|
const [createOpen, setCreateOpen] = useState(false);
|
|
22
42
|
const [editTarget, setEditTarget] = useState(null);
|
|
23
43
|
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
24
44
|
|
|
25
|
-
const loadAll = useCallback(async () => {
|
|
45
|
+
const loadAll = useCallback(async (searchTerm) => {
|
|
26
46
|
setLoading(true);
|
|
27
47
|
try {
|
|
28
|
-
const [empRes, deptRes] = await Promise.allSettled([
|
|
29
|
-
employeeApi.list(),
|
|
48
|
+
const [empRes, deptRes, posRes] = await Promise.allSettled([
|
|
49
|
+
employeeApi.list(searchTerm || undefined),
|
|
30
50
|
departmentApi.list(),
|
|
51
|
+
positionApi.list(),
|
|
31
52
|
]);
|
|
32
53
|
if (empRes.status === 'fulfilled') setEmployees(empRes.value?.data?.employees ?? []);
|
|
33
54
|
if (deptRes.status === 'fulfilled') setDepartments(deptRes.value?.data?.departments ?? []);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
setLoading(false);
|
|
38
|
-
}
|
|
55
|
+
if (posRes.status === 'fulfilled') setPositions(posRes.value?.data?.positions ?? []);
|
|
56
|
+
} catch { toast.error('Failed to load data'); }
|
|
57
|
+
finally { setLoading(false); }
|
|
39
58
|
}, []);
|
|
40
59
|
|
|
41
60
|
useEffect(() => { loadAll(); }, [loadAll]);
|
|
42
61
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
try { await excelApi.exportUsers(); toast.success('Report downloaded'); }
|
|
47
|
-
catch {
|
|
48
|
-
const rows = employees.map((e) => ({
|
|
49
|
-
'Emp #': e.employeeNumber,
|
|
50
|
-
'First Name': e.firstName,
|
|
51
|
-
'Last Name': e.lastName,
|
|
52
|
-
Position: e.position,
|
|
53
|
-
Gender: e.gender,
|
|
54
|
-
'Hired Date': formatDate(e.hiredDate),
|
|
55
|
-
Department: e.department?.departmentName ?? '',
|
|
56
|
-
}));
|
|
57
|
-
await excelApi.exportCustom('Employees', rows, 'employees-report');
|
|
58
|
-
toast.success('Report downloaded');
|
|
59
|
-
}
|
|
60
|
-
} catch (err) {
|
|
61
|
-
toast.error(err.message ?? 'Export failed');
|
|
62
|
-
} finally {
|
|
63
|
-
setExporting(false);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
62
|
+
const handleSearch = () => loadAll(search);
|
|
63
|
+
const handleClearSearch = () => { setSearch(''); loadAll(); };
|
|
66
64
|
|
|
67
65
|
const columns = [
|
|
68
|
-
{ key: '
|
|
69
|
-
{ key: '
|
|
70
|
-
{ key: '
|
|
71
|
-
{ key: '
|
|
72
|
-
{ key: '
|
|
73
|
-
{ key: '
|
|
74
|
-
{ key: '
|
|
75
|
-
{ key: 'department', label: '
|
|
66
|
+
{ key: 'empFirstName', label: 'First Name' },
|
|
67
|
+
{ key: 'empLastName', label: 'Last Name' },
|
|
68
|
+
{ key: 'empGender', label: 'Gender' },
|
|
69
|
+
{ key: 'empEmail', label: 'Email' },
|
|
70
|
+
{ key: 'empTelephone', label: 'Phone' },
|
|
71
|
+
{ key: 'empHireDate', label: 'Hired', render: (v) => formatDate(v) },
|
|
72
|
+
{ key: 'empStatus', label: 'Status', render: (v) => <StatusBadge status={v} /> },
|
|
73
|
+
{ key: 'department', label: 'Dept', render: (v) => v?.departName ?? '—' },
|
|
74
|
+
{ key: 'position', label: 'Position', render: (v) => v?.posName ?? '—' },
|
|
76
75
|
{
|
|
77
76
|
key: 'actions', label: '', align: 'right',
|
|
78
77
|
render: (_, row) => (
|
|
@@ -91,37 +90,48 @@ export default function Employee() {
|
|
|
91
90
|
<h1 className="text-[18px] font-bold text-gray-800">Employees</h1>
|
|
92
91
|
<p className="text-[13px] text-gray-400 mt-0.5">{employees.length} total records</p>
|
|
93
92
|
</div>
|
|
94
|
-
<
|
|
95
|
-
|
|
93
|
+
<Button size="sm" variant='dark' icon={<Plus className="w-3.5 h-3.5" />} onClick={() => setCreateOpen(true)}>Add Employee</Button>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex items-center gap-2">
|
|
96
|
+
<div className="relative w-72">
|
|
97
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
98
|
+
<input
|
|
99
|
+
type="text" value={search} onChange={(e) => setSearch(e.target.value)}
|
|
100
|
+
placeholder="Search employees..." onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
|
101
|
+
className="w-full pl-9 pr-3 py-2 text-[13px] border border-gray-200 rounded-md focus:outline-none focus:ring-1 focus:ring-[#008A75]"
|
|
102
|
+
/>
|
|
96
103
|
</div>
|
|
104
|
+
<Button size="sm" onClick={handleSearch}>Search</Button>
|
|
105
|
+
{search && <Button size="sm" variant="ghost" onClick={handleClearSearch}>Clear</Button>}
|
|
97
106
|
</div>
|
|
98
107
|
<div className="bg-white border border-gray-200 p-5">
|
|
99
108
|
<Table columns={columns} data={employees} rowKey="_id" loading={loading} emptyMessage="No employees yet." maxHeight="480px" />
|
|
100
109
|
</div>
|
|
101
|
-
{createOpen && <CreateModal departments={departments} onClose={() => setCreateOpen(false)} onCreated={() => { setCreateOpen(false); loadAll(); }} />}
|
|
102
|
-
{editTarget && <EditModal employee={editTarget} departments={departments} onClose={() => setEditTarget(null)} onUpdated={() => { setEditTarget(null); loadAll(); }} />}
|
|
110
|
+
{createOpen && <CreateModal departments={departments} positions={positions} onClose={() => setCreateOpen(false)} onCreated={() => { setCreateOpen(false); loadAll(); }} />}
|
|
111
|
+
{editTarget && <EditModal employee={editTarget} departments={departments} positions={positions} onClose={() => setEditTarget(null)} onUpdated={() => { setEditTarget(null); loadAll(); }} />}
|
|
103
112
|
{deleteTarget && <DeleteModal employee={deleteTarget} onClose={() => setDeleteTarget(null)} onDeleted={() => { setDeleteTarget(null); loadAll(); }} />}
|
|
104
113
|
</div>
|
|
105
114
|
);
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
function CreateModal({ departments, onClose, onCreated }) {
|
|
117
|
+
function CreateModal({ departments, positions, onClose, onCreated }) {
|
|
109
118
|
const toast = useToast();
|
|
110
119
|
const now = new Date().toISOString().split('T')[0];
|
|
111
|
-
const [form, setForm] = useState({
|
|
120
|
+
const [form, setForm] = useState({
|
|
121
|
+
empFirstName: '', empLastName: '', empGender: '', empDateOfBirth: '',
|
|
122
|
+
empEmail: '', empTelephone: '', empAddress: '', empHireDate: now,
|
|
123
|
+
empStatus: 'on mission', department: '', position: '',
|
|
124
|
+
});
|
|
112
125
|
const [loading, setLoading] = useState(false);
|
|
113
126
|
const [errors, setErrors] = useState({});
|
|
114
|
-
|
|
115
127
|
const set = (field) => (val) => setForm((f) => ({ ...f, [field]: val }));
|
|
116
128
|
|
|
117
129
|
const validate = () => {
|
|
118
130
|
const e = {};
|
|
119
|
-
if (!form.
|
|
120
|
-
if (!form.
|
|
121
|
-
if (!form.
|
|
122
|
-
if (!form.
|
|
123
|
-
if (!form.gender) e.gender = 'Required';
|
|
124
|
-
if (!form.hiredDate) e.hiredDate = 'Required';
|
|
131
|
+
if (!form.empFirstName) e.empFirstName = 'Required';
|
|
132
|
+
if (!form.empLastName) e.empLastName = 'Required';
|
|
133
|
+
if (!form.empGender) e.empGender = 'Required';
|
|
134
|
+
if (!form.empHireDate) e.empHireDate = 'Required';
|
|
125
135
|
if (!form.department) e.department = 'Required';
|
|
126
136
|
setErrors(e);
|
|
127
137
|
return Object.keys(e).length === 0;
|
|
@@ -132,51 +142,53 @@ function CreateModal({ departments, onClose, onCreated }) {
|
|
|
132
142
|
setLoading(true);
|
|
133
143
|
try {
|
|
134
144
|
await employeeApi.create(form);
|
|
135
|
-
toast.success('Employee created
|
|
145
|
+
toast.success('Employee created');
|
|
136
146
|
onCreated();
|
|
137
|
-
} catch (err) {
|
|
138
|
-
|
|
139
|
-
} finally {
|
|
140
|
-
setLoading(false);
|
|
141
|
-
}
|
|
147
|
+
} catch (err) { toast.error(err.message ?? 'Failed to create employee'); }
|
|
148
|
+
finally { setLoading(false); }
|
|
142
149
|
};
|
|
143
150
|
|
|
144
|
-
const deptOptions = departments.map((d) => ({ value: d._id, label:
|
|
151
|
+
const deptOptions = departments.map((d) => ({ value: d._id, label: d.departName }));
|
|
152
|
+
const posOptions = positions.map((p) => ({ value: p._id, label: p.posName }));
|
|
145
153
|
|
|
146
154
|
return (
|
|
147
|
-
<Modal open title="Add Employee" size="
|
|
155
|
+
<Modal open title="Add Employee" size="2xl" onClose={onClose}>
|
|
148
156
|
<div className="grid grid-cols-2 gap-3">
|
|
149
|
-
<FormField label="
|
|
150
|
-
<FormField.Input value={form.
|
|
151
|
-
</FormField>
|
|
152
|
-
<FormField label="First Name" required error={errors.firstName}>
|
|
153
|
-
<FormField.Input value={form.firstName} onChange={set('firstName')} placeholder="Jane" />
|
|
157
|
+
<FormField label="First Name" required error={errors.empFirstName}>
|
|
158
|
+
<FormField.Input value={form.empFirstName} onChange={set('empFirstName')} placeholder="First name" />
|
|
154
159
|
</FormField>
|
|
155
|
-
<FormField label="Last Name" required error={errors.
|
|
156
|
-
<FormField.Input value={form.
|
|
160
|
+
<FormField label="Last Name" required error={errors.empLastName}>
|
|
161
|
+
<FormField.Input value={form.empLastName} onChange={set('empLastName')} placeholder="Last name" />
|
|
157
162
|
</FormField>
|
|
158
|
-
<FormField label="
|
|
159
|
-
<FormField.Select value={form.
|
|
160
|
-
{ value: '
|
|
163
|
+
<FormField label="Gender" required error={errors.empGender}>
|
|
164
|
+
<FormField.Select value={form.empGender} onChange={set('empGender')} placeholder="Select gender" options={[
|
|
165
|
+
{ value: 'male', label: 'Male' }, { value: 'female', label: 'Female' },
|
|
161
166
|
]} />
|
|
162
167
|
</FormField>
|
|
168
|
+
<FormField label="Date of Birth">
|
|
169
|
+
<FormField.Input type="date" value={form.empDateOfBirth} onChange={set('empDateOfBirth')} />
|
|
170
|
+
</FormField>
|
|
171
|
+
<FormField label="Email">
|
|
172
|
+
<FormField.Input type="email" value={form.empEmail} onChange={set('empEmail')} placeholder="employee@company.com" />
|
|
173
|
+
</FormField>
|
|
174
|
+
<FormField label="Telephone">
|
|
175
|
+
<FormField.Input type="tel" value={form.empTelephone} onChange={set('empTelephone')} placeholder="07xxxxxxxx" />
|
|
176
|
+
</FormField>
|
|
163
177
|
<FormField label="Department" required error={errors.department}>
|
|
164
178
|
<FormField.Select value={form.department} onChange={set('department')} placeholder="Select department" options={deptOptions} />
|
|
165
179
|
</FormField>
|
|
166
|
-
<FormField label="
|
|
167
|
-
<FormField.Select value={form.
|
|
168
|
-
{ value: 'male', label: 'Male' }, { value: 'female', label: 'Female' },
|
|
169
|
-
]} />
|
|
180
|
+
<FormField label="Position">
|
|
181
|
+
<FormField.Select value={form.position} onChange={set('position')} placeholder="Select position" options={posOptions} />
|
|
170
182
|
</FormField>
|
|
171
|
-
<FormField label="
|
|
172
|
-
<FormField.Input type="date" value={form.
|
|
183
|
+
<FormField label="Hire Date" required error={errors.empHireDate}>
|
|
184
|
+
<FormField.Input type="date" value={form.empHireDate} onChange={set('empHireDate')} />
|
|
173
185
|
</FormField>
|
|
174
|
-
<FormField label="
|
|
175
|
-
<FormField.
|
|
186
|
+
<FormField label="Status">
|
|
187
|
+
<FormField.Select value={form.empStatus} onChange={set('empStatus')} options={STATUS_OPTIONS} />
|
|
176
188
|
</FormField>
|
|
177
189
|
<div className="col-span-2">
|
|
178
190
|
<FormField label="Address">
|
|
179
|
-
<FormField.Textarea value={form.
|
|
191
|
+
<FormField.Textarea value={form.empAddress} onChange={set('empAddress')} placeholder="Kigali, Rwanda" />
|
|
180
192
|
</FormField>
|
|
181
193
|
</div>
|
|
182
194
|
</div>
|
|
@@ -188,22 +200,25 @@ function CreateModal({ departments, onClose, onCreated }) {
|
|
|
188
200
|
);
|
|
189
201
|
}
|
|
190
202
|
|
|
191
|
-
function EditModal({ employee, departments, onClose, onUpdated }) {
|
|
203
|
+
function EditModal({ employee, departments, positions, onClose, onUpdated }) {
|
|
192
204
|
const toast = useToast();
|
|
193
205
|
const [form, setForm] = useState({
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
206
|
+
empFirstName: employee.empFirstName || '',
|
|
207
|
+
empLastName: employee.empLastName || '',
|
|
208
|
+
empGender: employee.empGender || '',
|
|
209
|
+
empDateOfBirth: employee.empDateOfBirth ? employee.empDateOfBirth.split('T')[0] : '',
|
|
210
|
+
empEmail: employee.empEmail || '',
|
|
211
|
+
empTelephone: employee.empTelephone || '',
|
|
212
|
+
empAddress: employee.empAddress || '',
|
|
213
|
+
empHireDate: employee.empHireDate ? employee.empHireDate.split('T')[0] : '',
|
|
214
|
+
empStatus: employee.empStatus || 'on mission',
|
|
202
215
|
department: employee.department?._id || '',
|
|
216
|
+
position: employee.position?._id || '',
|
|
203
217
|
});
|
|
204
218
|
const [loading, setLoading] = useState(false);
|
|
205
219
|
const set = (f) => (v) => setForm((p) => ({ ...p, [f]: v }));
|
|
206
|
-
const deptOptions = departments.map((d) => ({ value: d._id, label:
|
|
220
|
+
const deptOptions = departments.map((d) => ({ value: d._id, label: d.departName }));
|
|
221
|
+
const posOptions = positions.map((p) => ({ value: p._id, label: p.posName }));
|
|
207
222
|
|
|
208
223
|
const handleSave = async () => {
|
|
209
224
|
setLoading(true);
|
|
@@ -211,28 +226,34 @@ function EditModal({ employee, departments, onClose, onUpdated }) {
|
|
|
211
226
|
await employeeApi.update(employee._id, form);
|
|
212
227
|
toast.success('Employee updated');
|
|
213
228
|
onUpdated();
|
|
214
|
-
} catch (err) {
|
|
215
|
-
|
|
216
|
-
} finally {
|
|
217
|
-
setLoading(false);
|
|
218
|
-
}
|
|
229
|
+
} catch (err) { toast.error(err.message ?? 'Update failed'); }
|
|
230
|
+
finally { setLoading(false); }
|
|
219
231
|
};
|
|
220
232
|
|
|
221
233
|
return (
|
|
222
|
-
<Modal open title={`Edit — ${employee.
|
|
234
|
+
<Modal open title={`Edit — ${employee.empFirstName} ${employee.empLastName}`} size="2xl" onClose={onClose}>
|
|
223
235
|
<div className="grid grid-cols-2 gap-3">
|
|
224
|
-
<FormField label="
|
|
225
|
-
<FormField label="
|
|
226
|
-
<FormField label="
|
|
227
|
-
|
|
236
|
+
<FormField label="First Name"><FormField.Input value={form.empFirstName} onChange={set('empFirstName')} /></FormField>
|
|
237
|
+
<FormField label="Last Name"><FormField.Input value={form.empLastName} onChange={set('empLastName')} /></FormField>
|
|
238
|
+
<FormField label="Gender">
|
|
239
|
+
<FormField.Select value={form.empGender} onChange={set('empGender')} options={[{ value: 'male', label: 'Male' }, { value: 'female', label: 'Female' }]} />
|
|
240
|
+
</FormField>
|
|
241
|
+
<FormField label="Date of Birth"><FormField.Input type="date" value={form.empDateOfBirth} onChange={set('empDateOfBirth')} /></FormField>
|
|
242
|
+
<FormField label="Email"><FormField.Input type="email" value={form.empEmail} onChange={set('empEmail')} /></FormField>
|
|
243
|
+
<FormField label="Telephone"><FormField.Input value={form.empTelephone} onChange={set('empTelephone')} /></FormField>
|
|
228
244
|
<FormField label="Department">
|
|
229
245
|
<FormField.Select value={form.department} onChange={set('department')} options={deptOptions} placeholder="Select department" />
|
|
230
246
|
</FormField>
|
|
231
|
-
<FormField label="
|
|
232
|
-
<FormField.Select value={form.
|
|
247
|
+
<FormField label="Position">
|
|
248
|
+
<FormField.Select value={form.position} onChange={set('position')} options={posOptions} placeholder="Select position" />
|
|
249
|
+
</FormField>
|
|
250
|
+
<FormField label="Hire Date"><FormField.Input type="date" value={form.empHireDate} onChange={set('empHireDate')} /></FormField>
|
|
251
|
+
<FormField label="Status">
|
|
252
|
+
<FormField.Select value={form.empStatus} onChange={set('empStatus')} options={STATUS_OPTIONS} />
|
|
233
253
|
</FormField>
|
|
234
|
-
<
|
|
235
|
-
|
|
254
|
+
<div className="col-span-2">
|
|
255
|
+
<FormField label="Address"><FormField.Textarea value={form.empAddress} onChange={set('empAddress')} /></FormField>
|
|
256
|
+
</div>
|
|
236
257
|
</div>
|
|
237
258
|
<Modal.Footer>
|
|
238
259
|
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
|
@@ -250,20 +271,17 @@ function DeleteModal({ employee, onClose, onDeleted }) {
|
|
|
250
271
|
setLoading(true);
|
|
251
272
|
try {
|
|
252
273
|
await employeeApi.remove(employee._id);
|
|
253
|
-
toast.success(`${employee.
|
|
274
|
+
toast.success(`${employee.empFirstName} ${employee.empLastName} removed`);
|
|
254
275
|
onDeleted();
|
|
255
|
-
} catch (err) {
|
|
256
|
-
|
|
257
|
-
} finally {
|
|
258
|
-
setLoading(false);
|
|
259
|
-
}
|
|
276
|
+
} catch (err) { toast.error(err.message ?? 'Delete failed'); }
|
|
277
|
+
finally { setLoading(false); }
|
|
260
278
|
};
|
|
261
279
|
|
|
262
280
|
return (
|
|
263
281
|
<Modal open title="Confirm Deletion" size="sm" onClose={onClose}>
|
|
264
282
|
<p className="text-[13px] text-gray-600 leading-relaxed">
|
|
265
283
|
Are you sure you want to permanently delete{' '}
|
|
266
|
-
<span className="font-semibold text-gray-800">{employee.
|
|
284
|
+
<span className="font-semibold text-gray-800">{employee.empFirstName} {employee.empLastName}</span>?
|
|
267
285
|
</p>
|
|
268
286
|
<Modal.Footer>
|
|
269
287
|
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
|
@@ -1,56 +1,58 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { StatCard } from '../components/Card';
|
|
3
3
|
import Table from '../components/Table';
|
|
4
|
-
import { employeeApi,
|
|
4
|
+
import { employeeApi, departmentApi, positionApi } from '../api/ApiClient';
|
|
5
5
|
|
|
6
6
|
const TABLE_COLUMNS = [
|
|
7
|
-
{ key: '
|
|
8
|
-
{ key: '
|
|
9
|
-
{ key: '
|
|
10
|
-
{ key: '
|
|
11
|
-
{ key: '
|
|
12
|
-
{
|
|
13
|
-
|
|
7
|
+
{ key: 'empFirstName', label: 'First Name' },
|
|
8
|
+
{ key: 'empLastName', label: 'Last Name' },
|
|
9
|
+
{ key: 'empGender', label: 'Gender' },
|
|
10
|
+
{ key: 'empStatus', label: 'Status', render: (v) => <Badge status={v}>{v}</Badge> },
|
|
11
|
+
{ key: 'empEmail', label: 'Email' },
|
|
12
|
+
{
|
|
13
|
+
key: 'department',
|
|
14
|
+
label: 'Department',
|
|
15
|
+
render: (v) => v?.departName ?? '\u2014',
|
|
16
|
+
},
|
|
14
17
|
];
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
function Badge({ status, children }) {
|
|
20
|
+
const colors = {
|
|
21
|
+
'on leave': 'bg-yellow-100 text-yellow-800',
|
|
22
|
+
'on mission': 'bg-blue-100 text-blue-800',
|
|
23
|
+
left: 'bg-gray-100 text-gray-800',
|
|
24
|
+
blacklisted: 'bg-red-100 text-red-800',
|
|
25
|
+
deceased: 'bg-gray-200 text-gray-600',
|
|
26
|
+
};
|
|
22
27
|
return (
|
|
23
|
-
<span className=
|
|
28
|
+
<span className={`px-2 py-0.5 text-[11px] font-semibold ${colors[status] || 'bg-gray-100 text-gray-800'}`}>
|
|
24
29
|
{children}
|
|
25
30
|
</span>
|
|
26
31
|
);
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
export default function Home() {
|
|
30
|
-
const user = (() => { try { return JSON.parse(localStorage.getItem('user')); } catch { return {}; } })();
|
|
31
35
|
const [employees, setEmployees] = useState([]);
|
|
32
36
|
const [totalCount, setTotalCount] = useState(null);
|
|
33
|
-
const [avgSalary, setAvgSalary] = useState(null);
|
|
34
37
|
const [deptCount, setDeptCount] = useState(null);
|
|
38
|
+
const [posCount, setPosCount] = useState(null);
|
|
35
39
|
const [loading, setLoading] = useState(true);
|
|
36
40
|
|
|
37
41
|
useEffect(() => {
|
|
38
42
|
const load = async () => {
|
|
39
43
|
setLoading(true);
|
|
40
44
|
try {
|
|
41
|
-
const [empRes,
|
|
42
|
-
employeeApi.list(),
|
|
43
|
-
employeeApi.count(),
|
|
44
|
-
salaryApi.average(),
|
|
45
|
-
departmentApi.list(),
|
|
45
|
+
const [empRes, deptRes, posRes] = await Promise.allSettled([
|
|
46
|
+
employeeApi.list(), departmentApi.list(), positionApi.list(),
|
|
46
47
|
]);
|
|
47
|
-
if (empRes.status === 'fulfilled')
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
if (empRes.status === 'fulfilled') {
|
|
49
|
+
const data = empRes.value?.data?.employees ?? [];
|
|
50
|
+
setEmployees(data);
|
|
51
|
+
setTotalCount(data.length);
|
|
52
|
+
}
|
|
50
53
|
if (deptRes.status === 'fulfilled') setDeptCount(deptRes.value?.data?.departments?.length ?? 0);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
+
if (posRes.status === 'fulfilled') setPosCount(posRes.value?.data?.positions?.length ?? 0);
|
|
55
|
+
} finally { setLoading(false); }
|
|
54
56
|
};
|
|
55
57
|
load();
|
|
56
58
|
}, []);
|
|
@@ -59,16 +61,14 @@ export default function Home() {
|
|
|
59
61
|
|
|
60
62
|
return (
|
|
61
63
|
<div className="space-y-6">
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
</h1>
|
|
66
|
-
</div>
|
|
64
|
+
<h1 className="text-[18px] font-bold text-gray-800">
|
|
65
|
+
DAB Enterprise LTD — HRMS Dashboard
|
|
66
|
+
</h1>
|
|
67
67
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
68
68
|
<StatCard label="Total Employees" value={fmt(totalCount)} loading={loading && totalCount == null} className="text-white bg-[#008A75]" />
|
|
69
|
-
<StatCard label="
|
|
70
|
-
<StatCard label="
|
|
71
|
-
<StatCard label="
|
|
69
|
+
<StatCard label="Departments" value={fmt(deptCount)} loading={loading && deptCount == null} className="text-zinc-900 bg-zinc-50" />
|
|
70
|
+
<StatCard label="Positions" value={fmt(posCount)} loading={loading && posCount == null} className="text-white bg-[#008A75]" />
|
|
71
|
+
<StatCard label="On Leave" value={fmt(employees.filter(e => e.empStatus === 'on leave').length)} loading={loading && totalCount == null} className="text-zinc-900 bg-zinc-50" />
|
|
72
72
|
</div>
|
|
73
73
|
<div className="bg-white border border-gray-200 p-5">
|
|
74
74
|
<h2 className="text-[13px] font-semibold text-gray-700 mb-4">Recent Employees</h2>
|