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.
- package/core/components/menu-template-api/menu-template-api.js +2 -2
- package/core/lib/components/sidemenu/sidemenu.js +19 -13
- package/core/lib/pages/login/login.js +35 -10
- package/core/lib/pages/login/reset-password.js +2 -2
- package/core/lib/utils/api/api.utils.js +71 -48
- package/core/lib/utils/common/common.utils.js +24 -0
- package/core/lib/utils/index.js +22 -28
- package/core/models/base/base.js +7 -3
- package/core/models/menus/components/menu-lists/menu-lists.js +2 -2
- package/core/models/menus/menus.js +21 -2
- package/core/models/users/components/assign-role/assign-role.js +138 -50
- package/core/models/users/components/assign-role/assign-role.scss +209 -45
- package/core/models/users/components/assign-role/avatar-props.js +45 -0
- package/core/models/users/components/user-add/user-add.js +13 -10
- package/core/models/users/components/user-add/user-edit.js +19 -2
- package/core/models/users/users.js +24 -8
- package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +4 -2
- package/package.json +1 -1
|
@@ -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(
|
|
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.
|
|
162
|
+
const res = await MenusAPI.getCoreMenuByRoleId(role_id);
|
|
163
163
|
const allMenus = res.result || [];
|
|
164
|
-
|
|
165
|
-
setModules(
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
<
|
|
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="
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
</
|
|
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
|
|
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
|
-
<
|
|
297
|
-
<div className="sub-text">
|
|
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
|
-
<
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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:
|
|
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:
|
|
157
|
+
padding: 10px 12px;
|
|
158
|
+
transition: 0.2s;
|
|
159
|
+
|
|
160
|
+
&:hover {
|
|
161
|
+
background: #f5f7fa;
|
|
162
|
+
}
|
|
44
163
|
|
|
45
164
|
&.active {
|
|
46
|
-
background: #
|
|
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:
|
|
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
|
-
|
|
65
|
-
|
|
211
|
+
height: 100%;
|
|
212
|
+
min-height: 0;
|
|
66
213
|
|
|
67
214
|
.menus-header {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
gap: 12px;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.assign-role .left-panel {
|
|
101
|
-
width: 100%;
|
|
102
|
-
}
|
|
272
|
+
flex-direction: column;
|
|
273
|
+
height: auto;
|
|
103
274
|
|
|
104
|
-
.
|
|
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
|
+
};
|