ui-soxo-bootstrap-core 2.6.2 → 2.6.4

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.
@@ -21,6 +21,7 @@ import { UserRolesAPI } from '../../..';
21
21
  import { useParams } from 'react-router-dom';
22
22
  import { Button } from '../../../../lib';
23
23
  import { MenuTree } from '../../../../lib/elements/basic/menu-tree/menu-tree';
24
+ import { getAvatarProps } from './avatar-props';
24
25
  import './assign-role.scss';
25
26
 
26
27
  const { Text } = Typography;
@@ -42,7 +43,6 @@ export default function AssignRole() {
42
43
  const [modules, setModules] = useState([]);
43
44
  // loading
44
45
  const [selectedRoles, setSelectedRoles] = useState([]);
45
- const [loadingUser, setLoadingUser] = useState(false);
46
46
  const [loadingRoles, setLoadingRoles] = useState(false);
47
47
  const [loadingMenus, setLoadingMenus] = useState(false);
48
48
  // for save
@@ -54,6 +54,8 @@ export default function AssignRole() {
54
54
  // for initial roles
55
55
  const [initialRoles, setInitialRoles] = useState([]);
56
56
 
57
+ const userAvatar = getAvatarProps(user?.name);
58
+
57
59
  /**
58
60
  * Load user details and assigned roles when user ID changes
59
61
  */
@@ -72,8 +74,6 @@ export default function AssignRole() {
72
74
  */
73
75
 
74
76
  const loadUser = async () => {
75
- setLoadingUser(true);
76
-
77
77
  try {
78
78
  // Call both APIs in parallel
79
79
  const [userRes, roleRes] = await Promise.all([UsersAPI.getUser({ id }), UsersAPI.getUserRole({ id })]);
@@ -93,15 +93,13 @@ export default function AssignRole() {
93
93
  const roleList = Array.isArray(roleRes?.result) ? roleRes.result : [];
94
94
 
95
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)))];
96
+ const roleIds = [...new Set(roleList.filter((item) => item.role_id && item.active === 'Y').map((item) => Number(item.role_id)))];
97
97
 
98
98
  setSelectedRoles(roleIds);
99
- setInitialRoles(roleIds);
99
+ setInitialRoles(roleIds);
100
100
  } catch (e) {
101
101
  console.error(e);
102
102
  message.error('Unable to load user details');
103
- } finally {
104
- setLoadingUser(false);
105
103
  }
106
104
  };
107
105
 
