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.
- package/bin/epms.js +149 -60
- package/frontend-project/src/App.css +1 -0
- package/frontend-project/src/Auth/Login.jsx +85 -0
- package/frontend-project/src/Auth/Register.jsx +183 -0
- package/frontend-project/src/Intro.jsx +33 -0
- package/frontend-project/src/LayOut.jsx +38 -0
- package/frontend-project/src/api/ApiClient.js +92 -0
- package/frontend-project/src/assets/hero.png +0 -0
- package/frontend-project/src/assets/react.svg +1 -0
- package/frontend-project/src/assets/vite.svg +1 -0
- package/frontend-project/src/components/Aside.jsx +9 -0
- package/frontend-project/src/components/Button.jsx +100 -0
- package/frontend-project/src/components/Card.jsx +104 -0
- package/frontend-project/src/components/FormField.jsx +129 -0
- package/frontend-project/src/components/Modal.jsx +106 -0
- package/frontend-project/src/components/Table.jsx +127 -0
- package/frontend-project/src/components/Toast.jsx +64 -0
- package/frontend-project/src/components/index.js +14 -0
- package/frontend-project/src/config.js +66 -0
- package/frontend-project/src/design.js +115 -0
- package/frontend-project/src/index.css +60 -0
- package/frontend-project/src/layouts/BottomNav.jsx +156 -0
- package/frontend-project/src/layouts/TopNav.jsx +150 -0
- package/frontend-project/src/layouts/useShell.js +44 -0
- package/frontend-project/src/main.jsx +41 -0
- package/frontend-project/src/pages/Department.jsx +188 -0
- package/frontend-project/src/pages/Employee.jsx +274 -0
- package/frontend-project/src/pages/Home.jsx +79 -0
- package/frontend-project/src/pages/Profile.jsx +9 -0
- package/frontend-project/src/pages/Register.jsx +57 -0
- package/frontend-project/src/pages/Reports.jsx +91 -0
- package/frontend-project/src/pages/Salary.jsx +264 -0
- package/frontend-project/src/themes.js +175 -0
- package/package.json +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
import React, { useState } from 'react';
|
|
25
|
+
import { NavLink, Outlet } from 'react-router-dom';
|
|
26
|
+
import {
|
|
27
|
+
LayoutDashboard, Users, DollarSign, Building2,
|
|
28
|
+
FileText, User, LogOut, ChevronDown,
|
|
29
|
+
} from 'lucide-react';
|
|
30
|
+
import { useShell } from './useShell';
|
|
31
|
+
|
|
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
|
+
const NAV_ITEMS = [
|
|
37
|
+
{ label: 'Overview', path: '/dashboard/overview', Icon: LayoutDashboard },
|
|
38
|
+
{ label: 'Employees', path: '/dashboard/employees', Icon: Users },
|
|
39
|
+
{ label: 'Salary', path: '/dashboard/salary', Icon: DollarSign },
|
|
40
|
+
{ label: 'Departments', path: '/dashboard/departments', Icon: Building2 },
|
|
41
|
+
{ label: 'Reports', path: '/dashboard/reports', Icon: FileText },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export default function TopNav() {
|
|
45
|
+
const { user, handleLogout } = useShell();
|
|
46
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex flex-col h-screen overflow-hidden">
|
|
50
|
+
{/* ─── TOP NAV BAR ───────────────────────────────────────────────── */}
|
|
51
|
+
<header
|
|
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 */}
|
|
60
|
+
<div className="flex items-center gap-2 mr-4 pr-4" style={{ borderRight: '1px solid var(--color-border)' }}>
|
|
61
|
+
<img src="/logo.png" alt="logo" className="w-7 h-9" />
|
|
62
|
+
<div className="flex flex-col mt-4">
|
|
63
|
+
<span className="text-[15px] font-bold tracking-tight leading-tight">
|
|
64
|
+
EP<span style={{ color: 'var(--color-primary)' }}>MS</span>
|
|
65
|
+
</span>
|
|
66
|
+
<span className="text-[7px] font-medium uppercase tracking-wider leading-tight opacity-60">
|
|
67
|
+
Employee Payroll
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* Navigation links */}
|
|
73
|
+
<nav className="flex items-center gap-0.5 flex-1">
|
|
74
|
+
{NAV_ITEMS.map(({ label, path, Icon }) => (
|
|
75
|
+
<NavLink
|
|
76
|
+
key={path}
|
|
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
|
+
>
|
|
84
|
+
<Icon className="w-3.5 h-3.5 flex-shrink-0" />
|
|
85
|
+
<span className="hidden sm:inline">{label}</span>
|
|
86
|
+
</NavLink>
|
|
87
|
+
))}
|
|
88
|
+
</nav>
|
|
89
|
+
|
|
90
|
+
{/* User dropdown */}
|
|
91
|
+
<div className="relative ml-auto">
|
|
92
|
+
<button
|
|
93
|
+
onClick={() => setMenuOpen((v) => !v)}
|
|
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
|
+
>
|
|
100
|
+
<User className="w-3 h-3" />
|
|
101
|
+
</div>
|
|
102
|
+
<span className="hidden sm:inline">{user?.name ?? 'Admin'}</span>
|
|
103
|
+
<ChevronDown className={`w-3 h-3 transition-transform ${menuOpen ? 'rotate-180' : ''}`} />
|
|
104
|
+
</button>
|
|
105
|
+
{menuOpen && (
|
|
106
|
+
<>
|
|
107
|
+
<div className="fixed inset-0 z-10" onClick={() => setMenuOpen(false)} />
|
|
108
|
+
<div
|
|
109
|
+
className="absolute right-0 top-full mt-1 z-20 shadow-lg py-1 min-w-[150px]"
|
|
110
|
+
style={{ backgroundColor: 'var(--color-card)', border: '1px solid var(--color-border)' }}
|
|
111
|
+
>
|
|
112
|
+
<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>
|
|
119
|
+
</div>
|
|
120
|
+
<button
|
|
121
|
+
onClick={() => { setMenuOpen(false); handleLogout(); }}
|
|
122
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-[12px] font-medium transition-colors"
|
|
123
|
+
style={{ color: 'var(--color-danger)' }}
|
|
124
|
+
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
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</header>
|
|
135
|
+
|
|
136
|
+
{/* ─── MAIN CONTENT ──────────────────────────────────────────────── */}
|
|
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>
|
|
147
|
+
</main>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* layouts/useShell.js – Shared auth logic for every layout shell.
|
|
3
|
+
*
|
|
4
|
+
* Returns { user, handleLogout } — plug into any shell component.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { useNavigate } from 'react-router-dom';
|
|
9
|
+
import { authApi } from '../api/ApiClient';
|
|
10
|
+
|
|
11
|
+
export function useShell() {
|
|
12
|
+
const [user, setUser] = useState(null);
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const token = localStorage.getItem('token');
|
|
17
|
+
if (!token) { navigate('/'); return; }
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
21
|
+
if (payload.exp < Date.now() / 1000) {
|
|
22
|
+
localStorage.removeItem('token');
|
|
23
|
+
localStorage.removeItem('user');
|
|
24
|
+
navigate('/');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
navigate('/');
|
|
29
|
+
return;
|
|
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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import './index.css'
|
|
4
|
+
import { createBrowserRouter , createRoutesFromElements , RouterProvider , Route } from 'react-router-dom'
|
|
5
|
+
import Intro from './Intro'
|
|
6
|
+
import Login from './Auth/Login'
|
|
7
|
+
import Register from './Auth/Register'
|
|
8
|
+
import LayOut from './LayOut'
|
|
9
|
+
import Home from './pages/Home'
|
|
10
|
+
import Employee from './pages/Employee'
|
|
11
|
+
import Salary from './pages/Salary'
|
|
12
|
+
import Department from './pages/Department'
|
|
13
|
+
import Reports from './pages/Reports'
|
|
14
|
+
import Profile from './pages/Profile'
|
|
15
|
+
import { ToastProvider } from './components'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const router = createBrowserRouter(
|
|
19
|
+
createRoutesFromElements(
|
|
20
|
+
<>
|
|
21
|
+
<Route path='/' element={<Intro />} />
|
|
22
|
+
<Route path='/login' element={<Login />} />
|
|
23
|
+
<Route path='/register' element={<Register />} />
|
|
24
|
+
<Route path='/dashboard' element={<LayOut />} >
|
|
25
|
+
<Route path='overview' element={<Home />} />
|
|
26
|
+
<Route path='employees' element={<Employee />} />
|
|
27
|
+
<Route path='salary' element={<Salary />} />
|
|
28
|
+
<Route path='departments' element={<Department />} />
|
|
29
|
+
<Route path='reports' element={<Reports />} />
|
|
30
|
+
<Route path='profile' element={<Profile />} />
|
|
31
|
+
</Route>
|
|
32
|
+
</>
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
createRoot(document.getElementById('root')).render(
|
|
36
|
+
<StrictMode>
|
|
37
|
+
<ToastProvider >
|
|
38
|
+
<RouterProvider router={router} />
|
|
39
|
+
</ToastProvider>
|
|
40
|
+
</StrictMode>,
|
|
41
|
+
)
|
|
@@ -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
|
+
}
|