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,38 +1,13 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// 📱 BOTTOM NAV LAYOUT — Navbar at the bottom (mobile-first style)
|
|
3
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
-
//
|
|
5
|
-
// WHAT THIS FILE DOES:
|
|
6
|
-
// Renders the app shell with a bottom navigation bar.
|
|
7
|
-
// Switch to this by setting navigation: 'bottomnav' in config.js
|
|
8
|
-
//
|
|
9
|
-
// HOW IT LOOKS:
|
|
10
|
-
// ┌────────────────────────────────┐
|
|
11
|
-
// │ │
|
|
12
|
-
// │ PAGE CONTENT │ ← scrollable
|
|
13
|
-
// │ │
|
|
14
|
-
// ├────────────────────────────────┤
|
|
15
|
-
// │ 📊 👥 💰 🏢 📄 │ ← bottom nav (fixed)
|
|
16
|
-
// └────────────────────────────────┘
|
|
17
|
-
//
|
|
18
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
-
|
|
20
1
|
import React, { useState } from 'react';
|
|
21
2
|
import { NavLink, Outlet } from 'react-router-dom';
|
|
22
|
-
import {
|
|
23
|
-
LayoutDashboard, Users, DollarSign, Building2,
|
|
24
|
-
FileText, User, LogOut, ChevronDown,
|
|
25
|
-
} from 'lucide-react';
|
|
3
|
+
import { LayoutDashboard, Users, Building2, Briefcase, FileText, User, LogOut, ChevronDown } from 'lucide-react';
|
|
26
4
|
import { useShell } from './useShell';
|
|
27
5
|
|
|
28
|
-
// ─── NAVIGATION ITEMS ────────────────────────────────────────────────────────
|
|
29
|
-
// Add new pages here. The icon + label + path = one nav item.
|
|
30
|
-
//
|
|
31
6
|
const NAV_ITEMS = [
|
|
32
7
|
{ label: 'Home', path: '/dashboard/overview', Icon: LayoutDashboard },
|
|
33
8
|
{ label: 'Employees',path: '/dashboard/employees', Icon: Users },
|
|
34
|
-
{ label: 'Salary', path: '/dashboard/salary', Icon: DollarSign },
|
|
35
9
|
{ label: 'Depts', path: '/dashboard/departments', Icon: Building2 },
|
|
10
|
+
{ label: 'Positions',path: '/dashboard/positions', Icon: Briefcase },
|
|
36
11
|
{ label: 'Reports', path: '/dashboard/reports', Icon: FileText },
|
|
37
12
|
];
|
|
38
13
|
|
|
@@ -42,111 +17,53 @@ export default function BottomNav() {
|
|
|
42
17
|
|
|
43
18
|
return (
|
|
44
19
|
<div className="flex flex-col h-screen overflow-hidden">
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
className="flex-shrink-0 flex items-center h-[52px] px-4 gap-1 z-20"
|
|
48
|
-
style={{
|
|
49
|
-
backgroundColor: 'var(--color-nav-bg)',
|
|
50
|
-
color: 'var(--color-nav-text)',
|
|
51
|
-
borderBottom: '1px solid var(--color-border)',
|
|
52
|
-
}}
|
|
53
|
-
>
|
|
20
|
+
<header className="flex-shrink-0 flex items-center h-[52px] px-4 gap-1 z-20"
|
|
21
|
+
style={{ backgroundColor: 'var(--color-nav-bg)', color: 'var(--color-nav-text)', borderBottom: '1px solid var(--color-border)' }}>
|
|
54
22
|
<div className="flex items-center gap-2 mr-4">
|
|
55
23
|
<img src="/logo.png" alt="logo" className="w-7 h-9" />
|
|
56
24
|
<div className="flex flex-col mt-4">
|
|
57
|
-
<span className="text-[15px] font-bold tracking-tight leading-tight">
|
|
58
|
-
|
|
59
|
-
</span>
|
|
60
|
-
<span className="text-[7px] font-medium uppercase tracking-wider leading-tight opacity-60">
|
|
61
|
-
Employee Payroll
|
|
62
|
-
</span>
|
|
25
|
+
<span className="text-[15px] font-bold tracking-tight leading-tight">HR<span style={{ color: 'var(--color-primary)' }}>MS</span></span>
|
|
26
|
+
<span className="text-[7px] font-medium uppercase tracking-wider leading-tight opacity-60">DAB Enterprise</span>
|
|
63
27
|
</div>
|
|
64
28
|
</div>
|
|
65
|
-
|
|
66
|
-
{/* Spacer */}
|
|
67
29
|
<div className="flex-1" />
|
|
68
|
-
|
|
69
|
-
{/* User dropdown */}
|
|
70
30
|
<div className="relative">
|
|
71
|
-
<button
|
|
72
|
-
|
|
73
|
-
className="flex items-center gap-2 px-3 py-1.5 text-[12px] font-medium transition-colors hover:opacity-80"
|
|
74
|
-
>
|
|
75
|
-
<div
|
|
76
|
-
className="w-6 h-6 flex items-center justify-center"
|
|
77
|
-
style={{ backgroundColor: 'var(--color-primary)', color: '#fff' }}
|
|
78
|
-
>
|
|
31
|
+
<button onClick={() => setMenuOpen((v) => !v)} className="flex items-center gap-2 px-3 py-1.5 text-[12px] font-medium transition-colors hover:opacity-80">
|
|
32
|
+
<div className="w-6 h-6 flex items-center justify-center" style={{ backgroundColor: 'var(--color-primary)', color: '#fff' }}>
|
|
79
33
|
<User className="w-3 h-3" />
|
|
80
34
|
</div>
|
|
81
|
-
<span className="hidden sm:inline">{user?.
|
|
35
|
+
<span className="hidden sm:inline">{user?.userName ?? 'Admin'}</span>
|
|
82
36
|
<ChevronDown className={`w-3 h-3 transition-transform ${menuOpen ? 'rotate-180' : ''}`} />
|
|
83
37
|
</button>
|
|
84
38
|
{menuOpen && (
|
|
85
39
|
<>
|
|
86
40
|
<div className="fixed inset-0 z-10" onClick={() => setMenuOpen(false)} />
|
|
87
|
-
<div
|
|
88
|
-
|
|
89
|
-
style={{ backgroundColor: 'var(--color-card)', border: '1px solid var(--color-border)' }}
|
|
90
|
-
>
|
|
41
|
+
<div className="absolute right-0 top-full mt-1 z-20 shadow-lg py-1 min-w-[150px]"
|
|
42
|
+
style={{ backgroundColor: 'var(--color-card)', border: '1px solid var(--color-border)' }}>
|
|
91
43
|
<div className="px-3 py-2 border-b" style={{ borderColor: 'var(--color-border)' }}>
|
|
92
|
-
<p className="text-[12px] font-semibold" style={{ color: 'var(--color-text)' }}>
|
|
93
|
-
{user?.name ?? 'Admin'}
|
|
94
|
-
</p>
|
|
95
|
-
<p className="text-[11px]" style={{ color: 'var(--color-text-muted)' }}>
|
|
96
|
-
{user?.email ?? ''}
|
|
97
|
-
</p>
|
|
44
|
+
<p className="text-[12px] font-semibold" style={{ color: 'var(--color-text)' }}>{user?.userName ?? 'Admin'}</p>
|
|
98
45
|
</div>
|
|
99
|
-
<button
|
|
100
|
-
onClick={() => { setMenuOpen(false); handleLogout(); }}
|
|
101
|
-
className="w-full flex items-center gap-2 px-3 py-2 text-[12px] font-medium transition-colors"
|
|
46
|
+
<button onClick={() => { setMenuOpen(false); handleLogout(); }} className="w-full flex items-center gap-2 px-3 py-2 text-[12px] font-medium transition-colors"
|
|
102
47
|
style={{ color: 'var(--color-danger)' }}
|
|
103
48
|
onMouseEnter={(e) => e.target.style.backgroundColor = 'var(--color-danger)'}
|
|
104
|
-
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
|
|
105
|
-
|
|
106
|
-
<LogOut className="w-3.5 h-3.5" />
|
|
107
|
-
Logout
|
|
49
|
+
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}>
|
|
50
|
+
<LogOut className="w-3.5 h-3.5" /> Logout
|
|
108
51
|
</button>
|
|
109
52
|
</div>
|
|
110
53
|
</>
|
|
111
54
|
)}
|
|
112
55
|
</div>
|
|
113
56
|
</header>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<main
|
|
117
|
-
className="flex-1 overflow-y-auto"
|
|
118
|
-
style={{
|
|
119
|
-
backgroundColor: 'var(--color-surface)',
|
|
120
|
-
padding: 'var(--text-base)',
|
|
121
|
-
}}
|
|
122
|
-
>
|
|
123
|
-
<div className="max-w-6xl mx-auto">
|
|
124
|
-
<Outlet />
|
|
125
|
-
</div>
|
|
57
|
+
<main className="flex-1 overflow-y-auto" style={{ backgroundColor: 'var(--color-surface)', padding: 'var(--text-base)' }}>
|
|
58
|
+
<div className="max-w-6xl mx-auto"><Outlet /></div>
|
|
126
59
|
</main>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<nav
|
|
130
|
-
className="flex-shrink-0 flex items-center justify-around h-[64px] px-2 z-20"
|
|
131
|
-
style={{
|
|
132
|
-
backgroundColor: 'var(--color-nav-bg)',
|
|
133
|
-
borderTop: '1px solid var(--color-border)',
|
|
134
|
-
}}
|
|
135
|
-
>
|
|
60
|
+
<nav className="flex-shrink-0 flex items-center justify-around h-[64px] px-2 z-20"
|
|
61
|
+
style={{ backgroundColor: 'var(--color-nav-bg)', borderTop: '1px solid var(--color-border)' }}>
|
|
136
62
|
{NAV_ITEMS.map(({ label, path, Icon }) => (
|
|
137
|
-
<NavLink
|
|
138
|
-
|
|
139
|
-
to={path}
|
|
140
|
-
className="flex flex-col items-center gap-0.5 px-2 py-1 text-[10px] font-medium transition-colors"
|
|
141
|
-
style={({ isActive }) => ({
|
|
142
|
-
color: isActive ? 'var(--color-nav-active)' : 'var(--color-nav-text)',
|
|
143
|
-
})}
|
|
144
|
-
>
|
|
63
|
+
<NavLink key={path} to={path} className="flex flex-col items-center gap-0.5 px-2 py-1 text-[10px] font-medium transition-colors"
|
|
64
|
+
style={({ isActive }) => ({ color: isActive ? 'var(--color-nav-active)' : 'var(--color-nav-text)' })}>
|
|
145
65
|
{({ isActive }) => (
|
|
146
|
-
|
|
147
|
-
<Icon className="w-5 h-5" />
|
|
148
|
-
<span>{label}</span>
|
|
149
|
-
</>
|
|
66
|
+
<><Icon className="w-5 h-5" /><span>{label}</span></>
|
|
150
67
|
)}
|
|
151
68
|
</NavLink>
|
|
152
69
|
))}
|
|
@@ -1,43 +1,13 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// 📐 TOP NAV LAYOUT — Navbar at the top (classic web style)
|
|
3
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
-
//
|
|
5
|
-
// WHAT THIS FILE DOES:
|
|
6
|
-
// Renders the app shell with a horizontal nav bar at the top.
|
|
7
|
-
// This is the default layout (navigation: 'topnav' in config.js).
|
|
8
|
-
//
|
|
9
|
-
// HOW IT LOOKS:
|
|
10
|
-
// ┌──────────────────────────────────────────────┐
|
|
11
|
-
// │ LOGO Home Employees Salary Reports 👤│ ← top nav (fixed)
|
|
12
|
-
// ├──────────────────────────────────────────────┤
|
|
13
|
-
// │ │
|
|
14
|
-
// │ PAGE CONTENT │ ← scrollable
|
|
15
|
-
// │ │
|
|
16
|
-
// └──────────────────────────────────────────────┘
|
|
17
|
-
//
|
|
18
|
-
// HOW TO ADD A NEW NAV ITEM:
|
|
19
|
-
// Just add an entry to the NAV_ITEMS array below.
|
|
20
|
-
// Example: { label: 'Products', path: '/dashboard/products', Icon: Package }
|
|
21
|
-
//
|
|
22
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
-
|
|
24
1
|
import React, { useState } from 'react';
|
|
25
2
|
import { NavLink, Outlet } from 'react-router-dom';
|
|
26
|
-
import {
|
|
27
|
-
LayoutDashboard, Users, DollarSign, Building2,
|
|
28
|
-
FileText, User, LogOut, ChevronDown,
|
|
29
|
-
} from 'lucide-react';
|
|
3
|
+
import { LayoutDashboard, Users, Building2, Briefcase, FileText, User, LogOut, ChevronDown } from 'lucide-react';
|
|
30
4
|
import { useShell } from './useShell';
|
|
31
5
|
|
|
32
|
-
// ─── NAVIGATION ITEMS ────────────────────────────────────────────────────────
|
|
33
|
-
// To add a new page, just add an object to this array.
|
|
34
|
-
// Each item needs: label (shown text), path (URL), Icon (from lucide-react)
|
|
35
|
-
//
|
|
36
6
|
const NAV_ITEMS = [
|
|
37
7
|
{ label: 'Overview', path: '/dashboard/overview', Icon: LayoutDashboard },
|
|
38
8
|
{ label: 'Employees', path: '/dashboard/employees', Icon: Users },
|
|
39
|
-
{ label: 'Salary', path: '/dashboard/salary', Icon: DollarSign },
|
|
40
9
|
{ label: 'Departments', path: '/dashboard/departments', Icon: Building2 },
|
|
10
|
+
{ label: 'Positions', path: '/dashboard/positions', Icon: Briefcase },
|
|
41
11
|
{ label: 'Reports', path: '/dashboard/reports', Icon: FileText },
|
|
42
12
|
];
|
|
43
13
|
|
|
@@ -47,103 +17,54 @@ export default function TopNav() {
|
|
|
47
17
|
|
|
48
18
|
return (
|
|
49
19
|
<div className="flex flex-col h-screen overflow-hidden">
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
className="flex-shrink-0 flex items-center h-[52px] px-4 gap-1 z-20"
|
|
53
|
-
style={{
|
|
54
|
-
backgroundColor: 'var(--color-nav-bg)',
|
|
55
|
-
color: 'var(--color-nav-text)',
|
|
56
|
-
borderBottom: '1px solid var(--color-border)',
|
|
57
|
-
}}
|
|
58
|
-
>
|
|
59
|
-
{/* Logo + Brand */}
|
|
20
|
+
<header className="flex-shrink-0 flex items-center h-[52px] px-4 gap-1 z-20"
|
|
21
|
+
style={{ backgroundColor: 'var(--color-nav-bg)', color: 'var(--color-nav-text)', borderBottom: '1px solid var(--color-border)' }}>
|
|
60
22
|
<div className="flex items-center gap-2 mr-4 pr-4" style={{ borderRight: '1px solid var(--color-border)' }}>
|
|
61
23
|
<img src="/logo.png" alt="logo" className="w-7 h-9" />
|
|
62
24
|
<div className="flex flex-col mt-4">
|
|
63
|
-
<span className="text-[15px] font-bold tracking-tight leading-tight">
|
|
64
|
-
|
|
65
|
-
</span>
|
|
66
|
-
<span className="text-[7px] font-medium uppercase tracking-wider leading-tight opacity-60">
|
|
67
|
-
Employee Payroll
|
|
68
|
-
</span>
|
|
25
|
+
<span className="text-[15px] font-bold tracking-tight leading-tight">HR<span style={{ color: 'var(--color-primary)' }}>MS</span></span>
|
|
26
|
+
<span className="text-[7px] font-medium uppercase tracking-wider leading-tight opacity-60">DAB Enterprise</span>
|
|
69
27
|
</div>
|
|
70
28
|
</div>
|
|
71
|
-
|
|
72
|
-
{/* Navigation links */}
|
|
73
29
|
<nav className="flex items-center gap-0.5 flex-1">
|
|
74
30
|
{NAV_ITEMS.map(({ label, path, Icon }) => (
|
|
75
|
-
<NavLink
|
|
76
|
-
|
|
77
|
-
to={path}
|
|
78
|
-
className="flex items-center gap-1.5 px-3 py-1.5 text-[12px] font-medium transition-colors"
|
|
79
|
-
style={({ isActive }) => ({
|
|
80
|
-
backgroundColor: isActive ? 'var(--color-primary)' : 'transparent',
|
|
81
|
-
color: isActive ? '#ffffff' : 'var(--color-nav-text)',
|
|
82
|
-
})}
|
|
83
|
-
>
|
|
31
|
+
<NavLink key={path} to={path} className="flex items-center gap-1.5 px-3 py-1.5 text-[12px] font-medium transition-colors"
|
|
32
|
+
style={({ isActive }) => ({ backgroundColor: isActive ? 'var(--color-primary)' : 'transparent', color: isActive ? '#ffffff' : 'var(--color-nav-text)' })}>
|
|
84
33
|
<Icon className="w-3.5 h-3.5 flex-shrink-0" />
|
|
85
34
|
<span className="hidden sm:inline">{label}</span>
|
|
86
35
|
</NavLink>
|
|
87
36
|
))}
|
|
88
37
|
</nav>
|
|
89
|
-
|
|
90
|
-
{/* User dropdown */}
|
|
91
38
|
<div className="relative ml-auto">
|
|
92
|
-
<button
|
|
93
|
-
|
|
94
|
-
className="flex items-center gap-2 px-3 py-1.5 text-[12px] font-medium transition-colors hover:opacity-80"
|
|
95
|
-
>
|
|
96
|
-
<div
|
|
97
|
-
className="w-6 h-6 flex items-center justify-center"
|
|
98
|
-
style={{ backgroundColor: 'var(--color-primary)', color: '#fff' }}
|
|
99
|
-
>
|
|
39
|
+
<button onClick={() => setMenuOpen((v) => !v)} className="flex items-center gap-2 px-3 py-1.5 text-[12px] font-medium transition-colors hover:opacity-80">
|
|
40
|
+
<div className="w-6 h-6 flex items-center justify-center" style={{ backgroundColor: 'var(--color-primary)', color: '#fff' }}>
|
|
100
41
|
<User className="w-3 h-3" />
|
|
101
42
|
</div>
|
|
102
|
-
<span className="hidden sm:inline">{user?.
|
|
43
|
+
<span className="hidden sm:inline">{user?.userName ?? 'Admin'}</span>
|
|
103
44
|
<ChevronDown className={`w-3 h-3 transition-transform ${menuOpen ? 'rotate-180' : ''}`} />
|
|
104
45
|
</button>
|
|
105
46
|
{menuOpen && (
|
|
106
47
|
<>
|
|
107
48
|
<div className="fixed inset-0 z-10" onClick={() => setMenuOpen(false)} />
|
|
108
|
-
<div
|
|
109
|
-
|
|
110
|
-
style={{ backgroundColor: 'var(--color-card)', border: '1px solid var(--color-border)' }}
|
|
111
|
-
>
|
|
49
|
+
<div className="absolute right-0 top-full mt-1 z-20 shadow-lg py-1 min-w-[150px]"
|
|
50
|
+
style={{ backgroundColor: 'var(--color-card)', border: '1px solid var(--color-border)' }}>
|
|
112
51
|
<div className="px-3 py-2 border-b" style={{ borderColor: 'var(--color-border)' }}>
|
|
113
|
-
<p className="text-[12px] font-semibold" style={{ color: 'var(--color-text)' }}>
|
|
114
|
-
{user?.name ?? 'Admin'}
|
|
115
|
-
</p>
|
|
116
|
-
<p className="text-[11px]" style={{ color: 'var(--color-text-muted)' }}>
|
|
117
|
-
{user?.email ?? ''}
|
|
118
|
-
</p>
|
|
52
|
+
<p className="text-[12px] font-semibold" style={{ color: 'var(--color-text)' }}>{user?.userName ?? 'Admin'}</p>
|
|
119
53
|
</div>
|
|
120
|
-
<button
|
|
121
|
-
onClick={() => { setMenuOpen(false); handleLogout(); }}
|
|
54
|
+
<button onClick={() => { setMenuOpen(false); handleLogout(); }}
|
|
122
55
|
className="w-full flex items-center gap-2 px-3 py-2 text-[12px] font-medium transition-colors"
|
|
123
56
|
style={{ color: 'var(--color-danger)' }}
|
|
124
57
|
onMouseEnter={(e) => e.target.style.backgroundColor = 'var(--color-danger)'}
|
|
125
|
-
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
|
|
126
|
-
|
|
127
|
-
<LogOut className="w-3.5 h-3.5" />
|
|
128
|
-
Logout
|
|
58
|
+
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}>
|
|
59
|
+
<LogOut className="w-3.5 h-3.5" /> Logout
|
|
129
60
|
</button>
|
|
130
61
|
</div>
|
|
131
62
|
</>
|
|
132
63
|
)}
|
|
133
64
|
</div>
|
|
134
65
|
</header>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<main
|
|
138
|
-
className="flex-1 overflow-y-auto"
|
|
139
|
-
style={{
|
|
140
|
-
backgroundColor: 'var(--color-surface)',
|
|
141
|
-
padding: '24px',
|
|
142
|
-
}}
|
|
143
|
-
>
|
|
144
|
-
<div className="max-w-7xl mx-auto">
|
|
145
|
-
<Outlet />
|
|
146
|
-
</div>
|
|
66
|
+
<main className="flex-1 overflow-y-auto" style={{ backgroundColor: 'var(--color-surface)', padding: '24px' }}>
|
|
67
|
+
<div className="max-w-7xl mx-auto"><Outlet /></div>
|
|
147
68
|
</main>
|
|
148
69
|
</div>
|
|
149
70
|
);
|
|
@@ -1,44 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const stored = localStorage.getItem('user');
|
|
33
|
-
setUser(stored ? JSON.parse(stored) : {});
|
|
34
|
-
}, [navigate]);
|
|
35
|
-
|
|
36
|
-
const handleLogout = async () => {
|
|
37
|
-
try { await authApi.logout(); } catch { /* ignore */ }
|
|
38
|
-
localStorage.removeItem('token');
|
|
39
|
-
localStorage.removeItem('user');
|
|
40
|
-
navigate('/');
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
return { user, handleLogout };
|
|
44
|
-
}
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { authApi } from '../api/ApiClient';
|
|
4
|
+
|
|
5
|
+
export function useShell() {
|
|
6
|
+
const [user, setUser] = useState(null);
|
|
7
|
+
const [loading, setLoading] = useState(true);
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const checkAuth = async () => {
|
|
12
|
+
try {
|
|
13
|
+
const res = await authApi.me();
|
|
14
|
+
setUser(res?.data?.user ?? null);
|
|
15
|
+
} catch {
|
|
16
|
+
navigate('/');
|
|
17
|
+
} finally {
|
|
18
|
+
setLoading(false);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
checkAuth();
|
|
22
|
+
}, [navigate]);
|
|
23
|
+
|
|
24
|
+
const handleLogout = async () => {
|
|
25
|
+
try { await authApi.logout(); } catch { /* ignore */ }
|
|
26
|
+
navigate('/');
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return { user, loading, handleLogout };
|
|
30
|
+
}
|
|
@@ -8,13 +8,12 @@ import Register from './Auth/Register'
|
|
|
8
8
|
import LayOut from './LayOut'
|
|
9
9
|
import Home from './pages/Home'
|
|
10
10
|
import Employee from './pages/Employee'
|
|
11
|
-
import Salary from './pages/Salary'
|
|
12
11
|
import Department from './pages/Department'
|
|
12
|
+
import Position from './pages/Position'
|
|
13
13
|
import Reports from './pages/Reports'
|
|
14
14
|
import Profile from './pages/Profile'
|
|
15
15
|
import { ToastProvider } from './components'
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
const router = createBrowserRouter(
|
|
19
18
|
createRoutesFromElements(
|
|
20
19
|
<>
|
|
@@ -24,8 +23,8 @@ const router = createBrowserRouter(
|
|
|
24
23
|
<Route path='/dashboard' element={<LayOut />} >
|
|
25
24
|
<Route path='overview' element={<Home />} />
|
|
26
25
|
<Route path='employees' element={<Employee />} />
|
|
27
|
-
<Route path='salary' element={<Salary />} />
|
|
28
26
|
<Route path='departments' element={<Department />} />
|
|
27
|
+
<Route path='positions' element={<Position />} />
|
|
29
28
|
<Route path='reports' element={<Reports />} />
|
|
30
29
|
<Route path='profile' element={<Profile />} />
|
|
31
30
|
</Route>
|
|
@@ -20,20 +20,14 @@ export default function Department() {
|
|
|
20
20
|
try {
|
|
21
21
|
const res = await departmentApi.list();
|
|
22
22
|
setDepartments(res?.data?.departments ?? []);
|
|
23
|
-
} catch (
|
|
24
|
-
|
|
25
|
-
} finally {
|
|
26
|
-
setLoading(false);
|
|
27
|
-
}
|
|
23
|
+
} catch { toast.error('Failed to load departments'); }
|
|
24
|
+
finally { setLoading(false); }
|
|
28
25
|
}, []);
|
|
29
26
|
|
|
30
27
|
useEffect(() => { loadAll(); }, [loadAll]);
|
|
31
28
|
|
|
32
29
|
const columns = [
|
|
33
|
-
{ key: '
|
|
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` },
|
|
30
|
+
{ key: 'departName', label: 'Department Name' },
|
|
37
31
|
{
|
|
38
32
|
key: 'actions', label: '', align: 'right',
|
|
39
33
|
render: (_, row) => (
|
|
@@ -52,7 +46,7 @@ export default function Department() {
|
|
|
52
46
|
<h1 className="text-[18px] font-bold text-gray-800">Departments</h1>
|
|
53
47
|
<p className="text-[13px] text-gray-400 mt-0.5">{departments.length} departments</p>
|
|
54
48
|
</div>
|
|
55
|
-
<Button size="sm"
|
|
49
|
+
<Button size="sm" icon={<Plus className="w-3.5 h-3.5" />} onClick={() => setCreateOpen(true)}>Add Department</Button>
|
|
56
50
|
</div>
|
|
57
51
|
<div className="bg-white border border-gray-200 p-5">
|
|
58
52
|
<Table columns={columns} data={departments} rowKey="_id" loading={loading} emptyMessage="No departments yet." maxHeight="480px" />
|
|
@@ -66,17 +60,14 @@ export default function Department() {
|
|
|
66
60
|
|
|
67
61
|
function CreateModal({ onClose, onCreated }) {
|
|
68
62
|
const toast = useToast();
|
|
69
|
-
const [form, setForm] = useState({
|
|
63
|
+
const [form, setForm] = useState({ departName: '' });
|
|
70
64
|
const [loading, setLoading] = useState(false);
|
|
71
65
|
const [errors, setErrors] = useState({});
|
|
72
66
|
const set = (f) => (v) => setForm((p) => ({ ...p, [f]: v }));
|
|
73
67
|
|
|
74
68
|
const validate = () => {
|
|
75
69
|
const e = {};
|
|
76
|
-
if (!form.
|
|
77
|
-
if (!form.departmentName) e.departmentName = 'Required';
|
|
78
|
-
if (!form.grossSalary) e.grossSalary = 'Required';
|
|
79
|
-
if (form.totalDeduction === '') e.totalDeduction = 'Required';
|
|
70
|
+
if (!form.departName) e.departName = 'Required';
|
|
80
71
|
setErrors(e);
|
|
81
72
|
return Object.keys(e).length === 0;
|
|
82
73
|
};
|
|
@@ -85,30 +76,18 @@ function CreateModal({ onClose, onCreated }) {
|
|
|
85
76
|
if (!validate()) return;
|
|
86
77
|
setLoading(true);
|
|
87
78
|
try {
|
|
88
|
-
await departmentApi.create(
|
|
79
|
+
await departmentApi.create(form);
|
|
89
80
|
toast.success('Department created');
|
|
90
81
|
onCreated();
|
|
91
|
-
} catch (err) {
|
|
92
|
-
|
|
93
|
-
} finally {
|
|
94
|
-
setLoading(false);
|
|
95
|
-
}
|
|
82
|
+
} catch (err) { toast.error(err.message ?? 'Failed to create department'); }
|
|
83
|
+
finally { setLoading(false); }
|
|
96
84
|
};
|
|
97
85
|
|
|
98
86
|
return (
|
|
99
87
|
<Modal open title="Add Department" size="sm" onClose={onClose}>
|
|
100
88
|
<div className="space-y-3">
|
|
101
|
-
<FormField label="Department
|
|
102
|
-
<FormField.Input value={form.
|
|
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" />
|
|
89
|
+
<FormField label="Department Name" required error={errors.departName}>
|
|
90
|
+
<FormField.Input value={form.departName} onChange={set('departName')} placeholder="e.g. Human Resources" />
|
|
112
91
|
</FormField>
|
|
113
92
|
</div>
|
|
114
93
|
<Modal.Footer>
|
|
@@ -121,36 +100,23 @@ function CreateModal({ onClose, onCreated }) {
|
|
|
121
100
|
|
|
122
101
|
function EditModal({ department, onClose, onUpdated }) {
|
|
123
102
|
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
|
-
});
|
|
103
|
+
const [form, setForm] = useState({ departName: department.departName || '' });
|
|
130
104
|
const [loading, setLoading] = useState(false);
|
|
131
105
|
const set = (f) => (v) => setForm((p) => ({ ...p, [f]: v }));
|
|
132
106
|
|
|
133
107
|
const handleSave = async () => {
|
|
134
108
|
setLoading(true);
|
|
135
109
|
try {
|
|
136
|
-
await departmentApi.update(department._id,
|
|
110
|
+
await departmentApi.update(department._id, form);
|
|
137
111
|
toast.success('Department updated');
|
|
138
112
|
onUpdated();
|
|
139
|
-
} catch (err) {
|
|
140
|
-
|
|
141
|
-
} finally {
|
|
142
|
-
setLoading(false);
|
|
143
|
-
}
|
|
113
|
+
} catch (err) { toast.error(err.message ?? 'Update failed'); }
|
|
114
|
+
finally { setLoading(false); }
|
|
144
115
|
};
|
|
145
116
|
|
|
146
117
|
return (
|
|
147
|
-
<Modal open title={`Edit — ${department.
|
|
148
|
-
<
|
|
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>
|
|
118
|
+
<Modal open title={`Edit — ${department.departName}`} size="sm" onClose={onClose}>
|
|
119
|
+
<FormField label="Department Name"><FormField.Input value={form.departName} onChange={set('departName')} /></FormField>
|
|
154
120
|
<Modal.Footer>
|
|
155
121
|
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
|
156
122
|
<Button loading={loading} onClick={handleSave}>Save Changes</Button>
|
|
@@ -167,18 +133,15 @@ function DeleteModal({ department, onClose, onDeleted }) {
|
|
|
167
133
|
setLoading(true);
|
|
168
134
|
try {
|
|
169
135
|
await departmentApi.remove(department._id);
|
|
170
|
-
toast.success(`${department.
|
|
136
|
+
toast.success(`${department.departName} removed`);
|
|
171
137
|
onDeleted();
|
|
172
|
-
} catch (err) {
|
|
173
|
-
|
|
174
|
-
} finally {
|
|
175
|
-
setLoading(false);
|
|
176
|
-
}
|
|
138
|
+
} catch (err) { toast.error(err.message ?? 'Delete failed'); }
|
|
139
|
+
finally { setLoading(false); }
|
|
177
140
|
};
|
|
178
141
|
|
|
179
142
|
return (
|
|
180
143
|
<Modal open title="Confirm Deletion" size="sm" onClose={onClose}>
|
|
181
|
-
<p className="text-[13px] text-gray-600">Delete <strong>{department.
|
|
144
|
+
<p className="text-[13px] text-gray-600">Delete <strong>{department.departName}</strong>? This cannot be undone.</p>
|
|
182
145
|
<Modal.Footer>
|
|
183
146
|
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
|
184
147
|
<Button variant="danger" loading={loading} icon={<Trash2 className="w-3.5 h-3.5" />} onClick={handleDelete}>Delete</Button>
|