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