alpe-temp 1.0.0 → 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 (34) hide show
  1. package/bin/epms.js +149 -60
  2. package/frontend-project/src/App.css +1 -0
  3. package/frontend-project/src/Auth/Login.jsx +85 -0
  4. package/frontend-project/src/Auth/Register.jsx +183 -0
  5. package/frontend-project/src/Intro.jsx +33 -0
  6. package/frontend-project/src/LayOut.jsx +38 -0
  7. package/frontend-project/src/api/ApiClient.js +92 -0
  8. package/frontend-project/src/assets/hero.png +0 -0
  9. package/frontend-project/src/assets/react.svg +1 -0
  10. package/frontend-project/src/assets/vite.svg +1 -0
  11. package/frontend-project/src/components/Aside.jsx +9 -0
  12. package/frontend-project/src/components/Button.jsx +100 -0
  13. package/frontend-project/src/components/Card.jsx +104 -0
  14. package/frontend-project/src/components/FormField.jsx +129 -0
  15. package/frontend-project/src/components/Modal.jsx +106 -0
  16. package/frontend-project/src/components/Table.jsx +127 -0
  17. package/frontend-project/src/components/Toast.jsx +64 -0
  18. package/frontend-project/src/components/index.js +14 -0
  19. package/frontend-project/src/config.js +66 -0
  20. package/frontend-project/src/design.js +115 -0
  21. package/frontend-project/src/index.css +60 -0
  22. package/frontend-project/src/layouts/BottomNav.jsx +156 -0
  23. package/frontend-project/src/layouts/TopNav.jsx +150 -0
  24. package/frontend-project/src/layouts/useShell.js +44 -0
  25. package/frontend-project/src/main.jsx +41 -0
  26. package/frontend-project/src/pages/Department.jsx +188 -0
  27. package/frontend-project/src/pages/Employee.jsx +274 -0
  28. package/frontend-project/src/pages/Home.jsx +79 -0
  29. package/frontend-project/src/pages/Profile.jsx +9 -0
  30. package/frontend-project/src/pages/Register.jsx +57 -0
  31. package/frontend-project/src/pages/Reports.jsx +91 -0
  32. package/frontend-project/src/pages/Salary.jsx +264 -0
  33. package/frontend-project/src/themes.js +175 -0
  34. package/package.json +1 -1
