@xeplr/ui-account 1.0.0
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/LICENSE +21 -0
- package/package.json +17 -0
- package/src/AccessContext.jsx +125 -0
- package/src/AccessGuard.jsx +35 -0
- package/src/ProtectedRoute.jsx +55 -0
- package/src/ThemeContext.jsx +84 -0
- package/src/adminApi.js +68 -0
- package/src/api.js +196 -0
- package/src/designs/AccessMatrixSample.jsx +199 -0
- package/src/designs/ActivateSample.jsx +38 -0
- package/src/designs/ChangePasswordSample.jsx +31 -0
- package/src/designs/ForgotPasswordSample.jsx +22 -0
- package/src/designs/LoginSample.jsx +26 -0
- package/src/designs/MasterSettingsSample.jsx +206 -0
- package/src/designs/NotActivatedSample.jsx +17 -0
- package/src/designs/ProfileSample.jsx +36 -0
- package/src/designs/RegisterSample.jsx +34 -0
- package/src/designs/ResetPasswordSample.jsx +42 -0
- package/src/designs/TenantPickerSample.jsx +39 -0
- package/src/designs/TenantSample.jsx +182 -0
- package/src/designs/UserRolesMatrixSample.jsx +77 -0
- package/src/designs/admin.css +525 -0
- package/src/designs/auth.css +160 -0
- package/src/designs/index.js +13 -0
- package/src/designs/theme.css +231 -0
- package/src/index.js +41 -0
- package/src/masterApi.js +28 -0
- package/src/pages.jsx +92 -0
- package/src/token.js +38 -0
- package/src/useAccessMatrixController.js +244 -0
- package/src/useActivateController.js +37 -0
- package/src/useChangePasswordController.js +48 -0
- package/src/useForgotPasswordController.js +32 -0
- package/src/useLoginController.js +53 -0
- package/src/useMasterSettingsController.js +199 -0
- package/src/useProfileController.js +62 -0
- package/src/useRegisterController.js +41 -0
- package/src/useResetPasswordController.js +36 -0
- package/src/useTenantController.js +146 -0
- package/src/useTenantPickerController.js +97 -0
- package/src/useUserRolesController.js +81 -0
- package/src/validateDesign.js +101 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { useSearchParams } from 'react-router-dom';
|
|
3
|
+
import { activateAccount } from './api.js';
|
|
4
|
+
|
|
5
|
+
export function useActivateController() {
|
|
6
|
+
var [searchParams] = useSearchParams();
|
|
7
|
+
var token = searchParams.get('token');
|
|
8
|
+
var [error, setError] = useState('');
|
|
9
|
+
var [success, setSuccess] = useState('');
|
|
10
|
+
var [loading, setLoading] = useState(false);
|
|
11
|
+
var called = useRef(false);
|
|
12
|
+
|
|
13
|
+
useEffect(function() {
|
|
14
|
+
if (!token || called.current) return;
|
|
15
|
+
called.current = true;
|
|
16
|
+
setLoading(true);
|
|
17
|
+
setError('');
|
|
18
|
+
setSuccess('');
|
|
19
|
+
activateAccount(token)
|
|
20
|
+
.then(function(result) {
|
|
21
|
+
setSuccess(result.message || 'Account activated successfully');
|
|
22
|
+
})
|
|
23
|
+
.catch(function(err) {
|
|
24
|
+
setError(err.message);
|
|
25
|
+
})
|
|
26
|
+
.finally(function() {
|
|
27
|
+
setLoading(false);
|
|
28
|
+
});
|
|
29
|
+
}, [token]);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
token,
|
|
33
|
+
error,
|
|
34
|
+
success,
|
|
35
|
+
loading,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { changePassword } from './api.js';
|
|
3
|
+
|
|
4
|
+
export function useChangePasswordController(options = {}) {
|
|
5
|
+
var [currentPassword, setCurrentPassword] = useState('');
|
|
6
|
+
var [newPassword, setNewPassword] = useState('');
|
|
7
|
+
var [confirmPassword, setConfirmPassword] = useState('');
|
|
8
|
+
var [error, setError] = useState('');
|
|
9
|
+
var [success, setSuccess] = useState('');
|
|
10
|
+
var [loading, setLoading] = useState(false);
|
|
11
|
+
|
|
12
|
+
var onSuccess = options.onSuccess || null;
|
|
13
|
+
|
|
14
|
+
async function handleSubmit(e) {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
setError('');
|
|
17
|
+
setSuccess('');
|
|
18
|
+
|
|
19
|
+
if (newPassword !== confirmPassword) {
|
|
20
|
+
setError('Passwords do not match');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setLoading(true);
|
|
25
|
+
try {
|
|
26
|
+
var result = await changePassword({ currentPassword, newPassword });
|
|
27
|
+
setSuccess('Password changed successfully');
|
|
28
|
+
setCurrentPassword('');
|
|
29
|
+
setNewPassword('');
|
|
30
|
+
setConfirmPassword('');
|
|
31
|
+
if (onSuccess) onSuccess(result);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
setError(err.message);
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
currentPassword, setCurrentPassword,
|
|
41
|
+
newPassword, setNewPassword,
|
|
42
|
+
confirmPassword, setConfirmPassword,
|
|
43
|
+
error,
|
|
44
|
+
success,
|
|
45
|
+
loading,
|
|
46
|
+
handleSubmit,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { forgotPassword } from './api.js';
|
|
3
|
+
|
|
4
|
+
export function useForgotPasswordController() {
|
|
5
|
+
const [email, setEmail] = useState('');
|
|
6
|
+
const [error, setError] = useState('');
|
|
7
|
+
const [success, setSuccess] = useState('');
|
|
8
|
+
const [loading, setLoading] = useState(false);
|
|
9
|
+
|
|
10
|
+
async function handleSubmit(e) {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
setError('');
|
|
13
|
+
setSuccess('');
|
|
14
|
+
setLoading(true);
|
|
15
|
+
try {
|
|
16
|
+
const result = await forgotPassword({ email });
|
|
17
|
+
setSuccess(result.message);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
setError(err.message);
|
|
20
|
+
} finally {
|
|
21
|
+
setLoading(false);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
email, setEmail,
|
|
27
|
+
error,
|
|
28
|
+
success,
|
|
29
|
+
loading,
|
|
30
|
+
handleSubmit,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { loginUser } from './api.js';
|
|
4
|
+
import { setToken, setRefreshToken } from './token.js';
|
|
5
|
+
import { useAccess } from './AccessContext.jsx';
|
|
6
|
+
|
|
7
|
+
export function useLoginController(options = {}) {
|
|
8
|
+
const [email, setEmail] = useState('');
|
|
9
|
+
const [password, setPassword] = useState('');
|
|
10
|
+
const [error, setError] = useState('');
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
|
|
14
|
+
// Safe — returns null if no AccessProvider wraps the app
|
|
15
|
+
const accessCtx = useAccess();
|
|
16
|
+
|
|
17
|
+
const onSuccess = options.onSuccess || (() => navigate('/'));
|
|
18
|
+
const notActivatedPath = options.notActivatedPath || '/auth/not-activated';
|
|
19
|
+
|
|
20
|
+
async function handleSubmit(e) {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
setError('');
|
|
23
|
+
setLoading(true);
|
|
24
|
+
try {
|
|
25
|
+
const result = await loginUser({ email, password });
|
|
26
|
+
setToken(result.accessToken);
|
|
27
|
+
setRefreshToken(result.refreshToken);
|
|
28
|
+
|
|
29
|
+
// If AccessProvider is available, use it to store user + access
|
|
30
|
+
if (accessCtx) {
|
|
31
|
+
accessCtx.onLogin(result);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onSuccess(result);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (err.message === 'Please wait, someone will activate you.') {
|
|
37
|
+
navigate(notActivatedPath);
|
|
38
|
+
} else {
|
|
39
|
+
setError(err.message);
|
|
40
|
+
}
|
|
41
|
+
} finally {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
email, setEmail,
|
|
48
|
+
password, setPassword,
|
|
49
|
+
error,
|
|
50
|
+
loading,
|
|
51
|
+
handleSubmit,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
2
|
+
import { getMasterItems, saveMasterItem, deleteMasterItem } from './masterApi.js';
|
|
3
|
+
|
|
4
|
+
var TABS = [
|
|
5
|
+
{ key: 'roles', label: 'Roles', fields: [{ key: 'name', label: 'Name', required: true }] },
|
|
6
|
+
{ key: 'apis', label: 'APIs', fields: [
|
|
7
|
+
{ key: 'name', label: 'Name', required: true },
|
|
8
|
+
{ key: 'apiGroup', label: 'Group', type: 'group', placeholder: 'e.g. account:view' },
|
|
9
|
+
{ key: 'isPublic', label: 'Public', type: 'boolean' }
|
|
10
|
+
]},
|
|
11
|
+
{ key: 'pages', label: 'Pages', fields: [
|
|
12
|
+
{ key: 'name', label: 'Name', required: true },
|
|
13
|
+
{ key: 'uiPagesGroup', label: 'Group', type: 'group', placeholder: 'e.g. account:view' },
|
|
14
|
+
{ key: 'isPublic', label: 'Public', type: 'boolean' }
|
|
15
|
+
]},
|
|
16
|
+
{ key: 'elements', label: 'Elements', fields: [
|
|
17
|
+
{ key: 'name', label: 'Name', required: true },
|
|
18
|
+
{ key: 'uiElementsGroup', label: 'Group', type: 'group', placeholder: 'e.g. users:edit' }
|
|
19
|
+
]},
|
|
20
|
+
{ key: 'menus', label: 'Menus', fields: [
|
|
21
|
+
{ key: 'name', label: 'Name', required: true },
|
|
22
|
+
{ key: 'menuGroup', label: 'Group', type: 'group', placeholder: 'e.g. reports:view' },
|
|
23
|
+
{ key: 'isPublic', label: 'Public', type: 'boolean' }
|
|
24
|
+
]},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function useMasterSettingsController() {
|
|
28
|
+
var [activeTab, setActiveTab] = useState('roles');
|
|
29
|
+
var [items, setItems] = useState({});
|
|
30
|
+
var [search, setSearch] = useState('');
|
|
31
|
+
var [loading, setLoading] = useState(true);
|
|
32
|
+
var [error, setError] = useState('');
|
|
33
|
+
var [saving, setSaving] = useState(false);
|
|
34
|
+
var [editingItem, setEditingItem] = useState(null);
|
|
35
|
+
var [editForm, setEditForm] = useState({});
|
|
36
|
+
|
|
37
|
+
useEffect(function() {
|
|
38
|
+
loadItems(activeTab);
|
|
39
|
+
}, [activeTab]);
|
|
40
|
+
|
|
41
|
+
async function loadItems(type) {
|
|
42
|
+
setLoading(true);
|
|
43
|
+
setError('');
|
|
44
|
+
try {
|
|
45
|
+
var data = await getMasterItems(type);
|
|
46
|
+
setItems(function(prev) { var next = { ...prev }; next[type] = data; return next; });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
setError(err.message);
|
|
49
|
+
} finally {
|
|
50
|
+
setLoading(false);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var currentTab = TABS.find(function(t) { return t.key === activeTab; }) || TABS[0];
|
|
55
|
+
|
|
56
|
+
var GROUP_FIELDS = { apis: 'apiGroup', pages: 'uiPagesGroup', elements: 'uiElementsGroup', menus: 'menuGroup' };
|
|
57
|
+
|
|
58
|
+
var groupNames = useMemo(function() {
|
|
59
|
+
var set = {};
|
|
60
|
+
['apis', 'pages', 'elements', 'menus'].forEach(function(type) {
|
|
61
|
+
var field = GROUP_FIELDS[type];
|
|
62
|
+
(items[type] || []).forEach(function(item) {
|
|
63
|
+
var val = item[field];
|
|
64
|
+
if (val && val.indexOf(':') !== -1) set[val] = true;
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
return Object.keys(set).sort();
|
|
68
|
+
}, [items]);
|
|
69
|
+
|
|
70
|
+
var filteredItems = useMemo(function() {
|
|
71
|
+
var list = items[activeTab] || [];
|
|
72
|
+
// Hide Super Admin from roles tab
|
|
73
|
+
if (activeTab === 'roles') {
|
|
74
|
+
list = list.filter(function(item) { return item.name !== 'Super Admin'; });
|
|
75
|
+
}
|
|
76
|
+
if (!search.trim()) return list;
|
|
77
|
+
var q = search.toLowerCase();
|
|
78
|
+
return list.filter(function(item) {
|
|
79
|
+
return Object.values(item).some(function(val) {
|
|
80
|
+
return val && String(val).toLowerCase().includes(q);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}, [items, activeTab, search]);
|
|
84
|
+
|
|
85
|
+
var handleTabChange = useCallback(function(tabKey) {
|
|
86
|
+
setActiveTab(tabKey);
|
|
87
|
+
setSearch('');
|
|
88
|
+
setEditingItem(null);
|
|
89
|
+
setEditForm({});
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
function startAdd() {
|
|
93
|
+
var empty = {};
|
|
94
|
+
currentTab.fields.forEach(function(f) {
|
|
95
|
+
empty[f.key] = f.type === 'boolean' ? 0 : '';
|
|
96
|
+
});
|
|
97
|
+
setEditingItem('__new__');
|
|
98
|
+
setEditForm(empty);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function startEdit(item) {
|
|
102
|
+
var form = {};
|
|
103
|
+
currentTab.fields.forEach(function(f) {
|
|
104
|
+
form[f.key] = item[f.key] !== undefined ? item[f.key] : '';
|
|
105
|
+
});
|
|
106
|
+
setEditingItem(item.id);
|
|
107
|
+
setEditForm(form);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cancelEdit() {
|
|
111
|
+
setEditingItem(null);
|
|
112
|
+
setEditForm({});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function updateField(fieldKey, value) {
|
|
116
|
+
setEditForm(function(prev) { var next = { ...prev }; next[fieldKey] = value; return next; });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function handleSave() {
|
|
120
|
+
setSaving(true);
|
|
121
|
+
setError('');
|
|
122
|
+
try {
|
|
123
|
+
var data = { ...editForm };
|
|
124
|
+
if (editingItem !== '__new__') {
|
|
125
|
+
data.id = editingItem;
|
|
126
|
+
}
|
|
127
|
+
var result = await saveMasterItem(activeTab, data);
|
|
128
|
+
|
|
129
|
+
if (editingItem === '__new__') {
|
|
130
|
+
data.id = result.id;
|
|
131
|
+
setItems(function(prev) {
|
|
132
|
+
var next = { ...prev };
|
|
133
|
+
next[activeTab] = (next[activeTab] || []).concat([data]);
|
|
134
|
+
return next;
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
setItems(function(prev) {
|
|
138
|
+
var next = { ...prev };
|
|
139
|
+
next[activeTab] = (next[activeTab] || []).map(function(item) {
|
|
140
|
+
if (item.id !== editingItem) return item;
|
|
141
|
+
return { ...item, ...data };
|
|
142
|
+
});
|
|
143
|
+
return next;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
setEditingItem(null);
|
|
148
|
+
setEditForm({});
|
|
149
|
+
} catch (err) {
|
|
150
|
+
setError(err.message);
|
|
151
|
+
} finally {
|
|
152
|
+
setSaving(false);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function handleDelete(id) {
|
|
157
|
+
setSaving(true);
|
|
158
|
+
setError('');
|
|
159
|
+
try {
|
|
160
|
+
await deleteMasterItem(activeTab, id);
|
|
161
|
+
setItems(function(prev) {
|
|
162
|
+
var next = { ...prev };
|
|
163
|
+
next[activeTab] = (next[activeTab] || []).filter(function(item) { return item.id !== id; });
|
|
164
|
+
return next;
|
|
165
|
+
});
|
|
166
|
+
if (editingItem === id) {
|
|
167
|
+
setEditingItem(null);
|
|
168
|
+
setEditForm({});
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
setError(err.message);
|
|
172
|
+
} finally {
|
|
173
|
+
setSaving(false);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
tabs: TABS,
|
|
179
|
+
activeTab,
|
|
180
|
+
setActiveTab: handleTabChange,
|
|
181
|
+
currentTab,
|
|
182
|
+
items: filteredItems,
|
|
183
|
+
groupNames,
|
|
184
|
+
search,
|
|
185
|
+
setSearch,
|
|
186
|
+
loading,
|
|
187
|
+
error,
|
|
188
|
+
saving,
|
|
189
|
+
editingItem,
|
|
190
|
+
editForm,
|
|
191
|
+
startAdd,
|
|
192
|
+
startEdit,
|
|
193
|
+
cancelEdit,
|
|
194
|
+
updateField,
|
|
195
|
+
handleSave,
|
|
196
|
+
handleDelete,
|
|
197
|
+
reload: function() { loadItems(activeTab); },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { getProfile, updateProfile } from './api.js';
|
|
3
|
+
|
|
4
|
+
export function useProfileController(options = {}) {
|
|
5
|
+
var [form, setForm] = useState({ name: '', email: '', phoneNumber: '' });
|
|
6
|
+
var [error, setError] = useState('');
|
|
7
|
+
var [success, setSuccess] = useState('');
|
|
8
|
+
var [loading, setLoading] = useState(false);
|
|
9
|
+
var [fetching, setFetching] = useState(true);
|
|
10
|
+
|
|
11
|
+
var onSuccess = options.onSuccess || null;
|
|
12
|
+
|
|
13
|
+
useEffect(function() {
|
|
14
|
+
fetchProfile();
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
async function fetchProfile() {
|
|
18
|
+
setFetching(true);
|
|
19
|
+
try {
|
|
20
|
+
var data = await getProfile();
|
|
21
|
+
setForm({
|
|
22
|
+
name: data.name || '',
|
|
23
|
+
email: data.email || '',
|
|
24
|
+
phoneNumber: data.phoneNumber || '',
|
|
25
|
+
});
|
|
26
|
+
} catch (err) {
|
|
27
|
+
setError(err.message);
|
|
28
|
+
} finally {
|
|
29
|
+
setFetching(false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function handleChange(e) {
|
|
34
|
+
setForm({ ...form, [e.target.name]: e.target.value });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function handleSubmit(e) {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
setError('');
|
|
40
|
+
setSuccess('');
|
|
41
|
+
setLoading(true);
|
|
42
|
+
try {
|
|
43
|
+
var result = await updateProfile(form);
|
|
44
|
+
setSuccess('Profile updated successfully');
|
|
45
|
+
if (onSuccess) onSuccess(result);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
setError(err.message);
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
form,
|
|
55
|
+
error,
|
|
56
|
+
success,
|
|
57
|
+
loading,
|
|
58
|
+
fetching,
|
|
59
|
+
handleChange,
|
|
60
|
+
handleSubmit,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { registerUser } from './api.js';
|
|
3
|
+
|
|
4
|
+
export function useRegisterController(options = {}) {
|
|
5
|
+
const [form, setForm] = useState({ name: '', email: '', phoneNumber: '', password: '' });
|
|
6
|
+
const [error, setError] = useState('');
|
|
7
|
+
const [success, setSuccess] = useState('');
|
|
8
|
+
const [loading, setLoading] = useState(false);
|
|
9
|
+
|
|
10
|
+
const onSuccess = options.onSuccess || null;
|
|
11
|
+
|
|
12
|
+
function handleChange(e) {
|
|
13
|
+
setForm({ ...form, [e.target.name]: e.target.value });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function handleSubmit(e) {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
setError('');
|
|
19
|
+
setSuccess('');
|
|
20
|
+
setLoading(true);
|
|
21
|
+
try {
|
|
22
|
+
const result = await registerUser(form);
|
|
23
|
+
setSuccess('Registration successful. Please wait, someone will activate you.');
|
|
24
|
+
setForm({ name: '', email: '', phoneNumber: '', password: '' });
|
|
25
|
+
if (onSuccess) onSuccess(result);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
setError(err.message);
|
|
28
|
+
} finally {
|
|
29
|
+
setLoading(false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
form,
|
|
35
|
+
error,
|
|
36
|
+
success,
|
|
37
|
+
loading,
|
|
38
|
+
handleChange,
|
|
39
|
+
handleSubmit,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useSearchParams } from 'react-router-dom';
|
|
3
|
+
import { resetPassword } from './api.js';
|
|
4
|
+
|
|
5
|
+
export function useResetPasswordController() {
|
|
6
|
+
const [searchParams] = useSearchParams();
|
|
7
|
+
const token = searchParams.get('token');
|
|
8
|
+
const [password, setPassword] = useState('');
|
|
9
|
+
const [error, setError] = useState('');
|
|
10
|
+
const [success, setSuccess] = useState('');
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
|
|
13
|
+
async function handleSubmit(e) {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
setError('');
|
|
16
|
+
setSuccess('');
|
|
17
|
+
setLoading(true);
|
|
18
|
+
try {
|
|
19
|
+
const result = await resetPassword({ token, newPassword: password });
|
|
20
|
+
setSuccess(result.message);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
setError(err.message);
|
|
23
|
+
} finally {
|
|
24
|
+
setLoading(false);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
token,
|
|
30
|
+
password, setPassword,
|
|
31
|
+
error,
|
|
32
|
+
success,
|
|
33
|
+
loading,
|
|
34
|
+
handleSubmit,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { getTenants, saveTenant, deleteTenant, getUsers, assignUserTenant } from './adminApi.js';
|
|
3
|
+
|
|
4
|
+
export function useTenantController(options = {}) {
|
|
5
|
+
var [tenants, setTenants] = useState([]);
|
|
6
|
+
var [users, setUsers] = useState([]);
|
|
7
|
+
var [loading, setLoading] = useState(true);
|
|
8
|
+
var [error, setError] = useState('');
|
|
9
|
+
var [saving, setSaving] = useState(false);
|
|
10
|
+
var [editingItem, setEditingItem] = useState(null);
|
|
11
|
+
var [editForm, setEditForm] = useState({});
|
|
12
|
+
var [activeTab, setActiveTab] = useState('tenants');
|
|
13
|
+
var [search, setSearch] = useState('');
|
|
14
|
+
|
|
15
|
+
var level = options.level || 1;
|
|
16
|
+
var label = options.label || 'Company';
|
|
17
|
+
var labels = options.labels || 'Companies';
|
|
18
|
+
|
|
19
|
+
useEffect(function() {
|
|
20
|
+
loadData();
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
async function loadData() {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
setError('');
|
|
26
|
+
try {
|
|
27
|
+
var [tenantData, userData] = await Promise.all([
|
|
28
|
+
getTenants(level),
|
|
29
|
+
getUsers()
|
|
30
|
+
]);
|
|
31
|
+
setTenants(tenantData);
|
|
32
|
+
setUsers(userData);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
setError(err.message);
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
var filteredTenants = tenants.filter(function(t) {
|
|
41
|
+
if (!search) return true;
|
|
42
|
+
var q = search.toLowerCase();
|
|
43
|
+
return (t.name && t.name.toLowerCase().includes(q)) ||
|
|
44
|
+
(t.code && t.code.toLowerCase().includes(q));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function startAdd() {
|
|
48
|
+
setEditForm({ name: '', code: '', description: '' });
|
|
49
|
+
setEditingItem('__new__');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function startEdit(item) {
|
|
53
|
+
setEditForm({ name: item.name, code: item.code || '', description: item.description || '' });
|
|
54
|
+
setEditingItem(item.id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cancelEdit() {
|
|
58
|
+
setEditingItem(null);
|
|
59
|
+
setEditForm({});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function updateField(key, value) {
|
|
63
|
+
setEditForm(function(prev) { var next = { ...prev }; next[key] = value; return next; });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function handleSave() {
|
|
67
|
+
setSaving(true);
|
|
68
|
+
setError('');
|
|
69
|
+
try {
|
|
70
|
+
var data = { ...editForm, level: level };
|
|
71
|
+
if (editingItem !== '__new__') data.id = editingItem;
|
|
72
|
+
var result = await saveTenant(data);
|
|
73
|
+
if (editingItem === '__new__') {
|
|
74
|
+
data.id = result.id;
|
|
75
|
+
setTenants(function(prev) { return prev.concat([data]); });
|
|
76
|
+
} else {
|
|
77
|
+
setTenants(function(prev) {
|
|
78
|
+
return prev.map(function(t) { return t.id === editingItem ? { ...t, ...data } : t; });
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
setEditingItem(null);
|
|
82
|
+
setEditForm({});
|
|
83
|
+
} catch (err) {
|
|
84
|
+
setError(err.message);
|
|
85
|
+
} finally {
|
|
86
|
+
setSaving(false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function handleDelete(id) {
|
|
91
|
+
setSaving(true);
|
|
92
|
+
setError('');
|
|
93
|
+
try {
|
|
94
|
+
await deleteTenant(id);
|
|
95
|
+
setTenants(function(prev) { return prev.filter(function(t) { return t.id !== id; }); });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
setError(err.message);
|
|
98
|
+
} finally {
|
|
99
|
+
setSaving(false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function handleAssignUser(userId, tenantId, currentlyAssigned) {
|
|
104
|
+
setError('');
|
|
105
|
+
try {
|
|
106
|
+
await assignUserTenant({ userId, tenantId, assign: !currentlyAssigned });
|
|
107
|
+
// Reload to get fresh user-tenant mappings
|
|
108
|
+
var userData = await getUsers();
|
|
109
|
+
setUsers(userData);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
setError(err.message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isUserAssignedTenant(user, tenantId) {
|
|
116
|
+
return user.tenants && user.tenants.some(function(t) { return t.id === tenantId; });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
var reload = useCallback(function() { loadData(); }, []);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
tenants: filteredTenants,
|
|
123
|
+
users,
|
|
124
|
+
loading,
|
|
125
|
+
error,
|
|
126
|
+
saving,
|
|
127
|
+
editingItem,
|
|
128
|
+
editForm,
|
|
129
|
+
activeTab,
|
|
130
|
+
setActiveTab,
|
|
131
|
+
search,
|
|
132
|
+
setSearch,
|
|
133
|
+
level,
|
|
134
|
+
label,
|
|
135
|
+
labels,
|
|
136
|
+
startAdd,
|
|
137
|
+
startEdit,
|
|
138
|
+
cancelEdit,
|
|
139
|
+
updateField,
|
|
140
|
+
handleSave,
|
|
141
|
+
handleDelete,
|
|
142
|
+
handleAssignUser,
|
|
143
|
+
isUserAssignedTenant,
|
|
144
|
+
reload,
|
|
145
|
+
};
|
|
146
|
+
}
|