@flusys/ng-iam 1.1.0-beta → 2.0.0-rc.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/README.md +175 -24
- package/fesm2022/flusys-ng-iam-action-form-page.component-CVN8sV-c.mjs +389 -0
- package/fesm2022/flusys-ng-iam-action-form-page.component-CVN8sV-c.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-action-list-page.component-CQ6RazN0.mjs +262 -0
- package/fesm2022/flusys-ng-iam-action-list-page.component-CQ6RazN0.mjs.map +1 -0
- package/fesm2022/{flusys-ng-iam-flusys-ng-iam-BjdM-Vgz.mjs → flusys-ng-iam-flusys-ng-iam-DrGHlTiz.mjs} +1002 -1571
- package/fesm2022/flusys-ng-iam-flusys-ng-iam-DrGHlTiz.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-iam-container.component-BToYxEej.mjs +92 -0
- package/fesm2022/flusys-ng-iam-iam-container.component-BToYxEej.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-permission-page.component-BS7xXmsn.mjs +137 -0
- package/fesm2022/flusys-ng-iam-permission-page.component-BS7xXmsn.mjs.map +1 -0
- package/fesm2022/{flusys-ng-iam-role-form-page.component-Ctigzpon.mjs → flusys-ng-iam-role-form-page.component-BjPwXkip.mjs} +106 -148
- package/fesm2022/flusys-ng-iam-role-form-page.component-BjPwXkip.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-role-list-page.component-Cz-jk-R_.mjs +299 -0
- package/fesm2022/flusys-ng-iam-role-list-page.component-Cz-jk-R_.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam.mjs +1 -1
- package/package.json +4 -4
- package/types/flusys-ng-iam.d.ts +75 -454
- package/fesm2022/flusys-ng-iam-action-form-page.component-DBJzC5GS.mjs +0 -467
- package/fesm2022/flusys-ng-iam-action-form-page.component-DBJzC5GS.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-action-list-page.component-Dfts0JCt.mjs +0 -281
- package/fesm2022/flusys-ng-iam-action-list-page.component-Dfts0JCt.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-flusys-ng-iam-BjdM-Vgz.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-iam-container.component-Chl5MDkV.mjs +0 -97
- package/fesm2022/flusys-ng-iam-iam-container.component-Chl5MDkV.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-permission-page.component-cDrwUAQ_.mjs +0 -143
- package/fesm2022/flusys-ng-iam-permission-page.component-cDrwUAQ_.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-role-form-page.component-Ctigzpon.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-role-list-page.component-BF-Z_TQK.mjs +0 -266
- package/fesm2022/flusys-ng-iam-role-list-page.component-BF-Z_TQK.mjs.map +0 -1
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { HttpClient } from '@angular/common/http';
|
|
1
2
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, Injectable, signal, input, output,
|
|
3
|
-
import {
|
|
4
|
-
import { ApiResourceService, PermissionValidatorService, AngularModule, PrimeModule, COMPANY_API_PROVIDER, USER_PERMISSION_PROVIDER, UserSelectComponent, PROFILE_PERMISSION_PROVIDER } from '@flusys/ng-shared';
|
|
3
|
+
import { inject, Injectable, signal, input, output, effect, ChangeDetectionStrategy, Component, DestroyRef, computed } from '@angular/core';
|
|
4
|
+
import { ApiResourceService, PermissionValidatorService, AngularModule, PrimeModule, ROLE_ACTION_PERMISSIONS, HasPermissionDirective, COMPANY_ACTION_PERMISSIONS, COMPANY_API_PROVIDER, USER_ROLE_PERMISSIONS, USER_PERMISSION_PROVIDER, UserSelectComponent, USER_ACTION_PERMISSIONS, PROFILE_PERMISSION_PROVIDER, permissionGuard, ACTION_PERMISSIONS, ROLE_PERMISSIONS, anyPermissionGuard } from '@flusys/ng-shared';
|
|
5
5
|
import { APP_CONFIG, BaseApiService, isCompanyFeatureEnabled } from '@flusys/ng-core';
|
|
6
|
-
import * as i2$1 from 'primeng/api';
|
|
7
6
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
|
8
7
|
import { of, firstValueFrom, map as map$1 } from 'rxjs';
|
|
9
8
|
import { tap, catchError, map } from 'rxjs/operators';
|
|
@@ -12,20 +11,15 @@ import { CommonModule } from '@angular/common';
|
|
|
12
11
|
import * as i1$1 from '@angular/forms';
|
|
13
12
|
import { FormsModule } from '@angular/forms';
|
|
14
13
|
import * as i2 from 'primeng/button';
|
|
15
|
-
import
|
|
16
|
-
import * as
|
|
17
|
-
import * as i4 from 'primeng/
|
|
18
|
-
import * as i5 from 'primeng/
|
|
19
|
-
import * as
|
|
20
|
-
import * as i8 from 'primeng/treetable';
|
|
14
|
+
import * as i6 from 'primeng/tooltip';
|
|
15
|
+
import * as i3 from 'primeng/checkbox';
|
|
16
|
+
import * as i4 from 'primeng/select';
|
|
17
|
+
import * as i5 from 'primeng/tag';
|
|
18
|
+
import * as i7 from 'primeng/treetable';
|
|
21
19
|
import { LAYOUT_AUTH_STATE } from '@flusys/ng-layout';
|
|
22
20
|
import * as i5$1 from 'primeng/table';
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Action Type - determines how action is used
|
|
27
|
-
* CRITICAL: Must match backend ActionType enum exactly
|
|
28
|
-
*/
|
|
22
|
+
/** Must match backend ActionType enum */
|
|
29
23
|
var ActionType;
|
|
30
24
|
(function (ActionType) {
|
|
31
25
|
ActionType["BACKEND"] = "backend";
|
|
@@ -35,81 +29,40 @@ var ActionType;
|
|
|
35
29
|
|
|
36
30
|
// Company interfaces
|
|
37
31
|
|
|
38
|
-
/**
|
|
39
|
-
* Pagination Constants
|
|
40
|
-
*
|
|
41
|
-
* Standard pagination limits for IAM components.
|
|
42
|
-
* Prevents excessive data loading and potential DoS.
|
|
43
|
-
*/
|
|
44
|
-
/**
|
|
45
|
-
* Maximum items to fetch for dropdown lists
|
|
46
|
-
* Used for: companies, roles, users, branches
|
|
47
|
-
*
|
|
48
|
-
* Security: Prevents memory exhaustion from loading excessive records
|
|
49
|
-
*/
|
|
32
|
+
/** Maximum items for dropdown lists (companies, roles, users, branches) */
|
|
50
33
|
const MAX_DROPDOWN_ITEMS = 100;
|
|
51
34
|
|
|
52
|
-
/**
|
|
53
|
-
* Role API Service
|
|
54
|
-
* Handles role CRUD operations
|
|
55
|
-
* Endpoint: POST /iam/roles/*
|
|
56
|
-
* Conditional: Only available in RBAC/FULL mode
|
|
57
|
-
*/
|
|
58
35
|
class RoleApiService extends ApiResourceService {
|
|
59
36
|
constructor() {
|
|
60
|
-
|
|
61
|
-
super('iam/roles', http);
|
|
37
|
+
super('iam/roles', inject(HttpClient));
|
|
62
38
|
}
|
|
63
39
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RoleApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
64
40
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RoleApiService, providedIn: 'root' });
|
|
65
41
|
}
|
|
66
42
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RoleApiService, decorators: [{
|
|
67
43
|
type: Injectable,
|
|
68
|
-
args: [{
|
|
69
|
-
providedIn: 'root',
|
|
70
|
-
}]
|
|
44
|
+
args: [{ providedIn: 'root' }]
|
|
71
45
|
}], ctorParameters: () => [] });
|
|
72
46
|
|
|
73
|
-
/**
|
|
74
|
-
* Action API Service
|
|
75
|
-
* Handles action CRUD operations
|
|
76
|
-
* Endpoint: POST /iam/actions/*
|
|
77
|
-
*/
|
|
78
47
|
class ActionApiService extends ApiResourceService {
|
|
79
48
|
appConfig = inject(APP_CONFIG);
|
|
80
49
|
constructor() {
|
|
81
50
|
const http = inject(HttpClient);
|
|
82
51
|
super('iam/actions', http);
|
|
83
52
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Get actions for permission assignment
|
|
86
|
-
* GET /iam/actions/tree-for-permission
|
|
87
|
-
* Returns actions filtered by company whitelist if enabled
|
|
88
|
-
*/
|
|
53
|
+
/** Get actions filtered by company whitelist for permission assignment */
|
|
89
54
|
getActionsForPermission() {
|
|
90
|
-
return this.http.
|
|
55
|
+
return this.http.post(`${this.appConfig.apiBaseUrl}/iam/actions/tree-for-permission`, {});
|
|
91
56
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Get actions in hierarchical tree structure
|
|
94
|
-
* POST /iam/actions/tree
|
|
95
|
-
* Returns all actions organized in parent-child tree
|
|
96
|
-
*
|
|
97
|
-
* @param search - Optional search term (name or code)
|
|
98
|
-
* @param isActive - Optional filter by active status
|
|
99
|
-
* @param withDeleted - Include deleted actions (default: false)
|
|
100
|
-
* @returns Observable of action tree response
|
|
101
|
-
*/
|
|
57
|
+
/** Get actions in hierarchical tree structure */
|
|
102
58
|
getTree(search, isActive, withDeleted) {
|
|
103
59
|
const body = {};
|
|
104
|
-
if (search?.trim())
|
|
60
|
+
if (search?.trim())
|
|
105
61
|
body.search = search.trim();
|
|
106
|
-
|
|
107
|
-
if (isActive !== undefined) {
|
|
62
|
+
if (isActive !== undefined)
|
|
108
63
|
body.isActive = isActive;
|
|
109
|
-
|
|
110
|
-
if (withDeleted !== undefined) {
|
|
64
|
+
if (withDeleted !== undefined)
|
|
111
65
|
body.withDeleted = withDeleted;
|
|
112
|
-
}
|
|
113
66
|
return this.http.post(`${this.appConfig.apiBaseUrl}/iam/actions/tree`, body);
|
|
114
67
|
}
|
|
115
68
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ActionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
@@ -117,48 +70,24 @@ class ActionApiService extends ApiResourceService {
|
|
|
117
70
|
}
|
|
118
71
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ActionApiService, decorators: [{
|
|
119
72
|
type: Injectable,
|
|
120
|
-
args: [{
|
|
121
|
-
providedIn: 'root',
|
|
122
|
-
}]
|
|
73
|
+
args: [{ providedIn: 'root' }]
|
|
123
74
|
}], ctorParameters: () => [] });
|
|
124
75
|
|
|
125
|
-
/**
|
|
126
|
-
* Extract all required action IDs from a permission logic tree
|
|
127
|
-
* This is useful for prerequisite validation
|
|
128
|
-
*
|
|
129
|
-
* @param logic - Permission logic to analyze
|
|
130
|
-
* @returns Set of action IDs that are required
|
|
131
|
-
*/
|
|
132
76
|
function extractRequiredActionIds(logic) {
|
|
133
77
|
const actionIds = new Set();
|
|
134
|
-
if (
|
|
135
|
-
|
|
78
|
+
if (logic) {
|
|
79
|
+
collectActionIds(logic, actionIds);
|
|
136
80
|
}
|
|
137
|
-
collectActionIds(logic, actionIds);
|
|
138
81
|
return actionIds;
|
|
139
82
|
}
|
|
140
|
-
/**
|
|
141
|
-
* Recursively collect action IDs from logic tree
|
|
142
|
-
*/
|
|
143
83
|
function collectActionIds(node, actionIds) {
|
|
144
84
|
if (node.type === 'action' && node.actionId) {
|
|
145
85
|
actionIds.add(node.actionId);
|
|
146
86
|
}
|
|
147
87
|
else if (node.type === 'group' && node.children) {
|
|
148
|
-
|
|
149
|
-
collectActionIds(child, actionIds);
|
|
150
|
-
}
|
|
88
|
+
node.children.forEach((child) => collectActionIds(child, actionIds));
|
|
151
89
|
}
|
|
152
90
|
}
|
|
153
|
-
/**
|
|
154
|
-
* Validate if an action's prerequisites are satisfied
|
|
155
|
-
* Respects AND/OR logic operators in permission tree
|
|
156
|
-
*
|
|
157
|
-
* @param action - Action to validate
|
|
158
|
-
* @param selectedActionIds - Set of currently selected action IDs
|
|
159
|
-
* @param allActions - All available actions (to look up missing ones)
|
|
160
|
-
* @returns Validation result with missing actions
|
|
161
|
-
*/
|
|
162
91
|
function validateActionPrerequisites(action, selectedActionIds, allActions) {
|
|
163
92
|
if (!action.permissionLogic) {
|
|
164
93
|
return { valid: true, missingActions: [] };
|
|
@@ -170,20 +99,12 @@ function validateActionPrerequisites(action, selectedActionIds, allActions) {
|
|
|
170
99
|
const missingActions = allActions.filter((a) => a.id && result.missingActionIds.has(a.id));
|
|
171
100
|
return { valid: false, missingActions };
|
|
172
101
|
}
|
|
173
|
-
/**
|
|
174
|
-
* Recursively evaluate logic node respecting AND/OR operators
|
|
175
|
-
* Returns validation result with missing action IDs (for prerequisite dialogs)
|
|
176
|
-
* Note: This differs from evaluateLogicNode in ng-shared which returns boolean only
|
|
177
|
-
*/
|
|
178
102
|
function evaluateLogicNodeWithMissing(node, selectedActionIds) {
|
|
179
103
|
if (node.type === 'action' && node.actionId) {
|
|
180
104
|
const valid = selectedActionIds.has(node.actionId);
|
|
181
|
-
return {
|
|
182
|
-
valid,
|
|
183
|
-
missingActionIds: valid ? new Set() : new Set([node.actionId]),
|
|
184
|
-
};
|
|
105
|
+
return { valid, missingActionIds: valid ? new Set() : new Set([node.actionId]) };
|
|
185
106
|
}
|
|
186
|
-
if (node.type === 'group' && node.children
|
|
107
|
+
if (node.type === 'group' && node.children?.length) {
|
|
187
108
|
const operator = node.operator || 'AND';
|
|
188
109
|
if (operator === 'AND') {
|
|
189
110
|
const missingIds = new Set();
|
|
@@ -197,109 +118,28 @@ function evaluateLogicNodeWithMissing(node, selectedActionIds) {
|
|
|
197
118
|
}
|
|
198
119
|
return { valid: allValid, missingActionIds: missingIds };
|
|
199
120
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
121
|
+
// OR operator
|
|
122
|
+
for (const child of node.children) {
|
|
123
|
+
const childResult = evaluateLogicNodeWithMissing(child, selectedActionIds);
|
|
124
|
+
if (childResult.valid) {
|
|
125
|
+
return { valid: true, missingActionIds: new Set() };
|
|
206
126
|
}
|
|
207
|
-
const firstChildResult = evaluateLogicNodeWithMissing(node.children[0], selectedActionIds);
|
|
208
|
-
return {
|
|
209
|
-
valid: false,
|
|
210
|
-
missingActionIds: firstChildResult.missingActionIds,
|
|
211
|
-
};
|
|
212
127
|
}
|
|
128
|
+
const firstChildResult = evaluateLogicNodeWithMissing(node.children[0], selectedActionIds);
|
|
129
|
+
return { valid: false, missingActionIds: firstChildResult.missingActionIds };
|
|
213
130
|
}
|
|
214
131
|
return { valid: true, missingActionIds: new Set() };
|
|
215
132
|
}
|
|
216
|
-
/**
|
|
217
|
-
* Get human-readable prerequisite description
|
|
218
|
-
*
|
|
219
|
-
* @param logic - Permission logic to describe
|
|
220
|
-
* @param allActions - All available actions for name lookup
|
|
221
|
-
* @returns Human-readable string describing prerequisites
|
|
222
|
-
*/
|
|
223
|
-
function describePrerequisites(logic, allActions) {
|
|
224
|
-
if (!logic) {
|
|
225
|
-
return 'None';
|
|
226
|
-
}
|
|
227
|
-
const requiredIds = extractRequiredActionIds(logic);
|
|
228
|
-
if (requiredIds.size === 0) {
|
|
229
|
-
return 'None';
|
|
230
|
-
}
|
|
231
|
-
const actionMap = new Map(allActions.map((a) => [a.id, a]));
|
|
232
|
-
const names = Array.from(requiredIds)
|
|
233
|
-
.map((id) => actionMap.get(id)?.name || id)
|
|
234
|
-
.filter(Boolean);
|
|
235
|
-
if (names.length === 0) {
|
|
236
|
-
return 'Unknown prerequisites';
|
|
237
|
-
}
|
|
238
|
-
if (names.length === 1) {
|
|
239
|
-
return names[0];
|
|
240
|
-
}
|
|
241
|
-
return names.join(', ');
|
|
242
|
-
}
|
|
243
133
|
|
|
244
|
-
/**
|
|
245
|
-
* Action Permission Logic Service
|
|
246
|
-
*
|
|
247
|
-
* Shared service for handling smart dependency management across all action selectors:
|
|
248
|
-
* - Company-Action Selector
|
|
249
|
-
* - Role-Action Selector
|
|
250
|
-
* - User-Action Selector
|
|
251
|
-
*
|
|
252
|
-
* **Core Features:**
|
|
253
|
-
* - Smart auto-selection (AND/OR optimization)
|
|
254
|
-
* - Dependency detection and management
|
|
255
|
-
* - Alternative suggestion for OR logic
|
|
256
|
-
* - Visual formatting of permission logic trees
|
|
257
|
-
* - Prerequisite validation
|
|
258
|
-
*
|
|
259
|
-
* @example
|
|
260
|
-
* constructor() {
|
|
261
|
-
* this.permissionLogic = inject(ActionPermissionLogicService);
|
|
262
|
-
* }
|
|
263
|
-
*
|
|
264
|
-
* onActionToggle(action: IAction, newValue: boolean) {
|
|
265
|
-
* if (!newValue) {
|
|
266
|
-
* this.permissionLogic.handleUncheck(
|
|
267
|
-
* action,
|
|
268
|
-
* this.selectionMap(),
|
|
269
|
-
* this.actions(),
|
|
270
|
-
* (newMap) => this.selectionMap.set(newMap)
|
|
271
|
-
* );
|
|
272
|
-
* } else {
|
|
273
|
-
* this.permissionLogic.handleCheck(
|
|
274
|
-
* action,
|
|
275
|
-
* this.selectionMap(),
|
|
276
|
-
* this.actions(),
|
|
277
|
-
* (newMap) => this.selectionMap.set(newMap),
|
|
278
|
-
* (previousState) => this.selectionMap.set(previousState)
|
|
279
|
-
* );
|
|
280
|
-
* }
|
|
281
|
-
* }
|
|
282
|
-
*/
|
|
134
|
+
/** Shared service for smart dependency management across action selectors */
|
|
283
135
|
class ActionPermissionLogicService {
|
|
284
136
|
confirmationService = inject(ConfirmationService);
|
|
285
137
|
messageService = inject(MessageService);
|
|
286
|
-
/**
|
|
287
|
-
* Handle checking an action with prerequisite validation
|
|
288
|
-
*
|
|
289
|
-
* Uses recursive deep scan to find ALL missing prerequisites at all levels,
|
|
290
|
-
* not just direct dependencies. This ensures cascading dependencies are
|
|
291
|
-
* resolved in a single step.
|
|
292
|
-
*
|
|
293
|
-
* @param action - Action being checked
|
|
294
|
-
* @param currentSelection - Current selection map
|
|
295
|
-
* @param allActions - All available actions
|
|
296
|
-
* @param onUpdate - Callback to update selection
|
|
297
|
-
* @param onCancel - Callback when user cancels
|
|
298
|
-
*/
|
|
138
|
+
/** Handle checking an action with prerequisite validation (recursive deep scan) */
|
|
299
139
|
handleCheck(action, currentSelection, allActions, onUpdate, onCancel) {
|
|
300
140
|
// Validate prerequisites with RECURSIVE DEEP SCAN
|
|
301
141
|
if (action.permissionLogic) {
|
|
302
|
-
const selectedActionIds =
|
|
142
|
+
const selectedActionIds = this.getSelectedIds(currentSelection);
|
|
303
143
|
const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
|
|
304
144
|
if (!validationResult.valid) {
|
|
305
145
|
// Store previous state
|
|
@@ -320,14 +160,7 @@ class ActionPermissionLogicService {
|
|
|
320
160
|
selMap[action.id] = true;
|
|
321
161
|
onUpdate(selMap);
|
|
322
162
|
}
|
|
323
|
-
/**
|
|
324
|
-
* Handle unchecking an action with dependency detection
|
|
325
|
-
*
|
|
326
|
-
* @param action - Action being unchecked
|
|
327
|
-
* @param currentSelection - Current selection map
|
|
328
|
-
* @param allActions - All available actions
|
|
329
|
-
* @param onUpdate - Callback to update selection
|
|
330
|
-
*/
|
|
163
|
+
/** Handle unchecking an action with dependency detection */
|
|
331
164
|
handleUncheck(action, currentSelection, allActions, onUpdate) {
|
|
332
165
|
const affectedActions = this.findActionsDependingOn(action.id, currentSelection, allActions);
|
|
333
166
|
if (affectedActions.length > 0) {
|
|
@@ -339,46 +172,28 @@ class ActionPermissionLogicService {
|
|
|
339
172
|
selMap[action.id] = false;
|
|
340
173
|
onUpdate(selMap);
|
|
341
174
|
}
|
|
342
|
-
/**
|
|
343
|
-
* Check if an action has unmet prerequisites
|
|
344
|
-
*
|
|
345
|
-
* @param action - Action to check
|
|
346
|
-
* @param currentSelection - Current selection map
|
|
347
|
-
* @param allActions - All available actions
|
|
348
|
-
* @returns True if action has unmet prerequisites
|
|
349
|
-
*/
|
|
175
|
+
/** Check if an action has unmet prerequisites */
|
|
350
176
|
hasUnmetPrerequisites(action, currentSelection, allActions) {
|
|
351
177
|
const isSelected = currentSelection[action.id];
|
|
352
178
|
if (!isSelected || !action.permissionLogic) {
|
|
353
179
|
return false;
|
|
354
180
|
}
|
|
355
|
-
const selectedActionIds =
|
|
181
|
+
const selectedActionIds = this.getSelectedIds(currentSelection);
|
|
182
|
+
selectedActionIds.delete(action.id);
|
|
356
183
|
const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
|
|
357
184
|
return !validationResult.valid;
|
|
358
185
|
}
|
|
359
|
-
/**
|
|
360
|
-
* Get all selected actions that have unmet prerequisites
|
|
361
|
-
*
|
|
362
|
-
* @param currentSelection - Current selection map
|
|
363
|
-
* @param allActions - All available actions
|
|
364
|
-
* @returns Array of actions with unmet prerequisites
|
|
365
|
-
*/
|
|
186
|
+
/** Get all selected actions that have unmet prerequisites */
|
|
366
187
|
getActionsWithUnmetPrerequisites(currentSelection, allActions) {
|
|
367
188
|
const selectedActions = allActions.filter((a) => currentSelection[a.id]);
|
|
368
189
|
return selectedActions.filter((action) => this.hasUnmetPrerequisites(action, currentSelection, allActions));
|
|
369
190
|
}
|
|
370
|
-
/**
|
|
371
|
-
* Show validation error dialog with auto-fix options
|
|
372
|
-
*
|
|
373
|
-
* @param invalidActions - Actions with unmet prerequisites
|
|
374
|
-
* @param currentSelection - Current selection map
|
|
375
|
-
* @param allActions - All available actions
|
|
376
|
-
* @param onUpdate - Callback to update selection
|
|
377
|
-
*/
|
|
191
|
+
/** Show validation error dialog with auto-fix options */
|
|
378
192
|
showValidationErrorDialog(invalidActions, currentSelection, allActions, onUpdate) {
|
|
379
193
|
const errorList = invalidActions
|
|
380
194
|
.map((action) => {
|
|
381
|
-
const selectedActionIds =
|
|
195
|
+
const selectedActionIds = this.getSelectedIds(currentSelection);
|
|
196
|
+
selectedActionIds.delete(action.id);
|
|
382
197
|
const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
|
|
383
198
|
const sanitizedActionName = this.sanitizeHtml(action.name ?? 'Unknown');
|
|
384
199
|
const missingNames = validationResult.missingActions
|
|
@@ -420,19 +235,12 @@ class ActionPermissionLogicService {
|
|
|
420
235
|
},
|
|
421
236
|
});
|
|
422
237
|
}
|
|
423
|
-
/**
|
|
424
|
-
* Get prerequisite description for tooltip display
|
|
425
|
-
*
|
|
426
|
-
* @param action - Action to get prerequisites for
|
|
427
|
-
* @param currentSelection - Current selection map
|
|
428
|
-
* @param allActions - All available actions
|
|
429
|
-
* @returns Plain text prerequisite description
|
|
430
|
-
*/
|
|
238
|
+
/** Get prerequisite description for tooltip display */
|
|
431
239
|
getPrerequisiteTooltip(action, currentSelection, allActions) {
|
|
432
240
|
if (!action.permissionLogic) {
|
|
433
241
|
return '';
|
|
434
242
|
}
|
|
435
|
-
const selectedActionIds =
|
|
243
|
+
const selectedActionIds = this.getSelectedIds(currentSelection);
|
|
436
244
|
const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
|
|
437
245
|
if (validationResult.valid) {
|
|
438
246
|
return '[OK] Prerequisites Satisfied\n\nAll required actions are already selected.\nYou can safely add this action.';
|
|
@@ -444,23 +252,16 @@ class ActionPermissionLogicService {
|
|
|
444
252
|
const hint = '\n\n💡 Click to auto-select required actions';
|
|
445
253
|
return `${header}\n\n${logicTree}${hint}`;
|
|
446
254
|
}
|
|
447
|
-
/**
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
* @param logic - Permission logic tree
|
|
451
|
-
* @param missingActions - Actions that are missing
|
|
452
|
-
* @param allActions - All available actions
|
|
453
|
-
* @param currentSelection - Current selection map for accurate status
|
|
454
|
-
* @returns HTML formatted logic tree
|
|
455
|
-
*/
|
|
456
|
-
buildLogicMessage(logic, missingActions, allActions, currentSelection) {
|
|
457
|
-
const selectedIds = currentSelection
|
|
458
|
-
? new Set(Object.keys(currentSelection).filter((id) => currentSelection[id]))
|
|
459
|
-
: new Set();
|
|
255
|
+
/** Build dynamic logic tree message with AND/OR operators and nesting */
|
|
256
|
+
buildLogicMessage(logic, _missingActions, allActions, currentSelection) {
|
|
257
|
+
const selectedIds = currentSelection ? this.getSelectedIds(currentSelection) : new Set();
|
|
460
258
|
const actionMap = new Map(allActions.map((a) => [a.id, a]));
|
|
461
259
|
return this.formatLogicNode(logic, selectedIds, actionMap, 0);
|
|
462
260
|
}
|
|
463
|
-
|
|
261
|
+
/** Extract selected action IDs from selection map */
|
|
262
|
+
getSelectedIds(currentSelection) {
|
|
263
|
+
return new Set(Object.keys(currentSelection).filter((id) => currentSelection[id]));
|
|
264
|
+
}
|
|
464
265
|
sanitizeHtml(text) {
|
|
465
266
|
return text
|
|
466
267
|
.replace(/&/g, '&')
|
|
@@ -469,24 +270,7 @@ class ActionPermissionLogicService {
|
|
|
469
270
|
.replace(/"/g, '"')
|
|
470
271
|
.replace(/'/g, ''');
|
|
471
272
|
}
|
|
472
|
-
/**
|
|
473
|
-
* Recursively collect ALL missing prerequisites at all dependency levels
|
|
474
|
-
*
|
|
475
|
-
* This prevents cascading prerequisite dialogs by finding the complete
|
|
476
|
-
* dependency chain upfront.
|
|
477
|
-
*
|
|
478
|
-
* **Example:**
|
|
479
|
-
* - Action 4 requires Action 3
|
|
480
|
-
* - Action 3 requires Action 2
|
|
481
|
-
* - Action 2 requires Action 1
|
|
482
|
-
*
|
|
483
|
-
* Instead of showing 3 separate dialogs, this returns: [Action 3, Action 2, Action 1]
|
|
484
|
-
*
|
|
485
|
-
* @param action - Starting action to check
|
|
486
|
-
* @param currentSelection - Current selection map
|
|
487
|
-
* @param allActions - All available actions
|
|
488
|
-
* @returns Complete set of missing prerequisites across all levels
|
|
489
|
-
*/
|
|
273
|
+
/** Recursively collect ALL missing prerequisites at all dependency levels */
|
|
490
274
|
getAllMissingPrerequisitesRecursive(action, currentSelection, allActions) {
|
|
491
275
|
if (!action.id)
|
|
492
276
|
return [];
|
|
@@ -506,7 +290,7 @@ class ActionPermissionLogicService {
|
|
|
506
290
|
}
|
|
507
291
|
visited.add(targetAction.id);
|
|
508
292
|
stack.add(targetAction.id);
|
|
509
|
-
const selectedActionIds =
|
|
293
|
+
const selectedActionIds = this.getSelectedIds(currentSelection);
|
|
510
294
|
const result = validateActionPrerequisites(targetAction, selectedActionIds, allActions);
|
|
511
295
|
if (!result.valid) {
|
|
512
296
|
result.missingActions.forEach((missingAction) => {
|
|
@@ -567,7 +351,7 @@ class ActionPermissionLogicService {
|
|
|
567
351
|
},
|
|
568
352
|
});
|
|
569
353
|
}
|
|
570
|
-
showDependencyDialog(action, affectedActions, currentSelection,
|
|
354
|
+
showDependencyDialog(action, affectedActions, currentSelection, _allActions, onUpdate) {
|
|
571
355
|
if (!action.id)
|
|
572
356
|
return;
|
|
573
357
|
const alternatives = this.findAlternatives(action.id, affectedActions, currentSelection);
|
|
@@ -690,7 +474,7 @@ class ActionPermissionLogicService {
|
|
|
690
474
|
calculateSmartSelection(logic, missingActions, currentSelection, allActions) {
|
|
691
475
|
const missingIds = new Set(missingActions.map((a) => a.id));
|
|
692
476
|
const actionMap = new Map(allActions.map((a) => [a.id, a]));
|
|
693
|
-
const selectedIds =
|
|
477
|
+
const selectedIds = this.getSelectedIds(currentSelection);
|
|
694
478
|
const requiredIds = this.findRequiredActionIds(logic, missingIds, selectedIds);
|
|
695
479
|
return Array.from(requiredIds)
|
|
696
480
|
.map((id) => actionMap.get(id))
|
|
@@ -763,12 +547,10 @@ class ActionPermissionLogicService {
|
|
|
763
547
|
}
|
|
764
548
|
return `${indent}<em style="color: #9ca3af;">Invalid logic node</em>`;
|
|
765
549
|
}
|
|
766
|
-
/**
|
|
767
|
-
* Build clean text-based logic tree for tooltips
|
|
768
|
-
*/
|
|
550
|
+
/** Build clean text-based logic tree for tooltips */
|
|
769
551
|
buildTooltipLogicTree(node, currentSelection, allActions, depth) {
|
|
770
552
|
const indent = ' '.repeat(depth);
|
|
771
|
-
const selectedIds =
|
|
553
|
+
const selectedIds = this.getSelectedIds(currentSelection);
|
|
772
554
|
const actionMap = new Map(allActions.map((a) => [a.id, a]));
|
|
773
555
|
if (node.type === 'action' && node.actionId) {
|
|
774
556
|
const action = actionMap.get(node.actionId);
|
|
@@ -803,110 +585,43 @@ class ActionPermissionLogicService {
|
|
|
803
585
|
}
|
|
804
586
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ActionPermissionLogicService, decorators: [{
|
|
805
587
|
type: Injectable,
|
|
806
|
-
args: [{
|
|
807
|
-
providedIn: 'root',
|
|
808
|
-
}]
|
|
588
|
+
args: [{ providedIn: 'root' }]
|
|
809
589
|
}] });
|
|
810
590
|
|
|
811
|
-
/**
|
|
812
|
-
* Consolidated Permission API Service
|
|
813
|
-
* Handles all permission-related operations in one service
|
|
814
|
-
* Supports:
|
|
815
|
-
* - User → Action (direct permissions)
|
|
816
|
-
* - User → Role (role assignments)
|
|
817
|
-
* - Role → Action (role permissions)
|
|
818
|
-
* - Company → Action (company whitelisting)
|
|
819
|
-
*
|
|
820
|
-
* Endpoint: POST /permissions/*
|
|
821
|
-
*/
|
|
822
591
|
class PermissionApiService extends BaseApiService {
|
|
823
592
|
constructor() {
|
|
824
593
|
super('iam');
|
|
825
594
|
}
|
|
826
|
-
// =============================================================================
|
|
827
|
-
// User → Action (Direct Permissions)
|
|
828
|
-
// =============================================================================
|
|
829
|
-
/**
|
|
830
|
-
* Assign/remove actions directly to/from user
|
|
831
|
-
* POST /permissions/user-actions/assign
|
|
832
|
-
*/
|
|
833
595
|
assignUserActions(data) {
|
|
834
596
|
return this.http.post(this.getUrl('permissions/user-actions/assign'), data);
|
|
835
597
|
}
|
|
836
|
-
/**
|
|
837
|
-
* Get user's direct action permissions
|
|
838
|
-
* GET /permissions/user-actions/:userId
|
|
839
|
-
*/
|
|
840
598
|
getUserActions(userId, query) {
|
|
841
|
-
|
|
842
|
-
if (query?.branchId) {
|
|
843
|
-
params = params.set('branchId', query.branchId);
|
|
844
|
-
}
|
|
845
|
-
return this.http.get(this.getUrl(`permissions/user-actions/${userId}`), { params });
|
|
599
|
+
return this.http.post(this.getUrl('permissions/get-user-actions'), { userId, branchId: query?.branchId });
|
|
846
600
|
}
|
|
847
|
-
// =============================================================================
|
|
848
|
-
// User → Role (Role Assignments)
|
|
849
|
-
// =============================================================================
|
|
850
|
-
/**
|
|
851
|
-
* Assign/remove roles to/from user
|
|
852
|
-
* POST /permissions/user-roles/assign
|
|
853
|
-
*/
|
|
854
601
|
assignUserRoles(data) {
|
|
855
602
|
return this.http.post(this.getUrl('permissions/user-roles/assign'), data);
|
|
856
603
|
}
|
|
857
|
-
/**
|
|
858
|
-
* Get user's role assignments
|
|
859
|
-
* GET /permissions/user-roles/:userId
|
|
860
|
-
*/
|
|
861
604
|
getUserRoles(userId, query) {
|
|
862
|
-
|
|
863
|
-
if (query?.branchId) {
|
|
864
|
-
params = params.set('branchId', query.branchId);
|
|
865
|
-
}
|
|
866
|
-
return this.http.get(this.getUrl(`permissions/user-roles/${userId}`), { params });
|
|
605
|
+
return this.http.post(this.getUrl('permissions/get-user-roles'), { userId, branchId: query?.branchId });
|
|
867
606
|
}
|
|
868
|
-
// =============================================================================
|
|
869
|
-
// Role → Action (Role Permissions)
|
|
870
|
-
// =============================================================================
|
|
871
|
-
/**
|
|
872
|
-
* Assign/remove actions to/from role
|
|
873
|
-
* POST /permissions/role-actions/assign
|
|
874
|
-
*/
|
|
875
607
|
assignRoleActions(data) {
|
|
876
608
|
return this.http.post(this.getUrl('permissions/role-actions/assign'), data);
|
|
877
609
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
* GET /permissions/role-actions/:roleId
|
|
881
|
-
*/
|
|
882
|
-
getRoleActions(roleId, query) {
|
|
883
|
-
return this.http.get(this.getUrl(`permissions/role-actions/${roleId}`));
|
|
610
|
+
getRoleActions(roleId) {
|
|
611
|
+
return this.http.post(this.getUrl('permissions/get-role-actions'), { roleId });
|
|
884
612
|
}
|
|
885
|
-
// =============================================================================
|
|
886
|
-
// Company → Action (Company Whitelisting)
|
|
887
|
-
// =============================================================================
|
|
888
|
-
/**
|
|
889
|
-
* Assign/remove actions to/from company (whitelisting)
|
|
890
|
-
* POST /permissions/company-actions/assign
|
|
891
|
-
*/
|
|
892
613
|
assignCompanyActions(data) {
|
|
893
614
|
return this.http.post(this.getUrl('permissions/company-actions/assign'), data);
|
|
894
615
|
}
|
|
895
|
-
/**
|
|
896
|
-
* Get company's whitelisted actions
|
|
897
|
-
* GET /permissions/company-actions/:companyId
|
|
898
|
-
*/
|
|
899
616
|
getCompanyActions(companyId) {
|
|
900
|
-
return this.http.
|
|
617
|
+
return this.http.post(this.getUrl('permissions/get-company-actions'), { companyId });
|
|
901
618
|
}
|
|
902
619
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
903
620
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionApiService, providedIn: 'root' });
|
|
904
621
|
}
|
|
905
622
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionApiService, decorators: [{
|
|
906
623
|
type: Injectable,
|
|
907
|
-
args: [{
|
|
908
|
-
providedIn: 'root',
|
|
909
|
-
}]
|
|
624
|
+
args: [{ providedIn: 'root' }]
|
|
910
625
|
}], ctorParameters: () => [] });
|
|
911
626
|
|
|
912
627
|
/**
|
|
@@ -941,47 +656,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
941
656
|
}]
|
|
942
657
|
}], ctorParameters: () => [] });
|
|
943
658
|
|
|
944
|
-
/**
|
|
945
|
-
* Permission State Service
|
|
946
|
-
* Manages user permissions state and provides permission checking methods
|
|
947
|
-
*
|
|
948
|
-
* Uses shared PermissionValidatorService for centralized permission checking.
|
|
949
|
-
*
|
|
950
|
-
* @example
|
|
951
|
-
* ```typescript
|
|
952
|
-
* // In component
|
|
953
|
-
* readonly permissionState = inject(PermissionStateService);
|
|
954
|
-
*
|
|
955
|
-
* ngOnInit() {
|
|
956
|
-
* this.permissionState.loadPermissions();
|
|
957
|
-
* }
|
|
958
|
-
*
|
|
959
|
-
* // Check permission
|
|
960
|
-
* if (this.permissionState.hasAction('user.create')) {
|
|
961
|
-
* // Show create button
|
|
962
|
-
* }
|
|
963
|
-
* ```
|
|
964
|
-
*/
|
|
965
659
|
class PermissionStateService {
|
|
966
660
|
permissionApi = inject(MyPermissionsApiService);
|
|
967
661
|
permissionValidator = inject(PermissionValidatorService);
|
|
968
|
-
// Permission state
|
|
969
662
|
_permissions = signal(null, ...(ngDevMode ? [{ debugName: "_permissions" }] : []));
|
|
970
663
|
permissions = this._permissions.asReadonly();
|
|
971
|
-
// Loading state
|
|
972
664
|
_isLoading = signal(false, ...(ngDevMode ? [{ debugName: "_isLoading" }] : []));
|
|
973
665
|
isLoading = this._isLoading.asReadonly();
|
|
974
|
-
/**
|
|
975
|
-
* Load current user's permissions from API
|
|
976
|
-
* Call this on app initialization or after login
|
|
977
|
-
* Returns Observable for reactive composition
|
|
978
|
-
*/
|
|
979
666
|
loadPermissions(dto) {
|
|
980
667
|
this._isLoading.set(true);
|
|
981
668
|
return this.permissionApi.getMyPermissions(dto).pipe(tap((response) => {
|
|
982
669
|
this._permissions.set(response.data ?? null);
|
|
983
670
|
this._isLoading.set(false);
|
|
984
|
-
// Update shared permission validator
|
|
985
671
|
const actionCodes = response.data?.frontendActions.map((a) => a.code) ?? [];
|
|
986
672
|
this.permissionValidator.setPermissions(actionCodes);
|
|
987
673
|
}), catchError(() => {
|
|
@@ -991,14 +677,8 @@ class PermissionStateService {
|
|
|
991
677
|
return of(void 0);
|
|
992
678
|
}), map(() => void 0));
|
|
993
679
|
}
|
|
994
|
-
/**
|
|
995
|
-
* Check if permissions are loaded
|
|
996
|
-
*
|
|
997
|
-
* @returns true if permissions are loaded
|
|
998
|
-
*/
|
|
999
680
|
isLoaded() {
|
|
1000
|
-
return
|
|
1001
|
-
this.permissionValidator.isPermissionsLoaded());
|
|
681
|
+
return this._permissions() !== null && this.permissionValidator.isPermissionsLoaded();
|
|
1002
682
|
}
|
|
1003
683
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1004
684
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionStateService, providedIn: 'root' });
|
|
@@ -1022,120 +702,108 @@ function toLogicNode(node) {
|
|
|
1022
702
|
}
|
|
1023
703
|
function toBuilderNode(node) {
|
|
1024
704
|
if (node.type === 'action') {
|
|
1025
|
-
return
|
|
705
|
+
return createActionNode(node.actionId);
|
|
1026
706
|
}
|
|
1027
707
|
return {
|
|
1028
|
-
|
|
1029
|
-
type: 'group',
|
|
1030
|
-
operator: node.operator,
|
|
708
|
+
...createGroupNode(node.operator),
|
|
1031
709
|
children: node.children?.map(toBuilderNode) ?? [],
|
|
1032
710
|
};
|
|
1033
711
|
}
|
|
712
|
+
function createGroupNode(operator = 'AND') {
|
|
713
|
+
return { id: crypto.randomUUID(), type: 'group', operator, children: [] };
|
|
714
|
+
}
|
|
715
|
+
function createActionNode(actionId = '') {
|
|
716
|
+
return { id: crypto.randomUUID(), type: 'action', actionId };
|
|
717
|
+
}
|
|
1034
718
|
/** Visual builder for AND/OR permission logic trees */
|
|
1035
719
|
class LogicBuilderComponent {
|
|
1036
720
|
logic = input(null, ...(ngDevMode ? [{ debugName: "logic" }] : []));
|
|
1037
721
|
actions = input([], ...(ngDevMode ? [{ debugName: "actions" }] : []));
|
|
1038
722
|
logicChange = output();
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
builderLogic =
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
723
|
+
/** Internal builder tree state (private writable + public readonly pattern) */
|
|
724
|
+
_builderTree = signal(null, ...(ngDevMode ? [{ debugName: "_builderTree" }] : []));
|
|
725
|
+
builderLogic = this._builderTree.asReadonly();
|
|
726
|
+
constructor() {
|
|
727
|
+
effect(() => {
|
|
728
|
+
const logic = this.logic();
|
|
729
|
+
if (!logic) {
|
|
730
|
+
this._builderTree.set(null);
|
|
731
|
+
}
|
|
732
|
+
else if (!this._builderTree()) {
|
|
733
|
+
this._builderTree.set(toBuilderNode(logic));
|
|
734
|
+
}
|
|
735
|
+
}, { allowSignalWrites: true });
|
|
736
|
+
}
|
|
1052
737
|
initializeLogic() {
|
|
1053
|
-
this.
|
|
1054
|
-
id: crypto.randomUUID(),
|
|
1055
|
-
type: 'group',
|
|
1056
|
-
operator: 'AND',
|
|
1057
|
-
children: [],
|
|
1058
|
-
};
|
|
1059
|
-
this.emitChange();
|
|
738
|
+
this.updateTreeAndEmit(createGroupNode());
|
|
1060
739
|
}
|
|
1061
740
|
clearLogic() {
|
|
1062
|
-
this.
|
|
741
|
+
this._builderTree.set(null);
|
|
1063
742
|
this.logicChange.emit(null);
|
|
1064
743
|
}
|
|
1065
744
|
toggleOperator(nodeId) {
|
|
1066
|
-
|
|
1067
|
-
return;
|
|
1068
|
-
this.builderTree = this.updateNodeInTree(this.builderTree, nodeId, (node) => ({
|
|
745
|
+
this.updateNode(nodeId, (node) => ({
|
|
1069
746
|
...node,
|
|
1070
747
|
operator: node.operator === 'AND' ? 'OR' : 'AND',
|
|
1071
748
|
}));
|
|
1072
|
-
this.emitChange();
|
|
1073
749
|
}
|
|
1074
750
|
addChildNode(parentId, type) {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const newNode = type === 'group'
|
|
1078
|
-
? { id: crypto.randomUUID(), type: 'group', operator: 'AND', children: [] }
|
|
1079
|
-
: { id: crypto.randomUUID(), type: 'action', actionId: '' };
|
|
1080
|
-
this.builderTree = this.updateNodeInTree(this.builderTree, parentId, (node) => ({
|
|
751
|
+
const newNode = type === 'group' ? createGroupNode() : createActionNode();
|
|
752
|
+
this.updateNode(parentId, (node) => ({
|
|
1081
753
|
...node,
|
|
1082
754
|
children: [...(node.children || []), newNode],
|
|
1083
755
|
}));
|
|
1084
|
-
this.emitChange();
|
|
1085
756
|
}
|
|
1086
757
|
removeNode(nodeId) {
|
|
1087
|
-
|
|
758
|
+
const tree = this._builderTree();
|
|
759
|
+
if (!tree)
|
|
1088
760
|
return;
|
|
1089
|
-
if (
|
|
761
|
+
if (tree.id === nodeId) {
|
|
1090
762
|
this.clearLogic();
|
|
1091
763
|
return;
|
|
1092
764
|
}
|
|
1093
|
-
this.
|
|
1094
|
-
this.emitChange();
|
|
765
|
+
this.updateTreeAndEmit(this.removeNodeFromTree(tree, nodeId));
|
|
1095
766
|
}
|
|
1096
767
|
updateActionId(nodeId, actionId) {
|
|
1097
|
-
|
|
1098
|
-
return;
|
|
1099
|
-
this.builderTree = this.updateNodeInTree(this.builderTree, nodeId, (node) => ({
|
|
1100
|
-
...node,
|
|
1101
|
-
actionId,
|
|
1102
|
-
}));
|
|
1103
|
-
this.emitChange();
|
|
768
|
+
this.updateNode(nodeId, (node) => ({ ...node, actionId }));
|
|
1104
769
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
770
|
+
/** Updates a node in the tree and emits the change */
|
|
771
|
+
updateNode(nodeId, updater) {
|
|
772
|
+
const tree = this._builderTree();
|
|
773
|
+
if (!tree)
|
|
1108
774
|
return;
|
|
1109
|
-
|
|
1110
|
-
|
|
775
|
+
this.updateTreeAndEmit(this.updateNodeInTree(tree, nodeId, updater));
|
|
776
|
+
}
|
|
777
|
+
/** Sets the tree and emits the change */
|
|
778
|
+
updateTreeAndEmit(tree) {
|
|
779
|
+
this._builderTree.set(tree);
|
|
780
|
+
this.logicChange.emit(toLogicNode(tree));
|
|
1111
781
|
}
|
|
1112
782
|
updateNodeInTree(node, targetId, updater) {
|
|
1113
783
|
if (node.id === targetId)
|
|
1114
784
|
return updater(node);
|
|
1115
|
-
if (node.children)
|
|
1116
|
-
return
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1121
|
-
return node;
|
|
785
|
+
if (!node.children)
|
|
786
|
+
return node;
|
|
787
|
+
return {
|
|
788
|
+
...node,
|
|
789
|
+
children: node.children.map((child) => this.updateNodeInTree(child, targetId, updater)),
|
|
790
|
+
};
|
|
1122
791
|
}
|
|
1123
792
|
removeNodeFromTree(node, targetId) {
|
|
1124
|
-
if (node.children)
|
|
1125
|
-
return
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1132
|
-
return node;
|
|
793
|
+
if (!node.children)
|
|
794
|
+
return node;
|
|
795
|
+
return {
|
|
796
|
+
...node,
|
|
797
|
+
children: node.children
|
|
798
|
+
.filter((child) => child.id !== targetId)
|
|
799
|
+
.map((child) => this.removeNodeFromTree(child, targetId)),
|
|
800
|
+
};
|
|
1133
801
|
}
|
|
1134
802
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LogicBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1135
803
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: LogicBuilderComponent, isStandalone: true, selector: "lib-logic-builder", inputs: { logic: { classPropertyName: "logic", publicName: "logic", isSignal: true, isRequired: false, transformFunction: null }, actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { logicChange: "logicChange" }, ngImport: i0, template: `
|
|
1136
804
|
<div class="logic-builder">
|
|
1137
805
|
<div class="flex justify-between items-center mb-3">
|
|
1138
|
-
<h4 class="text-sm font-semibold">Permission Logic</h4>
|
|
806
|
+
<h4 class="text-sm font-semibold m-0">Permission Logic</h4>
|
|
1139
807
|
@if (!builderLogic()) {
|
|
1140
808
|
<p-button
|
|
1141
809
|
label="Add Logic"
|
|
@@ -1154,13 +822,13 @@ class LogicBuilderComponent {
|
|
|
1154
822
|
</div>
|
|
1155
823
|
|
|
1156
824
|
@if (builderLogic()) {
|
|
1157
|
-
<div class="border rounded p-3 bg-
|
|
1158
|
-
<div class="mb-3 text-sm text-
|
|
825
|
+
<div class="border border-surface rounded p-3 bg-surface-50">
|
|
826
|
+
<div class="mb-3 text-sm text-muted-color">
|
|
1159
827
|
Define permission requirements using AND/OR logic with actions
|
|
1160
828
|
</div>
|
|
1161
829
|
|
|
1162
830
|
<!-- Root Node -->
|
|
1163
|
-
<div class="
|
|
831
|
+
<div class="text-sm bg-surface-0 rounded border border-surface">
|
|
1164
832
|
<ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: builderLogic()!, depth: 0 }"></ng-container>
|
|
1165
833
|
</div>
|
|
1166
834
|
</div>
|
|
@@ -1169,11 +837,11 @@ class LogicBuilderComponent {
|
|
|
1169
837
|
|
|
1170
838
|
<!-- Recursive Node Template -->
|
|
1171
839
|
<ng-template #nodeTemplate let-node let-depth="depth">
|
|
1172
|
-
<div class="
|
|
1173
|
-
<div class="
|
|
840
|
+
<div class="p-3 border-b border-surface" [ngClass]="depth % 2 === 0 ? 'bg-surface-0' : 'bg-surface-50'">
|
|
841
|
+
<div class="flex items-center gap-3 mb-2">
|
|
1174
842
|
<span class="node-type" [ngClass]="node.type">{{ node.type.toUpperCase() }}</span>
|
|
1175
843
|
|
|
1176
|
-
<div class="
|
|
844
|
+
<div class="flex-1 flex items-center gap-2">
|
|
1177
845
|
@if (node.type === 'group') {
|
|
1178
846
|
<!-- Group: show operator toggle -->
|
|
1179
847
|
<span
|
|
@@ -1183,24 +851,24 @@ class LogicBuilderComponent {
|
|
|
1183
851
|
title="Click to toggle">
|
|
1184
852
|
{{ node.operator }}
|
|
1185
853
|
</span>
|
|
1186
|
-
<span class="text-
|
|
854
|
+
<span class="text-muted-color text-xs">({{ node.children?.length || 0 }} conditions)</span>
|
|
1187
855
|
}
|
|
1188
856
|
|
|
1189
857
|
@if (node.type === 'action') {
|
|
1190
858
|
<!-- Action: show action selector -->
|
|
1191
859
|
<select
|
|
1192
|
-
class="flex-1"
|
|
860
|
+
class="action-select flex-1 p-2 border border-surface rounded text-sm"
|
|
1193
861
|
[ngModel]="node.actionId"
|
|
1194
862
|
(ngModelChange)="updateActionId(node.id, $event)">
|
|
1195
|
-
<option [value]="null">Select Action... ({{
|
|
1196
|
-
@for (action of
|
|
863
|
+
<option [value]="null">Select Action... ({{ actions().length }} available)</option>
|
|
864
|
+
@for (action of actions(); track action.id) {
|
|
1197
865
|
<option [ngValue]="action.id">{{ action.name }}</option>
|
|
1198
866
|
}
|
|
1199
867
|
</select>
|
|
1200
868
|
}
|
|
1201
869
|
</div>
|
|
1202
870
|
|
|
1203
|
-
<div class="
|
|
871
|
+
<div class="flex gap-1">
|
|
1204
872
|
<p-button
|
|
1205
873
|
icon="pi pi-trash"
|
|
1206
874
|
[text]="true"
|
|
@@ -1213,7 +881,7 @@ class LogicBuilderComponent {
|
|
|
1213
881
|
|
|
1214
882
|
<!-- Children for group nodes -->
|
|
1215
883
|
@if (node.type === 'group' && node.children && node.children.length > 0) {
|
|
1216
|
-
<div class="
|
|
884
|
+
<div class="ml-5 mt-2 pl-3 border-l-2 border-surface">
|
|
1217
885
|
@for (child of node.children; track child.id) {
|
|
1218
886
|
<ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: child, depth: depth + 1 }"></ng-container>
|
|
1219
887
|
}
|
|
@@ -1222,9 +890,9 @@ class LogicBuilderComponent {
|
|
|
1222
890
|
|
|
1223
891
|
<!-- Add child buttons for group nodes -->
|
|
1224
892
|
@if (node.type === 'group') {
|
|
1225
|
-
<div class="
|
|
1226
|
-
<div class="text-xs font-semibold text-
|
|
1227
|
-
<div class="
|
|
893
|
+
<div class="mt-3 p-3 bg-surface-50 rounded border border-dashed border-surface">
|
|
894
|
+
<div class="text-xs font-semibold text-muted-color mb-2">Add Condition:</div>
|
|
895
|
+
<div class="flex gap-2 flex-wrap">
|
|
1228
896
|
<p-button
|
|
1229
897
|
label="Group"
|
|
1230
898
|
icon="pi pi-sitemap"
|
|
@@ -1243,14 +911,14 @@ class LogicBuilderComponent {
|
|
|
1243
911
|
}
|
|
1244
912
|
</div>
|
|
1245
913
|
</ng-template>
|
|
1246
|
-
`, isInline: true, styles: [":host{display:block}.
|
|
914
|
+
`, isInline: true, styles: [":host{display:block}.node-type{display:inline-flex;align-items:center;padding:.25rem .5rem;border-radius:.25rem;font-weight:600;font-size:.75rem;text-transform:uppercase}.node-type.group{background-color:var(--p-blue-100, #dbeafe);color:var(--p-blue-700, #1e40af)}.node-type.action{background-color:var(--p-green-100, #d1fae5);color:var(--p-green-700, #065f46)}:host-context(.p-dark) .node-type.group{background-color:#3b82f633;color:var(--p-blue-400, #60a5fa)}:host-context(.p-dark) .node-type.action{background-color:#22c55e33;color:var(--p-green-400, #4ade80)}.operator-badge{display:inline-flex;align-items:center;padding:.125rem .5rem;border-radius:9999px;font-weight:600;font-size:.75rem;cursor:pointer;transition:opacity .2s}.operator-badge.and{background-color:var(--p-yellow-100, #fef3c7);color:var(--p-yellow-700, #92400e)}.operator-badge.or{background-color:var(--p-indigo-100, #e0e7ff);color:var(--p-indigo-700, #3730a3)}:host-context(.p-dark) .operator-badge.and{background-color:#eab30833;color:var(--p-yellow-400, #facc15)}:host-context(.p-dark) .operator-badge.or{background-color:#6366f133;color:var(--p-indigo-400, #818cf8)}.operator-badge:hover{opacity:.8}.action-select{background-color:var(--p-surface-0, #ffffff);color:var(--p-text-color, #1f2937)}:host-context(.p-dark) .action-select{background-color:var(--p-surface-900, #1f2937);color:var(--p-text-color, #f9fafb)}.action-select:focus{outline:none;border-color:var(--p-primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "directive", type: i6.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1247
915
|
}
|
|
1248
916
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LogicBuilderComponent, decorators: [{
|
|
1249
917
|
type: Component,
|
|
1250
|
-
args: [{ selector: 'lib-logic-builder',
|
|
918
|
+
args: [{ selector: 'lib-logic-builder', imports: [AngularModule, PrimeModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1251
919
|
<div class="logic-builder">
|
|
1252
920
|
<div class="flex justify-between items-center mb-3">
|
|
1253
|
-
<h4 class="text-sm font-semibold">Permission Logic</h4>
|
|
921
|
+
<h4 class="text-sm font-semibold m-0">Permission Logic</h4>
|
|
1254
922
|
@if (!builderLogic()) {
|
|
1255
923
|
<p-button
|
|
1256
924
|
label="Add Logic"
|
|
@@ -1269,13 +937,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1269
937
|
</div>
|
|
1270
938
|
|
|
1271
939
|
@if (builderLogic()) {
|
|
1272
|
-
<div class="border rounded p-3 bg-
|
|
1273
|
-
<div class="mb-3 text-sm text-
|
|
940
|
+
<div class="border border-surface rounded p-3 bg-surface-50">
|
|
941
|
+
<div class="mb-3 text-sm text-muted-color">
|
|
1274
942
|
Define permission requirements using AND/OR logic with actions
|
|
1275
943
|
</div>
|
|
1276
944
|
|
|
1277
945
|
<!-- Root Node -->
|
|
1278
|
-
<div class="
|
|
946
|
+
<div class="text-sm bg-surface-0 rounded border border-surface">
|
|
1279
947
|
<ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: builderLogic()!, depth: 0 }"></ng-container>
|
|
1280
948
|
</div>
|
|
1281
949
|
</div>
|
|
@@ -1284,11 +952,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1284
952
|
|
|
1285
953
|
<!-- Recursive Node Template -->
|
|
1286
954
|
<ng-template #nodeTemplate let-node let-depth="depth">
|
|
1287
|
-
<div class="
|
|
1288
|
-
<div class="
|
|
955
|
+
<div class="p-3 border-b border-surface" [ngClass]="depth % 2 === 0 ? 'bg-surface-0' : 'bg-surface-50'">
|
|
956
|
+
<div class="flex items-center gap-3 mb-2">
|
|
1289
957
|
<span class="node-type" [ngClass]="node.type">{{ node.type.toUpperCase() }}</span>
|
|
1290
958
|
|
|
1291
|
-
<div class="
|
|
959
|
+
<div class="flex-1 flex items-center gap-2">
|
|
1292
960
|
@if (node.type === 'group') {
|
|
1293
961
|
<!-- Group: show operator toggle -->
|
|
1294
962
|
<span
|
|
@@ -1298,24 +966,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1298
966
|
title="Click to toggle">
|
|
1299
967
|
{{ node.operator }}
|
|
1300
968
|
</span>
|
|
1301
|
-
<span class="text-
|
|
969
|
+
<span class="text-muted-color text-xs">({{ node.children?.length || 0 }} conditions)</span>
|
|
1302
970
|
}
|
|
1303
971
|
|
|
1304
972
|
@if (node.type === 'action') {
|
|
1305
973
|
<!-- Action: show action selector -->
|
|
1306
974
|
<select
|
|
1307
|
-
class="flex-1"
|
|
975
|
+
class="action-select flex-1 p-2 border border-surface rounded text-sm"
|
|
1308
976
|
[ngModel]="node.actionId"
|
|
1309
977
|
(ngModelChange)="updateActionId(node.id, $event)">
|
|
1310
|
-
<option [value]="null">Select Action... ({{
|
|
1311
|
-
@for (action of
|
|
978
|
+
<option [value]="null">Select Action... ({{ actions().length }} available)</option>
|
|
979
|
+
@for (action of actions(); track action.id) {
|
|
1312
980
|
<option [ngValue]="action.id">{{ action.name }}</option>
|
|
1313
981
|
}
|
|
1314
982
|
</select>
|
|
1315
983
|
}
|
|
1316
984
|
</div>
|
|
1317
985
|
|
|
1318
|
-
<div class="
|
|
986
|
+
<div class="flex gap-1">
|
|
1319
987
|
<p-button
|
|
1320
988
|
icon="pi pi-trash"
|
|
1321
989
|
[text]="true"
|
|
@@ -1328,7 +996,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1328
996
|
|
|
1329
997
|
<!-- Children for group nodes -->
|
|
1330
998
|
@if (node.type === 'group' && node.children && node.children.length > 0) {
|
|
1331
|
-
<div class="
|
|
999
|
+
<div class="ml-5 mt-2 pl-3 border-l-2 border-surface">
|
|
1332
1000
|
@for (child of node.children; track child.id) {
|
|
1333
1001
|
<ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: child, depth: depth + 1 }"></ng-container>
|
|
1334
1002
|
}
|
|
@@ -1337,9 +1005,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1337
1005
|
|
|
1338
1006
|
<!-- Add child buttons for group nodes -->
|
|
1339
1007
|
@if (node.type === 'group') {
|
|
1340
|
-
<div class="
|
|
1341
|
-
<div class="text-xs font-semibold text-
|
|
1342
|
-
<div class="
|
|
1008
|
+
<div class="mt-3 p-3 bg-surface-50 rounded border border-dashed border-surface">
|
|
1009
|
+
<div class="text-xs font-semibold text-muted-color mb-2">Add Condition:</div>
|
|
1010
|
+
<div class="flex gap-2 flex-wrap">
|
|
1343
1011
|
<p-button
|
|
1344
1012
|
label="Group"
|
|
1345
1013
|
icon="pi pi-sitemap"
|
|
@@ -1358,105 +1026,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1358
1026
|
}
|
|
1359
1027
|
</div>
|
|
1360
1028
|
</ng-template>
|
|
1361
|
-
`, styles: [":host{display:block}.
|
|
1362
|
-
}], propDecorators: { logic: [{ type: i0.Input, args: [{ isSignal: true, alias: "logic", required: false }] }], actions: [{ type: i0.Input, args: [{ isSignal: true, alias: "actions", required: false }] }], logicChange: [{ type: i0.Output, args: ["logicChange"] }] } });
|
|
1029
|
+
`, styles: [":host{display:block}.node-type{display:inline-flex;align-items:center;padding:.25rem .5rem;border-radius:.25rem;font-weight:600;font-size:.75rem;text-transform:uppercase}.node-type.group{background-color:var(--p-blue-100, #dbeafe);color:var(--p-blue-700, #1e40af)}.node-type.action{background-color:var(--p-green-100, #d1fae5);color:var(--p-green-700, #065f46)}:host-context(.p-dark) .node-type.group{background-color:#3b82f633;color:var(--p-blue-400, #60a5fa)}:host-context(.p-dark) .node-type.action{background-color:#22c55e33;color:var(--p-green-400, #4ade80)}.operator-badge{display:inline-flex;align-items:center;padding:.125rem .5rem;border-radius:9999px;font-weight:600;font-size:.75rem;cursor:pointer;transition:opacity .2s}.operator-badge.and{background-color:var(--p-yellow-100, #fef3c7);color:var(--p-yellow-700, #92400e)}.operator-badge.or{background-color:var(--p-indigo-100, #e0e7ff);color:var(--p-indigo-700, #3730a3)}:host-context(.p-dark) .operator-badge.and{background-color:#eab30833;color:var(--p-yellow-400, #facc15)}:host-context(.p-dark) .operator-badge.or{background-color:#6366f133;color:var(--p-indigo-400, #818cf8)}.operator-badge:hover{opacity:.8}.action-select{background-color:var(--p-surface-0, #ffffff);color:var(--p-text-color, #1f2937)}:host-context(.p-dark) .action-select{background-color:var(--p-surface-900, #1f2937);color:var(--p-text-color, #f9fafb)}.action-select:focus{outline:none;border-color:var(--p-primary-color, #3b82f6)}\n"] }]
|
|
1030
|
+
}], ctorParameters: () => [], propDecorators: { logic: [{ type: i0.Input, args: [{ isSignal: true, alias: "logic", required: false }] }], actions: [{ type: i0.Input, args: [{ isSignal: true, alias: "actions", required: false }] }], logicChange: [{ type: i0.Output, args: ["logicChange"] }] } });
|
|
1363
1031
|
|
|
1364
|
-
/**
|
|
1365
|
-
* Tree Utility Functions
|
|
1366
|
-
* Shared utilities for working with hierarchical tree structures
|
|
1367
|
-
*/
|
|
1368
|
-
/**
|
|
1369
|
-
* Flattens a hierarchical tree structure into a flat array
|
|
1370
|
-
*
|
|
1371
|
-
* @template T - Type of tree node (must have optional children array)
|
|
1372
|
-
* @param tree - Array of tree nodes to flatten
|
|
1373
|
-
* @returns Flat array containing all nodes from the tree
|
|
1374
|
-
*
|
|
1375
|
-
* @example
|
|
1376
|
-
* ```typescript
|
|
1377
|
-
* const tree = [
|
|
1378
|
-
* { id: '1', name: 'Parent', children: [
|
|
1379
|
-
* { id: '2', name: 'Child 1' },
|
|
1380
|
-
* { id: '3', name: 'Child 2' }
|
|
1381
|
-
* ]}
|
|
1382
|
-
* ];
|
|
1383
|
-
* const flat = flattenTree(tree);
|
|
1384
|
-
* // Returns: [{ id: '1', ... }, { id: '2', ... }, { id: '3', ... }]
|
|
1385
|
-
* ```
|
|
1386
|
-
*/
|
|
1387
1032
|
function flattenTree(tree) {
|
|
1388
|
-
if (!tree?.length)
|
|
1033
|
+
if (!tree?.length)
|
|
1389
1034
|
return [];
|
|
1390
|
-
}
|
|
1391
1035
|
const result = [];
|
|
1392
1036
|
const flatten = (nodes) => {
|
|
1393
1037
|
for (const node of nodes) {
|
|
1394
1038
|
result.push(node);
|
|
1395
|
-
if (node.children?.length)
|
|
1039
|
+
if (node.children?.length)
|
|
1396
1040
|
flatten(node.children);
|
|
1397
|
-
}
|
|
1398
1041
|
}
|
|
1399
1042
|
};
|
|
1400
1043
|
flatten(tree);
|
|
1401
1044
|
return result;
|
|
1402
1045
|
}
|
|
1403
|
-
/**
|
|
1404
|
-
* Builds a map of items by their ID for quick lookup
|
|
1405
|
-
*
|
|
1406
|
-
* @template T - Type of item (must have id property)
|
|
1407
|
-
* @param items - Array of items to map
|
|
1408
|
-
* @returns Map with item IDs as keys and items as values
|
|
1409
|
-
*
|
|
1410
|
-
* @example
|
|
1411
|
-
* ```typescript
|
|
1412
|
-
* const actions = [{ id: '1', name: 'Action 1' }, { id: '2', name: 'Action 2' }];
|
|
1413
|
-
* const map = buildItemMap(actions);
|
|
1414
|
-
* const action = map.get('1'); // { id: '1', name: 'Action 1' }
|
|
1415
|
-
* ```
|
|
1416
|
-
*/
|
|
1417
|
-
function buildItemMap(items) {
|
|
1418
|
-
if (!items?.length) {
|
|
1419
|
-
return new Map();
|
|
1420
|
-
}
|
|
1421
|
-
return new Map(items
|
|
1422
|
-
.filter((item) => item?.id !== undefined && item?.id !== null)
|
|
1423
|
-
.map((item) => [String(item.id), item]));
|
|
1424
|
-
}
|
|
1425
|
-
/**
|
|
1426
|
-
* Build tree structure from flat list using parentId
|
|
1427
|
-
*
|
|
1428
|
-
* Converts flat array with parentId references into hierarchical tree structure.
|
|
1429
|
-
* Used when backend returns flat list but tree structure is needed for display.
|
|
1430
|
-
*
|
|
1431
|
-
* @template T - Type of node (must have id and optional parentId)
|
|
1432
|
-
* @param flatList - Flat array of nodes with parentId references
|
|
1433
|
-
* @returns Hierarchical tree with children arrays
|
|
1434
|
-
*
|
|
1435
|
-
* @example
|
|
1436
|
-
* ```typescript
|
|
1437
|
-
* const flat = [
|
|
1438
|
-
* { id: '1', name: 'Parent', parentId: null },
|
|
1439
|
-
* { id: '2', name: 'Child 1', parentId: '1' },
|
|
1440
|
-
* { id: '3', name: 'Child 2', parentId: '1' }
|
|
1441
|
-
* ];
|
|
1442
|
-
* const tree = buildTreeFromFlat(flat);
|
|
1443
|
-
* // Returns: [{ id: '1', name: 'Parent', children: [child1, child2] }]
|
|
1444
|
-
* ```
|
|
1445
|
-
*/
|
|
1446
1046
|
function buildTreeFromFlat(flatList) {
|
|
1447
|
-
if (!flatList?.length)
|
|
1047
|
+
if (!flatList?.length)
|
|
1448
1048
|
return [];
|
|
1449
|
-
}
|
|
1450
|
-
// Create map for O(1) lookup
|
|
1451
1049
|
const itemMap = new Map();
|
|
1452
1050
|
const rootItems = [];
|
|
1453
|
-
// First pass: Create all nodes with empty children arrays
|
|
1454
1051
|
flatList.forEach((item) => {
|
|
1455
|
-
if (item.id)
|
|
1052
|
+
if (item.id)
|
|
1456
1053
|
itemMap.set(item.id, { ...item, children: [] });
|
|
1457
|
-
}
|
|
1458
1054
|
});
|
|
1459
|
-
// Second pass: Build parent-child relationships
|
|
1460
1055
|
flatList.forEach((item) => {
|
|
1461
1056
|
if (!item.id)
|
|
1462
1057
|
return;
|
|
@@ -1464,51 +1059,23 @@ function buildTreeFromFlat(flatList) {
|
|
|
1464
1059
|
if (!node)
|
|
1465
1060
|
return;
|
|
1466
1061
|
if (!item.parentId) {
|
|
1467
|
-
// Root level item
|
|
1468
1062
|
rootItems.push(node);
|
|
1469
1063
|
}
|
|
1470
1064
|
else {
|
|
1471
|
-
// Child item - add to parent's children
|
|
1472
1065
|
const parent = itemMap.get(item.parentId);
|
|
1473
1066
|
if (parent) {
|
|
1474
1067
|
parent.children.push(node);
|
|
1475
1068
|
}
|
|
1476
1069
|
else {
|
|
1477
|
-
// Parent not found - treat as root
|
|
1478
1070
|
rootItems.push(node);
|
|
1479
1071
|
}
|
|
1480
1072
|
}
|
|
1481
1073
|
});
|
|
1482
1074
|
return rootItems;
|
|
1483
1075
|
}
|
|
1484
|
-
/**
|
|
1485
|
-
* Convert ActionTreeDto to PrimeNG TreeNode format
|
|
1486
|
-
*
|
|
1487
|
-
* Transforms hierarchical action data from backend into PrimeNG TreeTable format.
|
|
1488
|
-
* Recursively processes children and sets leaf/expanded properties.
|
|
1489
|
-
*
|
|
1490
|
-
* @param actions - Array of action tree DTOs from backend
|
|
1491
|
-
* @returns Array of TreeNodes for PrimeNG TreeTable
|
|
1492
|
-
*
|
|
1493
|
-
* @example
|
|
1494
|
-
* ```typescript
|
|
1495
|
-
* const actionsTree = [
|
|
1496
|
-
* {
|
|
1497
|
-
* id: '1',
|
|
1498
|
-
* name: 'User Management',
|
|
1499
|
-
* children: [
|
|
1500
|
-
* { id: '2', name: 'Create User', children: [] }
|
|
1501
|
-
* ]
|
|
1502
|
-
* }
|
|
1503
|
-
* ];
|
|
1504
|
-
* const treeNodes = convertActionToTreeNode(actionsTree);
|
|
1505
|
-
* // Use with: <p-treeTable [value]="treeNodes">
|
|
1506
|
-
* ```
|
|
1507
|
-
*/
|
|
1508
1076
|
function convertActionToTreeNode(actions) {
|
|
1509
|
-
if (!actions?.length)
|
|
1077
|
+
if (!actions?.length)
|
|
1510
1078
|
return [];
|
|
1511
|
-
}
|
|
1512
1079
|
const convert = (action) => {
|
|
1513
1080
|
const { children, ...actionData } = action;
|
|
1514
1081
|
return {
|
|
@@ -1556,7 +1123,10 @@ function convertActionToTreeNode(actions) {
|
|
|
1556
1123
|
* ```
|
|
1557
1124
|
*/
|
|
1558
1125
|
class RoleActionSelectorComponent {
|
|
1126
|
+
// Permission constants for template
|
|
1127
|
+
ROLE_ACTION_PERMISSIONS = ROLE_ACTION_PERMISSIONS;
|
|
1559
1128
|
// Dependencies
|
|
1129
|
+
destroyRef = inject(DestroyRef);
|
|
1560
1130
|
roleApi = inject(RoleApiService);
|
|
1561
1131
|
actionApi = inject(ActionApiService);
|
|
1562
1132
|
permissionApi = inject(PermissionApiService);
|
|
@@ -1605,17 +1175,14 @@ class RoleActionSelectorComponent {
|
|
|
1605
1175
|
const selMap = this.selectionMap();
|
|
1606
1176
|
const allActions = this.actions();
|
|
1607
1177
|
const unmetSet = new Set();
|
|
1608
|
-
|
|
1609
|
-
if (action.id &&
|
|
1610
|
-
this.permissionLogic.hasUnmetPrerequisites(action, selMap, allActions)) {
|
|
1178
|
+
for (const action of allActions) {
|
|
1179
|
+
if (action.id && this.permissionLogic.hasUnmetPrerequisites(action, selMap, allActions)) {
|
|
1611
1180
|
unmetSet.add(action.id);
|
|
1612
1181
|
}
|
|
1613
|
-
}
|
|
1182
|
+
}
|
|
1614
1183
|
return unmetSet;
|
|
1615
1184
|
}, ...(ngDevMode ? [{ debugName: "actionsWithUnmetPrerequisites" }] : []));
|
|
1616
|
-
invalidActionsCount = computed(() => {
|
|
1617
|
-
return this.actionsWithUnmetPrerequisites().size;
|
|
1618
|
-
}, ...(ngDevMode ? [{ debugName: "invalidActionsCount" }] : []));
|
|
1185
|
+
invalidActionsCount = computed(() => this.actionsWithUnmetPrerequisites().size, ...(ngDevMode ? [{ debugName: "invalidActionsCount" }] : []));
|
|
1619
1186
|
// Computed - Pending Changes
|
|
1620
1187
|
pendingAdd = computed(() => {
|
|
1621
1188
|
const current = this.selectionMap();
|
|
@@ -1637,6 +1204,10 @@ class RoleActionSelectorComponent {
|
|
|
1637
1204
|
loadDataAbortController = null;
|
|
1638
1205
|
constructor() {
|
|
1639
1206
|
this.loadRoles();
|
|
1207
|
+
// Cleanup on destroy
|
|
1208
|
+
this.destroyRef.onDestroy(() => {
|
|
1209
|
+
this.loadDataAbortController?.abort();
|
|
1210
|
+
});
|
|
1640
1211
|
// Effect: Load data when role selection changes
|
|
1641
1212
|
effect(() => {
|
|
1642
1213
|
const roleId = this.selectedRoleId();
|
|
@@ -1646,11 +1217,7 @@ class RoleActionSelectorComponent {
|
|
|
1646
1217
|
this.loadDataAbortController = new AbortController();
|
|
1647
1218
|
this.onRoleChange(this.loadDataAbortController.signal).catch((err) => {
|
|
1648
1219
|
if (err.name !== 'AbortError') {
|
|
1649
|
-
|
|
1650
|
-
severity: 'error',
|
|
1651
|
-
summary: 'Error',
|
|
1652
|
-
detail: 'Failed to load role permissions. Please refresh.',
|
|
1653
|
-
});
|
|
1220
|
+
// Error toast handled by global interceptor
|
|
1654
1221
|
this.loading.set(false);
|
|
1655
1222
|
}
|
|
1656
1223
|
});
|
|
@@ -1660,9 +1227,6 @@ class RoleActionSelectorComponent {
|
|
|
1660
1227
|
}
|
|
1661
1228
|
});
|
|
1662
1229
|
}
|
|
1663
|
-
ngOnDestroy() {
|
|
1664
|
-
this.loadDataAbortController?.abort();
|
|
1665
|
-
}
|
|
1666
1230
|
/**
|
|
1667
1231
|
* Load roles from API
|
|
1668
1232
|
*/
|
|
@@ -1674,11 +1238,7 @@ class RoleActionSelectorComponent {
|
|
|
1674
1238
|
this.roles.set(response?.data ?? []);
|
|
1675
1239
|
}
|
|
1676
1240
|
catch {
|
|
1677
|
-
|
|
1678
|
-
severity: 'error',
|
|
1679
|
-
summary: 'Error',
|
|
1680
|
-
detail: 'Failed to load roles',
|
|
1681
|
-
});
|
|
1241
|
+
// Error toast handled by global interceptor
|
|
1682
1242
|
}
|
|
1683
1243
|
}
|
|
1684
1244
|
/**
|
|
@@ -1707,13 +1267,12 @@ class RoleActionSelectorComponent {
|
|
|
1707
1267
|
const assignedIds = new Set((assignedResponse?.data || []).map((a) => a.actionId));
|
|
1708
1268
|
// Build selection map
|
|
1709
1269
|
const selMap = {};
|
|
1710
|
-
|
|
1270
|
+
for (const action of flatActions) {
|
|
1711
1271
|
if (action.id) {
|
|
1712
1272
|
selMap[action.id] = assignedIds.has(action.id);
|
|
1713
1273
|
}
|
|
1714
|
-
}
|
|
1715
|
-
this.
|
|
1716
|
-
this._initialSelection.set({ ...selMap });
|
|
1274
|
+
}
|
|
1275
|
+
this.applySelection(selMap);
|
|
1717
1276
|
}
|
|
1718
1277
|
finally {
|
|
1719
1278
|
if (!signal.aborted) {
|
|
@@ -1762,35 +1321,20 @@ class RoleActionSelectorComponent {
|
|
|
1762
1321
|
this.permissionLogic.handleCheck(action, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap), (previousState) => this._selectionMap.set(previousState));
|
|
1763
1322
|
}
|
|
1764
1323
|
}
|
|
1765
|
-
/**
|
|
1766
|
-
* Toggle all actions
|
|
1767
|
-
*/
|
|
1768
1324
|
toggleAll() {
|
|
1769
|
-
|
|
1770
|
-
const selMap = {};
|
|
1771
|
-
this.actions().forEach((action) => {
|
|
1772
|
-
selMap[action.id] = newValue;
|
|
1773
|
-
});
|
|
1774
|
-
this._selectionMap.set(selMap);
|
|
1325
|
+
this.setAllSelection(!this.allSelected());
|
|
1775
1326
|
}
|
|
1776
|
-
/**
|
|
1777
|
-
* Select all actions
|
|
1778
|
-
*/
|
|
1779
1327
|
selectAll() {
|
|
1780
|
-
|
|
1781
|
-
this.actions().forEach((action) => {
|
|
1782
|
-
selMap[action.id] = true;
|
|
1783
|
-
});
|
|
1784
|
-
this._selectionMap.set(selMap);
|
|
1328
|
+
this.setAllSelection(true);
|
|
1785
1329
|
}
|
|
1786
|
-
/**
|
|
1787
|
-
* Deselect all actions
|
|
1788
|
-
*/
|
|
1789
1330
|
deselectAll() {
|
|
1331
|
+
this.setAllSelection(false);
|
|
1332
|
+
}
|
|
1333
|
+
setAllSelection(value) {
|
|
1790
1334
|
const selMap = {};
|
|
1791
|
-
this.actions()
|
|
1792
|
-
selMap[action.id] =
|
|
1793
|
-
}
|
|
1335
|
+
for (const action of this.actions()) {
|
|
1336
|
+
selMap[action.id] = value;
|
|
1337
|
+
}
|
|
1794
1338
|
this._selectionMap.set(selMap);
|
|
1795
1339
|
}
|
|
1796
1340
|
/**
|
|
@@ -1807,19 +1351,7 @@ class RoleActionSelectorComponent {
|
|
|
1807
1351
|
return;
|
|
1808
1352
|
}
|
|
1809
1353
|
// Build payload
|
|
1810
|
-
const items =
|
|
1811
|
-
this.pendingAdd().forEach((action) => {
|
|
1812
|
-
items.push({
|
|
1813
|
-
id: action.id,
|
|
1814
|
-
action: 'add',
|
|
1815
|
-
});
|
|
1816
|
-
});
|
|
1817
|
-
this.pendingRemove().forEach((action) => {
|
|
1818
|
-
items.push({
|
|
1819
|
-
id: action.id,
|
|
1820
|
-
action: 'remove',
|
|
1821
|
-
});
|
|
1822
|
-
});
|
|
1354
|
+
const items = this.buildPayloadItems();
|
|
1823
1355
|
if (items.length === 0)
|
|
1824
1356
|
return;
|
|
1825
1357
|
this.saving.set(true);
|
|
@@ -1836,21 +1368,27 @@ class RoleActionSelectorComponent {
|
|
|
1836
1368
|
// Update baseline
|
|
1837
1369
|
this._initialSelection.set({ ...this.selectionMap() });
|
|
1838
1370
|
}
|
|
1839
|
-
catch
|
|
1840
|
-
|
|
1841
|
-
this.messageService.add({
|
|
1842
|
-
severity: 'error',
|
|
1843
|
-
summary: 'Error',
|
|
1844
|
-
detail: error?.error?.message || 'Failed to update role permissions',
|
|
1845
|
-
});
|
|
1371
|
+
catch {
|
|
1372
|
+
// Error toast handled by global interceptor
|
|
1846
1373
|
}
|
|
1847
1374
|
finally {
|
|
1848
1375
|
this.saving.set(false);
|
|
1849
1376
|
}
|
|
1850
1377
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1378
|
+
applySelection(selMap) {
|
|
1379
|
+
this._selectionMap.set(selMap);
|
|
1380
|
+
this._initialSelection.set({ ...selMap });
|
|
1381
|
+
}
|
|
1382
|
+
buildPayloadItems() {
|
|
1383
|
+
const items = [];
|
|
1384
|
+
for (const action of this.pendingAdd()) {
|
|
1385
|
+
items.push({ id: action.id, action: 'add' });
|
|
1386
|
+
}
|
|
1387
|
+
for (const action of this.pendingRemove()) {
|
|
1388
|
+
items.push({ id: action.id, action: 'remove' });
|
|
1389
|
+
}
|
|
1390
|
+
return items;
|
|
1391
|
+
}
|
|
1854
1392
|
resetState() {
|
|
1855
1393
|
this._actionsTree.set([]);
|
|
1856
1394
|
this._actions.set([]);
|
|
@@ -1862,11 +1400,12 @@ class RoleActionSelectorComponent {
|
|
|
1862
1400
|
<div class="role-action-selector">
|
|
1863
1401
|
<!-- Role Selector -->
|
|
1864
1402
|
<div class="mb-4">
|
|
1865
|
-
<div class="grid">
|
|
1866
|
-
<div
|
|
1403
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
1404
|
+
<div>
|
|
1867
1405
|
<label class="block font-semibold mb-2">Select Role</label>
|
|
1868
1406
|
<p-select
|
|
1869
|
-
[
|
|
1407
|
+
[ngModel]="selectedRoleId()"
|
|
1408
|
+
(ngModelChange)="selectedRoleId.set($event)"
|
|
1870
1409
|
[options]="roles()"
|
|
1871
1410
|
optionLabel="name"
|
|
1872
1411
|
optionValue="id"
|
|
@@ -1883,24 +1422,21 @@ class RoleActionSelectorComponent {
|
|
|
1883
1422
|
<!-- Loading State -->
|
|
1884
1423
|
@if (loading()) {
|
|
1885
1424
|
<div
|
|
1886
|
-
class="surface-card p-5 border
|
|
1425
|
+
class="surface-card p-5 rounded-border shadow-sm flex justify-center"
|
|
1887
1426
|
>
|
|
1888
|
-
<i
|
|
1889
|
-
class="pi pi-spin pi-spinner text-primary"
|
|
1890
|
-
style="font-size: 3rem"
|
|
1891
|
-
></i>
|
|
1427
|
+
<i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
|
|
1892
1428
|
</div>
|
|
1893
1429
|
}
|
|
1894
1430
|
|
|
1895
1431
|
<!-- Action List -->
|
|
1896
1432
|
@if (!loading() && actions().length > 0) {
|
|
1897
|
-
<div class="surface-card p-4 border
|
|
1433
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
1898
1434
|
<div
|
|
1899
|
-
class="flex flex-
|
|
1435
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
1900
1436
|
>
|
|
1901
1437
|
<div>
|
|
1902
1438
|
<h5 class="m-0 mb-1">Action Permissions</h5>
|
|
1903
|
-
<p class="text-sm text-color
|
|
1439
|
+
<p class="text-sm text-muted-color m-0">
|
|
1904
1440
|
{{ actions().length }} actions available
|
|
1905
1441
|
</p>
|
|
1906
1442
|
</div>
|
|
@@ -1922,6 +1458,7 @@ class RoleActionSelectorComponent {
|
|
|
1922
1458
|
(onClick)="deselectAll()"
|
|
1923
1459
|
/>
|
|
1924
1460
|
<p-button
|
|
1461
|
+
*hasPermission="ROLE_ACTION_PERMISSIONS.ASSIGN"
|
|
1925
1462
|
label="Save Changes"
|
|
1926
1463
|
icon="pi pi-save"
|
|
1927
1464
|
[disabled]="!canSave()"
|
|
@@ -1935,14 +1472,12 @@ class RoleActionSelectorComponent {
|
|
|
1935
1472
|
|
|
1936
1473
|
<!-- Validation Warning -->
|
|
1937
1474
|
@if (invalidActionsCount() > 0) {
|
|
1938
|
-
<div class="
|
|
1939
|
-
<div class="flex items-
|
|
1940
|
-
<i
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
<strong class="text-orange-800">Validation Warning:</strong>
|
|
1945
|
-
<p class="text-sm text-orange-700 mt-1">
|
|
1475
|
+
<div class="validation-warning rounded-border p-3 mb-4">
|
|
1476
|
+
<div class="flex items-start gap-2">
|
|
1477
|
+
<i class="pi pi-exclamation-triangle text-xl"></i>
|
|
1478
|
+
<div class="flex-1">
|
|
1479
|
+
<span class="font-semibold">Validation Warning:</span>
|
|
1480
|
+
<p class="text-sm mt-1 mb-0">
|
|
1946
1481
|
{{ invalidActionsCount() }} selected
|
|
1947
1482
|
action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
|
|
1948
1483
|
unmet prerequisites. Fix before saving or use auto-fix on
|
|
@@ -1954,33 +1489,33 @@ class RoleActionSelectorComponent {
|
|
|
1954
1489
|
}
|
|
1955
1490
|
|
|
1956
1491
|
<!-- Action Tree Table -->
|
|
1957
|
-
<
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
<ng-template
|
|
1982
|
-
<tr [class.
|
|
1983
|
-
<td
|
|
1492
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
1493
|
+
<p-treeTable
|
|
1494
|
+
[value]="treeNodes()"
|
|
1495
|
+
dataKey="id"
|
|
1496
|
+
styleClass="p-treetable-sm"
|
|
1497
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
1498
|
+
>
|
|
1499
|
+
<ng-template #header>
|
|
1500
|
+
<tr>
|
|
1501
|
+
<th class="w-12">
|
|
1502
|
+
<p-checkbox
|
|
1503
|
+
[ngModel]="allSelected()"
|
|
1504
|
+
[binary]="true"
|
|
1505
|
+
(ngModelChange)="toggleAll()"
|
|
1506
|
+
pTooltip="Select/Deselect All"
|
|
1507
|
+
tooltipPosition="top"
|
|
1508
|
+
/>
|
|
1509
|
+
</th>
|
|
1510
|
+
<th>Name</th>
|
|
1511
|
+
<th class="hidden md:table-cell">Code</th>
|
|
1512
|
+
<th>Type</th>
|
|
1513
|
+
<th class="hidden lg:table-cell">Requirements</th>
|
|
1514
|
+
</tr>
|
|
1515
|
+
</ng-template>
|
|
1516
|
+
<ng-template #body let-rowNode let-rowData="rowData">
|
|
1517
|
+
<tr [class.highlight-warning]="hasUnmetPrerequisites(rowData)">
|
|
1518
|
+
<td class="w-12">
|
|
1984
1519
|
<p-checkbox
|
|
1985
1520
|
[ngModel]="selectionMap()[rowData.id]"
|
|
1986
1521
|
[binary]="true"
|
|
@@ -1991,7 +1526,7 @@ class RoleActionSelectorComponent {
|
|
|
1991
1526
|
</td>
|
|
1992
1527
|
<td>
|
|
1993
1528
|
<p-treeTableToggler [rowNode]="rowNode" />
|
|
1994
|
-
<span class="inline-flex
|
|
1529
|
+
<span class="inline-flex items-center gap-2">
|
|
1995
1530
|
{{ rowData.name }}
|
|
1996
1531
|
@if (hasUnmetPrerequisites(rowData)) {
|
|
1997
1532
|
<i
|
|
@@ -2002,7 +1537,7 @@ class RoleActionSelectorComponent {
|
|
|
2002
1537
|
}
|
|
2003
1538
|
</span>
|
|
2004
1539
|
</td>
|
|
2005
|
-
<td>{{ rowData.code || '-' }}</td>
|
|
1540
|
+
<td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
|
|
2006
1541
|
<td>
|
|
2007
1542
|
<p-tag
|
|
2008
1543
|
[value]="rowData.actionType"
|
|
@@ -2011,42 +1546,41 @@ class RoleActionSelectorComponent {
|
|
|
2011
1546
|
"
|
|
2012
1547
|
/>
|
|
2013
1548
|
</td>
|
|
2014
|
-
<td>
|
|
1549
|
+
<td class="hidden lg:table-cell">
|
|
2015
1550
|
@if (rowData.permissionLogic) {
|
|
2016
|
-
<span class="text-sm text-muted">Has prerequisites</span>
|
|
1551
|
+
<span class="text-sm text-muted-color">Has prerequisites</span>
|
|
2017
1552
|
} @else {
|
|
2018
|
-
<span class="text-muted">-</span>
|
|
1553
|
+
<span class="text-muted-color">-</span>
|
|
2019
1554
|
}
|
|
2020
1555
|
</td>
|
|
2021
1556
|
</tr>
|
|
2022
1557
|
</ng-template>
|
|
2023
|
-
<ng-template
|
|
1558
|
+
<ng-template #emptymessage>
|
|
2024
1559
|
<tr>
|
|
2025
|
-
<td colspan="5" class="text-center p-4">
|
|
1560
|
+
<td colspan="5" class="text-center p-4 text-muted-color">
|
|
2026
1561
|
@if (loading()) {
|
|
2027
1562
|
<i class="pi pi-spin pi-spinner"></i> Loading actions...
|
|
2028
1563
|
} @else { No actions available. }
|
|
2029
1564
|
</td>
|
|
2030
1565
|
</tr>
|
|
2031
1566
|
</ng-template>
|
|
2032
|
-
|
|
1567
|
+
</p-treeTable>
|
|
1568
|
+
</div>
|
|
2033
1569
|
</div>
|
|
2034
1570
|
|
|
2035
1571
|
<!-- Change Summary -->
|
|
2036
1572
|
@if (hasChanges()) {
|
|
2037
|
-
<div class="
|
|
2038
|
-
<div class="flex
|
|
2039
|
-
<i class="pi pi-info-circle text-
|
|
2040
|
-
<
|
|
1573
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
1574
|
+
<div class="flex items-center gap-2 mb-3">
|
|
1575
|
+
<i class="pi pi-info-circle text-primary"></i>
|
|
1576
|
+
<span class="font-bold">Pending Changes</span>
|
|
2041
1577
|
</div>
|
|
2042
|
-
<div class="
|
|
1578
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
2043
1579
|
@if (pendingAdd().length > 0) {
|
|
2044
|
-
<div
|
|
2045
|
-
<div class="flex
|
|
1580
|
+
<div>
|
|
1581
|
+
<div class="flex items-center gap-2 mb-2">
|
|
2046
1582
|
<i class="pi pi-plus-circle text-green-500"></i>
|
|
2047
|
-
<strong class="text-sm"
|
|
2048
|
-
>To Add ({{ pendingAdd().length }})</strong
|
|
2049
|
-
>
|
|
1583
|
+
<strong class="text-sm">To Add ({{ pendingAdd().length }})</strong>
|
|
2050
1584
|
</div>
|
|
2051
1585
|
<ul class="list-none p-0 m-0 pl-4">
|
|
2052
1586
|
@for (action of pendingAdd(); track action.id) {
|
|
@@ -2056,12 +1590,10 @@ class RoleActionSelectorComponent {
|
|
|
2056
1590
|
</div>
|
|
2057
1591
|
}
|
|
2058
1592
|
@if (pendingRemove().length > 0) {
|
|
2059
|
-
<div
|
|
2060
|
-
<div class="flex
|
|
1593
|
+
<div>
|
|
1594
|
+
<div class="flex items-center gap-2 mb-2">
|
|
2061
1595
|
<i class="pi pi-minus-circle text-red-500"></i>
|
|
2062
|
-
<strong class="text-sm"
|
|
2063
|
-
>To Remove ({{ pendingRemove().length }})</strong
|
|
2064
|
-
>
|
|
1596
|
+
<strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
|
|
2065
1597
|
</div>
|
|
2066
1598
|
<ul class="list-none p-0 m-0 pl-4">
|
|
2067
1599
|
@for (action of pendingRemove(); track action.id) {
|
|
@@ -2076,31 +1608,29 @@ class RoleActionSelectorComponent {
|
|
|
2076
1608
|
}
|
|
2077
1609
|
|
|
2078
1610
|
@if (!loading() && actions().length === 0) {
|
|
2079
|
-
<div class="surface-card p-5 border
|
|
2080
|
-
<i
|
|
2081
|
-
|
|
2082
|
-
style="font-size: 3rem; display: block;"
|
|
2083
|
-
></i>
|
|
2084
|
-
<p class="text-color-secondary m-0">
|
|
1611
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
1612
|
+
<i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
|
|
1613
|
+
<p class="text-muted-color m-0">
|
|
2085
1614
|
No actions available for this role.
|
|
2086
1615
|
</p>
|
|
2087
1616
|
</div>
|
|
2088
1617
|
}
|
|
2089
1618
|
}
|
|
2090
1619
|
</div>
|
|
2091
|
-
`, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "
|
|
1620
|
+
`, isInline: true, styles: [":host{display:block}.validation-warning{background-color:var(--p-yellow-50, #fefce8);border-left:4px solid var(--p-yellow-500, #eab308);color:var(--p-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--p-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--p-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i3.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "component", type: i4.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "component", type: i5.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "directive", type: i6.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "component", type: i7.TreeTable, selector: "p-treeTable, p-treetable, p-tree-table", inputs: ["columns", "styleClass", "tableStyle", "tableStyleClass", "autoLayout", "lazy", "lazyLoadOnInit", "paginator", "rows", "first", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "customSort", "selectionMode", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "compareSelectionBy", "rowHover", "loading", "loadingIcon", "showLoader", "scrollable", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "frozenColumns", "resizableColumns", "columnResizeMode", "reorderableColumns", "contextMenu", "rowTrackBy", "filters", "globalFilterFields", "filterDelay", "filterMode", "filterLocale", "paginatorLocale", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "value", "virtualRowHeight", "selectionKeys", "showGridlines"], outputs: ["selectionChange", "contextMenuSelectionChange", "onFilter", "onNodeExpand", "onNodeCollapse", "onPage", "onSort", "onLazyLoad", "sortFunction", "onColResize", "onColReorder", "onNodeSelect", "onNodeUnselect", "onContextMenuSelect", "onHeaderCheckboxToggle", "onEditInit", "onEditComplete", "onEditCancel", "selectionKeysChange"] }, { kind: "component", type: i7.TreeTableToggler, selector: "p-treeTableToggler, p-treetabletoggler, p-treetable-toggler", inputs: ["rowNode"] }, { kind: "directive", type: HasPermissionDirective, selector: "[hasPermission]", inputs: ["hasPermission"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2092
1621
|
}
|
|
2093
1622
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RoleActionSelectorComponent, decorators: [{
|
|
2094
1623
|
type: Component,
|
|
2095
|
-
args: [{ selector: 'flusys-role-action-selector',
|
|
1624
|
+
args: [{ selector: 'flusys-role-action-selector', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule, HasPermissionDirective], template: `
|
|
2096
1625
|
<div class="role-action-selector">
|
|
2097
1626
|
<!-- Role Selector -->
|
|
2098
1627
|
<div class="mb-4">
|
|
2099
|
-
<div class="grid">
|
|
2100
|
-
<div
|
|
1628
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
1629
|
+
<div>
|
|
2101
1630
|
<label class="block font-semibold mb-2">Select Role</label>
|
|
2102
1631
|
<p-select
|
|
2103
|
-
[
|
|
1632
|
+
[ngModel]="selectedRoleId()"
|
|
1633
|
+
(ngModelChange)="selectedRoleId.set($event)"
|
|
2104
1634
|
[options]="roles()"
|
|
2105
1635
|
optionLabel="name"
|
|
2106
1636
|
optionValue="id"
|
|
@@ -2117,24 +1647,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2117
1647
|
<!-- Loading State -->
|
|
2118
1648
|
@if (loading()) {
|
|
2119
1649
|
<div
|
|
2120
|
-
class="surface-card p-5 border
|
|
1650
|
+
class="surface-card p-5 rounded-border shadow-sm flex justify-center"
|
|
2121
1651
|
>
|
|
2122
|
-
<i
|
|
2123
|
-
class="pi pi-spin pi-spinner text-primary"
|
|
2124
|
-
style="font-size: 3rem"
|
|
2125
|
-
></i>
|
|
1652
|
+
<i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
|
|
2126
1653
|
</div>
|
|
2127
1654
|
}
|
|
2128
1655
|
|
|
2129
1656
|
<!-- Action List -->
|
|
2130
1657
|
@if (!loading() && actions().length > 0) {
|
|
2131
|
-
<div class="surface-card p-4 border
|
|
1658
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
2132
1659
|
<div
|
|
2133
|
-
class="flex flex-
|
|
1660
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
2134
1661
|
>
|
|
2135
1662
|
<div>
|
|
2136
1663
|
<h5 class="m-0 mb-1">Action Permissions</h5>
|
|
2137
|
-
<p class="text-sm text-color
|
|
1664
|
+
<p class="text-sm text-muted-color m-0">
|
|
2138
1665
|
{{ actions().length }} actions available
|
|
2139
1666
|
</p>
|
|
2140
1667
|
</div>
|
|
@@ -2156,6 +1683,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2156
1683
|
(onClick)="deselectAll()"
|
|
2157
1684
|
/>
|
|
2158
1685
|
<p-button
|
|
1686
|
+
*hasPermission="ROLE_ACTION_PERMISSIONS.ASSIGN"
|
|
2159
1687
|
label="Save Changes"
|
|
2160
1688
|
icon="pi pi-save"
|
|
2161
1689
|
[disabled]="!canSave()"
|
|
@@ -2169,14 +1697,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2169
1697
|
|
|
2170
1698
|
<!-- Validation Warning -->
|
|
2171
1699
|
@if (invalidActionsCount() > 0) {
|
|
2172
|
-
<div class="
|
|
2173
|
-
<div class="flex items-
|
|
2174
|
-
<i
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
<strong class="text-orange-800">Validation Warning:</strong>
|
|
2179
|
-
<p class="text-sm text-orange-700 mt-1">
|
|
1700
|
+
<div class="validation-warning rounded-border p-3 mb-4">
|
|
1701
|
+
<div class="flex items-start gap-2">
|
|
1702
|
+
<i class="pi pi-exclamation-triangle text-xl"></i>
|
|
1703
|
+
<div class="flex-1">
|
|
1704
|
+
<span class="font-semibold">Validation Warning:</span>
|
|
1705
|
+
<p class="text-sm mt-1 mb-0">
|
|
2180
1706
|
{{ invalidActionsCount() }} selected
|
|
2181
1707
|
action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
|
|
2182
1708
|
unmet prerequisites. Fix before saving or use auto-fix on
|
|
@@ -2188,33 +1714,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2188
1714
|
}
|
|
2189
1715
|
|
|
2190
1716
|
<!-- Action Tree Table -->
|
|
2191
|
-
<
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
<ng-template
|
|
2216
|
-
<tr [class.
|
|
2217
|
-
<td
|
|
1717
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
1718
|
+
<p-treeTable
|
|
1719
|
+
[value]="treeNodes()"
|
|
1720
|
+
dataKey="id"
|
|
1721
|
+
styleClass="p-treetable-sm"
|
|
1722
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
1723
|
+
>
|
|
1724
|
+
<ng-template #header>
|
|
1725
|
+
<tr>
|
|
1726
|
+
<th class="w-12">
|
|
1727
|
+
<p-checkbox
|
|
1728
|
+
[ngModel]="allSelected()"
|
|
1729
|
+
[binary]="true"
|
|
1730
|
+
(ngModelChange)="toggleAll()"
|
|
1731
|
+
pTooltip="Select/Deselect All"
|
|
1732
|
+
tooltipPosition="top"
|
|
1733
|
+
/>
|
|
1734
|
+
</th>
|
|
1735
|
+
<th>Name</th>
|
|
1736
|
+
<th class="hidden md:table-cell">Code</th>
|
|
1737
|
+
<th>Type</th>
|
|
1738
|
+
<th class="hidden lg:table-cell">Requirements</th>
|
|
1739
|
+
</tr>
|
|
1740
|
+
</ng-template>
|
|
1741
|
+
<ng-template #body let-rowNode let-rowData="rowData">
|
|
1742
|
+
<tr [class.highlight-warning]="hasUnmetPrerequisites(rowData)">
|
|
1743
|
+
<td class="w-12">
|
|
2218
1744
|
<p-checkbox
|
|
2219
1745
|
[ngModel]="selectionMap()[rowData.id]"
|
|
2220
1746
|
[binary]="true"
|
|
@@ -2225,7 +1751,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2225
1751
|
</td>
|
|
2226
1752
|
<td>
|
|
2227
1753
|
<p-treeTableToggler [rowNode]="rowNode" />
|
|
2228
|
-
<span class="inline-flex
|
|
1754
|
+
<span class="inline-flex items-center gap-2">
|
|
2229
1755
|
{{ rowData.name }}
|
|
2230
1756
|
@if (hasUnmetPrerequisites(rowData)) {
|
|
2231
1757
|
<i
|
|
@@ -2236,7 +1762,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2236
1762
|
}
|
|
2237
1763
|
</span>
|
|
2238
1764
|
</td>
|
|
2239
|
-
<td>{{ rowData.code || '-' }}</td>
|
|
1765
|
+
<td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
|
|
2240
1766
|
<td>
|
|
2241
1767
|
<p-tag
|
|
2242
1768
|
[value]="rowData.actionType"
|
|
@@ -2245,42 +1771,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2245
1771
|
"
|
|
2246
1772
|
/>
|
|
2247
1773
|
</td>
|
|
2248
|
-
<td>
|
|
1774
|
+
<td class="hidden lg:table-cell">
|
|
2249
1775
|
@if (rowData.permissionLogic) {
|
|
2250
|
-
<span class="text-sm text-muted">Has prerequisites</span>
|
|
1776
|
+
<span class="text-sm text-muted-color">Has prerequisites</span>
|
|
2251
1777
|
} @else {
|
|
2252
|
-
<span class="text-muted">-</span>
|
|
1778
|
+
<span class="text-muted-color">-</span>
|
|
2253
1779
|
}
|
|
2254
1780
|
</td>
|
|
2255
1781
|
</tr>
|
|
2256
1782
|
</ng-template>
|
|
2257
|
-
<ng-template
|
|
1783
|
+
<ng-template #emptymessage>
|
|
2258
1784
|
<tr>
|
|
2259
|
-
<td colspan="5" class="text-center p-4">
|
|
1785
|
+
<td colspan="5" class="text-center p-4 text-muted-color">
|
|
2260
1786
|
@if (loading()) {
|
|
2261
1787
|
<i class="pi pi-spin pi-spinner"></i> Loading actions...
|
|
2262
1788
|
} @else { No actions available. }
|
|
2263
1789
|
</td>
|
|
2264
1790
|
</tr>
|
|
2265
1791
|
</ng-template>
|
|
2266
|
-
|
|
1792
|
+
</p-treeTable>
|
|
1793
|
+
</div>
|
|
2267
1794
|
</div>
|
|
2268
1795
|
|
|
2269
1796
|
<!-- Change Summary -->
|
|
2270
1797
|
@if (hasChanges()) {
|
|
2271
|
-
<div class="
|
|
2272
|
-
<div class="flex
|
|
2273
|
-
<i class="pi pi-info-circle text-
|
|
2274
|
-
<
|
|
1798
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
1799
|
+
<div class="flex items-center gap-2 mb-3">
|
|
1800
|
+
<i class="pi pi-info-circle text-primary"></i>
|
|
1801
|
+
<span class="font-bold">Pending Changes</span>
|
|
2275
1802
|
</div>
|
|
2276
|
-
<div class="
|
|
1803
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
2277
1804
|
@if (pendingAdd().length > 0) {
|
|
2278
|
-
<div
|
|
2279
|
-
<div class="flex
|
|
1805
|
+
<div>
|
|
1806
|
+
<div class="flex items-center gap-2 mb-2">
|
|
2280
1807
|
<i class="pi pi-plus-circle text-green-500"></i>
|
|
2281
|
-
<strong class="text-sm"
|
|
2282
|
-
>To Add ({{ pendingAdd().length }})</strong
|
|
2283
|
-
>
|
|
1808
|
+
<strong class="text-sm">To Add ({{ pendingAdd().length }})</strong>
|
|
2284
1809
|
</div>
|
|
2285
1810
|
<ul class="list-none p-0 m-0 pl-4">
|
|
2286
1811
|
@for (action of pendingAdd(); track action.id) {
|
|
@@ -2290,12 +1815,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2290
1815
|
</div>
|
|
2291
1816
|
}
|
|
2292
1817
|
@if (pendingRemove().length > 0) {
|
|
2293
|
-
<div
|
|
2294
|
-
<div class="flex
|
|
1818
|
+
<div>
|
|
1819
|
+
<div class="flex items-center gap-2 mb-2">
|
|
2295
1820
|
<i class="pi pi-minus-circle text-red-500"></i>
|
|
2296
|
-
<strong class="text-sm"
|
|
2297
|
-
>To Remove ({{ pendingRemove().length }})</strong
|
|
2298
|
-
>
|
|
1821
|
+
<strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
|
|
2299
1822
|
</div>
|
|
2300
1823
|
<ul class="list-none p-0 m-0 pl-4">
|
|
2301
1824
|
@for (action of pendingRemove(); track action.id) {
|
|
@@ -2310,19 +1833,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2310
1833
|
}
|
|
2311
1834
|
|
|
2312
1835
|
@if (!loading() && actions().length === 0) {
|
|
2313
|
-
<div class="surface-card p-5 border
|
|
2314
|
-
<i
|
|
2315
|
-
|
|
2316
|
-
style="font-size: 3rem; display: block;"
|
|
2317
|
-
></i>
|
|
2318
|
-
<p class="text-color-secondary m-0">
|
|
1836
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
1837
|
+
<i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
|
|
1838
|
+
<p class="text-muted-color m-0">
|
|
2319
1839
|
No actions available for this role.
|
|
2320
1840
|
</p>
|
|
2321
1841
|
</div>
|
|
2322
1842
|
}
|
|
2323
1843
|
}
|
|
2324
1844
|
</div>
|
|
2325
|
-
`, styles: [":host{display:block}\n"] }]
|
|
1845
|
+
`, styles: [":host{display:block}.validation-warning{background-color:var(--p-yellow-50, #fefce8);border-left:4px solid var(--p-yellow-500, #eab308);color:var(--p-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--p-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--p-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"] }]
|
|
2326
1846
|
}], ctorParameters: () => [] });
|
|
2327
1847
|
|
|
2328
1848
|
/**
|
|
@@ -2360,6 +1880,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2360
1880
|
* ```
|
|
2361
1881
|
*/
|
|
2362
1882
|
class CompanyActionSelectorComponent {
|
|
1883
|
+
// Permission constants for template
|
|
1884
|
+
COMPANY_ACTION_PERMISSIONS = COMPANY_ACTION_PERMISSIONS;
|
|
2363
1885
|
// Dependencies
|
|
2364
1886
|
companyApiProvider = inject(COMPANY_API_PROVIDER);
|
|
2365
1887
|
actionApi = inject(ActionApiService);
|
|
@@ -2367,6 +1889,7 @@ class CompanyActionSelectorComponent {
|
|
|
2367
1889
|
messageService = inject(MessageService);
|
|
2368
1890
|
confirmationService = inject(ConfirmationService);
|
|
2369
1891
|
permissionLogic = inject(ActionPermissionLogicService);
|
|
1892
|
+
destroyRef = inject(DestroyRef);
|
|
2370
1893
|
// State - Company Selection
|
|
2371
1894
|
selectedCompanyId = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedCompanyId" }] : []));
|
|
2372
1895
|
companies = signal([], ...(ngDevMode ? [{ debugName: "companies" }] : []));
|
|
@@ -2410,17 +1933,14 @@ class CompanyActionSelectorComponent {
|
|
|
2410
1933
|
const selMap = this.selectionMap();
|
|
2411
1934
|
const allActions = this.actions();
|
|
2412
1935
|
const unmetSet = new Set();
|
|
2413
|
-
|
|
2414
|
-
if (action.id &&
|
|
2415
|
-
this.permissionLogic.hasUnmetPrerequisites(action, selMap, allActions)) {
|
|
1936
|
+
for (const action of allActions) {
|
|
1937
|
+
if (action.id && this.permissionLogic.hasUnmetPrerequisites(action, selMap, allActions)) {
|
|
2416
1938
|
unmetSet.add(action.id);
|
|
2417
1939
|
}
|
|
2418
|
-
}
|
|
1940
|
+
}
|
|
2419
1941
|
return unmetSet;
|
|
2420
1942
|
}, ...(ngDevMode ? [{ debugName: "actionsWithUnmetPrerequisites" }] : []));
|
|
2421
|
-
invalidActionsCount = computed(() => {
|
|
2422
|
-
return this.actionsWithUnmetPrerequisites().size;
|
|
2423
|
-
}, ...(ngDevMode ? [{ debugName: "invalidActionsCount" }] : []));
|
|
1943
|
+
invalidActionsCount = computed(() => this.actionsWithUnmetPrerequisites().size, ...(ngDevMode ? [{ debugName: "invalidActionsCount" }] : []));
|
|
2424
1944
|
// Computed - Pending Changes
|
|
2425
1945
|
pendingAdd = computed(() => {
|
|
2426
1946
|
const current = this.selectionMap();
|
|
@@ -2441,6 +1961,10 @@ class CompanyActionSelectorComponent {
|
|
|
2441
1961
|
// AbortController for data loading
|
|
2442
1962
|
loadDataAbortController = null;
|
|
2443
1963
|
constructor() {
|
|
1964
|
+
// Cleanup on destroy
|
|
1965
|
+
this.destroyRef.onDestroy(() => {
|
|
1966
|
+
this.loadDataAbortController?.abort();
|
|
1967
|
+
});
|
|
2444
1968
|
this.loadCompanies();
|
|
2445
1969
|
// Effect: Load data when company selection changes
|
|
2446
1970
|
effect(() => {
|
|
@@ -2451,11 +1975,7 @@ class CompanyActionSelectorComponent {
|
|
|
2451
1975
|
this.loadDataAbortController = new AbortController();
|
|
2452
1976
|
this.loadData(this.loadDataAbortController.signal).catch((err) => {
|
|
2453
1977
|
if (err.name !== 'AbortError') {
|
|
2454
|
-
|
|
2455
|
-
severity: 'error',
|
|
2456
|
-
summary: 'Error',
|
|
2457
|
-
detail: 'Failed to load company actions. Please refresh.',
|
|
2458
|
-
});
|
|
1978
|
+
// Error toast handled by global interceptor
|
|
2459
1979
|
this.loading.set(false);
|
|
2460
1980
|
}
|
|
2461
1981
|
});
|
|
@@ -2465,9 +1985,6 @@ class CompanyActionSelectorComponent {
|
|
|
2465
1985
|
}
|
|
2466
1986
|
});
|
|
2467
1987
|
}
|
|
2468
|
-
ngOnDestroy() {
|
|
2469
|
-
this.loadDataAbortController?.abort();
|
|
2470
|
-
}
|
|
2471
1988
|
/**
|
|
2472
1989
|
* Load companies from API
|
|
2473
1990
|
*/
|
|
@@ -2480,11 +1997,7 @@ class CompanyActionSelectorComponent {
|
|
|
2480
1997
|
this.companies.set(response?.data ?? []);
|
|
2481
1998
|
}
|
|
2482
1999
|
catch {
|
|
2483
|
-
|
|
2484
|
-
severity: 'error',
|
|
2485
|
-
summary: 'Error',
|
|
2486
|
-
detail: 'Failed to load companies',
|
|
2487
|
-
});
|
|
2000
|
+
// Error toast handled by global interceptor
|
|
2488
2001
|
}
|
|
2489
2002
|
}
|
|
2490
2003
|
/**
|
|
@@ -2511,13 +2024,7 @@ class CompanyActionSelectorComponent {
|
|
|
2511
2024
|
if (signal.aborted)
|
|
2512
2025
|
return;
|
|
2513
2026
|
const assignedIds = new Set((assignedResponse?.data || []).map((a) => a.actionId));
|
|
2514
|
-
|
|
2515
|
-
const selMap = {};
|
|
2516
|
-
actionsList.forEach((action) => {
|
|
2517
|
-
if (action.id) {
|
|
2518
|
-
selMap[action.id] = assignedIds.has(action.id);
|
|
2519
|
-
}
|
|
2520
|
-
});
|
|
2027
|
+
const selMap = this.buildSelectionMap(actionsList, (action) => assignedIds.has(action.id));
|
|
2521
2028
|
this._selectionMap.set(selMap);
|
|
2522
2029
|
this._initialSelection.set({ ...selMap });
|
|
2523
2030
|
}
|
|
@@ -2568,36 +2075,17 @@ class CompanyActionSelectorComponent {
|
|
|
2568
2075
|
this.permissionLogic.handleCheck(action, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap), (previousState) => this._selectionMap.set(previousState));
|
|
2569
2076
|
}
|
|
2570
2077
|
}
|
|
2571
|
-
/**
|
|
2572
|
-
* Toggle all actions
|
|
2573
|
-
*/
|
|
2574
2078
|
toggleAll() {
|
|
2575
|
-
|
|
2576
|
-
const selMap = {};
|
|
2577
|
-
this.actions().forEach((action) => {
|
|
2578
|
-
selMap[action.id] = newValue;
|
|
2579
|
-
});
|
|
2580
|
-
this._selectionMap.set(selMap);
|
|
2079
|
+
this.setAllSelection(!this.allSelected());
|
|
2581
2080
|
}
|
|
2582
|
-
/**
|
|
2583
|
-
* Select all actions
|
|
2584
|
-
*/
|
|
2585
2081
|
selectAll() {
|
|
2586
|
-
|
|
2587
|
-
this.actions().forEach((action) => {
|
|
2588
|
-
selMap[action.id] = true;
|
|
2589
|
-
});
|
|
2590
|
-
this._selectionMap.set(selMap);
|
|
2082
|
+
this.setAllSelection(true);
|
|
2591
2083
|
}
|
|
2592
|
-
/**
|
|
2593
|
-
* Deselect all actions
|
|
2594
|
-
*/
|
|
2595
2084
|
deselectAll() {
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
this._selectionMap.set(selMap);
|
|
2085
|
+
this.setAllSelection(false);
|
|
2086
|
+
}
|
|
2087
|
+
setAllSelection(value) {
|
|
2088
|
+
this._selectionMap.set(this.buildSelectionMap(this.actions(), () => value));
|
|
2601
2089
|
}
|
|
2602
2090
|
/**
|
|
2603
2091
|
* Save changes to backend
|
|
@@ -2612,20 +2100,7 @@ class CompanyActionSelectorComponent {
|
|
|
2612
2100
|
this.permissionLogic.showValidationErrorDialog(invalidActions, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
|
|
2613
2101
|
return;
|
|
2614
2102
|
}
|
|
2615
|
-
|
|
2616
|
-
const items = [];
|
|
2617
|
-
this.pendingAdd().forEach((action) => {
|
|
2618
|
-
items.push({
|
|
2619
|
-
id: action.id,
|
|
2620
|
-
action: 'add',
|
|
2621
|
-
});
|
|
2622
|
-
});
|
|
2623
|
-
this.pendingRemove().forEach((action) => {
|
|
2624
|
-
items.push({
|
|
2625
|
-
id: action.id,
|
|
2626
|
-
action: 'remove',
|
|
2627
|
-
});
|
|
2628
|
-
});
|
|
2103
|
+
const items = this.buildPayloadItems();
|
|
2629
2104
|
if (items.length === 0)
|
|
2630
2105
|
return;
|
|
2631
2106
|
this.saving.set(true);
|
|
@@ -2649,13 +2124,8 @@ class CompanyActionSelectorComponent {
|
|
|
2649
2124
|
// Update baseline
|
|
2650
2125
|
this._initialSelection.set({ ...this.selectionMap() });
|
|
2651
2126
|
}
|
|
2652
|
-
catch
|
|
2653
|
-
|
|
2654
|
-
this.messageService.add({
|
|
2655
|
-
severity: 'error',
|
|
2656
|
-
summary: 'Error',
|
|
2657
|
-
detail: error?.error?.message || 'Failed to update company action whitelist',
|
|
2658
|
-
});
|
|
2127
|
+
catch {
|
|
2128
|
+
// Error toast handled by global interceptor
|
|
2659
2129
|
}
|
|
2660
2130
|
finally {
|
|
2661
2131
|
this.saving.set(false);
|
|
@@ -2708,25 +2178,40 @@ class CompanyActionSelectorComponent {
|
|
|
2708
2178
|
},
|
|
2709
2179
|
});
|
|
2710
2180
|
}
|
|
2711
|
-
/**
|
|
2712
|
-
* Reset component state
|
|
2713
|
-
*/
|
|
2714
2181
|
resetState() {
|
|
2715
2182
|
this._actionsTree.set([]);
|
|
2716
2183
|
this._actions.set([]);
|
|
2717
2184
|
this._selectionMap.set({});
|
|
2718
2185
|
this._initialSelection.set({});
|
|
2719
2186
|
}
|
|
2187
|
+
buildSelectionMap(actions, predicate) {
|
|
2188
|
+
const selMap = {};
|
|
2189
|
+
for (const action of actions) {
|
|
2190
|
+
selMap[action.id] = predicate(action);
|
|
2191
|
+
}
|
|
2192
|
+
return selMap;
|
|
2193
|
+
}
|
|
2194
|
+
buildPayloadItems() {
|
|
2195
|
+
const items = [];
|
|
2196
|
+
for (const action of this.pendingAdd()) {
|
|
2197
|
+
items.push({ id: action.id, action: 'add' });
|
|
2198
|
+
}
|
|
2199
|
+
for (const action of this.pendingRemove()) {
|
|
2200
|
+
items.push({ id: action.id, action: 'remove' });
|
|
2201
|
+
}
|
|
2202
|
+
return items;
|
|
2203
|
+
}
|
|
2720
2204
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CompanyActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2721
2205
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CompanyActionSelectorComponent, isStandalone: true, selector: "flusys-company-action-selector", ngImport: i0, template: `
|
|
2722
2206
|
<div class="company-action-selector">
|
|
2723
2207
|
<!-- Company Selector -->
|
|
2724
2208
|
<div class="mb-4">
|
|
2725
|
-
<div class="grid">
|
|
2726
|
-
<div
|
|
2209
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
2210
|
+
<div>
|
|
2727
2211
|
<label class="block font-semibold mb-2">Select Company</label>
|
|
2728
2212
|
<p-select
|
|
2729
|
-
[
|
|
2213
|
+
[ngModel]="selectedCompanyId()"
|
|
2214
|
+
(ngModelChange)="selectedCompanyId.set($event)"
|
|
2730
2215
|
[options]="companies()"
|
|
2731
2216
|
optionLabel="name"
|
|
2732
2217
|
optionValue="id"
|
|
@@ -2743,24 +2228,21 @@ class CompanyActionSelectorComponent {
|
|
|
2743
2228
|
<!-- Loading State -->
|
|
2744
2229
|
@if (loading()) {
|
|
2745
2230
|
<div
|
|
2746
|
-
class="surface-card p-5 border
|
|
2231
|
+
class="surface-card p-5 rounded-border shadow-sm flex justify-center"
|
|
2747
2232
|
>
|
|
2748
|
-
<i
|
|
2749
|
-
class="pi pi-spin pi-spinner text-primary"
|
|
2750
|
-
style="font-size: 3rem"
|
|
2751
|
-
></i>
|
|
2233
|
+
<i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
|
|
2752
2234
|
</div>
|
|
2753
2235
|
}
|
|
2754
2236
|
|
|
2755
2237
|
<!-- Action List -->
|
|
2756
2238
|
@if (!loading() && actions().length > 0) {
|
|
2757
|
-
<div class="surface-card p-4 border
|
|
2239
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
2758
2240
|
<div
|
|
2759
|
-
class="flex flex-
|
|
2241
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
2760
2242
|
>
|
|
2761
2243
|
<div>
|
|
2762
2244
|
<h5 class="m-0 mb-1">Action Whitelist</h5>
|
|
2763
|
-
<p class="text-sm text-color
|
|
2245
|
+
<p class="text-sm text-muted-color m-0">
|
|
2764
2246
|
{{ actions().length }} actions available
|
|
2765
2247
|
</p>
|
|
2766
2248
|
</div>
|
|
@@ -2782,6 +2264,7 @@ class CompanyActionSelectorComponent {
|
|
|
2782
2264
|
(onClick)="deselectAll()"
|
|
2783
2265
|
/>
|
|
2784
2266
|
<p-button
|
|
2267
|
+
*hasPermission="COMPANY_ACTION_PERMISSIONS.ASSIGN"
|
|
2785
2268
|
label="Save Changes"
|
|
2786
2269
|
icon="pi pi-save"
|
|
2787
2270
|
[disabled]="!canSave()"
|
|
@@ -2795,14 +2278,12 @@ class CompanyActionSelectorComponent {
|
|
|
2795
2278
|
|
|
2796
2279
|
<!-- Validation Warning -->
|
|
2797
2280
|
@if (invalidActionsCount() > 0) {
|
|
2798
|
-
<div class="
|
|
2799
|
-
<div class="flex items-
|
|
2800
|
-
<i
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
<strong class="text-orange-800">Validation Warning:</strong>
|
|
2805
|
-
<p class="text-sm text-orange-700 mt-1">
|
|
2281
|
+
<div class="validation-warning rounded-border p-3 mb-4">
|
|
2282
|
+
<div class="flex items-start gap-2">
|
|
2283
|
+
<i class="pi pi-exclamation-triangle text-xl"></i>
|
|
2284
|
+
<div class="flex-1">
|
|
2285
|
+
<span class="font-semibold">Validation Warning:</span>
|
|
2286
|
+
<p class="text-sm mt-1 mb-0">
|
|
2806
2287
|
{{ invalidActionsCount() }} selected
|
|
2807
2288
|
action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
|
|
2808
2289
|
unmet prerequisites. Fix before saving or use auto-fix on
|
|
@@ -2814,33 +2295,33 @@ class CompanyActionSelectorComponent {
|
|
|
2814
2295
|
}
|
|
2815
2296
|
|
|
2816
2297
|
<!-- Action Tree Table -->
|
|
2817
|
-
<
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
<ng-template
|
|
2842
|
-
<tr [class.
|
|
2843
|
-
<td
|
|
2298
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
2299
|
+
<p-treeTable
|
|
2300
|
+
[value]="treeNodes()"
|
|
2301
|
+
dataKey="id"
|
|
2302
|
+
styleClass="p-treetable-sm"
|
|
2303
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
2304
|
+
>
|
|
2305
|
+
<ng-template #header>
|
|
2306
|
+
<tr>
|
|
2307
|
+
<th class="w-12">
|
|
2308
|
+
<p-checkbox
|
|
2309
|
+
[ngModel]="allSelected()"
|
|
2310
|
+
[binary]="true"
|
|
2311
|
+
(ngModelChange)="toggleAll()"
|
|
2312
|
+
pTooltip="Select/Deselect All"
|
|
2313
|
+
tooltipPosition="top"
|
|
2314
|
+
/>
|
|
2315
|
+
</th>
|
|
2316
|
+
<th>Name</th>
|
|
2317
|
+
<th class="hidden md:table-cell">Code</th>
|
|
2318
|
+
<th>Type</th>
|
|
2319
|
+
<th class="hidden lg:table-cell">Description</th>
|
|
2320
|
+
</tr>
|
|
2321
|
+
</ng-template>
|
|
2322
|
+
<ng-template #body let-rowNode let-rowData="rowData">
|
|
2323
|
+
<tr [class.highlight-warning]="hasUnmetPrerequisites(rowData)">
|
|
2324
|
+
<td class="w-12">
|
|
2844
2325
|
<p-checkbox
|
|
2845
2326
|
[ngModel]="selectionMap()[rowData.id]"
|
|
2846
2327
|
[binary]="true"
|
|
@@ -2851,7 +2332,7 @@ class CompanyActionSelectorComponent {
|
|
|
2851
2332
|
</td>
|
|
2852
2333
|
<td>
|
|
2853
2334
|
<p-treeTableToggler [rowNode]="rowNode" />
|
|
2854
|
-
<span class="inline-flex
|
|
2335
|
+
<span class="inline-flex items-center gap-2">
|
|
2855
2336
|
{{ rowData.name }}
|
|
2856
2337
|
@if (hasUnmetPrerequisites(rowData)) {
|
|
2857
2338
|
<i
|
|
@@ -2862,7 +2343,7 @@ class CompanyActionSelectorComponent {
|
|
|
2862
2343
|
}
|
|
2863
2344
|
</span>
|
|
2864
2345
|
</td>
|
|
2865
|
-
<td>{{ rowData.code || '-' }}</td>
|
|
2346
|
+
<td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
|
|
2866
2347
|
<td>
|
|
2867
2348
|
<p-tag
|
|
2868
2349
|
[value]="rowData.actionType"
|
|
@@ -2871,36 +2352,35 @@ class CompanyActionSelectorComponent {
|
|
|
2871
2352
|
"
|
|
2872
2353
|
/>
|
|
2873
2354
|
</td>
|
|
2874
|
-
<td>{{ rowData.description || '-' }}</td>
|
|
2355
|
+
<td class="hidden lg:table-cell">{{ rowData.description || '-' }}</td>
|
|
2875
2356
|
</tr>
|
|
2876
2357
|
</ng-template>
|
|
2877
|
-
<ng-template
|
|
2358
|
+
<ng-template #emptymessage>
|
|
2878
2359
|
<tr>
|
|
2879
|
-
<td colspan="5" class="text-center p-4">
|
|
2360
|
+
<td colspan="5" class="text-center p-4 text-muted-color">
|
|
2880
2361
|
@if (loading()) {
|
|
2881
2362
|
<i class="pi pi-spin pi-spinner"></i> Loading actions...
|
|
2882
2363
|
} @else { No actions available. }
|
|
2883
2364
|
</td>
|
|
2884
2365
|
</tr>
|
|
2885
2366
|
</ng-template>
|
|
2886
|
-
|
|
2367
|
+
</p-treeTable>
|
|
2368
|
+
</div>
|
|
2887
2369
|
</div>
|
|
2888
2370
|
|
|
2889
2371
|
<!-- Change Summary -->
|
|
2890
2372
|
@if (hasChanges()) {
|
|
2891
|
-
<div class="
|
|
2892
|
-
<div class="flex
|
|
2893
|
-
<i class="pi pi-info-circle text-
|
|
2894
|
-
<
|
|
2373
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
2374
|
+
<div class="flex items-center gap-2 mb-3">
|
|
2375
|
+
<i class="pi pi-info-circle text-primary"></i>
|
|
2376
|
+
<span class="font-bold">Pending Changes</span>
|
|
2895
2377
|
</div>
|
|
2896
|
-
<div class="
|
|
2378
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
2897
2379
|
@if (pendingAdd().length > 0) {
|
|
2898
|
-
<div
|
|
2380
|
+
<div>
|
|
2899
2381
|
<div class="flex items-center gap-2 mb-2">
|
|
2900
2382
|
<i class="pi pi-plus-circle text-green-500"></i>
|
|
2901
|
-
<strong class="text-sm">
|
|
2902
|
-
To Whitelist ({{ pendingAdd().length }})
|
|
2903
|
-
</strong>
|
|
2383
|
+
<strong class="text-sm">To Whitelist ({{ pendingAdd().length }})</strong>
|
|
2904
2384
|
</div>
|
|
2905
2385
|
<ul class="list-none p-0 m-0 pl-4">
|
|
2906
2386
|
@for (action of pendingAdd(); track action.id) {
|
|
@@ -2909,14 +2389,11 @@ class CompanyActionSelectorComponent {
|
|
|
2909
2389
|
</ul>
|
|
2910
2390
|
</div>
|
|
2911
2391
|
}
|
|
2912
|
-
|
|
2913
2392
|
@if (pendingRemove().length > 0) {
|
|
2914
|
-
<div
|
|
2393
|
+
<div>
|
|
2915
2394
|
<div class="flex items-center gap-2 mb-2">
|
|
2916
2395
|
<i class="pi pi-minus-circle text-red-500"></i>
|
|
2917
|
-
<strong class="text-sm">
|
|
2918
|
-
To Remove ({{ pendingRemove().length }})
|
|
2919
|
-
</strong>
|
|
2396
|
+
<strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
|
|
2920
2397
|
</div>
|
|
2921
2398
|
<ul class="list-none p-0 m-0 pl-4">
|
|
2922
2399
|
@for (action of pendingRemove(); track action.id) {
|
|
@@ -2931,31 +2408,29 @@ class CompanyActionSelectorComponent {
|
|
|
2931
2408
|
}
|
|
2932
2409
|
|
|
2933
2410
|
@if (!loading() && actions().length === 0) {
|
|
2934
|
-
<div class="surface-card p-5 border
|
|
2935
|
-
<i
|
|
2936
|
-
|
|
2937
|
-
style="font-size: 3rem; display: block;"
|
|
2938
|
-
></i>
|
|
2939
|
-
<p class="text-color-secondary m-0">
|
|
2411
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
2412
|
+
<i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
|
|
2413
|
+
<p class="text-muted-color m-0">
|
|
2940
2414
|
No actions available for this company.
|
|
2941
2415
|
</p>
|
|
2942
2416
|
</div>
|
|
2943
2417
|
}
|
|
2944
2418
|
}
|
|
2945
2419
|
</div>
|
|
2946
|
-
`, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "
|
|
2420
|
+
`, isInline: true, styles: [":host{display:block}.validation-warning{background-color:var(--p-yellow-50, #fefce8);border-left:4px solid var(--p-yellow-500, #eab308);color:var(--p-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--p-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--p-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i3.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "component", type: i4.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "component", type: i5.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "directive", type: i6.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "component", type: i7.TreeTable, selector: "p-treeTable, p-treetable, p-tree-table", inputs: ["columns", "styleClass", "tableStyle", "tableStyleClass", "autoLayout", "lazy", "lazyLoadOnInit", "paginator", "rows", "first", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "customSort", "selectionMode", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "compareSelectionBy", "rowHover", "loading", "loadingIcon", "showLoader", "scrollable", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "frozenColumns", "resizableColumns", "columnResizeMode", "reorderableColumns", "contextMenu", "rowTrackBy", "filters", "globalFilterFields", "filterDelay", "filterMode", "filterLocale", "paginatorLocale", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "value", "virtualRowHeight", "selectionKeys", "showGridlines"], outputs: ["selectionChange", "contextMenuSelectionChange", "onFilter", "onNodeExpand", "onNodeCollapse", "onPage", "onSort", "onLazyLoad", "sortFunction", "onColResize", "onColReorder", "onNodeSelect", "onNodeUnselect", "onContextMenuSelect", "onHeaderCheckboxToggle", "onEditInit", "onEditComplete", "onEditCancel", "selectionKeysChange"] }, { kind: "component", type: i7.TreeTableToggler, selector: "p-treeTableToggler, p-treetabletoggler, p-treetable-toggler", inputs: ["rowNode"] }, { kind: "directive", type: HasPermissionDirective, selector: "[hasPermission]", inputs: ["hasPermission"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2947
2421
|
}
|
|
2948
2422
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CompanyActionSelectorComponent, decorators: [{
|
|
2949
2423
|
type: Component,
|
|
2950
|
-
args: [{ selector: 'flusys-company-action-selector',
|
|
2424
|
+
args: [{ selector: 'flusys-company-action-selector', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule, HasPermissionDirective], template: `
|
|
2951
2425
|
<div class="company-action-selector">
|
|
2952
2426
|
<!-- Company Selector -->
|
|
2953
2427
|
<div class="mb-4">
|
|
2954
|
-
<div class="grid">
|
|
2955
|
-
<div
|
|
2428
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
2429
|
+
<div>
|
|
2956
2430
|
<label class="block font-semibold mb-2">Select Company</label>
|
|
2957
2431
|
<p-select
|
|
2958
|
-
[
|
|
2432
|
+
[ngModel]="selectedCompanyId()"
|
|
2433
|
+
(ngModelChange)="selectedCompanyId.set($event)"
|
|
2959
2434
|
[options]="companies()"
|
|
2960
2435
|
optionLabel="name"
|
|
2961
2436
|
optionValue="id"
|
|
@@ -2972,24 +2447,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2972
2447
|
<!-- Loading State -->
|
|
2973
2448
|
@if (loading()) {
|
|
2974
2449
|
<div
|
|
2975
|
-
class="surface-card p-5 border
|
|
2450
|
+
class="surface-card p-5 rounded-border shadow-sm flex justify-center"
|
|
2976
2451
|
>
|
|
2977
|
-
<i
|
|
2978
|
-
class="pi pi-spin pi-spinner text-primary"
|
|
2979
|
-
style="font-size: 3rem"
|
|
2980
|
-
></i>
|
|
2452
|
+
<i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
|
|
2981
2453
|
</div>
|
|
2982
2454
|
}
|
|
2983
2455
|
|
|
2984
2456
|
<!-- Action List -->
|
|
2985
2457
|
@if (!loading() && actions().length > 0) {
|
|
2986
|
-
<div class="surface-card p-4 border
|
|
2458
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
2987
2459
|
<div
|
|
2988
|
-
class="flex flex-
|
|
2460
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
2989
2461
|
>
|
|
2990
2462
|
<div>
|
|
2991
2463
|
<h5 class="m-0 mb-1">Action Whitelist</h5>
|
|
2992
|
-
<p class="text-sm text-color
|
|
2464
|
+
<p class="text-sm text-muted-color m-0">
|
|
2993
2465
|
{{ actions().length }} actions available
|
|
2994
2466
|
</p>
|
|
2995
2467
|
</div>
|
|
@@ -3011,6 +2483,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3011
2483
|
(onClick)="deselectAll()"
|
|
3012
2484
|
/>
|
|
3013
2485
|
<p-button
|
|
2486
|
+
*hasPermission="COMPANY_ACTION_PERMISSIONS.ASSIGN"
|
|
3014
2487
|
label="Save Changes"
|
|
3015
2488
|
icon="pi pi-save"
|
|
3016
2489
|
[disabled]="!canSave()"
|
|
@@ -3024,14 +2497,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3024
2497
|
|
|
3025
2498
|
<!-- Validation Warning -->
|
|
3026
2499
|
@if (invalidActionsCount() > 0) {
|
|
3027
|
-
<div class="
|
|
3028
|
-
<div class="flex items-
|
|
3029
|
-
<i
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
<strong class="text-orange-800">Validation Warning:</strong>
|
|
3034
|
-
<p class="text-sm text-orange-700 mt-1">
|
|
2500
|
+
<div class="validation-warning rounded-border p-3 mb-4">
|
|
2501
|
+
<div class="flex items-start gap-2">
|
|
2502
|
+
<i class="pi pi-exclamation-triangle text-xl"></i>
|
|
2503
|
+
<div class="flex-1">
|
|
2504
|
+
<span class="font-semibold">Validation Warning:</span>
|
|
2505
|
+
<p class="text-sm mt-1 mb-0">
|
|
3035
2506
|
{{ invalidActionsCount() }} selected
|
|
3036
2507
|
action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
|
|
3037
2508
|
unmet prerequisites. Fix before saving or use auto-fix on
|
|
@@ -3043,33 +2514,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3043
2514
|
}
|
|
3044
2515
|
|
|
3045
2516
|
<!-- Action Tree Table -->
|
|
3046
|
-
<
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
<ng-template
|
|
3071
|
-
<tr [class.
|
|
3072
|
-
<td
|
|
2517
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
2518
|
+
<p-treeTable
|
|
2519
|
+
[value]="treeNodes()"
|
|
2520
|
+
dataKey="id"
|
|
2521
|
+
styleClass="p-treetable-sm"
|
|
2522
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
2523
|
+
>
|
|
2524
|
+
<ng-template #header>
|
|
2525
|
+
<tr>
|
|
2526
|
+
<th class="w-12">
|
|
2527
|
+
<p-checkbox
|
|
2528
|
+
[ngModel]="allSelected()"
|
|
2529
|
+
[binary]="true"
|
|
2530
|
+
(ngModelChange)="toggleAll()"
|
|
2531
|
+
pTooltip="Select/Deselect All"
|
|
2532
|
+
tooltipPosition="top"
|
|
2533
|
+
/>
|
|
2534
|
+
</th>
|
|
2535
|
+
<th>Name</th>
|
|
2536
|
+
<th class="hidden md:table-cell">Code</th>
|
|
2537
|
+
<th>Type</th>
|
|
2538
|
+
<th class="hidden lg:table-cell">Description</th>
|
|
2539
|
+
</tr>
|
|
2540
|
+
</ng-template>
|
|
2541
|
+
<ng-template #body let-rowNode let-rowData="rowData">
|
|
2542
|
+
<tr [class.highlight-warning]="hasUnmetPrerequisites(rowData)">
|
|
2543
|
+
<td class="w-12">
|
|
3073
2544
|
<p-checkbox
|
|
3074
2545
|
[ngModel]="selectionMap()[rowData.id]"
|
|
3075
2546
|
[binary]="true"
|
|
@@ -3080,7 +2551,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3080
2551
|
</td>
|
|
3081
2552
|
<td>
|
|
3082
2553
|
<p-treeTableToggler [rowNode]="rowNode" />
|
|
3083
|
-
<span class="inline-flex
|
|
2554
|
+
<span class="inline-flex items-center gap-2">
|
|
3084
2555
|
{{ rowData.name }}
|
|
3085
2556
|
@if (hasUnmetPrerequisites(rowData)) {
|
|
3086
2557
|
<i
|
|
@@ -3091,7 +2562,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3091
2562
|
}
|
|
3092
2563
|
</span>
|
|
3093
2564
|
</td>
|
|
3094
|
-
<td>{{ rowData.code || '-' }}</td>
|
|
2565
|
+
<td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
|
|
3095
2566
|
<td>
|
|
3096
2567
|
<p-tag
|
|
3097
2568
|
[value]="rowData.actionType"
|
|
@@ -3100,36 +2571,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3100
2571
|
"
|
|
3101
2572
|
/>
|
|
3102
2573
|
</td>
|
|
3103
|
-
<td>{{ rowData.description || '-' }}</td>
|
|
2574
|
+
<td class="hidden lg:table-cell">{{ rowData.description || '-' }}</td>
|
|
3104
2575
|
</tr>
|
|
3105
2576
|
</ng-template>
|
|
3106
|
-
<ng-template
|
|
2577
|
+
<ng-template #emptymessage>
|
|
3107
2578
|
<tr>
|
|
3108
|
-
<td colspan="5" class="text-center p-4">
|
|
2579
|
+
<td colspan="5" class="text-center p-4 text-muted-color">
|
|
3109
2580
|
@if (loading()) {
|
|
3110
2581
|
<i class="pi pi-spin pi-spinner"></i> Loading actions...
|
|
3111
2582
|
} @else { No actions available. }
|
|
3112
2583
|
</td>
|
|
3113
2584
|
</tr>
|
|
3114
2585
|
</ng-template>
|
|
3115
|
-
|
|
2586
|
+
</p-treeTable>
|
|
2587
|
+
</div>
|
|
3116
2588
|
</div>
|
|
3117
2589
|
|
|
3118
2590
|
<!-- Change Summary -->
|
|
3119
2591
|
@if (hasChanges()) {
|
|
3120
|
-
<div class="
|
|
3121
|
-
<div class="flex
|
|
3122
|
-
<i class="pi pi-info-circle text-
|
|
3123
|
-
<
|
|
2592
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
2593
|
+
<div class="flex items-center gap-2 mb-3">
|
|
2594
|
+
<i class="pi pi-info-circle text-primary"></i>
|
|
2595
|
+
<span class="font-bold">Pending Changes</span>
|
|
3124
2596
|
</div>
|
|
3125
|
-
<div class="
|
|
2597
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
3126
2598
|
@if (pendingAdd().length > 0) {
|
|
3127
|
-
<div
|
|
2599
|
+
<div>
|
|
3128
2600
|
<div class="flex items-center gap-2 mb-2">
|
|
3129
2601
|
<i class="pi pi-plus-circle text-green-500"></i>
|
|
3130
|
-
<strong class="text-sm">
|
|
3131
|
-
To Whitelist ({{ pendingAdd().length }})
|
|
3132
|
-
</strong>
|
|
2602
|
+
<strong class="text-sm">To Whitelist ({{ pendingAdd().length }})</strong>
|
|
3133
2603
|
</div>
|
|
3134
2604
|
<ul class="list-none p-0 m-0 pl-4">
|
|
3135
2605
|
@for (action of pendingAdd(); track action.id) {
|
|
@@ -3138,14 +2608,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3138
2608
|
</ul>
|
|
3139
2609
|
</div>
|
|
3140
2610
|
}
|
|
3141
|
-
|
|
3142
2611
|
@if (pendingRemove().length > 0) {
|
|
3143
|
-
<div
|
|
2612
|
+
<div>
|
|
3144
2613
|
<div class="flex items-center gap-2 mb-2">
|
|
3145
2614
|
<i class="pi pi-minus-circle text-red-500"></i>
|
|
3146
|
-
<strong class="text-sm">
|
|
3147
|
-
To Remove ({{ pendingRemove().length }})
|
|
3148
|
-
</strong>
|
|
2615
|
+
<strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
|
|
3149
2616
|
</div>
|
|
3150
2617
|
<ul class="list-none p-0 m-0 pl-4">
|
|
3151
2618
|
@for (action of pendingRemove(); track action.id) {
|
|
@@ -3160,19 +2627,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3160
2627
|
}
|
|
3161
2628
|
|
|
3162
2629
|
@if (!loading() && actions().length === 0) {
|
|
3163
|
-
<div class="surface-card p-5 border
|
|
3164
|
-
<i
|
|
3165
|
-
|
|
3166
|
-
style="font-size: 3rem; display: block;"
|
|
3167
|
-
></i>
|
|
3168
|
-
<p class="text-color-secondary m-0">
|
|
2630
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
2631
|
+
<i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
|
|
2632
|
+
<p class="text-muted-color m-0">
|
|
3169
2633
|
No actions available for this company.
|
|
3170
2634
|
</p>
|
|
3171
2635
|
</div>
|
|
3172
2636
|
}
|
|
3173
2637
|
}
|
|
3174
2638
|
</div>
|
|
3175
|
-
`, styles: [":host{display:block}\n"] }]
|
|
2639
|
+
`, styles: [":host{display:block}.validation-warning{background-color:var(--p-yellow-50, #fefce8);border-left:4px solid var(--p-yellow-500, #eab308);color:var(--p-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--p-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--p-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"] }]
|
|
3176
2640
|
}], ctorParameters: () => [] });
|
|
3177
2641
|
|
|
3178
2642
|
/**
|
|
@@ -3206,6 +2670,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3206
2670
|
* ```
|
|
3207
2671
|
*/
|
|
3208
2672
|
class UserRoleSelectorComponent {
|
|
2673
|
+
// Permission constants for template
|
|
2674
|
+
USER_ROLE_PERMISSIONS = USER_ROLE_PERMISSIONS;
|
|
3209
2675
|
// Dependencies
|
|
3210
2676
|
appConfig = inject(APP_CONFIG);
|
|
3211
2677
|
companyContext = inject(LAYOUT_AUTH_STATE);
|
|
@@ -3249,7 +2715,11 @@ class UserRoleSelectorComponent {
|
|
|
3249
2715
|
hasChanges = computed(() => {
|
|
3250
2716
|
const current = this.selectionMap();
|
|
3251
2717
|
const initial = this.initialSelection();
|
|
3252
|
-
|
|
2718
|
+
const currentKeys = Object.keys(current);
|
|
2719
|
+
const initialKeys = Object.keys(initial);
|
|
2720
|
+
if (currentKeys.length !== initialKeys.length)
|
|
2721
|
+
return true;
|
|
2722
|
+
return currentKeys.some((key) => current[key] !== initial[key]);
|
|
3253
2723
|
}, ...(ngDevMode ? [{ debugName: "hasChanges" }] : []));
|
|
3254
2724
|
// Computed - Pending Changes
|
|
3255
2725
|
pendingAdd = computed(() => {
|
|
@@ -3305,9 +2775,7 @@ class UserRoleSelectorComponent {
|
|
|
3305
2775
|
this.branches.set([]);
|
|
3306
2776
|
return;
|
|
3307
2777
|
}
|
|
3308
|
-
const response = await this.userPermissionProvider
|
|
3309
|
-
.getUserBranchPermissions(userId)
|
|
3310
|
-
.toPromise();
|
|
2778
|
+
const response = await firstValueFrom(this.userPermissionProvider.getUserBranchPermissions(userId));
|
|
3311
2779
|
const userBranches = response?.data?.map((ub) => ({
|
|
3312
2780
|
id: ub.branchId,
|
|
3313
2781
|
name: ub.branchName,
|
|
@@ -3316,11 +2784,7 @@ class UserRoleSelectorComponent {
|
|
|
3316
2784
|
this.branches.set(userBranches);
|
|
3317
2785
|
}
|
|
3318
2786
|
catch {
|
|
3319
|
-
|
|
3320
|
-
severity: 'error',
|
|
3321
|
-
summary: 'Error',
|
|
3322
|
-
detail: 'Failed to load user permitted branches',
|
|
3323
|
-
});
|
|
2787
|
+
// Error toast handled by global interceptor
|
|
3324
2788
|
}
|
|
3325
2789
|
}
|
|
3326
2790
|
/**
|
|
@@ -3334,20 +2798,16 @@ class UserRoleSelectorComponent {
|
|
|
3334
2798
|
this.loading.set(true);
|
|
3335
2799
|
try {
|
|
3336
2800
|
// Load all roles
|
|
3337
|
-
const rolesResponse = await this.roleApi
|
|
3338
|
-
.getAll('', {
|
|
2801
|
+
const rolesResponse = await firstValueFrom(this.roleApi.getAll('', {
|
|
3339
2802
|
pagination: { currentPage: 0, pageSize: MAX_DROPDOWN_ITEMS },
|
|
3340
|
-
})
|
|
3341
|
-
.toPromise();
|
|
2803
|
+
}));
|
|
3342
2804
|
const rolesList = rolesResponse?.data ?? [];
|
|
3343
2805
|
this._roles.set(rolesList);
|
|
3344
2806
|
// Load existing user-role assignments
|
|
3345
2807
|
const query = isCompanyFeatureEnabled(this.appConfig) && branchId
|
|
3346
2808
|
? { branchId }
|
|
3347
2809
|
: undefined;
|
|
3348
|
-
const assignedResponse = await this.permissionApi
|
|
3349
|
-
.getUserRoles(userId, query)
|
|
3350
|
-
.toPromise();
|
|
2810
|
+
const assignedResponse = await firstValueFrom(this.permissionApi.getUserRoles(userId, query));
|
|
3351
2811
|
const assignedIds = new Set((assignedResponse?.data || []).map((r) => r.roleId));
|
|
3352
2812
|
// Build selection map
|
|
3353
2813
|
const selMap = {};
|
|
@@ -3358,11 +2818,7 @@ class UserRoleSelectorComponent {
|
|
|
3358
2818
|
this._initialSelection.set({ ...selMap });
|
|
3359
2819
|
}
|
|
3360
2820
|
catch {
|
|
3361
|
-
|
|
3362
|
-
severity: 'error',
|
|
3363
|
-
summary: 'Error',
|
|
3364
|
-
detail: 'Failed to load user role assignments',
|
|
3365
|
-
});
|
|
2821
|
+
// Error toast handled by global interceptor
|
|
3366
2822
|
}
|
|
3367
2823
|
finally {
|
|
3368
2824
|
this.loading.set(false);
|
|
@@ -3391,30 +2847,27 @@ class UserRoleSelectorComponent {
|
|
|
3391
2847
|
* Toggle all roles
|
|
3392
2848
|
*/
|
|
3393
2849
|
toggleAll() {
|
|
3394
|
-
|
|
3395
|
-
const selMap = {};
|
|
3396
|
-
this.roles().forEach((role) => {
|
|
3397
|
-
selMap[role.id] = newValue;
|
|
3398
|
-
});
|
|
3399
|
-
this._selectionMap.set(selMap);
|
|
2850
|
+
this.setAllSelections(!this.allSelected());
|
|
3400
2851
|
}
|
|
3401
2852
|
/**
|
|
3402
2853
|
* Select all roles
|
|
3403
2854
|
*/
|
|
3404
2855
|
selectAll() {
|
|
3405
|
-
|
|
3406
|
-
this.roles().forEach((role) => {
|
|
3407
|
-
selMap[role.id] = true;
|
|
3408
|
-
});
|
|
3409
|
-
this._selectionMap.set(selMap);
|
|
2856
|
+
this.setAllSelections(true);
|
|
3410
2857
|
}
|
|
3411
2858
|
/**
|
|
3412
2859
|
* Deselect all roles
|
|
3413
2860
|
*/
|
|
3414
2861
|
deselectAll() {
|
|
2862
|
+
this.setAllSelections(false);
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Set all role selections to a given value
|
|
2866
|
+
*/
|
|
2867
|
+
setAllSelections(value) {
|
|
3415
2868
|
const selMap = {};
|
|
3416
2869
|
this.roles().forEach((role) => {
|
|
3417
|
-
selMap[role.id] =
|
|
2870
|
+
selMap[role.id] = value;
|
|
3418
2871
|
});
|
|
3419
2872
|
this._selectionMap.set(selMap);
|
|
3420
2873
|
}
|
|
@@ -3457,9 +2910,7 @@ class UserRoleSelectorComponent {
|
|
|
3457
2910
|
payload.branchId = branchId;
|
|
3458
2911
|
}
|
|
3459
2912
|
}
|
|
3460
|
-
const response = await this.permissionApi
|
|
3461
|
-
.assignUserRoles(payload)
|
|
3462
|
-
.toPromise();
|
|
2913
|
+
const response = await firstValueFrom(this.permissionApi.assignUserRoles(payload));
|
|
3463
2914
|
this.messageService.add({
|
|
3464
2915
|
severity: 'success',
|
|
3465
2916
|
summary: 'Success',
|
|
@@ -3469,13 +2920,8 @@ class UserRoleSelectorComponent {
|
|
|
3469
2920
|
// Update baseline
|
|
3470
2921
|
this._initialSelection.set({ ...this.selectionMap() });
|
|
3471
2922
|
}
|
|
3472
|
-
catch
|
|
3473
|
-
|
|
3474
|
-
this.messageService.add({
|
|
3475
|
-
severity: 'error',
|
|
3476
|
-
summary: 'Error',
|
|
3477
|
-
detail: error?.error?.message || 'Failed to update user role assignments',
|
|
3478
|
-
});
|
|
2923
|
+
catch {
|
|
2924
|
+
// Error toast handled by global interceptor
|
|
3479
2925
|
}
|
|
3480
2926
|
finally {
|
|
3481
2927
|
this.saving.set(false);
|
|
@@ -3493,28 +2939,30 @@ class UserRoleSelectorComponent {
|
|
|
3493
2939
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: UserRoleSelectorComponent, isStandalone: true, selector: "flusys-user-role-selector", ngImport: i0, template: `
|
|
3494
2940
|
<div class="user-role-selector">
|
|
3495
2941
|
<!-- User and Branch Selectors -->
|
|
3496
|
-
<div class="surface-card p-4 border
|
|
3497
|
-
<div class="
|
|
3498
|
-
<div
|
|
3499
|
-
<label class="block font-semibold mb-2
|
|
2942
|
+
<div class="surface-card p-4 rounded-border mb-4 shadow-sm">
|
|
2943
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
|
2944
|
+
<div>
|
|
2945
|
+
<label class="block font-semibold mb-2">
|
|
3500
2946
|
<i class="pi pi-user mr-2 text-primary"></i>
|
|
3501
2947
|
Select User
|
|
3502
2948
|
</label>
|
|
3503
2949
|
<lib-user-select
|
|
3504
|
-
[
|
|
2950
|
+
[value]="selectedUserId()"
|
|
2951
|
+
(valueChange)="selectedUserId.set($event)"
|
|
3505
2952
|
[isEditMode]="true"
|
|
3506
2953
|
placeHolder="Select a user"
|
|
3507
2954
|
/>
|
|
3508
2955
|
</div>
|
|
3509
2956
|
|
|
3510
2957
|
@if (showBranchSelector()) {
|
|
3511
|
-
<div
|
|
3512
|
-
<label class="block font-semibold mb-2
|
|
2958
|
+
<div>
|
|
2959
|
+
<label class="block font-semibold mb-2">
|
|
3513
2960
|
<i class="pi pi-building mr-2 text-primary"></i>
|
|
3514
2961
|
Select Branch
|
|
3515
2962
|
</label>
|
|
3516
2963
|
<p-select
|
|
3517
|
-
[
|
|
2964
|
+
[ngModel]="selectedBranchId()"
|
|
2965
|
+
(ngModelChange)="selectedBranchId.set($event)"
|
|
3518
2966
|
[options]="filteredBranches()"
|
|
3519
2967
|
optionLabel="name"
|
|
3520
2968
|
optionValue="id"
|
|
@@ -3525,20 +2973,20 @@ class UserRoleSelectorComponent {
|
|
|
3525
2973
|
>
|
|
3526
2974
|
<ng-template #selectedItem let-branch>
|
|
3527
2975
|
@if (branch) {
|
|
3528
|
-
<div class="flex
|
|
2976
|
+
<div class="flex items-center gap-2">
|
|
3529
2977
|
<i class="pi pi-building text-primary"></i>
|
|
3530
2978
|
<span class="font-semibold">{{ branch.name }}</span>
|
|
3531
2979
|
</div>
|
|
3532
2980
|
}
|
|
3533
2981
|
</ng-template>
|
|
3534
2982
|
<ng-template #item let-branch>
|
|
3535
|
-
<div class="flex
|
|
3536
|
-
<i class="pi pi-building text-color
|
|
2983
|
+
<div class="flex items-center gap-2">
|
|
2984
|
+
<i class="pi pi-building text-muted-color"></i>
|
|
3537
2985
|
<span>{{ branch.name }}</span>
|
|
3538
2986
|
</div>
|
|
3539
2987
|
</ng-template>
|
|
3540
2988
|
</p-select>
|
|
3541
|
-
<small class="text-color
|
|
2989
|
+
<small class="text-muted-color block mt-1">
|
|
3542
2990
|
{{ filteredBranches().length }} permitted branch{{
|
|
3543
2991
|
filteredBranches().length !== 1 ? 'es' : ''
|
|
3544
2992
|
}}
|
|
@@ -3553,24 +3001,21 @@ class UserRoleSelectorComponent {
|
|
|
3553
3001
|
<!-- Loading State -->
|
|
3554
3002
|
@if (loading()) {
|
|
3555
3003
|
<div
|
|
3556
|
-
class="surface-card p-5 border
|
|
3004
|
+
class="surface-card p-5 rounded-border shadow-sm flex justify-center"
|
|
3557
3005
|
>
|
|
3558
|
-
<i
|
|
3559
|
-
class="pi pi-spin pi-spinner text-primary"
|
|
3560
|
-
style="font-size: 3rem"
|
|
3561
|
-
></i>
|
|
3006
|
+
<i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
|
|
3562
3007
|
</div>
|
|
3563
3008
|
}
|
|
3564
3009
|
|
|
3565
3010
|
<!-- Role List -->
|
|
3566
3011
|
@if (!loading() && roles().length > 0) {
|
|
3567
|
-
<div class="surface-card p-4 border
|
|
3012
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
3568
3013
|
<div
|
|
3569
|
-
class="flex flex-
|
|
3014
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
3570
3015
|
>
|
|
3571
3016
|
<div>
|
|
3572
3017
|
<h5 class="m-0 mb-1">Role Assignments</h5>
|
|
3573
|
-
<p class="text-sm text-color
|
|
3018
|
+
<p class="text-sm text-muted-color m-0">
|
|
3574
3019
|
{{ roles().length }} roles available
|
|
3575
3020
|
</p>
|
|
3576
3021
|
</div>
|
|
@@ -3592,6 +3037,7 @@ class UserRoleSelectorComponent {
|
|
|
3592
3037
|
(onClick)="deselectAll()"
|
|
3593
3038
|
/>
|
|
3594
3039
|
<p-button
|
|
3040
|
+
*hasPermission="USER_ROLE_PERMISSIONS.ASSIGN"
|
|
3595
3041
|
label="Save Changes"
|
|
3596
3042
|
icon="pi pi-save"
|
|
3597
3043
|
[disabled]="!canSave()"
|
|
@@ -3604,77 +3050,78 @@ class UserRoleSelectorComponent {
|
|
|
3604
3050
|
</div>
|
|
3605
3051
|
|
|
3606
3052
|
<!-- Role Table -->
|
|
3607
|
-
<
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3053
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
3054
|
+
<p-table
|
|
3055
|
+
[value]="roles()"
|
|
3056
|
+
[rows]="10"
|
|
3057
|
+
[paginator]="roles().length > 10"
|
|
3058
|
+
[rowsPerPageOptions]="[10, 20, 50]"
|
|
3059
|
+
[globalFilterFields]="['name', 'code', 'description']"
|
|
3060
|
+
[showCurrentPageReport]="true"
|
|
3061
|
+
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} roles"
|
|
3062
|
+
styleClass="p-datatable-sm"
|
|
3063
|
+
[tableStyle]="{ 'min-width': '35rem' }"
|
|
3064
|
+
>
|
|
3065
|
+
<ng-template #header>
|
|
3066
|
+
<tr>
|
|
3067
|
+
<th style="width: 3rem">
|
|
3068
|
+
<p-checkbox
|
|
3069
|
+
[ngModel]="allSelected()"
|
|
3070
|
+
[binary]="true"
|
|
3071
|
+
(ngModelChange)="toggleAll()"
|
|
3072
|
+
pTooltip="Select/Deselect All"
|
|
3073
|
+
tooltipPosition="top"
|
|
3074
|
+
/>
|
|
3075
|
+
</th>
|
|
3076
|
+
<th>Name</th>
|
|
3077
|
+
<th class="hidden sm:table-cell">Code</th>
|
|
3078
|
+
<th class="hidden md:table-cell">Description</th>
|
|
3079
|
+
</tr>
|
|
3080
|
+
</ng-template>
|
|
3081
|
+
<ng-template #body let-role>
|
|
3082
|
+
<tr>
|
|
3083
|
+
<td>
|
|
3084
|
+
<p-checkbox
|
|
3085
|
+
[ngModel]="selectionMap()[role.id]"
|
|
3086
|
+
[binary]="true"
|
|
3087
|
+
(ngModelChange)="onRoleToggle(role, $event)"
|
|
3088
|
+
[pTooltip]="getTooltip(role)"
|
|
3089
|
+
tooltipPosition="top"
|
|
3090
|
+
/>
|
|
3091
|
+
</td>
|
|
3092
|
+
<td>{{ role.name }}</td>
|
|
3093
|
+
<td class="hidden sm:table-cell">{{ role.code || '-' }}</td>
|
|
3094
|
+
<td class="hidden md:table-cell">{{ role.description || '-' }}</td>
|
|
3095
|
+
</tr>
|
|
3096
|
+
</ng-template>
|
|
3097
|
+
<ng-template #emptymessage>
|
|
3098
|
+
<tr>
|
|
3099
|
+
<td colspan="4" class="text-center p-4 text-muted-color">
|
|
3100
|
+
@if (loading()) {
|
|
3101
|
+
<i class="pi pi-spin pi-spinner"></i> Loading roles...
|
|
3102
|
+
} @else {
|
|
3103
|
+
No roles available.
|
|
3104
|
+
}
|
|
3105
|
+
</td>
|
|
3106
|
+
</tr>
|
|
3107
|
+
</ng-template>
|
|
3108
|
+
</p-table>
|
|
3109
|
+
</div>
|
|
3661
3110
|
</div>
|
|
3662
3111
|
|
|
3663
3112
|
<!-- Change Summary -->
|
|
3664
3113
|
@if (hasChanges()) {
|
|
3665
|
-
<div class="
|
|
3666
|
-
<div class="flex
|
|
3667
|
-
<i class="pi pi-info-circle text-
|
|
3668
|
-
<
|
|
3114
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
3115
|
+
<div class="flex items-center gap-2 mb-3">
|
|
3116
|
+
<i class="pi pi-info-circle text-primary"></i>
|
|
3117
|
+
<span class="font-bold">Pending Changes</span>
|
|
3669
3118
|
</div>
|
|
3670
|
-
<div class="
|
|
3119
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
3671
3120
|
@if (pendingAdd().length > 0) {
|
|
3672
|
-
<div
|
|
3673
|
-
<div class="flex
|
|
3121
|
+
<div>
|
|
3122
|
+
<div class="flex items-center gap-2 mb-2">
|
|
3674
3123
|
<i class="pi pi-plus-circle text-green-500"></i>
|
|
3675
|
-
<strong class="text-sm"
|
|
3676
|
-
>To Assign ({{ pendingAdd().length }})</strong
|
|
3677
|
-
>
|
|
3124
|
+
<strong class="text-sm">To Assign ({{ pendingAdd().length }})</strong>
|
|
3678
3125
|
</div>
|
|
3679
3126
|
<ul class="list-none p-0 m-0 pl-4">
|
|
3680
3127
|
@for (role of pendingAdd(); track role.id) {
|
|
@@ -3684,12 +3131,10 @@ class UserRoleSelectorComponent {
|
|
|
3684
3131
|
</div>
|
|
3685
3132
|
}
|
|
3686
3133
|
@if (pendingRemove().length > 0) {
|
|
3687
|
-
<div
|
|
3688
|
-
<div class="flex
|
|
3134
|
+
<div>
|
|
3135
|
+
<div class="flex items-center gap-2 mb-2">
|
|
3689
3136
|
<i class="pi pi-minus-circle text-red-500"></i>
|
|
3690
|
-
<strong class="text-sm"
|
|
3691
|
-
>To Remove ({{ pendingRemove().length }})</strong
|
|
3692
|
-
>
|
|
3137
|
+
<strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
|
|
3693
3138
|
</div>
|
|
3694
3139
|
<ul class="list-none p-0 m-0 pl-4">
|
|
3695
3140
|
@for (role of pendingRemove(); track role.id) {
|
|
@@ -3704,47 +3149,46 @@ class UserRoleSelectorComponent {
|
|
|
3704
3149
|
}
|
|
3705
3150
|
|
|
3706
3151
|
@if (!loading() && roles().length === 0) {
|
|
3707
|
-
<div class="surface-card p-5 border
|
|
3708
|
-
<i
|
|
3709
|
-
|
|
3710
|
-
style="font-size: 3rem; display: block;"
|
|
3711
|
-
></i>
|
|
3712
|
-
<p class="text-color-secondary m-0">
|
|
3152
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
3153
|
+
<i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
|
|
3154
|
+
<p class="text-muted-color m-0">
|
|
3713
3155
|
No roles available for this user.
|
|
3714
3156
|
</p>
|
|
3715
3157
|
</div>
|
|
3716
3158
|
}
|
|
3717
3159
|
}
|
|
3718
3160
|
</div>
|
|
3719
|
-
`, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type:
|
|
3161
|
+
`, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i3.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "component", type: i4.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "component", type: i5$1.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i6.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "directive", type: HasPermissionDirective, selector: "[hasPermission]", inputs: ["hasPermission"] }, { kind: "component", type: UserSelectComponent, selector: "lib-user-select", inputs: ["value"], outputs: ["valueChange", "userSelected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3720
3162
|
}
|
|
3721
3163
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserRoleSelectorComponent, decorators: [{
|
|
3722
3164
|
type: Component,
|
|
3723
|
-
args: [{ selector: 'flusys-user-role-selector',
|
|
3165
|
+
args: [{ selector: 'flusys-user-role-selector', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule, HasPermissionDirective, UserSelectComponent], template: `
|
|
3724
3166
|
<div class="user-role-selector">
|
|
3725
3167
|
<!-- User and Branch Selectors -->
|
|
3726
|
-
<div class="surface-card p-4 border
|
|
3727
|
-
<div class="
|
|
3728
|
-
<div
|
|
3729
|
-
<label class="block font-semibold mb-2
|
|
3168
|
+
<div class="surface-card p-4 rounded-border mb-4 shadow-sm">
|
|
3169
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
|
3170
|
+
<div>
|
|
3171
|
+
<label class="block font-semibold mb-2">
|
|
3730
3172
|
<i class="pi pi-user mr-2 text-primary"></i>
|
|
3731
3173
|
Select User
|
|
3732
3174
|
</label>
|
|
3733
3175
|
<lib-user-select
|
|
3734
|
-
[
|
|
3176
|
+
[value]="selectedUserId()"
|
|
3177
|
+
(valueChange)="selectedUserId.set($event)"
|
|
3735
3178
|
[isEditMode]="true"
|
|
3736
3179
|
placeHolder="Select a user"
|
|
3737
3180
|
/>
|
|
3738
3181
|
</div>
|
|
3739
3182
|
|
|
3740
3183
|
@if (showBranchSelector()) {
|
|
3741
|
-
<div
|
|
3742
|
-
<label class="block font-semibold mb-2
|
|
3184
|
+
<div>
|
|
3185
|
+
<label class="block font-semibold mb-2">
|
|
3743
3186
|
<i class="pi pi-building mr-2 text-primary"></i>
|
|
3744
3187
|
Select Branch
|
|
3745
3188
|
</label>
|
|
3746
3189
|
<p-select
|
|
3747
|
-
[
|
|
3190
|
+
[ngModel]="selectedBranchId()"
|
|
3191
|
+
(ngModelChange)="selectedBranchId.set($event)"
|
|
3748
3192
|
[options]="filteredBranches()"
|
|
3749
3193
|
optionLabel="name"
|
|
3750
3194
|
optionValue="id"
|
|
@@ -3755,20 +3199,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3755
3199
|
>
|
|
3756
3200
|
<ng-template #selectedItem let-branch>
|
|
3757
3201
|
@if (branch) {
|
|
3758
|
-
<div class="flex
|
|
3202
|
+
<div class="flex items-center gap-2">
|
|
3759
3203
|
<i class="pi pi-building text-primary"></i>
|
|
3760
3204
|
<span class="font-semibold">{{ branch.name }}</span>
|
|
3761
3205
|
</div>
|
|
3762
3206
|
}
|
|
3763
3207
|
</ng-template>
|
|
3764
3208
|
<ng-template #item let-branch>
|
|
3765
|
-
<div class="flex
|
|
3766
|
-
<i class="pi pi-building text-color
|
|
3209
|
+
<div class="flex items-center gap-2">
|
|
3210
|
+
<i class="pi pi-building text-muted-color"></i>
|
|
3767
3211
|
<span>{{ branch.name }}</span>
|
|
3768
3212
|
</div>
|
|
3769
3213
|
</ng-template>
|
|
3770
3214
|
</p-select>
|
|
3771
|
-
<small class="text-color
|
|
3215
|
+
<small class="text-muted-color block mt-1">
|
|
3772
3216
|
{{ filteredBranches().length }} permitted branch{{
|
|
3773
3217
|
filteredBranches().length !== 1 ? 'es' : ''
|
|
3774
3218
|
}}
|
|
@@ -3783,24 +3227,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3783
3227
|
<!-- Loading State -->
|
|
3784
3228
|
@if (loading()) {
|
|
3785
3229
|
<div
|
|
3786
|
-
class="surface-card p-5 border
|
|
3230
|
+
class="surface-card p-5 rounded-border shadow-sm flex justify-center"
|
|
3787
3231
|
>
|
|
3788
|
-
<i
|
|
3789
|
-
class="pi pi-spin pi-spinner text-primary"
|
|
3790
|
-
style="font-size: 3rem"
|
|
3791
|
-
></i>
|
|
3232
|
+
<i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
|
|
3792
3233
|
</div>
|
|
3793
3234
|
}
|
|
3794
3235
|
|
|
3795
3236
|
<!-- Role List -->
|
|
3796
3237
|
@if (!loading() && roles().length > 0) {
|
|
3797
|
-
<div class="surface-card p-4 border
|
|
3238
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
3798
3239
|
<div
|
|
3799
|
-
class="flex flex-
|
|
3240
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
3800
3241
|
>
|
|
3801
3242
|
<div>
|
|
3802
3243
|
<h5 class="m-0 mb-1">Role Assignments</h5>
|
|
3803
|
-
<p class="text-sm text-color
|
|
3244
|
+
<p class="text-sm text-muted-color m-0">
|
|
3804
3245
|
{{ roles().length }} roles available
|
|
3805
3246
|
</p>
|
|
3806
3247
|
</div>
|
|
@@ -3822,6 +3263,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3822
3263
|
(onClick)="deselectAll()"
|
|
3823
3264
|
/>
|
|
3824
3265
|
<p-button
|
|
3266
|
+
*hasPermission="USER_ROLE_PERMISSIONS.ASSIGN"
|
|
3825
3267
|
label="Save Changes"
|
|
3826
3268
|
icon="pi pi-save"
|
|
3827
3269
|
[disabled]="!canSave()"
|
|
@@ -3834,77 +3276,78 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3834
3276
|
</div>
|
|
3835
3277
|
|
|
3836
3278
|
<!-- Role Table -->
|
|
3837
|
-
<
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3279
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
3280
|
+
<p-table
|
|
3281
|
+
[value]="roles()"
|
|
3282
|
+
[rows]="10"
|
|
3283
|
+
[paginator]="roles().length > 10"
|
|
3284
|
+
[rowsPerPageOptions]="[10, 20, 50]"
|
|
3285
|
+
[globalFilterFields]="['name', 'code', 'description']"
|
|
3286
|
+
[showCurrentPageReport]="true"
|
|
3287
|
+
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} roles"
|
|
3288
|
+
styleClass="p-datatable-sm"
|
|
3289
|
+
[tableStyle]="{ 'min-width': '35rem' }"
|
|
3290
|
+
>
|
|
3291
|
+
<ng-template #header>
|
|
3292
|
+
<tr>
|
|
3293
|
+
<th style="width: 3rem">
|
|
3294
|
+
<p-checkbox
|
|
3295
|
+
[ngModel]="allSelected()"
|
|
3296
|
+
[binary]="true"
|
|
3297
|
+
(ngModelChange)="toggleAll()"
|
|
3298
|
+
pTooltip="Select/Deselect All"
|
|
3299
|
+
tooltipPosition="top"
|
|
3300
|
+
/>
|
|
3301
|
+
</th>
|
|
3302
|
+
<th>Name</th>
|
|
3303
|
+
<th class="hidden sm:table-cell">Code</th>
|
|
3304
|
+
<th class="hidden md:table-cell">Description</th>
|
|
3305
|
+
</tr>
|
|
3306
|
+
</ng-template>
|
|
3307
|
+
<ng-template #body let-role>
|
|
3308
|
+
<tr>
|
|
3309
|
+
<td>
|
|
3310
|
+
<p-checkbox
|
|
3311
|
+
[ngModel]="selectionMap()[role.id]"
|
|
3312
|
+
[binary]="true"
|
|
3313
|
+
(ngModelChange)="onRoleToggle(role, $event)"
|
|
3314
|
+
[pTooltip]="getTooltip(role)"
|
|
3315
|
+
tooltipPosition="top"
|
|
3316
|
+
/>
|
|
3317
|
+
</td>
|
|
3318
|
+
<td>{{ role.name }}</td>
|
|
3319
|
+
<td class="hidden sm:table-cell">{{ role.code || '-' }}</td>
|
|
3320
|
+
<td class="hidden md:table-cell">{{ role.description || '-' }}</td>
|
|
3321
|
+
</tr>
|
|
3322
|
+
</ng-template>
|
|
3323
|
+
<ng-template #emptymessage>
|
|
3324
|
+
<tr>
|
|
3325
|
+
<td colspan="4" class="text-center p-4 text-muted-color">
|
|
3326
|
+
@if (loading()) {
|
|
3327
|
+
<i class="pi pi-spin pi-spinner"></i> Loading roles...
|
|
3328
|
+
} @else {
|
|
3329
|
+
No roles available.
|
|
3330
|
+
}
|
|
3331
|
+
</td>
|
|
3332
|
+
</tr>
|
|
3333
|
+
</ng-template>
|
|
3334
|
+
</p-table>
|
|
3335
|
+
</div>
|
|
3891
3336
|
</div>
|
|
3892
3337
|
|
|
3893
3338
|
<!-- Change Summary -->
|
|
3894
3339
|
@if (hasChanges()) {
|
|
3895
|
-
<div class="
|
|
3896
|
-
<div class="flex
|
|
3897
|
-
<i class="pi pi-info-circle text-
|
|
3898
|
-
<
|
|
3340
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
3341
|
+
<div class="flex items-center gap-2 mb-3">
|
|
3342
|
+
<i class="pi pi-info-circle text-primary"></i>
|
|
3343
|
+
<span class="font-bold">Pending Changes</span>
|
|
3899
3344
|
</div>
|
|
3900
|
-
<div class="
|
|
3345
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
3901
3346
|
@if (pendingAdd().length > 0) {
|
|
3902
|
-
<div
|
|
3903
|
-
<div class="flex
|
|
3347
|
+
<div>
|
|
3348
|
+
<div class="flex items-center gap-2 mb-2">
|
|
3904
3349
|
<i class="pi pi-plus-circle text-green-500"></i>
|
|
3905
|
-
<strong class="text-sm"
|
|
3906
|
-
>To Assign ({{ pendingAdd().length }})</strong
|
|
3907
|
-
>
|
|
3350
|
+
<strong class="text-sm">To Assign ({{ pendingAdd().length }})</strong>
|
|
3908
3351
|
</div>
|
|
3909
3352
|
<ul class="list-none p-0 m-0 pl-4">
|
|
3910
3353
|
@for (role of pendingAdd(); track role.id) {
|
|
@@ -3914,12 +3357,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3914
3357
|
</div>
|
|
3915
3358
|
}
|
|
3916
3359
|
@if (pendingRemove().length > 0) {
|
|
3917
|
-
<div
|
|
3918
|
-
<div class="flex
|
|
3360
|
+
<div>
|
|
3361
|
+
<div class="flex items-center gap-2 mb-2">
|
|
3919
3362
|
<i class="pi pi-minus-circle text-red-500"></i>
|
|
3920
|
-
<strong class="text-sm"
|
|
3921
|
-
>To Remove ({{ pendingRemove().length }})</strong
|
|
3922
|
-
>
|
|
3363
|
+
<strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
|
|
3923
3364
|
</div>
|
|
3924
3365
|
<ul class="list-none p-0 m-0 pl-4">
|
|
3925
3366
|
@for (role of pendingRemove(); track role.id) {
|
|
@@ -3934,12 +3375,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3934
3375
|
}
|
|
3935
3376
|
|
|
3936
3377
|
@if (!loading() && roles().length === 0) {
|
|
3937
|
-
<div class="surface-card p-5 border
|
|
3938
|
-
<i
|
|
3939
|
-
|
|
3940
|
-
style="font-size: 3rem; display: block;"
|
|
3941
|
-
></i>
|
|
3942
|
-
<p class="text-color-secondary m-0">
|
|
3378
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
3379
|
+
<i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
|
|
3380
|
+
<p class="text-muted-color m-0">
|
|
3943
3381
|
No roles available for this user.
|
|
3944
3382
|
</p>
|
|
3945
3383
|
</div>
|
|
@@ -3981,6 +3419,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3981
3419
|
* ```
|
|
3982
3420
|
*/
|
|
3983
3421
|
class UserActionSelectorComponent {
|
|
3422
|
+
// Permission constants for template
|
|
3423
|
+
USER_ACTION_PERMISSIONS = USER_ACTION_PERMISSIONS;
|
|
3984
3424
|
// Dependencies
|
|
3985
3425
|
appConfig = inject(APP_CONFIG);
|
|
3986
3426
|
companyContext = inject(LAYOUT_AUTH_STATE);
|
|
@@ -4007,8 +3447,10 @@ class UserActionSelectorComponent {
|
|
|
4007
3447
|
selectionMap = this._selectionMap.asReadonly();
|
|
4008
3448
|
_initialSelection = signal({}, ...(ngDevMode ? [{ debugName: "_initialSelection" }] : []));
|
|
4009
3449
|
initialSelection = this._initialSelection.asReadonly();
|
|
3450
|
+
// Computed - Company Feature Flag (cached)
|
|
3451
|
+
isCompanyFeatureActive = computed(() => isCompanyFeatureEnabled(this.appConfig) === true, ...(ngDevMode ? [{ debugName: "isCompanyFeatureActive" }] : []));
|
|
4010
3452
|
// Computed - UI Flags
|
|
4011
|
-
showBranchSelector =
|
|
3453
|
+
showBranchSelector = this.isCompanyFeatureActive;
|
|
4012
3454
|
filteredBranches = computed(() => {
|
|
4013
3455
|
const currentCompanyId = this.companyContext.currentCompanyInfo()?.id || undefined;
|
|
4014
3456
|
if (!currentCompanyId)
|
|
@@ -4030,7 +3472,11 @@ class UserActionSelectorComponent {
|
|
|
4030
3472
|
hasChanges = computed(() => {
|
|
4031
3473
|
const current = this.selectionMap();
|
|
4032
3474
|
const initial = this.initialSelection();
|
|
4033
|
-
|
|
3475
|
+
const currentKeys = Object.keys(current);
|
|
3476
|
+
const initialKeys = Object.keys(initial);
|
|
3477
|
+
if (currentKeys.length !== initialKeys.length)
|
|
3478
|
+
return true;
|
|
3479
|
+
return currentKeys.some((key) => current[key] !== initial[key]);
|
|
4034
3480
|
}, ...(ngDevMode ? [{ debugName: "hasChanges" }] : []));
|
|
4035
3481
|
// Computed - Validation
|
|
4036
3482
|
actionsWithUnmetPrerequisites = computed(() => {
|
|
@@ -4070,7 +3516,7 @@ class UserActionSelectorComponent {
|
|
|
4070
3516
|
effect(() => {
|
|
4071
3517
|
const userId = this.selectedUserId();
|
|
4072
3518
|
if (userId) {
|
|
4073
|
-
if (
|
|
3519
|
+
if (this.isCompanyFeatureActive()) {
|
|
4074
3520
|
this.loadUserBranches(userId);
|
|
4075
3521
|
}
|
|
4076
3522
|
this.loadData();
|
|
@@ -4085,9 +3531,7 @@ class UserActionSelectorComponent {
|
|
|
4085
3531
|
effect(() => {
|
|
4086
3532
|
const userId = this.selectedUserId();
|
|
4087
3533
|
const branchId = this.selectedBranchId();
|
|
4088
|
-
if (
|
|
4089
|
-
userId &&
|
|
4090
|
-
branchId !== undefined) {
|
|
3534
|
+
if (this.isCompanyFeatureActive() && userId && branchId !== undefined) {
|
|
4091
3535
|
this.loadData();
|
|
4092
3536
|
}
|
|
4093
3537
|
});
|
|
@@ -4096,15 +3540,13 @@ class UserActionSelectorComponent {
|
|
|
4096
3540
|
* Load user's permitted branches
|
|
4097
3541
|
*/
|
|
4098
3542
|
async loadUserBranches(userId) {
|
|
3543
|
+
const companyId = this.companyContext.currentCompanyInfo()?.id;
|
|
3544
|
+
if (!companyId) {
|
|
3545
|
+
this.branches.set([]);
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
4099
3548
|
try {
|
|
4100
|
-
const
|
|
4101
|
-
if (!companyId) {
|
|
4102
|
-
this.branches.set([]);
|
|
4103
|
-
return;
|
|
4104
|
-
}
|
|
4105
|
-
const response = await this.userPermissionProvider
|
|
4106
|
-
.getUserBranchPermissions(userId)
|
|
4107
|
-
.toPromise();
|
|
3549
|
+
const response = await firstValueFrom(this.userPermissionProvider.getUserBranchPermissions(userId));
|
|
4108
3550
|
const userBranches = response?.data?.map((ub) => ({
|
|
4109
3551
|
id: ub.branchId,
|
|
4110
3552
|
name: ub.branchName,
|
|
@@ -4113,11 +3555,7 @@ class UserActionSelectorComponent {
|
|
|
4113
3555
|
this.branches.set(userBranches);
|
|
4114
3556
|
}
|
|
4115
3557
|
catch {
|
|
4116
|
-
|
|
4117
|
-
severity: 'error',
|
|
4118
|
-
summary: 'Error',
|
|
4119
|
-
detail: 'Failed to load user permitted branches',
|
|
4120
|
-
});
|
|
3558
|
+
// Error toast handled by global interceptor
|
|
4121
3559
|
}
|
|
4122
3560
|
}
|
|
4123
3561
|
/**
|
|
@@ -4130,39 +3568,20 @@ class UserActionSelectorComponent {
|
|
|
4130
3568
|
const branchId = this.selectedBranchId();
|
|
4131
3569
|
this.loading.set(true);
|
|
4132
3570
|
try {
|
|
4133
|
-
|
|
4134
|
-
const actionsResponse = await this.actionApi
|
|
4135
|
-
.getActionsForPermission()
|
|
4136
|
-
.toPromise();
|
|
4137
|
-
// Get flat list (filtered by company if enabled)
|
|
3571
|
+
const actionsResponse = await firstValueFrom(this.actionApi.getActionsForPermission());
|
|
4138
3572
|
const flatActions = actionsResponse?.data ?? [];
|
|
4139
|
-
// Build tree structure from flat list with parentId
|
|
4140
3573
|
const actionsTree = buildTreeFromFlat(flatActions);
|
|
4141
|
-
// Store both representations
|
|
4142
3574
|
this._actionsTree.set(actionsTree);
|
|
4143
3575
|
this._actions.set(flatActions);
|
|
4144
|
-
|
|
4145
|
-
const
|
|
4146
|
-
? { branchId }
|
|
4147
|
-
: undefined;
|
|
4148
|
-
const assignedResponse = await this.permissionApi
|
|
4149
|
-
.getUserActions(userId, query)
|
|
4150
|
-
.toPromise();
|
|
3576
|
+
const query = this.isCompanyFeatureActive() && branchId ? { branchId } : undefined;
|
|
3577
|
+
const assignedResponse = await firstValueFrom(this.permissionApi.getUserActions(userId, query));
|
|
4151
3578
|
const assignedIds = new Set((assignedResponse?.data || []).map((a) => a.actionId));
|
|
4152
|
-
|
|
4153
|
-
const selMap = {};
|
|
4154
|
-
flatActions.forEach((action) => {
|
|
4155
|
-
selMap[action.id] = assignedIds.has(action.id);
|
|
4156
|
-
});
|
|
3579
|
+
const selMap = this.buildSelectionMap(flatActions, (action) => assignedIds.has(action.id));
|
|
4157
3580
|
this._selectionMap.set(selMap);
|
|
4158
3581
|
this._initialSelection.set({ ...selMap });
|
|
4159
3582
|
}
|
|
4160
3583
|
catch {
|
|
4161
|
-
|
|
4162
|
-
severity: 'error',
|
|
4163
|
-
summary: 'Error',
|
|
4164
|
-
detail: 'Failed to load user action permissions',
|
|
4165
|
-
});
|
|
3584
|
+
// Error toast handled by global interceptor
|
|
4166
3585
|
}
|
|
4167
3586
|
finally {
|
|
4168
3587
|
this.loading.set(false);
|
|
@@ -4197,55 +3616,37 @@ class UserActionSelectorComponent {
|
|
|
4197
3616
|
: false;
|
|
4198
3617
|
}
|
|
4199
3618
|
/**
|
|
4200
|
-
* Handle action
|
|
3619
|
+
* Handle action toggle with dependency management
|
|
4201
3620
|
*/
|
|
4202
3621
|
onActionToggle(action, newValue) {
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
3622
|
+
if (!newValue) {
|
|
3623
|
+
// Unchecking - validate dependencies
|
|
3624
|
+
this.permissionLogic.handleUncheck(action, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
|
|
3625
|
+
}
|
|
3626
|
+
else {
|
|
3627
|
+
// Checking - validate prerequisites
|
|
3628
|
+
this.permissionLogic.handleCheck(action, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap), (previousState) => this._selectionMap.set(previousState));
|
|
3629
|
+
}
|
|
4206
3630
|
}
|
|
4207
|
-
/**
|
|
4208
|
-
* Toggle all actions
|
|
4209
|
-
*/
|
|
4210
3631
|
toggleAll() {
|
|
4211
|
-
|
|
4212
|
-
const selMap = {};
|
|
4213
|
-
this.actions().forEach((action) => {
|
|
4214
|
-
selMap[action.id] = newValue;
|
|
4215
|
-
});
|
|
4216
|
-
this._selectionMap.set(selMap);
|
|
3632
|
+
this.setAllActions(!this.allSelected());
|
|
4217
3633
|
}
|
|
4218
|
-
/**
|
|
4219
|
-
* Select all actions
|
|
4220
|
-
*/
|
|
4221
3634
|
selectAll() {
|
|
4222
|
-
|
|
4223
|
-
this.actions().forEach((action) => {
|
|
4224
|
-
selMap[action.id] = true;
|
|
4225
|
-
});
|
|
4226
|
-
this._selectionMap.set(selMap);
|
|
3635
|
+
this.setAllActions(true);
|
|
4227
3636
|
}
|
|
4228
|
-
/**
|
|
4229
|
-
* Deselect all actions
|
|
4230
|
-
*/
|
|
4231
3637
|
deselectAll() {
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
this._selectionMap.set(selMap);
|
|
3638
|
+
this.setAllActions(false);
|
|
3639
|
+
}
|
|
3640
|
+
setAllActions(value) {
|
|
3641
|
+
this._selectionMap.set(this.buildSelectionMap(this.actions(), () => value));
|
|
4237
3642
|
}
|
|
4238
|
-
/**
|
|
4239
|
-
* Save changes to backend
|
|
4240
|
-
*/
|
|
4241
3643
|
async saveChanges() {
|
|
4242
3644
|
const userId = this.selectedUserId();
|
|
4243
3645
|
if (!userId)
|
|
4244
3646
|
return;
|
|
4245
3647
|
const branchId = this.selectedBranchId();
|
|
4246
|
-
const companyId = this.companyContext.currentCompanyInfo()?.id
|
|
4247
|
-
|
|
4248
|
-
if (isCompanyFeatureEnabled(this.appConfig) && !companyId) {
|
|
3648
|
+
const companyId = this.companyContext.currentCompanyInfo()?.id;
|
|
3649
|
+
if (this.isCompanyFeatureActive() && !companyId) {
|
|
4249
3650
|
this.messageService.add({
|
|
4250
3651
|
severity: 'warn',
|
|
4251
3652
|
summary: 'Company Required',
|
|
@@ -4253,92 +3654,90 @@ class UserActionSelectorComponent {
|
|
|
4253
3654
|
});
|
|
4254
3655
|
return;
|
|
4255
3656
|
}
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
});
|
|
4263
|
-
});
|
|
4264
|
-
this.pendingRemove().forEach((action) => {
|
|
4265
|
-
items.push({
|
|
4266
|
-
id: action.id,
|
|
4267
|
-
action: 'remove',
|
|
4268
|
-
});
|
|
4269
|
-
});
|
|
3657
|
+
const invalidActions = this.permissionLogic.getActionsWithUnmetPrerequisites(this.selectionMap(), this.actions());
|
|
3658
|
+
if (invalidActions.length > 0) {
|
|
3659
|
+
this.permissionLogic.showValidationErrorDialog(invalidActions, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3662
|
+
const items = this.buildPermissionItems();
|
|
4270
3663
|
if (items.length === 0)
|
|
4271
3664
|
return;
|
|
4272
3665
|
this.saving.set(true);
|
|
4273
3666
|
try {
|
|
4274
3667
|
const payload = { userId, items };
|
|
4275
|
-
if (
|
|
4276
|
-
if (companyId)
|
|
3668
|
+
if (this.isCompanyFeatureActive()) {
|
|
3669
|
+
if (companyId)
|
|
4277
3670
|
payload.companyId = companyId;
|
|
4278
|
-
|
|
4279
|
-
if (branchId) {
|
|
3671
|
+
if (branchId)
|
|
4280
3672
|
payload.branchId = branchId;
|
|
4281
|
-
}
|
|
4282
3673
|
}
|
|
4283
|
-
const response = await this.permissionApi
|
|
4284
|
-
.assignUserActions(payload)
|
|
4285
|
-
.toPromise();
|
|
3674
|
+
const response = await firstValueFrom(this.permissionApi.assignUserActions(payload));
|
|
4286
3675
|
this.messageService.add({
|
|
4287
3676
|
severity: 'success',
|
|
4288
3677
|
summary: 'Success',
|
|
4289
3678
|
detail: response?.data?.message ||
|
|
4290
3679
|
'User action permissions updated successfully',
|
|
4291
3680
|
});
|
|
4292
|
-
// Update baseline
|
|
4293
3681
|
this._initialSelection.set({ ...this.selectionMap() });
|
|
4294
3682
|
}
|
|
4295
|
-
catch
|
|
4296
|
-
|
|
4297
|
-
this.messageService.add({
|
|
4298
|
-
severity: 'error',
|
|
4299
|
-
summary: 'Error',
|
|
4300
|
-
detail: error?.error?.message || 'Failed to update user action permissions',
|
|
4301
|
-
});
|
|
3683
|
+
catch {
|
|
3684
|
+
// Error toast handled by global interceptor
|
|
4302
3685
|
}
|
|
4303
3686
|
finally {
|
|
4304
3687
|
this.saving.set(false);
|
|
4305
3688
|
}
|
|
4306
3689
|
}
|
|
4307
|
-
/**
|
|
4308
|
-
* Reset component state
|
|
4309
|
-
*/
|
|
4310
3690
|
resetState() {
|
|
4311
3691
|
this._actionsTree.set([]);
|
|
4312
3692
|
this._actions.set([]);
|
|
4313
3693
|
this._selectionMap.set({});
|
|
4314
3694
|
this._initialSelection.set({});
|
|
4315
3695
|
}
|
|
3696
|
+
buildSelectionMap(actions, predicate) {
|
|
3697
|
+
const selMap = {};
|
|
3698
|
+
actions.forEach((action) => {
|
|
3699
|
+
selMap[action.id] = predicate(action);
|
|
3700
|
+
});
|
|
3701
|
+
return selMap;
|
|
3702
|
+
}
|
|
3703
|
+
buildPermissionItems() {
|
|
3704
|
+
const items = [];
|
|
3705
|
+
this.pendingAdd().forEach((action) => {
|
|
3706
|
+
items.push({ id: action.id, action: 'add' });
|
|
3707
|
+
});
|
|
3708
|
+
this.pendingRemove().forEach((action) => {
|
|
3709
|
+
items.push({ id: action.id, action: 'remove' });
|
|
3710
|
+
});
|
|
3711
|
+
return items;
|
|
3712
|
+
}
|
|
4316
3713
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4317
3714
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: UserActionSelectorComponent, isStandalone: true, selector: "flusys-user-action-selector", ngImport: i0, template: `
|
|
4318
3715
|
<div class="user-action-selector">
|
|
4319
3716
|
<!-- User and Branch Selectors -->
|
|
4320
|
-
<div class="surface-card p-4 border
|
|
4321
|
-
<div class="
|
|
4322
|
-
<div
|
|
4323
|
-
<label class="block font-semibold mb-2
|
|
3717
|
+
<div class="surface-card p-4 rounded-border mb-4 shadow-sm">
|
|
3718
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
|
3719
|
+
<div>
|
|
3720
|
+
<label class="block font-semibold mb-2">
|
|
4324
3721
|
<i class="pi pi-user mr-2 text-primary"></i>
|
|
4325
3722
|
Select User
|
|
4326
3723
|
</label>
|
|
4327
3724
|
<lib-user-select
|
|
4328
|
-
[
|
|
3725
|
+
[value]="selectedUserId()"
|
|
3726
|
+
(valueChange)="selectedUserId.set($event)"
|
|
4329
3727
|
[isEditMode]="true"
|
|
4330
3728
|
placeHolder="Select a user"
|
|
4331
3729
|
/>
|
|
4332
3730
|
</div>
|
|
4333
3731
|
|
|
4334
3732
|
@if (showBranchSelector()) {
|
|
4335
|
-
<div
|
|
4336
|
-
<label class="block font-semibold mb-2
|
|
3733
|
+
<div>
|
|
3734
|
+
<label class="block font-semibold mb-2">
|
|
4337
3735
|
<i class="pi pi-building mr-2 text-primary"></i>
|
|
4338
3736
|
Select Branch
|
|
4339
3737
|
</label>
|
|
4340
3738
|
<p-select
|
|
4341
|
-
[
|
|
3739
|
+
[ngModel]="selectedBranchId()"
|
|
3740
|
+
(ngModelChange)="selectedBranchId.set($event)"
|
|
4342
3741
|
[options]="filteredBranches()"
|
|
4343
3742
|
optionLabel="name"
|
|
4344
3743
|
optionValue="id"
|
|
@@ -4349,20 +3748,20 @@ class UserActionSelectorComponent {
|
|
|
4349
3748
|
>
|
|
4350
3749
|
<ng-template #selectedItem let-branch>
|
|
4351
3750
|
@if (branch) {
|
|
4352
|
-
<div class="flex
|
|
3751
|
+
<div class="flex items-center gap-2">
|
|
4353
3752
|
<i class="pi pi-building text-primary"></i>
|
|
4354
3753
|
<span class="font-semibold">{{ branch.name }}</span>
|
|
4355
3754
|
</div>
|
|
4356
3755
|
}
|
|
4357
3756
|
</ng-template>
|
|
4358
3757
|
<ng-template #item let-branch>
|
|
4359
|
-
<div class="flex
|
|
4360
|
-
<i class="pi pi-building text-color
|
|
3758
|
+
<div class="flex items-center gap-2">
|
|
3759
|
+
<i class="pi pi-building text-muted-color"></i>
|
|
4361
3760
|
<span>{{ branch.name }}</span>
|
|
4362
3761
|
</div>
|
|
4363
3762
|
</ng-template>
|
|
4364
3763
|
</p-select>
|
|
4365
|
-
<small class="text-color
|
|
3764
|
+
<small class="text-muted-color block mt-1">
|
|
4366
3765
|
{{ filteredBranches().length }} permitted branch{{
|
|
4367
3766
|
filteredBranches().length !== 1 ? 'es' : ''
|
|
4368
3767
|
}}
|
|
@@ -4376,20 +3775,20 @@ class UserActionSelectorComponent {
|
|
|
4376
3775
|
@if (selectedUserId()) {
|
|
4377
3776
|
<!-- Loading State -->
|
|
4378
3777
|
@if (loading()) {
|
|
4379
|
-
<div class="flex justify-
|
|
4380
|
-
<i class="pi pi-spin pi-spinner
|
|
3778
|
+
<div class="flex justify-center p-5">
|
|
3779
|
+
<i class="pi pi-spin pi-spinner text-4xl"></i>
|
|
4381
3780
|
</div>
|
|
4382
3781
|
}
|
|
4383
3782
|
|
|
4384
3783
|
<!-- Action List -->
|
|
4385
3784
|
@if (!loading() && actions().length > 0) {
|
|
4386
|
-
<div class="surface-card p-4 border
|
|
3785
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
4387
3786
|
<div
|
|
4388
|
-
class="flex flex-
|
|
3787
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
4389
3788
|
>
|
|
4390
3789
|
<div>
|
|
4391
3790
|
<h5 class="m-0 mb-1">Direct Action Permissions</h5>
|
|
4392
|
-
<p class="text-sm text-color
|
|
3791
|
+
<p class="text-sm text-muted-color m-0">
|
|
4393
3792
|
{{ actions().length }} actions available
|
|
4394
3793
|
</p>
|
|
4395
3794
|
</div>
|
|
@@ -4411,6 +3810,7 @@ class UserActionSelectorComponent {
|
|
|
4411
3810
|
(onClick)="deselectAll()"
|
|
4412
3811
|
/>
|
|
4413
3812
|
<p-button
|
|
3813
|
+
*hasPermission="USER_ACTION_PERMISSIONS.ASSIGN"
|
|
4414
3814
|
label="Save Changes"
|
|
4415
3815
|
icon="pi pi-save"
|
|
4416
3816
|
[disabled]="!canSave()"
|
|
@@ -4422,34 +3822,52 @@ class UserActionSelectorComponent {
|
|
|
4422
3822
|
</div>
|
|
4423
3823
|
</div>
|
|
4424
3824
|
|
|
3825
|
+
<!-- Validation Warning -->
|
|
3826
|
+
@if (invalidActionsCount() > 0) {
|
|
3827
|
+
<div class="validation-warning rounded-border p-3 mb-4">
|
|
3828
|
+
<div class="flex items-start gap-2">
|
|
3829
|
+
<i class="pi pi-exclamation-triangle text-xl"></i>
|
|
3830
|
+
<div class="flex-1">
|
|
3831
|
+
<span class="font-semibold">Validation Warning:</span>
|
|
3832
|
+
<p class="text-sm mt-1 mb-0">
|
|
3833
|
+
{{ invalidActionsCount() }} selected
|
|
3834
|
+
action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
|
|
3835
|
+
unmet prerequisites. Fix before saving or use auto-fix on
|
|
3836
|
+
save.
|
|
3837
|
+
</p>
|
|
3838
|
+
</div>
|
|
3839
|
+
</div>
|
|
3840
|
+
</div>
|
|
3841
|
+
}
|
|
3842
|
+
|
|
4425
3843
|
<!-- Action Tree Table -->
|
|
4426
|
-
<
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
<ng-template
|
|
4451
|
-
<tr>
|
|
4452
|
-
<td
|
|
3844
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
3845
|
+
<p-treeTable
|
|
3846
|
+
[value]="treeNodes()"
|
|
3847
|
+
dataKey="id"
|
|
3848
|
+
styleClass="p-treetable-sm"
|
|
3849
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
3850
|
+
>
|
|
3851
|
+
<ng-template #header>
|
|
3852
|
+
<tr>
|
|
3853
|
+
<th class="w-12">
|
|
3854
|
+
<p-checkbox
|
|
3855
|
+
[ngModel]="allSelected()"
|
|
3856
|
+
[binary]="true"
|
|
3857
|
+
(ngModelChange)="toggleAll()"
|
|
3858
|
+
pTooltip="Select/Deselect All"
|
|
3859
|
+
tooltipPosition="top"
|
|
3860
|
+
/>
|
|
3861
|
+
</th>
|
|
3862
|
+
<th>Name</th>
|
|
3863
|
+
<th class="hidden md:table-cell">Code</th>
|
|
3864
|
+
<th>Type</th>
|
|
3865
|
+
<th class="hidden lg:table-cell">Description</th>
|
|
3866
|
+
</tr>
|
|
3867
|
+
</ng-template>
|
|
3868
|
+
<ng-template #body let-rowNode let-rowData="rowData">
|
|
3869
|
+
<tr [class.highlight-warning]="hasUnmetPrerequisites(rowData)">
|
|
3870
|
+
<td class="w-12">
|
|
4453
3871
|
<p-checkbox
|
|
4454
3872
|
[ngModel]="selectionMap()[rowData.id]"
|
|
4455
3873
|
[binary]="true"
|
|
@@ -4460,9 +3878,18 @@ class UserActionSelectorComponent {
|
|
|
4460
3878
|
</td>
|
|
4461
3879
|
<td>
|
|
4462
3880
|
<p-treeTableToggler [rowNode]="rowNode" />
|
|
4463
|
-
<span
|
|
3881
|
+
<span class="inline-flex items-center gap-2">
|
|
3882
|
+
{{ rowData.name }}
|
|
3883
|
+
@if (hasUnmetPrerequisites(rowData)) {
|
|
3884
|
+
<i
|
|
3885
|
+
class="pi pi-exclamation-triangle text-orange-500"
|
|
3886
|
+
pTooltip="This action has unmet prerequisites and will fail validation on save"
|
|
3887
|
+
tooltipPosition="top"
|
|
3888
|
+
></i>
|
|
3889
|
+
}
|
|
3890
|
+
</span>
|
|
4464
3891
|
</td>
|
|
4465
|
-
<td>{{ rowData.code || '-' }}</td>
|
|
3892
|
+
<td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
|
|
4466
3893
|
<td>
|
|
4467
3894
|
<p-tag
|
|
4468
3895
|
[value]="rowData.actionType"
|
|
@@ -4471,12 +3898,12 @@ class UserActionSelectorComponent {
|
|
|
4471
3898
|
"
|
|
4472
3899
|
/>
|
|
4473
3900
|
</td>
|
|
4474
|
-
<td>{{ rowData.description || '-' }}</td>
|
|
3901
|
+
<td class="hidden lg:table-cell">{{ rowData.description || '-' }}</td>
|
|
4475
3902
|
</tr>
|
|
4476
3903
|
</ng-template>
|
|
4477
|
-
<ng-template
|
|
3904
|
+
<ng-template #emptymessage>
|
|
4478
3905
|
<tr>
|
|
4479
|
-
<td colspan="5" class="text-center p-4">
|
|
3906
|
+
<td colspan="5" class="text-center p-4 text-muted-color">
|
|
4480
3907
|
@if (loading()) {
|
|
4481
3908
|
<i class="pi pi-spin pi-spinner"></i> Loading actions...
|
|
4482
3909
|
} @else {
|
|
@@ -4485,24 +3912,23 @@ class UserActionSelectorComponent {
|
|
|
4485
3912
|
</td>
|
|
4486
3913
|
</tr>
|
|
4487
3914
|
</ng-template>
|
|
4488
|
-
|
|
3915
|
+
</p-treeTable>
|
|
3916
|
+
</div>
|
|
4489
3917
|
</div>
|
|
4490
3918
|
|
|
4491
3919
|
<!-- Change Summary -->
|
|
4492
3920
|
@if (hasChanges()) {
|
|
4493
|
-
<div class="
|
|
4494
|
-
<div class="flex
|
|
4495
|
-
<i class="pi pi-info-circle text-
|
|
4496
|
-
<
|
|
3921
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
3922
|
+
<div class="flex items-center gap-2 mb-3">
|
|
3923
|
+
<i class="pi pi-info-circle text-primary"></i>
|
|
3924
|
+
<span class="font-bold">Pending Changes</span>
|
|
4497
3925
|
</div>
|
|
4498
|
-
<div class="
|
|
3926
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
4499
3927
|
@if (pendingAdd().length > 0) {
|
|
4500
|
-
<div
|
|
4501
|
-
<div class="flex
|
|
3928
|
+
<div>
|
|
3929
|
+
<div class="flex items-center gap-2 mb-2">
|
|
4502
3930
|
<i class="pi pi-plus-circle text-green-500"></i>
|
|
4503
|
-
<strong class="text-sm"
|
|
4504
|
-
>To Assign ({{ pendingAdd().length }})</strong
|
|
4505
|
-
>
|
|
3931
|
+
<strong class="text-sm">To Assign ({{ pendingAdd().length }})</strong>
|
|
4506
3932
|
</div>
|
|
4507
3933
|
<ul class="list-none p-0 m-0 pl-4">
|
|
4508
3934
|
@for (action of pendingAdd(); track action.id) {
|
|
@@ -4512,12 +3938,10 @@ class UserActionSelectorComponent {
|
|
|
4512
3938
|
</div>
|
|
4513
3939
|
}
|
|
4514
3940
|
@if (pendingRemove().length > 0) {
|
|
4515
|
-
<div
|
|
4516
|
-
<div class="flex
|
|
3941
|
+
<div>
|
|
3942
|
+
<div class="flex items-center gap-2 mb-2">
|
|
4517
3943
|
<i class="pi pi-minus-circle text-red-500"></i>
|
|
4518
|
-
<strong class="text-sm"
|
|
4519
|
-
>To Remove ({{ pendingRemove().length }})</strong
|
|
4520
|
-
>
|
|
3944
|
+
<strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
|
|
4521
3945
|
</div>
|
|
4522
3946
|
<ul class="list-none p-0 m-0 pl-4">
|
|
4523
3947
|
@for (action of pendingRemove(); track action.id) {
|
|
@@ -4532,47 +3956,46 @@ class UserActionSelectorComponent {
|
|
|
4532
3956
|
}
|
|
4533
3957
|
|
|
4534
3958
|
@if (!loading() && actions().length === 0) {
|
|
4535
|
-
<div class="surface-card p-5 border
|
|
4536
|
-
<i
|
|
4537
|
-
|
|
4538
|
-
style="font-size: 3rem; display: block;"
|
|
4539
|
-
></i>
|
|
4540
|
-
<p class="text-color-secondary m-0">
|
|
3959
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
3960
|
+
<i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
|
|
3961
|
+
<p class="text-muted-color m-0">
|
|
4541
3962
|
No actions available for this user.
|
|
4542
3963
|
</p>
|
|
4543
3964
|
</div>
|
|
4544
3965
|
}
|
|
4545
3966
|
}
|
|
4546
3967
|
</div>
|
|
4547
|
-
`, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "
|
|
3968
|
+
`, isInline: true, styles: [":host{display:block}.validation-warning{background-color:var(--p-yellow-50, #fefce8);border-left:4px solid var(--p-yellow-500, #eab308);color:var(--p-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--p-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--p-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i3.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "component", type: i4.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "component", type: i5.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "directive", type: i6.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "component", type: i7.TreeTable, selector: "p-treeTable, p-treetable, p-tree-table", inputs: ["columns", "styleClass", "tableStyle", "tableStyleClass", "autoLayout", "lazy", "lazyLoadOnInit", "paginator", "rows", "first", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "customSort", "selectionMode", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "compareSelectionBy", "rowHover", "loading", "loadingIcon", "showLoader", "scrollable", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "frozenColumns", "resizableColumns", "columnResizeMode", "reorderableColumns", "contextMenu", "rowTrackBy", "filters", "globalFilterFields", "filterDelay", "filterMode", "filterLocale", "paginatorLocale", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "value", "virtualRowHeight", "selectionKeys", "showGridlines"], outputs: ["selectionChange", "contextMenuSelectionChange", "onFilter", "onNodeExpand", "onNodeCollapse", "onPage", "onSort", "onLazyLoad", "sortFunction", "onColResize", "onColReorder", "onNodeSelect", "onNodeUnselect", "onContextMenuSelect", "onHeaderCheckboxToggle", "onEditInit", "onEditComplete", "onEditCancel", "selectionKeysChange"] }, { kind: "component", type: i7.TreeTableToggler, selector: "p-treeTableToggler, p-treetabletoggler, p-treetable-toggler", inputs: ["rowNode"] }, { kind: "directive", type: HasPermissionDirective, selector: "[hasPermission]", inputs: ["hasPermission"] }, { kind: "component", type: UserSelectComponent, selector: "lib-user-select", inputs: ["value"], outputs: ["valueChange", "userSelected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4548
3969
|
}
|
|
4549
3970
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserActionSelectorComponent, decorators: [{
|
|
4550
3971
|
type: Component,
|
|
4551
|
-
args: [{ selector: 'flusys-user-action-selector',
|
|
3972
|
+
args: [{ selector: 'flusys-user-action-selector', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule, HasPermissionDirective, UserSelectComponent], template: `
|
|
4552
3973
|
<div class="user-action-selector">
|
|
4553
3974
|
<!-- User and Branch Selectors -->
|
|
4554
|
-
<div class="surface-card p-4 border
|
|
4555
|
-
<div class="
|
|
4556
|
-
<div
|
|
4557
|
-
<label class="block font-semibold mb-2
|
|
3975
|
+
<div class="surface-card p-4 rounded-border mb-4 shadow-sm">
|
|
3976
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
|
3977
|
+
<div>
|
|
3978
|
+
<label class="block font-semibold mb-2">
|
|
4558
3979
|
<i class="pi pi-user mr-2 text-primary"></i>
|
|
4559
3980
|
Select User
|
|
4560
3981
|
</label>
|
|
4561
3982
|
<lib-user-select
|
|
4562
|
-
[
|
|
3983
|
+
[value]="selectedUserId()"
|
|
3984
|
+
(valueChange)="selectedUserId.set($event)"
|
|
4563
3985
|
[isEditMode]="true"
|
|
4564
3986
|
placeHolder="Select a user"
|
|
4565
3987
|
/>
|
|
4566
3988
|
</div>
|
|
4567
3989
|
|
|
4568
3990
|
@if (showBranchSelector()) {
|
|
4569
|
-
<div
|
|
4570
|
-
<label class="block font-semibold mb-2
|
|
3991
|
+
<div>
|
|
3992
|
+
<label class="block font-semibold mb-2">
|
|
4571
3993
|
<i class="pi pi-building mr-2 text-primary"></i>
|
|
4572
3994
|
Select Branch
|
|
4573
3995
|
</label>
|
|
4574
3996
|
<p-select
|
|
4575
|
-
[
|
|
3997
|
+
[ngModel]="selectedBranchId()"
|
|
3998
|
+
(ngModelChange)="selectedBranchId.set($event)"
|
|
4576
3999
|
[options]="filteredBranches()"
|
|
4577
4000
|
optionLabel="name"
|
|
4578
4001
|
optionValue="id"
|
|
@@ -4583,20 +4006,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4583
4006
|
>
|
|
4584
4007
|
<ng-template #selectedItem let-branch>
|
|
4585
4008
|
@if (branch) {
|
|
4586
|
-
<div class="flex
|
|
4009
|
+
<div class="flex items-center gap-2">
|
|
4587
4010
|
<i class="pi pi-building text-primary"></i>
|
|
4588
4011
|
<span class="font-semibold">{{ branch.name }}</span>
|
|
4589
4012
|
</div>
|
|
4590
4013
|
}
|
|
4591
4014
|
</ng-template>
|
|
4592
4015
|
<ng-template #item let-branch>
|
|
4593
|
-
<div class="flex
|
|
4594
|
-
<i class="pi pi-building text-color
|
|
4016
|
+
<div class="flex items-center gap-2">
|
|
4017
|
+
<i class="pi pi-building text-muted-color"></i>
|
|
4595
4018
|
<span>{{ branch.name }}</span>
|
|
4596
4019
|
</div>
|
|
4597
4020
|
</ng-template>
|
|
4598
4021
|
</p-select>
|
|
4599
|
-
<small class="text-color
|
|
4022
|
+
<small class="text-muted-color block mt-1">
|
|
4600
4023
|
{{ filteredBranches().length }} permitted branch{{
|
|
4601
4024
|
filteredBranches().length !== 1 ? 'es' : ''
|
|
4602
4025
|
}}
|
|
@@ -4610,20 +4033,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4610
4033
|
@if (selectedUserId()) {
|
|
4611
4034
|
<!-- Loading State -->
|
|
4612
4035
|
@if (loading()) {
|
|
4613
|
-
<div class="flex justify-
|
|
4614
|
-
<i class="pi pi-spin pi-spinner
|
|
4036
|
+
<div class="flex justify-center p-5">
|
|
4037
|
+
<i class="pi pi-spin pi-spinner text-4xl"></i>
|
|
4615
4038
|
</div>
|
|
4616
4039
|
}
|
|
4617
4040
|
|
|
4618
4041
|
<!-- Action List -->
|
|
4619
4042
|
@if (!loading() && actions().length > 0) {
|
|
4620
|
-
<div class="surface-card p-4 border
|
|
4043
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
4621
4044
|
<div
|
|
4622
|
-
class="flex flex-
|
|
4045
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
4623
4046
|
>
|
|
4624
4047
|
<div>
|
|
4625
4048
|
<h5 class="m-0 mb-1">Direct Action Permissions</h5>
|
|
4626
|
-
<p class="text-sm text-color
|
|
4049
|
+
<p class="text-sm text-muted-color m-0">
|
|
4627
4050
|
{{ actions().length }} actions available
|
|
4628
4051
|
</p>
|
|
4629
4052
|
</div>
|
|
@@ -4645,6 +4068,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4645
4068
|
(onClick)="deselectAll()"
|
|
4646
4069
|
/>
|
|
4647
4070
|
<p-button
|
|
4071
|
+
*hasPermission="USER_ACTION_PERMISSIONS.ASSIGN"
|
|
4648
4072
|
label="Save Changes"
|
|
4649
4073
|
icon="pi pi-save"
|
|
4650
4074
|
[disabled]="!canSave()"
|
|
@@ -4656,34 +4080,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4656
4080
|
</div>
|
|
4657
4081
|
</div>
|
|
4658
4082
|
|
|
4083
|
+
<!-- Validation Warning -->
|
|
4084
|
+
@if (invalidActionsCount() > 0) {
|
|
4085
|
+
<div class="validation-warning rounded-border p-3 mb-4">
|
|
4086
|
+
<div class="flex items-start gap-2">
|
|
4087
|
+
<i class="pi pi-exclamation-triangle text-xl"></i>
|
|
4088
|
+
<div class="flex-1">
|
|
4089
|
+
<span class="font-semibold">Validation Warning:</span>
|
|
4090
|
+
<p class="text-sm mt-1 mb-0">
|
|
4091
|
+
{{ invalidActionsCount() }} selected
|
|
4092
|
+
action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
|
|
4093
|
+
unmet prerequisites. Fix before saving or use auto-fix on
|
|
4094
|
+
save.
|
|
4095
|
+
</p>
|
|
4096
|
+
</div>
|
|
4097
|
+
</div>
|
|
4098
|
+
</div>
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4659
4101
|
<!-- Action Tree Table -->
|
|
4660
|
-
<
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
<ng-template
|
|
4685
|
-
<tr>
|
|
4686
|
-
<td
|
|
4102
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
4103
|
+
<p-treeTable
|
|
4104
|
+
[value]="treeNodes()"
|
|
4105
|
+
dataKey="id"
|
|
4106
|
+
styleClass="p-treetable-sm"
|
|
4107
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
4108
|
+
>
|
|
4109
|
+
<ng-template #header>
|
|
4110
|
+
<tr>
|
|
4111
|
+
<th class="w-12">
|
|
4112
|
+
<p-checkbox
|
|
4113
|
+
[ngModel]="allSelected()"
|
|
4114
|
+
[binary]="true"
|
|
4115
|
+
(ngModelChange)="toggleAll()"
|
|
4116
|
+
pTooltip="Select/Deselect All"
|
|
4117
|
+
tooltipPosition="top"
|
|
4118
|
+
/>
|
|
4119
|
+
</th>
|
|
4120
|
+
<th>Name</th>
|
|
4121
|
+
<th class="hidden md:table-cell">Code</th>
|
|
4122
|
+
<th>Type</th>
|
|
4123
|
+
<th class="hidden lg:table-cell">Description</th>
|
|
4124
|
+
</tr>
|
|
4125
|
+
</ng-template>
|
|
4126
|
+
<ng-template #body let-rowNode let-rowData="rowData">
|
|
4127
|
+
<tr [class.highlight-warning]="hasUnmetPrerequisites(rowData)">
|
|
4128
|
+
<td class="w-12">
|
|
4687
4129
|
<p-checkbox
|
|
4688
4130
|
[ngModel]="selectionMap()[rowData.id]"
|
|
4689
4131
|
[binary]="true"
|
|
@@ -4694,9 +4136,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4694
4136
|
</td>
|
|
4695
4137
|
<td>
|
|
4696
4138
|
<p-treeTableToggler [rowNode]="rowNode" />
|
|
4697
|
-
<span
|
|
4139
|
+
<span class="inline-flex items-center gap-2">
|
|
4140
|
+
{{ rowData.name }}
|
|
4141
|
+
@if (hasUnmetPrerequisites(rowData)) {
|
|
4142
|
+
<i
|
|
4143
|
+
class="pi pi-exclamation-triangle text-orange-500"
|
|
4144
|
+
pTooltip="This action has unmet prerequisites and will fail validation on save"
|
|
4145
|
+
tooltipPosition="top"
|
|
4146
|
+
></i>
|
|
4147
|
+
}
|
|
4148
|
+
</span>
|
|
4698
4149
|
</td>
|
|
4699
|
-
<td>{{ rowData.code || '-' }}</td>
|
|
4150
|
+
<td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
|
|
4700
4151
|
<td>
|
|
4701
4152
|
<p-tag
|
|
4702
4153
|
[value]="rowData.actionType"
|
|
@@ -4705,12 +4156,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4705
4156
|
"
|
|
4706
4157
|
/>
|
|
4707
4158
|
</td>
|
|
4708
|
-
<td>{{ rowData.description || '-' }}</td>
|
|
4159
|
+
<td class="hidden lg:table-cell">{{ rowData.description || '-' }}</td>
|
|
4709
4160
|
</tr>
|
|
4710
4161
|
</ng-template>
|
|
4711
|
-
<ng-template
|
|
4162
|
+
<ng-template #emptymessage>
|
|
4712
4163
|
<tr>
|
|
4713
|
-
<td colspan="5" class="text-center p-4">
|
|
4164
|
+
<td colspan="5" class="text-center p-4 text-muted-color">
|
|
4714
4165
|
@if (loading()) {
|
|
4715
4166
|
<i class="pi pi-spin pi-spinner"></i> Loading actions...
|
|
4716
4167
|
} @else {
|
|
@@ -4719,24 +4170,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4719
4170
|
</td>
|
|
4720
4171
|
</tr>
|
|
4721
4172
|
</ng-template>
|
|
4722
|
-
|
|
4173
|
+
</p-treeTable>
|
|
4174
|
+
</div>
|
|
4723
4175
|
</div>
|
|
4724
4176
|
|
|
4725
4177
|
<!-- Change Summary -->
|
|
4726
4178
|
@if (hasChanges()) {
|
|
4727
|
-
<div class="
|
|
4728
|
-
<div class="flex
|
|
4729
|
-
<i class="pi pi-info-circle text-
|
|
4730
|
-
<
|
|
4179
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
4180
|
+
<div class="flex items-center gap-2 mb-3">
|
|
4181
|
+
<i class="pi pi-info-circle text-primary"></i>
|
|
4182
|
+
<span class="font-bold">Pending Changes</span>
|
|
4731
4183
|
</div>
|
|
4732
|
-
<div class="
|
|
4184
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
4733
4185
|
@if (pendingAdd().length > 0) {
|
|
4734
|
-
<div
|
|
4735
|
-
<div class="flex
|
|
4186
|
+
<div>
|
|
4187
|
+
<div class="flex items-center gap-2 mb-2">
|
|
4736
4188
|
<i class="pi pi-plus-circle text-green-500"></i>
|
|
4737
|
-
<strong class="text-sm"
|
|
4738
|
-
>To Assign ({{ pendingAdd().length }})</strong
|
|
4739
|
-
>
|
|
4189
|
+
<strong class="text-sm">To Assign ({{ pendingAdd().length }})</strong>
|
|
4740
4190
|
</div>
|
|
4741
4191
|
<ul class="list-none p-0 m-0 pl-4">
|
|
4742
4192
|
@for (action of pendingAdd(); track action.id) {
|
|
@@ -4746,12 +4196,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4746
4196
|
</div>
|
|
4747
4197
|
}
|
|
4748
4198
|
@if (pendingRemove().length > 0) {
|
|
4749
|
-
<div
|
|
4750
|
-
<div class="flex
|
|
4199
|
+
<div>
|
|
4200
|
+
<div class="flex items-center gap-2 mb-2">
|
|
4751
4201
|
<i class="pi pi-minus-circle text-red-500"></i>
|
|
4752
|
-
<strong class="text-sm"
|
|
4753
|
-
>To Remove ({{ pendingRemove().length }})</strong
|
|
4754
|
-
>
|
|
4202
|
+
<strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
|
|
4755
4203
|
</div>
|
|
4756
4204
|
<ul class="list-none p-0 m-0 pl-4">
|
|
4757
4205
|
@for (action of pendingRemove(); track action.id) {
|
|
@@ -4766,19 +4214,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4766
4214
|
}
|
|
4767
4215
|
|
|
4768
4216
|
@if (!loading() && actions().length === 0) {
|
|
4769
|
-
<div class="surface-card p-5 border
|
|
4770
|
-
<i
|
|
4771
|
-
|
|
4772
|
-
style="font-size: 3rem; display: block;"
|
|
4773
|
-
></i>
|
|
4774
|
-
<p class="text-color-secondary m-0">
|
|
4217
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
4218
|
+
<i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
|
|
4219
|
+
<p class="text-muted-color m-0">
|
|
4775
4220
|
No actions available for this user.
|
|
4776
4221
|
</p>
|
|
4777
4222
|
</div>
|
|
4778
4223
|
}
|
|
4779
4224
|
}
|
|
4780
4225
|
</div>
|
|
4781
|
-
`, styles: [":host{display:block}\n"] }]
|
|
4226
|
+
`, styles: [":host{display:block}.validation-warning{background-color:var(--p-yellow-50, #fefce8);border-left:4px solid var(--p-yellow-500, #eab308);color:var(--p-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--p-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--p-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"] }]
|
|
4782
4227
|
}], ctorParameters: () => [] });
|
|
4783
4228
|
|
|
4784
4229
|
// Logic Builder Component
|
|
@@ -4835,90 +4280,76 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4835
4280
|
type: Injectable
|
|
4836
4281
|
}] });
|
|
4837
4282
|
|
|
4838
|
-
/**
|
|
4839
|
-
* Provide IAM Provider Adapters
|
|
4840
|
-
*
|
|
4841
|
-
* Registers IAM implementations for provider interfaces from ng-shared.
|
|
4842
|
-
* This allows ng-auth profile page to display permissions without direct dependencies.
|
|
4843
|
-
*
|
|
4844
|
-
* @example
|
|
4845
|
-
* // In app.config.ts
|
|
4846
|
-
* import { provideIamProviders } from '@flusys/ng-iam';
|
|
4847
|
-
*
|
|
4848
|
-
* export const appConfig: ApplicationConfig = {
|
|
4849
|
-
* providers: [
|
|
4850
|
-
* ...provideIamProviders(),
|
|
4851
|
-
* // ... other providers
|
|
4852
|
-
* ]
|
|
4853
|
-
* };
|
|
4854
|
-
*
|
|
4855
|
-
* @returns Array of Angular providers
|
|
4856
|
-
*/
|
|
4283
|
+
/** Registers IAM provider adapters for ng-shared interfaces */
|
|
4857
4284
|
function provideIamProviders() {
|
|
4858
4285
|
return [
|
|
4859
|
-
|
|
4860
|
-
{
|
|
4861
|
-
provide: PROFILE_PERMISSION_PROVIDER,
|
|
4862
|
-
useClass: ProfilePermissionProviderAdapter,
|
|
4863
|
-
},
|
|
4286
|
+
{ provide: PROFILE_PERMISSION_PROVIDER, useClass: ProfilePermissionProviderAdapter },
|
|
4864
4287
|
];
|
|
4865
4288
|
}
|
|
4866
4289
|
|
|
4867
4290
|
/**
|
|
4868
4291
|
* IAM Routes Configuration
|
|
4869
4292
|
*
|
|
4870
|
-
* Identity and Access Management routing
|
|
4871
|
-
* - Actions: Permission actions
|
|
4293
|
+
* Identity and Access Management routing with permission guards.
|
|
4294
|
+
* - Actions: Permission actions management
|
|
4872
4295
|
* - Roles: Role management (conditional on RBAC/FULL mode)
|
|
4873
|
-
* - Permissions: User permission assignments
|
|
4874
|
-
*
|
|
4875
|
-
* All routes are protected by permission guards to prevent direct URL access.
|
|
4296
|
+
* - Permissions: User permission assignments
|
|
4876
4297
|
*/
|
|
4877
4298
|
const IAM_ROUTES = [
|
|
4878
4299
|
{
|
|
4879
4300
|
path: '',
|
|
4880
|
-
loadComponent: () => import('./flusys-ng-iam-iam-container.component-
|
|
4301
|
+
loadComponent: () => import('./flusys-ng-iam-iam-container.component-BToYxEej.mjs').then((m) => m.IamContainerComponent),
|
|
4881
4302
|
children: [
|
|
4882
4303
|
// Actions Management
|
|
4883
4304
|
{
|
|
4884
4305
|
path: 'actions',
|
|
4306
|
+
canActivate: [permissionGuard(ACTION_PERMISSIONS.READ)],
|
|
4885
4307
|
children: [
|
|
4886
4308
|
{
|
|
4887
4309
|
path: '',
|
|
4888
|
-
loadComponent: () => import('./flusys-ng-iam-action-list-page.component-
|
|
4310
|
+
loadComponent: () => import('./flusys-ng-iam-action-list-page.component-CQ6RazN0.mjs').then((m) => m.ActionListPageComponent),
|
|
4889
4311
|
},
|
|
4890
4312
|
{
|
|
4891
4313
|
path: 'new',
|
|
4892
|
-
loadComponent: () => import('./flusys-ng-iam-action-form-page.component-
|
|
4314
|
+
loadComponent: () => import('./flusys-ng-iam-action-form-page.component-CVN8sV-c.mjs').then((m) => m.ActionFormPageComponent),
|
|
4893
4315
|
},
|
|
4894
4316
|
{
|
|
4895
4317
|
path: ':id',
|
|
4896
|
-
loadComponent: () => import('./flusys-ng-iam-action-form-page.component-
|
|
4318
|
+
loadComponent: () => import('./flusys-ng-iam-action-form-page.component-CVN8sV-c.mjs').then((m) => m.ActionFormPageComponent),
|
|
4897
4319
|
},
|
|
4898
4320
|
],
|
|
4899
4321
|
},
|
|
4900
4322
|
// Roles Management
|
|
4901
4323
|
{
|
|
4902
4324
|
path: 'roles',
|
|
4325
|
+
canActivate: [permissionGuard(ROLE_PERMISSIONS.READ)],
|
|
4903
4326
|
children: [
|
|
4904
4327
|
{
|
|
4905
4328
|
path: '',
|
|
4906
|
-
loadComponent: () => import('./flusys-ng-iam-role-list-page.component-
|
|
4329
|
+
loadComponent: () => import('./flusys-ng-iam-role-list-page.component-Cz-jk-R_.mjs').then((m) => m.RoleListPageComponent),
|
|
4907
4330
|
},
|
|
4908
4331
|
{
|
|
4909
4332
|
path: 'new',
|
|
4910
|
-
loadComponent: () => import('./flusys-ng-iam-role-form-page.component-
|
|
4333
|
+
loadComponent: () => import('./flusys-ng-iam-role-form-page.component-BjPwXkip.mjs').then((m) => m.RoleFormPageComponent),
|
|
4911
4334
|
},
|
|
4912
4335
|
{
|
|
4913
4336
|
path: ':id',
|
|
4914
|
-
loadComponent: () => import('./flusys-ng-iam-role-form-page.component-
|
|
4337
|
+
loadComponent: () => import('./flusys-ng-iam-role-form-page.component-BjPwXkip.mjs').then((m) => m.RoleFormPageComponent),
|
|
4915
4338
|
},
|
|
4916
4339
|
],
|
|
4917
4340
|
},
|
|
4918
|
-
// Permissions Management (
|
|
4341
|
+
// Permissions Management (requires any permission tab access)
|
|
4919
4342
|
{
|
|
4920
4343
|
path: 'permissions',
|
|
4921
|
-
|
|
4344
|
+
canActivate: [
|
|
4345
|
+
anyPermissionGuard([
|
|
4346
|
+
ROLE_ACTION_PERMISSIONS.READ,
|
|
4347
|
+
USER_ROLE_PERMISSIONS.READ,
|
|
4348
|
+
USER_ACTION_PERMISSIONS.READ,
|
|
4349
|
+
COMPANY_ACTION_PERMISSIONS.READ,
|
|
4350
|
+
]),
|
|
4351
|
+
],
|
|
4352
|
+
loadComponent: () => import('./flusys-ng-iam-permission-page.component-BS7xXmsn.mjs').then((m) => m.PermissionPageComponent),
|
|
4922
4353
|
},
|
|
4923
4354
|
// Default redirect to actions
|
|
4924
4355
|
{
|
|
@@ -4937,4 +4368,4 @@ const IAM_ROUTES = [
|
|
|
4937
4368
|
*/
|
|
4938
4369
|
|
|
4939
4370
|
export { ActionApiService as A, CompanyActionSelectorComponent as C, IAM_ROUTES as I, LogicBuilderComponent as L, MAX_DROPDOWN_ITEMS as M, PermissionApiService as P, RoleApiService as R, UserRoleSelectorComponent as U, ActionType as a, RoleActionSelectorComponent as b, convertActionToTreeNode as c, UserActionSelectorComponent as d, ActionPermissionLogicService as e, MyPermissionsApiService as f, PermissionStateService as g, ProfilePermissionProviderAdapter as h, provideIamProviders as p };
|
|
4940
|
-
//# sourceMappingURL=flusys-ng-iam-flusys-ng-iam-
|
|
4371
|
+
//# sourceMappingURL=flusys-ng-iam-flusys-ng-iam-DrGHlTiz.mjs.map
|