alpe-temp 1.0.1 → 1.0.2

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 (33) hide show
  1. package/frontend-project/src/App.css +1 -0
  2. package/frontend-project/src/Auth/Login.jsx +85 -0
  3. package/frontend-project/src/Auth/Register.jsx +183 -0
  4. package/frontend-project/src/Intro.jsx +33 -0
  5. package/frontend-project/src/LayOut.jsx +38 -0
  6. package/frontend-project/src/api/ApiClient.js +92 -0
  7. package/frontend-project/src/assets/hero.png +0 -0
  8. package/frontend-project/src/assets/react.svg +1 -0
  9. package/frontend-project/src/assets/vite.svg +1 -0
  10. package/frontend-project/src/components/Aside.jsx +9 -0
  11. package/frontend-project/src/components/Button.jsx +100 -0
  12. package/frontend-project/src/components/Card.jsx +104 -0
  13. package/frontend-project/src/components/FormField.jsx +129 -0
  14. package/frontend-project/src/components/Modal.jsx +106 -0
  15. package/frontend-project/src/components/Table.jsx +127 -0
  16. package/frontend-project/src/components/Toast.jsx +64 -0
  17. package/frontend-project/src/components/index.js +14 -0
  18. package/frontend-project/src/config.js +66 -0
  19. package/frontend-project/src/design.js +115 -0
  20. package/frontend-project/src/index.css +60 -0
  21. package/frontend-project/src/layouts/BottomNav.jsx +156 -0
  22. package/frontend-project/src/layouts/TopNav.jsx +150 -0
  23. package/frontend-project/src/layouts/useShell.js +44 -0
  24. package/frontend-project/src/main.jsx +41 -0
  25. package/frontend-project/src/pages/Department.jsx +188 -0
  26. package/frontend-project/src/pages/Employee.jsx +274 -0
  27. package/frontend-project/src/pages/Home.jsx +79 -0
  28. package/frontend-project/src/pages/Profile.jsx +9 -0
  29. package/frontend-project/src/pages/Register.jsx +57 -0
  30. package/frontend-project/src/pages/Reports.jsx +91 -0
  31. package/frontend-project/src/pages/Salary.jsx +264 -0
  32. package/frontend-project/src/themes.js +175 -0
  33. package/package.json +1 -1
