ui-soxo-bootstrap-core 2.6.0 → 2.6.1-dev.10
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/.github/workflows/npm-publish.yml +49 -19
- package/core/components/extra-info/extra-info-details.js +2 -2
- package/core/components/menu-template-api/menu-template-api.js +2 -2
- package/core/lib/Store.js +3 -3
- package/core/lib/components/global-header/global-header.js +2 -2
- package/core/lib/components/sidemenu/sidemenu.js +19 -13
- package/core/lib/elements/basic/country-phone-input/country-phone-input.js +35 -60
- package/core/lib/elements/basic/country-phone-input/phone-input.scss +14 -0
- package/core/lib/elements/basic/dragabble-wrapper/draggable-wrapper.js +1 -1
- package/core/lib/elements/basic/menu-tree/menu-tree.js +26 -13
- package/core/lib/models/forms/components/form-creator/form-creator.js +468 -502
- package/core/lib/models/forms/components/form-creator/form-creator.scss +5 -4
- package/core/lib/models/menus/components/menu-list/menu-list.js +424 -467
- package/core/lib/pages/change-password/change-password.js +17 -24
- package/core/lib/pages/change-password/change-password.scss +45 -48
- package/core/lib/pages/login/commnication-mode-selection.js +46 -0
- package/core/lib/pages/login/communication-mode-selection.scss +60 -0
- package/core/lib/pages/login/login.js +153 -24
- package/core/lib/pages/login/login.scss +229 -334
- package/core/lib/pages/login/reset-password.js +124 -0
- package/core/lib/pages/login/reset-password.scss +31 -0
- package/core/lib/pages/profile/themes.json +4 -4
- package/core/lib/utils/api/api.utils.js +71 -48
- package/core/lib/utils/common/common.utils.js +109 -0
- package/core/lib/utils/http/http.utils.js +1 -0
- package/core/lib/utils/index.js +25 -28
- package/core/models/base/base.js +7 -3
- package/core/models/core-scripts/core-scripts.js +9 -0
- package/core/models/doctor/components/doctor-add/doctor-add.js +9 -4
- package/core/models/menus/components/menu-add/menu-add.js +1 -1
- package/core/models/menus/components/menu-lists/menu-lists.js +5 -9
- package/core/models/menus/menus.js +21 -2
- package/core/models/roles/components/role-add/role-add.js +92 -59
- package/core/models/roles/components/role-list/role-list.js +1 -1
- package/core/models/roles/roles.js +9 -0
- package/core/models/staff/components/staff-add/staff-add.js +20 -32
- package/core/models/users/components/assign-role/assign-role.js +145 -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 +47 -56
- package/core/models/users/components/user-add/user-edit.js +25 -4
- package/core/models/users/users.js +34 -8
- package/core/modules/dashboard/components/dashboard-card/menu-dashboard-card.js +1 -1
- package/core/modules/reporting/components/reporting-dashboard/README.md +316 -0
- package/core/modules/reporting/components/reporting-dashboard/adavance-search/advance-search.js +120 -0
- package/core/modules/reporting/components/reporting-dashboard/display-columns/build-display-columns.js +75 -0
- package/core/modules/reporting/components/reporting-dashboard/display-columns/build-display-columns.test.js +74 -0
- package/core/modules/reporting/components/reporting-dashboard/display-columns/display-cell-renderer.js +252 -0
- package/core/modules/reporting/components/reporting-dashboard/display-columns/display-cell-renderer.test.js +126 -0
- package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +222 -376
- package/core/modules/steps/action-buttons.js +47 -45
- package/core/modules/steps/action-buttons.scss +35 -6
- package/core/modules/steps/steps.js +12 -10
- package/core/modules/steps/steps.scss +229 -31
- package/core/modules/steps/timeline.js +21 -19
- package/package.json +3 -2
- package/core/components/external-window/DEVELOPER_GUIDE.md +0 -705
|
@@ -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,165 @@ 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
|
+
dataSource={[...filteredRoles].sort((a, b) => {
|
|
340
|
+
const aSelected = selectedRoles.includes(a.id);
|
|
341
|
+
const bSelected = selectedRoles.includes(b.id);
|
|
342
|
+
|
|
343
|
+
if (aSelected === bSelected) return 0;
|
|
344
|
+
return aSelected ? -1 : 1;
|
|
345
|
+
})}
|
|
346
|
+
className="role-list"
|
|
347
|
+
renderItem={(role) => {
|
|
348
|
+
const roleAvatar = getAvatarProps(role?.name);
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<List.Item
|
|
352
|
+
key={role.id}
|
|
353
|
+
onClick={() => loadRoleMenus(role)}
|
|
354
|
+
className={`role-item ${activeRole?.id === role.id ? 'active' : ''}`}
|
|
355
|
+
actions={[
|
|
356
|
+
<Checkbox
|
|
357
|
+
checked={selectedRoles.includes(role.id)}
|
|
358
|
+
onChange={(e) => toggleRole(role.id, e.target.checked)}
|
|
359
|
+
onClick={(e) => e.stopPropagation()}
|
|
360
|
+
/>,
|
|
361
|
+
]}
|
|
362
|
+
>
|
|
363
|
+
<List.Item.Meta
|
|
364
|
+
avatar={<Avatar style={roleAvatar.style}>{roleAvatar?.letter}</Avatar>}
|
|
365
|
+
title={role.name}
|
|
366
|
+
description={role.description}
|
|
367
|
+
/>
|
|
368
|
+
</List.Item>
|
|
369
|
+
);
|
|
370
|
+
}}
|
|
371
|
+
/>
|
|
289
372
|
)}
|
|
290
|
-
|
|
373
|
+
</div>
|
|
291
374
|
</Card>
|
|
292
375
|
|
|
293
376
|
{/* RIGHT PANEL */}
|
|
294
|
-
<Card
|
|
377
|
+
<Card
|
|
378
|
+
className="right-panel"
|
|
379
|
+
bodyStyle={{
|
|
380
|
+
height: '100%',
|
|
381
|
+
display: 'flex',
|
|
382
|
+
flexDirection: 'column',
|
|
383
|
+
padding: 16,
|
|
384
|
+
}}
|
|
385
|
+
>
|
|
295
386
|
<div className="menus-header">
|
|
296
|
-
<
|
|
297
|
-
<div className="sub-text">
|
|
387
|
+
<div className="title">Menus {activeRole ? `– ${activeRole.name}` : ''}</div>
|
|
388
|
+
<div className="sub-text">You don’t have permission to edit this here. Update it in Role Settings.</div>
|
|
298
389
|
</div>
|
|
299
390
|
|
|
300
391
|
<div className="menus-content">
|
|
301
392
|
{loadingMenus ? (
|
|
302
393
|
<Skeleton active paragraph={{ rows: 6 }} />
|
|
303
394
|
) : modules.length === 0 ? (
|
|
304
|
-
<
|
|
395
|
+
<div className="empty-state">
|
|
396
|
+
<Empty description="Select a role to view menus" />
|
|
397
|
+
</div>
|
|
305
398
|
) : (
|
|
306
399
|
<MenuTree menus={modules} selectedMenus={selectedMenus} toggleMenu={toggleMenu} showCheckbox={false} />
|
|
307
400
|
)}
|
|
308
401
|
</div>
|
|
309
402
|
|
|
310
403
|
<div className="footer-actions">
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
404
|
+
{rolesChanged && (
|
|
405
|
+
<Button type="primary" onClick={handleSaveUserRole} loading={saving}>
|
|
406
|
+
Save Changes
|
|
407
|
+
</Button>
|
|
408
|
+
)}
|
|
314
409
|
</div>
|
|
315
410
|
</Card>
|
|
316
411
|
</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
|
+
};
|