@flusys/ng-iam 1.1.0-beta → 2.0.0-rc.0

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