@@ -0,0 +1,188 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import { Plus, Pencil, Trash2 } from 'lucide-react';
3
+ import Table from '../components/Table';
4
+ import Modal from '../components/Modal';
5
+ import Button from '../components/Button';
6
+ import FormField from '../components/FormField';
7
+ import { useToast } from '../components/Toast';
8
+ import { departmentApi } from '../api/ApiClient';
9
+
10
+ export default function Department() {
11
+ const toast = useToast();
12
+ const [departments, setDepartments] = useState([]);
13
+ const [loading, setLoading] = useState(true);
14
+ const [createOpen, setCreateOpen] = useState(false);
15
+ const [editTarget, setEditTarget] = useState(null);
16
+ const [deleteTarget, setDeleteTarget] = useState(null);
17
+
18
+ const loadAll = useCallback(async () => {
19
+ setLoading(true);
20
+ try {
21
+ const res = await departmentApi.list();
22
+ setDepartments(res?.data?.departments ?? []);
23
+ } catch (err) {
24
+ toast.error('Failed to load departments');
25
+ } finally {
26
+ setLoading(false);
27
+ }
28
+ }, []);
29
+
30
+ useEffect(() => { loadAll(); }, [loadAll]);
31
+
32
+ const columns = [
33
+ { key: 'departmentCode', label: 'Code' },
34
+ { key: 'departmentName', label: 'Department Name' },
35
+ { key: 'grossSalary', label: 'Gross Salary', render: (v) => `${Number(v).toLocaleString()} RWF` },
36
+ { key: 'totalDeduction', label: 'Deductions', render: (v) => `${Number(v).toLocaleString()} RWF` },
37
+ {
38
+ key: 'actions', label: '', align: 'right',
39
+ render: (_, row) => (
40
+ <div className="flex items-center justify-end gap-1.5">
41
+ <Button size="xs" variant="outline" icon={<Pencil className="w-3 h-3" />} onClick={() => setEditTarget(row)}>Edit</Button>
42
+ <Button size="xs" variant="danger" icon={<Trash2 className="w-3 h-3" />} onClick={() => setDeleteTarget(row)}>Delete</Button>
43
+ </div>
44
+ ),
45
+ },
46
+ ];
47
+
48
+ return (
49
+ <div className="space-y-5">
50
+ <div className="flex items-start justify-between">
51
+ <div>
52
+ <h1 className="text-[18px] font-bold text-gray-800">Departments</h1>
53
+ <p className="text-[13px] text-gray-400 mt-0.5">{departments.length} departments</p>
54
+ </div>
55
+ <Button size="sm" icon={<Plus className="w-3.5 h-3.5" />} onClick={() => setCreateOpen(true)}>Add Department</Button>
56
+ </div>
57
+ <div className="bg-white border border-gray-200 p-5">
58
+ <Table columns={columns} data={departments} rowKey="_id" loading={loading} emptyMessage="No departments yet." maxHeight="480px" />
59
+ </div>
60
+ {createOpen && <CreateModal onClose={() => setCreateOpen(false)} onCreated={() => { setCreateOpen(false); loadAll(); }} />}
61
+ {editTarget && <EditModal department={editTarget} onClose={() => setEditTarget(null)} onUpdated={() => { setEditTarget(null); loadAll(); }} />}
62
+ {deleteTarget && <DeleteModal department={deleteTarget} onClose={() => setDeleteTarget(null)} onDeleted={() => { setDeleteTarget(null); loadAll(); }} />}
63
+ </div>
64
+ );
65
+ }
66
+
67
+ function CreateModal({ onClose, onCreated }) {
68
+ const toast = useToast();
69
+ const [form, setForm] = useState({ departmentCode: '', departmentName: '', grossSalary: '', totalDeduction: '0' });
70
+ const [loading, setLoading] = useState(false);
71
+ const [errors, setErrors] = useState({});
72
+ const set = (f) => (v) => setForm((p) => ({ ...p, [f]: v }));
73
+
74
+ const validate = () => {
75
+ const e = {};
76
+ if (!form.departmentCode) e.departmentCode = 'Required';
77
+ if (!form.departmentName) e.departmentName = 'Required';
78
+ if (!form.grossSalary) e.grossSalary = 'Required';
79
+ if (form.totalDeduction === '') e.totalDeduction = 'Required';
80
+ setErrors(e);
81
+ return Object.keys(e).length === 0;
82
+ };
83
+
84
+ const handleSubmit = async () => {
85
+ if (!validate()) return;
86
+ setLoading(true);
87
+ try {
88
+ await departmentApi.create({ ...form, grossSalary: Number(form.grossSalary), totalDeduction: Number(form.totalDeduction) });
89
+ toast.success('Department created');
90
+ onCreated();
91
+ } catch (err) {
92
+ toast.error(err.message ?? 'Failed to create department');
93
+ } finally {
94
+ setLoading(false);
95
+ }
96
+ };
97
+
98
+ return (
99
+ <Modal open title="Add Department" size="sm" onClose={onClose}>
100
+ <div className="space-y-3">
101
+ <FormField label="Department Code" required error={errors.departmentCode}>
102
+ <FormField.Input value={form.departmentCode} onChange={set('departmentCode')} placeholder="e.g. HR" />
103
+ </FormField>
104
+ <FormField label="Department Name" required error={errors.departmentName}>
105
+ <FormField.Input value={form.departmentName} onChange={set('departmentName')} placeholder="e.g. Human Resources" />
106
+ </FormField>
107
+ <FormField label="Gross Salary (RWF)" required error={errors.grossSalary}>
108
+ <FormField.Input type="number" value={form.grossSalary} onChange={set('grossSalary')} placeholder="300000" />
109
+ </FormField>
110
+ <FormField label="Total Deduction (RWF)" required error={errors.totalDeduction}>
111
+ <FormField.Input type="number" value={form.totalDeduction} onChange={set('totalDeduction')} placeholder="30000" />
112
+ </FormField>
113
+ </div>
114
+ <Modal.Footer>
115
+ <Button variant="outline" onClick={onClose}>Cancel</Button>
116
+ <Button loading={loading} onClick={handleSubmit}>Create Department</Button>
117
+ </Modal.Footer>
118
+ </Modal>
119
+ );
120
+ }
121
+
122
+ function EditModal({ department, onClose, onUpdated }) {
123
+ const toast = useToast();
124
+ const [form, setForm] = useState({
125
+ departmentCode: department.departmentCode || '',
126
+ departmentName: department.departmentName || '',
127
+ grossSalary: department.grossSalary || '',
128
+ totalDeduction: department.totalDeduction ?? '0',
129
+ });
130
+ const [loading, setLoading] = useState(false);
131
+ const set = (f) => (v) => setForm((p) => ({ ...p, [f]: v }));
132
+
133
+ const handleSave = async () => {
134
+ setLoading(true);
135
+ try {
136
+ await departmentApi.update(department._id, { ...form, grossSalary: Number(form.grossSalary), totalDeduction: Number(form.totalDeduction) });
137
+ toast.success('Department updated');
138
+ onUpdated();
139
+ } catch (err) {
140
+ toast.error(err.message ?? 'Update failed');
141
+ } finally {
142
+ setLoading(false);
143
+ }
144
+ };
145
+
146
+ return (
147
+ <Modal open title={`Edit — ${department.departmentName}`} size="sm" onClose={onClose}>
148
+ <div className="space-y-3">
149
+ <FormField label="Department Code"><FormField.Input value={form.departmentCode} onChange={set('departmentCode')} /></FormField>
150
+ <FormField label="Department Name"><FormField.Input value={form.departmentName} onChange={set('departmentName')} /></FormField>
151
+ <FormField label="Gross Salary (RWF)"><FormField.Input type="number" value={form.grossSalary} onChange={set('grossSalary')} /></FormField>
152
+ <FormField label="Total Deduction (RWF)"><FormField.Input type="number" value={form.totalDeduction} onChange={set('totalDeduction')} /></FormField>
153
+ </div>
154
+ <Modal.Footer>
155
+ <Button variant="outline" onClick={onClose}>Cancel</Button>
156
+ <Button loading={loading} onClick={handleSave}>Save Changes</Button>
157
+ </Modal.Footer>
158
+ </Modal>
159
+ );
160
+ }
161
+
162
+ function DeleteModal({ department, onClose, onDeleted }) {
163
+ const toast = useToast();
164
+ const [loading, setLoading] = useState(false);
165
+
166
+ const handleDelete = async () => {
167
+ setLoading(true);
168
+ try {
169
+ await departmentApi.remove(department._id);
170
+ toast.success(`${department.departmentName} removed`);
171
+ onDeleted();
172
+ } catch (err) {
173
+ toast.error(err.message ?? 'Delete failed');
174
+ } finally {
175
+ setLoading(false);
176
+ }
177
+ };
178
+
179
+ return (
180
+ <Modal open title="Confirm Deletion" size="sm" onClose={onClose}>
181
+ <p className="text-[13px] text-gray-600">Delete <strong>{department.departmentName}</strong>? This cannot be undone.</p>
182
+ <Modal.Footer>
183
+ <Button variant="outline" onClick={onClose}>Cancel</Button>
184
+ <Button variant="danger" loading={loading} icon={<Trash2 className="w-3.5 h-3.5" />} onClick={handleDelete}>Delete</Button>
185
+ </Modal.Footer>
186
+ </Modal>
187
+ );
188
+ }
@@ -0,0 +1,274 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import { Plus, Download, Pencil, Trash2 } from 'lucide-react';
3
+ import Table from '../components/Table';
4
+ import Modal from '../components/Modal';
5
+ import Button from '../components/Button';
6
+ import FormField from '../components/FormField';
7
+ import { useToast } from '../components/Toast';
8
+ import { employeeApi, departmentApi, excelApi } from '../api/ApiClient';
9
+
10
+ const formatDate = (iso) => {
11
+ if (!iso) return '—';
12
+ try { return new Date(iso).toISOString().split('T')[0]; } catch { return '—'; }
13
+ };
14
+
15
+ export default function Employee() {
16
+ const toast = useToast();
17
+ const [employees, setEmployees] = useState([]);
18
+ const [departments, setDepartments] = useState([]);
19
+ const [loading, setLoading] = useState(true);
20
+ const [exporting, setExporting] = useState(false);
21
+ const [createOpen, setCreateOpen] = useState(false);
22
+ const [editTarget, setEditTarget] = useState(null);
23
+ const [deleteTarget, setDeleteTarget] = useState(null);
24
+
25
+ const loadAll = useCallback(async () => {
26
+ setLoading(true);
27
+ try {
28
+ const [empRes, deptRes] = await Promise.allSettled([
29
+ employeeApi.list(),
30
+ departmentApi.list(),
31
+ ]);
32
+ if (empRes.status === 'fulfilled') setEmployees(empRes.value?.data?.employees ?? []);
33
+ if (deptRes.status === 'fulfilled') setDepartments(deptRes.value?.data?.departments ?? []);
34
+ } catch (err) {
35
+ toast.error('Failed to load data');
36
+ } finally {
37
+ setLoading(false);
38
+ }
39
+ }, []);
40
+
41
+ useEffect(() => { loadAll(); }, [loadAll]);
42
+
43
+ const handleExport = async () => {
44
+ setExporting(true);
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
+ };
66
+
67
+ const columns = [
68
+ { key: 'employeeNumber', label: 'Emp #' },
69
+ { key: 'firstName', label: 'First Name' },
70
+ { key: 'lastName', label: 'Last Name' },
71
+ { key: 'position', label: 'Position' },
72
+ { key: 'gender', label: 'Gender' },
73
+ { key: 'telephone', label: 'Phone' },
74
+ { key: 'hiredDate', label: 'Hired', render: (v) => formatDate(v) },
75
+ { key: 'department', label: 'Department', render: (v) => v?.departmentName ?? '—' },
76
+ {
77
+ key: 'actions', label: '', align: 'right',
78
+ render: (_, row) => (
79
+ <div className="flex items-center justify-end gap-1.5">
80
+ <Button size="xs" variant="outline" icon={<Pencil className="w-3 h-3" />} onClick={() => setEditTarget(row)}>Edit</Button>
81
+ <Button size="xs" variant="danger" icon={<Trash2 className="w-3 h-3" />} onClick={() => setDeleteTarget(row)}>Delete</Button>
82
+ </div>
83
+ ),
84
+ },
85
+ ];
86
+
87
+ return (
88
+ <div className="space-y-5">
89
+ <div className="flex items-start justify-between">
90
+ <div>
91
+ <h1 className="text-[18px] font-bold text-gray-800">Employees</h1>
92
+ <p className="text-[13px] text-gray-400 mt-0.5">{employees.length} total records</p>
93
+ </div>
94
+ <div className="flex ">
95
+ <Button size="sm" variant='dark' icon={<Plus className="w-3.5 h-3.5" />} onClick={() => setCreateOpen(true)}>Add Employee</Button>
96
+ </div>
97
+ </div>
98
+ <div className="bg-white border border-gray-200 p-5">
99
+ <Table columns={columns} data={employees} rowKey="_id" loading={loading} emptyMessage="No employees yet." maxHeight="480px" />
100
+ </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(); }} />}
103
+ {deleteTarget && <DeleteModal employee={deleteTarget} onClose={() => setDeleteTarget(null)} onDeleted={() => { setDeleteTarget(null); loadAll(); }} />}
104
+ </div>
105
+ );
106
+ }
107
+
108
+ function CreateModal({ departments, onClose, onCreated }) {
109
+ const toast = useToast();
110
+ const now = new Date().toISOString().split('T')[0];
111
+ const [form, setForm] = useState({ employeeNumber: '', firstName: '', lastName: '', position: '', address: '', telephone: '', gender: '', hiredDate: now, department: '' });
112
+ const [loading, setLoading] = useState(false);
113
+ const [errors, setErrors] = useState({});
114
+
115
+ const set = (field) => (val) => setForm((f) => ({ ...f, [field]: val }));
116
+
117
+ const validate = () => {
118
+ const e = {};
119
+ if (!form.employeeNumber) e.employeeNumber = 'Required';
120
+ if (!form.firstName) e.firstName = 'Required';
121
+ if (!form.lastName) e.lastName = 'Required';
122
+ if (!form.position) e.position = 'Required';
123
+ if (!form.gender) e.gender = 'Required';
124
+ if (!form.hiredDate) e.hiredDate = 'Required';
125
+ if (!form.department) e.department = 'Required';
126
+ setErrors(e);
127
+ return Object.keys(e).length === 0;
128
+ };
129
+
130
+ const handleSubmit = async () => {
131
+ if (!validate()) return;
132
+ setLoading(true);
133
+ try {
134
+ await employeeApi.create(form);
135
+ toast.success('Employee created successfully');
136
+ onCreated();
137
+ } catch (err) {
138
+ toast.error(err.message ?? 'Failed to create employee');
139
+ } finally {
140
+ setLoading(false);
141
+ }
142
+ };
143
+
144
+ const deptOptions = departments.map((d) => ({ value: d._id, label: `${d.departmentCode} - ${d.departmentName}` }));
145
+
146
+ return (
147
+ <Modal open title="Add Employee" size="lg" onClose={onClose}>
148
+ <div className="grid grid-cols-2 gap-3">
149
+ <FormField label="Employee #" required error={errors.employeeNumber}>
150
+ <FormField.Input value={form.employeeNumber} onChange={set('employeeNumber')} placeholder="e.g. EMP001" />
151
+ </FormField>
152
+ <FormField label="First Name" required error={errors.firstName}>
153
+ <FormField.Input value={form.firstName} onChange={set('firstName')} placeholder="Jane" />
154
+ </FormField>
155
+ <FormField label="Last Name" required error={errors.lastName}>
156
+ <FormField.Input value={form.lastName} onChange={set('lastName')} placeholder="Doe" />
157
+ </FormField>
158
+ <FormField label="Position" required error={errors.position}>
159
+ <FormField.Select value={form.position} onChange={set('position')} placeholder="Select position" options={[
160
+ { value: 'Admin', label: 'Admin' }, { value: 'Employee', label: 'Employee' }, { value: 'Manager', label: 'Manager' }, { value: 'Mechanic', label: 'Mechanic' }, { value: 'Cashier', label: 'Cashier' },
161
+ ]} />
162
+ </FormField>
163
+ <FormField label="Department" required error={errors.department}>
164
+ <FormField.Select value={form.department} onChange={set('department')} placeholder="Select department" options={deptOptions} />
165
+ </FormField>
166
+ <FormField label="Gender" required error={errors.gender}>
167
+ <FormField.Select value={form.gender} onChange={set('gender')} placeholder="Select gender" options={[
168
+ { value: 'male', label: 'Male' }, { value: 'female', label: 'Female' },
169
+ ]} />
170
+ </FormField>
171
+ <FormField label="Hired Date" required error={errors.hiredDate}>
172
+ <FormField.Input type="date" value={form.hiredDate} onChange={set('hiredDate')} />
173
+ </FormField>
174
+ <FormField label="Phone Number">
175
+ <FormField.Input type="tel" value={form.telephone} onChange={set('telephone')} placeholder="07xxxxxxxx" />
176
+ </FormField>
177
+ <div className="col-span-2">
178
+ <FormField label="Address">
179
+ <FormField.Textarea value={form.address} onChange={set('address')} placeholder="Kigali, Rwanda" />
180
+ </FormField>
181
+ </div>
182
+ </div>
183
+ <Modal.Footer>
184
+ <Button variant="outline" onClick={onClose}>Cancel</Button>
185
+ <Button loading={loading} icon={<Plus className="w-3.5 h-3.5" />} onClick={handleSubmit}>Create Employee</Button>
186
+ </Modal.Footer>
187
+ </Modal>
188
+ );
189
+ }
190
+
191
+ function EditModal({ employee, departments, onClose, onUpdated }) {
192
+ const toast = useToast();
193
+ const [form, setForm] = useState({
194
+ employeeNumber: employee.employeeNumber || '',
195
+ firstName: employee.firstName || '',
196
+ lastName: employee.lastName || '',
197
+ position: employee.position || '',
198
+ address: employee.address || '',
199
+ telephone: employee.telephone || '',
200
+ gender: employee.gender || '',
201
+ hiredDate: employee.hiredDate ? employee.hiredDate.split('T')[0] : '',
202
+ department: employee.department?._id || '',
203
+ });
204
+ const [loading, setLoading] = useState(false);
205
+ const set = (f) => (v) => setForm((p) => ({ ...p, [f]: v }));
206
+ const deptOptions = departments.map((d) => ({ value: d._id, label: `${d.departmentCode} - ${d.departmentName}` }));
207
+
208
+ const handleSave = async () => {
209
+ setLoading(true);
210
+ try {
211
+ await employeeApi.update(employee._id, form);
212
+ toast.success('Employee updated');
213
+ onUpdated();
214
+ } catch (err) {
215
+ toast.error(err.message ?? 'Update failed');
216
+ } finally {
217
+ setLoading(false);
218
+ }
219
+ };
220
+
221
+ return (
222
+ <Modal open title={`Edit — ${employee.firstName} ${employee.lastName}`} size="lg" onClose={onClose}>
223
+ <div className="grid grid-cols-2 gap-3">
224
+ <FormField label="Employee #"><FormField.Input value={form.employeeNumber} onChange={set('employeeNumber')} /></FormField>
225
+ <FormField label="First Name"><FormField.Input value={form.firstName} onChange={set('firstName')} /></FormField>
226
+ <FormField label="Last Name"><FormField.Input value={form.lastName} onChange={set('lastName')} /></FormField>
227
+ <FormField label="Position"><FormField.Input value={form.position} onChange={set('position')} /></FormField>
228
+ <FormField label="Department">
229
+ <FormField.Select value={form.department} onChange={set('department')} options={deptOptions} placeholder="Select department" />
230
+ </FormField>
231
+ <FormField label="Gender">
232
+ <FormField.Select value={form.gender} onChange={set('gender')} options={[{ value: 'male', label: 'Male' }, { value: 'female', label: 'Female' }]} placeholder="Select gender" />
233
+ </FormField>
234
+ <FormField label="Hired Date"><FormField.Input type="date" value={form.hiredDate} onChange={set('hiredDate')} /></FormField>
235
+ <FormField label="Phone"><FormField.Input value={form.telephone} onChange={set('telephone')} /></FormField>
236
+ </div>
237
+ <Modal.Footer>
238
+ <Button variant="outline" onClick={onClose}>Cancel</Button>
239
+ <Button loading={loading} onClick={handleSave}>Save Changes</Button>
240
+ </Modal.Footer>
241
+ </Modal>
242
+ );
243
+ }
244
+
245
+ function DeleteModal({ employee, onClose, onDeleted }) {
246
+ const toast = useToast();
247
+ const [loading, setLoading] = useState(false);
248
+
249
+ const handleDelete = async () => {
250
+ setLoading(true);
251
+ try {
252
+ await employeeApi.remove(employee._id);
253
+ toast.success(`${employee.firstName} ${employee.lastName} removed`);
254
+ onDeleted();
255
+ } catch (err) {
256
+ toast.error(err.message ?? 'Delete failed');
257
+ } finally {
258
+ setLoading(false);
259
+ }
260
+ };
261
+
262
+ return (
263
+ <Modal open title="Confirm Deletion" size="sm" onClose={onClose}>
264
+ <p className="text-[13px] text-gray-600 leading-relaxed">
265
+ Are you sure you want to permanently delete{' '}
266
+ <span className="font-semibold text-gray-800">{employee.firstName} {employee.lastName}</span>?
267
+ </p>
268
+ <Modal.Footer>
269
+ <Button variant="outline" onClick={onClose}>Cancel</Button>
270
+ <Button variant="danger" loading={loading} icon={<Trash2 className="w-3.5 h-3.5" />} onClick={handleDelete}>Delete</Button>
271
+ </Modal.Footer>
272
+ </Modal>
273
+ );
274
+ }
@@ -0,0 +1,79 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { StatCard } from '../components/Card';
3
+ import Table from '../components/Table';
4
+ import { employeeApi, salaryApi, departmentApi } from '../api/ApiClient';
5
+
6
+ const TABLE_COLUMNS = [
7
+ { key: 'employeeNumber', label: 'Emp #' },
8
+ { key: 'firstName', label: 'First Name' },
9
+ { key: 'lastName', label: 'Last Name' },
10
+ { key: 'position', label: 'Position', render: (v) => <Badge>{v}</Badge> },
11
+ { key: 'gender', label: 'Gender' },
12
+ { key: 'hiredDate', label: 'Hired', render: (v) => formatDate(v) },
13
+ { key: 'department', label: 'Department', render: (v) => v?.departmentName ?? '—' },
14
+ ];
15
+
16
+ const formatDate = (iso) => {
17
+ if (!iso) return '—';
18
+ try { return new Date(iso).toISOString().split('T')[0]; } catch { return '—'; }
19
+ };
20
+
21
+ function Badge({ children }) {
22
+ return (
23
+ <span className="px-2 py-0.5 bg-[#E6F7F5] text-[#008A75] text-[11px] font-semibold">
24
+ {children}
25
+ </span>
26
+ );
27
+ }
28
+
29
+ export default function Home() {
30
+ const user = (() => { try { return JSON.parse(localStorage.getItem('user')); } catch { return {}; } })();
31
+ const [employees, setEmployees] = useState([]);
32
+ const [totalCount, setTotalCount] = useState(null);
33
+ const [avgSalary, setAvgSalary] = useState(null);
34
+ const [deptCount, setDeptCount] = useState(null);
35
+ const [loading, setLoading] = useState(true);
36
+
37
+ useEffect(() => {
38
+ const load = async () => {
39
+ setLoading(true);
40
+ try {
41
+ const [empRes, countRes, salaryRes, deptRes] = await Promise.allSettled([
42
+ employeeApi.list(),
43
+ employeeApi.count(),
44
+ salaryApi.average(),
45
+ departmentApi.list(),
46
+ ]);
47
+ if (empRes.status === 'fulfilled') setEmployees(empRes.value?.data?.employees ?? []);
48
+ if (countRes.status === 'fulfilled') setTotalCount(countRes.value?.data?.count ?? 0);
49
+ if (salaryRes.status === 'fulfilled') setAvgSalary(salaryRes.value?.data?.averageSalary ?? 0);
50
+ if (deptRes.status === 'fulfilled') setDeptCount(deptRes.value?.data?.departments?.length ?? 0);
51
+ } finally {
52
+ setLoading(false);
53
+ }
54
+ };
55
+ load();
56
+ }, []);
57
+
58
+ const fmt = (n) => (n != null ? Number(n).toLocaleString() : null);
59
+
60
+ return (
61
+ <div className="space-y-6">
62
+ <div className="flex justify-between">
63
+ <h1 className="text-[18px] font-bold text-gray-800">
64
+ Welcome back, <span className="text-[#008A75]">{user?.name ?? 'Admin'}</span>
65
+ </h1>
66
+ </div>
67
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
68
+ <StatCard label="Total Employees" value={fmt(totalCount)} loading={loading && totalCount == null} className="text-white bg-[#008A75]" />
69
+ <StatCard label="Avg. Net Salary" value={avgSalary != null ? `${Math.round(avgSalary).toLocaleString()} RWF` : null} loading={loading && avgSalary == null} className="text-zinc-900 bg-zinc-50" />
70
+ <StatCard label="Departments" value={fmt(deptCount)} loading={loading && deptCount == null} className="text-white bg-[#008A75]" />
71
+ <StatCard label="Active Today" value={fmt(totalCount)} loading={loading && totalCount == null} className="text-zinc-900 bg-zinc-50" />
72
+ </div>
73
+ <div className="bg-white border border-gray-200 p-5">
74
+ <h2 className="text-[13px] font-semibold text-gray-700 mb-4">Recent Employees</h2>
75
+ <Table columns={TABLE_COLUMNS} data={employees.slice(0, 10)} rowKey="_id" loading={loading} emptyMessage="No employees found" maxHeight="280px" />
76
+ </div>
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react'
2
+
3
+ function Profile() {
4
+ return (
5
+ <div>Profile</div>
6
+ )
7
+ }
8
+
9
+ export default Profile
@@ -0,0 +1,57 @@
1
+ import React, { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { UserPlus } from 'lucide-react';
4
+ import { authApi } from '../api/ApiClient';
5
+ import Button from '../components/Button';
6
+ import FormField from '../components/FormField';
7
+ import { useToast } from '../components/Toast';
8
+
9
+ export default function RegisterDash() {
10
+ const [name, setName] = useState('');
11
+ const [email, setEmail] = useState('');
12
+ const [password, setPassword] = useState('');
13
+ const [confirm, setConfirm] = useState('');
14
+ const [loading, setLoading] = useState(false);
15
+ const navigate = useNavigate();
16
+ const toast = useToast();
17
+
18
+ const handleRegister = async () => {
19
+ if (!name || !email || !password) { toast.error('Please fill in all fields'); return; }
20
+ if (password !== confirm) { toast.error('Passwords do not match'); return; }
21
+ setLoading(true);
22
+ try {
23
+ const data = await authApi.signup({ name: name.trim(), email: email.trim(), password });
24
+ localStorage.setItem('token', data.data?.accessToken ?? '');
25
+ localStorage.setItem('user', JSON.stringify(data.data?.user ?? {}));
26
+ toast.success('Account created successfully');
27
+ navigate('/dashboard/overview');
28
+ } catch (err) {
29
+ toast.error(err.message ?? 'Registration failed');
30
+ } finally {
31
+ setLoading(false);
32
+ }
33
+ };
34
+
35
+ return (
36
+ <div className="max-w-md mx-auto mt-10">
37
+ <h1 className="text-[18px] font-bold text-gray-800 mb-6">Create Dashboard Account</h1>
38
+ <div className="bg-white border border-gray-200 p-6 space-y-4">
39
+ <FormField label="Full Name" required>
40
+ <FormField.Input value={name} onChange={setName} placeholder="Jane Doe" />
41
+ </FormField>
42
+ <FormField label="Email" required>
43
+ <FormField.Input type="email" value={email} onChange={setEmail} placeholder="you@company.com" />
44
+ </FormField>
45
+ <FormField label="Password" required>
46
+ <FormField.Input type="password" value={password} onChange={setPassword} placeholder="Min. 6 characters" />
47
+ </FormField>
48
+ <FormField label="Confirm Password" required>
49
+ <FormField.Input type="password" value={confirm} onChange={setConfirm} placeholder="Repeat password" />
50
+ </FormField>
51
+ <Button fullWidth size="lg" loading={loading} icon={<UserPlus className="w-4 h-4" />} onClick={handleRegister}>
52
+ Create Account
53
+ </Button>
54
+ </div>
55
+ </div>
56
+ );
57
+ }