@flusys/ng-iam 1.0.0-beta → 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 -24
  2. package/fesm2022/{flusys-ng-iam-action-form-page.component-0b9GwJqa.mjs → flusys-ng-iam-action-form-page.component-C_BRrrWW.mjs} +175 -203
  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-C-MQjakK.mjs → flusys-ng-iam-flusys-ng-iam-BPIpfrjN.mjs} +892 -818
  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-e_RX5mky.mjs → flusys-ng-iam-permission-page.component-CmxOBJPu.mjs} +42 -13
  11. package/fesm2022/flusys-ng-iam-permission-page.component-CmxOBJPu.mjs.map +1 -0
  12. package/fesm2022/{flusys-ng-iam-role-form-page.component-eZM1EPps.mjs → flusys-ng-iam-role-form-page.component-ByNueI1a.mjs} +107 -135
  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 +87 -23
  19. package/fesm2022/flusys-ng-iam-action-form-page.component-0b9GwJqa.mjs.map +0 -1
  20. package/fesm2022/flusys-ng-iam-action-list-page.component-BpvewEGL.mjs +0 -281
  21. package/fesm2022/flusys-ng-iam-action-list-page.component-BpvewEGL.mjs.map +0 -1
  22. package/fesm2022/flusys-ng-iam-flusys-ng-iam-C-MQjakK.mjs.map +0 -1
  23. package/fesm2022/flusys-ng-iam-iam-container.component-Chl5MDkV.mjs +0 -97
  24. package/fesm2022/flusys-ng-iam-iam-container.component-Chl5MDkV.mjs.map +0 -1
  25. package/fesm2022/flusys-ng-iam-permission-page.component-e_RX5mky.mjs.map +0 -1
  26. package/fesm2022/flusys-ng-iam-role-form-page.component-eZM1EPps.mjs.map +0 -1
  27. package/fesm2022/flusys-ng-iam-role-list-page.component-BgzxmHjk.mjs +0 -266
  28. package/fesm2022/flusys-ng-iam-role-list-page.component-BgzxmHjk.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_PERMISSION_PROVIDER, UserSelectComponent } 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
 
@@ -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
@@ -835,14 +833,10 @@ class PermissionApiService extends BaseApiService {
835
833
  }
836
834
  /**
837
835
  * Get user's direct action permissions
838
- * GET /permissions/user-actions/:userId
836
+ * POST /iam/permissions/get-user-actions
839
837
  */
840
838
  getUserActions(userId, query) {
841
- let params = new HttpParams();
842
- if (query?.branchId) {
843
- params = params.set('branchId', query.branchId);
844
- }
845
- return this.http.get(this.getUrl(`permissions/user-actions/${userId}`), { params });
839
+ return this.http.post(this.getUrl('permissions/get-user-actions'), { userId, branchId: query?.branchId });
846
840
  }
847
841
  // =============================================================================
848
842
  // User → Role (Role Assignments)
@@ -856,14 +850,10 @@ class PermissionApiService extends BaseApiService {
856
850
  }
857
851
  /**
858
852
  * Get user's role assignments
859
- * GET /permissions/user-roles/:userId
853
+ * POST /iam/permissions/get-user-roles
860
854
  */
861
855
  getUserRoles(userId, query) {
862
- let params = new HttpParams();
863
- if (query?.branchId) {
864
- params = params.set('branchId', query.branchId);
865
- }
866
- return this.http.get(this.getUrl(`permissions/user-roles/${userId}`), { params });
856
+ return this.http.post(this.getUrl('permissions/get-user-roles'), { userId, branchId: query?.branchId });
867
857
  }
868
858
  // =============================================================================
869
859
  // Role → Action (Role Permissions)
@@ -877,10 +867,10 @@ class PermissionApiService extends BaseApiService {
877
867
  }
878
868
  /**
879
869
  * Get role's action permissions
880
- * GET /permissions/role-actions/:roleId
870
+ * POST /iam/permissions/get-role-actions
881
871
  */
882
872
  getRoleActions(roleId, query) {
883
- return this.http.get(this.getUrl(`permissions/role-actions/${roleId}`));
873
+ return this.http.post(this.getUrl('permissions/get-role-actions'), { roleId });
884
874
  }
885
875
  // =============================================================================
886
876
  // Company → Action (Company Whitelisting)
@@ -894,10 +884,10 @@ class PermissionApiService extends BaseApiService {
894
884
  }
895
885
  /**
896
886
  * Get company's whitelisted actions
897
- * GET /permissions/company-actions/:companyId
887
+ * POST /iam/permissions/get-company-actions
898
888
  */
899
889
  getCompanyActions(companyId) {
900
- return this.http.get(this.getUrl(`permissions/company-actions/${companyId}`));
890
+ return this.http.post(this.getUrl('permissions/get-company-actions'), { companyId });
901
891
  }
902
892
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
903
893
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionApiService, providedIn: 'root' });
@@ -1037,77 +1027,85 @@ class LogicBuilderComponent {
1037
1027
  actions = input([], ...(ngDevMode ? [{ debugName: "actions" }] : []));
1038
1028
  logicChange = output();
1039
1029
  availableActions = computed(() => this.actions(), ...(ngDevMode ? [{ debugName: "availableActions" }] : []));
1040
- builderTree = null;
1041
- builderLogic = computed(() => {
1042
- const logic = this.logic();
1043
- if (!logic) {
1044
- this.builderTree = null;
1045
- return null;
1046
- }
1047
- if (!this.builderTree) {
1048
- this.builderTree = toBuilderNode(logic);
1049
- }
1050
- return this.builderTree;
1051
- }, ...(ngDevMode ? [{ debugName: "builderLogic" }] : []));
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
+ }
1052
1045
  initializeLogic() {
1053
- this.builderTree = {
1046
+ this._builderTree.set({
1054
1047
  id: crypto.randomUUID(),
1055
1048
  type: 'group',
1056
1049
  operator: 'AND',
1057
1050
  children: [],
1058
- };
1051
+ });
1059
1052
  this.emitChange();
1060
1053
  }
1061
1054
  clearLogic() {
1062
- this.builderTree = null;
1055
+ this._builderTree.set(null);
1063
1056
  this.logicChange.emit(null);
1064
1057
  }
1065
1058
  toggleOperator(nodeId) {
1066
- if (!this.builderTree)
1059
+ const tree = this._builderTree();
1060
+ if (!tree)
1067
1061
  return;
1068
- this.builderTree = this.updateNodeInTree(this.builderTree, nodeId, (node) => ({
1062
+ this._builderTree.set(this.updateNodeInTree(tree, nodeId, (node) => ({
1069
1063
  ...node,
1070
1064
  operator: node.operator === 'AND' ? 'OR' : 'AND',
1071
- }));
1065
+ })));
1072
1066
  this.emitChange();
1073
1067
  }
1074
1068
  addChildNode(parentId, type) {
1075
- if (!this.builderTree)
1069
+ const tree = this._builderTree();
1070
+ if (!tree)
1076
1071
  return;
1077
1072
  const newNode = type === 'group'
1078
1073
  ? { id: crypto.randomUUID(), type: 'group', operator: 'AND', children: [] }
1079
1074
  : { id: crypto.randomUUID(), type: 'action', actionId: '' };
1080
- this.builderTree = this.updateNodeInTree(this.builderTree, parentId, (node) => ({
1075
+ this._builderTree.set(this.updateNodeInTree(tree, parentId, (node) => ({
1081
1076
  ...node,
1082
1077
  children: [...(node.children || []), newNode],
1083
- }));
1078
+ })));
1084
1079
  this.emitChange();
1085
1080
  }
