@flusys/ng-iam 0.1.0-beta.3 → 1.0.0-rc

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 (28) hide show
  1. package/README.md +225 -25
  2. package/fesm2022/{flusys-ng-iam-action-form-page.component-BVWZMlLU.mjs → flusys-ng-iam-action-form-page.component-C_BRrrWW.mjs} +181 -210
  3. package/fesm2022/flusys-ng-iam-action-form-page.component-C_BRrrWW.mjs.map +1 -0
  4. package/fesm2022/flusys-ng-iam-action-list-page.component-Daf93zpS.mjs +289 -0
  5. package/fesm2022/flusys-ng-iam-action-list-page.component-Daf93zpS.mjs.map +1 -0
  6. package/fesm2022/{flusys-ng-iam-flusys-ng-iam-D_i24Gub.mjs → flusys-ng-iam-flusys-ng-iam-BPIpfrjN.mjs} +967 -1087
  7. package/fesm2022/flusys-ng-iam-flusys-ng-iam-BPIpfrjN.mjs.map +1 -0
  8. package/fesm2022/flusys-ng-iam-iam-container.component-Bn4kQtxW.mjs +92 -0
  9. package/fesm2022/flusys-ng-iam-iam-container.component-Bn4kQtxW.mjs.map +1 -0
  10. package/fesm2022/{flusys-ng-iam-permission-page.component-C0yLMdW5.mjs → flusys-ng-iam-permission-page.component-CmxOBJPu.mjs} +45 -16
  11. package/fesm2022/flusys-ng-iam-permission-page.component-CmxOBJPu.mjs.map +1 -0
  12. package/fesm2022/{flusys-ng-iam-role-form-page.component-Co6bbHik.mjs → flusys-ng-iam-role-form-page.component-ByNueI1a.mjs} +110 -138
  13. package/fesm2022/flusys-ng-iam-role-form-page.component-ByNueI1a.mjs.map +1 -0
  14. package/fesm2022/flusys-ng-iam-role-list-page.component-CFly5KnH.mjs +316 -0
  15. package/fesm2022/flusys-ng-iam-role-list-page.component-CFly5KnH.mjs.map +1 -0
  16. package/fesm2022/flusys-ng-iam.mjs +1 -1
  17. package/package.json +5 -5
  18. package/types/flusys-ng-iam.d.ts +89 -37
  19. package/fesm2022/flusys-ng-iam-action-form-page.component-BVWZMlLU.mjs.map +0 -1
  20. package/fesm2022/flusys-ng-iam-action-list-page.component-CCp7M7xo.mjs +0 -282
  21. package/fesm2022/flusys-ng-iam-action-list-page.component-CCp7M7xo.mjs.map +0 -1
  22. package/fesm2022/flusys-ng-iam-flusys-ng-iam-D_i24Gub.mjs.map +0 -1
  23. package/fesm2022/flusys-ng-iam-iam-container.component-BFKoNtdz.mjs +0 -97
  24. package/fesm2022/flusys-ng-iam-iam-container.component-BFKoNtdz.mjs.map +0 -1
  25. package/fesm2022/flusys-ng-iam-permission-page.component-C0yLMdW5.mjs.map +0 -1
  26. package/fesm2022/flusys-ng-iam-role-form-page.component-Co6bbHik.mjs.map +0 -1
  27. package/fesm2022/flusys-ng-iam-role-list-page.component-KJUVOrf0.mjs +0 -267
  28. package/fesm2022/flusys-ng-iam-role-list-page.component-KJUVOrf0.mjs.map +0 -1
@@ -1,23 +1,21 @@
1
1
  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_PROVIDER, USER_PERMISSION_PROVIDER } from '@flusys/ng-shared';
2
+ import { inject, Injectable, signal, input, output, computed, effect, ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core';
3
+ import { HttpClient } from '@angular/common/http';
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 } from '@flusys/ng-shared';
5
5
  import { APP_CONFIG, BaseApiService, isCompanyFeatureEnabled } from '@flusys/ng-core';
6
- import * as i3$1 from 'primeng/api';
7
6
  import { ConfirmationService, MessageService } from 'primeng/api';
8
- import { of, firstValueFrom } from 'rxjs';
7
+ import { of, firstValueFrom, map as map$1 } from 'rxjs';
9
8
  import { tap, catchError, map } from 'rxjs/operators';
10
9
  import * as i1 from '@angular/common';
11
10
  import { CommonModule } from '@angular/common';
12
11
  import * as i1$1 from '@angular/forms';
13
12
  import { FormsModule } from '@angular/forms';
14
- import * as i3 from 'primeng/button';
15
- import { ButtonModule } from 'primeng/button';
16
- import * as i5 from 'primeng/tooltip';
17
- import * as i2 from 'primeng/tag';
18
- import * as i6 from 'primeng/checkbox';
19
- import * as i7 from 'primeng/select';
20
- import * as i8 from 'primeng/treetable';
13
+ import * as i2 from 'primeng/button';
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
 
@@ -60,10 +58,10 @@ class RoleApiService extends ApiResourceService {
60
58
  const http = inject(HttpClient);
61
59
  super('iam/roles', http);
62
60
  }
63
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RoleApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
64
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RoleApiService, providedIn: 'root' });
61
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RoleApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
62
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RoleApiService, providedIn: 'root' });
65
63
  }
