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
|
@@ -0,0 +1,161 @@
|
|
|
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 { positionApi } from '../api/ApiClient';
|
|
9
|
+
|
|
10
|
+
export default function Position() {
|
|
11
|
+
const toast = useToast();
|
|
12
|
+
const [positions, setPositions] = 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 positionApi.list();
|
|
22
|
+
setPositions(res?.data?.positions ?? []);
|
|
23
|
+
} catch { toast.error('Failed to load positions'); }
|
|
24
|
+
finally { setLoading(false); }
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
useEffect(() => { loadAll(); }, [loadAll]);
|
|
28
|
+
|
|
29
|
+
const columns = [
|
|
30
|
+
{ key: 'posName', label: 'Position Name' },
|
|
31
|
+
{ key: 'requiredQualification', label: 'Required Qualification' },
|
|
32
|
+
{
|
|
33
|
+
key: 'actions', label: '', align: 'right',
|
|
34
|
+
render: (_, row) => (
|
|
35
|
+
<div className="flex items-center justify-end gap-1.5">
|
|
36
|
+
<Button size="xs" variant="outline" icon={<Pencil className="w-3 h-3" />} onClick={() => setEditTarget(row)}>Edit</Button>
|
|
37
|
+
<Button size="xs" variant="danger" icon={<Trash2 className="w-3 h-3" />} onClick={() => setDeleteTarget(row)}>Delete</Button>
|
|
38
|
+
</div>
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="space-y-5">
|
|
45
|
+
<div className="flex items-start justify-between">
|
|
46
|
+
<div>
|
|
47
|
+
<h1 className="text-[18px] font-bold text-gray-800">Positions</h1>
|
|
48
|
+
<p className="text-[13px] text-gray-400 mt-0.5">{positions.length} positions</p>
|
|
49
|
+
</div>
|
|
50
|
+
<Button size="sm" icon={<Plus className="w-3.5 h-3.5" />} onClick={() => setCreateOpen(true)}>Add Position</Button>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="bg-white border border-gray-200 p-5">
|
|
53
|
+
<Table columns={columns} data={positions} rowKey="_id" loading={loading} emptyMessage="No positions yet." maxHeight="480px" />
|
|
54
|
+
</div>
|
|
55
|
+
{createOpen && <CreateModal onClose={() => setCreateOpen(false)} onCreated={() => { setCreateOpen(false); loadAll(); }} />}
|
|
56
|
+
{editTarget && <EditModal position={editTarget} onClose={() => setEditTarget(null)} onUpdated={() => { setEditTarget(null); loadAll(); }} />}
|
|
57
|
+
{deleteTarget && <DeleteModal position={deleteTarget} onClose={() => setDeleteTarget(null)} onDeleted={() => { setDeleteTarget(null); loadAll(); }} />}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function CreateModal({ onClose, onCreated }) {
|
|
63
|
+
const toast = useToast();
|
|
64
|
+
const [form, setForm] = useState({ posName: '', requiredQualification: '' });
|
|
65
|
+
const [loading, setLoading] = useState(false);
|
|
66
|
+
const [errors, setErrors] = useState({});
|
|
67
|
+
const set = (f) => (v) => setForm((p) => ({ ...p, [f]: v }));
|
|
68
|
+
|
|
69
|
+
const validate = () => {
|
|
70
|
+
const e = {};
|
|
71
|
+
if (!form.posName) e.posName = 'Required';
|
|
72
|
+
setErrors(e);
|
|
73
|
+
return Object.keys(e).length === 0;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleSubmit = async () => {
|
|
77
|
+
if (!validate()) return;
|
|
78
|
+
setLoading(true);
|
|
79
|
+
try {
|
|
80
|
+
await positionApi.create(form);
|
|
81
|
+
toast.success('Position created');
|
|
82
|
+
onCreated();
|
|
83
|
+
} catch (err) { toast.error(err.message ?? 'Failed to create position'); }
|
|
84
|
+
finally { setLoading(false); }
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Modal open title="Add Position" size="sm" onClose={onClose}>
|
|
89
|
+
<div className="space-y-3">
|
|
90
|
+
<FormField label="Position Name" required error={errors.posName}>
|
|
91
|
+
<FormField.Input value={form.posName} onChange={set('posName')} placeholder="e.g. Sales Representative" />
|
|
92
|
+
</FormField>
|
|
93
|
+
<FormField label="Required Qualification">
|
|
94
|
+
<FormField.Input value={form.requiredQualification} onChange={set('requiredQualification')} placeholder="e.g. Bachelor in Business" />
|
|
95
|
+
</FormField>
|
|
96
|
+
</div>
|
|
97
|
+
<Modal.Footer>
|
|
98
|
+
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
|
99
|
+
<Button loading={loading} onClick={handleSubmit}>Create Position</Button>
|
|
100
|
+
</Modal.Footer>
|
|
101
|
+
</Modal>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function EditModal({ position, onClose, onUpdated }) {
|
|
106
|
+
const toast = useToast();
|
|
107
|
+
const [form, setForm] = useState({
|
|
108
|
+
posName: position.posName || '',
|
|
109
|
+
requiredQualification: position.requiredQualification || '',
|
|
110
|
+
});
|
|
111
|
+
const [loading, setLoading] = useState(false);
|
|
112
|
+
const set = (f) => (v) => setForm((p) => ({ ...p, [f]: v }));
|
|
113
|
+
|
|
114
|
+
const handleSave = async () => {
|
|
115
|
+
setLoading(true);
|
|
116
|
+
try {
|
|
117
|
+
await positionApi.update(position._id, form);
|
|
118
|
+
toast.success('Position updated');
|
|
119
|
+
onUpdated();
|
|
120
|
+
} catch (err) { toast.error(err.message ?? 'Update failed'); }
|
|
121
|
+
finally { setLoading(false); }
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Modal open title={`Edit — ${position.posName}`} size="sm" onClose={onClose}>
|
|
126
|
+
<div className="space-y-3">
|
|
127
|
+
<FormField label="Position Name"><FormField.Input value={form.posName} onChange={set('posName')} /></FormField>
|
|
128
|
+
<FormField label="Required Qualification"><FormField.Input value={form.requiredQualification} onChange={set('requiredQualification')} /></FormField>
|
|
129
|
+
</div>
|
|
130
|
+
<Modal.Footer>
|
|
131
|
+
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
|
132
|
+
<Button loading={loading} onClick={handleSave}>Save Changes</Button>
|
|
133
|
+
</Modal.Footer>
|
|
134
|
+
</Modal>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function DeleteModal({ position, onClose, onDeleted }) {
|
|
139
|
+
const toast = useToast();
|
|
140
|
+
const [loading, setLoading] = useState(false);
|
|
141
|
+
|
|
142
|
+
const handleDelete = async () => {
|
|
143
|
+
setLoading(true);
|
|
144
|
+
try {
|
|
145
|
+
await positionApi.remove(position._id);
|
|
146
|
+
toast.success(`${position.posName} removed`);
|
|
147
|
+
onDeleted();
|
|
148
|
+
} catch (err) { toast.error(err.message ?? 'Delete failed'); }
|
|
149
|
+
finally { setLoading(false); }
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Modal open title="Confirm Deletion" size="sm" onClose={onClose}>
|
|
154
|
+
<p className="text-[13px] text-gray-600">Delete <strong>{position.posName}</strong>? This cannot be undone.</p>
|
|
155
|
+
<Modal.Footer>
|
|
156
|
+
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
|
157
|
+
<Button variant="danger" loading={loading} icon={<Trash2 className="w-3.5 h-3.5" />} onClick={handleDelete}>Delete</Button>
|
|
158
|
+
</Modal.Footer>
|
|
159
|
+
</Modal>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -1,91 +1,104 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import Table from '../components/Table';
|
|
4
|
-
import Button from '../components/Button';
|
|
5
|
-
import FormField from '../components/FormField';
|
|
2
|
+
import { FileText, Users } from 'lucide-react';
|
|
6
3
|
import { useToast } from '../components/Toast';
|
|
7
|
-
import { reportsApi
|
|
4
|
+
import { reportsApi } from '../api/ApiClient';
|
|
8
5
|
|
|
9
6
|
export default function Reports() {
|
|
10
7
|
const toast = useToast();
|
|
11
|
-
const [
|
|
8
|
+
const [report, setReport] = useState(null);
|
|
12
9
|
const [loading, setLoading] = useState(true);
|
|
13
|
-
const [exporting, setExporting] = useState(false);
|
|
14
|
-
const [month, setMonth] = useState('');
|
|
15
10
|
|
|
16
|
-
const
|
|
11
|
+
const loadReport = useCallback(async () => {
|
|
17
12
|
setLoading(true);
|
|
18
13
|
try {
|
|
19
|
-
const res = await reportsApi.
|
|
20
|
-
|
|
21
|
-
} catch (
|
|
22
|
-
|
|
23
|
-
} finally {
|
|
24
|
-
setLoading(false);
|
|
25
|
-
}
|
|
14
|
+
const res = await reportsApi.employeesOnLeave();
|
|
15
|
+
setReport(res?.data ?? null);
|
|
16
|
+
} catch { toast.error('Failed to load report'); }
|
|
17
|
+
finally { setLoading(false); }
|
|
26
18
|
}, []);
|
|
27
19
|
|
|
28
|
-
useEffect(() => {
|
|
20
|
+
useEffect(() => { loadReport(); }, [loadReport]);
|
|
29
21
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const handleExport = async () => {
|
|
34
|
-
setExporting(true);
|
|
35
|
-
try {
|
|
36
|
-
const rows = payroll.map((r) => ({
|
|
37
|
-
'First Name': r.firstName,
|
|
38
|
-
'Last Name': r.lastName,
|
|
39
|
-
Position: r.position,
|
|
40
|
-
Department: r.departmentName,
|
|
41
|
-
'Gross Salary': `${r.grossSalary?.toLocaleString()} RWF`,
|
|
42
|
-
'Deductions': `${r.totalDeduction?.toLocaleString()} RWF`,
|
|
43
|
-
'Net Salary': `${r.netSalary?.toLocaleString()} RWF`,
|
|
44
|
-
Month: r.month,
|
|
45
|
-
}));
|
|
46
|
-
const filename = month ? `payroll-${month}` : 'payroll-all';
|
|
47
|
-
await excelApi.exportCustom('Payroll', rows, filename);
|
|
48
|
-
toast.success('Payroll report downloaded');
|
|
49
|
-
} catch (err) {
|
|
50
|
-
toast.error(err.message ?? 'Export failed');
|
|
51
|
-
} finally {
|
|
52
|
-
setExporting(false);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const columns = [
|
|
57
|
-
{ key: 'firstName', label: 'First Name' },
|
|
58
|
-
{ key: 'lastName', label: 'Last Name' },
|
|
59
|
-
{ key: 'position', label: 'Position' },
|
|
60
|
-
{ key: 'departmentName', label: 'Department' },
|
|
61
|
-
{ key: 'netSalary', label: 'Net Salary', render: (v) => `${Number(v).toLocaleString()} RWF` },
|
|
62
|
-
{ key: 'month', label: 'Month' },
|
|
63
|
-
];
|
|
22
|
+
const departments = report?.departments ?? {};
|
|
23
|
+
const total = report?.total ?? 0;
|
|
24
|
+
const deptNames = Object.keys(departments);
|
|
64
25
|
|
|
65
26
|
return (
|
|
66
27
|
<div className="space-y-5">
|
|
67
28
|
<div className="flex items-start justify-between">
|
|
68
29
|
<div>
|
|
69
|
-
<h1 className="text-[18px] font-bold text-gray-800">
|
|
70
|
-
<p className="text-[13px] text-gray-400 mt-0.5">
|
|
30
|
+
<h1 className="text-[18px] font-bold text-gray-800">Employee Status Report</h1>
|
|
31
|
+
<p className="text-[13px] text-gray-400 mt-0.5">
|
|
32
|
+
Employees currently on leave — organised by department
|
|
33
|
+
</p>
|
|
71
34
|
</div>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<FormField.Input type="month" value={month} onChange={setMonth} />
|
|
78
|
-
</FormField>
|
|
35
|
+
<div className="flex items-center gap-2 px-4 py-2 bg-yellow-50 border border-yellow-200 rounded-md">
|
|
36
|
+
<Users className="w-4 h-4 text-yellow-600" />
|
|
37
|
+
<span className="text-[13px] font-semibold text-yellow-800">
|
|
38
|
+
Total on leave: {total}
|
|
39
|
+
</span>
|
|
79
40
|
</div>
|
|
80
|
-
<Button size="sm" icon={<FileText className="w-3.5 h-3.5" />} onClick={handleFilter}>Filter</Button>
|
|
81
|
-
{month && <Button size="sm" variant="ghost" onClick={handleClear}>Clear</Button>}
|
|
82
|
-
</div>
|
|
83
|
-
<div className="bg-white border border-gray-200 p-5">
|
|
84
|
-
<h2 className="text-[13px] font-semibold text-gray-700 mb-4">
|
|
85
|
-
{month ? `Payroll for ${month}` : 'All Payroll Records'} ({payroll.length} entries)
|
|
86
|
-
</h2>
|
|
87
|
-
<Table columns={columns} data={payroll} rowKey="_id" loading={loading} emptyMessage="No payroll data found." maxHeight="480px" />
|
|
88
41
|
</div>
|
|
42
|
+
|
|
43
|
+
{loading ? (
|
|
44
|
+
<div className="space-y-4">
|
|
45
|
+
{[1, 2, 3].map((i) => (
|
|
46
|
+
<div key={i} className="bg-white border border-gray-200 p-5">
|
|
47
|
+
<div className="h-5 w-48 animate-pulse bg-gray-200 rounded mb-4" />
|
|
48
|
+
<div className="space-y-2">
|
|
49
|
+
{[1, 2].map((j) => (
|
|
50
|
+
<div key={j} className="h-10 animate-pulse bg-gray-100 rounded" />
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
) : deptNames.length === 0 ? (
|
|
57
|
+
<div className="bg-white border border-gray-200 p-10 text-center">
|
|
58
|
+
<FileText className="w-10 h-10 text-gray-300 mx-auto mb-3" />
|
|
59
|
+
<p className="text-[14px] text-gray-500">No employees are currently on leave.</p>
|
|
60
|
+
</div>
|
|
61
|
+
) : (
|
|
62
|
+
deptNames.map((deptName) => {
|
|
63
|
+
const emps = departments[deptName];
|
|
64
|
+
return (
|
|
65
|
+
<div key={deptName} className="bg-white border border-gray-200 overflow-hidden">
|
|
66
|
+
<div className="px-5 py-3 border-b border-gray-100" style={{ backgroundColor: 'var(--color-surface)' }}>
|
|
67
|
+
<h2 className="text-[14px] font-bold text-gray-700">{deptName}</h2>
|
|
68
|
+
<p className="text-[11px] text-gray-400">{emps.length} employee{emps.length > 1 ? 's' : ''} on leave</p>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="overflow-x-auto">
|
|
71
|
+
<table className="w-full">
|
|
72
|
+
<thead>
|
|
73
|
+
<tr className="border-b border-gray-100">
|
|
74
|
+
<th className="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-left text-gray-500">#</th>
|
|
75
|
+
<th className="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-left text-gray-500">First Name</th>
|
|
76
|
+
<th className="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-left text-gray-500">Last Name</th>
|
|
77
|
+
<th className="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-left text-gray-500">Gender</th>
|
|
78
|
+
<th className="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-left text-gray-500">Position</th>
|
|
79
|
+
<th className="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-left text-gray-500">Email</th>
|
|
80
|
+
<th className="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-left text-gray-500">Phone</th>
|
|
81
|
+
</tr>
|
|
82
|
+
</thead>
|
|
83
|
+
<tbody className="divide-y divide-gray-50">
|
|
84
|
+
{emps.map((emp, idx) => (
|
|
85
|
+
<tr key={emp._id} className="hover:bg-gray-50 transition-colors">
|
|
86
|
+
<td className="px-5 py-3 text-[12px] text-gray-400 font-mono">{idx + 1}</td>
|
|
87
|
+
<td className="px-5 py-3 text-[13px] font-medium text-gray-800">{emp.empFirstName}</td>
|
|
88
|
+
<td className="px-5 py-3 text-[13px] font-medium text-gray-800">{emp.empLastName}</td>
|
|
89
|
+
<td className="px-5 py-3 text-[12px] text-gray-600 capitalize">{emp.empGender}</td>
|
|
90
|
+
<td className="px-5 py-3 text-[12px] text-gray-600">{emp.position?.posName || '\u2014'}</td>
|
|
91
|
+
<td className="px-5 py-3 text-[12px] text-gray-600">{emp.empEmail || '\u2014'}</td>
|
|
92
|
+
<td className="px-5 py-3 text-[12px] text-gray-600">{emp.empTelephone || '\u2014'}</td>
|
|
93
|
+
</tr>
|
|
94
|
+
))}
|
|
95
|
+
</tbody>
|
|
96
|
+
</table>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
})
|
|
101
|
+
)}
|
|
89
102
|
</div>
|
|
90
103
|
);
|
|
91
104
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alpe-temp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Employee Payroll Management System (EPMS) - Full-stack Express + React app",
|
|
5
5
|
"main": "bin/epms.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"bcryptjs": "^2.4.3",
|
|
18
|
+
"connect-mongo": "^6.0.0",
|
|
18
19
|
"cors": "^2.8.5",
|
|
19
20
|
"dotenv": "^16.4.5",
|
|
20
21
|
"exceljs": "^4.4.0",
|
|
21
22
|
"express": "^4.19.2",
|
|
22
|
-
"
|
|
23
|
+
"express-session": "^1.19.0",
|
|
23
24
|
"helmet": "^7.1.0",
|
|
24
25
|
"jsonwebtoken": "^9.0.2",
|
|
26
|
+
"mongoose": "^8.5.0",
|
|
25
27
|
"uuid": "^10.0.0"
|
|
26
28
|
},
|
|
27
29
|
"repository": {
|
|
File without changes
|
|
File without changes
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// 📦 EXAMPLE CONTROLLER — Handles HTTP requests and sends responses
|
|
3
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
-
//
|
|
5
|
-
// WHAT THIS FILE DOES:
|
|
6
|
-
// - Receives incoming HTTP requests (from routes)
|
|
7
|
-
// - Calls the service to do the actual work
|
|
8
|
-
// - Sends back a JSON response to the client
|
|
9
|
-
//
|
|
10
|
-
// HOW IT WORKS:
|
|
11
|
-
// 1. The route calls a controller function (e.g. create)
|
|
12
|
-
// 2. The controller reads the request data (req.body, req.params)
|
|
13
|
-
// 3. It calls the service to handle the database operation
|
|
14
|
-
// 4. It sends back a success or error response
|
|
15
|
-
//
|
|
16
|
-
// RESPONSE HELPERS (from utils/response.js):
|
|
17
|
-
// res_.success(res, data, message) → 200 OK
|
|
18
|
-
// res_.created(res, data, message) → 201 Created
|
|
19
|
-
// res_.error(res, message, statusCode) → any error
|
|
20
|
-
// res_.notFound(res, message) → 404
|
|
21
|
-
//
|
|
22
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
-
|
|
24
|
-
const ExampleService = require('./example.service');
|
|
25
|
-
const res_ = require('../../utils/response');
|
|
26
|
-
|
|
27
|
-
const ExampleController = {
|
|
28
|
-
// ─── POST /api/examples (add new) ────────────────────────────────────
|
|
29
|
-
async create(req, res) {
|
|
30
|
-
try {
|
|
31
|
-
const record = await ExampleService.create(req.body);
|
|
32
|
-
return res_.created(res, record, 'Record created');
|
|
33
|
-
} catch (err) {
|
|
34
|
-
return res_.error(res, err.message, err.statusCode || 500);
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
// ─── GET /api/examples (list all) ────────────────────────────────────
|
|
39
|
-
async list(req, res) {
|
|
40
|
-
try {
|
|
41
|
-
const records = await ExampleService.list();
|
|
42
|
-
return res_.success(res, { records });
|
|
43
|
-
} catch (err) {
|
|
44
|
-
return res_.error(res, err.message, 500);
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
// ─── GET /api/examples/:id (get one) ─────────────────────────────────
|
|
49
|
-
async getById(req, res) {
|
|
50
|
-
try {
|
|
51
|
-
const record = await ExampleService.getById(req.params.id);
|
|
52
|
-
if (!record) return res_.notFound(res, 'Record not found');
|
|
53
|
-
return res_.success(res, record);
|
|
54
|
-
} catch (err) {
|
|
55
|
-
return res_.error(res, err.message, 500);
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
// ─── PUT /api/examples/:id (update) ──────────────────────────────────
|
|
60
|
-
async update(req, res) {
|
|
61
|
-
try {
|
|
62
|
-
const record = await ExampleService.update(req.params.id, req.body);
|
|
63
|
-
if (!record) return res_.notFound(res, 'Record not found');
|
|
64
|
-
return res_.success(res, record, 'Record updated');
|
|
65
|
-
} catch (err) {
|
|
66
|
-
return res_.error(res, err.message, 500);
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
// ─── DELETE /api/examples/:id (delete) ───────────────────────────────
|
|
71
|
-
async remove(req, res) {
|
|
72
|
-
try {
|
|
73
|
-
const record = await ExampleService.remove(req.params.id);
|
|
74
|
-
if (!record) return res_.notFound(res, 'Record not found');
|
|
75
|
-
return res_.success(res, null, 'Record deleted');
|
|
76
|
-
} catch (err) {
|
|
77
|
-
return res_.error(res, err.message, 500);
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
module.exports = ExampleController;
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// 📦 EXAMPLE MODEL — Mongoose schema (database structure)
|
|
3
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
-
//
|
|
5
|
-
// WHAT THIS FILE DOES:
|
|
6
|
-
// Defines the shape of your data in MongoDB.
|
|
7
|
-
// Each field = a column in the database.
|
|
8
|
-
//
|
|
9
|
-
// HOW TO USE:
|
|
10
|
-
// 1. Rename "Example" to your feature name (e.g. "Product")
|
|
11
|
-
// 2. Change the fields below to match your data
|
|
12
|
-
// 3. That's it! The model is ready to use in your service.
|
|
13
|
-
//
|
|
14
|
-
// EXAMPLE FIELDS (replace these):
|
|
15
|
-
// name: { type: String, required: true } ← text field
|
|
16
|
-
// price: { type: Number, default: 0 } ← number field
|
|
17
|
-
// category: { type: String, enum: ['a','b'] } ← dropdown/choice
|
|
18
|
-
// isActive: { type: Boolean, default: true } ← yes/no
|
|
19
|
-
// createdAt: added automatically by timestamps ← date
|
|
20
|
-
//
|
|
21
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
-
|
|
23
|
-
const mongoose = require('mongoose');
|
|
24
|
-
|
|
25
|
-
const exampleSchema = new mongoose.Schema(
|
|
26
|
-
{
|
|
27
|
-
// ── YOUR FIELDS HERE ────────────────────────────────────────────────
|
|
28
|
-
name: {
|
|
29
|
-
type: String,
|
|
30
|
-
required: true,
|
|
31
|
-
trim: true,
|
|
32
|
-
},
|
|
33
|
-
description: {
|
|
34
|
-
type: String,
|
|
35
|
-
trim: true,
|
|
36
|
-
},
|
|
37
|
-
status: {
|
|
38
|
-
type: String,
|
|
39
|
-
enum: ['active', 'inactive'],
|
|
40
|
-
default: 'active',
|
|
41
|
-
},
|
|
42
|
-
// Add more fields as needed
|
|
43
|
-
},
|
|
44
|
-
{ timestamps: true } // adds createdAt and updatedAt automatically
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
module.exports = mongoose.model('Example', exampleSchema);
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// 📦 EXAMPLE ROUTES — Maps URLs to controller functions
|
|
3
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
-
//
|
|
5
|
-
// WHAT THIS FILE DOES:
|
|
6
|
-
// - Connects a URL path to a controller function
|
|
7
|
-
// - Example: GET /api/examples → ExampleController.list
|
|
8
|
-
//
|
|
9
|
-
// HOW TO ADD A NEW ROUTE:
|
|
10
|
-
// router.get('/search', ExampleController.search) ← GET request
|
|
11
|
-
// router.post('/', ExampleController.create) ← POST request
|
|
12
|
-
// router.put('/:id', ExampleController.update) ← PUT request
|
|
13
|
-
// router.delete('/:id', ExampleController.remove) ← DELETE request
|
|
14
|
-
//
|
|
15
|
-
// COMMON HTTP METHODS:
|
|
16
|
-
// GET → fetch data
|
|
17
|
-
// POST → create new data
|
|
18
|
-
// PUT → update existing data
|
|
19
|
-
// DELETE → remove data
|
|
20
|
-
//
|
|
21
|
-
// AUTHENTICATION:
|
|
22
|
-
// - Add `authenticate` before any route to require login
|
|
23
|
-
// - Example: router.get('/', authenticate, ExampleController.list)
|
|
24
|
-
//
|
|
25
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
-
|
|
27
|
-
const { Router } = require('express');
|
|
28
|
-
const ExampleController = require('./example.controller');
|
|
29
|
-
const { authenticate } = require('../../middleware/auth.middleware');
|
|
30
|
-
|
|
31
|
-
const router = Router();
|
|
32
|
-
|
|
33
|
-
// ─── All routes require authentication ─────────────────────────────────────
|
|
34
|
-
router.use(authenticate);
|
|
35
|
-
|
|
36
|
-
// ─── CRUD ROUTES ────────────────────────────────────────────────────────────
|
|
37
|
-
router.post('/', ExampleController.create); // POST /api/examples
|
|
38
|
-
router.get('/', ExampleController.list); // GET /api/examples
|
|
39
|
-
router.get('/:id', ExampleController.getById); // GET /api/examples/:id
|
|
40
|
-
router.put('/:id', ExampleController.update); // PUT /api/examples/:id
|
|
41
|
-
router.delete('/:id', ExampleController.remove); // DELETE /api/examples/:id
|
|
42
|
-
|
|
43
|
-
module.exports = router;
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// 📦 EXAMPLE SERVICE — Business logic (talks to database)
|
|
3
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
-
//
|
|
5
|
-
// WHAT THIS FILE DOES:
|
|
6
|
-
// - Every function here does one job (create, list, get, update, delete)
|
|
7
|
-
// - The controller calls these functions
|
|
8
|
-
// - The model is used here to query MongoDB
|
|
9
|
-
//
|
|
10
|
-
// HOW TO ADD A NEW FUNCTION:
|
|
11
|
-
// Just add a new method to the object below.
|
|
12
|
-
// Example: async search(query) { ... }
|
|
13
|
-
//
|
|
14
|
-
// AVAILABLE FUNCTIONS (customize these):
|
|
15
|
-
// create(data) → adds a new record
|
|
16
|
-
// list() → gets all records
|
|
17
|
-
// getById(id) → gets one record by ID
|
|
18
|
-
// update(id, data) → updates a record
|
|
19
|
-
// remove(id) → deletes a record
|
|
20
|
-
//
|
|
21
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
-
|
|
23
|
-
const Example = require('./example.model');
|
|
24
|
-
|
|
25
|
-
const ExampleService = {
|
|
26
|
-
// ─── CREATE ───────────────────────────────────────────────────────────
|
|
27
|
-
async create(data) {
|
|
28
|
-
const record = await Example.create(data);
|
|
29
|
-
return record;
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
// ─── LIST ALL ─────────────────────────────────────────────────────────
|
|
33
|
-
async list() {
|
|
34
|
-
return Example.find().sort({ createdAt: -1 });
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
// ─── GET ONE ──────────────────────────────────────────────────────────
|
|
38
|
-
async getById(id) {
|
|
39
|
-
return Example.findById(id);
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
// ─── UPDATE ───────────────────────────────────────────────────────────
|
|
43
|
-
async update(id, data) {
|
|
44
|
-
return Example.findByIdAndUpdate(id, data, { new: true });
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
// ─── DELETE ───────────────────────────────────────────────────────────
|
|
48
|
-
async remove(id) {
|
|
49
|
-
return Example.findByIdAndDelete(id);
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
// ─── COUNT ────────────────────────────────────────────────────────────
|
|
53
|
-
async count() {
|
|
54
|
-
return Example.countDocuments();
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
module.exports = ExampleService;
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
const ExcelService = require('./excel.service');
|
|
2
|
-
const res_ = require('../../utils/response');
|
|
3
|
-
|
|
4
|
-
const ExcelController = {
|
|
5
|
-
/**
|
|
6
|
-
* GET /api/excel/export/users
|
|
7
|
-
* Demo: exports a users sheet directly to the browser.
|
|
8
|
-
* Replace `sampleData` with a real DB query.
|
|
9
|
-
*/
|
|
10
|
-
async exportUsers(req, res) {
|
|
11
|
-
try {
|
|
12
|
-
const sampleData = [
|
|
13
|
-
{ id: 'u-001', name: 'Alice Martin', email: 'alice@example.com', role: 'admin', joined: new Date('2024-01-15'), revenue: 4800 },
|
|
14
|
-
{ id: 'u-002', name: 'Bob Karenzi', email: 'bob@example.com', role: 'user', joined: new Date('2024-03-22'), revenue: 1200 },
|
|
15
|
-
{ id: 'u-003', name: 'Claire Umutesi', email: 'claire@example.com', role: 'user', joined: new Date('2024-07-09'), revenue: 2500 },
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
const sheets = [
|
|
19
|
-
{
|
|
20
|
-
name: 'Users',
|
|
21
|
-
columns: [
|
|
22
|
-
{ header: 'ID', key: 'id', width: 10 },
|
|
23
|
-
{ header: 'Name', key: 'name', width: 25 },
|
|
24
|
-
{ header: 'Email', key: 'email', width: 35 },
|
|
25
|
-
{ header: 'Role', key: 'role', width: 12 },
|
|
26
|
-
{ header: 'Joined', key: 'joined', width: 14, type: 'date' },
|
|
27
|
-
{ header: 'Revenue', key: 'revenue', width: 15, type: 'currency' },
|
|
28
|
-
],
|
|
29
|
-
rows: sampleData,
|
|
30
|
-
totals: { revenue: `=SUM(F2:F${sampleData.length + 1})` },
|
|
31
|
-
},
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
await ExcelService.exportToResponse(res, 'users-report', sheets);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
return res_.error(res, err.message);
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* GET /api/excel/export/custom
|
|
42
|
-
* Generic export: pass `sheetName` + `data` as JSON body or query params.
|
|
43
|
-
* Useful for ad-hoc client-driven exports.
|
|
44
|
-
*/
|
|
45
|
-
async exportCustom(req, res) {
|
|
46
|
-
try {
|
|
47
|
-
const { sheetName = 'Export', data = [] } = req.body;
|
|
48
|
-
|
|
49
|
-
if (!Array.isArray(data) || !data.length) {
|
|
50
|
-
return res_.badRequest(res, '`data` must be a non-empty array');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const sheet = ExcelService.buildSheetDef(sheetName, data);
|
|
54
|
-
await ExcelService.exportToResponse(res, sheetName, [sheet]);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
return res_.error(res, err.message);
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
module.exports = ExcelController;
|