1086
1081
  removeNode(nodeId) {
1087
- if (!this.builderTree)
1082
+ const tree = this._builderTree();
1083
+ if (!tree)
1088
1084
  return;
1089
- if (this.builderTree.id === nodeId) {
1085
+ if (tree.id === nodeId) {
1090
1086
  this.clearLogic();
1091
1087
  return;
1092
1088
  }
1093
- this.builderTree = this.removeNodeFromTree(this.builderTree, nodeId);
1089
+ this._builderTree.set(this.removeNodeFromTree(tree, nodeId));
1094
1090
  this.emitChange();
1095
1091
  }
1096
1092
  updateActionId(nodeId, actionId) {
1097
- if (!this.builderTree)
1093
+ const tree = this._builderTree();
1094
+ if (!tree)
1098
1095
  return;
1099
- this.builderTree = this.updateNodeInTree(this.builderTree, nodeId, (node) => ({
1096
+ this._builderTree.set(this.updateNodeInTree(tree, nodeId, (node) => ({
1100
1097
  ...node,
1101
1098
  actionId,
1102
- }));
1099
+ })));
1103
1100
  this.emitChange();
1104
1101
  }
1105
1102
  emitChange() {
1106
- if (!this.builderTree) {
1103
+ const tree = this._builderTree();
1104
+ if (!tree) {
1107
1105
  this.logicChange.emit(null);
1108
1106
  return;
1109
1107
  }
1110
- this.logicChange.emit(toLogicNode(this.builderTree));
1108
+ this.logicChange.emit(toLogicNode(tree));
1111
1109
  }
1112
1110
  updateNodeInTree(node, targetId, updater) {
1113
1111
  if (node.id === targetId)
@@ -1135,7 +1133,7 @@ class LogicBuilderComponent {
1135
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: `
1136
1134
  <div class="logic-builder">
1137
1135
  <div class="flex justify-between items-center mb-3">
1138
- <h4 class="text-sm font-semibold">Permission Logic</h4>
1136
+ <h4 class="text-sm font-semibold m-0">Permission Logic</h4>
1139
1137
  @if (!builderLogic()) {
1140
1138
  <p-button
1141
1139
  label="Add Logic"
@@ -1154,13 +1152,13 @@ class LogicBuilderComponent {
1154
1152
  </div>
1155
1153
 
1156
1154
  @if (builderLogic()) {
1157
- <div class="border rounded p-3 bg-gray-50">
1158
- <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">
1159
1157
  Define permission requirements using AND/OR logic with actions
1160
1158
  </div>
1161
1159
 
1162
1160
  <!-- Root Node -->
1163
- <div class="logic-tree bg-white rounded border">
1161
+ <div class="text-sm bg-surface-0 rounded border border-surface">
1164
1162
  <ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: builderLogic()!, depth: 0 }"></ng-container>
1165
1163
  </div>
1166
1164
  </div>
@@ -1169,11 +1167,11 @@ class LogicBuilderComponent {
1169
1167
 
1170
1168
  <!-- Recursive Node Template -->
1171
1169
  <ng-template #nodeTemplate let-node let-depth="depth">
1172
- <div class="node" [style.background-color]="depth % 2 === 0 ? '#ffffff' : '#f9fafb'">
1173
- <div class="node-header">
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">
1174
1172
  <span class="node-type" [ngClass]="node.type">{{ node.type.toUpperCase() }}</span>
1175
1173
 
1176
- <div class="node-content">
1174
+ <div class="flex-1 flex items-center gap-2">
1177
1175
  @if (node.type === 'group') {
1178
1176
  <!-- Group: show operator toggle -->
1179
1177
  <span
@@ -1183,13 +1181,13 @@ class LogicBuilderComponent {
1183
1181
  title="Click to toggle">
1184
1182
  {{ node.operator }}
1185
1183
  </span>
1186
- <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>
1187
1185
  }
1188
1186
 
1189
1187
  @if (node.type === 'action') {
1190
1188
  <!-- Action: show action selector -->
1191
1189
  <select
1192
- class="flex-1"
1190
+ class="action-select flex-1 p-2 border border-surface rounded text-sm"
1193
1191
  [ngModel]="node.actionId"
1194
1192
  (ngModelChange)="updateActionId(node.id, $event)">
1195
1193
  <option [value]="null">Select Action... ({{ availableActions().length }} available)</option>
@@ -1200,7 +1198,7 @@ class LogicBuilderComponent {
1200
1198
  }
1201
1199
  </div>
1202
1200
 
1203
- <div class="node-actions">
1201
+ <div class="flex gap-1">
1204
1202
  <p-button
1205
1203
  icon="pi pi-trash"
1206
1204
  [text]="true"
@@ -1213,7 +1211,7 @@ class LogicBuilderComponent {
1213
1211
 
1214
1212
  <!-- Children for group nodes -->
1215
1213
  @if (node.type === 'group' && node.children && node.children.length > 0) {
1216
- <div class="node-children">
1214
+ <div class="ml-5 mt-2 pl-3 border-l-2 border-surface">
1217
1215
  @for (child of node.children; track child.id) {
1218
1216
  <ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: child, depth: depth + 1 }"></ng-container>
1219
1217
  }
@@ -1222,9 +1220,9 @@ class LogicBuilderComponent {
1222
1220
 
1223
1221
  <!-- Add child buttons for group nodes -->
1224
1222
  @if (node.type === 'group') {
1225
- <div class="add-child-section">
1226
- <div class="text-xs font-semibold text-gray-600 mb-2">Add Condition:</div>
1227
- <div class="add-child-buttons">
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">
1228
1226
  <p-button
1229
1227
  label="Group"
1230
1228
  icon="pi pi-sitemap"
@@ -1243,14 +1241,14 @@ class LogicBuilderComponent {
1243
1241
  }
1244
1242
  </div>
1245
1243
  </ng-template>
1246
- `, isInline: true, styles: [":host{display:block}.logic-tree{font-size:.875rem}.node{padding:.75rem;border-bottom:1px solid #e5e7eb}.node:last-child{border-bottom:none}.node-header{display:flex;align-items:center;gap:.75rem;margin-bottom:.5rem}.node-type{display:inline-flex;align-items:center;padding:.25rem .5rem;border-radius:.25rem;font-weight:600;font-size:.75rem;text-transform:uppercase}.node-type.group{background-color:#dbeafe;color:#1e40af}.node-type.action{background-color:#d1fae5;color:#065f46}.node-content{flex:1;display:flex;align-items:center;gap:.5rem}.node-actions{display:flex;gap:.25rem}.node-children{margin-left:1.5rem;margin-top:.5rem;padding-left:1rem;border-left:2px solid #e5e7eb}.operator-badge{display:inline-flex;align-items:center;padding:.125rem .5rem;border-radius:9999px;font-weight:600;font-size:.75rem;cursor:pointer;transition:all .2s}.operator-badge.and{background-color:#fef3c7;color:#92400e}.operator-badge.or{background-color:#e0e7ff;color:#3730a3}.operator-badge:hover{opacity:.8}select{padding:.375rem .75rem;border:1px solid #d1d5db;border-radius:.375rem;font-size:.875rem;background-color:#fff}select:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.add-child-section{margin-top:.75rem;padding:.75rem;background-color:#f9fafb;border-radius:.375rem;border:1px dashed #d1d5db}.add-child-buttons{display:flex;gap:.5rem;flex-wrap:wrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: AngularModule }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: 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", "showOnEllipsis", "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 });
1247
1245
  }
1248
1246
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LogicBuilderComponent, decorators: [{
1249
1247
  type: Component,
1250
- 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: `
1251
1249
  <div class="logic-builder">
1252
1250
  <div class="flex justify-between items-center mb-3">
1253
- <h4 class="text-sm font-semibold">Permission Logic</h4>
1251
+ <h4 class="text-sm font-semibold m-0">Permission Logic</h4>
1254
1252
  @if (!builderLogic()) {
1255
1253
  <p-button
1256
1254
  label="Add Logic"
@@ -1269,13 +1267,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1269
1267
  </div>
1270
1268
 
1271
1269
  @if (builderLogic()) {
1272
- <div class="border rounded p-3 bg-gray-50">
1273
- <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">
1274
1272
  Define permission requirements using AND/OR logic with actions
1275
1273
  </div>
1276
1274
 
1277
1275
  <!-- Root Node -->
1278
- <div class="logic-tree bg-white rounded border">
1276
+ <div class="text-sm bg-surface-0 rounded border border-surface">
1279
1277
  <ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: builderLogic()!, depth: 0 }"></ng-container>
1280
1278
  </div>
1281
1279
  </div>
@@ -1284,11 +1282,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1284
1282
 
1285
1283
  <!-- Recursive Node Template -->
1286
1284
  <ng-template #nodeTemplate let-node let-depth="depth">
1287
- <div class="node" [style.background-color]="depth % 2 === 0 ? '#ffffff' : '#f9fafb'">
1288
- <div class="node-header">
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">
1289
1287
  <span class="node-type" [ngClass]="node.type">{{ node.type.toUpperCase() }}</span>
1290
1288
 
1291
- <div class="node-content">
1289
+ <div class="flex-1 flex items-center gap-2">
1292
1290
  @if (node.type === 'group') {
1293
1291
  <!-- Group: show operator toggle -->
1294
1292
  <span
@@ -1298,13 +1296,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1298
1296
  title="Click to toggle">
1299
1297
  {{ node.operator }}
1300
1298
  </span>
1301
- <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>
1302
1300
  }
1303
1301
 
1304
1302
  @if (node.type === 'action') {
1305
1303
  <!-- Action: show action selector -->
1306
1304
  <select
1307
- class="flex-1"
1305
+ class="action-select flex-1 p-2 border border-surface rounded text-sm"
1308
1306
  [ngModel]="node.actionId"
1309
1307
  (ngModelChange)="updateActionId(node.id, $event)">
1310
1308
  <option [value]="null">Select Action... ({{ availableActions().length }} available)</option>
@@ -1315,7 +1313,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1315
1313
  }
1316
1314
  </div>
1317
1315
 
1318
- <div class="node-actions">
1316
+ <div class="flex gap-1">
1319
1317
  <p-button
1320
1318
  icon="pi pi-trash"
1321
1319
  [text]="true"
@@ -1328,7 +1326,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1328
1326
 
1329
1327
  <!-- Children for group nodes -->
1330
1328
  @if (node.type === 'group' && node.children && node.children.length > 0) {
1331
- <div class="node-children">
1329
+ <div class="ml-5 mt-2 pl-3 border-l-2 border-surface">
1332
1330
  @for (child of node.children; track child.id) {
1333
1331
  <ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: child, depth: depth + 1 }"></ng-container>
1334
1332
  }
@@ -1337,9 +1335,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1337
1335
 
1338
1336
  <!-- Add child buttons for group nodes -->
1339
1337
  @if (node.type === 'group') {
1340
- <div class="add-child-section">
1341
- <div class="text-xs font-semibold text-gray-600 mb-2">Add Condition:</div>
1342
- <div class="add-child-buttons">
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">
1343
1341
  <p-button
1344
1342
  label="Group"
1345
1343
  icon="pi pi-sitemap"
@@ -1358,8 +1356,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1358
1356
  }
1359
1357
  </div>
1360
1358
  </ng-template>
1361
- `, styles: [":host{display:block}.logic-tree{font-size:.875rem}.node{padding:.75rem;border-bottom:1px solid #e5e7eb}.node:last-child{border-bottom:none}.node-header{display:flex;align-items:center;gap:.75rem;margin-bottom:.5rem}.node-type{display:inline-flex;align-items:center;padding:.25rem .5rem;border-radius:.25rem;font-weight:600;font-size:.75rem;text-transform:uppercase}.node-type.group{background-color:#dbeafe;color:#1e40af}.node-type.action{background-color:#d1fae5;color:#065f46}.node-content{flex:1;display:flex;align-items:center;gap:.5rem}.node-actions{display:flex;gap:.25rem}.node-children{margin-left:1.5rem;margin-top:.5rem;padding-left:1rem;border-left:2px solid #e5e7eb}.operator-badge{display:inline-flex;align-items:center;padding:.125rem .5rem;border-radius:9999px;font-weight:600;font-size:.75rem;cursor:pointer;transition:all .2s}.operator-badge.and{background-color:#fef3c7;color:#92400e}.operator-badge.or{background-color:#e0e7ff;color:#3730a3}.operator-badge:hover{opacity:.8}select{padding:.375rem .75rem;border:1px solid #d1d5db;border-radius:.375rem;font-size:.875rem;background-color:#fff}select:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.add-child-section{margin-top:.75rem;padding:.75rem;background-color:#f9fafb;border-radius:.375rem;border:1px dashed #d1d5db}.add-child-buttons{display:flex;gap:.5rem;flex-wrap:wrap}\n"] }]
1362
- }], propDecorators: { logic: [{ type: i0.Input, args: [{ isSignal: true, alias: "logic", required: false }] }], actions: [{ type: i0.Input, args: [{ isSignal: true, alias: "actions", required: false }] }], logicChange: [{ type: i0.Output, args: ["logicChange"] }] } });
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"] }] } });
1363
1361
 
1364
1362
  /**
1365
1363
  * Tree Utility Functions
@@ -1556,7 +1554,10 @@ function convertActionToTreeNode(actions) {
1556
1554
  * ```
1557
1555
  */