@@ -158,11 +156,13 @@ export default function AssignRole() {
158
156
  setActiveRole(role);
159
157
  setSelectedMenus(role.menu_ids || []);
160
158
  setLoadingMenus(true);
159
+
160
+ const role_id = role.id;
161
161
  try {
162
- const res = await MenusAPI.getCoreMenuLists();
162
+ const res = await MenusAPI.getCoreMenuByRoleId(role_id);
163
163
  const allMenus = res.result || [];
164
- const filteredMenus = filterAndSortMenus(allMenus, role.menu_ids);
165
- setModules(filteredMenus);
164
+
165
+ setModules(allMenus);
166
166
  } catch (e) {
167
167
  console.error(e);
168
168
  setModules([]);
@@ -206,10 +206,10 @@ export default function AssignRole() {
206
206
  * @returns {Promise<void>}
207
207
  */
208
208
  const handleSaveUserRole = async () => {
209
- if (!id) {
210
- message.error('Invalid user');
211
- return;
212
- }
209
+ if (!id) {
210
+ message.error('Invalid user');
211
+ return;
212
+ }
213
213
  // start button spinner
214
214
  setSaving(true);
215
215
 
@@ -236,9 +236,8 @@ export default function AssignRole() {
236
236
  // Only show success AFTER all saves
237
237
 
238
238
  message.success('User roles updated successfully');
239
-
240
- await loadUser();
241
239
 
240
+ await loadUser();
242
241
  } catch (err) {
243
242
  console.error(err);
244
243
  message.error('Failed to save user roles');
@@ -248,69 +247,158 @@ export default function AssignRole() {
248
247
  }
249
248
  };
250
249
 
250
+ // Role Change
251
+ const rolesChanged = useMemo(() => {
252
+ // Length mismatch means roles were added or removed
253
+ if (initialRoles.length !== selectedRoles.length) return true;
254
+
255
+ // Sort both arrays before comparison (order should not matter)
256
+ const sortedInitial = [...initialRoles].sort();
257
+ const sortedSelected = [...selectedRoles].sort();
258
+
259
+ // Check for any value difference
260
+ return sortedInitial.some((value, index) => value !== sortedSelected[index]);
261
+ }, [initialRoles, selectedRoles]);
262
+
263
+ // View All Role
264
+ const handleViewAll = async () => {
265
+ setLoadingMenus(true);
266
+ setModules([]);
267
+ setActiveRole({ name: 'All Roles' });
268
+
269
+ try {
270
+ const res = await MenusAPI.getMenubyUser(id);
271
+ setModules(res?.result?.menus ?? []);
272
+ setLoadingMenus(false);
273
+ } catch (err) {
274
+ setLoadingMenus(false);
275
+ console.error(err);
276
+ setModules([]);
277
+ }
278
+ };
279
+
251
280
  return (
252
281
  <section className="assign-role">
253
282
  {/* 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>
283
+ <Card
284
+ className="left-panel"
285
+ bodyStyle={{
286
+ padding: 16,
287
+ height: '100%',
288
+ display: 'flex',
289
+ flexDirection: 'column',
290
+ }}
291
+ >
292
+ <div className="user-card">
293
+ <Avatar size={44} style={userAvatar.style}>
294
+ {userAvatar.letter}
295
+ </Avatar>
257
296
 
258
297
  <div className="user-info">
259
- <div>{user?.name || '--'}</div>
298
+ <div className="name">{user?.name || '--'}</div>
260
299
  <Text className="user-id">ID : {user?.id || '--'}</Text>
261
300
  </div>
262
301
  </div>
263
302
 
264
303
  <div className="role-list-header">
265
- <Text strong>Role List ({roles.length})</Text>
304
+ <div>
305
+ <Text strong>Role List</Text>
306
+ <div className="count">{roles.length} roles</div>
307
+ </div>
308
+
309
+ <Button size="small" onClick={handleViewAll}>
310
+ View Access
311
+ </Button>
266
312
  </div>
267
313
 
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>
314
+ <Search placeholder="Search roles..." allowClear onChange={(e) => setSearch(e.target.value)} className="role-search" />
315
+
316
+ <div className="selected-summary">{selectedRoles.length} selected</div>
317
+
318
+ {/* SCROLL AREA */}
319
+ <div className="role-list-wrapper">
320
+ {loadingRoles ? (
321
+ <div className="role-skeleton">
322
+ {Array.from({ length: 6 }).map((_, i) => (
323
+ <div key={i} className="role-skeleton-item">
324
+ <Skeleton.Avatar active size={32} shape="circle" />
325
+
326
+ <div className="meta">
327
+ <Skeleton.Input active size="small" style={{ width: 140 }} />
328
+ <Skeleton.Input active size="small" style={{ width: 200 }} />
329
+ </div>
330
+
331
+ <Skeleton.Button active size="small" shape="square" />
332
+ </div>
333
+ ))}
334
+ </div>
335
+ ) : (
336
+ <List
337
+ itemLayout="horizontal"
338
+ dataSource={filteredRoles}
339
+ className="role-list"
340
+ renderItem={(role) => {
341
+ const roleAvatar = getAvatarProps(role?.name);
342
+
343
+ return (
344
+ <List.Item
345
+ key={role.id}
346
+ onClick={() => loadRoleMenus(role)}
347
+ className={`role-item ${activeRole?.id === role.id ? 'active' : ''}`}
348
+ actions={[
349
+ <Checkbox
350
+ checked={selectedRoles.includes(role.id)}
351
+ onChange={(e) => toggleRole(role.id, e.target.checked)}
352
+ onClick={(e) => e.stopPropagation()}
353
+ />,
354
+ ]}
355
+ >
356
+ <List.Item.Meta
357
+ avatar={<Avatar style={roleAvatar.style}>{roleAvatar?.letter}</Avatar>}
358
+ title={role.name}
359
+ description={role.description}
360
+ />
361
+ </List.Item>
362
+ );
363
+ }}
364
+ />
289
365
  )}
290
- />
366
+ </div>
291
367
  </Card>
292
368
 
293
369
  {/* RIGHT PANEL */}
294
- <Card className="right-panel">
370
+ <Card
371
+ className="right-panel"
372
+ bodyStyle={{
373
+ height: '100%',
374
+ display: 'flex',
375
+ flexDirection: 'column',
376
+ padding: 16,
377
+ }}
378
+ >
295
379
  <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>
380
+ <div className="title">Menus {activeRole ? `– ${activeRole.name}` : ''}</div>
381
+ <div className="sub-text">Not editable here. Go to role settings to modify.</div>
298
382
  </div>
299
383
 
300
384
  <div className="menus-content">
301
385
  {loadingMenus ? (
302
386
  <Skeleton active paragraph={{ rows: 6 }} />
303
387
  ) : modules.length === 0 ? (
304
- <Empty description="Click a role to view menus" />
388
+ <div className="empty-state">
389
+ <Empty description="Select a role to view menus" />
390
+ </div>
305
391
  ) : (
306
392
  <MenuTree menus={modules} selectedMenus={selectedMenus} toggleMenu={toggleMenu} showCheckbox={false} />
307
393
  )}
308
394
  </div>
309
395
 
310
396
  <div className="footer-actions">
311
- <Button type="primary" onClick={handleSaveUserRole} loading={saving} disabled={!selectedRoles.length}>
312
- Save
313
- </Button>
397
+ {rolesChanged && (
398
+ <Button type="primary" onClick={handleSaveUserRole} loading={saving}>
399
+ Save Changes
400
+ </Button>
401
+ )}
314
402
  </div>
315
403
  </Card>
316
404
  </section>
@@ -1,25 +1,82 @@
1
1
  .assign-role {
2
2
  display: flex;
3
- gap: 6px;
3
+ gap: 8px;
4
4
 
5
+ /* Checkbox Color Override */
6
+ .ant-checkbox-checked .ant-checkbox-inner {
7
+ background-color: rgb(68, 106, 169);
8
+ border-color: rgb(68, 106, 169);
9
+ }
10
+
11
+ .ant-checkbox:hover .ant-checkbox-inner,
12
+ .ant-checkbox-wrapper:hover .ant-checkbox-inner {
13
+ border-color: rgb(68, 106, 169);
14
+ }
15
+
16
+ .ant-checkbox-checked::after {
17
+ border-color: rgb(68, 106, 169);
18
+ }
19
+
20
+ /* TAKE FULL SCREEN HEIGHT */
21
+ height: calc(100vh - 80px);
22
+ min-height: 0;
23
+
24
+ .ant-card {
25
+ border-radius: 6px;
26
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
27
+ border: none;
28
+ }
29
+
30
+ /* LEFT PANEL */
5
31
  .left-panel {
6
- width: 340px;
32
+ width: 360px;
33
+ display: flex;
34
+ flex-direction: column;
35
+ height: 100%;
36
+ min-height: 0;
7
37
 
38
+ /* User Card */
8
39
  .user-card {
9
- margin-bottom: 12px;
10
- background: #fafafa;
11
- border: none;
12
40
  display: flex;
13
41
  align-items: center;
14
42
  gap: 12px;
15
- padding: 8px;
43
+ padding: 14px;
44
+
45
+ background: #ffffff;
46
+ border-radius: 10px;
47
+
48
+ border: 1px solid #f0f0f0;
49
+
50
+ box-shadow:
51
+ 0 2px 6px rgba(0, 0, 0, 0.06),
52
+ 0 6px 16px rgba(0, 0, 0, 0.04);
53
+
54
+ margin-bottom: 16px;
55
+
56
+ transition: all 0.2s ease;
57
+
58
+ &:hover {
59
+ box-shadow:
60
+ 0 4px 10px rgba(0, 0, 0, 0.08),
61
+ 0 10px 22px rgba(0, 0, 0, 0.06);
62
+ transform: translateY(-1px);
63
+ }
16
64
 
17
65
  .user-info {
18
- font-weight: 500;
66
+ display: flex;
67
+ flex-direction: column;
68
+ line-height: 1.2;
69
+
70
+ .name {
71
+ font-weight: 600;
72
+ font-size: 14px;
73
+ color: #1f1f1f;
74
+ }
19
75
 
20
76
  .user-id {
21
77
  font-size: 12px;
22
- color: rgba(0, 0, 0, 0.45);
78
+ color: #8c8c8c;
79
+ margin-top: 2px;
23
80
  }
24
81
  }
25
82
  }
@@ -27,23 +84,86 @@
27
84
  .role-list-header {
28
85
  display: flex;
29
86
  justify-content: space-between;
30
- margin-bottom: 12px;
31
- strong {
32
- font-weight: 600;
87
+ align-items: center;
88
+ margin-bottom: 10px;
89
+
90
+ .count {
91
+ font-size: 12px;
92
+ color: #888;
33
93
  }
34
94
  }
35
95
 
96
+ .view-all-btn {
97
+ cursor: pointer;
98
+ color: #1677ff;
99
+ font-weight: 500;
100
+ }
101
+
36
102
  .role-search {
37
- margin: 12px 0;
103
+ margin-bottom: 10px;
104
+ }
105
+
106
+ .selected-summary {
107
+ font-size: 12px;
108
+ color: rgb(68, 106, 169);
109
+ margin-bottom: 10px;
110
+ font-weight: 500;
111
+ }
112
+
113
+ /* SCROLL AREA */
114
+ .role-list-wrapper {
115
+ flex: 1;
116
+ overflow-y: auto;
117
+ min-height: 0;
118
+ padding-right: 4px;
119
+ }
120
+
121
+ /* Scrollbar */
122
+ .role-list-wrapper,
123
+ .menus-content {
124
+ scrollbar-width: thin; /* Firefox */
125
+ scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
126
+
127
+ &::-webkit-scrollbar-thumb {
128
+ background: rgba(0, 0, 0, 0);
129
+ }
130
+
131
+ &:hover::-webkit-scrollbar-thumb {
132
+ background: rgba(0, 0, 0, 0.25);
133
+ }
134
+
135
+ &::-webkit-scrollbar {
136
+ width: 6px;
137
+ height: 6px;
138
+ }
139
+
140
+ &::-webkit-scrollbar-track {
141
+ background: transparent;
142
+ }
143
+
144
+ &::-webkit-scrollbar-thumb {
145
+ background: rgba(0, 0, 0, 0.25);
146
+ border-radius: 10px;
147
+ }
148
+
149
+ &::-webkit-scrollbar-thumb:hover {
150
+ background: rgba(0, 0, 0, 0.4);
151
+ }
38
152
  }
39
153
 
40
154
  .role-item {
41
155
  cursor: pointer;
42
156
  border-radius: 6px;
43
- padding: 8px 12px;
157
+ padding: 10px 12px;
158
+ transition: 0.2s;
159
+
160
+ &:hover {
161
+ background: #f5f7fa;
162
+ }
44
163
 
45
164
  &.active {
46
- background: #e6f7ff;
165
+ background: #e6f4ff;
166
+ // border-left: 3px solid rgb(68, 106, 169);
47
167
  }
48
168
 
49
169
  .ant-list-item-meta-title {
@@ -52,66 +172,110 @@
52
172
 
53
173
  .ant-list-item-meta-description {
54
174
  font-size: 12px;
55
- color: rgba(0, 0, 0, 0.45);
175
+ color: #777;
176
+ }
177
+ }
178
+
179
+ /* Skeleton Loader */
180
+ .role-skeleton {
181
+ display: flex;
182
+ flex-direction: column;
183
+ gap: 8px;
184
+ padding: 2px;
185
+
186
+ .role-skeleton-item {
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 12px;
190
+ padding: 10px 12px;
191
+ border-radius: 6px;
192
+
193
+ background: #fff;
194
+ border: 1px solid #f0f0f0;
195
+
196
+ .meta {
197
+ display: flex;
198
+ flex-direction: column;
199
+ gap: 6px;
200
+ flex: 1;
201
+ }
56
202
  }
57
203
  }
58
204
  }
59
205
 
206
+ /* RIGHT PANEL */
60
207
  .right-panel {
61
208
  flex: 1;
62
209
  display: flex;
63
210
  flex-direction: column;
64
- justify-content: space-between;
65
- padding: 16px;
211
+ height: 100%;
212
+ min-height: 0;
66
213
 
67
214
  .menus-header {
68
- font-weight: 500;
69
- margin-bottom: 4px;
70
- color: #000;
215
+ margin-bottom: 12px;
216
+
217
+ .title {
218
+ font-size: 16px;
219
+ font-weight: 600;
220
+ }
71
221
 
72
222
  .sub-text {
73
223
  font-size: 12px;
74
224
  color: #888;
75
- margin-top: 4px;
76
225
  }
77
226
  }
78
227
 
79
228
  .menus-content {
80
- margin-top: 16px;
229
+ flex: 1;
230
+ overflow-y: auto;
231
+ min-height: 0;
232
+ padding: 8px 4px;
233
+ scrollbar-width: thin; /* Firefox */
234
+ scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
235
+
236
+ &::-webkit-scrollbar {
237
+ width: 6px;
238
+ height: 6px;
239
+ }
240
+
241
+ &::-webkit-scrollbar-track {
242
+ background: transparent;
243
+ }
244
+
245
+ &::-webkit-scrollbar-thumb {
246
+ background: rgba(0, 0, 0, 0.25);
247
+ border-radius: 10px;
248
+ }
249
+
250
+ &::-webkit-scrollbar-thumb:hover {
251
+ background: rgba(0, 0, 0, 0.4);
252
+ }
253
+ }
254
+
255
+ .empty-state {
256
+ height: 300px;
257
+ display: flex;
258
+ align-items: center;
259
+ justify-content: center;
81
260
  }
82
261
 
83
262
  .footer-actions {
84
- margin-top: 16px;
85
263
  display: flex;
86
264
  justify-content: flex-end;
87
- gap: 8px;
265
+ padding-top: 12px;
266
+ border-top: 1px solid #f0f0f0;
88
267
  }
89
268
  }
90
269
 
91
- /* ===============================
92
- 📱 iPad Mini (481px – 768px)
93
- =============================== */
270
+ /* TABLET / MOBILE */
94
271
  @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
- }
272
+ flex-direction: column;
273
+ height: auto;
103
274
 
104
- .assign-role .right-panel {
275
+ .left-panel,
276
+ .right-panel {
105
277
  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;
278
+ height: auto;
115
279
  }
116
280
  }
117
281
  }
@@ -0,0 +1,45 @@
1
+ const avatarColors = [
2
+ '#5B8FF9',
3
+ '#61DDAA',
4
+ '#65789B',
5
+ '#a0d911',
6
+ '#F6BD16',
7
+ '#7262FD',
8
+ '#faad14',
9
+ '#78D3F8',
10
+ '#9661BC',
11
+ '#F6903D',
12
+ '#008685',
13
+ '#F08BB4',
14
+ '#722ed1',
15
+ '#eb2f96',
16
+ '#13c2c2',
17
+ '#eb2f96',
18
+ '#fa8c16',
19
+ '#52c41a',
20
+ ];
21
+
22
+ export const getAvatarProps = (name) => {
23
+ const safeName = (name ?? '').toString().trim();
24
+
25
+ // Find first alphabetic character
26
+ const match = safeName.match(/[A-Za-z]/);
27
+ const letter = match ? match[0].toUpperCase() : '-';
28
+
29
+ // deterministic color based on string
30
+ let hash = 0;
31
+ for (let i = 0; i < safeName.length; i++) {
32
+ hash = safeName.charCodeAt(i) + ((hash << 5) - hash);
33
+ }
34
+
35
+ const color = avatarColors[Math.abs(hash) % avatarColors.length] || '#999';
36
+
37
+ return {
38
+ letter,
39
+ style: {
40
+ backgroundColor: color,
41
+ color: '#fff',
42
+ fontWeight: 600,
43
+ },
44
+ };
45
+ };