@@ -0,0 +1,264 @@
1
+ import React, { useCallback, useEffect, useMemo, 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 { salaryApi, employeeApi } from '../api/ApiClient';
9
+
10
+ const getCurrentMonth = () => {
11
+ const d = new Date();
12
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
13
+ };
14
+
15
+ export default function Salary() {
16
+ const toast = useToast();
17
+ const [salaries, setSalaries] = useState([]);
18
+ const [employees, setEmployees] = useState([]);
19
+ const [loading, setLoading] = useState(true);
20
+ const [createOpen, setCreateOpen] = useState(false);
21
+ const [editTarget, setEditTarget] = useState(null);
22
+ const [deleteTarget, setDeleteTarget] = useState(null);
23
+
24
+ const loadAll = useCallback(async () => {
25
+ setLoading(true);
26
+ try {
27
+ const [salRes, empRes] = await Promise.allSettled([
28
+ salaryApi.list(),
29
+ employeeApi.list(),
30
+ ]);
31
+ if (salRes.status === 'fulfilled') setSalaries(salRes.value?.data?.salaries ?? []);
32
+ if (empRes.status === 'fulfilled') setEmployees(empRes.value?.data?.employees ?? []);
33
+ } catch (err) {
34
+ toast.error('Failed to load data');
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ }, []);
39
+
40
+ useEffect(() => { loadAll(); }, [loadAll]);
41
+
42
+ const columns = [
43
+ {
44
+ key: 'employee', label: 'Employee',
45
+ render: (v) => v ? `${v.firstName} ${v.lastName}` : '—',
46
+ },
47
+ {
48
+ key: 'dept', label: 'Department',
49
+ render: (_, row) => row?.employee?.department?.departmentName || '—',
50
+ },
51
+ { key: 'grossSalary', label: 'Gross Salary', render: (v) => `${Number(v).toLocaleString()} RWF` },
52
+ { key: 'totalDeduction', label: 'Deductions', render: (v) => `${Number(v).toLocaleString()} RWF` },
53
+ { key: 'netSalary', label: 'Net Salary', render: (v) => `${Number(v).toLocaleString()} RWF` },
54
+ { key: 'month', label: 'Month' },
55
+ {
56
+ key: 'actions', label: '', align: 'right',
57
+ render: (_, row) => (
58
+ <div className="flex items-center justify-end gap-1.5">
59
+ <Button size="xs" variant="outline" icon={<Pencil className="w-3 h-3" />} onClick={() => setEditTarget(row)}>Edit</Button>
60
+ <Button size="xs" variant="danger" icon={<Trash2 className="w-3 h-3" />} onClick={() => setDeleteTarget(row)}>Delete</Button>
61
+ </div>
62
+ ),
63
+ },
64
+ ];
65
+
66
+ return (
67
+ <div className="space-y-5">
68
+ <div className="flex items-start justify-between">
69
+ <div>
70
+ <h1 className="text-[18px] font-bold text-gray-800">Salary Management</h1>
71
+ <p className="text-[13px] text-gray-400 mt-0.5">{salaries.length} salary records</p>
72
+ </div>
73
+ <Button size="sm" className='bg-[#008A75]' icon={<Plus className="w-3.5 h-3.5" />} onClick={() => setCreateOpen(true)}>Add Salary Record</Button>
74
+ </div>
75
+ <div className="bg-white border border-gray-200 p-5">
76
+ <Table columns={columns} data={salaries} rowKey="_id" loading={loading} emptyMessage="No salary records yet." maxHeight="480px" />
77
+ </div>
78
+ {createOpen && <CreateModal employees={employees} onClose={() => setCreateOpen(false)} onCreated={() => { setCreateOpen(false); loadAll(); }} />}
79
+ {editTarget && <EditModal salary={editTarget} employees={employees} onClose={() => setEditTarget(null)} onUpdated={() => { setEditTarget(null); loadAll(); }} />}
80
+ {deleteTarget && <DeleteModal salary={deleteTarget} onClose={() => setDeleteTarget(null)} onDeleted={() => { setDeleteTarget(null); loadAll(); }} />}
81
+ </div>
82
+ );
83
+ }
84
+
85
+ function CreateModal({ employees, onClose, onCreated }) {
86
+ const toast = useToast();
87
+ const [form, setForm] = useState({ employee: '', grossSalary: '', totalDeduction: '0', month: getCurrentMonth() });
88
+ const [loading, setLoading] = useState(false);
89
+ const [errors, setErrors] = useState({});
90
+ const set = (f) => (v) => setForm((p) => ({ ...p, [f]: v }));
91
+ const gross = Number(form.grossSalary) || 0;
92
+ const deduction = Number(form.totalDeduction) || 0;
93
+ const net = gross - deduction;
94
+
95
+ const empMap = useMemo(() => {
96
+ const m = {};
97
+ employees.forEach((e) => { m[e._id] = e; });
98
+ return m;
99
+ }, [employees]);
100
+
101
+ useEffect(() => {
102
+ if (form.employee) {
103
+ const emp = empMap[form.employee];
104
+ if (emp?.department) {
105
+ setForm((p) => ({
106
+ ...p,
107
+ grossSalary: emp.department.grossSalary ?? p.grossSalary,
108
+ totalDeduction: emp.department.totalDeduction ?? p.totalDeduction,
109
+ }));
110
+ }
111
+ }
112
+ }, [form.employee, empMap]);
113
+
114
+ const validate = () => {
115
+ const e = {};
116
+ if (!form.employee) e.employee = 'Required';
117
+ if (!form.grossSalary) e.grossSalary = 'Required';
118
+ if (form.totalDeduction === '') e.totalDeduction = 'Required';
119
+ if (!form.month) e.month = 'Required';
120
+ setErrors(e);
121
+ return Object.keys(e).length === 0;
122
+ };
123
+
124
+ const handleSubmit = async () => {
125
+ if (!validate()) return;
126
+ setLoading(true);
127
+ try {
128
+ await salaryApi.create({ employee: form.employee, grossSalary: gross, totalDeduction: deduction, netSalary: net, month: form.month });
129
+ toast.success('Salary record created');
130
+ onCreated();
131
+ } catch (err) {
132
+ toast.error(err.message ?? 'Failed to create salary record');
133
+ } finally {
134
+ setLoading(false);
135
+ }
136
+ };
137
+
138
+ const empOptions = employees.map((e) => ({ value: e._id, label: `${e.employeeNumber} - ${e.firstName} ${e.lastName}` }));
139
+
140
+ return (
141
+ <Modal open title="Add Salary Record" size="sm" onClose={onClose}>
142
+ <div className="space-y-3">
143
+ <FormField label="Employee" required error={errors.employee}>
144
+ <FormField.Select value={form.employee} onChange={set('employee')} placeholder="Select employee" options={empOptions} />
145
+ </FormField>
146
+ <FormField label="Gross Salary (RWF)" required error={errors.grossSalary}>
147
+ <div className="px-3 py-2 bg-gray-50 text-[14px] font-semibold text-gray-700 border border-gray-200">{gross.toLocaleString()} RWF</div>
148
+ </FormField>
149
+ <FormField label="Total Deduction (RWF)" required error={errors.totalDeduction}>
150
+ <div className="px-3 py-2 bg-gray-50 text-[14px] font-semibold text-gray-700 border border-gray-200">{deduction.toLocaleString()} RWF</div>
151
+ </FormField>
152
+ <FormField label="Net Salary (RWF)">
153
+ <div className="px-3 py-2 bg-gray-50 text-[14px] font-semibold text-gray-700 border border-gray-200">{net.toLocaleString()} RWF</div>
154
+ </FormField>
155
+ <FormField label="Month" required error={errors.month}>
156
+ <FormField.Input type="month" value={form.month} onChange={set('month')} />
157
+ </FormField>
158
+ </div>
159
+ <Modal.Footer>
160
+ <Button variant="outline" onClick={onClose}>Cancel</Button>
161
+ <Button loading={loading} onClick={handleSubmit}>Create Record</Button>
162
+ </Modal.Footer>
163
+ </Modal>
164
+ );
165
+ }
166
+
167
+ function EditModal({ salary, employees, onClose, onUpdated }) {
168
+ const toast = useToast();
169
+ const [form, setForm] = useState({ employee: salary.employee?._id || '', grossSalary: salary.grossSalary || '', totalDeduction: salary.totalDeduction || '', month: salary.month || '' });
170
+ const [loading, setLoading] = useState(false);
171
+ const set = (f) => (v) => setForm((p) => ({ ...p, [f]: v }));
172
+ const gross = Number(form.grossSalary) || 0;
173
+ const deduction = Number(form.totalDeduction) || 0;
174
+ const net = gross - deduction;
175
+ const empOptions = employees.map((e) => ({ value: e._id, label: `${e.employeeNumber} - ${e.firstName} ${e.lastName}` }));
176
+
177
+ const empMap = useMemo(() => {
178
+ const m = {};
179
+ employees.forEach((e) => { m[e._id] = e; });
180
+ return m;
181
+ }, [employees]);
182
+
183
+ useEffect(() => {
184
+ if (form.employee) {
185
+ const emp = empMap[form.employee];
186
+ if (emp?.department) {
187
+ setForm((p) => ({
188
+ ...p,
189
+ grossSalary: emp.department.grossSalary ?? p.grossSalary,
190
+ totalDeduction: emp.department.totalDeduction ?? p.totalDeduction,
191
+ }));
192
+ }
193
+ }
194
+ }, [form.employee, empMap]);
195
+
196
+ const handleSave = async () => {
197
+ setLoading(true);
198
+ try {
199
+ await salaryApi.update(salary._id, { employee: form.employee, grossSalary: gross, totalDeduction: deduction, netSalary: net, month: form.month });
200
+ toast.success('Salary record updated');
201
+ onUpdated();
202
+ } catch (err) {
203
+ toast.error(err.message ?? 'Update failed');
204
+ } finally {
205
+ setLoading(false);
206
+ }
207
+ };
208
+
209
+ return (
210
+ <Modal open title="Edit Salary Record" size="sm" onClose={onClose}>
211
+ <div className="space-y-3">
212
+ <FormField label="Employee">
213
+ <FormField.Select value={form.employee} onChange={set('employee')} options={empOptions} placeholder="Select employee" />
214
+ </FormField>
215
+ <FormField label="Gross Salary (RWF)">
216
+ <div className="px-3 py-2 bg-gray-50 text-[14px] font-semibold text-gray-700 border border-gray-200">{gross.toLocaleString()} RWF</div>
217
+ </FormField>
218
+ <FormField label="Total Deduction (RWF)">
219
+ <div className="px-3 py-2 bg-gray-50 text-[14px] font-semibold text-gray-700 border border-gray-200">{deduction.toLocaleString()} RWF</div>
220
+ </FormField>
221
+ <FormField label="Net Salary (RWF)">
222
+ <div className="px-3 py-2 bg-gray-50 text-[14px] font-semibold text-gray-700 border border-gray-200">{net.toLocaleString()} RWF</div>
223
+ </FormField>
224
+ <FormField label="Month">
225
+ <FormField.Input type="month" value={form.month} onChange={set('month')} />
226
+ </FormField>
227
+ </div>
228
+ <Modal.Footer>
229
+ <Button variant="outline" onClick={onClose}>Cancel</Button>
230
+ <Button loading={loading} onClick={handleSave}>Save Changes</Button>
231
+ </Modal.Footer>
232
+ </Modal>
233
+ );
234
+ }
235
+
236
+ function DeleteModal({ salary, onClose, onDeleted }) {
237
+ const toast = useToast();
238
+ const [loading, setLoading] = useState(false);
239
+
240
+ const handleDelete = async () => {
241
+ setLoading(true);
242
+ try {
243
+ await salaryApi.remove(salary._id);
244
+ toast.success('Salary record deleted');
245
+ onDeleted();
246
+ } catch (err) {
247
+ toast.error(err.message ?? 'Delete failed');
248
+ } finally {
249
+ setLoading(false);
250
+ }
251
+ };
252
+
253
+ return (
254
+ <Modal open title="Confirm Deletion" size="sm" onClose={onClose}>
255
+ <p className="text-[13px] text-gray-600">
256
+ Delete salary record for <strong>{salary.employee?.firstName} {salary.employee?.lastName}</strong> ({salary.month})? This cannot be undone.
257
+ </p>
258
+ <Modal.Footer>
259
+ <Button variant="outline" onClick={onClose}>Cancel</Button>
260
+ <Button variant="danger" loading={loading} icon={<Trash2 className="w-3.5 h-3.5" />} onClick={handleDelete}>Delete</Button>
261
+ </Modal.Footer>
262
+ </Modal>
263
+ );
264
+ }
@@ -0,0 +1,175 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // 🎨 THEMES — Every visual style for the app in one place
3
+ // ═══════════════════════════════════════════════════════════════════════════
4
+ //
5
+ // WHAT THIS FILE DOES:
6
+ // Each theme defines colors for every part of the UI.
7
+ // Change `theme: 'forest'` in config.js and everything updates.
8
+ //
9
+ // HOW TO CREATE A NEW THEME:
10
+ // 1. Copy one of the existing themes
11
+ // 2. Give it a name (e.g. 'sunset')
12
+ // 3. Change the hex colors below
13
+ // 4. Add it to this object (e.g. sunset: { ... })
14
+ // 5. Use it: theme: 'sunset' in config.js
15
+ //
16
+ // COLOR GUIDE:
17
+ // primary: main brand color (buttons, links, active nav)
18
+ // surface: page background
19
+ // card: card/panel background
20
+ // border: default border color
21
+ // text: default text color
22
+ // textMuted: secondary/label text
23
+ // navBg: navigation bar background
24
+ // navText: navigation text
25
+ // navActive: navigation active item bg
26
+ // danger: delete/error actions
27
+ // success: success states
28
+ //
29
+ // ═══════════════════════════════════════════════════════════════════════════
30
+
31
+ export const themes = {
32
+ // ── 1. FOREST (green - default) ────────────────────────────────────────
33
+ forest: {
34
+ primary: '#008A75',
35
+ surface: '#F9FAFB',
36
+ card: '#FFFFFF',
37
+ border: '#E5E7EB',
38
+ text: '#1F2937',
39
+ textMuted: '#9CA3AF',
40
+ navBg: '#FFFFFF',
41
+ navText: '#4B5563',
42
+ navActive: '#008A75',
43
+ danger: '#EF4444',
44
+ success: '#10B981',
45
+ warning: '#F59E0B',
46
+ info: '#3B82F6',
47
+ },
48
+
49
+ // ── 2. MIDNIGHT (dark) ─────────────────────────────────────────────────
50
+ midnight: {
51
+ primary: '#6B7280',
52
+ surface: '#18181B',
53
+ card: '#27272A',
54
+ border: '#3F3F46',
55
+ text: '#F4F4F5',
56
+ textMuted: '#A1A1AA',
57
+ navBg: '#27272A',
58
+ navText: '#D4D4D8',
59
+ navActive: '#FFFFFF',
60
+ danger: '#F87171',
61
+ success: '#34D399',
62
+ warning: '#FBBF24',
63
+ info: '#60A5FA',
64
+ },
65
+
66
+ // ── 3. SLATE (blue-gray professional) ──────────────────────────────────
67
+ slate: {
68
+ primary: '#6366F1',
69
+ surface: '#F1F5F9',
70
+ card: '#FFFFFF',
71
+ border: '#CBD5E1',
72
+ text: '#1E293B',
73
+ textMuted: '#94A3B8',
74
+ navBg: '#1E293B',
75
+ navText: '#CBD5E1',
76
+ navActive: '#818CF8',
77
+ danger: '#EF4444',
78
+ success: '#10B981',
79
+ warning: '#F59E0B',
80
+ info: '#3B82F6',
81
+ },
82
+
83
+ // ── 4. ROSE (warm pink/crimson) ────────────────────────────────────────
84
+ rose: {
85
+ primary: '#E11D48',
86
+ surface: '#FFF1F2',
87
+ card: '#FFFFFF',
88
+ border: '#FECDD3',
89
+ text: '#1F2937',
90
+ textMuted: '#FDA4AF',
91
+ navBg: '#FFFFFF',
92
+ navText: '#6B7280',
93
+ navActive: '#E11D48',
94
+ danger: '#DC2626',
95
+ success: '#16A34A',
96
+ warning: '#D97706',
97
+ info: '#2563EB',
98
+ },
99
+
100
+ // ── 5. AMBER (orange/gold) ─────────────────────────────────────────────
101
+ amber: {
102
+ primary: '#D97706',
103
+ surface: '#FFFBEB',
104
+ card: '#FFFFFF',
105
+ border: '#FDE68A',
106
+ text: '#1F2937',
107
+ textMuted: '#9CA3AF',
108
+ navBg: '#451A03',
109
+ navText: '#FDE68A',
110
+ navActive: '#F59E0B',
111
+ danger: '#DC2626',
112
+ success: '#16A34A',
113
+ warning: '#D97706',
114
+ info: '#2563EB',
115
+ },
116
+
117
+ // ── 6. OCEAN (deep navy + cyan) ────────────────────────────────────────
118
+ ocean: {
119
+ primary: '#06B6D4',
120
+ surface: '#F0F9FF',
121
+ card: '#FFFFFF',
122
+ border: '#BAE6FD',
123
+ text: '#0F172A',
124
+ textMuted: '#94A3B8',
125
+ navBg: '#172554',
126
+ navText: '#BAE6FD',
127
+ navActive: '#22D3EE',
128
+ danger: '#EF4444',
129
+ success: '#10B981',
130
+ warning: '#F59E0B',
131
+ info: '#3B82F6',
132
+ },
133
+
134
+ // ── 7. CHALK (black & white minimal) ───────────────────────────────────
135
+ chalk: {
136
+ primary: '#000000',
137
+ surface: '#FAFAFA',
138
+ card: '#FFFFFF',
139
+ border: '#D4D4D4',
140
+ text: '#171717',
141
+ textMuted: '#A3A3A3',
142
+ navBg: '#FFFFFF',
143
+ navText: '#525252',
144
+ navActive: '#000000',
145
+ danger: '#DC2626',
146
+ success: '#16A34A',
147
+ warning: '#D97706',
148
+ info: '#2563EB',
149
+ },
150
+
151
+ // ── 8. VIOLET (purple modern) ──────────────────────────────────────────
152
+ violet: {
153
+ primary: '#7C3AED',
154
+ surface: '#F5F3FF',
155
+ card: '#FFFFFF',
156
+ border: '#DDD6FE',
157
+ text: '#1F2937',
158
+ textMuted: '#9CA3AF',
159
+ navBg: '#2E1065',
160
+ navText: '#DDD6FE',
161
+ navActive: '#A78BFA',
162
+ danger: '#EF4444',
163
+ success: '#10B981',
164
+ warning: '#F59E0B',
165
+ info: '#3B82F6',
166
+ },
167
+ };
168
+
169
+ // ─── GET RESOLVED THEME (applies config overrides) ───────────────────────────
170
+ // This merges the selected theme with any user overrides from config.js.
171
+ //
172
+ export function getTheme(themeName = 'forest', overrides = {}) {
173
+ const base = themes[themeName] || themes.forest;
174
+ return { ...base, ...overrides };
175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alpe-temp",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Employee Payroll Management System (EPMS) - Full-stack Express + React app",
5
5
  "main": "bin/epms.js",
6
6
  "bin": {