66
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RoleApiService, decorators: [{
64
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RoleApiService, decorators: [{
67
65
  type: Injectable,
68
66
  args: [{
69
67
  providedIn: 'root',
@@ -83,11 +81,11 @@ class ActionApiService extends ApiResourceService {
83
81
  }
84
82
  /**
85
83
  * Get actions for permission assignment
86
- * GET /iam/actions/tree-for-permission
84
+ * POST /iam/actions/tree-for-permission
87
85
  * Returns actions filtered by company whitelist if enabled
88
86
  */
89
87
  getActionsForPermission() {
90
- return this.http.get(`${this.appConfig.apiBaseUrl}/iam/actions/tree-for-permission`);
88
+ return this.http.post(`${this.appConfig.apiBaseUrl}/iam/actions/tree-for-permission`, {});
91
89
  }
92
90
  /**
93
91
  * Get actions in hierarchical tree structure
@@ -112,10 +110,10 @@ class ActionApiService extends ApiResourceService {
112
110
  }
113
111
  return this.http.post(`${this.appConfig.apiBaseUrl}/iam/actions/tree`, body);
114
112
  }
115
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
116
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionApiService, providedIn: 'root' });
113
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ActionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
114
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ActionApiService, providedIn: 'root' });
117
115
  }
118
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionApiService, decorators: [{
116
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ActionApiService, decorators: [{
119
117
  type: Injectable,
120
118
  args: [{
121
119
  providedIn: 'root',
@@ -435,7 +433,7 @@ class ActionPermissionLogicService {
435
433
  const selectedActionIds = new Set(Object.keys(currentSelection).filter((id) => currentSelection[id]));
436
434
  const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
437
435
  if (validationResult.valid) {
438
- return ' Prerequisites Satisfied\n\nAll required actions are already selected.\nYou can safely add this action.';
436
+ return '[OK] Prerequisites Satisfied\n\nAll required actions are already selected.\nYou can safely add this action.';
439
437
  }
440
438
  const allMissingActions = this.getAllMissingPrerequisitesRecursive(action, currentSelection, allActions);
441
439
  const logicTree = this.buildTooltipLogicTree(action.permissionLogic, currentSelection, allActions, 0);
@@ -497,13 +495,11 @@ class ActionPermissionLogicService {
497
495
  if (!targetAction.permissionLogic || !targetAction.id)
498
496
  return;
499
497
  if (depth > 50) {
500
- console.error('[Permission Logic] Circular dependency detected', targetAction.id);
501
498
  return;
502
499
  }
503
500
  if (visited.has(targetAction.id))
504
501
  return;
505
502
  if (stack.has(targetAction.id)) {
506
- console.warn('[Permission Logic] Circular reference detected:', targetAction.id);
507
503
  return;
508
504
  }
509
505
  visited.add(targetAction.id);
@@ -800,10 +796,10 @@ class ActionPermissionLogicService {
800
796
  }
801
797
  return `${indent}(invalid)`;
802
798
  }
803
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionPermissionLogicService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
804
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionPermissionLogicService, providedIn: 'root' });
799
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ActionPermissionLogicService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
800
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ActionPermissionLogicService, providedIn: 'root' });
805
801
  }
806
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionPermissionLogicService, decorators: [{
802
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ActionPermissionLogicService, decorators: [{
807
803
  type: Injectable,
808
804
  args: [{
809
805
  providedIn: 'root',
@@ -837,14 +833,10 @@ class PermissionApiService extends BaseApiService {
837
833
  }
838
834
  /**
839
835
  * Get user's direct action permissions
840
- * GET /permissions/user-actions/:userId
836
+ * POST /iam/permissions/get-user-actions
841
837
  */
842
838
  getUserActions(userId, query) {
843
- let params = new HttpParams();
844
- if (query?.branchId) {
845
- params = params.set('branchId', query.branchId);
846
- }
847
- return this.http.get(this.getUrl(`permissions/user-actions/${userId}`), { params });
839
+ return this.http.post(this.getUrl('permissions/get-user-actions'), { userId, branchId: query?.branchId });
848
840
  }
849
841
  // =============================================================================
850
842
  // User → Role (Role Assignments)
@@ -858,14 +850,10 @@ class PermissionApiService extends BaseApiService {
858
850
  }
859
851
  /**
860
852
  * Get user's role assignments
861
- * GET /permissions/user-roles/:userId
853
+ * POST /iam/permissions/get-user-roles
862
854
  */
863
855
  getUserRoles(userId, query) {
864
- let params = new HttpParams();
865
- if (query?.branchId) {
866
- params = params.set('branchId', query.branchId);
867
- }
868
- return this.http.get(this.getUrl(`permissions/user-roles/${userId}`), { params });
856
+ return this.http.post(this.getUrl('permissions/get-user-roles'), { userId, branchId: query?.branchId });
869
857
  }
870
858
  // =============================================================================
871
859
  // Role → Action (Role Permissions)
@@ -879,10 +867,10 @@ class PermissionApiService extends BaseApiService {
879
867
  }
880
868
  /**
881
869
  * Get role's action permissions
882
- * GET /permissions/role-actions/:roleId
870
+ * POST /iam/permissions/get-role-actions
883
871
  */
884
872
  getRoleActions(roleId, query) {
885
- return this.http.get(this.getUrl(`permissions/role-actions/${roleId}`));
873
+ return this.http.post(this.getUrl('permissions/get-role-actions'), { roleId });
886
874
  }
887
875
  // =============================================================================
888
876
  // Company → Action (Company Whitelisting)
@@ -896,15 +884,15 @@ class PermissionApiService extends BaseApiService {
896
884
  }
897
885
  /**
898
886
  * Get company's whitelisted actions
899
- * GET /permissions/company-actions/:companyId
887
+ * POST /iam/permissions/get-company-actions
900
888
  */
901
889
  getCompanyActions(companyId) {
902
- return this.http.get(this.getUrl(`permissions/company-actions/${companyId}`));
890
+ return this.http.post(this.getUrl('permissions/get-company-actions'), { companyId });
903
891
  }
904
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PermissionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
905
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PermissionApiService, providedIn: 'root' });
892
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
893
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionApiService, providedIn: 'root' });
906
894
  }
907
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PermissionApiService, decorators: [{
895
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionApiService, decorators: [{
908
896
  type: Injectable,
909
897
  args: [{
910
898
  providedIn: 'root',
@@ -933,10 +921,10 @@ class MyPermissionsApiService {
933
921
  getMyPermissions(dto = {}) {
934
922
  return this.http.post(`${this.baseUrl}/my-permissions`, dto);
935
923
  }
936
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MyPermissionsApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
937
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MyPermissionsApiService, providedIn: 'root' });
924
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MyPermissionsApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
925
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MyPermissionsApiService, providedIn: 'root' });
938
926
  }
939
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MyPermissionsApiService, decorators: [{
927
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MyPermissionsApiService, decorators: [{
940
928
  type: Injectable,
941
929
  args: [{
942
930
  providedIn: 'root',
@@ -986,11 +974,9 @@ class PermissionStateService {
986
974
  // Update shared permission validator
987
975
  const actionCodes = response.data?.frontendActions.map((a) => a.code) ?? [];
988
976
  this.permissionValidator.setPermissions(actionCodes);
989
- }), catchError((error) => {
990
- console.error('Failed to load permissions:', error);
977
+ }), catchError(() => {
991
978
  this._permissions.set(null);
992
979
  this._isLoading.set(false);
993
- // Clear shared permission validator
994
980
  this.permissionValidator.clearPermissions();
995
981
  return of(void 0);
996
982
  }), map(() => void 0));
@@ -1004,10 +990,10 @@ class PermissionStateService {
1004
990
  return (this._permissions() !== null &&
1005
991
  this.permissionValidator.isPermissionsLoaded());
1006
992
  }
1007
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PermissionStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1008
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PermissionStateService, providedIn: 'root' });
993
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
994
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionStateService, providedIn: 'root' });
1009
995
  }
1010
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PermissionStateService, decorators: [{
996
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionStateService, decorators: [{
1011
997
  type: Injectable,
1012
998
  args: [{ providedIn: 'root' }]
1013
999
  }] });
@@ -1041,77 +1027,85 @@ class LogicBuilderComponent {
1041
1027
  actions = input([], ...(ngDevMode ? [{ debugName: "actions" }] : []));
1042
1028
  logicChange = output();
1043
1029
  availableActions = computed(() => this.actions(), ...(ngDevMode ? [{ debugName: "availableActions" }] : []));
1044
- builderTree = null;
1045
- builderLogic = computed(() => {
1046
- const logic = this.logic();
1047
- if (!logic) {
1048
- this.builderTree = null;
1049
- return null;
1050
- }
1051
- if (!this.builderTree) {
1052
- this.builderTree = toBuilderNode(logic);
1053
- }
1054
- return this.builderTree;
1055
- }, ...(ngDevMode ? [{ debugName: "builderLogic" }] : []));
1030
+ /** Internal builder tree state (private writable + public readonly pattern) */
1031
+ _builderTree = signal(null, ...(ngDevMode ? [{ debugName: "_builderTree" }] : []));
1032
+ builderLogic = this._builderTree.asReadonly();
1033
+ constructor() {
1034
+ // Sync builderTree with logic input changes
1035
+ effect(() => {
1036
+ const logic = this.logic();
1037
+ if (!logic) {
1038
+ this._builderTree.set(null);
1039
+ }
1040
+ else if (!this._builderTree()) {
1041
+ this._builderTree.set(toBuilderNode(logic));
1042
+ }
1043
+ }, { allowSignalWrites: true });
1044
+ }
1056
1045
  initializeLogic() {
1057
- this.builderTree = {
1046
+ this._builderTree.set({
1058
1047
  id: crypto.randomUUID(),
1059
1048
  type: 'group',
1060
1049
  operator: 'AND',
1061
1050
  children: [],
1062
- };
1051
+ });
1063
1052
  this.emitChange();
1064
1053
  }
1065
1054
  clearLogic() {
1066
- this.builderTree = null;
1055
+ this._builderTree.set(null);
1067
1056
  this.logicChange.emit(null);
1068
1057
  }
1069
1058
  toggleOperator(nodeId) {
1070
- if (!this.builderTree)
1059
+ const tree = this._builderTree();
1060
+ if (!tree)
1071
1061
  return;
1072
- this.builderTree = this.updateNodeInTree(this.builderTree, nodeId, (node) => ({
1062
+ this._builderTree.set(this.updateNodeInTree(tree, nodeId, (node) => ({
1073
1063
  ...node,
1074
1064
  operator: node.operator === 'AND' ? 'OR' : 'AND',
1075
- }));
1065
+ })));
1076
1066
  this.emitChange();
1077
1067
  }
1078
1068
  addChildNode(parentId, type) {
1079
- if (!this.builderTree)
1069
+ const tree = this._builderTree();
1070
+ if (!tree)
1080
1071
  return;
1081
1072
  const newNode = type === 'group'
1082
1073
  ? { id: crypto.randomUUID(), type: 'group', operator: 'AND', children: [] }
1083
1074
  : { id: crypto.randomUUID(), type: 'action', actionId: '' };
1084
- this.builderTree = this.updateNodeInTree(this.builderTree, parentId, (node) => ({
1075
+ this._builderTree.set(this.updateNodeInTree(tree, parentId, (node) => ({
1085
1076
  ...node,
1086
1077
  children: [...(node.children || []), newNode],
1087
- }));
1078
+ })));
1088
1079
  this.emitChange();
1089
1080
  }
1090
1081
  removeNode(nodeId) {
1091
- if (!this.builderTree)
1082
+ const tree = this._builderTree();
1083
+ if (!tree)
1092
1084
  return;
1093
- if (this.builderTree.id === nodeId) {
1085
+ if (tree.id === nodeId) {
1094
1086
  this.clearLogic();
1095
1087
  return;
1096
1088
  }
1097
- this.builderTree = this.removeNodeFromTree(this.builderTree, nodeId);
1089
+ this._builderTree.set(this.removeNodeFromTree(tree, nodeId));
1098
1090
  this.emitChange();
1099
1091
  }
1100
1092
  updateActionId(nodeId, actionId) {
1101
- if (!this.builderTree)
1093
+ const tree = this._builderTree();
1094
+ if (!tree)
1102
1095
  return;
1103
- this.builderTree = this.updateNodeInTree(this.builderTree, nodeId, (node) => ({
1096
+ this._builderTree.set(this.updateNodeInTree(tree, nodeId, (node) => ({
1104
1097
  ...node,
1105
1098
  actionId,
1106
- }));
1099
+ })));
1107
1100
  this.emitChange();
1108
1101
  }
1109
1102
  emitChange() {
1110
- if (!this.builderTree) {
1103
+ const tree = this._builderTree();
1104
+ if (!tree) {
1111
1105
  this.logicChange.emit(null);
1112
1106
  return;
1113
1107
  }
1114
- this.logicChange.emit(toLogicNode(this.builderTree));
1108
+ this.logicChange.emit(toLogicNode(tree));
1115
1109
  }
1116
1110
  updateNodeInTree(node, targetId, updater) {
1117
1111
  if (node.id === targetId)
@@ -1135,11 +1129,11 @@ class LogicBuilderComponent {
1135
1129
  }
1136
1130
  return node;
1137
1131
  }
1138
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: LogicBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1139
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", 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: `
1132
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LogicBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1133
+ 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: `
1140
1134
  <div class="logic-builder">
1141
1135
  <div class="flex justify-between items-center mb-3">
1142
- <h4 class="text-sm font-semibold">Permission Logic</h4>
1136
+ <h4 class="text-sm font-semibold m-0">Permission Logic</h4>
1143
1137
  @if (!builderLogic()) {
1144
1138
  <p-button
1145
1139
  label="Add Logic"
@@ -1158,13 +1152,13 @@ class LogicBuilderComponent {
1158
1152
  </div>
1159
1153
 
1160
1154
  @if (builderLogic()) {
1161
- <div class="border rounded p-3 bg-gray-50">
1162
- <div class="mb-3 text-sm text-gray-600">
1155
+ <div class="border border-surface rounded p-3 bg-surface-50">
1156
+ <div class="mb-3 text-sm text-muted-color">
1163
1157
  Define permission requirements using AND/OR logic with actions
1164
1158
  </div>
1165
1159
 
1166
1160
  <!-- Root Node -->
1167
- <div class="logic-tree bg-white rounded border">
1161
+ <div class="text-sm bg-surface-0 rounded border border-surface">
1168
1162
  <ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: builderLogic()!, depth: 0 }"></ng-container>
1169
1163
  </div>
1170
1164
  </div>
@@ -1173,11 +1167,11 @@ class LogicBuilderComponent {
1173
1167
 
1174
1168
  <!-- Recursive Node Template -->
1175
1169
  <ng-template #nodeTemplate let-node let-depth="depth">
1176
- <div class="node" [style.background-color]="depth % 2 === 0 ? '#ffffff' : '#f9fafb'">
1177
- <div class="node-header">
1170
+ <div class="p-3 border-b border-surface" [ngClass]="depth % 2 === 0 ? 'bg-surface-0' : 'bg-surface-50'">
1171
+ <div class="flex items-center gap-3 mb-2">
1178
1172
  <span class="node-type" [ngClass]="node.type">{{ node.type.toUpperCase() }}</span>
1179
1173
 
1180
- <div class="node-content">
1174
+ <div class="flex-1 flex items-center gap-2">
1181
1175
  @if (node.type === 'group') {
1182
1176
  <!-- Group: show operator toggle -->
1183
1177
  <span
@@ -1187,13 +1181,13 @@ class LogicBuilderComponent {
1187
1181
  title="Click to toggle">
1188
1182
  {{ node.operator }}
1189
1183
  </span>
1190
- <span class="text-gray-600 text-xs">({{ node.children?.length || 0 }} conditions)</span>
1184
+ <span class="text-muted-color text-xs">({{ node.children?.length || 0 }} conditions)</span>
1191
1185
  }
1192
1186
 
1193
1187
  @if (node.type === 'action') {
1194
1188
  <!-- Action: show action selector -->
1195
1189
  <select
1196
- class="flex-1"
1190
+ class="action-select flex-1 p-2 border border-surface rounded text-sm"
1197
1191
  [ngModel]="node.actionId"
1198
1192
  (ngModelChange)="updateActionId(node.id, $event)">
1199
1193
  <option [value]="null">Select Action... ({{ availableActions().length }} available)</option>
@@ -1204,7 +1198,7 @@ class LogicBuilderComponent {
1204
1198
  }
1205
1199
  </div>
1206
1200
 
1207
- <div class="node-actions">
1201
+ <div class="flex gap-1">
1208
1202
  <p-button
1209
1203
  icon="pi pi-trash"
1210
1204
  [text]="true"
@@ -1217,7 +1211,7 @@ class LogicBuilderComponent {
1217
1211
 
1218
1212
  <!-- Children for group nodes -->
1219
1213
  @if (node.type === 'group' && node.children && node.children.length > 0) {
1220
- <div class="node-children">
1214
+ <div class="ml-5 mt-2 pl-3 border-l-2 border-surface">
1221
1215
  @for (child of node.children; track child.id) {
1222
1216
  <ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: child, depth: depth + 1 }"></ng-container>
1223
1217
  }
@@ -1226,9 +1220,9 @@ class LogicBuilderComponent {
1226
1220
 
1227
1221
  <!-- Add child buttons for group nodes -->
1228
1222
  @if (node.type === 'group') {
1229
- <div class="add-child-section">
1230
- <div class="text-xs font-semibold text-gray-600 mb-2">Add Condition:</div>
1231
- <div class="add-child-buttons">
1223
+ <div class="mt-3 p-3 bg-surface-50 rounded border border-dashed border-surface">
1224
+ <div class="text-xs font-semibold text-muted-color mb-2">Add Condition:</div>
1225
+ <div class="flex gap-2 flex-wrap">
1232
1226
  <p-button
1233
1227
  label="Group"
1234
1228
  icon="pi pi-sitemap"
@@ -1247,14 +1241,14 @@ class LogicBuilderComponent {
1247
1241
  }
1248
1242
  </div>
1249
1243
  </ng-template>
1250
- `, 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: i3.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: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "ngmodule", type: ButtonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1244
+ `, 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 });
1251
1245
  }
1252
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: LogicBuilderComponent, decorators: [{
1246
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LogicBuilderComponent, decorators: [{
1253
1247
  type: Component,
1254
- args: [{ selector: 'lib-logic-builder', standalone: true, imports: [CommonModule, FormsModule, AngularModule, PrimeModule, ButtonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1248
+ args: [{ selector: 'lib-logic-builder', standalone: true, imports: [AngularModule, PrimeModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1255
1249
  <div class="logic-builder">
1256
1250
  <div class="flex justify-between items-center mb-3">
1257
- <h4 class="text-sm font-semibold">Permission Logic</h4>
1251
+ <h4 class="text-sm font-semibold m-0">Permission Logic</h4>
1258
1252
  @if (!builderLogic()) {
1259
1253
  <p-button
1260
1254
  label="Add Logic"
@@ -1273,13 +1267,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
1273
1267
  </div>
1274
1268
 
1275
1269
  @if (builderLogic()) {
1276
- <div class="border rounded p-3 bg-gray-50">
1277
- <div class="mb-3 text-sm text-gray-600">
1270
+ <div class="border border-surface rounded p-3 bg-surface-50">
1271
+ <div class="mb-3 text-sm text-muted-color">
1278
1272
  Define permission requirements using AND/OR logic with actions
1279
1273
  </div>
1280
1274
 
1281
1275
  <!-- Root Node -->
1282
- <div class="logic-tree bg-white rounded border">
1276
+ <div class="text-sm bg-surface-0 rounded border border-surface">
1283
1277
  <ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: builderLogic()!, depth: 0 }"></ng-container>
1284
1278
  </div>
1285
1279
  </div>
@@ -1288,11 +1282,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
1288
1282
 
1289
1283
  <!-- Recursive Node Template -->
1290
1284
  <ng-template #nodeTemplate let-node let-depth="depth">
1291
- <div class="node" [style.background-color]="depth % 2 === 0 ? '#ffffff' : '#f9fafb'">
1292
- <div class="node-header">
1285
+ <div class="p-3 border-b border-surface" [ngClass]="depth % 2 === 0 ? 'bg-surface-0' : 'bg-surface-50'">
1286
+ <div class="flex items-center gap-3 mb-2">
1293
1287
  <span class="node-type" [ngClass]="node.type">{{ node.type.toUpperCase() }}</span>
1294
1288
 
1295
- <div class="node-content">
1289
+ <div class="flex-1 flex items-center gap-2">
1296
1290
  @if (node.type === 'group') {
1297
1291
  <!-- Group: show operator toggle -->
1298
1292
  <span
@@ -1302,13 +1296,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
1302
1296
  title="Click to toggle">
1303
1297
  {{ node.operator }}
1304
1298
  </span>
1305
- <span class="text-gray-600 text-xs">({{ node.children?.length || 0 }} conditions)</span>
1299
+ <span class="text-muted-color text-xs">({{ node.children?.length || 0 }} conditions)</span>
1306
1300
  }
1307
1301
 
1308
1302
  @if (node.type === 'action') {
1309
1303
  <!-- Action: show action selector -->
1310
1304
  <select
1311
- class="flex-1"
1305
+ class="action-select flex-1 p-2 border border-surface rounded text-sm"
1312
1306
  [ngModel]="node.actionId"
1313
1307
  (ngModelChange)="updateActionId(node.id, $event)">
1314
1308
  <option [value]="null">Select Action... ({{ availableActions().length }} available)</option>
@@ -1319,7 +1313,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
1319
1313
  }
1320
1314
  </div>
1321
1315
 
1322
- <div class="node-actions">
1316
+ <div class="flex gap-1">
1323
1317
  <p-button
1324
1318
  icon="pi pi-trash"
1325
1319
  [text]="true"
@@ -1332,7 +1326,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
1332
1326
 
1333
1327
  <!-- Children for group nodes -->
1334
1328
  @if (node.type === 'group' && node.children && node.children.length > 0) {
1335
- <div class="node-children">
1329
+ <div class="ml-5 mt-2 pl-3 border-l-2 border-surface">
1336
1330
  @for (child of node.children; track child.id) {
1337
1331
  <ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: child, depth: depth + 1 }"></ng-container>
1338
1332
  }
@@ -1341,9 +1335,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
1341
1335
 
1342
1336
  <!-- Add child buttons for group nodes -->
1343
1337
  @if (node.type === 'group') {
1344
- <div class="add-child-section">
1345
- <div class="text-xs font-semibold text-gray-600 mb-2">Add Condition:</div>
1346
- <div class="add-child-buttons">
1338
+ <div class="mt-3 p-3 bg-surface-50 rounded border border-dashed border-surface">
1339
+ <div class="text-xs font-semibold text-muted-color mb-2">Add Condition:</div>
1340
+ <div class="flex gap-2 flex-wrap">
1347
1341
  <p-button
1348
1342
  label="Group"
1349
1343
  icon="pi pi-sitemap"
@@ -1362,8 +1356,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
1362
1356
  }
1363
1357
  </div>
1364
1358
  </ng-template>
1365
- `, 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"] }]
1366
- }], 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"] }] } });
1359
+ `, 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"] }]
1360
+ }], 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"] }] } });
1367
1361
 
1368
1362
  /**
1369
1363
  * Tree Utility Functions
@@ -1560,7 +1554,10 @@ function convertActionToTreeNode(actions) {
1560
1554
  * ```
1561
1555
  */
1562
1556
  class RoleActionSelectorComponent {
1557
+ // Permission constants for template
1558
+ ROLE_ACTION_PERMISSIONS = ROLE_ACTION_PERMISSIONS;
1563
1559
  // Dependencies
1560
+ destroyRef = inject(DestroyRef);
1564
1561
  roleApi = inject(RoleApiService);
1565
1562
  actionApi = inject(ActionApiService);
1566
1563
  permissionApi = inject(PermissionApiService);
@@ -1641,6 +1638,10 @@ class RoleActionSelectorComponent {
1641
1638
  loadDataAbortController = null;
1642
1639
  constructor() {
1643
1640
  this.loadRoles();
1641
+ // Cleanup on destroy
1642
+ this.destroyRef.onDestroy(() => {
1643
+ this.loadDataAbortController?.abort();
1644
+ });
1644
1645
  // Effect: Load data when role selection changes
1645
1646
  effect(() => {
1646
1647
  const roleId = this.selectedRoleId();
@@ -1650,12 +1651,7 @@ class RoleActionSelectorComponent {
1650
1651
  this.loadDataAbortController = new AbortController();
1651
1652
  this.onRoleChange(this.loadDataAbortController.signal).catch((err) => {
1652
1653
  if (err.name !== 'AbortError') {
1653
- console.error('[Role Selector] Failed to load data:', err);
1654
- this.messageService.add({
1655
- severity: 'error',
1656
- summary: 'Error',
1657
- detail: 'Failed to load role permissions. Please refresh.',
1658
- });
1654
+ // Error toast handled by global interceptor
1659
1655
  this.loading.set(false);
1660
1656
  }
1661
1657
  });
@@ -1665,9 +1661,6 @@ class RoleActionSelectorComponent {
1665
1661
  }
1666
1662
  });
1667
1663
  }
1668
- ngOnDestroy() {
1669
- this.loadDataAbortController?.abort();
1670
- }
1671
1664
  /**
1672
1665
  * Load roles from API
1673
1666
  */
@@ -1678,13 +1671,8 @@ class RoleActionSelectorComponent {
1678
1671
  }));
1679
1672
  this.roles.set(response?.data ?? []);
1680
1673
  }
1681
- catch (err) {
1682
- console.error('[Role Selector] Failed to load roles:', err);
1683
- this.messageService.add({
1684
- severity: 'error',
1685
- summary: 'Error',
1686
- detail: 'Failed to load roles',
1687
- });
1674
+ catch {
1675
+ // Error toast handled by global interceptor
1688
1676
  }
1689
1677
  }
1690
1678
  /**
@@ -1700,13 +1688,7 @@ class RoleActionSelectorComponent {
1700
1688
  if (signal.aborted)
1701
1689
  return;
1702
1690
  // Get flat list (filtered by company if enabled)
1703
- const flatActions = (actionsResponse?.data ?? []).filter((a) => {
1704
- if (!a.id) {
1705
- console.warn('[Role Selector] Skipping action without ID:', a.name);
1706
- return false;
1707
- }
1708
- return true;
1709
- });
1691
+ const flatActions = (actionsResponse?.data ?? []).filter((a) => !!a.id);
1710
1692
  // Build tree structure from flat list with parentId
1711
1693
  const actionsTree = buildTreeFromFlat(flatActions);
1712
1694
  // Store both representations
@@ -1743,15 +1725,15 @@ class RoleActionSelectorComponent {
1743
1725
  const prerequisiteInfo = this.permissionLogic.getPrerequisiteTooltip(action, selMap, this.actions());
1744
1726
  if (prerequisiteInfo) {
1745
1727
  const actionHint = isCurrentlySelected
1746
- ? '\n\n━━━━━━━━━━━━━━━━━━\n🗑️ Click to remove'
1747
- : '\n\n━━━━━━━━━━━━━━━━━━\n Click to add (auto-selects required)';
1728
+ ? '\n\n---\n[Remove] Click to remove'
1729
+ : '\n\n---\n[Add] Click to add (auto-selects required)';
1748
1730
  return `${prerequisiteInfo}${actionHint}`;
1749
1731
  }
1750
1732
  }
1751
1733
  if (isCurrentlySelected) {
1752
- return ' Assigned to role\n\n🗑️ Click to remove';
1734
+ return '[Assigned] Assigned to role\n\n[Remove] Click to remove';
1753
1735
  }
1754
- return ' Click to assign to role';
1736
+ return '[Add] Click to assign to role';
1755
1737
  }
1756
1738
  /**
1757
1739
  * Check if action has unmet prerequisites
@@ -1848,13 +1830,8 @@ class RoleActionSelectorComponent {
1848
1830
  // Update baseline
1849
1831
  this._initialSelection.set({ ...this.selectionMap() });
1850
1832
  }
1851
- catch (err) {
1852
- const error = err;
1853
- this.messageService.add({
1854
- severity: 'error',
1855
- summary: 'Error',
1856
- detail: error?.error?.message || 'Failed to update role permissions',
1857
- });
1833
+ catch {
1834
+ // Error toast handled by global interceptor
1858
1835
  }
1859
1836
  finally {
1860
1837
  this.saving.set(false);
@@ -1869,16 +1846,17 @@ class RoleActionSelectorComponent {
1869
1846
  this._selectionMap.set({});
1870
1847
  this._initialSelection.set({});
1871
1848
  }
1872
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RoleActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1873
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: RoleActionSelectorComponent, isStandalone: true, selector: "flusys-role-action-selector", ngImport: i0, template: `
1849
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RoleActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1850
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: RoleActionSelectorComponent, isStandalone: true, selector: "flusys-role-action-selector", ngImport: i0, template: `
1874
1851
  <div class="role-action-selector">
1875
1852
  <!-- Role Selector -->
1876
1853
  <div class="mb-4">
1877
- <div class="grid">
1878
- <div class="col-12 md:col-6 lg:col-4">
1854
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
1855
+ <div>
1879
1856
  <label class="block font-semibold mb-2">Select Role</label>
1880
1857
  <p-select
1881
- [(ngModel)]="selectedRoleId"
1858
+ [ngModel]="selectedRoleId()"
1859
+ (ngModelChange)="selectedRoleId.set($event)"
1882
1860
  [options]="roles()"
1883
1861
  optionLabel="name"
1884
1862
  optionValue="id"
@@ -1895,24 +1873,21 @@ class RoleActionSelectorComponent {
1895
1873
  <!-- Loading State -->
1896
1874
  @if (loading()) {
1897
1875
  <div
1898
- class="surface-card p-5 border-round shadow-1 flex justify-content-center"
1876
+ class="surface-card p-5 rounded-border shadow-sm flex justify-center"
1899
1877
  >
1900
- <i
1901
- class="pi pi-spin pi-spinner text-primary"
1902
- style="font-size: 3rem"
1903
- ></i>
1878
+ <i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
1904
1879
  </div>
1905
1880
  }
1906
1881
 
1907
1882
  <!-- Action List -->
1908
1883
  @if (!loading() && actions().length > 0) {
1909
- <div class="surface-card p-4 border-round shadow-1">
1884
+ <div class="surface-card p-4 rounded-border shadow-sm">
1910
1885
  <div
1911
- class="flex flex-column md:flex-row justify-content-between align-items-start md:align-items-center gap-3 mb-4"
1886
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
1912
1887
  >
1913
1888
  <div>
1914
1889
  <h5 class="m-0 mb-1">Action Permissions</h5>
1915
- <p class="text-sm text-color-secondary m-0">
1890
+ <p class="text-sm text-muted-color m-0">
1916
1891
  {{ actions().length }} actions available
1917
1892
  </p>
1918
1893
  </div>
@@ -1934,6 +1909,7 @@ class RoleActionSelectorComponent {
1934
1909
  (onClick)="deselectAll()"
1935
1910
  />
1936
1911
  <p-button
1912
+ *hasPermission="ROLE_ACTION_PERMISSIONS.ASSIGN"
1937
1913
  label="Save Changes"
1938
1914
  icon="pi pi-save"
1939
1915
  [disabled]="!canSave()"
@@ -1947,14 +1923,12 @@ class RoleActionSelectorComponent {
1947
1923
 
1948
1924
  <!-- Validation Warning -->
1949
1925
  @if (invalidActionsCount() > 0) {
1950
- <div class="bg-orange-50 border-l-4 border-orange-500 p-4 mb-4">
1951
- <div class="flex items-center">
1952
- <i
1953
- class="pi pi-exclamation-triangle text-orange-500 mr-2"
1954
- ></i>
1955
- <div>
1956
- <strong class="text-orange-800">Validation Warning:</strong>
1957
- <p class="text-sm text-orange-700 mt-1">
1926
+ <div class="validation-warning rounded-border p-3 mb-4">
1927
+ <div class="flex items-start gap-2">
1928
+ <i class="pi pi-exclamation-triangle text-xl"></i>
1929
+ <div class="flex-1">
1930
+ <span class="font-semibold">Validation Warning:</span>
1931
+ <p class="text-sm mt-1 mb-0">
1958
1932
  {{ invalidActionsCount() }} selected
1959
1933
  action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
1960
1934
  unmet prerequisites. Fix before saving or use auto-fix on
@@ -1966,33 +1940,33 @@ class RoleActionSelectorComponent {
1966
1940
  }
1967
1941
 
1968
1942
  <!-- Action Tree Table -->
1969
- <p-treeTable
1970
- [value]="treeNodes()"
1971
- [scrollable]="true"
1972
- scrollHeight="flex"
1973
- dataKey="id"
1974
- styleClass="p-treetable-sm"
1975
- >
1976
- <ng-template pTemplate="header">
1977
- <tr>
1978
- <th style="width: 3rem">
1979
- <p-checkbox
1980
- [ngModel]="allSelected()"
1981
- [binary]="true"
1982
- (ngModelChange)="toggleAll()"
1983
- pTooltip="Select/Deselect All"
1984
- tooltipPosition="top"
1985
- />
1986
- </th>
1987
- <th>Name</th>
1988
- <th>Code</th>
1989
- <th>Type</th>
1990
- <th>Requirements</th>
1991
- </tr>
1992
- </ng-template>
1993
- <ng-template pTemplate="body" let-rowNode let-rowData="rowData">
1994
- <tr [class.bg-red-50]="hasUnmetPrerequisites(rowData)">
1995
- <td style="width: 3rem">
1943
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
1944
+ <p-treeTable
1945
+ [value]="treeNodes()"
1946
+ dataKey="id"
1947
+ styleClass="p-treetable-sm"
1948
+ [tableStyle]="{ 'min-width': '50rem' }"
1949
+ >
1950
+ <ng-template #header>
1951
+ <tr>
1952
+ <th class="w-12">
1953
+ <p-checkbox
1954
+ [ngModel]="allSelected()"
1955
+ [binary]="true"
1956
+ (ngModelChange)="toggleAll()"
1957
+ pTooltip="Select/Deselect All"
1958
+ tooltipPosition="top"
1959
+ />
1960
+ </th>
1961
+ <th>Name</th>
1962
+ <th class="hidden md:table-cell">Code</th>
1963
+ <th>Type</th>
1964
+ <th class="hidden lg:table-cell">Requirements</th>
1965
+ </tr>
1966
+ </ng-template>
1967
+ <ng-template #body let-rowNode let-rowData="rowData">
1968
+ <tr [class.highlight-warning]="hasUnmetPrerequisites(rowData)">
1969
+ <td class="w-12">
1996
1970
  <p-checkbox
1997
1971
  [ngModel]="selectionMap()[rowData.id]"
1998
1972
  [binary]="true"
@@ -2003,7 +1977,7 @@ class RoleActionSelectorComponent {
2003
1977
  </td>
2004
1978
  <td>
2005
1979
  <p-treeTableToggler [rowNode]="rowNode" />
2006
- <span class="inline-flex align-items-center gap-2">
1980
+ <span class="inline-flex items-center gap-2">
2007
1981
  {{ rowData.name }}
2008
1982
  @if (hasUnmetPrerequisites(rowData)) {
2009
1983
  <i
@@ -2014,7 +1988,7 @@ class RoleActionSelectorComponent {
2014
1988
  }
2015
1989
  </span>
2016
1990
  </td>
2017
- <td>{{ rowData.code || '-' }}</td>
1991
+ <td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
2018
1992
  <td>
2019
1993
  <p-tag
2020
1994
  [value]="rowData.actionType"
@@ -2023,42 +1997,41 @@ class RoleActionSelectorComponent {
2023
1997
  "
2024
1998
  />
2025
1999
  </td>
2026
- <td>
2000
+ <td class="hidden lg:table-cell">
2027
2001
  @if (rowData.permissionLogic) {
2028
- <span class="text-sm text-muted">Has prerequisites</span>
2002
+ <span class="text-sm text-muted-color">Has prerequisites</span>
2029
2003
  } @else {
2030
- <span class="text-muted">-</span>
2004
+ <span class="text-muted-color">-</span>
2031
2005
  }
2032
2006
  </td>
2033
2007
  </tr>
2034
2008
  </ng-template>
2035
- <ng-template pTemplate="emptymessage">
2009
+ <ng-template #emptymessage>
2036
2010
  <tr>
2037
- <td colspan="5" class="text-center p-4">
2011
+ <td colspan="5" class="text-center p-4 text-muted-color">
2038
2012
  @if (loading()) {
2039
2013
  <i class="pi pi-spin pi-spinner"></i> Loading actions...
2040
2014
  } @else { No actions available. }
2041
2015
  </td>
2042
2016
  </tr>
2043
2017
  </ng-template>
2044
- </p-treeTable>
2018
+ </p-treeTable>
2019
+ </div>
2045
2020
  </div>
2046
2021
 
2047
2022
  <!-- Change Summary -->
2048
2023
  @if (hasChanges()) {
2049
- <div class="surface-border border-1 border-round p-3 mt-4">
2050
- <div class="flex align-items-center gap-2 mb-3">
2051
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
2052
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
2024
+ <div class="border border-surface rounded-border p-3 mt-4">
2025
+ <div class="flex items-center gap-2 mb-3">
2026
+ <i class="pi pi-info-circle text-primary"></i>
2027
+ <span class="font-bold">Pending Changes</span>
2053
2028
  </div>
2054
- <div class="flex flex-col md:flex-row gap-4">
2029
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
2055
2030
  @if (pendingAdd().length > 0) {
2056
- <div class="w-full md:w-1/2">
2057
- <div class="flex align-items-center gap-2 mb-2">
2031
+ <div>
2032
+ <div class="flex items-center gap-2 mb-2">
2058
2033
  <i class="pi pi-plus-circle text-green-500"></i>
2059
- <strong class="text-sm"
2060
- >To Add ({{ pendingAdd().length }})</strong
2061
- >
2034
+ <strong class="text-sm">To Add ({{ pendingAdd().length }})</strong>
2062
2035
  </div>
2063
2036
  <ul class="list-none p-0 m-0 pl-4">
2064
2037
  @for (action of pendingAdd(); track action.id) {
@@ -2068,12 +2041,10 @@ class RoleActionSelectorComponent {
2068
2041
  </div>
2069
2042
  }
2070
2043
  @if (pendingRemove().length > 0) {
2071
- <div class="w-full md:w-1/2">
2072
- <div class="flex align-items-center gap-2 mb-2">
2044
+ <div>
2045
+ <div class="flex items-center gap-2 mb-2">
2073
2046
  <i class="pi pi-minus-circle text-red-500"></i>
2074
- <strong class="text-sm"
2075
- >To Remove ({{ pendingRemove().length }})</strong
2076
- >
2047
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
2077
2048
  </div>
2078
2049
  <ul class="list-none p-0 m-0 pl-4">
2079
2050
  @for (action of pendingRemove(); track action.id) {
@@ -2088,31 +2059,29 @@ class RoleActionSelectorComponent {
2088
2059
  }
2089
2060
 
2090
2061
  @if (!loading() && actions().length === 0) {
2091
- <div class="surface-card p-5 border-round shadow-1 text-center">
2092
- <i
2093
- class="pi pi-info-circle text-color-secondary mb-3"
2094
- style="font-size: 3rem; display: block;"
2095
- ></i>
2096
- <p class="text-color-secondary m-0">
2062
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
2063
+ <i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
2064
+ <p class="text-muted-color m-0">
2097
2065
  No actions available for this role.
2098
2066
  </p>
2099
2067
  </div>
2100
2068
  }
2101
2069
  }
2102
2070
  </div>
2103
- `, 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.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "directive", type: i3$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i3.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: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "component", type: i6.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: i7.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: 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 });
2071
+ `, 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 });
2104
2072
  }
2105
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RoleActionSelectorComponent, decorators: [{
2073
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RoleActionSelectorComponent, decorators: [{
2106
2074
  type: Component,
2107
- args: [{ selector: 'flusys-role-action-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule], template: `
2075
+ args: [{ selector: 'flusys-role-action-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule, HasPermissionDirective], template: `
2108
2076
  <div class="role-action-selector">
2109
2077
  <!-- Role Selector -->
2110
2078
  <div class="mb-4">
2111
- <div class="grid">
2112
- <div class="col-12 md:col-6 lg:col-4">
2079
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
2080
+ <div>
2113
2081
  <label class="block font-semibold mb-2">Select Role</label>
2114
2082
  <p-select
2115
- [(ngModel)]="selectedRoleId"
2083
+ [ngModel]="selectedRoleId()"
2084
+ (ngModelChange)="selectedRoleId.set($event)"
2116
2085
  [options]="roles()"
2117
2086
  optionLabel="name"
2118
2087
  optionValue="id"
@@ -2129,24 +2098,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2129
2098
  <!-- Loading State -->
2130
2099
  @if (loading()) {
2131
2100
  <div
2132
- class="surface-card p-5 border-round shadow-1 flex justify-content-center"
2101
+ class="surface-card p-5 rounded-border shadow-sm flex justify-center"
2133
2102
  >
2134
- <i
2135
- class="pi pi-spin pi-spinner text-primary"
2136
- style="font-size: 3rem"
2137
- ></i>
2103
+ <i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
2138
2104
  </div>
2139
2105
  }
2140
2106
 
2141
2107
  <!-- Action List -->
2142
2108
  @if (!loading() && actions().length > 0) {
2143
- <div class="surface-card p-4 border-round shadow-1">
2109
+ <div class="surface-card p-4 rounded-border shadow-sm">
2144
2110
  <div
2145
- class="flex flex-column md:flex-row justify-content-between align-items-start md:align-items-center gap-3 mb-4"
2111
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
2146
2112
  >
2147
2113
  <div>
2148
2114
  <h5 class="m-0 mb-1">Action Permissions</h5>
2149
- <p class="text-sm text-color-secondary m-0">
2115
+ <p class="text-sm text-muted-color m-0">
2150
2116
  {{ actions().length }} actions available
2151
2117
  </p>
2152
2118
  </div>
@@ -2168,6 +2134,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2168
2134
  (onClick)="deselectAll()"
2169
2135
  />
2170
2136
  <p-button
2137
+ *hasPermission="ROLE_ACTION_PERMISSIONS.ASSIGN"
2171
2138
  label="Save Changes"
2172
2139
  icon="pi pi-save"
2173
2140
  [disabled]="!canSave()"
@@ -2181,14 +2148,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2181
2148
 
2182
2149
  <!-- Validation Warning -->
2183
2150
  @if (invalidActionsCount() > 0) {
2184
- <div class="bg-orange-50 border-l-4 border-orange-500 p-4 mb-4">
2185
- <div class="flex items-center">
2186
- <i
2187
- class="pi pi-exclamation-triangle text-orange-500 mr-2"
2188
- ></i>
2189
- <div>
2190
- <strong class="text-orange-800">Validation Warning:</strong>
2191
- <p class="text-sm text-orange-700 mt-1">
2151
+ <div class="validation-warning rounded-border p-3 mb-4">
2152
+ <div class="flex items-start gap-2">
2153
+ <i class="pi pi-exclamation-triangle text-xl"></i>
2154
+ <div class="flex-1">
2155
+ <span class="font-semibold">Validation Warning:</span>
2156
+ <p class="text-sm mt-1 mb-0">
2192
2157
  {{ invalidActionsCount() }} selected
2193
2158
  action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
2194
2159
  unmet prerequisites. Fix before saving or use auto-fix on
@@ -2200,33 +2165,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2200
2165
  }
2201
2166
 
2202
2167
  <!-- Action Tree Table -->
2203
- <p-treeTable
2204
- [value]="treeNodes()"
2205
- [scrollable]="true"
2206
- scrollHeight="flex"
2207
- dataKey="id"
2208
- styleClass="p-treetable-sm"
2209
- >
2210
- <ng-template pTemplate="header">
2211
- <tr>
2212
- <th style="width: 3rem">
2213
- <p-checkbox
2214
- [ngModel]="allSelected()"
2215
- [binary]="true"
2216
- (ngModelChange)="toggleAll()"
2217
- pTooltip="Select/Deselect All"
2218
- tooltipPosition="top"
2219
- />
2220
- </th>
2221
- <th>Name</th>
2222
- <th>Code</th>
2223
- <th>Type</th>
2224
- <th>Requirements</th>
2225
- </tr>
2226
- </ng-template>
2227
- <ng-template pTemplate="body" let-rowNode let-rowData="rowData">
2228
- <tr [class.bg-red-50]="hasUnmetPrerequisites(rowData)">
2229
- <td style="width: 3rem">
2168
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
2169
+ <p-treeTable
2170
+ [value]="treeNodes()"
2171
+ dataKey="id"
2172
+ styleClass="p-treetable-sm"
2173
+ [tableStyle]="{ 'min-width': '50rem' }"
2174
+ >
2175
+ <ng-template #header>
2176
+ <tr>
2177
+ <th class="w-12">
2178
+ <p-checkbox
2179
+ [ngModel]="allSelected()"
2180
+ [binary]="true"
2181
+ (ngModelChange)="toggleAll()"
2182
+ pTooltip="Select/Deselect All"
2183
+ tooltipPosition="top"
2184
+ />
2185
+ </th>
2186
+ <th>Name</th>
2187
+ <th class="hidden md:table-cell">Code</th>
2188
+ <th>Type</th>
2189
+ <th class="hidden lg:table-cell">Requirements</th>
2190
+ </tr>
2191
+ </ng-template>
2192
+ <ng-template #body let-rowNode let-rowData="rowData">
2193
+ <tr [class.highlight-warning]="hasUnmetPrerequisites(rowData)">
2194
+ <td class="w-12">
2230
2195
  <p-checkbox
2231
2196
  [ngModel]="selectionMap()[rowData.id]"
2232
2197
  [binary]="true"
@@ -2237,7 +2202,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2237
2202
  </td>
2238
2203
  <td>
2239
2204
  <p-treeTableToggler [rowNode]="rowNode" />
2240
- <span class="inline-flex align-items-center gap-2">
2205
+ <span class="inline-flex items-center gap-2">
2241
2206
  {{ rowData.name }}
2242
2207
  @if (hasUnmetPrerequisites(rowData)) {
2243
2208
  <i
@@ -2248,7 +2213,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2248
2213
  }
2249
2214
  </span>
2250
2215
  </td>
2251
- <td>{{ rowData.code || '-' }}</td>
2216
+ <td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
2252
2217
  <td>
2253
2218
  <p-tag
2254
2219
  [value]="rowData.actionType"
@@ -2257,42 +2222,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2257
2222
  "
2258
2223
  />
2259
2224
  </td>
2260
- <td>
2225
+ <td class="hidden lg:table-cell">
2261
2226
  @if (rowData.permissionLogic) {
2262
- <span class="text-sm text-muted">Has prerequisites</span>
2227
+ <span class="text-sm text-muted-color">Has prerequisites</span>
2263
2228
  } @else {
2264
- <span class="text-muted">-</span>
2229
+ <span class="text-muted-color">-</span>
2265
2230
  }
2266
2231
  </td>
2267
2232
  </tr>
2268
2233
  </ng-template>
2269
- <ng-template pTemplate="emptymessage">
2234
+ <ng-template #emptymessage>
2270
2235
  <tr>
2271
- <td colspan="5" class="text-center p-4">
2236
+ <td colspan="5" class="text-center p-4 text-muted-color">
2272
2237
  @if (loading()) {
2273
2238
  <i class="pi pi-spin pi-spinner"></i> Loading actions...
2274
2239
  } @else { No actions available. }
2275
2240
  </td>
2276
2241
  </tr>
2277
2242
  </ng-template>
2278
- </p-treeTable>
2243
+ </p-treeTable>
2244
+ </div>
2279
2245
  </div>
2280
2246
 
2281
2247
  <!-- Change Summary -->
2282
2248
  @if (hasChanges()) {
2283
- <div class="surface-border border-1 border-round p-3 mt-4">
2284
- <div class="flex align-items-center gap-2 mb-3">
2285
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
2286
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
2249
+ <div class="border border-surface rounded-border p-3 mt-4">
2250
+ <div class="flex items-center gap-2 mb-3">
2251
+ <i class="pi pi-info-circle text-primary"></i>
2252
+ <span class="font-bold">Pending Changes</span>
2287
2253
  </div>
2288
- <div class="flex flex-col md:flex-row gap-4">
2254
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
2289
2255
  @if (pendingAdd().length > 0) {
2290
- <div class="w-full md:w-1/2">
2291
- <div class="flex align-items-center gap-2 mb-2">
2256
+ <div>
2257
+ <div class="flex items-center gap-2 mb-2">
2292
2258
  <i class="pi pi-plus-circle text-green-500"></i>
2293
- <strong class="text-sm"
2294
- >To Add ({{ pendingAdd().length }})</strong
2295
- >
2259
+ <strong class="text-sm">To Add ({{ pendingAdd().length }})</strong>
2296
2260
  </div>
2297
2261
  <ul class="list-none p-0 m-0 pl-4">
2298
2262
  @for (action of pendingAdd(); track action.id) {
@@ -2302,12 +2266,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2302
2266
  </div>
2303
2267
  }
2304
2268
  @if (pendingRemove().length > 0) {
2305
- <div class="w-full md:w-1/2">
2306
- <div class="flex align-items-center gap-2 mb-2">
2269
+ <div>
2270
+ <div class="flex items-center gap-2 mb-2">
2307
2271
  <i class="pi pi-minus-circle text-red-500"></i>
2308
- <strong class="text-sm"
2309
- >To Remove ({{ pendingRemove().length }})</strong
2310
- >
2272
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
2311
2273
  </div>
2312
2274
  <ul class="list-none p-0 m-0 pl-4">
2313
2275
  @for (action of pendingRemove(); track action.id) {
@@ -2322,19 +2284,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2322
2284
  }
2323
2285
 
2324
2286
  @if (!loading() && actions().length === 0) {
2325
- <div class="surface-card p-5 border-round shadow-1 text-center">
2326
- <i
2327
- class="pi pi-info-circle text-color-secondary mb-3"
2328
- style="font-size: 3rem; display: block;"
2329
- ></i>
2330
- <p class="text-color-secondary m-0">
2287
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
2288
+ <i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
2289
+ <p class="text-muted-color m-0">
2331
2290
  No actions available for this role.
2332
2291
  </p>
2333
2292
  </div>
2334
2293
  }
2335
2294
  }
2336
2295
  </div>
2337
- `, styles: [":host{display:block}\n"] }]
2296
+ `, 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"] }]
2338
2297
  }], ctorParameters: () => [] });
2339
2298
 
2340
2299
  /**
@@ -2372,6 +2331,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2372
2331
  * ```
2373
2332
  */
2374
2333
  class CompanyActionSelectorComponent {
2334
+ // Permission constants for template
2335
+ COMPANY_ACTION_PERMISSIONS = COMPANY_ACTION_PERMISSIONS;
2375
2336
  // Dependencies
2376
2337
  companyApiProvider = inject(COMPANY_API_PROVIDER);
2377
2338
  actionApi = inject(ActionApiService);
@@ -2379,6 +2340,7 @@ class CompanyActionSelectorComponent {
2379
2340
  messageService = inject(MessageService);
2380
2341
  confirmationService = inject(ConfirmationService);
2381
2342
  permissionLogic = inject(ActionPermissionLogicService);
2343
+ destroyRef = inject(DestroyRef);
2382
2344
  // State - Company Selection
2383
2345
  selectedCompanyId = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedCompanyId" }] : []));
2384
2346
  companies = signal([], ...(ngDevMode ? [{ debugName: "companies" }] : []));
@@ -2453,6 +2415,10 @@ class CompanyActionSelectorComponent {
2453
2415
  // AbortController for data loading
2454
2416
  loadDataAbortController = null;
2455
2417
  constructor() {
2418
+ // Cleanup on destroy
2419
+ this.destroyRef.onDestroy(() => {
2420
+ this.loadDataAbortController?.abort();
2421
+ });
2456
2422
  this.loadCompanies();
2457
2423
  // Effect: Load data when company selection changes
2458
2424
  effect(() => {
@@ -2463,12 +2429,7 @@ class CompanyActionSelectorComponent {
2463
2429
  this.loadDataAbortController = new AbortController();
2464
2430
  this.loadData(this.loadDataAbortController.signal).catch((err) => {
2465
2431
  if (err.name !== 'AbortError') {
2466
- console.error('[Company Selector] Failed to load data:', err);
2467
- this.messageService.add({
2468
- severity: 'error',
2469
- summary: 'Error',
2470
- detail: 'Failed to load company actions. Please refresh.',
2471
- });
2432
+ // Error toast handled by global interceptor
2472
2433
  this.loading.set(false);
2473
2434
  }
2474
2435
  });
@@ -2478,9 +2439,6 @@ class CompanyActionSelectorComponent {
2478
2439
  }
2479
2440
  });
2480
2441
  }
2481
- ngOnDestroy() {
2482
- this.loadDataAbortController?.abort();
2483
- }
2484
2442
  /**
2485
2443
  * Load companies from API
2486
2444
  */
@@ -2492,13 +2450,8 @@ class CompanyActionSelectorComponent {
2492
2450
  }));
2493
2451
  this.companies.set(response?.data ?? []);
2494
2452
  }
2495
- catch (err) {
2496
- console.error('[Company Selector] Failed to load companies:', err);
2497
- this.messageService.add({
2498
- severity: 'error',
2499
- summary: 'Error',
2500
- detail: 'Failed to load companies',
2501
- });
2453
+ catch {
2454
+ // Error toast handled by global interceptor
2502
2455
  }
2503
2456
  }
2504
2457
  /**
@@ -2516,13 +2469,7 @@ class CompanyActionSelectorComponent {
2516
2469
  return;
2517
2470
  const actionsTree = actionsResponse?.data ?? [];
2518
2471
  // Flatten tree for business logic
2519
- const actionsList = flattenTree(actionsTree).filter((a) => {
2520
- if (!a.id) {
2521
- console.warn('[Company Selector] Skipping action without ID:', a.name);
2522
- return false;
2523
- }
2524
- return true;
2525
- });
2472
+ const actionsList = flattenTree(actionsTree).filter((a) => !!a.id);
2526
2473
  // Store both representations
2527
2474
  this._actionsTree.set(actionsTree);
2528
2475
  this._actions.set(actionsList);
@@ -2565,15 +2512,15 @@ class CompanyActionSelectorComponent {
2565
2512
  const prerequisiteInfo = this.permissionLogic.getPrerequisiteTooltip(action, selMap, this.actions());
2566
2513
  if (prerequisiteInfo) {
2567
2514
  const actionHint = isCurrentlySelected
2568
- ? '\n\n━━━━━━━━━━━━━━━━━━\n🗑️ Click to remove'
2569
- : '\n\n━━━━━━━━━━━━━━━━━━\n Click to add (auto-selects required)';
2515
+ ? '\n\n---\n[Remove] Click to remove'
2516
+ : '\n\n---\n[Add] Click to add (auto-selects required)';
2570
2517
  return `${prerequisiteInfo}${actionHint}`;
2571
2518
  }
2572
2519
  }
2573
2520
  if (isCurrentlySelected) {
2574
- return ' Selected\n\n🗑️ Click to remove from company whitelist';
2521
+ return '[Selected] Selected\n\n[Remove] Click to remove from company whitelist';
2575
2522
  }
2576
- return ' Click to add to company whitelist';
2523
+ return '[Add] Click to add to company whitelist';
2577
2524
  }
2578
2525
  /**
2579
2526
  * Handle action checkbox toggle
@@ -2669,13 +2616,8 @@ class CompanyActionSelectorComponent {
2669
2616
  // Update baseline
2670
2617
  this._initialSelection.set({ ...this.selectionMap() });
2671
2618
  }
2672
- catch (err) {
2673
- const error = err;
2674
- this.messageService.add({
2675
- severity: 'error',
2676
- summary: 'Error',
2677
- detail: error?.error?.message || 'Failed to update company action whitelist',
2678
- });
2619
+ catch {
2620
+ // Error toast handled by global interceptor
2679
2621
  }
2680
2622
  finally {
2681
2623
  this.saving.set(false);
@@ -2737,16 +2679,17 @@ class CompanyActionSelectorComponent {
2737
2679
  this._selectionMap.set({});
2738
2680
  this._initialSelection.set({});
2739
2681
  }
2740
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: CompanyActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2741
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: CompanyActionSelectorComponent, isStandalone: true, selector: "flusys-company-action-selector", ngImport: i0, template: `
2682
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CompanyActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2683
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CompanyActionSelectorComponent, isStandalone: true, selector: "flusys-company-action-selector", ngImport: i0, template: `
2742
2684
  <div class="company-action-selector">
2743
2685
  <!-- Company Selector -->
2744
2686
  <div class="mb-4">
2745
- <div class="grid">
2746
- <div class="col-12 md:col-6 lg:col-4">
2687
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
2688
+ <div>
2747
2689
  <label class="block font-semibold mb-2">Select Company</label>
2748
2690
  <p-select
2749
- [(ngModel)]="selectedCompanyId"
2691
+ [ngModel]="selectedCompanyId()"
2692
+ (ngModelChange)="selectedCompanyId.set($event)"
2750
2693
  [options]="companies()"
2751
2694
  optionLabel="name"
2752
2695
  optionValue="id"
@@ -2763,24 +2706,21 @@ class CompanyActionSelectorComponent {
2763
2706
  <!-- Loading State -->
2764
2707
  @if (loading()) {
2765
2708
  <div
2766
- class="surface-card p-5 border-round shadow-1 flex justify-content-center"
2709
+ class="surface-card p-5 rounded-border shadow-sm flex justify-center"
2767
2710
  >
2768
- <i
2769
- class="pi pi-spin pi-spinner text-primary"
2770
- style="font-size: 3rem"
2771
- ></i>
2711
+ <i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
2772
2712
  </div>
2773
2713
  }
2774
2714
 
2775
2715
  <!-- Action List -->
2776
2716
  @if (!loading() && actions().length > 0) {
2777
- <div class="surface-card p-4 border-round shadow-1">
2717
+ <div class="surface-card p-4 rounded-border shadow-sm">
2778
2718
  <div
2779
- class="flex flex-column md:flex-row justify-content-between align-items-start md:align-items-center gap-3 mb-4"
2719
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
2780
2720
  >
2781
2721
  <div>
2782
2722
  <h5 class="m-0 mb-1">Action Whitelist</h5>
2783
- <p class="text-sm text-color-secondary m-0">
2723
+ <p class="text-sm text-muted-color m-0">
2784
2724
  {{ actions().length }} actions available
2785
2725
  </p>
2786
2726
  </div>
@@ -2802,6 +2742,7 @@ class CompanyActionSelectorComponent {
2802
2742
  (onClick)="deselectAll()"
2803
2743
  />
2804
2744
  <p-button
2745
+ *hasPermission="COMPANY_ACTION_PERMISSIONS.ASSIGN"
2805
2746
  label="Save Changes"
2806
2747
  icon="pi pi-save"
2807
2748
  [disabled]="!canSave()"
@@ -2815,14 +2756,12 @@ class CompanyActionSelectorComponent {
2815
2756
 
2816
2757
  <!-- Validation Warning -->
2817
2758
  @if (invalidActionsCount() > 0) {
2818
- <div class="bg-orange-50 border-l-4 border-orange-500 p-4 mb-4">
2819
- <div class="flex items-center">
2820
- <i
2821
- class="pi pi-exclamation-triangle text-orange-500 mr-2"
2822
- ></i>
2823
- <div>
2824
- <strong class="text-orange-800">Validation Warning:</strong>
2825
- <p class="text-sm text-orange-700 mt-1">
2759
+ <div class="validation-warning rounded-border p-3 mb-4">
2760
+ <div class="flex items-start gap-2">
2761
+ <i class="pi pi-exclamation-triangle text-xl"></i>
2762
+ <div class="flex-1">
2763
+ <span class="font-semibold">Validation Warning:</span>
2764
+ <p class="text-sm mt-1 mb-0">
2826
2765
  {{ invalidActionsCount() }} selected
2827
2766
  action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
2828
2767
  unmet prerequisites. Fix before saving or use auto-fix on
@@ -2834,33 +2773,33 @@ class CompanyActionSelectorComponent {
2834
2773
  }
2835
2774
 
2836
2775
  <!-- Action Tree Table -->
2837
- <p-treeTable
2838
- [value]="treeNodes()"
2839
- [scrollable]="true"
2840
- scrollHeight="flex"
2841
- dataKey="id"
2842
- styleClass="p-treetable-sm"
2843
- >
2844
- <ng-template pTemplate="header">
2845
- <tr>
2846
- <th style="width: 3rem">
2847
- <p-checkbox
2848
- [ngModel]="allSelected()"
2849
- [binary]="true"
2850
- (ngModelChange)="toggleAll()"
2851
- pTooltip="Select/Deselect All"
2852
- tooltipPosition="top"
2853
- />
2854
- </th>
2855
- <th>Name</th>
2856
- <th>Code</th>
2857
- <th>Type</th>
2858
- <th>Description</th>
2859
- </tr>
2860
- </ng-template>
2861
- <ng-template pTemplate="body" let-rowNode let-rowData="rowData">
2862
- <tr [class.bg-red-50]="hasUnmetPrerequisites(rowData)">
2863
- <td style="width: 3rem">
2776
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
2777
+ <p-treeTable
2778
+ [value]="treeNodes()"
2779
+ dataKey="id"
2780
+ styleClass="p-treetable-sm"
2781
+ [tableStyle]="{ 'min-width': '50rem' }"
2782
+ >
2783
+ <ng-template #header>
2784
+ <tr>
2785
+ <th class="w-12">
2786
+ <p-checkbox
2787
+ [ngModel]="allSelected()"
2788
+ [binary]="true"
2789
+ (ngModelChange)="toggleAll()"
2790
+ pTooltip="Select/Deselect All"
2791
+ tooltipPosition="top"
2792
+ />
2793
+ </th>
2794
+ <th>Name</th>
2795
+ <th class="hidden md:table-cell">Code</th>
2796
+ <th>Type</th>
2797
+ <th class="hidden lg:table-cell">Description</th>
2798
+ </tr>
2799
+ </ng-template>
2800
+ <ng-template #body let-rowNode let-rowData="rowData">
2801
+ <tr [class.highlight-warning]="hasUnmetPrerequisites(rowData)">
2802
+ <td class="w-12">
2864
2803
  <p-checkbox
2865
2804
  [ngModel]="selectionMap()[rowData.id]"
2866
2805
  [binary]="true"
@@ -2871,7 +2810,7 @@ class CompanyActionSelectorComponent {
2871
2810
  </td>
2872
2811
  <td>
2873
2812
  <p-treeTableToggler [rowNode]="rowNode" />
2874
- <span class="inline-flex align-items-center gap-2">
2813
+ <span class="inline-flex items-center gap-2">
2875
2814
  {{ rowData.name }}
2876
2815
  @if (hasUnmetPrerequisites(rowData)) {
2877
2816
  <i
@@ -2882,7 +2821,7 @@ class CompanyActionSelectorComponent {
2882
2821
  }
2883
2822
  </span>
2884
2823
  </td>
2885
- <td>{{ rowData.code || '-' }}</td>
2824
+ <td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
2886
2825
  <td>
2887
2826
  <p-tag
2888
2827
  [value]="rowData.actionType"
@@ -2891,36 +2830,35 @@ class CompanyActionSelectorComponent {
2891
2830
  "
2892
2831
  />
2893
2832
  </td>
2894
- <td>{{ rowData.description || '-' }}</td>
2833
+ <td class="hidden lg:table-cell">{{ rowData.description || '-' }}</td>
2895
2834
  </tr>
2896
2835
  </ng-template>
2897
- <ng-template pTemplate="emptymessage">
2836
+ <ng-template #emptymessage>
2898
2837
  <tr>
2899
- <td colspan="5" class="text-center p-4">
2838
+ <td colspan="5" class="text-center p-4 text-muted-color">
2900
2839
  @if (loading()) {
2901
2840
  <i class="pi pi-spin pi-spinner"></i> Loading actions...
2902
2841
  } @else { No actions available. }
2903
2842
  </td>
2904
2843
  </tr>
2905
2844
  </ng-template>
2906
- </p-treeTable>
2845
+ </p-treeTable>
2846
+ </div>
2907
2847
  </div>
2908
2848
 
2909
2849
  <!-- Change Summary -->
2910
2850
  @if (hasChanges()) {
2911
- <div class="surface-border border-1 border-round p-3 mt-4">
2912
- <div class="flex align-items-center gap-2 mb-3">
2913
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
2914
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
2851
+ <div class="border border-surface rounded-border p-3 mt-4">
2852
+ <div class="flex items-center gap-2 mb-3">
2853
+ <i class="pi pi-info-circle text-primary"></i>
2854
+ <span class="font-bold">Pending Changes</span>
2915
2855
  </div>
2916
- <div class="flex flex-col md:flex-row gap-4">
2856
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
2917
2857
  @if (pendingAdd().length > 0) {
2918
- <div class="w-full md:w-1/2">
2858
+ <div>
2919
2859
  <div class="flex items-center gap-2 mb-2">
2920
2860
  <i class="pi pi-plus-circle text-green-500"></i>
2921
- <strong class="text-sm">
2922
- To Whitelist ({{ pendingAdd().length }})
2923
- </strong>
2861
+ <strong class="text-sm">To Whitelist ({{ pendingAdd().length }})</strong>
2924
2862
  </div>
2925
2863
  <ul class="list-none p-0 m-0 pl-4">
2926
2864
  @for (action of pendingAdd(); track action.id) {
@@ -2929,14 +2867,11 @@ class CompanyActionSelectorComponent {
2929
2867
  </ul>
2930
2868
  </div>
2931
2869
  }
2932
-
2933
2870
  @if (pendingRemove().length > 0) {
2934
- <div class="w-full md:w-1/2">
2871
+ <div>
2935
2872
  <div class="flex items-center gap-2 mb-2">
2936
2873
  <i class="pi pi-minus-circle text-red-500"></i>
2937
- <strong class="text-sm">
2938
- To Remove ({{ pendingRemove().length }})
2939
- </strong>
2874
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
2940
2875
  </div>
2941
2876
  <ul class="list-none p-0 m-0 pl-4">
2942
2877
  @for (action of pendingRemove(); track action.id) {
@@ -2951,31 +2886,29 @@ class CompanyActionSelectorComponent {
2951
2886
  }
2952
2887
 
2953
2888
  @if (!loading() && actions().length === 0) {
2954
- <div class="surface-card p-5 border-round shadow-1 text-center">
2955
- <i
2956
- class="pi pi-info-circle text-color-secondary mb-3"
2957
- style="font-size: 3rem; display: block;"
2958
- ></i>
2959
- <p class="text-color-secondary m-0">
2889
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
2890
+ <i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
2891
+ <p class="text-muted-color m-0">
2960
2892
  No actions available for this company.
2961
2893
  </p>
2962
2894
  </div>
2963
2895
  }
2964
2896
  }
2965
2897
  </div>
2966
- `, 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.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "directive", type: i3$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i3.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: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "component", type: i6.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: i7.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: 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 });
2898
+ `, 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 });
2967
2899
  }
2968
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: CompanyActionSelectorComponent, decorators: [{
2900
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CompanyActionSelectorComponent, decorators: [{
2969
2901
  type: Component,
2970
- args: [{ selector: 'flusys-company-action-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule], template: `
2902
+ args: [{ selector: 'flusys-company-action-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule, HasPermissionDirective], template: `
2971
2903
  <div class="company-action-selector">
2972
2904
  <!-- Company Selector -->
2973
2905
  <div class="mb-4">
2974
- <div class="grid">
2975
- <div class="col-12 md:col-6 lg:col-4">
2906
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
2907
+ <div>
2976
2908
  <label class="block font-semibold mb-2">Select Company</label>
2977
2909
  <p-select
2978
- [(ngModel)]="selectedCompanyId"
2910
+ [ngModel]="selectedCompanyId()"
2911
+ (ngModelChange)="selectedCompanyId.set($event)"
2979
2912
  [options]="companies()"
2980
2913
  optionLabel="name"
2981
2914
  optionValue="id"
@@ -2992,24 +2925,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2992
2925
  <!-- Loading State -->
2993
2926
  @if (loading()) {
2994
2927
  <div
2995
- class="surface-card p-5 border-round shadow-1 flex justify-content-center"
2928
+ class="surface-card p-5 rounded-border shadow-sm flex justify-center"
2996
2929
  >
2997
- <i
2998
- class="pi pi-spin pi-spinner text-primary"
2999
- style="font-size: 3rem"
3000
- ></i>
2930
+ <i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
3001
2931
  </div>
3002
2932
  }
3003
2933
 
3004
2934
  <!-- Action List -->
3005
2935
  @if (!loading() && actions().length > 0) {
3006
- <div class="surface-card p-4 border-round shadow-1">
2936
+ <div class="surface-card p-4 rounded-border shadow-sm">
3007
2937
  <div
3008
- class="flex flex-column md:flex-row justify-content-between align-items-start md:align-items-center gap-3 mb-4"
2938
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
3009
2939
  >
3010
2940
  <div>
3011
2941
  <h5 class="m-0 mb-1">Action Whitelist</h5>
3012
- <p class="text-sm text-color-secondary m-0">
2942
+ <p class="text-sm text-muted-color m-0">
3013
2943
  {{ actions().length }} actions available
3014
2944
  </p>
3015
2945
  </div>
@@ -3031,6 +2961,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3031
2961
  (onClick)="deselectAll()"
3032
2962
  />
3033
2963
  <p-button
2964
+ *hasPermission="COMPANY_ACTION_PERMISSIONS.ASSIGN"
3034
2965
  label="Save Changes"
3035
2966
  icon="pi pi-save"
3036
2967
  [disabled]="!canSave()"
@@ -3044,14 +2975,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3044
2975
 
3045
2976
  <!-- Validation Warning -->
3046
2977
  @if (invalidActionsCount() > 0) {
3047
- <div class="bg-orange-50 border-l-4 border-orange-500 p-4 mb-4">
3048
- <div class="flex items-center">
3049
- <i
3050
- class="pi pi-exclamation-triangle text-orange-500 mr-2"
3051
- ></i>
3052
- <div>
3053
- <strong class="text-orange-800">Validation Warning:</strong>
3054
- <p class="text-sm text-orange-700 mt-1">
2978
+ <div class="validation-warning rounded-border p-3 mb-4">
2979
+ <div class="flex items-start gap-2">
2980
+ <i class="pi pi-exclamation-triangle text-xl"></i>
2981
+ <div class="flex-1">
2982
+ <span class="font-semibold">Validation Warning:</span>
2983
+ <p class="text-sm mt-1 mb-0">
3055
2984
  {{ invalidActionsCount() }} selected
3056
2985
  action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
3057
2986
  unmet prerequisites. Fix before saving or use auto-fix on
@@ -3063,33 +2992,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3063
2992
  }
3064
2993
 
3065
2994
  <!-- Action Tree Table -->
3066
- <p-treeTable
3067
- [value]="treeNodes()"
3068
- [scrollable]="true"
3069
- scrollHeight="flex"
3070
- dataKey="id"
3071
- styleClass="p-treetable-sm"
3072
- >
3073
- <ng-template pTemplate="header">
3074
- <tr>
3075
- <th style="width: 3rem">
3076
- <p-checkbox
3077
- [ngModel]="allSelected()"
3078
- [binary]="true"
3079
- (ngModelChange)="toggleAll()"
3080
- pTooltip="Select/Deselect All"
3081
- tooltipPosition="top"
3082
- />
3083
- </th>
3084
- <th>Name</th>
3085
- <th>Code</th>
3086
- <th>Type</th>
3087
- <th>Description</th>
3088
- </tr>
3089
- </ng-template>
3090
- <ng-template pTemplate="body" let-rowNode let-rowData="rowData">
3091
- <tr [class.bg-red-50]="hasUnmetPrerequisites(rowData)">
3092
- <td style="width: 3rem">
2995
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
2996
+ <p-treeTable
2997
+ [value]="treeNodes()"
2998
+ dataKey="id"
2999
+ styleClass="p-treetable-sm"
3000
+ [tableStyle]="{ 'min-width': '50rem' }"
3001
+ >
3002
+ <ng-template #header>
3003
+ <tr>
3004
+ <th class="w-12">
3005
+ <p-checkbox
3006
+ [ngModel]="allSelected()"
3007
+ [binary]="true"
3008
+ (ngModelChange)="toggleAll()"
3009
+ pTooltip="Select/Deselect All"
3010
+ tooltipPosition="top"
3011
+ />
3012
+ </th>
3013
+ <th>Name</th>
3014
+ <th class="hidden md:table-cell">Code</th>
3015
+ <th>Type</th>
3016
+ <th class="hidden lg:table-cell">Description</th>
3017
+ </tr>
3018
+ </ng-template>
3019
+ <ng-template #body let-rowNode let-rowData="rowData">
3020
+ <tr [class.highlight-warning]="hasUnmetPrerequisites(rowData)">
3021
+ <td class="w-12">
3093
3022
  <p-checkbox
3094
3023
  [ngModel]="selectionMap()[rowData.id]"
3095
3024
  [binary]="true"
@@ -3100,7 +3029,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3100
3029
  </td>
3101
3030
  <td>
3102
3031
  <p-treeTableToggler [rowNode]="rowNode" />
3103
- <span class="inline-flex align-items-center gap-2">
3032
+ <span class="inline-flex items-center gap-2">
3104
3033
  {{ rowData.name }}
3105
3034
  @if (hasUnmetPrerequisites(rowData)) {
3106
3035
  <i
@@ -3111,7 +3040,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3111
3040
  }
3112
3041
  </span>
3113
3042
  </td>
3114
- <td>{{ rowData.code || '-' }}</td>
3043
+ <td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
3115
3044
  <td>
3116
3045
  <p-tag
3117
3046
  [value]="rowData.actionType"
@@ -3120,36 +3049,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3120
3049
  "
3121
3050
  />
3122
3051
  </td>
3123
- <td>{{ rowData.description || '-' }}</td>
3052
+ <td class="hidden lg:table-cell">{{ rowData.description || '-' }}</td>
3124
3053
  </tr>
3125
3054
  </ng-template>
3126
- <ng-template pTemplate="emptymessage">
3055
+ <ng-template #emptymessage>
3127
3056
  <tr>
3128
- <td colspan="5" class="text-center p-4">
3057
+ <td colspan="5" class="text-center p-4 text-muted-color">
3129
3058
  @if (loading()) {
3130
3059
  <i class="pi pi-spin pi-spinner"></i> Loading actions...
3131
3060
  } @else { No actions available. }
3132
3061
  </td>
3133
3062
  </tr>
3134
3063
  </ng-template>
3135
- </p-treeTable>
3064
+ </p-treeTable>
3065
+ </div>
3136
3066
  </div>
3137
3067
 
3138
3068
  <!-- Change Summary -->
3139
3069
  @if (hasChanges()) {
3140
- <div class="surface-border border-1 border-round p-3 mt-4">
3141
- <div class="flex align-items-center gap-2 mb-3">
3142
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
3143
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
3070
+ <div class="border border-surface rounded-border p-3 mt-4">
3071
+ <div class="flex items-center gap-2 mb-3">
3072
+ <i class="pi pi-info-circle text-primary"></i>
3073
+ <span class="font-bold">Pending Changes</span>
3144
3074
  </div>
3145
- <div class="flex flex-col md:flex-row gap-4">
3075
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
3146
3076
  @if (pendingAdd().length > 0) {
3147
- <div class="w-full md:w-1/2">
3077
+ <div>
3148
3078
  <div class="flex items-center gap-2 mb-2">
3149
3079
  <i class="pi pi-plus-circle text-green-500"></i>
3150
- <strong class="text-sm">
3151
- To Whitelist ({{ pendingAdd().length }})
3152
- </strong>
3080
+ <strong class="text-sm">To Whitelist ({{ pendingAdd().length }})</strong>
3153
3081
  </div>
3154
3082
  <ul class="list-none p-0 m-0 pl-4">
3155
3083
  @for (action of pendingAdd(); track action.id) {
@@ -3158,14 +3086,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3158
3086
  </ul>
3159
3087
  </div>
3160
3088
  }
3161
-
3162
3089
  @if (pendingRemove().length > 0) {
3163
- <div class="w-full md:w-1/2">
3090
+ <div>
3164
3091
  <div class="flex items-center gap-2 mb-2">
3165
3092
  <i class="pi pi-minus-circle text-red-500"></i>
3166
- <strong class="text-sm">
3167
- To Remove ({{ pendingRemove().length }})
3168
- </strong>
3093
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
3169
3094
  </div>
3170
3095
  <ul class="list-none p-0 m-0 pl-4">
3171
3096
  @for (action of pendingRemove(); track action.id) {
@@ -3180,19 +3105,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3180
3105
  }
3181
3106
 
3182
3107
  @if (!loading() && actions().length === 0) {
3183
- <div class="surface-card p-5 border-round shadow-1 text-center">
3184
- <i
3185
- class="pi pi-info-circle text-color-secondary mb-3"
3186
- style="font-size: 3rem; display: block;"
3187
- ></i>
3188
- <p class="text-color-secondary m-0">
3108
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
3109
+ <i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
3110
+ <p class="text-muted-color m-0">
3189
3111
  No actions available for this company.
3190
3112
  </p>
3191
3113
  </div>
3192
3114
  }
3193
3115
  }
3194
3116
  </div>
3195
- `, styles: [":host{display:block}\n"] }]
3117
+ `, 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"] }]
3196
3118
  }], ctorParameters: () => [] });
3197
3119
 
3198
3120
  /**
@@ -3226,18 +3148,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3226
3148
  * ```
3227
3149
  */
3228
3150
  class UserRoleSelectorComponent {
3151
+ // Permission constants for template
3152
+ USER_ROLE_PERMISSIONS = USER_ROLE_PERMISSIONS;
3229
3153
  // Dependencies
3230
3154
  appConfig = inject(APP_CONFIG);
3231
3155
  companyContext = inject(LAYOUT_AUTH_STATE);
3232
- userProvider = inject(USER_PROVIDER);
3233
3156
  userPermissionProvider = inject(USER_PERMISSION_PROVIDER);
3234
3157
  roleApi = inject(RoleApiService);
3235
3158
  permissionApi = inject(PermissionApiService);
3236
3159
  messageService = inject(MessageService);
3160
+ destroyRef = inject(DestroyRef);
3161
+ // AbortController for data loading
3162
+ loadDataAbortController = null;
3237
3163
  // State - User/Branch Selection
3238
- selectedUserId = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedUserId" }] : []));
3164
+ selectedUserId = signal(null, ...(ngDevMode ? [{ debugName: "selectedUserId" }] : []));
3239
3165
  selectedBranchId = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedBranchId" }] : []));
3240
- users = signal([], ...(ngDevMode ? [{ debugName: "users" }] : []));
3241
3166
  branches = signal([], ...(ngDevMode ? [{ debugName: "branches" }] : []));
3242
3167
  // State - Loading/Saving
3243
3168
  loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
@@ -3271,7 +3196,11 @@ class UserRoleSelectorComponent {
3271
3196
  hasChanges = computed(() => {
3272
3197
  const current = this.selectionMap();
3273
3198
  const initial = this.initialSelection();
3274
- return JSON.stringify(current) !== JSON.stringify(initial);
3199
+ const currentKeys = Object.keys(current);
3200
+ const initialKeys = Object.keys(initial);
3201
+ if (currentKeys.length !== initialKeys.length)
3202
+ return true;
3203
+ return currentKeys.some((key) => current[key] !== initial[key]);
3275
3204
  }, ...(ngDevMode ? [{ debugName: "hasChanges" }] : []));
3276
3205
  // Computed - Pending Changes
3277
3206
  pendingAdd = computed(() => {
@@ -3291,7 +3220,10 @@ class UserRoleSelectorComponent {
3291
3220
  return this.hasChanges() && !this.saving();
3292
3221
  }, ...(ngDevMode ? [{ debugName: "canSave" }] : []));
3293
3222
  constructor() {
3294
- this.loadUsers();
3223
+ // Cleanup on destroy
3224
+ this.destroyRef.onDestroy(() => {
3225
+ this.loadDataAbortController?.abort();
3226
+ });
3295
3227
  // Effect: Load user branches and data when user changes
3296
3228
  effect(() => {
3297
3229
  const userId = this.selectedUserId();
@@ -3318,28 +3250,6 @@ class UserRoleSelectorComponent {
3318
3250
  }
3319
3251
  });
3320
3252
  }
3321
- /**
3322
- * Load users from provider
3323
- */
3324
- async loadUsers() {
3325
- try {
3326
- const response = await this.userProvider
3327
- .getUsers({
3328
- page: 0,
3329
- pageSize: MAX_DROPDOWN_ITEMS,
3330
- })
3331
- .toPromise();
3332
- this.users.set(response?.data ?? []);
3333
- }
3334
- catch (err) {
3335
- console.error('[User Role Selector] Failed to load users:', err);
3336
- this.messageService.add({
3337
- severity: 'error',
3338
- summary: 'Error',
3339
- detail: 'Failed to load users',
3340
- });
3341
- }
3342
- }
3343
3253
  /**
3344
3254
  * Load user's permitted branches
3345
3255
  */
@@ -3360,13 +3270,8 @@ class UserRoleSelectorComponent {
3360
3270
  })) ?? [];
3361
3271
  this.branches.set(userBranches);
3362
3272
  }
3363
- catch (err) {
3364
- console.error('[User Role Selector] Failed to load user branches:', err);
3365
- this.messageService.add({
3366
- severity: 'error',
3367
- summary: 'Error',
3368
- detail: 'Failed to load user permitted branches',
3369
- });
3273
+ catch {
3274
+ // Error toast handled by global interceptor
3370
3275
  }
3371
3276
  }
3372
3277
  /**
@@ -3403,13 +3308,8 @@ class UserRoleSelectorComponent {
3403
3308
  this._selectionMap.set(selMap);
3404
3309
  this._initialSelection.set({ ...selMap });
3405
3310
  }
3406
- catch (err) {
3407
- console.error('[User Role Selector] Failed to load user roles:', err);
3408
- this.messageService.add({
3409
- severity: 'error',
3410
- summary: 'Error',
3411
- detail: 'Failed to load user role assignments',
3412
- });
3311
+ catch {
3312
+ // Error toast handled by global interceptor
3413
3313
  }
3414
3314
  finally {
3415
3315
  this.loading.set(false);
@@ -3422,9 +3322,9 @@ class UserRoleSelectorComponent {
3422
3322
  const selMap = this.selectionMap();
3423
3323
  const isCurrentlySelected = selMap[role.id];
3424
3324
  if (isCurrentlySelected) {
3425
- return ' Assigned to user\n\n🗑️ Click to remove role';
3325
+ return '[Assigned] Assigned to user\n\n[Remove] Click to remove role';
3426
3326
  }
3427
- return ' Click to assign role to user';
3327
+ return '[Add] Click to assign role to user';
3428
3328
  }
3429
3329
  /**
3430
3330
  * Handle role checkbox toggle
@@ -3516,13 +3416,8 @@ class UserRoleSelectorComponent {
3516
3416
  // Update baseline
3517
3417
  this._initialSelection.set({ ...this.selectionMap() });
3518
3418
  }
3519
- catch (err) {
3520
- const error = err;
3521
- this.messageService.add({
3522
- severity: 'error',
3523
- summary: 'Error',
3524
- detail: error?.error?.message || 'Failed to update user role assignments',
3525
- });
3419
+ catch {
3420
+ // Error toast handled by global interceptor
3526
3421
  }
3527
3422
  finally {
3528
3423
  this.saving.set(false);
@@ -3536,62 +3431,34 @@ class UserRoleSelectorComponent {
3536
3431
  this._selectionMap.set({});
3537
3432
  this._initialSelection.set({});
3538
3433
  }
3539
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UserRoleSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3540
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: UserRoleSelectorComponent, isStandalone: true, selector: "flusys-user-role-selector", ngImport: i0, template: `
3434
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserRoleSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3435
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: UserRoleSelectorComponent, isStandalone: true, selector: "flusys-user-role-selector", ngImport: i0, template: `
3541
3436
  <div class="user-role-selector">
3542
3437
  <!-- User and Branch Selectors -->
3543
- <div class="surface-card p-4 border-round mb-4 shadow-1">
3544
- <div class="formgrid grid gap-5">
3545
- <div class="field col-12 sm:col-6 mb-0">
3546
- <label class="block font-semibold mb-2 text-900">
3438
+ <div class="surface-card p-4 rounded-border mb-4 shadow-sm">
3439
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
3440
+ <div>
3441
+ <label class="block font-semibold mb-2">
3547
3442
  <i class="pi pi-user mr-2 text-primary"></i>
3548
3443
  Select User
3549
3444
  </label>
3550
- <p-select
3551
- [(ngModel)]="selectedUserId"
3552
- [options]="users()"
3553
- optionLabel="name"
3554
- optionValue="id"
3555
- placeholder="Select a user"
3556
- [showClear]="true"
3557
- [filter]="true"
3558
- styleClass="w-full"
3559
- >
3560
- <ng-template #selectedItem let-user>
3561
- @if (user) {
3562
- <div class="flex align-items-center gap-2">
3563
- <i class="pi pi-user text-primary"></i>
3564
- <div>
3565
- <div class="font-semibold">{{ user.name }}</div>
3566
- <div class="text-xs text-color-secondary">
3567
- {{ user.email }}
3568
- </div>
3569
- </div>
3570
- </div>
3571
- }
3572
- </ng-template>
3573
- <ng-template #item let-user>
3574
- <div class="flex align-items-center gap-2">
3575
- <i class="pi pi-user text-color-secondary"></i>
3576
- <div>
3577
- <div class="font-semibold">{{ user.name }}</div>
3578
- <div class="text-xs text-color-secondary">
3579
- {{ user.email }}
3580
- </div>
3581
- </div>
3582
- </div>
3583
- </ng-template>
3584
- </p-select>
3445
+ <lib-user-select
3446
+ [value]="selectedUserId()"
3447
+ (valueChange)="selectedUserId.set($event)"
3448
+ [isEditMode]="true"
3449
+ placeHolder="Select a user"
3450
+ />
3585
3451
  </div>
3586
3452
 
3587
3453
  @if (showBranchSelector()) {
3588
- <div class="field col-12 sm:col-6 mb-0">
3589
- <label class="block font-semibold mb-2 text-900">
3454
+ <div>
3455
+ <label class="block font-semibold mb-2">
3590
3456
  <i class="pi pi-building mr-2 text-primary"></i>
3591
3457
  Select Branch
3592
3458
  </label>
3593
3459
  <p-select
3594
- [(ngModel)]="selectedBranchId"
3460
+ [ngModel]="selectedBranchId()"
3461
+ (ngModelChange)="selectedBranchId.set($event)"
3595
3462
  [options]="filteredBranches()"
3596
3463
  optionLabel="name"
3597
3464
  optionValue="id"
@@ -3602,20 +3469,20 @@ class UserRoleSelectorComponent {
3602
3469
  >
3603
3470
  <ng-template #selectedItem let-branch>
3604
3471
  @if (branch) {
3605
- <div class="flex align-items-center gap-2">
3472
+ <div class="flex items-center gap-2">
3606
3473
  <i class="pi pi-building text-primary"></i>
3607
3474
  <span class="font-semibold">{{ branch.name }}</span>
3608
3475
  </div>
3609
3476
  }
3610
3477
  </ng-template>
3611
3478
  <ng-template #item let-branch>
3612
- <div class="flex align-items-center gap-2">
3613
- <i class="pi pi-building text-color-secondary"></i>
3479
+ <div class="flex items-center gap-2">
3480
+ <i class="pi pi-building text-muted-color"></i>
3614
3481
  <span>{{ branch.name }}</span>
3615
3482
  </div>
3616
3483
  </ng-template>
3617
3484
  </p-select>
3618
- <small class="text-color-secondary block mt-1">
3485
+ <small class="text-muted-color block mt-1">
3619
3486
  {{ filteredBranches().length }} permitted branch{{
3620
3487
  filteredBranches().length !== 1 ? 'es' : ''
3621
3488
  }}
@@ -3630,24 +3497,21 @@ class UserRoleSelectorComponent {
3630
3497
  <!-- Loading State -->
3631
3498
  @if (loading()) {
3632
3499
  <div
3633
- class="surface-card p-5 border-round shadow-1 flex justify-content-center"
3500
+ class="surface-card p-5 rounded-border shadow-sm flex justify-center"
3634
3501
  >
3635
- <i
3636
- class="pi pi-spin pi-spinner text-primary"
3637
- style="font-size: 3rem"
3638
- ></i>
3502
+ <i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
3639
3503
  </div>
3640
3504
  }
3641
3505
 
3642
3506
  <!-- Role List -->
3643
3507
  @if (!loading() && roles().length > 0) {
3644
- <div class="surface-card p-4 border-round shadow-1">
3508
+ <div class="surface-card p-4 rounded-border shadow-sm">
3645
3509
  <div
3646
- class="flex flex-column md:flex-row justify-content-between align-items-start md:align-items-center gap-3 mb-4"
3510
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
3647
3511
  >
3648
3512
  <div>
3649
3513
  <h5 class="m-0 mb-1">Role Assignments</h5>
3650
- <p class="text-sm text-color-secondary m-0">
3514
+ <p class="text-sm text-muted-color m-0">
3651
3515
  {{ roles().length }} roles available
3652
3516
  </p>
3653
3517
  </div>
@@ -3669,6 +3533,7 @@ class UserRoleSelectorComponent {
3669
3533
  (onClick)="deselectAll()"
3670
3534
  />
3671
3535
  <p-button
3536
+ *hasPermission="USER_ROLE_PERMISSIONS.ASSIGN"
3672
3537
  label="Save Changes"
3673
3538
  icon="pi pi-save"
3674
3539
  [disabled]="!canSave()"
@@ -3681,77 +3546,78 @@ class UserRoleSelectorComponent {
3681
3546
  </div>
3682
3547
 
3683
3548
  <!-- Role Table -->
3684
- <p-table
3685
- [value]="roles()"
3686
- [rows]="10"
3687
- [paginator]="roles().length > 10"
3688
- [rowsPerPageOptions]="[10, 20, 50]"
3689
- [globalFilterFields]="['name', 'code', 'description']"
3690
- [showCurrentPageReport]="true"
3691
- currentPageReportTemplate="Showing {first} to {last} of {totalRecords} roles"
3692
- styleClass="p-datatable-sm"
3693
- >
3694
- <ng-template #header>
3695
- <tr>
3696
- <th style="width: 3rem">
3697
- <p-checkbox
3698
- [ngModel]="allSelected()"
3699
- [binary]="true"
3700
- (ngModelChange)="toggleAll()"
3701
- pTooltip="Select/Deselect All"
3702
- tooltipPosition="top"
3703
- />
3704
- </th>
3705
- <th>Name</th>
3706
- <th>Code</th>
3707
- <th>Description</th>
3708
- </tr>
3709
- </ng-template>
3710
- <ng-template #body let-role>
3711
- <tr>
3712
- <td>
3713
- <p-checkbox
3714
- [ngModel]="selectionMap()[role.id]"
3715
- [binary]="true"
3716
- (ngModelChange)="onRoleToggle(role, $event)"
3717
- [pTooltip]="getTooltip(role)"
3718
- tooltipPosition="top"
3719
- />
3720
- </td>
3721
- <td>{{ role.name }}</td>
3722
- <td>{{ role.code || '-' }}</td>
3723
- <td>{{ role.description || '-' }}</td>
3724
- </tr>
3725
- </ng-template>
3726
- <ng-template #emptymessage>
3727
- <tr>
3728
- <td colspan="4" class="text-center p-4">
3729
- @if (loading()) {
3730
- <i class="pi pi-spin pi-spinner"></i> Loading roles...
3731
- } @else {
3732
- No roles available.
3733
- }
3734
- </td>
3735
- </tr>
3736
- </ng-template>
3737
- </p-table>
3549
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
3550
+ <p-table
3551
+ [value]="roles()"
3552
+ [rows]="10"
3553
+ [paginator]="roles().length > 10"
3554
+ [rowsPerPageOptions]="[10, 20, 50]"
3555
+ [globalFilterFields]="['name', 'code', 'description']"
3556
+ [showCurrentPageReport]="true"
3557
+ currentPageReportTemplate="Showing {first} to {last} of {totalRecords} roles"
3558
+ styleClass="p-datatable-sm"
3559
+ [tableStyle]="{ 'min-width': '35rem' }"
3560
+ >
3561
+ <ng-template #header>
3562
+ <tr>
3563
+ <th style="width: 3rem">
3564
+ <p-checkbox
3565
+ [ngModel]="allSelected()"
3566
+ [binary]="true"
3567
+ (ngModelChange)="toggleAll()"
3568
+ pTooltip="Select/Deselect All"
3569
+ tooltipPosition="top"
3570
+ />
3571
+ </th>
3572
+ <th>Name</th>
3573
+ <th class="hidden sm:table-cell">Code</th>
3574
+ <th class="hidden md:table-cell">Description</th>
3575
+ </tr>
3576
+ </ng-template>
3577
+ <ng-template #body let-role>
3578
+ <tr>
3579
+ <td>
3580
+ <p-checkbox
3581
+ [ngModel]="selectionMap()[role.id]"
3582
+ [binary]="true"
3583
+ (ngModelChange)="onRoleToggle(role, $event)"
3584
+ [pTooltip]="getTooltip(role)"
3585
+ tooltipPosition="top"
3586
+ />
3587
+ </td>
3588
+ <td>{{ role.name }}</td>
3589
+ <td class="hidden sm:table-cell">{{ role.code || '-' }}</td>
3590
+ <td class="hidden md:table-cell">{{ role.description || '-' }}</td>
3591
+ </tr>
3592
+ </ng-template>
3593
+ <ng-template #emptymessage>
3594
+ <tr>
3595
+ <td colspan="4" class="text-center p-4 text-muted-color">
3596
+ @if (loading()) {
3597
+ <i class="pi pi-spin pi-spinner"></i> Loading roles...
3598
+ } @else {
3599
+ No roles available.
3600
+ }
3601
+ </td>
3602
+ </tr>
3603
+ </ng-template>
3604
+ </p-table>
3605
+ </div>
3738
3606
  </div>
3739
3607
 
3740
3608
  <!-- Change Summary -->
3741
3609
  @if (hasChanges()) {
3742
- <div class="surface-border border-1 border-round p-3 mt-4">
3743
- <div class="flex align-items-center gap-2 mb-3">
3744
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
3745
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
3610
+ <div class="border border-surface rounded-border p-3 mt-4">
3611
+ <div class="flex items-center gap-2 mb-3">
3612
+ <i class="pi pi-info-circle text-primary"></i>
3613
+ <span class="font-bold">Pending Changes</span>
3746
3614
  </div>
3747
- <div class="flex flex-col md:flex-row gap-4">
3615
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
3748
3616
  @if (pendingAdd().length > 0) {
3749
- <div class="w-full md:w-1/2">
3750
- <div class="flex align-items-center gap-2 mb-2">
3617
+ <div>
3618
+ <div class="flex items-center gap-2 mb-2">
3751
3619
  <i class="pi pi-plus-circle text-green-500"></i>
3752
- <strong class="text-sm"
3753
- >To Assign ({{ pendingAdd().length }})</strong
3754
- >
3620
+ <strong class="text-sm">To Assign ({{ pendingAdd().length }})</strong>
3755
3621
  </div>
3756
3622
  <ul class="list-none p-0 m-0 pl-4">
3757
3623
  @for (role of pendingAdd(); track role.id) {
@@ -3761,12 +3627,10 @@ class UserRoleSelectorComponent {
3761
3627
  </div>
3762
3628
  }
3763
3629
  @if (pendingRemove().length > 0) {
3764
- <div class="w-full md:w-1/2">
3765
- <div class="flex align-items-center gap-2 mb-2">
3630
+ <div>
3631
+ <div class="flex items-center gap-2 mb-2">
3766
3632
  <i class="pi pi-minus-circle text-red-500"></i>
3767
- <strong class="text-sm"
3768
- >To Remove ({{ pendingRemove().length }})</strong
3769
- >
3633
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
3770
3634
  </div>
3771
3635
  <ul class="list-none p-0 m-0 pl-4">
3772
3636
  @for (role of pendingRemove(); track role.id) {
@@ -3781,77 +3645,46 @@ class UserRoleSelectorComponent {
3781
3645
  }
3782
3646
 
3783
3647
  @if (!loading() && roles().length === 0) {
3784
- <div class="surface-card p-5 border-round shadow-1 text-center">
3785
- <i
3786
- class="pi pi-info-circle text-color-secondary mb-3"
3787
- style="font-size: 3rem; display: block;"
3788
- ></i>
3789
- <p class="text-color-secondary m-0">
3648
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
3649
+ <i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
3650
+ <p class="text-muted-color m-0">
3790
3651
  No roles available for this user.
3791
3652
  </p>
3792
3653
  </div>
3793
3654
  }
3794
3655
  }
3795
3656
  </div>
3796
- `, 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: i3.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: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "component", type: i6.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$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: "component", type: i7.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3657
+ `, 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: ["loadUsers", "placeHolder", "isEditMode", "filterActive", "additionalFilters", "pageSize", "value"], outputs: ["valueChange", "userSelected", "onError"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3797
3658
  }
3798
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UserRoleSelectorComponent, decorators: [{
3659
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserRoleSelectorComponent, decorators: [{
3799
3660
  type: Component,
3800
- args: [{ selector: 'flusys-user-role-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule], template: `
3661
+ args: [{ selector: 'flusys-user-role-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule, HasPermissionDirective, UserSelectComponent], template: `
3801
3662
  <div class="user-role-selector">
3802
3663
  <!-- User and Branch Selectors -->
3803
- <div class="surface-card p-4 border-round mb-4 shadow-1">
3804
- <div class="formgrid grid gap-5">
3805
- <div class="field col-12 sm:col-6 mb-0">
3806
- <label class="block font-semibold mb-2 text-900">
3664
+ <div class="surface-card p-4 rounded-border mb-4 shadow-sm">
3665
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
3666
+ <div>
3667
+ <label class="block font-semibold mb-2">
3807
3668
  <i class="pi pi-user mr-2 text-primary"></i>
3808
3669
  Select User
3809
3670
  </label>
3810
- <p-select
3811
- [(ngModel)]="selectedUserId"
3812
- [options]="users()"
3813
- optionLabel="name"
3814
- optionValue="id"
3815
- placeholder="Select a user"
3816
- [showClear]="true"
3817
- [filter]="true"
3818
- styleClass="w-full"
3819
- >
3820
- <ng-template #selectedItem let-user>
3821
- @if (user) {
3822
- <div class="flex align-items-center gap-2">
3823
- <i class="pi pi-user text-primary"></i>
3824
- <div>
3825
- <div class="font-semibold">{{ user.name }}</div>
3826
- <div class="text-xs text-color-secondary">
3827
- {{ user.email }}
3828
- </div>
3829
- </div>
3830
- </div>
3831
- }
3832
- </ng-template>
3833
- <ng-template #item let-user>
3834
- <div class="flex align-items-center gap-2">
3835
- <i class="pi pi-user text-color-secondary"></i>
3836
- <div>
3837
- <div class="font-semibold">{{ user.name }}</div>
3838
- <div class="text-xs text-color-secondary">
3839
- {{ user.email }}
3840
- </div>
3841
- </div>
3842
- </div>
3843
- </ng-template>
3844
- </p-select>
3671
+ <lib-user-select
3672
+ [value]="selectedUserId()"
3673
+ (valueChange)="selectedUserId.set($event)"
3674
+ [isEditMode]="true"
3675
+ placeHolder="Select a user"
3676
+ />
3845
3677
  </div>
3846
3678
 
3847
3679
  @if (showBranchSelector()) {
3848
- <div class="field col-12 sm:col-6 mb-0">
3849
- <label class="block font-semibold mb-2 text-900">
3680
+ <div>
3681
+ <label class="block font-semibold mb-2">
3850
3682
  <i class="pi pi-building mr-2 text-primary"></i>
3851
3683
  Select Branch
3852
3684
  </label>
3853
3685
  <p-select
3854
- [(ngModel)]="selectedBranchId"
3686
+ [ngModel]="selectedBranchId()"
3687
+ (ngModelChange)="selectedBranchId.set($event)"
3855
3688
  [options]="filteredBranches()"
3856
3689
  optionLabel="name"
3857
3690
  optionValue="id"
@@ -3862,20 +3695,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3862
3695
  >
3863
3696
  <ng-template #selectedItem let-branch>
3864
3697
  @if (branch) {
3865
- <div class="flex align-items-center gap-2">
3698
+ <div class="flex items-center gap-2">
3866
3699
  <i class="pi pi-building text-primary"></i>
3867
3700
  <span class="font-semibold">{{ branch.name }}</span>
3868
3701
  </div>
3869
3702
  }
3870
3703
  </ng-template>
3871
3704
  <ng-template #item let-branch>
3872
- <div class="flex align-items-center gap-2">
3873
- <i class="pi pi-building text-color-secondary"></i>
3705
+ <div class="flex items-center gap-2">
3706
+ <i class="pi pi-building text-muted-color"></i>
3874
3707
  <span>{{ branch.name }}</span>
3875
3708
  </div>
3876
3709
  </ng-template>
3877
3710
  </p-select>
3878
- <small class="text-color-secondary block mt-1">
3711
+ <small class="text-muted-color block mt-1">
3879
3712
  {{ filteredBranches().length }} permitted branch{{
3880
3713
  filteredBranches().length !== 1 ? 'es' : ''
3881
3714
  }}
@@ -3890,24 +3723,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3890
3723
  <!-- Loading State -->
3891
3724
  @if (loading()) {
3892
3725
  <div
3893
- class="surface-card p-5 border-round shadow-1 flex justify-content-center"
3726
+ class="surface-card p-5 rounded-border shadow-sm flex justify-center"
3894
3727
  >
3895
- <i
3896
- class="pi pi-spin pi-spinner text-primary"
3897
- style="font-size: 3rem"
3898
- ></i>
3728
+ <i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
3899
3729
  </div>
3900
3730
  }
3901
3731
 
3902
3732
  <!-- Role List -->
3903
3733
  @if (!loading() && roles().length > 0) {
3904
- <div class="surface-card p-4 border-round shadow-1">
3734
+ <div class="surface-card p-4 rounded-border shadow-sm">
3905
3735
  <div
3906
- class="flex flex-column md:flex-row justify-content-between align-items-start md:align-items-center gap-3 mb-4"
3736
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
3907
3737
  >
3908
3738
  <div>
3909
3739
  <h5 class="m-0 mb-1">Role Assignments</h5>
3910
- <p class="text-sm text-color-secondary m-0">
3740
+ <p class="text-sm text-muted-color m-0">
3911
3741
  {{ roles().length }} roles available
3912
3742
  </p>
3913
3743
  </div>
@@ -3929,6 +3759,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3929
3759
  (onClick)="deselectAll()"
3930
3760
  />
3931
3761
  <p-button
3762
+ *hasPermission="USER_ROLE_PERMISSIONS.ASSIGN"
3932
3763
  label="Save Changes"
3933
3764
  icon="pi pi-save"
3934
3765
  [disabled]="!canSave()"
@@ -3941,77 +3772,78 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3941
3772
  </div>
3942
3773
 
3943
3774
  <!-- Role Table -->
3944
- <p-table
3945
- [value]="roles()"
3946
- [rows]="10"
3947
- [paginator]="roles().length > 10"
3948
- [rowsPerPageOptions]="[10, 20, 50]"
3949
- [globalFilterFields]="['name', 'code', 'description']"
3950
- [showCurrentPageReport]="true"
3951
- currentPageReportTemplate="Showing {first} to {last} of {totalRecords} roles"
3952
- styleClass="p-datatable-sm"
3953
- >
3954
- <ng-template #header>
3955
- <tr>
3956
- <th style="width: 3rem">
3957
- <p-checkbox
3958
- [ngModel]="allSelected()"
3959
- [binary]="true"
3960
- (ngModelChange)="toggleAll()"
3961
- pTooltip="Select/Deselect All"
3962
- tooltipPosition="top"
3963
- />
3964
- </th>
3965
- <th>Name</th>
3966
- <th>Code</th>
3967
- <th>Description</th>
3968
- </tr>
3969
- </ng-template>
3970
- <ng-template #body let-role>
3971
- <tr>
3972
- <td>
3973
- <p-checkbox
3974
- [ngModel]="selectionMap()[role.id]"
3975
- [binary]="true"
3976
- (ngModelChange)="onRoleToggle(role, $event)"
3977
- [pTooltip]="getTooltip(role)"
3978
- tooltipPosition="top"
3979
- />
3980
- </td>
3981
- <td>{{ role.name }}</td>
3982
- <td>{{ role.code || '-' }}</td>
3983
- <td>{{ role.description || '-' }}</td>
3984
- </tr>
3985
- </ng-template>
3986
- <ng-template #emptymessage>
3987
- <tr>
3988
- <td colspan="4" class="text-center p-4">
3989
- @if (loading()) {
3990
- <i class="pi pi-spin pi-spinner"></i> Loading roles...
3991
- } @else {
3992
- No roles available.
3993
- }
3994
- </td>
3995
- </tr>
3996
- </ng-template>
3997
- </p-table>
3775
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
3776
+ <p-table
3777
+ [value]="roles()"
3778
+ [rows]="10"
3779
+ [paginator]="roles().length > 10"
3780
+ [rowsPerPageOptions]="[10, 20, 50]"
3781
+ [globalFilterFields]="['name', 'code', 'description']"
3782
+ [showCurrentPageReport]="true"
3783
+ currentPageReportTemplate="Showing {first} to {last} of {totalRecords} roles"
3784
+ styleClass="p-datatable-sm"
3785
+ [tableStyle]="{ 'min-width': '35rem' }"
3786
+ >
3787
+ <ng-template #header>
3788
+ <tr>
3789
+ <th style="width: 3rem">
3790
+ <p-checkbox
3791
+ [ngModel]="allSelected()"
3792
+ [binary]="true"
3793
+ (ngModelChange)="toggleAll()"
3794
+ pTooltip="Select/Deselect All"
3795
+ tooltipPosition="top"
3796
+ />
3797
+ </th>
3798
+ <th>Name</th>
3799
+ <th class="hidden sm:table-cell">Code</th>
3800
+ <th class="hidden md:table-cell">Description</th>
3801
+ </tr>
3802
+ </ng-template>
3803
+ <ng-template #body let-role>
3804
+ <tr>
3805
+ <td>
3806
+ <p-checkbox
3807
+ [ngModel]="selectionMap()[role.id]"
3808
+ [binary]="true"
3809
+ (ngModelChange)="onRoleToggle(role, $event)"
3810
+ [pTooltip]="getTooltip(role)"
3811
+ tooltipPosition="top"
3812
+ />
3813
+ </td>
3814
+ <td>{{ role.name }}</td>
3815
+ <td class="hidden sm:table-cell">{{ role.code || '-' }}</td>
3816
+ <td class="hidden md:table-cell">{{ role.description || '-' }}</td>
3817
+ </tr>
3818
+ </ng-template>
3819
+ <ng-template #emptymessage>
3820
+ <tr>
3821
+ <td colspan="4" class="text-center p-4 text-muted-color">
3822
+ @if (loading()) {
3823
+ <i class="pi pi-spin pi-spinner"></i> Loading roles...
3824
+ } @else {
3825
+ No roles available.
3826
+ }
3827
+ </td>
3828
+ </tr>
3829
+ </ng-template>
3830
+ </p-table>
3831
+ </div>
3998
3832
  </div>
3999
3833
 
4000
3834
  <!-- Change Summary -->
4001
3835
  @if (hasChanges()) {
4002
- <div class="surface-border border-1 border-round p-3 mt-4">
4003
- <div class="flex align-items-center gap-2 mb-3">
4004
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
4005
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
3836
+ <div class="border border-surface rounded-border p-3 mt-4">
3837
+ <div class="flex items-center gap-2 mb-3">
3838
+ <i class="pi pi-info-circle text-primary"></i>
3839
+ <span class="font-bold">Pending Changes</span>
4006
3840
  </div>
4007
- <div class="flex flex-col md:flex-row gap-4">
3841
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
4008
3842
  @if (pendingAdd().length > 0) {
4009
- <div class="w-full md:w-1/2">
4010
- <div class="flex align-items-center gap-2 mb-2">
3843
+ <div>
3844
+ <div class="flex items-center gap-2 mb-2">
4011
3845
  <i class="pi pi-plus-circle text-green-500"></i>
4012
- <strong class="text-sm"
4013
- >To Assign ({{ pendingAdd().length }})</strong
4014
- >
3846
+ <strong class="text-sm">To Assign ({{ pendingAdd().length }})</strong>
4015
3847
  </div>
4016
3848
  <ul class="list-none p-0 m-0 pl-4">
4017
3849
  @for (role of pendingAdd(); track role.id) {
@@ -4021,12 +3853,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4021
3853
  </div>
4022
3854
  }
4023
3855
  @if (pendingRemove().length > 0) {
4024
- <div class="w-full md:w-1/2">
4025
- <div class="flex align-items-center gap-2 mb-2">
3856
+ <div>
3857
+ <div class="flex items-center gap-2 mb-2">
4026
3858
  <i class="pi pi-minus-circle text-red-500"></i>
4027
- <strong class="text-sm"
4028
- >To Remove ({{ pendingRemove().length }})</strong
4029
- >
3859
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
4030
3860
  </div>
4031
3861
  <ul class="list-none p-0 m-0 pl-4">
4032
3862
  @for (role of pendingRemove(); track role.id) {
@@ -4041,12 +3871,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4041
3871
  }
4042
3872
 
4043
3873
  @if (!loading() && roles().length === 0) {
4044
- <div class="surface-card p-5 border-round shadow-1 text-center">
4045
- <i
4046
- class="pi pi-info-circle text-color-secondary mb-3"
4047
- style="font-size: 3rem; display: block;"
4048
- ></i>
4049
- <p class="text-color-secondary m-0">
3874
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
3875
+ <i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
3876
+ <p class="text-muted-color m-0">
4050
3877
  No roles available for this user.
4051
3878
  </p>
4052
3879
  </div>
@@ -4088,19 +3915,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4088
3915
  * ```
4089
3916
  */
4090
3917
  class UserActionSelectorComponent {
3918
+ // Permission constants for template
3919
+ USER_ACTION_PERMISSIONS = USER_ACTION_PERMISSIONS;
4091
3920
  // Dependencies
4092
3921
  appConfig = inject(APP_CONFIG);
4093
3922
  companyContext = inject(LAYOUT_AUTH_STATE);
4094
- userProvider = inject(USER_PROVIDER);
4095
3923
  userPermissionProvider = inject(USER_PERMISSION_PROVIDER);
4096
3924
  actionApi = inject(ActionApiService);
4097
3925
  permissionApi = inject(PermissionApiService);
4098
3926
  permissionLogic = inject(ActionPermissionLogicService);
4099
3927
  messageService = inject(MessageService);
3928
+ destroyRef = inject(DestroyRef);
3929
+ // AbortController for data loading
3930
+ loadDataAbortController = null;
4100
3931
  // State - User/Branch Selection
4101
- selectedUserId = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedUserId" }] : []));
3932
+ selectedUserId = signal(null, ...(ngDevMode ? [{ debugName: "selectedUserId" }] : []));
4102
3933
  selectedBranchId = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedBranchId" }] : []));
4103
- users = signal([], ...(ngDevMode ? [{ debugName: "users" }] : []));
4104
3934
  branches = signal([], ...(ngDevMode ? [{ debugName: "branches" }] : []));
4105
3935
  // State - Loading/Saving
4106
3936
  loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
@@ -4139,7 +3969,11 @@ class UserActionSelectorComponent {
4139
3969
  hasChanges = computed(() => {
4140
3970
  const current = this.selectionMap();
4141
3971
  const initial = this.initialSelection();
4142
- return JSON.stringify(current) !== JSON.stringify(initial);
3972
+ const currentKeys = Object.keys(current);
3973
+ const initialKeys = Object.keys(initial);
3974
+ if (currentKeys.length !== initialKeys.length)
3975
+ return true;
3976
+ return currentKeys.some((key) => current[key] !== initial[key]);
4143
3977
  }, ...(ngDevMode ? [{ debugName: "hasChanges" }] : []));
4144
3978
  // Computed - Validation
4145
3979
  actionsWithUnmetPrerequisites = computed(() => {
@@ -4175,7 +4009,10 @@ class UserActionSelectorComponent {
4175
4009
  return (this.hasChanges() && !this.saving() && this.invalidActionsCount() === 0);
4176
4010
  }, ...(ngDevMode ? [{ debugName: "canSave" }] : []));
4177
4011
  constructor() {
4178
- this.loadUsers();
4012
+ // Cleanup on destroy
4013
+ this.destroyRef.onDestroy(() => {
4014
+ this.loadDataAbortController?.abort();
4015
+ });
4179
4016
  // Effect: Load user branches and data when user changes
4180
4017
  effect(() => {
4181
4018
  const userId = this.selectedUserId();
@@ -4202,28 +4039,6 @@ class UserActionSelectorComponent {
4202
4039
  }
4203
4040
  });
4204
4041
  }
4205
- /**
4206
- * Load users from provider
4207
- */
4208
- async loadUsers() {
4209
- try {
4210
- const response = await this.userProvider
4211
- .getUsers({
4212
- page: 0,
4213
- pageSize: MAX_DROPDOWN_ITEMS,
4214
- })
4215
- .toPromise();
4216
- this.users.set(response?.data ?? []);
4217
- }
4218
- catch (err) {
4219
- console.error('[User Selector] Failed to load users:', err);
4220
- this.messageService.add({
4221
- severity: 'error',
4222
- summary: 'Error',
4223
- detail: 'Failed to load users',
4224
- });
4225
- }
4226
- }
4227
4042
  /**
4228
4043
  * Load user's permitted branches
4229
4044
  */
@@ -4244,13 +4059,8 @@ class UserActionSelectorComponent {
4244
4059
  })) ?? [];
4245
4060
  this.branches.set(userBranches);
4246
4061
  }
4247
- catch (err) {
4248
- console.error('[User Selector] Failed to load user branches:', err);
4249
- this.messageService.add({
4250
- severity: 'error',
4251
- summary: 'Error',
4252
- detail: 'Failed to load user permitted branches',
4253
- });
4062
+ catch {
4063
+ // Error toast handled by global interceptor
4254
4064
  }
4255
4065
  }
4256
4066
  /**
@@ -4290,13 +4100,8 @@ class UserActionSelectorComponent {
4290
4100
  this._selectionMap.set(selMap);
4291
4101
  this._initialSelection.set({ ...selMap });
4292
4102
  }
4293
- catch (err) {
4294
- console.error('[User Selector] Failed to load user actions:', err);
4295
- this.messageService.add({
4296
- severity: 'error',
4297
- summary: 'Error',
4298
- detail: 'Failed to load user action permissions',
4299
- });
4103
+ catch {
4104
+ // Error toast handled by global interceptor
4300
4105
  }
4301
4106
  finally {
4302
4107
  this.loading.set(false);
@@ -4312,15 +4117,15 @@ class UserActionSelectorComponent {
4312
4117
  const prerequisiteInfo = this.permissionLogic.getPrerequisiteTooltip(action, selMap, this.actions());
4313
4118
  if (prerequisiteInfo) {
4314
4119
  const actionHint = isCurrentlySelected
4315
- ? '\n\n━━━━━━━━━━━━━━━━━━\n🗑️ Click to remove'
4316
- : '\n\n━━━━━━━━━━━━━━━━━━\n Click to add (auto-selects required)';
4120
+ ? '\n\n---\n[Remove] Click to remove'
4121
+ : '\n\n---\n[Add] Click to add (auto-selects required)';
4317
4122
  return `${prerequisiteInfo}${actionHint}`;
4318
4123
  }
4319
4124
  }
4320
4125
  if (isCurrentlySelected) {
4321
- return ' Assigned to user\n\n🗑️ Click to remove direct permission';
4126
+ return '[Assigned] Assigned to user\n\n[Remove] Click to remove direct permission';
4322
4127
  }
4323
- return ' Click to assign direct permission';
4128
+ return '[Add] Click to assign direct permission';
4324
4129
  }
4325
4130
  /**
4326
4131
  * Check if action has unmet prerequisites
@@ -4331,12 +4136,17 @@ class UserActionSelectorComponent {
4331
4136
  : false;
4332
4137
  }
4333
4138
  /**
4334
- * Handle action checkbox toggle
4139
+ * Handle action toggle with dependency management
4335
4140
  */
4336
4141
  onActionToggle(action, newValue) {
4337
- const selMap = { ...this.selectionMap() };
4338
- selMap[action.id] = newValue;
4339
- this._selectionMap.set(selMap);
4142
+ if (!newValue) {
4143
+ // Unchecking - validate dependencies
4144
+ this.permissionLogic.handleUncheck(action, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
4145
+ }
4146
+ else {
4147
+ // Checking - validate prerequisites
4148
+ this.permissionLogic.handleCheck(action, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap), (previousState) => this._selectionMap.set(previousState));
4149
+ }
4340
4150
  }
4341
4151
  /**
4342
4152
  * Toggle all actions
@@ -4387,6 +4197,12 @@ class UserActionSelectorComponent {
4387
4197
  });
4388
4198
  return;
4389
4199
  }
4200
+ // Pre-save validation
4201
+ const invalidActions = this.permissionLogic.getActionsWithUnmetPrerequisites(this.selectionMap(), this.actions());
4202
+ if (invalidActions.length > 0) {
4203
+ this.permissionLogic.showValidationErrorDialog(invalidActions, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
4204
+ return;
4205
+ }
4390
4206
  // Build payload
4391
4207
  const items = [];
4392
4208
  this.pendingAdd().forEach((action) => {
@@ -4426,13 +4242,8 @@ class UserActionSelectorComponent {
4426
4242
  // Update baseline
4427
4243
  this._initialSelection.set({ ...this.selectionMap() });
4428
4244
  }
4429
- catch (err) {
4430
- const error = err;
4431
- this.messageService.add({
4432
- severity: 'error',
4433
- summary: 'Error',
4434
- detail: error?.error?.message || 'Failed to update user action permissions',
4435
- });
4245
+ catch {
4246
+ // Error toast handled by global interceptor
4436
4247
  }
4437
4248
  finally {
4438
4249
  this.saving.set(false);
@@ -4447,62 +4258,34 @@ class UserActionSelectorComponent {
4447
4258
  this._selectionMap.set({});
4448
4259
  this._initialSelection.set({});
4449
4260
  }
4450
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UserActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4451
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: UserActionSelectorComponent, isStandalone: true, selector: "flusys-user-action-selector", ngImport: i0, template: `
4261
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4262
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: UserActionSelectorComponent, isStandalone: true, selector: "flusys-user-action-selector", ngImport: i0, template: `
4452
4263
  <div class="user-action-selector">
4453
4264
  <!-- User and Branch Selectors -->
4454
- <div class="surface-card p-4 border-round mb-4 shadow-1">
4455
- <div class="formgrid grid gap-5">
4456
- <div class="field col-12 sm:col-6 mb-0">
4457
- <label class="block font-semibold mb-2 text-900">
4265
+ <div class="surface-card p-4 rounded-border mb-4 shadow-sm">
4266
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
4267
+ <div>
4268
+ <label class="block font-semibold mb-2">
4458
4269
  <i class="pi pi-user mr-2 text-primary"></i>
4459
4270
  Select User
4460
4271
  </label>
4461
- <p-select
4462
- [(ngModel)]="selectedUserId"
4463
- [options]="users()"
4464
- optionLabel="name"
4465
- optionValue="id"
4466
- placeholder="Select a user"
4467
- [showClear]="true"
4468
- [filter]="true"
4469
- styleClass="w-full"
4470
- >
4471
- <ng-template #selectedItem let-user>
4472
- @if (user) {
4473
- <div class="flex align-items-center gap-2">
4474
- <i class="pi pi-user text-primary"></i>
4475
- <div>
4476
- <div class="font-semibold">{{ user.name }}</div>
4477
- <div class="text-xs text-color-secondary">
4478
- {{ user.email }}
4479
- </div>
4480
- </div>
4481
- </div>
4482
- }
4483
- </ng-template>
4484
- <ng-template #item let-user>
4485
- <div class="flex align-items-center gap-2">
4486
- <i class="pi pi-user text-color-secondary"></i>
4487
- <div>
4488
- <div class="font-semibold">{{ user.name }}</div>
4489
- <div class="text-xs text-color-secondary">
4490
- {{ user.email }}
4491
- </div>
4492
- </div>
4493
- </div>
4494
- </ng-template>
4495
- </p-select>
4272
+ <lib-user-select
4273
+ [value]="selectedUserId()"
4274
+ (valueChange)="selectedUserId.set($event)"
4275
+ [isEditMode]="true"
4276
+ placeHolder="Select a user"
4277
+ />
4496
4278
  </div>
4497
4279
 
4498
4280
  @if (showBranchSelector()) {
4499
- <div class="field col-12 sm:col-6 mb-0">
4500
- <label class="block font-semibold mb-2 text-900">
4281
+ <div>
4282
+ <label class="block font-semibold mb-2">
4501
4283
  <i class="pi pi-building mr-2 text-primary"></i>
4502
4284
  Select Branch
4503
4285
  </label>
4504
4286
  <p-select
4505
- [(ngModel)]="selectedBranchId"
4287
+ [ngModel]="selectedBranchId()"
4288
+ (ngModelChange)="selectedBranchId.set($event)"
4506
4289
  [options]="filteredBranches()"
4507
4290
  optionLabel="name"
4508
4291
  optionValue="id"
@@ -4513,20 +4296,20 @@ class UserActionSelectorComponent {
4513
4296
  >
4514
4297
  <ng-template #selectedItem let-branch>
4515
4298
  @if (branch) {
4516
- <div class="flex align-items-center gap-2">
4299
+ <div class="flex items-center gap-2">
4517
4300
  <i class="pi pi-building text-primary"></i>
4518
4301
  <span class="font-semibold">{{ branch.name }}</span>
4519
4302
  </div>
4520
4303
  }
4521
4304
  </ng-template>
4522
4305
  <ng-template #item let-branch>
4523
- <div class="flex align-items-center gap-2">
4524
- <i class="pi pi-building text-color-secondary"></i>
4306
+ <div class="flex items-center gap-2">
4307
+ <i class="pi pi-building text-muted-color"></i>
4525
4308
  <span>{{ branch.name }}</span>
4526
4309
  </div>
4527
4310
  </ng-template>
4528
4311
  </p-select>
4529
- <small class="text-color-secondary block mt-1">
4312
+ <small class="text-muted-color block mt-1">
4530
4313
  {{ filteredBranches().length }} permitted branch{{
4531
4314
  filteredBranches().length !== 1 ? 'es' : ''
4532
4315
  }}
@@ -4540,20 +4323,20 @@ class UserActionSelectorComponent {
4540
4323
  @if (selectedUserId()) {
4541
4324
  <!-- Loading State -->
4542
4325
  @if (loading()) {
4543
- <div class="flex justify-content-center p-5">
4544
- <i class="pi pi-spin pi-spinner" style="font-size: 2rem"></i>
4326
+ <div class="flex justify-center p-5">
4327
+ <i class="pi pi-spin pi-spinner text-4xl"></i>
4545
4328
  </div>
4546
4329
  }
4547
4330
 
4548
4331
  <!-- Action List -->
4549
4332
  @if (!loading() && actions().length > 0) {
4550
- <div class="surface-card p-4 border-round shadow-1">
4333
+ <div class="surface-card p-4 rounded-border shadow-sm">
4551
4334
  <div
4552
- class="flex flex-column md:flex-row justify-content-between align-items-start md:align-items-center gap-3 mb-4"
4335
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
4553
4336
  >
4554
4337
  <div>
4555
4338
  <h5 class="m-0 mb-1">Direct Action Permissions</h5>
4556
- <p class="text-sm text-color-secondary m-0">
4339
+ <p class="text-sm text-muted-color m-0">
4557
4340
  {{ actions().length }} actions available
4558
4341
  </p>
4559
4342
  </div>
@@ -4575,6 +4358,7 @@ class UserActionSelectorComponent {
4575
4358
  (onClick)="deselectAll()"
4576
4359
  />
4577
4360
  <p-button
4361
+ *hasPermission="USER_ACTION_PERMISSIONS.ASSIGN"
4578
4362
  label="Save Changes"
4579
4363
  icon="pi pi-save"
4580
4364
  [disabled]="!canSave()"
@@ -4586,34 +4370,52 @@ class UserActionSelectorComponent {
4586
4370
  </div>
4587
4371
  </div>
4588
4372
 
4373
+ <!-- Validation Warning -->
4374
+ @if (invalidActionsCount() > 0) {
4375
+ <div class="validation-warning rounded-border p-3 mb-4">
4376
+ <div class="flex items-start gap-2">
4377
+ <i class="pi pi-exclamation-triangle text-xl"></i>
4378
+ <div class="flex-1">
4379
+ <span class="font-semibold">Validation Warning:</span>
4380
+ <p class="text-sm mt-1 mb-0">
4381
+ {{ invalidActionsCount() }} selected
4382
+ action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
4383
+ unmet prerequisites. Fix before saving or use auto-fix on
4384
+ save.
4385
+ </p>
4386
+ </div>
4387
+ </div>
4388
+ </div>
4389
+ }
4390
+
4589
4391
  <!-- Action Tree Table -->
4590
- <p-treeTable
4591
- [value]="treeNodes()"
4592
- [scrollable]="true"
4593
- scrollHeight="flex"
4594
- dataKey="id"
4595
- styleClass="p-treetable-sm"
4596
- >
4597
- <ng-template pTemplate="header">
4598
- <tr>
4599
- <th style="width: 3rem">
4600
- <p-checkbox
4601
- [ngModel]="allSelected()"
4602
- [binary]="true"
4603
- (ngModelChange)="toggleAll()"
4604
- pTooltip="Select/Deselect All"
4605
- tooltipPosition="top"
4606
- />
4607
- </th>
4608
- <th>Name</th>
4609
- <th>Code</th>
4610
- <th>Type</th>
4611
- <th>Description</th>
4612
- </tr>
4613
- </ng-template>
4614
- <ng-template pTemplate="body" let-rowNode let-rowData="rowData">
4615
- <tr>
4616
- <td style="width: 3rem">
4392
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
4393
+ <p-treeTable
4394
+ [value]="treeNodes()"
4395
+ dataKey="id"
4396
+ styleClass="p-treetable-sm"
4397
+ [tableStyle]="{ 'min-width': '50rem' }"
4398
+ >
4399
+ <ng-template #header>
4400
+ <tr>
4401
+ <th class="w-12">
4402
+ <p-checkbox
4403
+ [ngModel]="allSelected()"
4404
+ [binary]="true"
4405
+ (ngModelChange)="toggleAll()"
4406
+ pTooltip="Select/Deselect All"
4407
+ tooltipPosition="top"
4408
+ />
4409
+ </th>
4410
+ <th>Name</th>
4411
+ <th class="hidden md:table-cell">Code</th>
4412
+ <th>Type</th>
4413
+ <th class="hidden lg:table-cell">Description</th>
4414
+ </tr>
4415
+ </ng-template>
4416
+ <ng-template #body let-rowNode let-rowData="rowData">
4417
+ <tr [class.highlight-warning]="hasUnmetPrerequisites(rowData)">
4418
+ <td class="w-12">
4617
4419
  <p-checkbox
4618
4420
  [ngModel]="selectionMap()[rowData.id]"
4619
4421
  [binary]="true"
@@ -4624,9 +4426,18 @@ class UserActionSelectorComponent {
4624
4426
  </td>
4625
4427
  <td>
4626
4428
  <p-treeTableToggler [rowNode]="rowNode" />
4627
- <span>{{ rowData.name }}</span>
4429
+ <span class="inline-flex items-center gap-2">
4430
+ {{ rowData.name }}
4431
+ @if (hasUnmetPrerequisites(rowData)) {
4432
+ <i
4433
+ class="pi pi-exclamation-triangle text-orange-500"
4434
+ pTooltip="This action has unmet prerequisites and will fail validation on save"
4435
+ tooltipPosition="top"
4436
+ ></i>
4437
+ }
4438
+ </span>
4628
4439
  </td>
4629
- <td>{{ rowData.code || '-' }}</td>
4440
+ <td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
4630
4441
  <td>
4631
4442
  <p-tag
4632
4443
  [value]="rowData.actionType"
@@ -4635,12 +4446,12 @@ class UserActionSelectorComponent {
4635
4446
  "
4636
4447
  />
4637
4448
  </td>
4638
- <td>{{ rowData.description || '-' }}</td>
4449
+ <td class="hidden lg:table-cell">{{ rowData.description || '-' }}</td>
4639
4450
  </tr>
4640
4451
  </ng-template>
4641
- <ng-template pTemplate="emptymessage">
4452
+ <ng-template #emptymessage>
4642
4453
  <tr>
4643
- <td colspan="5" class="text-center p-4">
4454
+ <td colspan="5" class="text-center p-4 text-muted-color">
4644
4455
  @if (loading()) {
4645
4456
  <i class="pi pi-spin pi-spinner"></i> Loading actions...
4646
4457
  } @else {
@@ -4649,24 +4460,23 @@ class UserActionSelectorComponent {
4649
4460
  </td>
4650
4461
  </tr>
4651
4462
  </ng-template>
4652
- </p-treeTable>
4463
+ </p-treeTable>
4464
+ </div>
4653
4465
  </div>
4654
4466
 
4655
4467
  <!-- Change Summary -->
4656
4468
  @if (hasChanges()) {
4657
- <div class="surface-border border-1 border-round p-3 mt-4">
4658
- <div class="flex align-items-center gap-2 mb-3">
4659
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
4660
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
4469
+ <div class="border border-surface rounded-border p-3 mt-4">
4470
+ <div class="flex items-center gap-2 mb-3">
4471
+ <i class="pi pi-info-circle text-primary"></i>
4472
+ <span class="font-bold">Pending Changes</span>
4661
4473
  </div>
4662
- <div class="flex flex-col md:flex-row gap-4">
4474
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
4663
4475
  @if (pendingAdd().length > 0) {
4664
- <div class="w-full md:w-1/2">
4665
- <div class="flex align-items-center gap-2 mb-2">
4476
+ <div>
4477
+ <div class="flex items-center gap-2 mb-2">
4666
4478
  <i class="pi pi-plus-circle text-green-500"></i>
4667
- <strong class="text-sm"
4668
- >To Assign ({{ pendingAdd().length }})</strong
4669
- >
4479
+ <strong class="text-sm">To Assign ({{ pendingAdd().length }})</strong>
4670
4480
  </div>
4671
4481
  <ul class="list-none p-0 m-0 pl-4">
4672
4482
  @for (action of pendingAdd(); track action.id) {
@@ -4676,12 +4486,10 @@ class UserActionSelectorComponent {
4676
4486
  </div>
4677
4487
  }
4678
4488
  @if (pendingRemove().length > 0) {
4679
- <div class="w-full md:w-1/2">
4680
- <div class="flex align-items-center gap-2 mb-2">
4489
+ <div>
4490
+ <div class="flex items-center gap-2 mb-2">
4681
4491
  <i class="pi pi-minus-circle text-red-500"></i>
4682
- <strong class="text-sm"
4683
- >To Remove ({{ pendingRemove().length }})</strong
4684
- >
4492
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
4685
4493
  </div>
4686
4494
  <ul class="list-none p-0 m-0 pl-4">
4687
4495
  @for (action of pendingRemove(); track action.id) {
@@ -4696,77 +4504,46 @@ class UserActionSelectorComponent {
4696
4504
  }
4697
4505
 
4698
4506
  @if (!loading() && actions().length === 0) {
4699
- <div class="surface-card p-5 border-round shadow-1 text-center">
4700
- <i
4701
- class="pi pi-info-circle text-color-secondary mb-3"
4702
- style="font-size: 3rem; display: block;"
4703
- ></i>
4704
- <p class="text-color-secondary m-0">
4507
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
4508
+ <i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
4509
+ <p class="text-muted-color m-0">
4705
4510
  No actions available for this user.
4706
4511
  </p>
4707
4512
  </div>
4708
4513
  }
4709
4514
  }
4710
4515
  </div>
4711
- `, 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.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "directive", type: i3$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i3.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: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "component", type: i6.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: i7.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: 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 });
4516
+ `, 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: ["loadUsers", "placeHolder", "isEditMode", "filterActive", "additionalFilters", "pageSize", "value"], outputs: ["valueChange", "userSelected", "onError"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4712
4517
  }
4713
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UserActionSelectorComponent, decorators: [{
4518
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserActionSelectorComponent, decorators: [{
4714
4519
  type: Component,
4715
- args: [{ selector: 'flusys-user-action-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule], template: `
4520
+ args: [{ selector: 'flusys-user-action-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule, HasPermissionDirective, UserSelectComponent], template: `
4716
4521
  <div class="user-action-selector">
4717
4522
  <!-- User and Branch Selectors -->
4718
- <div class="surface-card p-4 border-round mb-4 shadow-1">
4719
- <div class="formgrid grid gap-5">
4720
- <div class="field col-12 sm:col-6 mb-0">
4721
- <label class="block font-semibold mb-2 text-900">
4523
+ <div class="surface-card p-4 rounded-border mb-4 shadow-sm">
4524
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
4525
+ <div>
4526
+ <label class="block font-semibold mb-2">
4722
4527
  <i class="pi pi-user mr-2 text-primary"></i>
4723
4528
  Select User
4724
4529
  </label>
4725
- <p-select
4726
- [(ngModel)]="selectedUserId"
4727
- [options]="users()"
4728
- optionLabel="name"
4729
- optionValue="id"
4730
- placeholder="Select a user"
4731
- [showClear]="true"
4732
- [filter]="true"
4733
- styleClass="w-full"
4734
- >
4735
- <ng-template #selectedItem let-user>
4736
- @if (user) {
4737
- <div class="flex align-items-center gap-2">
4738
- <i class="pi pi-user text-primary"></i>
4739
- <div>
4740
- <div class="font-semibold">{{ user.name }}</div>
4741
- <div class="text-xs text-color-secondary">
4742
- {{ user.email }}
4743
- </div>
4744
- </div>
4745
- </div>
4746
- }
4747
- </ng-template>
4748
- <ng-template #item let-user>
4749
- <div class="flex align-items-center gap-2">
4750
- <i class="pi pi-user text-color-secondary"></i>
4751
- <div>
4752
- <div class="font-semibold">{{ user.name }}</div>
4753
- <div class="text-xs text-color-secondary">
4754
- {{ user.email }}
4755
- </div>
4756
- </div>
4757
- </div>
4758
- </ng-template>
4759
- </p-select>
4530
+ <lib-user-select
4531
+ [value]="selectedUserId()"
4532
+ (valueChange)="selectedUserId.set($event)"
4533
+ [isEditMode]="true"
4534
+ placeHolder="Select a user"
4535
+ />
4760
4536
  </div>
4761
4537
 
4762
4538
  @if (showBranchSelector()) {
4763
- <div class="field col-12 sm:col-6 mb-0">
4764
- <label class="block font-semibold mb-2 text-900">
4539
+ <div>
4540
+ <label class="block font-semibold mb-2">
4765
4541
  <i class="pi pi-building mr-2 text-primary"></i>
4766
4542
  Select Branch
4767
4543
  </label>
4768
4544
  <p-select
4769
- [(ngModel)]="selectedBranchId"
4545
+ [ngModel]="selectedBranchId()"
4546
+ (ngModelChange)="selectedBranchId.set($event)"
4770
4547
  [options]="filteredBranches()"
4771
4548
  optionLabel="name"
4772
4549
  optionValue="id"
@@ -4777,20 +4554,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4777
4554
  >
4778
4555
  <ng-template #selectedItem let-branch>
4779
4556
  @if (branch) {
4780
- <div class="flex align-items-center gap-2">
4557
+ <div class="flex items-center gap-2">
4781
4558
  <i class="pi pi-building text-primary"></i>
4782
4559
  <span class="font-semibold">{{ branch.name }}</span>
4783
4560
  </div>
4784
4561
  }
4785
4562
  </ng-template>
4786
4563
  <ng-template #item let-branch>
4787
- <div class="flex align-items-center gap-2">
4788
- <i class="pi pi-building text-color-secondary"></i>
4564
+ <div class="flex items-center gap-2">
4565
+ <i class="pi pi-building text-muted-color"></i>
4789
4566
  <span>{{ branch.name }}</span>
4790
4567
  </div>
4791
4568
  </ng-template>
4792
4569
  </p-select>
4793
- <small class="text-color-secondary block mt-1">
4570
+ <small class="text-muted-color block mt-1">
4794
4571
  {{ filteredBranches().length }} permitted branch{{
4795
4572
  filteredBranches().length !== 1 ? 'es' : ''
4796
4573
  }}
@@ -4804,20 +4581,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4804
4581
  @if (selectedUserId()) {
4805
4582
  <!-- Loading State -->
4806
4583
  @if (loading()) {
4807
- <div class="flex justify-content-center p-5">
4808
- <i class="pi pi-spin pi-spinner" style="font-size: 2rem"></i>
4584
+ <div class="flex justify-center p-5">
4585
+ <i class="pi pi-spin pi-spinner text-4xl"></i>
4809
4586
  </div>
4810
4587
  }
4811
4588
 
4812
4589
  <!-- Action List -->
4813
4590
  @if (!loading() && actions().length > 0) {
4814
- <div class="surface-card p-4 border-round shadow-1">
4591
+ <div class="surface-card p-4 rounded-border shadow-sm">
4815
4592
  <div
4816
- class="flex flex-column md:flex-row justify-content-between align-items-start md:align-items-center gap-3 mb-4"
4593
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
4817
4594
  >
4818
4595
  <div>
4819
4596
  <h5 class="m-0 mb-1">Direct Action Permissions</h5>
4820
- <p class="text-sm text-color-secondary m-0">
4597
+ <p class="text-sm text-muted-color m-0">
4821
4598
  {{ actions().length }} actions available
4822
4599
  </p>
4823
4600
  </div>
@@ -4839,6 +4616,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4839
4616
  (onClick)="deselectAll()"
4840
4617
  />
4841
4618
  <p-button
4619
+ *hasPermission="USER_ACTION_PERMISSIONS.ASSIGN"
4842
4620
  label="Save Changes"
4843
4621
  icon="pi pi-save"
4844
4622
  [disabled]="!canSave()"
@@ -4850,34 +4628,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4850
4628
  </div>
4851
4629
  </div>
4852
4630
 
4631
+ <!-- Validation Warning -->
4632
+ @if (invalidActionsCount() > 0) {
4633
+ <div class="validation-warning rounded-border p-3 mb-4">
4634
+ <div class="flex items-start gap-2">
4635
+ <i class="pi pi-exclamation-triangle text-xl"></i>
4636
+ <div class="flex-1">
4637
+ <span class="font-semibold">Validation Warning:</span>
4638
+ <p class="text-sm mt-1 mb-0">
4639
+ {{ invalidActionsCount() }} selected
4640
+ action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
4641
+ unmet prerequisites. Fix before saving or use auto-fix on
4642
+ save.
4643
+ </p>
4644
+ </div>
4645
+ </div>
4646
+ </div>
4647
+ }
4648
+
4853
4649
  <!-- Action Tree Table -->
4854
- <p-treeTable
4855
- [value]="treeNodes()"
4856
- [scrollable]="true"
4857
- scrollHeight="flex"
4858
- dataKey="id"
4859
- styleClass="p-treetable-sm"
4860
- >
4861
- <ng-template pTemplate="header">
4862
- <tr>
4863
- <th style="width: 3rem">
4864
- <p-checkbox
4865
- [ngModel]="allSelected()"
4866
- [binary]="true"
4867
- (ngModelChange)="toggleAll()"
4868
- pTooltip="Select/Deselect All"
4869
- tooltipPosition="top"
4870
- />
4871
- </th>
4872
- <th>Name</th>
4873
- <th>Code</th>
4874
- <th>Type</th>
4875
- <th>Description</th>
4876
- </tr>
4877
- </ng-template>
4878
- <ng-template pTemplate="body" let-rowNode let-rowData="rowData">
4879
- <tr>
4880
- <td style="width: 3rem">
4650
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
4651
+ <p-treeTable
4652
+ [value]="treeNodes()"
4653
+ dataKey="id"
4654
+ styleClass="p-treetable-sm"
4655
+ [tableStyle]="{ 'min-width': '50rem' }"
4656
+ >
4657
+ <ng-template #header>
4658
+ <tr>
4659
+ <th class="w-12">
4660
+ <p-checkbox
4661
+ [ngModel]="allSelected()"
4662
+ [binary]="true"
4663
+ (ngModelChange)="toggleAll()"
4664
+ pTooltip="Select/Deselect All"
4665
+ tooltipPosition="top"
4666
+ />
4667
+ </th>
4668
+ <th>Name</th>
4669
+ <th class="hidden md:table-cell">Code</th>
4670
+ <th>Type</th>
4671
+ <th class="hidden lg:table-cell">Description</th>
4672
+ </tr>
4673
+ </ng-template>
4674
+ <ng-template #body let-rowNode let-rowData="rowData">
4675
+ <tr [class.highlight-warning]="hasUnmetPrerequisites(rowData)">
4676
+ <td class="w-12">
4881
4677
  <p-checkbox
4882
4678
  [ngModel]="selectionMap()[rowData.id]"
4883
4679
  [binary]="true"
@@ -4888,9 +4684,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4888
4684
  </td>
4889
4685
  <td>
4890
4686
  <p-treeTableToggler [rowNode]="rowNode" />
4891
- <span>{{ rowData.name }}</span>
4687
+ <span class="inline-flex items-center gap-2">
4688
+ {{ rowData.name }}
4689
+ @if (hasUnmetPrerequisites(rowData)) {
4690
+ <i
4691
+ class="pi pi-exclamation-triangle text-orange-500"
4692
+ pTooltip="This action has unmet prerequisites and will fail validation on save"
4693
+ tooltipPosition="top"
4694
+ ></i>
4695
+ }
4696
+ </span>
4892
4697
  </td>
4893
- <td>{{ rowData.code || '-' }}</td>
4698
+ <td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
4894
4699
  <td>
4895
4700
  <p-tag
4896
4701
  [value]="rowData.actionType"
@@ -4899,12 +4704,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4899
4704
  "
4900
4705
  />
4901
4706
  </td>
4902
- <td>{{ rowData.description || '-' }}</td>
4707
+ <td class="hidden lg:table-cell">{{ rowData.description || '-' }}</td>
4903
4708
  </tr>
4904
4709
  </ng-template>
4905
- <ng-template pTemplate="emptymessage">
4710
+ <ng-template #emptymessage>
4906
4711
  <tr>
4907
- <td colspan="5" class="text-center p-4">
4712
+ <td colspan="5" class="text-center p-4 text-muted-color">
4908
4713
  @if (loading()) {
4909
4714
  <i class="pi pi-spin pi-spinner"></i> Loading actions...
4910
4715
  } @else {
@@ -4913,24 +4718,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4913
4718
  </td>
4914
4719
  </tr>
4915
4720
  </ng-template>
4916
- </p-treeTable>
4721
+ </p-treeTable>
4722
+ </div>
4917
4723
  </div>
4918
4724
 
4919
4725
  <!-- Change Summary -->
4920
4726
  @if (hasChanges()) {
4921
- <div class="surface-border border-1 border-round p-3 mt-4">
4922
- <div class="flex align-items-center gap-2 mb-3">
4923
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
4924
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
4727
+ <div class="border border-surface rounded-border p-3 mt-4">
4728
+ <div class="flex items-center gap-2 mb-3">
4729
+ <i class="pi pi-info-circle text-primary"></i>
4730
+ <span class="font-bold">Pending Changes</span>
4925
4731
  </div>
4926
- <div class="flex flex-col md:flex-row gap-4">
4732
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
4927
4733
  @if (pendingAdd().length > 0) {
4928
- <div class="w-full md:w-1/2">
4929
- <div class="flex align-items-center gap-2 mb-2">
4734
+ <div>
4735
+ <div class="flex items-center gap-2 mb-2">
4930
4736
  <i class="pi pi-plus-circle text-green-500"></i>
4931
- <strong class="text-sm"
4932
- >To Assign ({{ pendingAdd().length }})</strong
4933
- >
4737
+ <strong class="text-sm">To Assign ({{ pendingAdd().length }})</strong>
4934
4738
  </div>
4935
4739
  <ul class="list-none p-0 m-0 pl-4">
4936
4740
  @for (action of pendingAdd(); track action.id) {
@@ -4940,12 +4744,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4940
4744
  </div>
4941
4745
  }
4942
4746
  @if (pendingRemove().length > 0) {
4943
- <div class="w-full md:w-1/2">
4944
- <div class="flex align-items-center gap-2 mb-2">
4747
+ <div>
4748
+ <div class="flex items-center gap-2 mb-2">
4945
4749
  <i class="pi pi-minus-circle text-red-500"></i>
4946
- <strong class="text-sm"
4947
- >To Remove ({{ pendingRemove().length }})</strong
4948
- >
4750
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
4949
4751
  </div>
4950
4752
  <ul class="list-none p-0 m-0 pl-4">
4951
4753
  @for (action of pendingRemove(); track action.id) {
@@ -4960,23 +4762,101 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4960
4762
  }
4961
4763
 
4962
4764
  @if (!loading() && actions().length === 0) {
4963
- <div class="surface-card p-5 border-round shadow-1 text-center">
4964
- <i
4965
- class="pi pi-info-circle text-color-secondary mb-3"
4966
- style="font-size: 3rem; display: block;"
4967
- ></i>
4968
- <p class="text-color-secondary m-0">
4765
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
4766
+ <i class="pi pi-info-circle text-muted-color mb-3 block text-5xl"></i>
4767
+ <p class="text-muted-color m-0">
4969
4768
  No actions available for this user.
4970
4769
  </p>
4971
4770
  </div>
4972
4771
  }
4973
4772
  }
4974
4773
  </div>
4975
- `, styles: [":host{display:block}\n"] }]
4774
+ `, 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"] }]
4976
4775
  }], ctorParameters: () => [] });
4977
4776
 
4978
4777
  // Logic Builder Component
4979
4778
 
4779
+ /**
4780
+ * Profile Permission Provider Adapter
4781
+ *
4782
+ * Implements IProfilePermissionProvider using ng-iam's PermissionApiService.
4783
+ * Allows ng-auth profile page to display user permissions without
4784
+ * depending on ng-iam directly.
4785
+ *
4786
+ * @example
4787
+ * // In app.config.ts
4788
+ * providers: [
4789
+ * ...provideIamProviders()
4790
+ * ]
4791
+ *
4792
+ * // In profile component
4793
+ * private readonly permissionProvider = inject(PROFILE_PERMISSION_PROVIDER, { optional: true });
4794
+ */
4795
+ class ProfilePermissionProviderAdapter {
4796
+ permissionApi = inject(PermissionApiService);
4797
+ getUserRoles(userId, branchId) {
4798
+ return this.permissionApi
4799
+ .getUserRoles(userId, branchId ? { branchId } : undefined)
4800
+ .pipe(map$1((response) => ({
4801
+ success: response.success,
4802
+ message: response.message,
4803
+ data: (response.data ?? []).map((role) => ({
4804
+ id: role.roleId,
4805
+ name: role.roleName,
4806
+ description: null,
4807
+ })),
4808
+ })));
4809
+ }
4810
+ getUserActions(userId, branchId) {
4811
+ return this.permissionApi
4812
+ .getUserActions(userId, branchId ? { branchId } : undefined)
4813
+ .pipe(map$1((response) => ({
4814
+ success: response.success,
4815
+ message: response.message,
4816
+ data: (response.data ?? []).map((action) => ({
4817
+ id: action.actionId,
4818
+ name: action.actionName,
4819
+ code: action.actionCode,
4820
+ description: null,
4821
+ })),
4822
+ })));
4823
+ }
4824
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ProfilePermissionProviderAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4825
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ProfilePermissionProviderAdapter });
4826
+ }
4827
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ProfilePermissionProviderAdapter, decorators: [{
4828
+ type: Injectable
4829
+ }] });
4830
+
4831
+ /**
4832
+ * Provide IAM Provider Adapters
4833
+ *
4834
+ * Registers IAM implementations for provider interfaces from ng-shared.
4835
+ * This allows ng-auth profile page to display permissions without direct dependencies.
4836
+ *
4837
+ * @example
4838
+ * // In app.config.ts
4839
+ * import { provideIamProviders } from '@flusys/ng-iam';
4840
+ *
4841
+ * export const appConfig: ApplicationConfig = {
4842
+ * providers: [
4843
+ * ...provideIamProviders(),
4844
+ * // ... other providers
4845
+ * ]
4846
+ * };
4847
+ *
4848
+ * @returns Array of Angular providers
4849
+ */
4850
+ function provideIamProviders() {
4851
+ return [
4852
+ // Profile permission provider (for auth profile page)
4853
+ {
4854
+ provide: PROFILE_PERMISSION_PROVIDER,
4855
+ useClass: ProfilePermissionProviderAdapter,
4856
+ },
4857
+ ];
4858
+ }
4859
+
4980
4860
  /**
4981
4861
  * IAM Routes Configuration
4982
4862
  *
@@ -4990,7 +4870,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4990
4870
  const IAM_ROUTES = [
4991
4871
  {
4992
4872
  path: '',
4993
- loadComponent: () => import('./flusys-ng-iam-iam-container.component-BFKoNtdz.mjs').then((m) => m.IamContainerComponent),
4873
+ loadComponent: () => import('./flusys-ng-iam-iam-container.component-Bn4kQtxW.mjs').then((m) => m.IamContainerComponent),
4994
4874
  children: [
4995
4875
  // Actions Management
4996
4876
  {
@@ -4998,15 +4878,15 @@ const IAM_ROUTES = [
4998
4878
  children: [
4999
4879
  {
5000
4880
  path: '',
5001
- loadComponent: () => import('./flusys-ng-iam-action-list-page.component-CCp7M7xo.mjs').then((m) => m.ActionListPageComponent),
4881
+ loadComponent: () => import('./flusys-ng-iam-action-list-page.component-Daf93zpS.mjs').then((m) => m.ActionListPageComponent),
5002
4882
  },
5003
4883
  {
5004
4884
  path: 'new',
5005
- loadComponent: () => import('./flusys-ng-iam-action-form-page.component-BVWZMlLU.mjs').then((m) => m.ActionFormPageComponent),
4885
+ loadComponent: () => import('./flusys-ng-iam-action-form-page.component-C_BRrrWW.mjs').then((m) => m.ActionFormPageComponent),
5006
4886
  },
5007
4887
  {
5008
4888
  path: ':id',
5009
- loadComponent: () => import('./flusys-ng-iam-action-form-page.component-BVWZMlLU.mjs').then((m) => m.ActionFormPageComponent),
4889
+ loadComponent: () => import('./flusys-ng-iam-action-form-page.component-C_BRrrWW.mjs').then((m) => m.ActionFormPageComponent),
5010
4890
  },
5011
4891
  ],
5012
4892
  },
@@ -5016,22 +4896,22 @@ const IAM_ROUTES = [
5016
4896
  children: [
5017
4897
  {
5018
4898
  path: '',
5019
- loadComponent: () => import('./flusys-ng-iam-role-list-page.component-KJUVOrf0.mjs').then((m) => m.RoleListPageComponent),
4899
+ loadComponent: () => import('./flusys-ng-iam-role-list-page.component-CFly5KnH.mjs').then((m) => m.RoleListPageComponent),
5020
4900
  },
5021
4901
  {
5022
4902
  path: 'new',
5023
- loadComponent: () => import('./flusys-ng-iam-role-form-page.component-Co6bbHik.mjs').then((m) => m.RoleFormPageComponent),
4903
+ loadComponent: () => import('./flusys-ng-iam-role-form-page.component-ByNueI1a.mjs').then((m) => m.RoleFormPageComponent),
5024
4904
  },
5025
4905
  {
5026
4906
  path: ':id',
5027
- loadComponent: () => import('./flusys-ng-iam-role-form-page.component-Co6bbHik.mjs').then((m) => m.RoleFormPageComponent),
4907
+ loadComponent: () => import('./flusys-ng-iam-role-form-page.component-ByNueI1a.mjs').then((m) => m.RoleFormPageComponent),
5028
4908
  },
5029
4909
  ],
5030
4910
  },
5031
4911
  // Permissions Management (User permission assignment)
5032
4912
  {
5033
4913
  path: 'permissions',
5034
- loadComponent: () => import('./flusys-ng-iam-permission-page.component-C0yLMdW5.mjs').then((m) => m.PermissionPageComponent),
4914
+ loadComponent: () => import('./flusys-ng-iam-permission-page.component-CmxOBJPu.mjs').then((m) => m.PermissionPageComponent),
5035
4915
  },
5036
4916
  // Default redirect to actions
5037
4917
  {
@@ -5049,5 +4929,5 @@ const IAM_ROUTES = [
5049
4929
  * Generated bundle index. Do not edit.
5050
4930
  */
5051
4931
 
5052
- 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 };
5053
- //# sourceMappingURL=flusys-ng-iam-flusys-ng-iam-D_i24Gub.mjs.map
4932
+ 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 };
4933
+ //# sourceMappingURL=flusys-ng-iam-flusys-ng-iam-BPIpfrjN.mjs.map