ui-soxo-bootstrap-core 2.5.6 → 2.5.7

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.
@@ -0,0 +1,318 @@
1
+ /**
2
+ * AssignRole Component
3
+ *
4
+ * @description
5
+ * Allows administrators to:
6
+ * - View user details
7
+ * - Assign one or more roles to a user
8
+ * - View menus associated with a selected role (read-only)
9
+ *
10
+ * UI Layout:
11
+ * - Left Panel: User info + role list with search and checkboxes
12
+ * - Right Panel: Menu tree preview for the selected role
13
+ *
14
+ * @returns {JSX.Element}
15
+ */
16
+ import React, { useMemo, useState, useEffect } from 'react';
17
+ import { Checkbox, Input, Typography, Empty, Avatar, List, Card, message, Skeleton } from 'antd';
18
+
19
+ import { UsersAPI, RolesAPI, MenusAPI } from '../../..';
20
+ import { UserRolesAPI } from '../../..';
21
+ import { useParams } from 'react-router-dom';
22
+ import { Button } from '../../../../lib';
23
+ import { MenuTree } from '../../../../lib/elements/basic/menu-tree/menu-tree';
24
+ import './assign-role.scss';
25
+
26
+ const { Text } = Typography;
27
+
28
+ const { Search } = Input;
29
+
30
+ export default function AssignRole() {
31
+ /**
32
+ * User ID from route params
33
+ * @type {{ id: string }}
34
+ */
35
+ const { id } = useParams();
36
+ // for user
37
+ const [user, setUser] = useState(null);
38
+ // for roles
39
+ const [roles, setRoles] = useState([]);
40
+ // for active roles
41
+ const [activeRole, setActiveRole] = useState(null);
42
+ const [modules, setModules] = useState([]);
43
+ // loading
44
+ const [selectedRoles, setSelectedRoles] = useState([]);
45
+ const [loadingUser, setLoadingUser] = useState(false);
46
+ const [loadingRoles, setLoadingRoles] = useState(false);
47
+ const [loadingMenus, setLoadingMenus] = useState(false);
48
+ // for save
49
+ const [saving, setSaving] = useState(false);
50
+
51
+ const [selectedMenus, setSelectedMenus] = useState([]);
52
+ const [search, setSearch] = useState('');
53
+
54
+ // for initial roles
55
+ const [initialRoles, setInitialRoles] = useState([]);
56
+
57
+ /**
58
+ * Load user details and assigned roles when user ID changes
59
+ */
60
+ useEffect(() => {
61
+ if (id) loadUser();
62
+ }, [id]);
63
+
64
+ /**
65
+ * Fetch user details and assigned roles
66
+ *
67
+ * - Extracts user info from first record
68
+ * - Deduplicates role IDs
69
+ *
70
+ * @async
71
+ * @returns {Promise<void>}
72
+ */
73
+
74
+ const loadUser = async () => {
75
+ setLoadingUser(true);
76
+
77
+ try {
78
+ // Call both APIs in parallel
79
+ const [userRes, roleRes] = await Promise.all([UsersAPI.getUser({ id }), UsersAPI.getUserRole({ id })]);
80
+
81
+ /* ---------- USER DETAILS ---------- */
82
+ const userData = userRes?.result;
83
+ if (userData) {
84
+ setUser({
85
+ id: userData.id,
86
+ name: userData.name,
87
+ });
88
+ } else {
89
+ setUser(null);
90
+ }
91
+
92
+ /* ---------- USER ROLES ---------- */
93
+ const roleList = Array.isArray(roleRes?.result) ? roleRes.result : [];
94
+
95
+ // Extract VALID role IDs (ignore nulls & duplicates)
96
+ const roleIds = [...new Set(list.filter((item) => item.role_id && item.active === 'Y').map((item) => Number(item.role_id)))];
97
+
98
+ setSelectedRoles(roleIds);
99
+ setInitialRoles(roleIds);
100
+ } catch (e) {
101
+ console.error(e);
102
+ message.error('Unable to load user details');
103
+ } finally {
104
+ setLoadingUser(false);
105
+ }
106
+ };
107
+
108
+ /**
109
+ * Load active roles on component mount
110
+ */
111
+ useEffect(() => {
112
+ getRoles();
113
+ }, []);
114
+
115
+ /**
116
+ * Fetch all active roles
117
+ *
118
+ * @async
119
+ * @returns {Promise<void>}
120
+ */
121
+
122
+ const getRoles = async () => {
123
+ setLoadingRoles(true);
124
+ try {
125
+ const res = await RolesAPI.getRole();
126
+ const activeRoles = Array.isArray(res?.result) ? res.result.filter((r) => r.active === 'Y') : [];
127
+ setRoles(activeRoles);
128
+ } catch (e) {
129
+ console.error(e);
130
+ setRoles([]);
131
+ } finally {
132
+ setLoadingRoles(false);
133
+ }
134
+ };
135
+
136
+ /**
137
+ * Recursively filter menus by allowed menu IDs
138
+ *
139
+ * @param {Array<Object>} menus - Full menu tree
140
+ * @param {Array<number>} allowedIds - Menu IDs assigned to role
141
+ * @returns {Array<Object>}
142
+ */
143
+
144
+ const filterAndSortMenus = (menus, allowedIds = []) => {
145
+ if (!Array.isArray(menus) || !Array.isArray(allowedIds)) return [];
146
+ return menus.filter((m) => allowedIds.includes(m.id)).map((m) => ({ ...m, sub_menus: filterAndSortMenus(m.sub_menus || [], allowedIds) }));
147
+ };
148
+
149
+ /**
150
+ * Load menus for a selected role
151
+ *
152
+ * @param {Object} role - Selected role object
153
+ * @async
154
+ * @returns {Promise<void>}
155
+ */
156
+
157
+ const loadRoleMenus = async (role) => {
158
+ setActiveRole(role);
159
+ setSelectedMenus(role.menu_ids || []);
160
+ setLoadingMenus(true);
161
+ try {
162
+ const res = await MenusAPI.getCoreMenuLists();
163
+ const allMenus = res.result || [];
164
+ const filteredMenus = filterAndSortMenus(allMenus, role.menu_ids);
165
+ setModules(filteredMenus);
166
+ } catch (e) {
167
+ console.error(e);
168
+ setModules([]);
169
+ } finally {
170
+ setLoadingMenus(false);
171
+ }
172
+ };
173
+
174
+ /**
175
+ * Toggle menu selection (currently read-only usage)
176
+ *
177
+ * @param {number} menuId
178
+ * @param {boolean} checked
179
+ */
180
+ const toggleMenu = (menuId, checked) => {
181
+ setSelectedMenus((prev) => (checked ? [...prev, menuId] : prev.filter((id) => id !== menuId)));
182
+ };
183
+
184
+ /** Filtered roles */
185
+ const filteredRoles = useMemo(() => {
186
+ if (!search) return roles;
187
+ return roles.filter((r) => r?.name?.toLowerCase().includes(search.toLowerCase()));
188
+ }, [roles, search]);
189
+
190
+ /**
191
+ * Toggle role selection
192
+ *
193
+ * @param {number} roleId
194
+ * @param {boolean} checked
195
+ */
196
+
197
+ const toggleRole = (roleId, checked) => {
198
+ const next = checked ? [...selectedRoles, roleId] : selectedRoles.filter((v) => v !== roleId);
199
+ setSelectedRoles(next);
200
+ };
201
+
202
+ /**
203
+ * Save selected roles for the user
204
+ *
205
+ * @async
206
+ * @returns {Promise<void>}
207
+ */
208
+ const handleSaveUserRole = async () => {
209
+ if (!id) {
210
+ message.error('Invalid user');
211
+ return;
212
+ }
213
+ // start button spinner
214
+ setSaving(true);
215
+
216
+ try {
217
+ // roles to ADD
218
+ const role_ids = selectedRoles.filter((rid) => !initialRoles.includes(rid));
219
+
220
+ // roles to REMOVE
221
+ const update_ids = initialRoles.filter((rid) => !selectedRoles.includes(rid));
222
+
223
+ if (!role_ids.length && !update_ids.length) {
224
+ message.info('No changes to save');
225
+ return;
226
+ }
227
+
228
+ const payload = {
229
+ user_id: id,
230
+ role_ids,
231
+ update_ids,
232
+ };
233
+
234
+ await UserRolesAPI.addUserRoleSave(payload);
235
+
236
+ // Only show success AFTER all saves
237
+
238
+ message.success('User roles updated successfully');
239
+
240
+ await loadUser();
241
+
242
+ } catch (err) {
243
+ console.error(err);
244
+ message.error('Failed to save user roles');
245
+ } finally {
246
+ // stop button spinner
247
+ setSaving(false);
248
+ }
249
+ };
250
+
251
+ return (
252
+ <section className="assign-role">
253
+ {/* LEFT PANEL */}
254
+ <Card className="left-panel" bodyStyle={{ padding: 12 }}>
255
+ <div size="small" className="user-card">
256
+ <Avatar size={40}>{user?.name?.[0] || ''}</Avatar>
257
+
258
+ <div className="user-info">
259
+ <div>{user?.name || '--'}</div>
260
+ <Text className="user-id">ID : {user?.id || '--'}</Text>
261
+ </div>
262
+ </div>
263
+
264
+ <div className="role-list-header">
265
+ <Text strong>Role List ({roles.length})</Text>
266
+ </div>
267
+
268
+ <Search placeholder="Enter Search Value" allowClear style={{ width: 300, marginBottom: '0px' }} onChange={(e) => setSearch(e.target.value)} />
269
+ {/* <Input className="role-search" placeholder="Search Here" allowClear value={search} onChange={(e) => setSearch(e.target.value)} /> */}
270
+
271
+ <List
272
+ itemLayout="horizontal"
273
+ dataSource={filteredRoles}
274
+ renderItem={(role) => (
275
+ <List.Item
276
+ key={role.id}
277
+ onClick={() => loadRoleMenus(role)}
278
+ className={`role-item ${activeRole?.id === role.id ? 'active' : ''}`}
279
+ actions={[
280
+ <Checkbox
281
+ checked={selectedRoles.includes(role.id)}
282
+ onChange={(e) => toggleRole(role.id, e.target.checked)}
283
+ onClick={(e) => e.stopPropagation()}
284
+ />,
285
+ ]}
286
+ >
287
+ <List.Item.Meta avatar={<Avatar size={28}>{role?.name?.[0] || '-'}</Avatar>} title={role.name} description={role.description} />
288
+ </List.Item>
289
+ )}
290
+ />
291
+ </Card>
292
+
293
+ {/* RIGHT PANEL */}
294
+ <Card className="right-panel">
295
+ <div className="menus-header">
296
+ <Text>Menus {activeRole ? `– ${activeRole.name}` : ''}</Text>
297
+ <div className="sub-text">It's not editable. To edit it, go to the role editing page.</div>
298
+ </div>
299
+
300
+ <div className="menus-content">
301
+ {loadingMenus ? (
302
+ <Skeleton active paragraph={{ rows: 6 }} />
303
+ ) : modules.length === 0 ? (
304
+ <Empty description="Click a role to view menus" />
305
+ ) : (
306
+ <MenuTree menus={modules} selectedMenus={selectedMenus} toggleMenu={toggleMenu} showCheckbox={false} />
307
+ )}
308
+ </div>
309
+
310
+ <div className="footer-actions">
311
+ <Button type="primary" onClick={handleSaveUserRole} loading={saving} disabled={!selectedRoles.length}>
312
+ Save
313
+ </Button>
314
+ </div>
315
+ </Card>
316
+ </section>
317
+ );
318
+ }
@@ -0,0 +1,117 @@
1
+ .assign-role {
2
+ display: flex;
3
+ gap: 6px;
4
+
5
+ .left-panel {
6
+ width: 340px;
7
+
8
+ .user-card {
9
+ margin-bottom: 12px;
10
+ background: #fafafa;
11
+ border: none;
12
+ display: flex;
13
+ align-items: center;
14
+ gap: 12px;
15
+ padding: 8px;
16
+
17
+ .user-info {
18
+ font-weight: 500;
19
+
20
+ .user-id {
21
+ font-size: 12px;
22
+ color: rgba(0, 0, 0, 0.45);
23
+ }
24
+ }
25
+ }
26
+
27
+ .role-list-header {
28
+ display: flex;
29
+ justify-content: space-between;
30
+ margin-bottom: 12px;
31
+ strong {
32
+ font-weight: 600;
33
+ }
34
+ }
35
+
36
+ .role-search {
37
+ margin: 12px 0;
38
+ }
39
+
40
+ .role-item {
41
+ cursor: pointer;
42
+ border-radius: 6px;
43
+ padding: 8px 12px;
44
+
45
+ &.active {
46
+ background: #e6f7ff;
47
+ }
48
+
49
+ .ant-list-item-meta-title {
50
+ font-weight: 500;
51
+ }
52
+
53
+ .ant-list-item-meta-description {
54
+ font-size: 12px;
55
+ color: rgba(0, 0, 0, 0.45);
56
+ }
57
+ }
58
+ }
59
+
60
+ .right-panel {
61
+ flex: 1;
62
+ display: flex;
63
+ flex-direction: column;
64
+ justify-content: space-between;
65
+ padding: 16px;
66
+
67
+ .menus-header {
68
+ font-weight: 500;
69
+ margin-bottom: 4px;
70
+ color: #000;
71
+
72
+ .sub-text {
73
+ font-size: 12px;
74
+ color: #888;
75
+ margin-top: 4px;
76
+ }
77
+ }
78
+
79
+ .menus-content {
80
+ margin-top: 16px;
81
+ }
82
+
83
+ .footer-actions {
84
+ margin-top: 16px;
85
+ display: flex;
86
+ justify-content: flex-end;
87
+ gap: 8px;
88
+ }
89
+ }
90
+
91
+ /* ===============================
92
+ 📱 iPad Mini (481px – 768px)
93
+ =============================== */
94
+ @media (max-width: 768px) {
95
+ .assign-role {
96
+ flex-direction: column;
97
+ gap: 12px;
98
+ }
99
+
100
+ .assign-role .left-panel {
101
+ width: 100%;
102
+ }
103
+
104
+ .assign-role .right-panel {
105
+ width: 100%;
106
+ }
107
+
108
+ /* Better spacing for tablets */
109
+ .assign-role .right-panel {
110
+ padding: 14px;
111
+ }
112
+
113
+ .assign-role .footer-actions {
114
+ justify-content: flex-end;
115
+ }
116
+ }
117
+ }
@@ -11,7 +11,6 @@ export default function UserEdit(record) {
11
11
  // Handle edit button click
12
12
  const handleEditClick = () => {
13
13
  if (!record.id) {
14
- console.warn('Invalid record: Missing ID');
15
14
  return;
16
15
  }
17
16
 
@@ -29,9 +28,7 @@ export default function UserEdit(record) {
29
28
  // Try parsing other_details, handle error if invalid JSON
30
29
  try {
31
30
  otherDetails = JSON.parse(apiData.other_details);
32
- } catch (err) {
33
- console.warn('Failed to parse other_details:', apiData.other_details);
34
- }
31
+ } catch (err) {}
35
32
  }
36
33
  let orgDetails = {};
37
34
  try {
@@ -50,8 +47,10 @@ export default function UserEdit(record) {
50
47
  designation: apiData.designation_code,
51
48
  department: apiData.department_id,
52
49
  // Handle selected branches and default branch
50
+ organization_details: orgDetails,
53
51
  selectedBranches: orgDetails.branch_ids || [],
54
- defaultBranch: apiData.firm_id || null,
52
+ defaultBranch: apiData.branch_id || null,
53
+ role_id: apiData.role_id,
55
54
  password: apiData.password,
56
55
  user_group: apiData.user_group,
57
56
  // Handle doctor codes
@@ -365,7 +365,7 @@ const UserList = ({ model, match, relativeAdd = false, additional_queries = [],
365
365
  )}
366
366
 
367
367
  {/* Add Modal */}
368
- <Modal destroyOnClose confirmLoading={loading} visible={visible} onCancel={closeModal} footer={null}>
368
+ <Modal destroyOnClose={false} confirmLoading={loading} visible={visible} onCancel={closeModal} footer={null}>
369
369
  <model.ModalAddComponent
370
370
  match={match}
371
371
  model={model}
@@ -273,8 +273,23 @@ class Users extends Base {
273
273
  updateUser = ({ id, formBody }) => {
274
274
  return ApiUtils.put({ url: `users/update-user/${id}`, formBody });
275
275
  };
276
+
277
+ /* The `getUser` method is a function that takes an object with an `id` property as a parameter. It
278
+ then uses the `ApiUtils.get` function to make a GET request to the endpoint `users/` to fetch
279
+ user data based on the provided `id`. This method is used to retrieve a specific user's
280
+ information from the API. */
281
+
276
282
  getUser = ({ id }) => {
277
283
  return ApiUtils.get({ url: `users/${id}` });
284
+ };
285
+ /* The `getUserRole` method is a function that takes an object with an `id` property as a parameter.
286
+ It then uses the `ApiUtils.get` function to make a GET request to the endpoint `core-user-roles`
287
+ with the `user_id` parameter set to the provided `id`. This method is used to fetch the role or
288
+ roles associated with a specific user from the API. */
289
+
290
+ getUserRole = ({ id }) => {
291
+ return ApiUtils.get({ url: `core-user-roles?user_id=${id}&active=Y` });
292
+
278
293
  };
279
294
 
280
295
  /**
@@ -8,6 +8,8 @@ import GenericDetail from './generic/components/generic-detail/generic-detail';
8
8
 
9
9
  import ModuleRoutes from './module-routes/module-routes';
10
10
 
11
+ import { AssignRole } from './reporting/components';
12
+
11
13
  // All Dashboard Components
12
14
 
13
15
  import DashboardCard from './dashboard/components/dashboard-card/dashboard-card';
@@ -30,6 +32,7 @@ export {
30
32
  GenericDetail,
31
33
  ModuleRoutes,
32
34
  DashboardCard,
35
+ AssignRole,
33
36
  PopQueryDashboard,
34
37
  HomePageAPI,
35
38
  ReportingDashboard,
@@ -1,5 +1,6 @@
1
1
  import UserEdit from '../../../models/users/components/user-add/user-edit';
2
2
 
3
3
  import UserAdd from '../../../models/users/components/user-add/user-add';
4
+ import AssignRole from '../../../models/users/components/assign-role/assign-role';
4
5
 
5
- export { UserEdit, UserAdd };
6
+ export { UserEdit, UserAdd, AssignRole };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-soxo-bootstrap-core",
3
- "version": "2.5.6",
3
+ "version": "2.5.7",
4
4
  "description": "All the Core Components for you to start",
5
5
  "keywords": [
6
6
  "all in one"