1558
1556
  class RoleActionSelectorComponent {
1557
+ // Permission constants for template
1558
+ ROLE_ACTION_PERMISSIONS = ROLE_ACTION_PERMISSIONS;
1559
1559
  // Dependencies
1560
+ destroyRef = inject(DestroyRef);
1560
1561
  roleApi = inject(RoleApiService);
1561
1562
  actionApi = inject(ActionApiService);
1562
1563
  permissionApi = inject(PermissionApiService);
@@ -1637,6 +1638,10 @@ class RoleActionSelectorComponent {
1637
1638
  loadDataAbortController = null;
1638
1639
  constructor() {
1639
1640
  this.loadRoles();
1641
+ // Cleanup on destroy
1642
+ this.destroyRef.onDestroy(() => {
1643
+ this.loadDataAbortController?.abort();
1644
+ });
1640
1645
  // Effect: Load data when role selection changes
1641
1646
  effect(() => {
1642
1647
  const roleId = this.selectedRoleId();
@@ -1646,11 +1651,7 @@ class RoleActionSelectorComponent {
1646
1651
  this.loadDataAbortController = new AbortController();
1647
1652
  this.onRoleChange(this.loadDataAbortController.signal).catch((err) => {
1648
1653
  if (err.name !== 'AbortError') {
1649
- this.messageService.add({
1650
- severity: 'error',
1651
- summary: 'Error',
1652
- detail: 'Failed to load role permissions. Please refresh.',
1653
- });
1654
+ // Error toast handled by global interceptor
1654
1655
  this.loading.set(false);
1655
1656
  }
1656
1657
  });
@@ -1660,9 +1661,6 @@ class RoleActionSelectorComponent {
1660
1661
  }
1661
1662
  });
1662
1663
  }
1663
- ngOnDestroy() {
1664
- this.loadDataAbortController?.abort();
1665
- }
1666
1664
  /**
1667
1665
  * Load roles from API
1668
1666
  */
@@ -1674,11 +1672,7 @@ class RoleActionSelectorComponent {
1674
1672
  this.roles.set(response?.data ?? []);
1675
1673
  }
1676
1674
  catch {
1677
- this.messageService.add({
1678
- severity: 'error',
1679
- summary: 'Error',
1680
- detail: 'Failed to load roles',
1681
- });
1675
+ // Error toast handled by global interceptor
1682
1676
  }
1683
1677
  }
1684
1678
  /**
@@ -1836,13 +1830,8 @@ class RoleActionSelectorComponent {
1836
1830
  // Update baseline
1837
1831
  this._initialSelection.set({ ...this.selectionMap() });
1838
1832
  }
1839
- catch (err) {
1840
- const error = err;
1841
- this.messageService.add({
1842
- severity: 'error',
1843
- summary: 'Error',
1844
- detail: error?.error?.message || 'Failed to update role permissions',
1845
- });
1833
+ catch {
1834
+ // Error toast handled by global interceptor
1846
1835
  }
1847
1836
  finally {
1848
1837
  this.saving.set(false);
@@ -1862,11 +1851,12 @@ class RoleActionSelectorComponent {
1862
1851
  <div class="role-action-selector">
1863
1852
  <!-- Role Selector -->
1864
1853
  <div class="mb-4">
1865
- <div class="grid">
1866
- <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>
1867
1856
  <label class="block font-semibold mb-2">Select Role</label>
1868
1857
  <p-select
1869
- [(ngModel)]="selectedRoleId"
1858
+ [ngModel]="selectedRoleId()"
1859
+ (ngModelChange)="selectedRoleId.set($event)"
1870
1860
  [options]="roles()"
1871
1861
  optionLabel="name"
1872
1862
  optionValue="id"
@@ -1883,24 +1873,21 @@ class RoleActionSelectorComponent {
1883
1873
  <!-- Loading State -->
1884
1874
  @if (loading()) {
1885
1875
  <div
1886
- 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"
1887
1877
  >
1888
- <i
1889
- class="pi pi-spin pi-spinner text-primary"
1890
- style="font-size: 3rem"
1891
- ></i>
1878
+ <i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
1892
1879
  </div>
1893
1880
  }
1894
1881
 
1895
1882
  <!-- Action List -->
1896
1883
  @if (!loading() && actions().length > 0) {
1897
- <div class="surface-card p-4 border-round shadow-1">
1884
+ <div class="surface-card p-4 rounded-border shadow-sm">
1898
1885
  <div
1899
- 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"
1900
1887
  >
1901
1888
  <div>
1902
1889
  <h5 class="m-0 mb-1">Action Permissions</h5>
1903
- <p class="text-sm text-color-secondary m-0">
1890
+ <p class="text-sm text-muted-color m-0">
1904
1891
  {{ actions().length }} actions available
1905
1892
  </p>
1906
1893
  </div>
@@ -1922,6 +1909,7 @@ class RoleActionSelectorComponent {
1922
1909
  (onClick)="deselectAll()"
1923
1910
  />
1924
1911
  <p-button
1912
+ *hasPermission="ROLE_ACTION_PERMISSIONS.ASSIGN"
1925
1913
  label="Save Changes"
1926
1914
  icon="pi pi-save"
1927
1915
  [disabled]="!canSave()"
@@ -1935,14 +1923,12 @@ class RoleActionSelectorComponent {
1935
1923
 
1936
1924
  <!-- Validation Warning -->
1937
1925
  @if (invalidActionsCount() > 0) {
1938
- <div class="bg-orange-50 border-l-4 border-orange-500 p-4 mb-4">
1939
- <div class="flex items-center">
1940
- <i
1941
- class="pi pi-exclamation-triangle text-orange-500 mr-2"
1942
- ></i>
1943
- <div>
1944
- <strong class="text-orange-800">Validation Warning:</strong>
1945
- <p class="text-sm text-orange-700 mt-1">
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">
1946
1932
  {{ invalidActionsCount() }} selected
1947
1933
  action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
1948
1934
  unmet prerequisites. Fix before saving or use auto-fix on
@@ -1954,33 +1940,33 @@ class RoleActionSelectorComponent {
1954
1940
  }
1955
1941
 
1956
1942
  <!-- Action Tree Table -->
1957
- <p-treeTable
1958
- [value]="treeNodes()"
1959
- [scrollable]="true"
1960
- scrollHeight="flex"
1961
- dataKey="id"
1962
- styleClass="p-treetable-sm"
1963
- >
1964
- <ng-template pTemplate="header">
1965
- <tr>
1966
- <th style="width: 3rem">
1967
- <p-checkbox
1968
- [ngModel]="allSelected()"
1969
- [binary]="true"
1970
- (ngModelChange)="toggleAll()"
1971
- pTooltip="Select/Deselect All"
1972
- tooltipPosition="top"
1973
- />
1974
- </th>
1975
- <th>Name</th>
1976
- <th>Code</th>
1977
- <th>Type</th>
1978
- <th>Requirements</th>
1979
- </tr>
1980
- </ng-template>
1981
- <ng-template pTemplate="body" let-rowNode let-rowData="rowData">
1982
- <tr [class.bg-red-50]="hasUnmetPrerequisites(rowData)">
1983
- <td style="width: 3rem">
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">
1984
1970
  <p-checkbox
1985
1971
  [ngModel]="selectionMap()[rowData.id]"
1986
1972
  [binary]="true"
@@ -1991,7 +1977,7 @@ class RoleActionSelectorComponent {
1991
1977
  </td>
1992
1978
  <td>
1993
1979
  <p-treeTableToggler [rowNode]="rowNode" />
1994
- <span class="inline-flex align-items-center gap-2">
1980
+ <span class="inline-flex items-center gap-2">
1995
1981
  {{ rowData.name }}
1996
1982
  @if (hasUnmetPrerequisites(rowData)) {
1997
1983
  <i
@@ -2002,7 +1988,7 @@ class RoleActionSelectorComponent {
2002
1988
  }
2003
1989
  </span>
2004
1990
  </td>
2005
- <td>{{ rowData.code || '-' }}</td>
1991
+ <td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
2006
1992
  <td>
2007
1993
  <p-tag
2008
1994
  [value]="rowData.actionType"
@@ -2011,42 +1997,41 @@ class RoleActionSelectorComponent {
2011
1997
  "
2012
1998
  />
2013
1999
  </td>
2014
- <td>
2000
+ <td class="hidden lg:table-cell">
2015
2001
  @if (rowData.permissionLogic) {
2016
- <span class="text-sm text-muted">Has prerequisites</span>
2002
+ <span class="text-sm text-muted-color">Has prerequisites</span>
2017
2003
  } @else {
2018
- <span class="text-muted">-</span>
2004
+ <span class="text-muted-color">-</span>
2019
2005
  }
2020
2006
  </td>
2021
2007
  </tr>
2022
2008
  </ng-template>
2023
- <ng-template pTemplate="emptymessage">
2009
+ <ng-template #emptymessage>
2024
2010
  <tr>
2025
- <td colspan="5" class="text-center p-4">
2011
+ <td colspan="5" class="text-center p-4 text-muted-color">
2026
2012
  @if (loading()) {
2027
2013
  <i class="pi pi-spin pi-spinner"></i> Loading actions...
2028
2014
  } @else { No actions available. }
2029
2015
  </td>
2030
2016
  </tr>
2031
2017
  </ng-template>
2032
- </p-treeTable>
2018
+ </p-treeTable>
2019
+ </div>
2033
2020
  </div>
2034
2021
 
2035
2022
  <!-- Change Summary -->
2036
2023
  @if (hasChanges()) {
2037
- <div class="surface-border border-1 border-round p-3 mt-4">
2038
- <div class="flex align-items-center gap-2 mb-3">
2039
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
2040
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
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>
2041
2028
  </div>
2042
- <div class="flex flex-col md:flex-row gap-4">
2029
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
2043
2030
  @if (pendingAdd().length > 0) {
2044
- <div class="w-full md:w-1/2">
2045
- <div class="flex align-items-center gap-2 mb-2">
2031
+ <div>
2032
+ <div class="flex items-center gap-2 mb-2">
2046
2033
  <i class="pi pi-plus-circle text-green-500"></i>
2047
- <strong class="text-sm"
2048
- >To Add ({{ pendingAdd().length }})</strong
2049
- >
2034
+ <strong class="text-sm">To Add ({{ pendingAdd().length }})</strong>
2050
2035
  </div>
2051
2036
  <ul class="list-none p-0 m-0 pl-4">
2052
2037
  @for (action of pendingAdd(); track action.id) {
@@ -2056,12 +2041,10 @@ class RoleActionSelectorComponent {
2056
2041
  </div>
2057
2042
  }
2058
2043
  @if (pendingRemove().length > 0) {
2059
- <div class="w-full md:w-1/2">
2060
- <div class="flex align-items-center gap-2 mb-2">
2044
+ <div>
2045
+ <div class="flex items-center gap-2 mb-2">
2061
2046
  <i class="pi pi-minus-circle text-red-500"></i>
2062
- <strong class="text-sm"
2063
- >To Remove ({{ pendingRemove().length }})</strong
2064
- >
2047
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
2065
2048
  </div>
2066
2049
  <ul class="list-none p-0 m-0 pl-4">
2067
2050
  @for (action of pendingRemove(); track action.id) {
@@ -2076,31 +2059,29 @@ class RoleActionSelectorComponent {
2076
2059
  }
2077
2060
 
2078
2061
  @if (!loading() && actions().length === 0) {
2079
- <div class="surface-card p-5 border-round shadow-1 text-center">
2080
- <i
2081
- class="pi pi-info-circle text-color-secondary mb-3"
2082
- style="font-size: 3rem; display: block;"
2083
- ></i>
2084
- <p class="text-color-secondary m-0">
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">
2085
2065
  No actions available for this role.
2086
2066
  </p>
2087
2067
  </div>
2088
2068
  }
2089
2069
  }
2090
2070
  </div>
2091
- `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "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", "showOnEllipsis", "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 });
2092
2072
  }
2093
2073
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RoleActionSelectorComponent, decorators: [{
2094
2074
  type: Component,
2095
- 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: `
2096
2076
  <div class="role-action-selector">
2097
2077
  <!-- Role Selector -->
2098
2078
  <div class="mb-4">
2099
- <div class="grid">
2100
- <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>
2101
2081
  <label class="block font-semibold mb-2">Select Role</label>
2102
2082
  <p-select
2103
- [(ngModel)]="selectedRoleId"
2083
+ [ngModel]="selectedRoleId()"
2084
+ (ngModelChange)="selectedRoleId.set($event)"
2104
2085
  [options]="roles()"
2105
2086
  optionLabel="name"
2106
2087
  optionValue="id"
@@ -2117,24 +2098,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2117
2098
  <!-- Loading State -->
2118
2099
  @if (loading()) {
2119
2100
  <div
2120
- 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"
2121
2102
  >
2122
- <i
2123
- class="pi pi-spin pi-spinner text-primary"
2124
- style="font-size: 3rem"
2125
- ></i>
2103
+ <i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
2126
2104
  </div>
2127
2105
  }
2128
2106
 
2129
2107
  <!-- Action List -->
2130
2108
  @if (!loading() && actions().length > 0) {
2131
- <div class="surface-card p-4 border-round shadow-1">
2109
+ <div class="surface-card p-4 rounded-border shadow-sm">
2132
2110
  <div
2133
- 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"
2134
2112
  >
2135
2113
  <div>
2136
2114
  <h5 class="m-0 mb-1">Action Permissions</h5>
2137
- <p class="text-sm text-color-secondary m-0">
2115
+ <p class="text-sm text-muted-color m-0">
2138
2116
  {{ actions().length }} actions available
2139
2117
  </p>
2140
2118
  </div>
@@ -2156,6 +2134,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2156
2134
  (onClick)="deselectAll()"
2157
2135
  />
2158
2136
  <p-button
2137
+ *hasPermission="ROLE_ACTION_PERMISSIONS.ASSIGN"
2159
2138
  label="Save Changes"
2160
2139
  icon="pi pi-save"
2161
2140
  [disabled]="!canSave()"
@@ -2169,14 +2148,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2169
2148
 
2170
2149
  <!-- Validation Warning -->
2171
2150
  @if (invalidActionsCount() > 0) {
2172
- <div class="bg-orange-50 border-l-4 border-orange-500 p-4 mb-4">
2173
- <div class="flex items-center">
2174
- <i
2175
- class="pi pi-exclamation-triangle text-orange-500 mr-2"
2176
- ></i>
2177
- <div>
2178
- <strong class="text-orange-800">Validation Warning:</strong>
2179
- <p class="text-sm text-orange-700 mt-1">
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">
2180
2157
  {{ invalidActionsCount() }} selected
2181
2158
  action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
2182
2159
  unmet prerequisites. Fix before saving or use auto-fix on
@@ -2188,33 +2165,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2188
2165
  }
2189
2166
 
2190
2167
  <!-- Action Tree Table -->
2191
- <p-treeTable
2192
- [value]="treeNodes()"
2193
- [scrollable]="true"
2194
- scrollHeight="flex"
2195
- dataKey="id"
2196
- styleClass="p-treetable-sm"
2197
- >
2198
- <ng-template pTemplate="header">
2199
- <tr>
2200
- <th style="width: 3rem">
2201
- <p-checkbox
2202
- [ngModel]="allSelected()"
2203
- [binary]="true"
2204
- (ngModelChange)="toggleAll()"
2205
- pTooltip="Select/Deselect All"
2206
- tooltipPosition="top"
2207
- />
2208
- </th>
2209
- <th>Name</th>
2210
- <th>Code</th>
2211
- <th>Type</th>
2212
- <th>Requirements</th>
2213
- </tr>
2214
- </ng-template>
2215
- <ng-template pTemplate="body" let-rowNode let-rowData="rowData">
2216
- <tr [class.bg-red-50]="hasUnmetPrerequisites(rowData)">
2217
- <td style="width: 3rem">
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">
2218
2195
  <p-checkbox
2219
2196
  [ngModel]="selectionMap()[rowData.id]"
2220
2197
  [binary]="true"
@@ -2225,7 +2202,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2225
2202
  </td>
2226
2203
  <td>
2227
2204
  <p-treeTableToggler [rowNode]="rowNode" />
2228
- <span class="inline-flex align-items-center gap-2">
2205
+ <span class="inline-flex items-center gap-2">
2229
2206
  {{ rowData.name }}
2230
2207
  @if (hasUnmetPrerequisites(rowData)) {
2231
2208
  <i
@@ -2236,7 +2213,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2236
2213
  }
2237
2214
  </span>
2238
2215
  </td>
2239
- <td>{{ rowData.code || '-' }}</td>
2216
+ <td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
2240
2217
  <td>
2241
2218
  <p-tag
2242
2219
  [value]="rowData.actionType"
@@ -2245,42 +2222,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2245
2222
  "
2246
2223
  />
2247
2224
  </td>
2248
- <td>
2225
+ <td class="hidden lg:table-cell">
2249
2226
  @if (rowData.permissionLogic) {
2250
- <span class="text-sm text-muted">Has prerequisites</span>
2227
+ <span class="text-sm text-muted-color">Has prerequisites</span>
2251
2228
  } @else {
2252
- <span class="text-muted">-</span>
2229
+ <span class="text-muted-color">-</span>
2253
2230
  }
2254
2231
  </td>
2255
2232
  </tr>
2256
2233
  </ng-template>
2257
- <ng-template pTemplate="emptymessage">
2234
+ <ng-template #emptymessage>
2258
2235
  <tr>
2259
- <td colspan="5" class="text-center p-4">
2236
+ <td colspan="5" class="text-center p-4 text-muted-color">
2260
2237
  @if (loading()) {
2261
2238
  <i class="pi pi-spin pi-spinner"></i> Loading actions...
2262
2239
  } @else { No actions available. }
2263
2240
  </td>
2264
2241
  </tr>
2265
2242
  </ng-template>
2266
- </p-treeTable>
2243
+ </p-treeTable>
2244
+ </div>
2267
2245
  </div>
2268
2246
 
2269
2247
  <!-- Change Summary -->
2270
2248
  @if (hasChanges()) {
2271
- <div class="surface-border border-1 border-round p-3 mt-4">
2272
- <div class="flex align-items-center gap-2 mb-3">
2273
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
2274
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
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>
2275
2253
  </div>
2276
- <div class="flex flex-col md:flex-row gap-4">
2254
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
2277
2255
  @if (pendingAdd().length > 0) {
2278
- <div class="w-full md:w-1/2">
2279
- <div class="flex align-items-center gap-2 mb-2">
2256
+ <div>
2257
+ <div class="flex items-center gap-2 mb-2">
2280
2258
  <i class="pi pi-plus-circle text-green-500"></i>
2281
- <strong class="text-sm"
2282
- >To Add ({{ pendingAdd().length }})</strong
2283
- >
2259
+ <strong class="text-sm">To Add ({{ pendingAdd().length }})</strong>
2284
2260
  </div>
2285
2261
  <ul class="list-none p-0 m-0 pl-4">
2286
2262
  @for (action of pendingAdd(); track action.id) {
@@ -2290,12 +2266,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2290
2266
  </div>
2291
2267
  }
2292
2268
  @if (pendingRemove().length > 0) {
2293
- <div class="w-full md:w-1/2">
2294
- <div class="flex align-items-center gap-2 mb-2">
2269
+ <div>
2270
+ <div class="flex items-center gap-2 mb-2">
2295
2271
  <i class="pi pi-minus-circle text-red-500"></i>
2296
- <strong class="text-sm"
2297
- >To Remove ({{ pendingRemove().length }})</strong
2298
- >
2272
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
2299
2273
  </div>
2300
2274
  <ul class="list-none p-0 m-0 pl-4">
2301
2275
  @for (action of pendingRemove(); track action.id) {
@@ -2310,19 +2284,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2310
2284
  }
2311
2285
 
2312
2286
  @if (!loading() && actions().length === 0) {
2313
- <div class="surface-card p-5 border-round shadow-1 text-center">
2314
- <i
2315
- class="pi pi-info-circle text-color-secondary mb-3"
2316
- style="font-size: 3rem; display: block;"
2317
- ></i>
2318
- <p class="text-color-secondary m-0">
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">
2319
2290
  No actions available for this role.
2320
2291
  </p>
2321
2292
  </div>
2322
2293
  }
2323
2294
  }
2324
2295
  </div>
2325
- `, 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"] }]
2326
2297
  }], ctorParameters: () => [] });
2327
2298
 
2328
2299
  /**
@@ -2360,6 +2331,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2360
2331
  * ```
2361
2332
  */
2362
2333
  class CompanyActionSelectorComponent {
2334
+ // Permission constants for template
2335
+ COMPANY_ACTION_PERMISSIONS = COMPANY_ACTION_PERMISSIONS;
2363
2336
  // Dependencies
2364
2337
  companyApiProvider = inject(COMPANY_API_PROVIDER);
2365
2338
  actionApi = inject(ActionApiService);
@@ -2367,6 +2340,7 @@ class CompanyActionSelectorComponent {
2367
2340
  messageService = inject(MessageService);
2368
2341
  confirmationService = inject(ConfirmationService);
2369
2342
  permissionLogic = inject(ActionPermissionLogicService);
2343
+ destroyRef = inject(DestroyRef);
2370
2344
  // State - Company Selection
2371
2345
  selectedCompanyId = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedCompanyId" }] : []));
2372
2346
  companies = signal([], ...(ngDevMode ? [{ debugName: "companies" }] : []));
@@ -2441,6 +2415,10 @@ class CompanyActionSelectorComponent {
2441
2415
  // AbortController for data loading
2442
2416
  loadDataAbortController = null;
2443
2417
  constructor() {
2418
+ // Cleanup on destroy
2419
+ this.destroyRef.onDestroy(() => {
2420
+ this.loadDataAbortController?.abort();
2421
+ });
2444
2422
  this.loadCompanies();
2445
2423
  // Effect: Load data when company selection changes
2446
2424
  effect(() => {
@@ -2451,11 +2429,7 @@ class CompanyActionSelectorComponent {
2451
2429
  this.loadDataAbortController = new AbortController();
2452
2430
  this.loadData(this.loadDataAbortController.signal).catch((err) => {
2453
2431
  if (err.name !== 'AbortError') {
2454
- this.messageService.add({
2455
- severity: 'error',
2456
- summary: 'Error',
2457
- detail: 'Failed to load company actions. Please refresh.',
2458
- });
2432
+ // Error toast handled by global interceptor
2459
2433
  this.loading.set(false);
2460
2434
  }
2461
2435
  });
@@ -2465,9 +2439,6 @@ class CompanyActionSelectorComponent {
2465
2439
  }
2466
2440
  });
2467
2441
  }
2468
- ngOnDestroy() {
2469
- this.loadDataAbortController?.abort();
2470
- }
2471
2442
  /**
2472
2443
  * Load companies from API
2473
2444
  */
@@ -2480,11 +2451,7 @@ class CompanyActionSelectorComponent {
2480
2451
  this.companies.set(response?.data ?? []);
2481
2452
  }
2482
2453
  catch {
2483
- this.messageService.add({
2484
- severity: 'error',
2485
- summary: 'Error',
2486
- detail: 'Failed to load companies',
2487
- });
2454
+ // Error toast handled by global interceptor
2488
2455
  }
2489
2456
  }
2490
2457
  /**
@@ -2649,13 +2616,8 @@ class CompanyActionSelectorComponent {
2649
2616
  // Update baseline
2650
2617
  this._initialSelection.set({ ...this.selectionMap() });
2651
2618
  }
2652
- catch (err) {
2653
- const error = err;
2654
- this.messageService.add({
2655
- severity: 'error',
2656
- summary: 'Error',
2657
- detail: error?.error?.message || 'Failed to update company action whitelist',
2658
- });
2619
+ catch {
2620
+ // Error toast handled by global interceptor
2659
2621
  }
2660
2622
  finally {
2661
2623
  this.saving.set(false);
@@ -2722,11 +2684,12 @@ class CompanyActionSelectorComponent {
2722
2684
  <div class="company-action-selector">
2723
2685
  <!-- Company Selector -->
2724
2686
  <div class="mb-4">
2725
- <div class="grid">
2726
- <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>
2727
2689
  <label class="block font-semibold mb-2">Select Company</label>
2728
2690
  <p-select
2729
- [(ngModel)]="selectedCompanyId"
2691
+ [ngModel]="selectedCompanyId()"
2692
+ (ngModelChange)="selectedCompanyId.set($event)"
2730
2693
  [options]="companies()"
2731
2694
  optionLabel="name"
2732
2695
  optionValue="id"
@@ -2743,24 +2706,21 @@ class CompanyActionSelectorComponent {
2743
2706
  <!-- Loading State -->
2744
2707
  @if (loading()) {
2745
2708
  <div
2746
- 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"
2747
2710
  >
2748
- <i
2749
- class="pi pi-spin pi-spinner text-primary"
2750
- style="font-size: 3rem"
2751
- ></i>
2711
+ <i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
2752
2712
  </div>
2753
2713
  }
2754
2714
 
2755
2715
  <!-- Action List -->
2756
2716
  @if (!loading() && actions().length > 0) {
2757
- <div class="surface-card p-4 border-round shadow-1">
2717
+ <div class="surface-card p-4 rounded-border shadow-sm">
2758
2718
  <div
2759
- 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"
2760
2720
  >
2761
2721
  <div>
2762
2722
  <h5 class="m-0 mb-1">Action Whitelist</h5>
2763
- <p class="text-sm text-color-secondary m-0">
2723
+ <p class="text-sm text-muted-color m-0">
2764
2724
  {{ actions().length }} actions available
2765
2725
  </p>
2766
2726
  </div>
@@ -2782,6 +2742,7 @@ class CompanyActionSelectorComponent {
2782
2742
  (onClick)="deselectAll()"
2783
2743
  />
2784
2744
  <p-button
2745
+ *hasPermission="COMPANY_ACTION_PERMISSIONS.ASSIGN"
2785
2746
  label="Save Changes"
2786
2747
  icon="pi pi-save"
2787
2748
  [disabled]="!canSave()"
@@ -2795,14 +2756,12 @@ class CompanyActionSelectorComponent {
2795
2756
 
2796
2757
  <!-- Validation Warning -->
2797
2758
  @if (invalidActionsCount() > 0) {
2798
- <div class="bg-orange-50 border-l-4 border-orange-500 p-4 mb-4">
2799
- <div class="flex items-center">
2800
- <i
2801
- class="pi pi-exclamation-triangle text-orange-500 mr-2"
2802
- ></i>
2803
- <div>
2804
- <strong class="text-orange-800">Validation Warning:</strong>
2805
- <p class="text-sm text-orange-700 mt-1">
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">
2806
2765
  {{ invalidActionsCount() }} selected
2807
2766
  action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
2808
2767
  unmet prerequisites. Fix before saving or use auto-fix on
@@ -2814,33 +2773,33 @@ class CompanyActionSelectorComponent {
2814
2773
  }
2815
2774
 
2816
2775
  <!-- Action Tree Table -->
2817
- <p-treeTable
2818
- [value]="treeNodes()"
2819
- [scrollable]="true"
2820
- scrollHeight="flex"
2821
- dataKey="id"
2822
- styleClass="p-treetable-sm"
2823
- >
2824
- <ng-template pTemplate="header">
2825
- <tr>
2826
- <th style="width: 3rem">
2827
- <p-checkbox
2828
- [ngModel]="allSelected()"
2829
- [binary]="true"
2830
- (ngModelChange)="toggleAll()"
2831
- pTooltip="Select/Deselect All"
2832
- tooltipPosition="top"
2833
- />
2834
- </th>
2835
- <th>Name</th>
2836
- <th>Code</th>
2837
- <th>Type</th>
2838
- <th>Description</th>
2839
- </tr>
2840
- </ng-template>
2841
- <ng-template pTemplate="body" let-rowNode let-rowData="rowData">
2842
- <tr [class.bg-red-50]="hasUnmetPrerequisites(rowData)">
2843
- <td style="width: 3rem">
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">
2844
2803
  <p-checkbox
2845
2804
  [ngModel]="selectionMap()[rowData.id]"
2846
2805
  [binary]="true"
@@ -2851,7 +2810,7 @@ class CompanyActionSelectorComponent {
2851
2810
  </td>
2852
2811
  <td>
2853
2812
  <p-treeTableToggler [rowNode]="rowNode" />
2854
- <span class="inline-flex align-items-center gap-2">
2813
+ <span class="inline-flex items-center gap-2">
2855
2814
  {{ rowData.name }}
2856
2815
  @if (hasUnmetPrerequisites(rowData)) {
2857
2816
  <i
@@ -2862,7 +2821,7 @@ class CompanyActionSelectorComponent {
2862
2821
  }
2863
2822
  </span>
2864
2823
  </td>
2865
- <td>{{ rowData.code || '-' }}</td>
2824
+ <td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
2866
2825
  <td>
2867
2826
  <p-tag
2868
2827
  [value]="rowData.actionType"
@@ -2871,36 +2830,35 @@ class CompanyActionSelectorComponent {
2871
2830
  "
2872
2831
  />
2873
2832
  </td>
2874
- <td>{{ rowData.description || '-' }}</td>
2833
+ <td class="hidden lg:table-cell">{{ rowData.description || '-' }}</td>
2875
2834
  </tr>
2876
2835
  </ng-template>
2877
- <ng-template pTemplate="emptymessage">
2836
+ <ng-template #emptymessage>
2878
2837
  <tr>
2879
- <td colspan="5" class="text-center p-4">
2838
+ <td colspan="5" class="text-center p-4 text-muted-color">
2880
2839
  @if (loading()) {
2881
2840
  <i class="pi pi-spin pi-spinner"></i> Loading actions...
2882
2841
  } @else { No actions available. }
2883
2842
  </td>
2884
2843
  </tr>
2885
2844
  </ng-template>
2886
- </p-treeTable>
2845
+ </p-treeTable>
2846
+ </div>
2887
2847
  </div>
2888
2848
 
2889
2849
  <!-- Change Summary -->
2890
2850
  @if (hasChanges()) {
2891
- <div class="surface-border border-1 border-round p-3 mt-4">
2892
- <div class="flex align-items-center gap-2 mb-3">
2893
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
2894
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
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>
2895
2855
  </div>
2896
- <div class="flex flex-col md:flex-row gap-4">
2856
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
2897
2857
  @if (pendingAdd().length > 0) {
2898
- <div class="w-full md:w-1/2">
2858
+ <div>
2899
2859
  <div class="flex items-center gap-2 mb-2">
2900
2860
  <i class="pi pi-plus-circle text-green-500"></i>
2901
- <strong class="text-sm">
2902
- To Whitelist ({{ pendingAdd().length }})
2903
- </strong>
2861
+ <strong class="text-sm">To Whitelist ({{ pendingAdd().length }})</strong>
2904
2862
  </div>
2905
2863
  <ul class="list-none p-0 m-0 pl-4">
2906
2864
  @for (action of pendingAdd(); track action.id) {
@@ -2909,14 +2867,11 @@ class CompanyActionSelectorComponent {
2909
2867
  </ul>
2910
2868
  </div>
2911
2869
  }
2912
-
2913
2870
  @if (pendingRemove().length > 0) {
2914
- <div class="w-full md:w-1/2">
2871
+ <div>
2915
2872
  <div class="flex items-center gap-2 mb-2">
2916
2873
  <i class="pi pi-minus-circle text-red-500"></i>
2917
- <strong class="text-sm">
2918
- To Remove ({{ pendingRemove().length }})
2919
- </strong>
2874
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
2920
2875
  </div>
2921
2876
  <ul class="list-none p-0 m-0 pl-4">
2922
2877
  @for (action of pendingRemove(); track action.id) {
@@ -2931,31 +2886,29 @@ class CompanyActionSelectorComponent {
2931
2886
  }
2932
2887
 
2933
2888
  @if (!loading() && actions().length === 0) {
2934
- <div class="surface-card p-5 border-round shadow-1 text-center">
2935
- <i
2936
- class="pi pi-info-circle text-color-secondary mb-3"
2937
- style="font-size: 3rem; display: block;"
2938
- ></i>
2939
- <p class="text-color-secondary m-0">
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">
2940
2892
  No actions available for this company.
2941
2893
  </p>
2942
2894
  </div>
2943
2895
  }
2944
2896
  }
2945
2897
  </div>
2946
- `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "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", "showOnEllipsis", "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 });
2947
2899
  }
2948
2900
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CompanyActionSelectorComponent, decorators: [{
2949
2901
  type: Component,
2950
- 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: `
2951
2903
  <div class="company-action-selector">
2952
2904
  <!-- Company Selector -->
2953
2905
  <div class="mb-4">
2954
- <div class="grid">
2955
- <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>
2956
2908
  <label class="block font-semibold mb-2">Select Company</label>
2957
2909
  <p-select
2958
- [(ngModel)]="selectedCompanyId"
2910
+ [ngModel]="selectedCompanyId()"
2911
+ (ngModelChange)="selectedCompanyId.set($event)"
2959
2912
  [options]="companies()"
2960
2913
  optionLabel="name"
2961
2914
  optionValue="id"
@@ -2972,24 +2925,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2972
2925
  <!-- Loading State -->
2973
2926
  @if (loading()) {
2974
2927
  <div
2975
- 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"
2976
2929
  >
2977
- <i
2978
- class="pi pi-spin pi-spinner text-primary"
2979
- style="font-size: 3rem"
2980
- ></i>
2930
+ <i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
2981
2931
  </div>
2982
2932
  }
2983
2933
 
2984
2934
  <!-- Action List -->
2985
2935
  @if (!loading() && actions().length > 0) {
2986
- <div class="surface-card p-4 border-round shadow-1">
2936
+ <div class="surface-card p-4 rounded-border shadow-sm">
2987
2937
  <div
2988
- 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"
2989
2939
  >
2990
2940
  <div>
2991
2941
  <h5 class="m-0 mb-1">Action Whitelist</h5>
2992
- <p class="text-sm text-color-secondary m-0">
2942
+ <p class="text-sm text-muted-color m-0">
2993
2943
  {{ actions().length }} actions available
2994
2944
  </p>
2995
2945
  </div>
@@ -3011,6 +2961,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3011
2961
  (onClick)="deselectAll()"
3012
2962
  />
3013
2963
  <p-button
2964
+ *hasPermission="COMPANY_ACTION_PERMISSIONS.ASSIGN"
3014
2965
  label="Save Changes"
3015
2966
  icon="pi pi-save"
3016
2967
  [disabled]="!canSave()"
@@ -3024,14 +2975,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3024
2975
 
3025
2976
  <!-- Validation Warning -->
3026
2977
  @if (invalidActionsCount() > 0) {
3027
- <div class="bg-orange-50 border-l-4 border-orange-500 p-4 mb-4">
3028
- <div class="flex items-center">
3029
- <i
3030
- class="pi pi-exclamation-triangle text-orange-500 mr-2"
3031
- ></i>
3032
- <div>
3033
- <strong class="text-orange-800">Validation Warning:</strong>
3034
- <p class="text-sm text-orange-700 mt-1">
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">
3035
2984
  {{ invalidActionsCount() }} selected
3036
2985
  action{{ invalidActionsCount() > 1 ? 's have' : ' has' }}
3037
2986
  unmet prerequisites. Fix before saving or use auto-fix on
@@ -3043,33 +2992,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3043
2992
  }
3044
2993
 
3045
2994
  <!-- Action Tree Table -->
3046
- <p-treeTable
3047
- [value]="treeNodes()"
3048
- [scrollable]="true"
3049
- scrollHeight="flex"
3050
- dataKey="id"
3051
- styleClass="p-treetable-sm"
3052
- >
3053
- <ng-template pTemplate="header">
3054
- <tr>
3055
- <th style="width: 3rem">
3056
- <p-checkbox
3057
- [ngModel]="allSelected()"
3058
- [binary]="true"
3059
- (ngModelChange)="toggleAll()"
3060
- pTooltip="Select/Deselect All"
3061
- tooltipPosition="top"
3062
- />
3063
- </th>
3064
- <th>Name</th>
3065
- <th>Code</th>
3066
- <th>Type</th>
3067
- <th>Description</th>
3068
- </tr>
3069
- </ng-template>
3070
- <ng-template pTemplate="body" let-rowNode let-rowData="rowData">
3071
- <tr [class.bg-red-50]="hasUnmetPrerequisites(rowData)">
3072
- <td style="width: 3rem">
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">
3073
3022
  <p-checkbox
3074
3023
  [ngModel]="selectionMap()[rowData.id]"
3075
3024
  [binary]="true"
@@ -3080,7 +3029,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3080
3029
  </td>
3081
3030
  <td>
3082
3031
  <p-treeTableToggler [rowNode]="rowNode" />
3083
- <span class="inline-flex align-items-center gap-2">
3032
+ <span class="inline-flex items-center gap-2">
3084
3033
  {{ rowData.name }}
3085
3034
  @if (hasUnmetPrerequisites(rowData)) {
3086
3035
  <i
@@ -3091,7 +3040,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3091
3040
  }
3092
3041
  </span>
3093
3042
  </td>
3094
- <td>{{ rowData.code || '-' }}</td>
3043
+ <td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
3095
3044
  <td>
3096
3045
  <p-tag
3097
3046
  [value]="rowData.actionType"
@@ -3100,36 +3049,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3100
3049
  "
3101
3050
  />
3102
3051
  </td>
3103
- <td>{{ rowData.description || '-' }}</td>
3052
+ <td class="hidden lg:table-cell">{{ rowData.description || '-' }}</td>
3104
3053
  </tr>
3105
3054
  </ng-template>
3106
- <ng-template pTemplate="emptymessage">
3055
+ <ng-template #emptymessage>
3107
3056
  <tr>
3108
- <td colspan="5" class="text-center p-4">
3057
+ <td colspan="5" class="text-center p-4 text-muted-color">
3109
3058
  @if (loading()) {
3110
3059
  <i class="pi pi-spin pi-spinner"></i> Loading actions...
3111
3060
  } @else { No actions available. }
3112
3061
  </td>
3113
3062
  </tr>
3114
3063
  </ng-template>
3115
- </p-treeTable>
3064
+ </p-treeTable>
3065
+ </div>
3116
3066
  </div>
3117
3067
 
3118
3068
  <!-- Change Summary -->
3119
3069
  @if (hasChanges()) {
3120
- <div class="surface-border border-1 border-round p-3 mt-4">
3121
- <div class="flex align-items-center gap-2 mb-3">
3122
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
3123
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
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>
3124
3074
  </div>
3125
- <div class="flex flex-col md:flex-row gap-4">
3075
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
3126
3076
  @if (pendingAdd().length > 0) {
3127
- <div class="w-full md:w-1/2">
3077
+ <div>
3128
3078
  <div class="flex items-center gap-2 mb-2">
3129
3079
  <i class="pi pi-plus-circle text-green-500"></i>
3130
- <strong class="text-sm">
3131
- To Whitelist ({{ pendingAdd().length }})
3132
- </strong>
3080
+ <strong class="text-sm">To Whitelist ({{ pendingAdd().length }})</strong>
3133
3081
  </div>
3134
3082
  <ul class="list-none p-0 m-0 pl-4">
3135
3083
  @for (action of pendingAdd(); track action.id) {
@@ -3138,14 +3086,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3138
3086
  </ul>
3139
3087
  </div>
3140
3088
  }
3141
-
3142
3089
  @if (pendingRemove().length > 0) {
3143
- <div class="w-full md:w-1/2">
3090
+ <div>
3144
3091
  <div class="flex items-center gap-2 mb-2">
3145
3092
  <i class="pi pi-minus-circle text-red-500"></i>
3146
- <strong class="text-sm">
3147
- To Remove ({{ pendingRemove().length }})
3148
- </strong>
3093
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
3149
3094
  </div>
3150
3095
  <ul class="list-none p-0 m-0 pl-4">
3151
3096
  @for (action of pendingRemove(); track action.id) {
@@ -3160,19 +3105,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3160
3105
  }
3161
3106
 
3162
3107
  @if (!loading() && actions().length === 0) {
3163
- <div class="surface-card p-5 border-round shadow-1 text-center">
3164
- <i
3165
- class="pi pi-info-circle text-color-secondary mb-3"
3166
- style="font-size: 3rem; display: block;"
3167
- ></i>
3168
- <p class="text-color-secondary m-0">
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">
3169
3111
  No actions available for this company.
3170
3112
  </p>
3171
3113
  </div>
3172
3114
  }
3173
3115
  }
3174
3116
  </div>
3175
- `, 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"] }]
3176
3118
  }], ctorParameters: () => [] });
3177
3119
 
3178
3120
  /**
@@ -3206,6 +3148,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3206
3148
  * ```
3207
3149
  */
3208
3150
  class UserRoleSelectorComponent {
3151
+ // Permission constants for template
3152
+ USER_ROLE_PERMISSIONS = USER_ROLE_PERMISSIONS;
3209
3153
  // Dependencies
3210
3154
  appConfig = inject(APP_CONFIG);
3211
3155
  companyContext = inject(LAYOUT_AUTH_STATE);
@@ -3213,6 +3157,9 @@ class UserRoleSelectorComponent {
3213
3157
  roleApi = inject(RoleApiService);
3214
3158
  permissionApi = inject(PermissionApiService);
3215
3159
  messageService = inject(MessageService);
3160
+ destroyRef = inject(DestroyRef);
3161
+ // AbortController for data loading
3162
+ loadDataAbortController = null;
3216
3163
  // State - User/Branch Selection
3217
3164
  selectedUserId = signal(null, ...(ngDevMode ? [{ debugName: "selectedUserId" }] : []));
3218
3165
  selectedBranchId = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedBranchId" }] : []));
@@ -3249,7 +3196,11 @@ class UserRoleSelectorComponent {
3249
3196
  hasChanges = computed(() => {
3250
3197
  const current = this.selectionMap();
3251
3198
  const initial = this.initialSelection();
3252
- 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]);
3253
3204
  }, ...(ngDevMode ? [{ debugName: "hasChanges" }] : []));
3254
3205
  // Computed - Pending Changes
3255
3206
  pendingAdd = computed(() => {
@@ -3269,6 +3220,10 @@ class UserRoleSelectorComponent {
3269
3220
  return this.hasChanges() && !this.saving();
3270
3221
  }, ...(ngDevMode ? [{ debugName: "canSave" }] : []));
3271
3222
  constructor() {
3223
+ // Cleanup on destroy
3224
+ this.destroyRef.onDestroy(() => {
3225
+ this.loadDataAbortController?.abort();
3226
+ });
3272
3227
  // Effect: Load user branches and data when user changes
3273
3228
  effect(() => {
3274
3229
  const userId = this.selectedUserId();
@@ -3316,11 +3271,7 @@ class UserRoleSelectorComponent {
3316
3271
  this.branches.set(userBranches);
3317
3272
  }
3318
3273
  catch {
3319
- this.messageService.add({
3320
- severity: 'error',
3321
- summary: 'Error',
3322
- detail: 'Failed to load user permitted branches',
3323
- });
3274
+ // Error toast handled by global interceptor
3324
3275
  }
3325
3276
  }
3326
3277
  /**
@@ -3358,11 +3309,7 @@ class UserRoleSelectorComponent {
3358
3309
  this._initialSelection.set({ ...selMap });
3359
3310
  }
3360
3311
  catch {
3361
- this.messageService.add({
3362
- severity: 'error',
3363
- summary: 'Error',
3364
- detail: 'Failed to load user role assignments',
3365
- });
3312
+ // Error toast handled by global interceptor
3366
3313
  }
3367
3314
  finally {
3368
3315
  this.loading.set(false);
@@ -3469,13 +3416,8 @@ class UserRoleSelectorComponent {
3469
3416
  // Update baseline
3470
3417
  this._initialSelection.set({ ...this.selectionMap() });
3471
3418
  }
3472
- catch (err) {
3473
- const error = err;
3474
- this.messageService.add({
3475
- severity: 'error',
3476
- summary: 'Error',
3477
- detail: error?.error?.message || 'Failed to update user role assignments',
3478
- });
3419
+ catch {
3420
+ // Error toast handled by global interceptor
3479
3421
  }
3480
3422
  finally {
3481
3423
  this.saving.set(false);
@@ -3493,28 +3435,30 @@ class UserRoleSelectorComponent {
3493
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: `
3494
3436
  <div class="user-role-selector">
3495
3437
  <!-- User and Branch Selectors -->
3496
- <div class="surface-card p-4 border-round mb-4 shadow-1">
3497
- <div class="formgrid grid gap-5">
3498
- <div class="field col-12 sm:col-6 mb-0">
3499
- <label class="block font-semibold mb-2 text-900">
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">
3500
3442
  <i class="pi pi-user mr-2 text-primary"></i>
3501
3443
  Select User
3502
3444
  </label>
3503
3445
  <lib-user-select
3504
- [(value)]="selectedUserId"
3446
+ [value]="selectedUserId()"
3447
+ (valueChange)="selectedUserId.set($event)"
3505
3448
  [isEditMode]="true"
3506
3449
  placeHolder="Select a user"
3507
3450
  />
3508
3451
  </div>
3509
3452
 
3510
3453
  @if (showBranchSelector()) {
3511
- <div class="field col-12 sm:col-6 mb-0">
3512
- <label class="block font-semibold mb-2 text-900">
3454
+ <div>
3455
+ <label class="block font-semibold mb-2">
3513
3456
  <i class="pi pi-building mr-2 text-primary"></i>
3514
3457
  Select Branch
3515
3458
  </label>
3516
3459
  <p-select
3517
- [(ngModel)]="selectedBranchId"
3460
+ [ngModel]="selectedBranchId()"
3461
+ (ngModelChange)="selectedBranchId.set($event)"
3518
3462
  [options]="filteredBranches()"
3519
3463
  optionLabel="name"
3520
3464
  optionValue="id"
@@ -3525,20 +3469,20 @@ class UserRoleSelectorComponent {
3525
3469
  >
3526
3470
  <ng-template #selectedItem let-branch>
3527
3471
  @if (branch) {
3528
- <div class="flex align-items-center gap-2">
3472
+ <div class="flex items-center gap-2">
3529
3473
  <i class="pi pi-building text-primary"></i>
3530
3474
  <span class="font-semibold">{{ branch.name }}</span>
3531
3475
  </div>
3532
3476
  }
3533
3477
  </ng-template>
3534
3478
  <ng-template #item let-branch>
3535
- <div class="flex align-items-center gap-2">
3536
- <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>
3537
3481
  <span>{{ branch.name }}</span>
3538
3482
  </div>
3539
3483
  </ng-template>
3540
3484
  </p-select>
3541
- <small class="text-color-secondary block mt-1">
3485
+ <small class="text-muted-color block mt-1">
3542
3486
  {{ filteredBranches().length }} permitted branch{{
3543
3487
  filteredBranches().length !== 1 ? 'es' : ''
3544
3488
  }}
@@ -3553,24 +3497,21 @@ class UserRoleSelectorComponent {
3553
3497
  <!-- Loading State -->
3554
3498
  @if (loading()) {
3555
3499
  <div
3556
- 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"
3557
3501
  >
3558
- <i
3559
- class="pi pi-spin pi-spinner text-primary"
3560
- style="font-size: 3rem"
3561
- ></i>
3502
+ <i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
3562
3503
  </div>
3563
3504
  }
3564
3505
 
3565
3506
  <!-- Role List -->
3566
3507
  @if (!loading() && roles().length > 0) {
3567
- <div class="surface-card p-4 border-round shadow-1">
3508
+ <div class="surface-card p-4 rounded-border shadow-sm">
3568
3509
  <div
3569
- 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"
3570
3511
  >
3571
3512
  <div>
3572
3513
  <h5 class="m-0 mb-1">Role Assignments</h5>
3573
- <p class="text-sm text-color-secondary m-0">
3514
+ <p class="text-sm text-muted-color m-0">
3574
3515
  {{ roles().length }} roles available
3575
3516
  </p>
3576
3517
  </div>
@@ -3592,6 +3533,7 @@ class UserRoleSelectorComponent {
3592
3533
  (onClick)="deselectAll()"
3593
3534
  />
3594
3535
  <p-button
3536
+ *hasPermission="USER_ROLE_PERMISSIONS.ASSIGN"
3595
3537
  label="Save Changes"
3596
3538
  icon="pi pi-save"
3597
3539
  [disabled]="!canSave()"
@@ -3604,77 +3546,78 @@ class UserRoleSelectorComponent {
3604
3546
  </div>
3605
3547
 
3606
3548
  <!-- Role Table -->
3607
- <p-table
3608
- [value]="roles()"
3609
- [rows]="10"
3610
- [paginator]="roles().length > 10"
3611
- [rowsPerPageOptions]="[10, 20, 50]"
3612
- [globalFilterFields]="['name', 'code', 'description']"
3613
- [showCurrentPageReport]="true"
3614
- currentPageReportTemplate="Showing {first} to {last} of {totalRecords} roles"
3615
- styleClass="p-datatable-sm"
3616
- >
3617
- <ng-template #header>
3618
- <tr>
3619
- <th style="width: 3rem">
3620
- <p-checkbox
3621
- [ngModel]="allSelected()"
3622
- [binary]="true"
3623
- (ngModelChange)="toggleAll()"
3624
- pTooltip="Select/Deselect All"
3625
- tooltipPosition="top"
3626
- />
3627
- </th>
3628
- <th>Name</th>
3629
- <th>Code</th>
3630
- <th>Description</th>
3631
- </tr>
3632
- </ng-template>
3633
- <ng-template #body let-role>
3634
- <tr>
3635
- <td>
3636
- <p-checkbox
3637
- [ngModel]="selectionMap()[role.id]"
3638
- [binary]="true"
3639
- (ngModelChange)="onRoleToggle(role, $event)"
3640
- [pTooltip]="getTooltip(role)"
3641
- tooltipPosition="top"
3642
- />
3643
- </td>
3644
- <td>{{ role.name }}</td>
3645
- <td>{{ role.code || '-' }}</td>
3646
- <td>{{ role.description || '-' }}</td>
3647
- </tr>
3648
- </ng-template>
3649
- <ng-template #emptymessage>
3650
- <tr>
3651
- <td colspan="4" class="text-center p-4">
3652
- @if (loading()) {
3653
- <i class="pi pi-spin pi-spinner"></i> Loading roles...
3654
- } @else {
3655
- No roles available.
3656
- }
3657
- </td>
3658
- </tr>
3659
- </ng-template>
3660
- </p-table>
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>
3661
3606
  </div>
3662
3607
 
3663
3608
  <!-- Change Summary -->
3664
3609
  @if (hasChanges()) {
3665
- <div class="surface-border border-1 border-round p-3 mt-4">
3666
- <div class="flex align-items-center gap-2 mb-3">
3667
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
3668
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
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>
3669
3614
  </div>
3670
- <div class="flex flex-col md:flex-row gap-4">
3615
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
3671
3616
  @if (pendingAdd().length > 0) {
3672
- <div class="w-full md:w-1/2">
3673
- <div class="flex align-items-center gap-2 mb-2">
3617
+ <div>
3618
+ <div class="flex items-center gap-2 mb-2">
3674
3619
  <i class="pi pi-plus-circle text-green-500"></i>
3675
- <strong class="text-sm"
3676
- >To Assign ({{ pendingAdd().length }})</strong
3677
- >
3620
+ <strong class="text-sm">To Assign ({{ pendingAdd().length }})</strong>
3678
3621
  </div>
3679
3622
  <ul class="list-none p-0 m-0 pl-4">
3680
3623
  @for (role of pendingAdd(); track role.id) {
@@ -3684,12 +3627,10 @@ class UserRoleSelectorComponent {
3684
3627
  </div>
3685
3628
  }
3686
3629
  @if (pendingRemove().length > 0) {
3687
- <div class="w-full md:w-1/2">
3688
- <div class="flex align-items-center gap-2 mb-2">
3630
+ <div>
3631
+ <div class="flex items-center gap-2 mb-2">
3689
3632
  <i class="pi pi-minus-circle text-red-500"></i>
3690
- <strong class="text-sm"
3691
- >To Remove ({{ pendingRemove().length }})</strong
3692
- >
3633
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
3693
3634
  </div>
3694
3635
  <ul class="list-none p-0 m-0 pl-4">
3695
3636
  @for (role of pendingRemove(); track role.id) {
@@ -3704,47 +3645,46 @@ class UserRoleSelectorComponent {
3704
3645
  }
3705
3646
 
3706
3647
  @if (!loading() && roles().length === 0) {
3707
- <div class="surface-card p-5 border-round shadow-1 text-center">
3708
- <i
3709
- class="pi pi-info-circle text-color-secondary mb-3"
3710
- style="font-size: 3rem; display: block;"
3711
- ></i>
3712
- <p class="text-color-secondary m-0">
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">
3713
3651
  No roles available for this user.
3714
3652
  </p>
3715
3653
  </div>
3716
3654
  }
3717
3655
  }
3718
3656
  </div>
3719
- `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: 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", "showOnEllipsis", "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"] }, { kind: "component", type: UserSelectComponent, selector: "lib-user-select", inputs: ["loadUsers", "placeHolder", "isEditMode", "filterActive", "additionalFilters", "pageSize", "value"], outputs: ["valueChange", "userSelected", "onError"] }], 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 });
3720
3658
  }
3721
3659
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserRoleSelectorComponent, decorators: [{
3722
3660
  type: Component,
3723
- args: [{ selector: 'flusys-user-role-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule, UserSelectComponent], template: `
3661
+ args: [{ selector: 'flusys-user-role-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule, HasPermissionDirective, UserSelectComponent], template: `
3724
3662
  <div class="user-role-selector">
3725
3663
  <!-- User and Branch Selectors -->
3726
- <div class="surface-card p-4 border-round mb-4 shadow-1">
3727
- <div class="formgrid grid gap-5">
3728
- <div class="field col-12 sm:col-6 mb-0">
3729
- <label class="block font-semibold mb-2 text-900">
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">
3730
3668
  <i class="pi pi-user mr-2 text-primary"></i>
3731
3669
  Select User
3732
3670
  </label>
3733
3671
  <lib-user-select
3734
- [(value)]="selectedUserId"
3672
+ [value]="selectedUserId()"
3673
+ (valueChange)="selectedUserId.set($event)"
3735
3674
  [isEditMode]="true"
3736
3675
  placeHolder="Select a user"
3737
3676
  />
3738
3677
  </div>
3739
3678
 
3740
3679
  @if (showBranchSelector()) {
3741
- <div class="field col-12 sm:col-6 mb-0">
3742
- <label class="block font-semibold mb-2 text-900">
3680
+ <div>
3681
+ <label class="block font-semibold mb-2">
3743
3682
  <i class="pi pi-building mr-2 text-primary"></i>
3744
3683
  Select Branch
3745
3684
  </label>
3746
3685
  <p-select
3747
- [(ngModel)]="selectedBranchId"
3686
+ [ngModel]="selectedBranchId()"
3687
+ (ngModelChange)="selectedBranchId.set($event)"
3748
3688
  [options]="filteredBranches()"
3749
3689
  optionLabel="name"
3750
3690
  optionValue="id"
@@ -3755,20 +3695,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3755
3695
  >
3756
3696
  <ng-template #selectedItem let-branch>
3757
3697
  @if (branch) {
3758
- <div class="flex align-items-center gap-2">
3698
+ <div class="flex items-center gap-2">
3759
3699
  <i class="pi pi-building text-primary"></i>
3760
3700
  <span class="font-semibold">{{ branch.name }}</span>
3761
3701
  </div>
3762
3702
  }
3763
3703
  </ng-template>
3764
3704
  <ng-template #item let-branch>
3765
- <div class="flex align-items-center gap-2">
3766
- <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>
3767
3707
  <span>{{ branch.name }}</span>
3768
3708
  </div>
3769
3709
  </ng-template>
3770
3710
  </p-select>
3771
- <small class="text-color-secondary block mt-1">
3711
+ <small class="text-muted-color block mt-1">
3772
3712
  {{ filteredBranches().length }} permitted branch{{
3773
3713
  filteredBranches().length !== 1 ? 'es' : ''
3774
3714
  }}
@@ -3783,24 +3723,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3783
3723
  <!-- Loading State -->
3784
3724
  @if (loading()) {
3785
3725
  <div
3786
- 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"
3787
3727
  >
3788
- <i
3789
- class="pi pi-spin pi-spinner text-primary"
3790
- style="font-size: 3rem"
3791
- ></i>
3728
+ <i class="pi pi-spin pi-spinner text-primary text-5xl"></i>
3792
3729
  </div>
3793
3730
  }
3794
3731
 
3795
3732
  <!-- Role List -->
3796
3733
  @if (!loading() && roles().length > 0) {
3797
- <div class="surface-card p-4 border-round shadow-1">
3734
+ <div class="surface-card p-4 rounded-border shadow-sm">
3798
3735
  <div
3799
- 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"
3800
3737
  >
3801
3738
  <div>
3802
3739
  <h5 class="m-0 mb-1">Role Assignments</h5>
3803
- <p class="text-sm text-color-secondary m-0">
3740
+ <p class="text-sm text-muted-color m-0">
3804
3741
  {{ roles().length }} roles available
3805
3742
  </p>
3806
3743
  </div>
@@ -3822,6 +3759,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3822
3759
  (onClick)="deselectAll()"
3823
3760
  />
3824
3761
  <p-button
3762
+ *hasPermission="USER_ROLE_PERMISSIONS.ASSIGN"
3825
3763
  label="Save Changes"
3826
3764
  icon="pi pi-save"
3827
3765
  [disabled]="!canSave()"
@@ -3834,77 +3772,78 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3834
3772
  </div>
3835
3773
 
3836
3774
  <!-- Role Table -->
3837
- <p-table
3838
- [value]="roles()"
3839
- [rows]="10"
3840
- [paginator]="roles().length > 10"
3841
- [rowsPerPageOptions]="[10, 20, 50]"
3842
- [globalFilterFields]="['name', 'code', 'description']"
3843
- [showCurrentPageReport]="true"
3844
- currentPageReportTemplate="Showing {first} to {last} of {totalRecords} roles"
3845
- styleClass="p-datatable-sm"
3846
- >
3847
- <ng-template #header>
3848
- <tr>
3849
- <th style="width: 3rem">
3850
- <p-checkbox
3851
- [ngModel]="allSelected()"
3852
- [binary]="true"
3853
- (ngModelChange)="toggleAll()"
3854
- pTooltip="Select/Deselect All"
3855
- tooltipPosition="top"
3856
- />
3857
- </th>
3858
- <th>Name</th>
3859
- <th>Code</th>
3860
- <th>Description</th>
3861
- </tr>
3862
- </ng-template>
3863
- <ng-template #body let-role>
3864
- <tr>
3865
- <td>
3866
- <p-checkbox
3867
- [ngModel]="selectionMap()[role.id]"
3868
- [binary]="true"
3869
- (ngModelChange)="onRoleToggle(role, $event)"
3870
- [pTooltip]="getTooltip(role)"
3871
- tooltipPosition="top"
3872
- />
3873
- </td>
3874
- <td>{{ role.name }}</td>
3875
- <td>{{ role.code || '-' }}</td>
3876
- <td>{{ role.description || '-' }}</td>
3877
- </tr>
3878
- </ng-template>
3879
- <ng-template #emptymessage>
3880
- <tr>
3881
- <td colspan="4" class="text-center p-4">
3882
- @if (loading()) {
3883
- <i class="pi pi-spin pi-spinner"></i> Loading roles...
3884
- } @else {
3885
- No roles available.
3886
- }
3887
- </td>
3888
- </tr>
3889
- </ng-template>
3890
- </p-table>
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>
3891
3832
  </div>
3892
3833
 
3893
3834
  <!-- Change Summary -->
3894
3835
  @if (hasChanges()) {
3895
- <div class="surface-border border-1 border-round p-3 mt-4">
3896
- <div class="flex align-items-center gap-2 mb-3">
3897
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
3898
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
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>
3899
3840
  </div>
3900
- <div class="flex flex-col md:flex-row gap-4">
3841
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
3901
3842
  @if (pendingAdd().length > 0) {
3902
- <div class="w-full md:w-1/2">
3903
- <div class="flex align-items-center gap-2 mb-2">
3843
+ <div>
3844
+ <div class="flex items-center gap-2 mb-2">
3904
3845
  <i class="pi pi-plus-circle text-green-500"></i>
3905
- <strong class="text-sm"
3906
- >To Assign ({{ pendingAdd().length }})</strong
3907
- >
3846
+ <strong class="text-sm">To Assign ({{ pendingAdd().length }})</strong>
3908
3847
  </div>
3909
3848
  <ul class="list-none p-0 m-0 pl-4">
3910
3849
  @for (role of pendingAdd(); track role.id) {
@@ -3914,12 +3853,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3914
3853
  </div>
3915
3854
  }
3916
3855
  @if (pendingRemove().length > 0) {
3917
- <div class="w-full md:w-1/2">
3918
- <div class="flex align-items-center gap-2 mb-2">
3856
+ <div>
3857
+ <div class="flex items-center gap-2 mb-2">
3919
3858
  <i class="pi pi-minus-circle text-red-500"></i>
3920
- <strong class="text-sm"
3921
- >To Remove ({{ pendingRemove().length }})</strong
3922
- >
3859
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
3923
3860
  </div>
3924
3861
  <ul class="list-none p-0 m-0 pl-4">
3925
3862
  @for (role of pendingRemove(); track role.id) {
@@ -3934,12 +3871,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3934
3871
  }
3935
3872
 
3936
3873
  @if (!loading() && roles().length === 0) {
3937
- <div class="surface-card p-5 border-round shadow-1 text-center">
3938
- <i
3939
- class="pi pi-info-circle text-color-secondary mb-3"
3940
- style="font-size: 3rem; display: block;"
3941
- ></i>
3942
- <p class="text-color-secondary m-0">
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">
3943
3877
  No roles available for this user.
3944
3878
  </p>
3945
3879
  </div>
@@ -3981,6 +3915,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3981
3915
  * ```
3982
3916
  */
3983
3917
  class UserActionSelectorComponent {
3918
+ // Permission constants for template
3919
+ USER_ACTION_PERMISSIONS = USER_ACTION_PERMISSIONS;
3984
3920
  // Dependencies
3985
3921
  appConfig = inject(APP_CONFIG);
3986
3922
  companyContext = inject(LAYOUT_AUTH_STATE);
@@ -3989,6 +3925,9 @@ class UserActionSelectorComponent {
3989
3925
  permissionApi = inject(PermissionApiService);
3990
3926
  permissionLogic = inject(ActionPermissionLogicService);
3991
3927
  messageService = inject(MessageService);
3928
+ destroyRef = inject(DestroyRef);
3929
+ // AbortController for data loading
3930
+ loadDataAbortController = null;
3992
3931
  // State - User/Branch Selection
3993
3932
  selectedUserId = signal(null, ...(ngDevMode ? [{ debugName: "selectedUserId" }] : []));
3994
3933
  selectedBranchId = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedBranchId" }] : []));
@@ -4030,7 +3969,11 @@ class UserActionSelectorComponent {
4030
3969
  hasChanges = computed(() => {
4031
3970
  const current = this.selectionMap();
4032
3971
  const initial = this.initialSelection();
4033
- 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]);
4034
3977
  }, ...(ngDevMode ? [{ debugName: "hasChanges" }] : []));
4035
3978
  // Computed - Validation
4036
3979
  actionsWithUnmetPrerequisites = computed(() => {
@@ -4066,6 +4009,10 @@ class UserActionSelectorComponent {
4066
4009
  return (this.hasChanges() && !this.saving() && this.invalidActionsCount() === 0);
4067
4010
  }, ...(ngDevMode ? [{ debugName: "canSave" }] : []));
4068
4011
  constructor() {
4012
+ // Cleanup on destroy
4013
+ this.destroyRef.onDestroy(() => {
4014
+ this.loadDataAbortController?.abort();
4015
+ });
4069
4016
  // Effect: Load user branches and data when user changes
4070
4017
  effect(() => {
4071
4018
  const userId = this.selectedUserId();
@@ -4113,11 +4060,7 @@ class UserActionSelectorComponent {
4113
4060
  this.branches.set(userBranches);
4114
4061
  }
4115
4062
  catch {
4116
- this.messageService.add({
4117
- severity: 'error',
4118
- summary: 'Error',
4119
- detail: 'Failed to load user permitted branches',
4120
- });
4063
+ // Error toast handled by global interceptor
4121
4064
  }
4122
4065
  }
4123
4066
  /**
@@ -4158,11 +4101,7 @@ class UserActionSelectorComponent {
4158
4101
  this._initialSelection.set({ ...selMap });
4159
4102
  }
4160
4103
  catch {
4161
- this.messageService.add({
4162
- severity: 'error',
4163
- summary: 'Error',
4164
- detail: 'Failed to load user action permissions',
4165
- });
4104
+ // Error toast handled by global interceptor
4166
4105
  }
4167
4106
  finally {
4168
4107
  this.loading.set(false);
@@ -4197,12 +4136,17 @@ class UserActionSelectorComponent {
4197
4136
  : false;
4198
4137
  }
4199
4138
  /**
4200
- * Handle action checkbox toggle
4139
+ * Handle action toggle with dependency management
4201
4140
  */
4202
4141
  onActionToggle(action, newValue) {
4203
- const selMap = { ...this.selectionMap() };
4204
- selMap[action.id] = newValue;
4205
- 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
+ }
4206
4150
  }
4207
4151
  /**
4208
4152
  * Toggle all actions
@@ -4253,6 +4197,12 @@ class UserActionSelectorComponent {
4253
4197
  });
4254
4198
  return;
4255
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
+ }
4256
4206
  // Build payload
4257
4207
  const items = [];
4258
4208
  this.pendingAdd().forEach((action) => {
@@ -4292,13 +4242,8 @@ class UserActionSelectorComponent {
4292
4242
  // Update baseline
4293
4243
  this._initialSelection.set({ ...this.selectionMap() });
4294
4244
  }
4295
- catch (err) {
4296
- const error = err;
4297
- this.messageService.add({
4298
- severity: 'error',
4299
- summary: 'Error',
4300
- detail: error?.error?.message || 'Failed to update user action permissions',
4301
- });
4245
+ catch {
4246
+ // Error toast handled by global interceptor
4302
4247
  }
4303
4248
  finally {
4304
4249
  this.saving.set(false);
@@ -4317,28 +4262,30 @@ class UserActionSelectorComponent {
4317
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: `
4318
4263
  <div class="user-action-selector">
4319
4264
  <!-- User and Branch Selectors -->
4320
- <div class="surface-card p-4 border-round mb-4 shadow-1">
4321
- <div class="formgrid grid gap-5">
4322
- <div class="field col-12 sm:col-6 mb-0">
4323
- <label class="block font-semibold mb-2 text-900">
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">
4324
4269
  <i class="pi pi-user mr-2 text-primary"></i>
4325
4270
  Select User
4326
4271
  </label>
4327
4272
  <lib-user-select
4328
- [(value)]="selectedUserId"
4273
+ [value]="selectedUserId()"
4274
+ (valueChange)="selectedUserId.set($event)"
4329
4275
  [isEditMode]="true"
4330
4276
  placeHolder="Select a user"
4331
4277
  />
4332
4278
  </div>
4333
4279
 
4334
4280
  @if (showBranchSelector()) {
4335
- <div class="field col-12 sm:col-6 mb-0">
4336
- <label class="block font-semibold mb-2 text-900">
4281
+ <div>
4282
+ <label class="block font-semibold mb-2">
4337
4283
  <i class="pi pi-building mr-2 text-primary"></i>
4338
4284
  Select Branch
4339
4285
  </label>
4340
4286
  <p-select
4341
- [(ngModel)]="selectedBranchId"
4287
+ [ngModel]="selectedBranchId()"
4288
+ (ngModelChange)="selectedBranchId.set($event)"
4342
4289
  [options]="filteredBranches()"
4343
4290
  optionLabel="name"
4344
4291
  optionValue="id"
@@ -4349,20 +4296,20 @@ class UserActionSelectorComponent {
4349
4296
  >
4350
4297
  <ng-template #selectedItem let-branch>
4351
4298
  @if (branch) {
4352
- <div class="flex align-items-center gap-2">
4299
+ <div class="flex items-center gap-2">
4353
4300
  <i class="pi pi-building text-primary"></i>
4354
4301
  <span class="font-semibold">{{ branch.name }}</span>
4355
4302
  </div>
4356
4303
  }
4357
4304
  </ng-template>
4358
4305
  <ng-template #item let-branch>
4359
- <div class="flex align-items-center gap-2">
4360
- <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>
4361
4308
  <span>{{ branch.name }}</span>
4362
4309
  </div>
4363
4310
  </ng-template>
4364
4311
  </p-select>
4365
- <small class="text-color-secondary block mt-1">
4312
+ <small class="text-muted-color block mt-1">
4366
4313
  {{ filteredBranches().length }} permitted branch{{
4367
4314
  filteredBranches().length !== 1 ? 'es' : ''
4368
4315
  }}
@@ -4376,20 +4323,20 @@ class UserActionSelectorComponent {
4376
4323
  @if (selectedUserId()) {
4377
4324
  <!-- Loading State -->
4378
4325
  @if (loading()) {
4379
- <div class="flex justify-content-center p-5">
4380
- <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>
4381
4328
  </div>
4382
4329
  }
4383
4330
 
4384
4331
  <!-- Action List -->
4385
4332
  @if (!loading() && actions().length > 0) {
4386
- <div class="surface-card p-4 border-round shadow-1">
4333
+ <div class="surface-card p-4 rounded-border shadow-sm">
4387
4334
  <div
4388
- 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"
4389
4336
  >
4390
4337
  <div>
4391
4338
  <h5 class="m-0 mb-1">Direct Action Permissions</h5>
4392
- <p class="text-sm text-color-secondary m-0">
4339
+ <p class="text-sm text-muted-color m-0">
4393
4340
  {{ actions().length }} actions available
4394
4341
  </p>
4395
4342
  </div>
@@ -4411,6 +4358,7 @@ class UserActionSelectorComponent {
4411
4358
  (onClick)="deselectAll()"
4412
4359
  />
4413
4360
  <p-button
4361
+ *hasPermission="USER_ACTION_PERMISSIONS.ASSIGN"
4414
4362
  label="Save Changes"
4415
4363
  icon="pi pi-save"
4416
4364
  [disabled]="!canSave()"
@@ -4422,34 +4370,52 @@ class UserActionSelectorComponent {
4422
4370
  </div>
4423
4371
  </div>
4424
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
+
4425
4391
  <!-- Action Tree Table -->
4426
- <p-treeTable
4427
- [value]="treeNodes()"
4428
- [scrollable]="true"
4429
- scrollHeight="flex"
4430
- dataKey="id"
4431
- styleClass="p-treetable-sm"
4432
- >
4433
- <ng-template pTemplate="header">
4434
- <tr>
4435
- <th style="width: 3rem">
4436
- <p-checkbox
4437
- [ngModel]="allSelected()"
4438
- [binary]="true"
4439
- (ngModelChange)="toggleAll()"
4440
- pTooltip="Select/Deselect All"
4441
- tooltipPosition="top"
4442
- />
4443
- </th>
4444
- <th>Name</th>
4445
- <th>Code</th>
4446
- <th>Type</th>
4447
- <th>Description</th>
4448
- </tr>
4449
- </ng-template>
4450
- <ng-template pTemplate="body" let-rowNode let-rowData="rowData">
4451
- <tr>
4452
- <td style="width: 3rem">
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">
4453
4419
  <p-checkbox
4454
4420
  [ngModel]="selectionMap()[rowData.id]"
4455
4421
  [binary]="true"
@@ -4460,9 +4426,18 @@ class UserActionSelectorComponent {
4460
4426
  </td>
4461
4427
  <td>
4462
4428
  <p-treeTableToggler [rowNode]="rowNode" />
4463
- <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>
4464
4439
  </td>
4465
- <td>{{ rowData.code || '-' }}</td>
4440
+ <td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
4466
4441
  <td>
4467
4442
  <p-tag
4468
4443
  [value]="rowData.actionType"
@@ -4471,12 +4446,12 @@ class UserActionSelectorComponent {
4471
4446
  "
4472
4447
  />
4473
4448
  </td>
4474
- <td>{{ rowData.description || '-' }}</td>
4449
+ <td class="hidden lg:table-cell">{{ rowData.description || '-' }}</td>
4475
4450
  </tr>
4476
4451
  </ng-template>
4477
- <ng-template pTemplate="emptymessage">
4452
+ <ng-template #emptymessage>
4478
4453
  <tr>
4479
- <td colspan="5" class="text-center p-4">
4454
+ <td colspan="5" class="text-center p-4 text-muted-color">
4480
4455
  @if (loading()) {
4481
4456
  <i class="pi pi-spin pi-spinner"></i> Loading actions...
4482
4457
  } @else {
@@ -4485,24 +4460,23 @@ class UserActionSelectorComponent {
4485
4460
  </td>
4486
4461
  </tr>
4487
4462
  </ng-template>
4488
- </p-treeTable>
4463
+ </p-treeTable>
4464
+ </div>
4489
4465
  </div>
4490
4466
 
4491
4467
  <!-- Change Summary -->
4492
4468
  @if (hasChanges()) {
4493
- <div class="surface-border border-1 border-round p-3 mt-4">
4494
- <div class="flex align-items-center gap-2 mb-3">
4495
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
4496
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
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>
4497
4473
  </div>
4498
- <div class="flex flex-col md:flex-row gap-4">
4474
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
4499
4475
  @if (pendingAdd().length > 0) {
4500
- <div class="w-full md:w-1/2">
4501
- <div class="flex align-items-center gap-2 mb-2">
4476
+ <div>
4477
+ <div class="flex items-center gap-2 mb-2">
4502
4478
  <i class="pi pi-plus-circle text-green-500"></i>
4503
- <strong class="text-sm"
4504
- >To Assign ({{ pendingAdd().length }})</strong
4505
- >
4479
+ <strong class="text-sm">To Assign ({{ pendingAdd().length }})</strong>
4506
4480
  </div>
4507
4481
  <ul class="list-none p-0 m-0 pl-4">
4508
4482
  @for (action of pendingAdd(); track action.id) {
@@ -4512,12 +4486,10 @@ class UserActionSelectorComponent {
4512
4486
  </div>
4513
4487
  }
4514
4488
  @if (pendingRemove().length > 0) {
4515
- <div class="w-full md:w-1/2">
4516
- <div class="flex align-items-center gap-2 mb-2">
4489
+ <div>
4490
+ <div class="flex items-center gap-2 mb-2">
4517
4491
  <i class="pi pi-minus-circle text-red-500"></i>
4518
- <strong class="text-sm"
4519
- >To Remove ({{ pendingRemove().length }})</strong
4520
- >
4492
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
4521
4493
  </div>
4522
4494
  <ul class="list-none p-0 m-0 pl-4">
4523
4495
  @for (action of pendingRemove(); track action.id) {
@@ -4532,47 +4504,46 @@ class UserActionSelectorComponent {
4532
4504
  }
4533
4505
 
4534
4506
  @if (!loading() && actions().length === 0) {
4535
- <div class="surface-card p-5 border-round shadow-1 text-center">
4536
- <i
4537
- class="pi pi-info-circle text-color-secondary mb-3"
4538
- style="font-size: 3rem; display: block;"
4539
- ></i>
4540
- <p class="text-color-secondary m-0">
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">
4541
4510
  No actions available for this user.
4542
4511
  </p>
4543
4512
  </div>
4544
4513
  }
4545
4514
  }
4546
4515
  </div>
4547
- `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "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", "showOnEllipsis", "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"] }, { kind: "component", type: UserSelectComponent, selector: "lib-user-select", inputs: ["loadUsers", "placeHolder", "isEditMode", "filterActive", "additionalFilters", "pageSize", "value"], outputs: ["valueChange", "userSelected", "onError"] }], 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 });
4548
4517
  }
4549
4518
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserActionSelectorComponent, decorators: [{
4550
4519
  type: Component,
4551
- args: [{ selector: 'flusys-user-action-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule, UserSelectComponent], template: `
4520
+ args: [{ selector: 'flusys-user-action-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, PrimeModule, HasPermissionDirective, UserSelectComponent], template: `
4552
4521
  <div class="user-action-selector">
4553
4522
  <!-- User and Branch Selectors -->
4554
- <div class="surface-card p-4 border-round mb-4 shadow-1">
4555
- <div class="formgrid grid gap-5">
4556
- <div class="field col-12 sm:col-6 mb-0">
4557
- <label class="block font-semibold mb-2 text-900">
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">
4558
4527
  <i class="pi pi-user mr-2 text-primary"></i>
4559
4528
  Select User
4560
4529
  </label>
4561
4530
  <lib-user-select
4562
- [(value)]="selectedUserId"
4531
+ [value]="selectedUserId()"
4532
+ (valueChange)="selectedUserId.set($event)"
4563
4533
  [isEditMode]="true"
4564
4534
  placeHolder="Select a user"
4565
4535
  />
4566
4536
  </div>
4567
4537
 
4568
4538
  @if (showBranchSelector()) {
4569
- <div class="field col-12 sm:col-6 mb-0">
4570
- <label class="block font-semibold mb-2 text-900">
4539
+ <div>
4540
+ <label class="block font-semibold mb-2">
4571
4541
  <i class="pi pi-building mr-2 text-primary"></i>
4572
4542
  Select Branch
4573
4543
  </label>
4574
4544
  <p-select
4575
- [(ngModel)]="selectedBranchId"
4545
+ [ngModel]="selectedBranchId()"
4546
+ (ngModelChange)="selectedBranchId.set($event)"
4576
4547
  [options]="filteredBranches()"
4577
4548
  optionLabel="name"
4578
4549
  optionValue="id"
@@ -4583,20 +4554,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4583
4554
  >
4584
4555
  <ng-template #selectedItem let-branch>
4585
4556
  @if (branch) {
4586
- <div class="flex align-items-center gap-2">
4557
+ <div class="flex items-center gap-2">
4587
4558
  <i class="pi pi-building text-primary"></i>
4588
4559
  <span class="font-semibold">{{ branch.name }}</span>
4589
4560
  </div>
4590
4561
  }
4591
4562
  </ng-template>
4592
4563
  <ng-template #item let-branch>
4593
- <div class="flex align-items-center gap-2">
4594
- <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>
4595
4566
  <span>{{ branch.name }}</span>
4596
4567
  </div>
4597
4568
  </ng-template>
4598
4569
  </p-select>
4599
- <small class="text-color-secondary block mt-1">
4570
+ <small class="text-muted-color block mt-1">
4600
4571
  {{ filteredBranches().length }} permitted branch{{
4601
4572
  filteredBranches().length !== 1 ? 'es' : ''
4602
4573
  }}
@@ -4610,20 +4581,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4610
4581
  @if (selectedUserId()) {
4611
4582
  <!-- Loading State -->
4612
4583
  @if (loading()) {
4613
- <div class="flex justify-content-center p-5">
4614
- <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>
4615
4586
  </div>
4616
4587
  }
4617
4588
 
4618
4589
  <!-- Action List -->
4619
4590
  @if (!loading() && actions().length > 0) {
4620
- <div class="surface-card p-4 border-round shadow-1">
4591
+ <div class="surface-card p-4 rounded-border shadow-sm">
4621
4592
  <div
4622
- 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"
4623
4594
  >
4624
4595
  <div>
4625
4596
  <h5 class="m-0 mb-1">Direct Action Permissions</h5>
4626
- <p class="text-sm text-color-secondary m-0">
4597
+ <p class="text-sm text-muted-color m-0">
4627
4598
  {{ actions().length }} actions available
4628
4599
  </p>
4629
4600
  </div>
@@ -4645,6 +4616,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4645
4616
  (onClick)="deselectAll()"
4646
4617
  />
4647
4618
  <p-button
4619
+ *hasPermission="USER_ACTION_PERMISSIONS.ASSIGN"
4648
4620
  label="Save Changes"
4649
4621
  icon="pi pi-save"
4650
4622
  [disabled]="!canSave()"
@@ -4656,34 +4628,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4656
4628
  </div>
4657
4629
  </div>
4658
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
+
4659
4649
  <!-- Action Tree Table -->
4660
- <p-treeTable
4661
- [value]="treeNodes()"
4662
- [scrollable]="true"
4663
- scrollHeight="flex"
4664
- dataKey="id"
4665
- styleClass="p-treetable-sm"
4666
- >
4667
- <ng-template pTemplate="header">
4668
- <tr>
4669
- <th style="width: 3rem">
4670
- <p-checkbox
4671
- [ngModel]="allSelected()"
4672
- [binary]="true"
4673
- (ngModelChange)="toggleAll()"
4674
- pTooltip="Select/Deselect All"
4675
- tooltipPosition="top"
4676
- />
4677
- </th>
4678
- <th>Name</th>
4679
- <th>Code</th>
4680
- <th>Type</th>
4681
- <th>Description</th>
4682
- </tr>
4683
- </ng-template>
4684
- <ng-template pTemplate="body" let-rowNode let-rowData="rowData">
4685
- <tr>
4686
- <td style="width: 3rem">
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">
4687
4677
  <p-checkbox
4688
4678
  [ngModel]="selectionMap()[rowData.id]"
4689
4679
  [binary]="true"
@@ -4694,9 +4684,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4694
4684
  </td>
4695
4685
  <td>
4696
4686
  <p-treeTableToggler [rowNode]="rowNode" />
4697
- <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>
4698
4697
  </td>
4699
- <td>{{ rowData.code || '-' }}</td>
4698
+ <td class="hidden md:table-cell">{{ rowData.code || '-' }}</td>
4700
4699
  <td>
4701
4700
  <p-tag
4702
4701
  [value]="rowData.actionType"
@@ -4705,12 +4704,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4705
4704
  "
4706
4705
  />
4707
4706
  </td>
4708
- <td>{{ rowData.description || '-' }}</td>
4707
+ <td class="hidden lg:table-cell">{{ rowData.description || '-' }}</td>
4709
4708
  </tr>
4710
4709
  </ng-template>
4711
- <ng-template pTemplate="emptymessage">
4710
+ <ng-template #emptymessage>
4712
4711
  <tr>
4713
- <td colspan="5" class="text-center p-4">
4712
+ <td colspan="5" class="text-center p-4 text-muted-color">
4714
4713
  @if (loading()) {
4715
4714
  <i class="pi pi-spin pi-spinner"></i> Loading actions...
4716
4715
  } @else {
@@ -4719,24 +4718,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4719
4718
  </td>
4720
4719
  </tr>
4721
4720
  </ng-template>
4722
- </p-treeTable>
4721
+ </p-treeTable>
4722
+ </div>
4723
4723
  </div>
4724
4724
 
4725
4725
  <!-- Change Summary -->
4726
4726
  @if (hasChanges()) {
4727
- <div class="surface-border border-1 border-round p-3 mt-4">
4728
- <div class="flex align-items-center gap-2 mb-3">
4729
- <i class="pi pi-info-circle text-blue-500 content-center"></i>
4730
- <p class="m-0 mb-1 font-bold">Pending Changes</p>
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>
4731
4731
  </div>
4732
- <div class="flex flex-col md:flex-row gap-4">
4732
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
4733
4733
  @if (pendingAdd().length > 0) {
4734
- <div class="w-full md:w-1/2">
4735
- <div class="flex align-items-center gap-2 mb-2">
4734
+ <div>
4735
+ <div class="flex items-center gap-2 mb-2">
4736
4736
  <i class="pi pi-plus-circle text-green-500"></i>
4737
- <strong class="text-sm"
4738
- >To Assign ({{ pendingAdd().length }})</strong
4739
- >
4737
+ <strong class="text-sm">To Assign ({{ pendingAdd().length }})</strong>
4740
4738
  </div>
4741
4739
  <ul class="list-none p-0 m-0 pl-4">
4742
4740
  @for (action of pendingAdd(); track action.id) {
@@ -4746,12 +4744,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4746
4744
  </div>
4747
4745
  }
4748
4746
  @if (pendingRemove().length > 0) {
4749
- <div class="w-full md:w-1/2">
4750
- <div class="flex align-items-center gap-2 mb-2">
4747
+ <div>
4748
+ <div class="flex items-center gap-2 mb-2">
4751
4749
  <i class="pi pi-minus-circle text-red-500"></i>
4752
- <strong class="text-sm"
4753
- >To Remove ({{ pendingRemove().length }})</strong
4754
- >
4750
+ <strong class="text-sm">To Remove ({{ pendingRemove().length }})</strong>
4755
4751
  </div>
4756
4752
  <ul class="list-none p-0 m-0 pl-4">
4757
4753
  @for (action of pendingRemove(); track action.id) {
@@ -4766,23 +4762,101 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4766
4762
  }
4767
4763
 
4768
4764
  @if (!loading() && actions().length === 0) {
4769
- <div class="surface-card p-5 border-round shadow-1 text-center">
4770
- <i
4771
- class="pi pi-info-circle text-color-secondary mb-3"
4772
- style="font-size: 3rem; display: block;"
4773
- ></i>
4774
- <p class="text-color-secondary m-0">
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">
4775
4768
  No actions available for this user.
4776
4769
  </p>
4777
4770
  </div>
4778
4771
  }
4779
4772
  }
4780
4773
  </div>
4781
- `, 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"] }]
4782
4775
  }], ctorParameters: () => [] });
4783
4776
 
4784
4777
  // Logic Builder Component
4785
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
+
4786
4860
  /**
4787
4861
  * IAM Routes Configuration
4788
4862
  *
@@ -4796,7 +4870,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4796
4870
  const IAM_ROUTES = [
4797
4871
  {
4798
4872
  path: '',
4799
- loadComponent: () => import('./flusys-ng-iam-iam-container.component-Chl5MDkV.mjs').then((m) => m.IamContainerComponent),
4873
+ loadComponent: () => import('./flusys-ng-iam-iam-container.component-Bn4kQtxW.mjs').then((m) => m.IamContainerComponent),
4800
4874
  children: [
4801
4875
  // Actions Management
4802
4876
  {
@@ -4804,15 +4878,15 @@ const IAM_ROUTES = [
4804
4878
  children: [
4805
4879
  {
4806
4880
  path: '',
4807
- loadComponent: () => import('./flusys-ng-iam-action-list-page.component-BpvewEGL.mjs').then((m) => m.ActionListPageComponent),
4881
+ loadComponent: () => import('./flusys-ng-iam-action-list-page.component-Daf93zpS.mjs').then((m) => m.ActionListPageComponent),
4808
4882
  },
4809
4883
  {
4810
4884
  path: 'new',
4811
- loadComponent: () => import('./flusys-ng-iam-action-form-page.component-0b9GwJqa.mjs').then((m) => m.ActionFormPageComponent),
4885
+ loadComponent: () => import('./flusys-ng-iam-action-form-page.component-C_BRrrWW.mjs').then((m) => m.ActionFormPageComponent),
4812
4886
  },
4813
4887
  {
4814
4888
  path: ':id',
4815
- loadComponent: () => import('./flusys-ng-iam-action-form-page.component-0b9GwJqa.mjs').then((m) => m.ActionFormPageComponent),
4889
+ loadComponent: () => import('./flusys-ng-iam-action-form-page.component-C_BRrrWW.mjs').then((m) => m.ActionFormPageComponent),
4816
4890
  },
4817
4891
  ],
4818
4892
  },
@@ -4822,22 +4896,22 @@ const IAM_ROUTES = [
4822
4896
  children: [
4823
4897
  {
4824
4898
  path: '',
4825
- loadComponent: () => import('./flusys-ng-iam-role-list-page.component-BgzxmHjk.mjs').then((m) => m.RoleListPageComponent),
4899
+ loadComponent: () => import('./flusys-ng-iam-role-list-page.component-CFly5KnH.mjs').then((m) => m.RoleListPageComponent),
4826
4900
  },
4827
4901
  {
4828
4902
  path: 'new',
4829
- loadComponent: () => import('./flusys-ng-iam-role-form-page.component-eZM1EPps.mjs').then((m) => m.RoleFormPageComponent),
4903
+ loadComponent: () => import('./flusys-ng-iam-role-form-page.component-ByNueI1a.mjs').then((m) => m.RoleFormPageComponent),
4830
4904
  },
4831
4905
  {
4832
4906
  path: ':id',
4833
- loadComponent: () => import('./flusys-ng-iam-role-form-page.component-eZM1EPps.mjs').then((m) => m.RoleFormPageComponent),
4907
+ loadComponent: () => import('./flusys-ng-iam-role-form-page.component-ByNueI1a.mjs').then((m) => m.RoleFormPageComponent),
4834
4908
  },
4835
4909
  ],
4836
4910
  },
4837
4911
  // Permissions Management (User permission assignment)
4838
4912
  {
4839
4913
  path: 'permissions',
4840
- loadComponent: () => import('./flusys-ng-iam-permission-page.component-e_RX5mky.mjs').then((m) => m.PermissionPageComponent),
4914
+ loadComponent: () => import('./flusys-ng-iam-permission-page.component-CmxOBJPu.mjs').then((m) => m.PermissionPageComponent),
4841
4915
  },
4842
4916
  // Default redirect to actions
4843
4917
  {
@@ -4855,5 +4929,5 @@ const IAM_ROUTES = [
4855
4929
  * Generated bundle index. Do not edit.
4856
4930
  */
4857
4931
 
4858
- 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 };
4859
- //# sourceMappingURL=flusys-ng-iam-flusys-ng-iam-C-MQjakK.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