@flusys/ng-iam 1.1.1-beta → 1.2.0-rc.1

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 (32) hide show
  1. package/README.md +44 -1027
  2. package/fesm2022/flusys-ng-iam-action-form-page.component-Vc5Toouu.mjs +502 -0
  3. package/fesm2022/flusys-ng-iam-action-form-page.component-Vc5Toouu.mjs.map +1 -0
  4. package/fesm2022/flusys-ng-iam-action-list-page.component-UGKclpFx.mjs +348 -0
  5. package/fesm2022/flusys-ng-iam-action-list-page.component-UGKclpFx.mjs.map +1 -0
  6. package/fesm2022/flusys-ng-iam-flusys-ng-iam-CrycDULz.mjs +4561 -0
  7. package/fesm2022/flusys-ng-iam-flusys-ng-iam-CrycDULz.mjs.map +1 -0
  8. package/fesm2022/flusys-ng-iam-iam-container.component-D0m8u6nu.mjs +105 -0
  9. package/fesm2022/flusys-ng-iam-iam-container.component-D0m8u6nu.mjs.map +1 -0
  10. package/fesm2022/flusys-ng-iam-permission-page.component-x88X4AgJ.mjs +143 -0
  11. package/fesm2022/flusys-ng-iam-permission-page.component-x88X4AgJ.mjs.map +1 -0
  12. package/fesm2022/flusys-ng-iam-role-form-page.component-InsxdriD.mjs +335 -0
  13. package/fesm2022/flusys-ng-iam-role-form-page.component-InsxdriD.mjs.map +1 -0
  14. package/fesm2022/flusys-ng-iam-role-list-page.component-8NGXCeSZ.mjs +389 -0
  15. package/fesm2022/flusys-ng-iam-role-list-page.component-8NGXCeSZ.mjs.map +1 -0
  16. package/fesm2022/flusys-ng-iam.mjs +1 -1
  17. package/package.json +11 -10
  18. package/types/flusys-ng-iam.d.ts +107 -686
  19. package/fesm2022/flusys-ng-iam-action-form-page.component-C1j10Qhw.mjs +0 -467
  20. package/fesm2022/flusys-ng-iam-action-form-page.component-C1j10Qhw.mjs.map +0 -1
  21. package/fesm2022/flusys-ng-iam-action-list-page.component-BCzSardO.mjs +0 -281
  22. package/fesm2022/flusys-ng-iam-action-list-page.component-BCzSardO.mjs.map +0 -1
  23. package/fesm2022/flusys-ng-iam-flusys-ng-iam-DISrddPh.mjs +0 -4940
  24. package/fesm2022/flusys-ng-iam-flusys-ng-iam-DISrddPh.mjs.map +0 -1
  25. package/fesm2022/flusys-ng-iam-iam-container.component-BkhqmzLi.mjs +0 -97
  26. package/fesm2022/flusys-ng-iam-iam-container.component-BkhqmzLi.mjs.map +0 -1
  27. package/fesm2022/flusys-ng-iam-permission-page.component-BSQFPt_N.mjs +0 -143
  28. package/fesm2022/flusys-ng-iam-permission-page.component-BSQFPt_N.mjs.map +0 -1
  29. package/fesm2022/flusys-ng-iam-role-form-page.component-Cqziu_BM.mjs +0 -314
  30. package/fesm2022/flusys-ng-iam-role-form-page.component-Cqziu_BM.mjs.map +0 -1
  31. package/fesm2022/flusys-ng-iam-role-list-page.component-BObCxHiB.mjs +0 -266
  32. package/fesm2022/flusys-ng-iam-role-list-page.component-BObCxHiB.mjs.map +0 -1
@@ -0,0 +1,4561 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, inject, signal, input, output, effect, Component, computed, DestroyRef } from '@angular/core';
3
+ import { ApiResourceService, TranslateService, PermissionValidatorService, AngularModule, TranslatePipe, ROLE_ACTION_PERMISSIONS, HasPermissionDirective, COMPANY_ACTION_PERMISSIONS, COMPANY_API_PROVIDER, USER_ROLE_PERMISSIONS, USER_PERMISSION_PROVIDER, UserSelectComponent, USER_ACTION_PERMISSIONS, PROFILE_PERMISSION_PROVIDER, permissionGuard, ACTION_PERMISSIONS, ROLE_PERMISSIONS, anyPermissionGuard, resolveTranslationModule, SHARED_MESSAGES } from '@flusys/ng-shared';
4
+ import { APP_CONFIG, getServiceUrl, BaseApiService, isCompanyFeatureEnabled } from '@flusys/ng-core';
5
+ import * as i2 from '@flusys/ng-ui';
6
+ import { ConfirmationService, MessageService, UiModule } from '@flusys/ng-ui';
7
+ import { HttpClient } from '@angular/common/http';
8
+ import { of, firstValueFrom, map as map$1 } from 'rxjs';
9
+ import { tap, catchError, map } from 'rxjs/operators';
10
+ import * as i1 from '@angular/common';
11
+ import { CommonModule } from '@angular/common';
12
+ import * as i1$1 from '@angular/forms';
13
+ import { FormsModule } from '@angular/forms';
14
+ import { LAYOUT_AUTH_STATE } from '@flusys/ng-layout';
15
+
16
+ /** Must match backend ActionType enum */
17
+ var ActionType;
18
+ (function (ActionType) {
19
+ ActionType["BACKEND"] = "backend";
20
+ ActionType["FRONTEND"] = "frontend";
21
+ ActionType["BOTH"] = "both";
22
+ })(ActionType || (ActionType = {}));
23
+
24
+ // Company interfaces
25
+
26
+ /** Maximum items for dropdown lists (companies, roles, users, branches) */
27
+ const MAX_DROPDOWN_ITEMS = 100;
28
+
29
+ const IAM_MESSAGES = {
30
+ // Action CRUD (API keys)
31
+ 'action.create.success': 'Action created successfully',
32
+ 'action.create.many.success': '{{count}} actions created successfully',
33
+ 'action.get.success': 'Action retrieved successfully',
34
+ 'action.get.by.ids.success': 'Actions retrieved successfully by Ids',
35
+ 'action.get.by.filter.success': 'Actions retrieved successfully by filter',
36
+ 'action.get.all.success': 'Actions retrieved successfully',
37
+ 'action.update.success': 'Action updated successfully',
38
+ 'action.update.many.success': '{{count}} actions updated successfully',
39
+ 'action.delete.success': 'Action deleted successfully',
40
+ 'action.restore.success': 'Action restored successfully',
41
+ // Role CRUD (API keys)
42
+ 'role.create.success': 'Role created successfully',
43
+ 'role.create.many.success': '{{count}} roles created successfully',
44
+ 'role.get.success': 'Role retrieved successfully',
45
+ 'role.get.by.ids.success': 'Roles retrieved successfully by Ids',
46
+ 'role.get.by.filter.success': 'Roles retrieved successfully by filter',
47
+ 'role.get.all.success': 'Roles retrieved successfully',
48
+ 'role.update.success': 'Role updated successfully',
49
+ 'role.update.many.success': '{{count}} roles updated successfully',
50
+ 'role.delete.success': 'Role deleted successfully',
51
+ 'role.restore.success': 'Role restored successfully',
52
+ // PERMISSION_OPERATION_MESSAGES (API keys)
53
+ 'permission.process.success': 'Successfully processed {{total}} items: {{added}} added, {{removed}} removed',
54
+ 'permission.user.required': 'User is required for {{method}}',
55
+ 'permission.already.exists': 'Permission already exists',
56
+ // ROLE_PERMISSION_MESSAGES (API keys)
57
+ 'role.permission.actions.success': 'Role actions retrieved successfully',
58
+ 'role.permission.user.roles.success': 'User roles retrieved successfully',
59
+ // USER_ACTION_PERMISSION_MESSAGES (API keys)
60
+ 'user.action.permission.get.success': 'User action permissions retrieved successfully',
61
+ // COMPANY_ACTION_PERMISSION_MESSAGES (API keys)
62
+ 'company.action.permission.get.success': 'Company action permissions retrieved successfully',
63
+ // MY_PERMISSION_MESSAGES (API keys)
64
+ 'my.permission.get.success': 'Permissions loaded successfully',
65
+ // IAM_MODE_MESSAGES (API keys)
66
+ 'iam.direct.mode.unavailable': 'Direct permission assignment not available in RBAC-only mode',
67
+ 'iam.rbac.mode.unavailable': 'Role-based permission assignment not available in direct-only mode',
68
+ 'iam.role.assignment.unavailable': 'Role assignment not available in direct-only mode',
69
+ // pages/iam-container/iam-container.component.ts
70
+ 'iam.title': 'Identity & Access Management',
71
+ 'iam.subtitle': 'Manage roles, permissions, and access control',
72
+ 'iam.action.title': 'Actions',
73
+ 'iam.role.title': 'Roles',
74
+ 'iam.permission.title': 'Permissions',
75
+ // pages/action/action-list-page.component.ts
76
+ 'iam.action.new': 'New Action',
77
+ 'iam.action.name': 'Action Name',
78
+ 'iam.action.code': 'Action Code',
79
+ 'iam.action.type': 'Type',
80
+ 'iam.action.no.actions': 'No actions found',
81
+ 'iam.action.type.backend': 'Backend',
82
+ 'iam.action.type.frontend': 'Frontend',
83
+ 'iam.action.type.both': 'Both',
84
+ 'iam.action.delete.title': 'Delete Action',
85
+ // pages/action/action-form-page.component.ts
86
+ 'iam.action.edit': 'Edit Action',
87
+ 'iam.action.name.placeholder': 'Enter action name',
88
+ 'iam.action.code.placeholder': 'Enter action code',
89
+ 'iam.action.parent': 'Parent Action',
90
+ 'iam.action.select.parent': 'Select Parent Action',
91
+ 'iam.action.type.backend.label': 'Backend Only',
92
+ 'iam.action.type.frontend.label': 'Frontend Only',
93
+ 'iam.action.type.both.label': 'Backend & Frontend',
94
+ // pages/role/role-list-page.component.ts
95
+ 'iam.company.title': 'Companies',
96
+ 'iam.role.new': 'New Role',
97
+ 'iam.role.name': 'Role Name',
98
+ 'iam.role.no.roles': 'No roles found',
99
+ 'iam.role.delete.title': 'Delete Role',
100
+ // pages/role/role-form-page.component.ts
101
+ 'iam.role.edit': 'Edit Role',
102
+ 'iam.role.name.placeholder': 'Enter role name',
103
+ // pages/permission/permission-page.component.ts
104
+ 'iam.permission.role.actions': 'Role Actions',
105
+ 'iam.permission.user.roles': 'User Roles',
106
+ 'iam.permission.user.actions': 'User Actions',
107
+ 'iam.permission.company.actions': 'Company Actions',
108
+ // components/logic-builder.component.ts
109
+ 'iam.logic.title': 'Permission Logic',
110
+ 'iam.logic.add.logic': 'Add Logic',
111
+ 'iam.logic.clear.logic': 'Clear Logic',
112
+ 'iam.logic.description': 'Define permission requirements using AND/OR logic with actions',
113
+ 'iam.logic.click.to.toggle': 'Click to toggle',
114
+ 'iam.logic.conditions': '{{count}} conditions',
115
+ 'iam.logic.select.action': 'Select Action...',
116
+ 'iam.logic.actions.available': '{{count}} available',
117
+ 'iam.logic.add.condition': 'Add Condition:',
118
+ 'iam.logic.group': 'Group',
119
+ 'iam.logic.action': 'Action',
120
+ // components/role-action-selector.component.ts
121
+ 'iam.permission.select.role': 'Select Role',
122
+ 'iam.permission.select.role.placeholder': 'Search and select a role',
123
+ 'iam.permission.action.permissions': 'Action Permissions',
124
+ 'iam.permission.actions.available': '{{count}} actions available',
125
+ 'iam.validation.warning.title': 'Validation Warning',
126
+ 'iam.validation.unmet.prerequisites.plural': '{{count}} selected actions have unmet prerequisites. Fix before saving or use auto-fix on save.',
127
+ 'iam.validation.unmet.prerequisites.singular': '{{count}} selected action has unmet prerequisites. Fix before saving or use auto-fix on save.',
128
+ 'iam.permission.requirements': 'Requirements',
129
+ 'iam.validation.unmet.prerequisites.tooltip': 'This action has unmet prerequisites and will fail validation on save',
130
+ 'iam.permission.has.prerequisites': 'Has prerequisites',
131
+ 'iam.permission.no.actions.available': 'No actions available',
132
+ 'iam.permission.no.actions.for.role': 'No actions available for this role',
133
+ 'iam.tooltip.remove.action': 'Click to remove',
134
+ 'iam.tooltip.add.action': 'Click to add (auto-selects required)',
135
+ 'iam.tooltip.assigned.to.role': 'Assigned to role',
136
+ 'iam.tooltip.click.to.remove.role': 'Click to remove',
137
+ 'iam.tooltip.click.to.assign.role': 'Click to assign to role',
138
+ // components/user-action-selector.component.ts
139
+ 'iam.permission.select.user': 'Select User',
140
+ 'iam.permission.select.user.placeholder': 'Search and select a user',
141
+ 'iam.permission.select.branch': 'Select Branch',
142
+ 'iam.permission.select.branch.placeholder': 'Search and select a branch',
143
+ 'iam.branch.permitted.count.plural': '{{count}} permitted branches in current company',
144
+ 'iam.branch.permitted.count': '{{count}} permitted branch in current company',
145
+ 'iam.permission.direct.action.permissions': 'Direct Action Permissions',
146
+ 'iam.permission.no.actions.for.user': 'No actions available for this user',
147
+ 'iam.tooltip.assigned.to.user': 'Assigned to user',
148
+ 'iam.tooltip.click.to.remove.user': 'Click to remove direct permission',
149
+ 'iam.tooltip.click.to.assign.user': 'Click to assign direct permission',
150
+ 'iam.permission.company.required': 'Please select a company first',
151
+ // components/company-action-selector.component.ts
152
+ 'iam.permission.select.company': 'Select Company',
153
+ 'iam.permission.select.company.placeholder': 'Search and select a company',
154
+ 'iam.permission.action.whitelist': 'Action Whitelist',
155
+ 'iam.permission.no.actions.for.company': 'No actions available for this company',
156
+ 'iam.tooltip.selected': 'Selected',
157
+ 'iam.tooltip.click.to.remove': 'Click to remove from company whitelist',
158
+ 'iam.tooltip.click.to.add': 'Click to add to company whitelist',
159
+ 'iam.permission.requires': 'requires',
160
+ 'iam.permission.prerequisite.validation.failed': 'Prerequisite Validation Failed',
161
+ 'iam.permission.prerequisite.error.message': 'The following actions have unmet prerequisites:',
162
+ 'iam.permission.auto.select.required': 'Auto-select Required',
163
+ 'iam.permission.actions.selected': 'Actions Selected',
164
+ 'iam.permission.auto.selected.prerequisites': 'Required prerequisites have been auto-selected. Click Save to apply changes.',
165
+ 'iam.permission.changes.reverted': 'Changes Reverted',
166
+ 'iam.permission.selection.reverted': 'Selection has been reverted to the initial state.',
167
+ // components/user-role-selector.component.ts
168
+ 'iam.permission.role.assignments': 'Role Assignments',
169
+ 'iam.permission.roles.available': '{{count}} roles available',
170
+ 'iam.pagination.roles.template': 'Showing {first} to {last} of {totalRecords} roles',
171
+ 'iam.permission.no.roles.available': 'No roles available',
172
+ 'iam.permission.no.roles.for.user': 'No roles available for this user',
173
+ 'iam.tooltip.click.to.remove.role.from.user': 'Click to remove role',
174
+ 'iam.tooltip.click.to.assign.role.to.user': 'Click to assign role to user',
175
+ // services/action-permission-logic.service.ts
176
+ 'iam.logic.unknown.action': 'Unknown Action',
177
+ 'iam.logic.validation.failed': 'Validation Failed',
178
+ 'iam.logic.validation.failed.message': 'The following actions have unmet prerequisites:',
179
+ 'iam.logic.validation.failed.prompt': 'Would you like to:',
180
+ 'iam.logic.auto.fix': 'Auto-fix (Remove Invalid)',
181
+ 'iam.logic.invalid.actions.removed': 'Invalid Actions Removed',
182
+ 'iam.logic.removed.actions.detail': 'Removed {{count}} action(s) with unmet prerequisites. You can now save.',
183
+ 'iam.logic.save.cancelled': 'Save Cancelled',
184
+ 'iam.logic.fix.prerequisites.manually': 'Please fix the prerequisites manually before saving',
185
+ 'iam.logic.prerequisites.satisfied': '[OK] Prerequisites Satisfied',
186
+ 'iam.logic.all.required.selected': 'All required actions are already selected.\nYou can safely add this action.',
187
+ 'iam.logic.prerequisites.required': 'Prerequisites Required ({{count}} missing)',
188
+ 'iam.logic.click.to.auto.select': 'Click to auto-select required actions',
189
+ 'iam.logic.select.action.label': 'Select Action',
190
+ 'iam.logic.auto.select.actions': 'Auto-select Actions',
191
+ 'iam.logic.action.selected.detail': 'Action selected successfully (prerequisites already satisfied)',
192
+ 'iam.logic.auto.selected.detail': 'Automatically selected {{count}} action(s) including prerequisites',
193
+ 'iam.logic.minimum.required': ' (minimum required)',
194
+ 'iam.logic.auto.select.prompt': 'Auto-select will choose {{count}} action(s){{suffix}}.',
195
+ 'iam.logic.missing.prerequisites': 'Missing Prerequisites',
196
+ 'iam.logic.requires.conditions': 'requires the following conditions to be satisfied:',
197
+ 'iam.logic.would.you.continue': 'Would you like to continue?',
198
+ 'iam.logic.actions.selected.summary': 'Actions Selected',
199
+ 'iam.logic.selection.cancelled': 'Selection Cancelled',
200
+ 'iam.logic.action.not.added': 'Action not added to whitelist',
201
+ 'iam.logic.required.by.actions': 'is required by the following action(s):',
202
+ 'iam.logic.alternatives.available': 'Alternative options available:',
203
+ 'iam.logic.switch.to.alternatives': 'Would you like to automatically switch to alternatives?',
204
+ 'iam.logic.remove.dependents': 'Removing this will also remove the dependent action(s).',
205
+ 'iam.logic.dependency.warning': 'Dependency Warning',
206
+ 'iam.logic.use.alternatives': 'Use Alternatives',
207
+ 'iam.logic.remove.all': 'Remove All',
208
+ 'iam.logic.alternatives.applied': 'Alternatives Applied',
209
+ 'iam.logic.switched.to.alternatives': 'Automatically switched to alternative action(s)',
210
+ 'iam.logic.actions.removed': 'Actions Removed',
211
+ 'iam.logic.removed.with.dependents': 'Removed {{name}} and {{count}} dependent action(s)',
212
+ 'iam.logic.cancelled': 'Cancelled',
213
+ 'iam.logic.no.changes.made': 'No changes made',
214
+ 'iam.logic.satisfied': '(satisfied)',
215
+ 'iam.logic.missing': '(missing)',
216
+ 'iam.logic.invalid.node': 'Invalid logic node',
217
+ 'iam.logic.invalid': '(invalid)',
218
+ };
219
+
220
+ class RoleApiService extends ApiResourceService {
221
+ constructor() {
222
+ super('roles', 'iam');
223
+ }
224
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RoleApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
225
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RoleApiService, providedIn: 'root' });
226
+ }
227
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RoleApiService, decorators: [{
228
+ type: Injectable,
229
+ args: [{ providedIn: 'root' }]
230
+ }], ctorParameters: () => [] });
231
+
232
+ class ActionApiService extends ApiResourceService {
233
+ appConfig = inject(APP_CONFIG);
234
+ constructor() {
235
+ super('actions', 'iam');
236
+ }
237
+ /** Get actions filtered by company whitelist for permission assignment */
238
+ getActionsForPermission() {
239
+ const iamBaseUrl = getServiceUrl(this.appConfig, 'iam');
240
+ return this.http.post(`${iamBaseUrl}/actions/tree-for-permission`, {});
241
+ }
242
+ /** Get actions in hierarchical tree structure */
243
+ getTree(search, isActive, withDeleted) {
244
+ const body = {};
245
+ if (search?.trim())
246
+ body.search = search.trim();
247
+ if (isActive !== undefined)
248
+ body.isActive = isActive;
249
+ if (withDeleted !== undefined)
250
+ body.withDeleted = withDeleted;
251
+ const iamBaseUrl = getServiceUrl(this.appConfig, 'iam');
252
+ return this.http.post(`${iamBaseUrl}/actions/tree`, body);
253
+ }
254
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ActionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
255
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ActionApiService, providedIn: 'root' });
256
+ }
257
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ActionApiService, decorators: [{
258
+ type: Injectable,
259
+ args: [{ providedIn: 'root' }]
260
+ }], ctorParameters: () => [] });
261
+
262
+ function extractRequiredActionIds(logic) {
263
+ const actionIds = new Set();
264
+ if (logic) {
265
+ collectActionIds(logic, actionIds);
266
+ }
267
+ return actionIds;
268
+ }
269
+ function collectActionIds(node, actionIds) {
270
+ if (node.type === 'action' && node.actionId) {
271
+ actionIds.add(node.actionId);
272
+ }
273
+ else if (node.type === 'group' && node.children) {
274
+ node.children.forEach((child) => collectActionIds(child, actionIds));
275
+ }
276
+ }
277
+ function validateActionPrerequisites(action, selectedActionIds, allActions) {
278
+ if (!action.permissionLogic) {
279
+ return { valid: true, missingActions: [] };
280
+ }
281
+ const result = evaluateLogicNodeWithMissing(action.permissionLogic, selectedActionIds);
282
+ if (result.valid) {
283
+ return { valid: true, missingActions: [] };
284
+ }
285
+ const missingActions = allActions.filter((a) => a.id && result.missingActionIds.has(a.id));
286
+ return { valid: false, missingActions };
287
+ }
288
+ function evaluateLogicNodeWithMissing(node, selectedActionIds) {
289
+ if (node.type === 'action' && node.actionId) {
290
+ const valid = selectedActionIds.has(node.actionId);
291
+ return { valid, missingActionIds: valid ? new Set() : new Set([node.actionId]) };
292
+ }
293
+ if (node.type === 'group' && node.children?.length) {
294
+ const operator = node.operator || 'AND';
295
+ if (operator === 'AND') {
296
+ const missingIds = new Set();
297
+ let allValid = true;
298
+ for (const child of node.children) {
299
+ const childResult = evaluateLogicNodeWithMissing(child, selectedActionIds);
300
+ if (!childResult.valid) {
301
+ allValid = false;
302
+ childResult.missingActionIds.forEach((id) => missingIds.add(id));
303
+ }
304
+ }
305
+ return { valid: allValid, missingActionIds: missingIds };
306
+ }
307
+ // OR operator
308
+ for (const child of node.children) {
309
+ const childResult = evaluateLogicNodeWithMissing(child, selectedActionIds);
310
+ if (childResult.valid) {
311
+ return { valid: true, missingActionIds: new Set() };
312
+ }
313
+ }
314
+ const firstChildResult = evaluateLogicNodeWithMissing(node.children[0], selectedActionIds);
315
+ return { valid: false, missingActionIds: firstChildResult.missingActionIds };
316
+ }
317
+ return { valid: true, missingActionIds: new Set() };
318
+ }
319
+
320
+ /** Shared service for smart dependency management across action selectors */
321
+ class ActionPermissionLogicService {
322
+ confirmationService = inject(ConfirmationService);
323
+ messageService = inject(MessageService);
324
+ translateAdapter = inject(TranslateService, {
325
+ optional: true,
326
+ });
327
+ t(key, variables) {
328
+ return this.translateAdapter?.translate(key, variables) ?? key;
329
+ }
330
+ /** Handle checking an action with prerequisite validation (recursive deep scan) */
331
+ handleCheck(action, currentSelection, allActions, onUpdate, onCancel) {
332
+ // Validate prerequisites with RECURSIVE DEEP SCAN
333
+ if (action.permissionLogic) {
334
+ const selectedActionIds = this.getSelectedIds(currentSelection);
335
+ const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
336
+ if (!validationResult.valid) {
337
+ // Store previous state
338
+ const previousState = { ...currentSelection };
339
+ // Temporarily check the box
340
+ const tempMap = { ...currentSelection };
341
+ tempMap[action.id] = true;
342
+ onUpdate(tempMap);
343
+ // CRITICAL: Recursively collect ALL missing prerequisites (deep scan)
344
+ const allMissingActions = this.getAllMissingPrerequisitesRecursive(action, currentSelection, allActions);
345
+ // Show prerequisite dialog with COMPLETE dependency chain
346
+ this.showPrerequisiteDialog(action, allMissingActions, currentSelection, allActions, previousState, onUpdate, onCancel);
347
+ return;
348
+ }
349
+ }
350
+ // All prerequisites satisfied, allow selection
351
+ const selMap = { ...currentSelection };
352
+ selMap[action.id] = true;
353
+ onUpdate(selMap);
354
+ }
355
+ /** Handle unchecking an action with dependency detection */
356
+ handleUncheck(action, currentSelection, allActions, onUpdate) {
357
+ const affectedActions = this.findActionsDependingOn(action.id, currentSelection, allActions);
358
+ if (affectedActions.length > 0) {
359
+ this.showDependencyDialog(action, affectedActions, currentSelection, allActions, onUpdate);
360
+ return;
361
+ }
362
+ // No dependencies, safe to uncheck
363
+ const selMap = { ...currentSelection };
364
+ selMap[action.id] = false;
365
+ onUpdate(selMap);
366
+ }
367
+ /** Check if an action has unmet prerequisites */
368
+ hasUnmetPrerequisites(action, currentSelection, allActions) {
369
+ const isSelected = currentSelection[action.id];
370
+ if (!isSelected || !action.permissionLogic) {
371
+ return false;
372
+ }
373
+ const selectedActionIds = this.getSelectedIds(currentSelection);
374
+ selectedActionIds.delete(action.id);
375
+ const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
376
+ return !validationResult.valid;
377
+ }
378
+ /** Get all selected actions that have unmet prerequisites */
379
+ getActionsWithUnmetPrerequisites(currentSelection, allActions) {
380
+ const selectedActions = allActions.filter((a) => currentSelection[a.id]);
381
+ return selectedActions.filter((action) => this.hasUnmetPrerequisites(action, currentSelection, allActions));
382
+ }
383
+ /** Show validation error dialog with auto-fix options */
384
+ showValidationErrorDialog(invalidActions, currentSelection, allActions, onUpdate) {
385
+ const errorList = invalidActions
386
+ .map((action) => {
387
+ const selectedActionIds = this.getSelectedIds(currentSelection);
388
+ selectedActionIds.delete(action.id);
389
+ const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
390
+ const sanitizedActionName = this.sanitizeHtml(action.name ?? this.t('iam.logic.unknown.action'));
391
+ const missingNames = validationResult.missingActions
392
+ .map((a) => this.sanitizeHtml(a.name ?? this.t('iam.logic.unknown.action')))
393
+ .join(', ');
394
+ return `<li><strong>${sanitizedActionName}</strong> requires: ${missingNames}</li>`;
395
+ })
396
+ .join('');
397
+ this.confirmationService.confirm({
398
+ header: this.t('iam.logic.validation.failed'),
399
+ message: `${this.t('iam.logic.validation.failed.message')}<br/><br/><ul class="pl-4">${errorList}</ul><br/>${this.t('iam.logic.validation.failed.prompt')}`,
400
+ icon: 'exclamation-triangle',
401
+ acceptLabel: this.t('iam.logic.auto.fix'),
402
+ rejectLabel: this.t('shared.cancel'),
403
+ acceptIcon: 'trash',
404
+ rejectIcon: 'times',
405
+ acceptButtonStyleClass: 'p-button-warning',
406
+ closeOnEscape: false,
407
+ closable: false,
408
+ accept: () => {
409
+ // Remove all invalid actions
410
+ const selMap = { ...currentSelection };
411
+ invalidActions.forEach((action) => {
412
+ selMap[action.id] = false;
413
+ });
414
+ onUpdate(selMap);
415
+ this.messageService.add({
416
+ severity: 'warn',
417
+ summary: this.t('iam.logic.invalid.actions.removed'),
418
+ detail: this.t('iam.logic.removed.actions.detail', {
419
+ count: invalidActions.length,
420
+ }),
421
+ });
422
+ },
423
+ reject: () => {
424
+ this.messageService.add({
425
+ severity: 'info',
426
+ summary: this.t('iam.logic.save.cancelled'),
427
+ detail: this.t('iam.logic.fix.prerequisites.manually'),
428
+ });
429
+ },
430
+ });
431
+ }
432
+ /** Get prerequisite description for tooltip display */
433
+ getPrerequisiteTooltip(action, currentSelection, allActions) {
434
+ if (!action.permissionLogic) {
435
+ return '';
436
+ }
437
+ const selectedActionIds = this.getSelectedIds(currentSelection);
438
+ const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
439
+ if (validationResult.valid) {
440
+ return `${this.t('iam.logic.prerequisites.satisfied')}\n\n${this.t('iam.logic.all.required.selected')}`;
441
+ }
442
+ const allMissingActions = this.getAllMissingPrerequisitesRecursive(action, currentSelection, allActions);
443
+ const logicTree = this.buildTooltipLogicTree(action.permissionLogic, currentSelection, allActions, 0);
444
+ const missingCount = allMissingActions.length;
445
+ const header = `⚠ ${this.t('iam.logic.prerequisites.required', { count: missingCount })}`;
446
+ const hint = `\n\n💡 ${this.t('iam.logic.click.to.auto.select')}`;
447
+ return `${header}\n\n${logicTree}${hint}`;
448
+ }
449
+ /** Build dynamic logic tree message with AND/OR operators and nesting */
450
+ buildLogicMessage(logic, _missingActions, allActions, currentSelection) {
451
+ const selectedIds = currentSelection
452
+ ? this.getSelectedIds(currentSelection)
453
+ : new Set();
454
+ const actionMap = new Map(allActions.map((a) => [a.id, a]));
455
+ return this.formatLogicNode(logic, selectedIds, actionMap, 0);
456
+ }
457
+ /** Extract selected action IDs from selection map */
458
+ getSelectedIds(currentSelection) {
459
+ return new Set(Object.keys(currentSelection).filter((id) => currentSelection[id]));
460
+ }
461
+ sanitizeHtml(text) {
462
+ return text
463
+ .replace(/&/g, '&amp;')
464
+ .replace(/</g, '&lt;')
465
+ .replace(/>/g, '&gt;')
466
+ .replace(/"/g, '&quot;')
467
+ .replace(/'/g, '&#x27;');
468
+ }
469
+ /** Recursively collect ALL missing prerequisites at all dependency levels */
470
+ getAllMissingPrerequisitesRecursive(action, currentSelection, allActions) {
471
+ if (!action.id)
472
+ return [];
473
+ const visited = new Set();
474
+ const allMissing = new Map();
475
+ const stack = new Set();
476
+ const collectMissing = (targetAction, depth = 0) => {
477
+ if (!targetAction.permissionLogic || !targetAction.id)
478
+ return;
479
+ if (depth > 50) {
480
+ return;
481
+ }
482
+ if (visited.has(targetAction.id))
483
+ return;
484
+ if (stack.has(targetAction.id)) {
485
+ return;
486
+ }
487
+ visited.add(targetAction.id);
488
+ stack.add(targetAction.id);
489
+ const selectedActionIds = this.getSelectedIds(currentSelection);
490
+ const result = validateActionPrerequisites(targetAction, selectedActionIds, allActions);
491
+ if (!result.valid) {
492
+ result.missingActions.forEach((missingAction) => {
493
+ if (missingAction.id && missingAction.id !== action.id) {
494
+ allMissing.set(missingAction.id, missingAction);
495
+ collectMissing(missingAction, depth + 1);
496
+ }
497
+ });
498
+ }
499
+ stack.delete(targetAction.id);
500
+ };
501
+ collectMissing(action);
502
+ return Array.from(allMissing.values());
503
+ }
504
+ showPrerequisiteDialog(action, missingActions, currentSelection, allActions, previousState, onUpdate, onCancel) {
505
+ const logicMessage = action.permissionLogic
506
+ ? this.buildLogicMessage(action.permissionLogic, missingActions, allActions, currentSelection)
507
+ : this.buildSimpleMessage(missingActions);
508
+ const actionsToSelect = action.permissionLogic
509
+ ? this.calculateSmartSelection(action.permissionLogic, missingActions, currentSelection, allActions)
510
+ : missingActions;
511
+ const selectionCount = actionsToSelect.length;
512
+ const sanitizedActionName = this.sanitizeHtml(action.name ?? this.t('iam.logic.unknown.action'));
513
+ const acceptLabel = selectionCount === 0
514
+ ? this.t('iam.logic.select.action.label')
515
+ : this.t('iam.logic.auto.select.actions');
516
+ const detailMessage = selectionCount === 0
517
+ ? this.t('iam.logic.action.selected.detail')
518
+ : this.t('iam.logic.auto.selected.detail', { count: selectionCount });
519
+ const suffix = selectionCount < missingActions.length
520
+ ? this.t('iam.logic.minimum.required')
521
+ : '';
522
+ const autoSelectPrompt = this.t('iam.logic.auto.select.prompt', {
523
+ count: selectionCount,
524
+ suffix,
525
+ });
526
+ this.confirmationService.confirm({
527
+ header: this.t('iam.logic.missing.prerequisites'),
528
+ message: `<strong>${sanitizedActionName}</strong> ${this.t('iam.logic.requires.conditions')}<br/><br/>${logicMessage}<br/><br/><strong>${autoSelectPrompt}</strong><br/>${this.t('iam.logic.would.you.continue')}`,
529
+ icon: 'exclamation-triangle',
530
+ acceptLabel,
531
+ rejectLabel: this.t('shared.cancel'),
532
+ acceptIcon: 'check',
533
+ rejectIcon: 'times',
534
+ closeOnEscape: false,
535
+ closable: false,
536
+ accept: () => {
537
+ const selMap = { ...currentSelection };
538
+ selMap[action.id] = true;
539
+ actionsToSelect.forEach((a) => {
540
+ selMap[a.id] = true;
541
+ });
542
+ onUpdate(selMap);
543
+ this.messageService.add({
544
+ severity: 'info',
545
+ summary: this.t('iam.logic.actions.selected.summary'),
546
+ detail: detailMessage,
547
+ });
548
+ },
549
+ reject: () => {
550
+ onCancel(previousState);
551
+ this.messageService.add({
552
+ severity: 'info',
553
+ summary: this.t('iam.logic.selection.cancelled'),
554
+ detail: this.t('iam.logic.action.not.added'),
555
+ });
556
+ },
557
+ });
558
+ }
559
+ showDependencyDialog(action, affectedActions, currentSelection, _allActions, onUpdate) {
560
+ if (!action.id)
561
+ return;
562
+ const alternatives = this.findAlternatives(action.id, affectedActions, currentSelection);
563
+ const sanitizedActionName = this.sanitizeHtml(action.name ?? this.t('iam.logic.unknown.action'));
564
+ let message = `<strong>${sanitizedActionName}</strong> ${this.t('iam.logic.required.by.actions')}<br/><br/><ul class="pl-4">`;
565
+ affectedActions.forEach((a) => {
566
+ const sanitizedName = this.sanitizeHtml(a.name ?? this.t('iam.logic.unknown.action'));
567
+ message += `<li>${sanitizedName}</li>`;
568
+ });
569
+ message += '</ul><br/>';
570
+ if (alternatives.length > 0) {
571
+ message += `<strong>${this.t('iam.logic.alternatives.available')}</strong><br/><br/>`;
572
+ message += `<em>${this.t('iam.logic.switch.to.alternatives')}</em>`;
573
+ }
574
+ else {
575
+ message += `<em>${this.t('iam.logic.remove.dependents')}</em><br/><br/>`;
576
+ message += this.t('iam.logic.would.you.continue');
577
+ }
578
+ this.confirmationService.confirm({
579
+ header: this.t('iam.logic.dependency.warning'),
580
+ message,
581
+ icon: 'exclamation-triangle',
582
+ acceptLabel: alternatives.length > 0
583
+ ? this.t('iam.logic.use.alternatives')
584
+ : this.t('iam.logic.remove.all'),
585
+ rejectLabel: this.t('shared.cancel'),
586
+ acceptIcon: 'check',
587
+ rejectIcon: 'times',
588
+ closeOnEscape: false,
589
+ closable: false,
590
+ accept: () => {
591
+ const selMap = { ...currentSelection };
592
+ if (alternatives.length > 0) {
593
+ // Apply alternatives
594
+ alternatives.forEach(({ alternativeId }) => {
595
+ selMap[alternativeId] = true;
596
+ });
597
+ selMap[action.id] = false;
598
+ this.messageService.add({
599
+ severity: 'success',
600
+ summary: this.t('iam.logic.alternatives.applied'),
601
+ detail: this.t('iam.logic.switched.to.alternatives'),
602
+ });
603
+ }
604
+ else {
605
+ // Remove action and all dependents
606
+ selMap[action.id] = false;
607
+ affectedActions.forEach((a) => {
608
+ selMap[a.id] = false;
609
+ });
610
+ this.messageService.add({
611
+ severity: 'warn',
612
+ summary: this.t('iam.logic.actions.removed'),
613
+ detail: this.t('iam.logic.removed.with.dependents', {
614
+ name: action.name ?? '',
615
+ count: affectedActions.length,
616
+ }),
617
+ });
618
+ }
619
+ onUpdate(selMap);
620
+ },
621
+ reject: () => {
622
+ this.messageService.add({
623
+ severity: 'info',
624
+ summary: this.t('iam.logic.cancelled'),
625
+ detail: this.t('iam.logic.no.changes.made'),
626
+ });
627
+ },
628
+ });
629
+ }
630
+ findActionsDependingOn(actionId, currentSelection, allActions) {
631
+ const selectedActions = allActions.filter((a) => currentSelection[a.id] && a.id !== actionId);
632
+ return selectedActions.filter((action) => {
633
+ if (!action.permissionLogic)
634
+ return false;
635
+ return this.doesLogicDependOn(action.permissionLogic, actionId);
636
+ });
637
+ }
638
+ doesLogicDependOn(node, targetActionId) {
639
+ if (node.type === 'action') {
640
+ return node.actionId === targetActionId;
641
+ }
642
+ if (node.type === 'group' && node.children) {
643
+ return node.children.some((child) => this.doesLogicDependOn(child, targetActionId));
644
+ }
645
+ return false;
646
+ }
647
+ findAlternatives(removingActionId, affectedActions, currentSelection) {
648
+ const alternatives = [];
649
+ for (const affectedAction of affectedActions) {
650
+ if (!affectedAction.permissionLogic)
651
+ continue;
652
+ const alternative = this.findAlternativeInLogic(affectedAction.permissionLogic, removingActionId, currentSelection);
653
+ if (alternative) {
654
+ alternatives.push({ alternativeId: alternative });
655
+ }
656
+ }
657
+ return alternatives;
658
+ }
659
+ findAlternativeInLogic(node, removingActionId, currentSelection) {
660
+ if (node.type === 'group' && node.operator === 'OR' && node.children) {
661
+ const hasRemoving = node.children.some((child) => child.type === 'action' && child.actionId === removingActionId);
662
+ if (hasRemoving) {
663
+ for (const child of node.children) {
664
+ if (child.type === 'action' &&
665
+ child.actionId &&
666
+ child.actionId !== removingActionId &&
667
+ !currentSelection[child.actionId]) {
668
+ return child.actionId;
669
+ }
670
+ }
671
+ }
672
+ }
673
+ if (node.type === 'group' && node.children) {
674
+ for (const child of node.children) {
675
+ const alternative = this.findAlternativeInLogic(child, removingActionId, currentSelection);
676
+ if (alternative)
677
+ return alternative;
678
+ }
679
+ }
680
+ return null;
681
+ }
682
+ calculateSmartSelection(logic, missingActions, currentSelection, allActions) {
683
+ const missingIds = new Set(missingActions.map((a) => a.id));
684
+ const actionMap = new Map(allActions.map((a) => [a.id, a]));
685
+ const selectedIds = this.getSelectedIds(currentSelection);
686
+ const requiredIds = this.findRequiredActionIds(logic, missingIds, selectedIds);
687
+ return Array.from(requiredIds)
688
+ .map((id) => actionMap.get(id))
689
+ .filter((a) => !!a);
690
+ }
691
+ findRequiredActionIds(node, missingIds, selectedIds) {
692
+ if (node.type === 'action' && node.actionId) {
693
+ if (missingIds.has(node.actionId)) {
694
+ return new Set([node.actionId]);
695
+ }
696
+ return new Set();
697
+ }
698
+ if (node.type === 'group' && node.children && node.children.length > 0) {
699
+ const operator = node.operator || 'AND';
700
+ if (operator === 'AND') {
701
+ const required = new Set();
702
+ for (const child of node.children) {
703
+ const childRequired = this.findRequiredActionIds(child, missingIds, selectedIds);
704
+ childRequired.forEach((id) => required.add(id));
705
+ }
706
+ return required;
707
+ }
708
+ else {
709
+ let minRequired = null;
710
+ let minSize = Infinity;
711
+ for (const child of node.children) {
712
+ const childRequired = this.findRequiredActionIds(child, missingIds, selectedIds);
713
+ if (childRequired.size === 0) {
714
+ return new Set();
715
+ }
716
+ if (childRequired.size < minSize) {
717
+ minSize = childRequired.size;
718
+ minRequired = childRequired;
719
+ }
720
+ }
721
+ return minRequired || new Set();
722
+ }
723
+ }
724
+ return new Set();
725
+ }
726
+ buildSimpleMessage(missingActions) {
727
+ return `<ul class="pl-4">${missingActions.map((a) => `<li>${this.sanitizeHtml(a.name ?? this.t('iam.logic.unknown.action'))}</li>`).join('')}</ul>`;
728
+ }
729
+ formatLogicNode(node, selectedIds, actionMap, depth) {
730
+ const indent = '&nbsp;&nbsp;&nbsp;&nbsp;'.repeat(depth);
731
+ if (node.type === 'action' && node.actionId) {
732
+ const action = actionMap.get(node.actionId);
733
+ const actionName = this.sanitizeHtml(action?.name || this.t('iam.logic.unknown.action'));
734
+ const isSelected = selectedIds.has(node.actionId);
735
+ if (isSelected) {
736
+ return `${indent}<span style="color: #10b981; font-weight: 500;">✓ ${actionName}</span> <em style="color: #059669;">${this.t('iam.logic.satisfied')}</em>`;
737
+ }
738
+ else {
739
+ return `${indent}<span style="color: #ef4444;">✗ ${actionName}</span> <em style="color: #dc2626;">${this.t('iam.logic.missing')}</em>`;
740
+ }
741
+ }
742
+ if (node.type === 'group' && node.children) {
743
+ const operator = node.operator || 'AND';
744
+ const operatorColor = operator === 'AND' ? '#3b82f6' : '#8b5cf6';
745
+ const lines = [];
746
+ lines.push(`${indent}<strong style="color: ${operatorColor};">${operator}</strong> <span style="color: #6b7280;">(${node.children.length} condition${node.children.length !== 1 ? 's' : ''})</span>`);
747
+ node.children.forEach((child, index) => {
748
+ const childHtml = this.formatLogicNode(child, selectedIds, actionMap, depth + 1);
749
+ lines.push(childHtml);
750
+ if (node.children && index < node.children.length - 1) {
751
+ lines.push(`${indent}&nbsp;&nbsp;<em style="color: ${operatorColor}; font-size: 0.75rem;">${operator.toLowerCase()}</em>`);
752
+ }
753
+ });
754
+ return lines.join('<br/>');
755
+ }
756
+ return `${indent}<em style="color: #9ca3af;">${this.t('iam.logic.invalid.node')}</em>`;
757
+ }
758
+ /** Build clean text-based logic tree for tooltips */
759
+ buildTooltipLogicTree(node, currentSelection, allActions, depth) {
760
+ const indent = ' '.repeat(depth);
761
+ const selectedIds = this.getSelectedIds(currentSelection);
762
+ const actionMap = new Map(allActions.map((a) => [a.id, a]));
763
+ if (node.type === 'action' && node.actionId) {
764
+ const action = actionMap.get(node.actionId);
765
+ const actionName = action?.name || this.t('iam.logic.unknown.action');
766
+ const isSelected = selectedIds.has(node.actionId);
767
+ if (isSelected) {
768
+ return `${indent}✓ ${actionName}`;
769
+ }
770
+ else {
771
+ return `${indent}✗ ${actionName}`;
772
+ }
773
+ }
774
+ if (node.type === 'group' && node.children) {
775
+ const operator = node.operator || 'AND';
776
+ const lines = [];
777
+ lines.push(`${indent}${operator}:`);
778
+ node.children.forEach((child, index) => {
779
+ const childText = this.buildTooltipLogicTree(child, currentSelection, allActions, depth + 1);
780
+ lines.push(childText);
781
+ if (node.children &&
782
+ index < node.children.length - 1 &&
783
+ operator === 'OR') {
784
+ lines.push(`${indent} ${operator.toLowerCase()}`);
785
+ }
786
+ });
787
+ return lines.join('\n');
788
+ }
789
+ return `${indent}${this.t('iam.logic.invalid')}`;
790
+ }
791
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ActionPermissionLogicService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
792
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ActionPermissionLogicService, providedIn: 'root' });
793
+ }
794
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ActionPermissionLogicService, decorators: [{
795
+ type: Injectable,
796
+ args: [{ providedIn: 'root' }]
797
+ }] });
798
+
799
+ class PermissionApiService extends BaseApiService {
800
+ constructor() {
801
+ super('iam');
802
+ }
803
+ assignUserActions(data) {
804
+ return this.http.post(this.getUrl('permissions/user-actions/assign'), data);
805
+ }
806
+ getUserActions(userId, query) {
807
+ return this.http.post(this.getUrl('permissions/get-user-actions'), { userId, branchId: query?.branchId });
808
+ }
809
+ assignUserRoles(data) {
810
+ return this.http.post(this.getUrl('permissions/user-roles/assign'), data);
811
+ }
812
+ getUserRoles(userId, query) {
813
+ return this.http.post(this.getUrl('permissions/get-user-roles'), { userId, branchId: query?.branchId });
814
+ }
815
+ assignRoleActions(data) {
816
+ return this.http.post(this.getUrl('permissions/role-actions/assign'), data);
817
+ }
818
+ getRoleActions(roleId) {
819
+ return this.http.post(this.getUrl('permissions/get-role-actions'), { roleId });
820
+ }
821
+ assignCompanyActions(data) {
822
+ return this.http.post(this.getUrl('permissions/company-actions/assign'), data);
823
+ }
824
+ getCompanyActions(companyId) {
825
+ return this.http.post(this.getUrl('permissions/get-company-actions'), { companyId });
826
+ }
827
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: PermissionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
828
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: PermissionApiService, providedIn: 'root' });
829
+ }
830
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: PermissionApiService, decorators: [{
831
+ type: Injectable,
832
+ args: [{ providedIn: 'root' }]
833
+ }], ctorParameters: () => [] });
834
+
835
+ /**
836
+ * My Permissions API Service
837
+ * Get current user's complete permissions
838
+ * Endpoint: POST /iam/permissions/my-permissions
839
+ * Always enabled
840
+ */
841
+ class MyPermissionsApiService {
842
+ http = inject(HttpClient);
843
+ appConfig = inject(APP_CONFIG);
844
+ baseUrl = `${getServiceUrl(this.appConfig, 'iam')}/permissions`;
845
+ /**
846
+ * Get current user's complete permissions
847
+ * POST /iam/permissions/my-permissions
848
+ * Returns menus, frontend actions, and cached endpoint count
849
+ * @param dto Optional filter by parent codes (only affects frontend actions)
850
+ */
851
+ getMyPermissions(dto = {}) {
852
+ return this.http.post(`${this.baseUrl}/my-permissions`, dto);
853
+ }
854
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: MyPermissionsApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
855
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: MyPermissionsApiService, providedIn: 'root' });
856
+ }
857
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: MyPermissionsApiService, decorators: [{
858
+ type: Injectable,
859
+ args: [{
860
+ providedIn: 'root',
861
+ }]
862
+ }] });
863
+
864
+ class PermissionStateService {
865
+ permissionApi = inject(MyPermissionsApiService);
866
+ permissionValidator = inject(PermissionValidatorService);
867
+ _permissions = signal(null, /* @ts-ignore */
868
+ ...(ngDevMode ? [{ debugName: "_permissions" }] : /* istanbul ignore next */ []));
869
+ permissions = this._permissions.asReadonly();
870
+ _isLoading = signal(false, /* @ts-ignore */
871
+ ...(ngDevMode ? [{ debugName: "_isLoading" }] : /* istanbul ignore next */ []));
872
+ isLoading = this._isLoading.asReadonly();
873
+ loadPermissions(dto) {
874
+ this._isLoading.set(true);
875
+ return this.permissionApi.getMyPermissions(dto).pipe(tap((response) => {
876
+ this._permissions.set(response.data ?? null);
877
+ this._isLoading.set(false);
878
+ const actionCodes = response.data?.frontendActions.map((a) => a.code) ?? [];
879
+ this.permissionValidator.setPermissions(actionCodes);
880
+ }), catchError(() => {
881
+ this._permissions.set(null);
882
+ this._isLoading.set(false);
883
+ this.permissionValidator.clearPermissions();
884
+ return of(void 0);
885
+ }), map(() => void 0));
886
+ }
887
+ isLoaded() {
888
+ return this._permissions() !== null && this.permissionValidator.isPermissionsLoaded();
889
+ }
890
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: PermissionStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
891
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: PermissionStateService, providedIn: 'root' });
892
+ }
893
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: PermissionStateService, decorators: [{
894
+ type: Injectable,
895
+ args: [{ providedIn: 'root' }]
896
+ }] });
897
+
898
+ // Role services
899
+
900
+ function toLogicNode(node) {
901
+ if (node.type === 'action') {
902
+ return { type: 'action', actionId: node.actionId };
903
+ }
904
+ return {
905
+ type: 'group',
906
+ operator: node.operator,
907
+ children: node.children?.map(toLogicNode) ?? [],
908
+ };
909
+ }
910
+ function toBuilderNode(node) {
911
+ if (node.type === 'action') {
912
+ return createActionNode(node.actionId);
913
+ }
914
+ return {
915
+ ...createGroupNode(node.operator),
916
+ children: node.children?.map(toBuilderNode) ?? [],
917
+ };
918
+ }
919
+ function createGroupNode(operator = 'AND') {
920
+ return { id: crypto.randomUUID(), type: 'group', operator, children: [] };
921
+ }
922
+ function createActionNode(actionId = '') {
923
+ return { id: crypto.randomUUID(), type: 'action', actionId };
924
+ }
925
+ /** Visual builder for AND/OR permission logic trees */
926
+ class LogicBuilderComponent {
927
+ logic = input(null, /* @ts-ignore */
928
+ ...(ngDevMode ? [{ debugName: "logic" }] : /* istanbul ignore next */ []));
929
+ actions = input([], /* @ts-ignore */
930
+ ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
931
+ logicChange = output();
932
+ /** Internal builder tree state (private writable + public readonly pattern) */
933
+ _builderTree = signal(null, /* @ts-ignore */
934
+ ...(ngDevMode ? [{ debugName: "_builderTree" }] : /* istanbul ignore next */ []));
935
+ builderLogic = this._builderTree.asReadonly();
936
+ constructor() {
937
+ effect(() => {
938
+ const logic = this.logic();
939
+ if (!logic) {
940
+ this._builderTree.set(null);
941
+ }
942
+ else if (!this._builderTree()) {
943
+ this._builderTree.set(toBuilderNode(logic));
944
+ }
945
+ });
946
+ }
947
+ initializeLogic() {
948
+ this.updateTreeAndEmit(createGroupNode());
949
+ }
950
+ clearLogic() {
951
+ this._builderTree.set(null);
952
+ this.logicChange.emit(null);
953
+ }
954
+ toggleOperator(nodeId) {
955
+ this.updateNode(nodeId, (node) => ({
956
+ ...node,
957
+ operator: node.operator === 'AND' ? 'OR' : 'AND',
958
+ }));
959
+ }
960
+ addChildNode(parentId, type) {
961
+ const newNode = type === 'group' ? createGroupNode() : createActionNode();
962
+ this.updateNode(parentId, (node) => ({
963
+ ...node,
964
+ children: [...(node.children || []), newNode],
965
+ }));
966
+ }
967
+ removeNode(nodeId) {
968
+ const tree = this._builderTree();
969
+ if (!tree)
970
+ return;
971
+ if (tree.id === nodeId) {
972
+ this.clearLogic();
973
+ return;
974
+ }
975
+ this.updateTreeAndEmit(this.removeNodeFromTree(tree, nodeId));
976
+ }
977
+ updateActionId(nodeId, actionId) {
978
+ this.updateNode(nodeId, (node) => ({ ...node, actionId }));
979
+ }
980
+ /** Updates a node in the tree and emits the change */
981
+ updateNode(nodeId, updater) {
982
+ const tree = this._builderTree();
983
+ if (!tree)
984
+ return;
985
+ this.updateTreeAndEmit(this.updateNodeInTree(tree, nodeId, updater));
986
+ }
987
+ /** Sets the tree and emits the change */
988
+ updateTreeAndEmit(tree) {
989
+ this._builderTree.set(tree);
990
+ this.logicChange.emit(toLogicNode(tree));
991
+ }
992
+ updateNodeInTree(node, targetId, updater) {
993
+ if (node.id === targetId)
994
+ return updater(node);
995
+ if (!node.children)
996
+ return node;
997
+ return {
998
+ ...node,
999
+ children: node.children.map((child) => this.updateNodeInTree(child, targetId, updater)),
1000
+ };
1001
+ }
1002
+ removeNodeFromTree(node, targetId) {
1003
+ if (!node.children)
1004
+ return node;
1005
+ return {
1006
+ ...node,
1007
+ children: node.children
1008
+ .filter((child) => child.id !== targetId)
1009
+ .map((child) => this.removeNodeFromTree(child, targetId)),
1010
+ };
1011
+ }
1012
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LogicBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1013
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", 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: `
1014
+ <div class="logic-builder">
1015
+ <div class="flex justify-between items-center mb-3">
1016
+ <h4 class="text-sm font-semibold m-0">
1017
+ {{ 'iam.logic.title' | translate }}
1018
+ </h4>
1019
+ @if (!builderLogic()) {
1020
+ <button
1021
+ fButton
1022
+ size="small"
1023
+ (click)="initializeLogic()"
1024
+ >
1025
+ <ui-icon pButtonIcon name="plus"></ui-icon>
1026
+ <span pButtonLabel>{{ 'iam.logic.add.logic' | translate }}</span>
1027
+ </button>
1028
+ } @else {
1029
+ <button
1030
+ fButton
1031
+ size="small"
1032
+ severity="danger"
1033
+ [outlined]="true"
1034
+ (click)="clearLogic()"
1035
+ >
1036
+ <ui-icon pButtonIcon name="trash"></ui-icon>
1037
+ <span pButtonLabel>{{ 'iam.logic.clear.logic' | translate }}</span>
1038
+ </button>
1039
+ }
1040
+ </div>
1041
+
1042
+ @if (builderLogic()) {
1043
+ <div class="border border-surface rounded p-3 bg-surface-50">
1044
+ <div class="mb-3 text-sm text-muted-color">
1045
+ {{ 'iam.logic.description' | translate }}
1046
+ </div>
1047
+
1048
+ <!-- Root Node -->
1049
+ <div class="text-sm bg-surface-0 rounded border border-surface">
1050
+ <ng-container
1051
+ *ngTemplateOutlet="
1052
+ nodeTemplate;
1053
+ context: { $implicit: builderLogic()!, depth: 0 }
1054
+ "
1055
+ ></ng-container>
1056
+ </div>
1057
+ </div>
1058
+ }
1059
+ </div>
1060
+
1061
+ <!-- Recursive Node Template -->
1062
+ <ng-template #nodeTemplate let-node let-depth="depth">
1063
+ <div
1064
+ class="p-3 border-b border-surface"
1065
+ [class.bg-surface-0]="depth % 2 === 0"
1066
+ [class.bg-surface-50]="depth % 2 !== 0"
1067
+ >
1068
+ <div class="flex items-center gap-3 mb-2">
1069
+ <span
1070
+ class="node-type"
1071
+ [class.group]="node.type === 'group'"
1072
+ [class.action]="node.type === 'action'"
1073
+ >{{ node.type.toUpperCase() }}</span
1074
+ >
1075
+
1076
+ <div class="flex-1 flex items-center gap-2">
1077
+ @if (node.type === 'group') {
1078
+ <!-- Group: show operator toggle -->
1079
+ <span
1080
+ class="operator-badge"
1081
+ [class.and]="node.operator === 'AND'"
1082
+ [class.or]="node.operator !== 'AND'"
1083
+ (click)="toggleOperator(node.id)"
1084
+ [fTooltip]="'iam.logic.click.to.toggle' | translate"
1085
+ >
1086
+ {{ node.operator }}
1087
+ </span>
1088
+ <span class="text-muted-color text-xs"
1089
+ >({{
1090
+ 'iam.logic.conditions'
1091
+ | translate: { count: node.children?.length || 0 }
1092
+ }})</span
1093
+ >
1094
+ }
1095
+
1096
+ @if (node.type === 'action') {
1097
+ <f-select
1098
+ class="flex-1"
1099
+ [options]="actions()"
1100
+ optionLabel="name"
1101
+ optionValue="id"
1102
+ [ngModel]="node.actionId"
1103
+ (ngModelChange)="updateActionId(node.id, $event)"
1104
+ [placeholder]="'iam.logic.select.action' | translate"
1105
+ [filter]="true"
1106
+ />
1107
+ }
1108
+ </div>
1109
+
1110
+ <div class="flex gap-1">
1111
+ <button
1112
+ fButton
1113
+ [text]="true"
1114
+ severity="danger"
1115
+ size="small"
1116
+ (click)="removeNode(node.id)"
1117
+ [fTooltip]="'shared.remove' | translate"
1118
+ >
1119
+ <ui-icon pButtonIcon name="trash"></ui-icon>
1120
+ </button>
1121
+ </div>
1122
+ </div>
1123
+
1124
+ <!-- Children for group nodes -->
1125
+ @if (
1126
+ node.type === 'group' && node.children && node.children.length > 0
1127
+ ) {
1128
+ <div class="ml-5 mt-2 pl-3 border-l-2 border-surface">
1129
+ @for (child of node.children; track child.id) {
1130
+ <ng-container
1131
+ *ngTemplateOutlet="
1132
+ nodeTemplate;
1133
+ context: { $implicit: child, depth: depth + 1 }
1134
+ "
1135
+ ></ng-container>
1136
+ }
1137
+ </div>
1138
+ }
1139
+
1140
+ <!-- Add child buttons for group nodes -->
1141
+ @if (node.type === 'group') {
1142
+ <div
1143
+ class="mt-3 p-3 bg-surface-50 rounded border border-dashed border-surface"
1144
+ >
1145
+ <div class="text-xs font-semibold text-muted-color mb-2">
1146
+ {{ 'iam.logic.add.condition' | translate }}
1147
+ </div>
1148
+ <div class="flex gap-2 flex-wrap">
1149
+ <button
1150
+ fButton
1151
+ size="small"
1152
+ [outlined]="true"
1153
+ (click)="addChildNode(node.id, 'group')"
1154
+ >
1155
+ <ui-icon pButtonIcon name="sitemap"></ui-icon>
1156
+ <span pButtonLabel>{{ 'iam.logic.group' | translate }}</span>
1157
+ </button>
1158
+ <button
1159
+ fButton
1160
+ size="small"
1161
+ severity="success"
1162
+ [outlined]="true"
1163
+ (click)="addChildNode(node.id, 'action')"
1164
+ >
1165
+ <ui-icon pButtonIcon name="bolt"></ui-icon>
1166
+ <span pButtonLabel>{{ 'iam.logic.action' | translate }}</span>
1167
+ </button>
1168
+ </div>
1169
+ </div>
1170
+ }
1171
+ </div>
1172
+ </ng-template>
1173
+ `, 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(--f-blue-100, #dbeafe);color:var(--f-blue-700, #1e40af)}.node-type.action{background-color:var(--f-green-100, #d1fae5);color:var(--f-green-700, #065f46)}:host-context(.p-dark) .node-type.group{background-color:#3b82f633;color:var(--f-blue-400, #60a5fa)}:host-context(.p-dark) .node-type.action{background-color:#22c55e33;color:var(--f-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(--f-yellow-100, #fef3c7);color:var(--f-yellow-700, #92400e)}.operator-badge.or{background-color:var(--f-indigo-100, #e0e7ff);color:var(--f-indigo-700, #3730a3)}:host-context(.p-dark) .operator-badge.and{background-color:#eab30833;color:var(--f-yellow-400, #facc15)}:host-context(.p-dark) .operator-badge.or{background-color:#6366f133;color:var(--f-indigo-400, #818cf8)}.operator-badge:hover{opacity:.8}.action-select{background-color:var(--f-surface-0, #ffffff);color:var(--f-text-color, #1f2937)}:host-context(.p-dark) .action-select{background-color:var(--f-surface-900, #1f2937);color:var(--f-text-color, #f9fafb)}.action-select:focus{outline:none;border-color:var(--f-primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { 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: UiModule }, { kind: "directive", type: i2.FButtonDirective, selector: "[fButton]", inputs: ["severity", "text", "outlined", "link", "variant", "rounded", "raised", "fluid", "disabled", "loading", "loadingIcon", "size", "label", "icon", "iconPos", "ariaLabel", "plain"] }, { kind: "component", type: i2.SelectComponent, selector: "f-select", inputs: ["options", "optionLabel", "optionValue", "placeholder", "disabled", "filter", "filterBy", "filterMatchMode", "filterLocale", "filterPlaceholder", "resetFilterOnHide", "autofocusFilter", "emptyMessage", "emptyFilterMessage", "showClear", "loading", "loadingIcon", "dropdownIcon", "clearIcon", "inputId", "invalid", "touched", "styleClass", "style", "editable", "readonly", "autofocus", "checkmark", "group", "optionGroupLabel", "optionGroupChildren", "virtualScroll", "virtualScrollItemSize", "lazy"], outputs: ["onFilter", "onChange", "onShow", "onHide", "onBlur", "onFocus", "onClear", "onLazyLoad"] }, { kind: "directive", type: i2.TooltipDirective, selector: "[fTooltip]", inputs: ["fTooltip", "tooltipPosition", "positionStyle", "tooltipStyleClass", "disabled", "tooltipEvent", "showDelay", "hideDelay", "life", "autoHide", "positionTop", "positionLeft", "escape"] }, { kind: "component", type: i2.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1174
+ }
1175
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LogicBuilderComponent, decorators: [{
1176
+ type: Component,
1177
+ args: [{ selector: 'lib-logic-builder', imports: [AngularModule, UiModule, TranslatePipe], template: `
1178
+ <div class="logic-builder">
1179
+ <div class="flex justify-between items-center mb-3">
1180
+ <h4 class="text-sm font-semibold m-0">
1181
+ {{ 'iam.logic.title' | translate }}
1182
+ </h4>
1183
+ @if (!builderLogic()) {
1184
+ <button
1185
+ fButton
1186
+ size="small"
1187
+ (click)="initializeLogic()"
1188
+ >
1189
+ <ui-icon pButtonIcon name="plus"></ui-icon>
1190
+ <span pButtonLabel>{{ 'iam.logic.add.logic' | translate }}</span>
1191
+ </button>
1192
+ } @else {
1193
+ <button
1194
+ fButton
1195
+ size="small"
1196
+ severity="danger"
1197
+ [outlined]="true"
1198
+ (click)="clearLogic()"
1199
+ >
1200
+ <ui-icon pButtonIcon name="trash"></ui-icon>
1201
+ <span pButtonLabel>{{ 'iam.logic.clear.logic' | translate }}</span>
1202
+ </button>
1203
+ }
1204
+ </div>
1205
+
1206
+ @if (builderLogic()) {
1207
+ <div class="border border-surface rounded p-3 bg-surface-50">
1208
+ <div class="mb-3 text-sm text-muted-color">
1209
+ {{ 'iam.logic.description' | translate }}
1210
+ </div>
1211
+
1212
+ <!-- Root Node -->
1213
+ <div class="text-sm bg-surface-0 rounded border border-surface">
1214
+ <ng-container
1215
+ *ngTemplateOutlet="
1216
+ nodeTemplate;
1217
+ context: { $implicit: builderLogic()!, depth: 0 }
1218
+ "
1219
+ ></ng-container>
1220
+ </div>
1221
+ </div>
1222
+ }
1223
+ </div>
1224
+
1225
+ <!-- Recursive Node Template -->
1226
+ <ng-template #nodeTemplate let-node let-depth="depth">
1227
+ <div
1228
+ class="p-3 border-b border-surface"
1229
+ [class.bg-surface-0]="depth % 2 === 0"
1230
+ [class.bg-surface-50]="depth % 2 !== 0"
1231
+ >
1232
+ <div class="flex items-center gap-3 mb-2">
1233
+ <span
1234
+ class="node-type"
1235
+ [class.group]="node.type === 'group'"
1236
+ [class.action]="node.type === 'action'"
1237
+ >{{ node.type.toUpperCase() }}</span
1238
+ >
1239
+
1240
+ <div class="flex-1 flex items-center gap-2">
1241
+ @if (node.type === 'group') {
1242
+ <!-- Group: show operator toggle -->
1243
+ <span
1244
+ class="operator-badge"
1245
+ [class.and]="node.operator === 'AND'"
1246
+ [class.or]="node.operator !== 'AND'"
1247
+ (click)="toggleOperator(node.id)"
1248
+ [fTooltip]="'iam.logic.click.to.toggle' | translate"
1249
+ >
1250
+ {{ node.operator }}
1251
+ </span>
1252
+ <span class="text-muted-color text-xs"
1253
+ >({{
1254
+ 'iam.logic.conditions'
1255
+ | translate: { count: node.children?.length || 0 }
1256
+ }})</span
1257
+ >
1258
+ }
1259
+
1260
+ @if (node.type === 'action') {
1261
+ <f-select
1262
+ class="flex-1"
1263
+ [options]="actions()"
1264
+ optionLabel="name"
1265
+ optionValue="id"
1266
+ [ngModel]="node.actionId"
1267
+ (ngModelChange)="updateActionId(node.id, $event)"
1268
+ [placeholder]="'iam.logic.select.action' | translate"
1269
+ [filter]="true"
1270
+ />
1271
+ }
1272
+ </div>
1273
+
1274
+ <div class="flex gap-1">
1275
+ <button
1276
+ fButton
1277
+ [text]="true"
1278
+ severity="danger"
1279
+ size="small"
1280
+ (click)="removeNode(node.id)"
1281
+ [fTooltip]="'shared.remove' | translate"
1282
+ >
1283
+ <ui-icon pButtonIcon name="trash"></ui-icon>
1284
+ </button>
1285
+ </div>
1286
+ </div>
1287
+
1288
+ <!-- Children for group nodes -->
1289
+ @if (
1290
+ node.type === 'group' && node.children && node.children.length > 0
1291
+ ) {
1292
+ <div class="ml-5 mt-2 pl-3 border-l-2 border-surface">
1293
+ @for (child of node.children; track child.id) {
1294
+ <ng-container
1295
+ *ngTemplateOutlet="
1296
+ nodeTemplate;
1297
+ context: { $implicit: child, depth: depth + 1 }
1298
+ "
1299
+ ></ng-container>
1300
+ }
1301
+ </div>
1302
+ }
1303
+
1304
+ <!-- Add child buttons for group nodes -->
1305
+ @if (node.type === 'group') {
1306
+ <div
1307
+ class="mt-3 p-3 bg-surface-50 rounded border border-dashed border-surface"
1308
+ >
1309
+ <div class="text-xs font-semibold text-muted-color mb-2">
1310
+ {{ 'iam.logic.add.condition' | translate }}
1311
+ </div>
1312
+ <div class="flex gap-2 flex-wrap">
1313
+ <button
1314
+ fButton
1315
+ size="small"
1316
+ [outlined]="true"
1317
+ (click)="addChildNode(node.id, 'group')"
1318
+ >
1319
+ <ui-icon pButtonIcon name="sitemap"></ui-icon>
1320
+ <span pButtonLabel>{{ 'iam.logic.group' | translate }}</span>
1321
+ </button>
1322
+ <button
1323
+ fButton
1324
+ size="small"
1325
+ severity="success"
1326
+ [outlined]="true"
1327
+ (click)="addChildNode(node.id, 'action')"
1328
+ >
1329
+ <ui-icon pButtonIcon name="bolt"></ui-icon>
1330
+ <span pButtonLabel>{{ 'iam.logic.action' | translate }}</span>
1331
+ </button>
1332
+ </div>
1333
+ </div>
1334
+ }
1335
+ </div>
1336
+ </ng-template>
1337
+ `, 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(--f-blue-100, #dbeafe);color:var(--f-blue-700, #1e40af)}.node-type.action{background-color:var(--f-green-100, #d1fae5);color:var(--f-green-700, #065f46)}:host-context(.p-dark) .node-type.group{background-color:#3b82f633;color:var(--f-blue-400, #60a5fa)}:host-context(.p-dark) .node-type.action{background-color:#22c55e33;color:var(--f-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(--f-yellow-100, #fef3c7);color:var(--f-yellow-700, #92400e)}.operator-badge.or{background-color:var(--f-indigo-100, #e0e7ff);color:var(--f-indigo-700, #3730a3)}:host-context(.p-dark) .operator-badge.and{background-color:#eab30833;color:var(--f-yellow-400, #facc15)}:host-context(.p-dark) .operator-badge.or{background-color:#6366f133;color:var(--f-indigo-400, #818cf8)}.operator-badge:hover{opacity:.8}.action-select{background-color:var(--f-surface-0, #ffffff);color:var(--f-text-color, #1f2937)}:host-context(.p-dark) .action-select{background-color:var(--f-surface-900, #1f2937);color:var(--f-text-color, #f9fafb)}.action-select:focus{outline:none;border-color:var(--f-primary-color, #3b82f6)}\n"] }]
1338
+ }], 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"] }] } });
1339
+
1340
+ function flattenTree(tree) {
1341
+ if (!tree?.length)
1342
+ return [];
1343
+ const result = [];
1344
+ const flatten = (nodes) => {
1345
+ for (const node of nodes) {
1346
+ result.push(node);
1347
+ if (node.children?.length)
1348
+ flatten(node.children);
1349
+ }
1350
+ };
1351
+ flatten(tree);
1352
+ return result;
1353
+ }
1354
+ function buildTreeFromFlat(flatList) {
1355
+ if (!flatList?.length)
1356
+ return [];
1357
+ const itemMap = new Map();
1358
+ const rootItems = [];
1359
+ flatList.forEach((item) => {
1360
+ if (item.id)
1361
+ itemMap.set(item.id, { ...item, children: [] });
1362
+ });
1363
+ flatList.forEach((item) => {
1364
+ if (!item.id)
1365
+ return;
1366
+ const node = itemMap.get(item.id);
1367
+ if (!node)
1368
+ return;
1369
+ if (!item.parentId) {
1370
+ rootItems.push(node);
1371
+ }
1372
+ else {
1373
+ const parent = itemMap.get(item.parentId);
1374
+ if (parent) {
1375
+ parent.children.push(node);
1376
+ }
1377
+ else {
1378
+ rootItems.push(node);
1379
+ }
1380
+ }
1381
+ });
1382
+ return rootItems;
1383
+ }
1384
+ function convertActionToTreeNode(actions) {
1385
+ if (!actions?.length)
1386
+ return [];
1387
+ const convert = (action) => {
1388
+ const { children, ...actionData } = action;
1389
+ return {
1390
+ data: actionData,
1391
+ children: children?.map(convert) ?? [],
1392
+ leaf: !children?.length,
1393
+ expanded: false,
1394
+ };
1395
+ };
1396
+ return actions.map(convert);
1397
+ }
1398
+
1399
+ class BaseActionSelectorComponent {
1400
+ permissionLogic = inject(ActionPermissionLogicService);
1401
+ translateAdapter = inject(TranslateService, {
1402
+ optional: true,
1403
+ });
1404
+ loading = signal(false, /* @ts-ignore */
1405
+ ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1406
+ saving = signal(false, /* @ts-ignore */
1407
+ ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
1408
+ _actions = signal([], /* @ts-ignore */
1409
+ ...(ngDevMode ? [{ debugName: "_actions" }] : /* istanbul ignore next */ []));
1410
+ actions = this._actions.asReadonly();
1411
+ _actionsTree = signal([], /* @ts-ignore */
1412
+ ...(ngDevMode ? [{ debugName: "_actionsTree" }] : /* istanbul ignore next */ []));
1413
+ actionsTree = this._actionsTree.asReadonly();
1414
+ _selectionMap = signal({}, /* @ts-ignore */
1415
+ ...(ngDevMode ? [{ debugName: "_selectionMap" }] : /* istanbul ignore next */ []));
1416
+ selectionMap = this._selectionMap.asReadonly();
1417
+ _initialSelection = signal({}, /* @ts-ignore */
1418
+ ...(ngDevMode ? [{ debugName: "_initialSelection" }] : /* istanbul ignore next */ []));
1419
+ initialSelection = this._initialSelection.asReadonly();
1420
+ treeNodes = computed(() => convertActionToTreeNode(this.actionsTree()), /* @ts-ignore */
1421
+ ...(ngDevMode ? [{ debugName: "treeNodes" }] : /* istanbul ignore next */ []));
1422
+ allSelected = computed(() => {
1423
+ const selMap = this.selectionMap();
1424
+ const total = this.actions().length;
1425
+ if (total === 0)
1426
+ return false;
1427
+ return Object.values(selMap).filter((v) => v).length === total;
1428
+ }, /* @ts-ignore */
1429
+ ...(ngDevMode ? [{ debugName: "allSelected" }] : /* istanbul ignore next */ []));
1430
+ hasChanges = computed(() => {
1431
+ const current = this.selectionMap();
1432
+ const initial = this.initialSelection();
1433
+ const currentKeys = Object.keys(current);
1434
+ if (currentKeys.length !== Object.keys(initial).length)
1435
+ return true;
1436
+ return currentKeys.some((key) => current[key] !== initial[key]);
1437
+ }, /* @ts-ignore */
1438
+ ...(ngDevMode ? [{ debugName: "hasChanges" }] : /* istanbul ignore next */ []));
1439
+ actionsWithUnmetPrerequisites = computed(() => {
1440
+ const selMap = this.selectionMap();
1441
+ const allActions = this.actions();
1442
+ const unmetSet = new Set();
1443
+ for (const action of allActions) {
1444
+ if (action.id &&
1445
+ this.permissionLogic.hasUnmetPrerequisites(action, selMap, allActions)) {
1446
+ unmetSet.add(action.id);
1447
+ }
1448
+ }
1449
+ return unmetSet;
1450
+ }, /* @ts-ignore */
1451
+ ...(ngDevMode ? [{ debugName: "actionsWithUnmetPrerequisites" }] : /* istanbul ignore next */ []));
1452
+ invalidActionsCount = computed(() => this.actionsWithUnmetPrerequisites().size, /* @ts-ignore */
1453
+ ...(ngDevMode ? [{ debugName: "invalidActionsCount" }] : /* istanbul ignore next */ []));
1454
+ pendingAdd = computed(() => {
1455
+ const current = this.selectionMap();
1456
+ const initial = this.initialSelection();
1457
+ return this.actions().filter((a) => current[a.id] && !initial[a.id]);
1458
+ }, /* @ts-ignore */
1459
+ ...(ngDevMode ? [{ debugName: "pendingAdd" }] : /* istanbul ignore next */ []));
1460
+ pendingRemove = computed(() => {
1461
+ const current = this.selectionMap();
1462
+ const initial = this.initialSelection();
1463
+ return this.actions().filter((a) => !current[a.id] && initial[a.id]);
1464
+ }, /* @ts-ignore */
1465
+ ...(ngDevMode ? [{ debugName: "pendingRemove" }] : /* istanbul ignore next */ []));
1466
+ canSave = computed(() => this.hasChanges() && !this.saving() && this.invalidActionsCount() === 0, /* @ts-ignore */
1467
+ ...(ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []));
1468
+ translate(key, vars) {
1469
+ return this.translateAdapter?.translate(key, vars) ?? key;
1470
+ }
1471
+ hasUnmetPrerequisites(action) {
1472
+ return action.id
1473
+ ? this.actionsWithUnmetPrerequisites().has(action.id)
1474
+ : false;
1475
+ }
1476
+ getTooltip(action) {
1477
+ const selMap = this.selectionMap();
1478
+ const isCurrentlySelected = selMap[action.id];
1479
+ if (action.permissionLogic) {
1480
+ const prerequisiteInfo = this.permissionLogic.getPrerequisiteTooltip(action, selMap, this.actions());
1481
+ if (prerequisiteInfo) {
1482
+ const actionHint = isCurrentlySelected
1483
+ ? `\n\n---\n[${this.translate('shared.remove')}] ${this.translate('iam.tooltip.remove.action')}`
1484
+ : `\n\n---\n[${this.translate('shared.add')}] ${this.translate('iam.tooltip.add.action')}`;
1485
+ return `${prerequisiteInfo}${actionHint}`;
1486
+ }
1487
+ }
1488
+ return this.getDefaultTooltip(isCurrentlySelected);
1489
+ }
1490
+ onActionToggle(action, newValue) {
1491
+ if (!newValue) {
1492
+ this.permissionLogic.handleUncheck(action, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
1493
+ }
1494
+ else {
1495
+ this.permissionLogic.handleCheck(action, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap), (previousState) => this._selectionMap.set(previousState));
1496
+ }
1497
+ }
1498
+ toggleAll() {
1499
+ this.setAllSelection(!this.allSelected());
1500
+ }
1501
+ selectAll() {
1502
+ this.setAllSelection(true);
1503
+ }
1504
+ deselectAll() {
1505
+ this.setAllSelection(false);
1506
+ }
1507
+ setAllSelection(value) {
1508
+ this._selectionMap.set(this.buildSelectionMap(this.actions(), () => value));
1509
+ }
1510
+ buildSelectionMap(actions, predicate) {
1511
+ const selMap = {};
1512
+ for (const action of actions) {
1513
+ selMap[action.id] = predicate(action);
1514
+ }
1515
+ return selMap;
1516
+ }
1517
+ buildPayloadItems() {
1518
+ const items = [];
1519
+ for (const action of this.pendingAdd()) {
1520
+ items.push({ id: action.id, action: 'add' });
1521
+ }
1522
+ for (const action of this.pendingRemove()) {
1523
+ items.push({ id: action.id, action: 'remove' });
1524
+ }
1525
+ return items;
1526
+ }
1527
+ resetState() {
1528
+ this._actionsTree.set([]);
1529
+ this._actions.set([]);
1530
+ this._selectionMap.set({});
1531
+ this._initialSelection.set({});
1532
+ }
1533
+ }
1534
+
1535
+ class RoleActionSelectorComponent extends BaseActionSelectorComponent {
1536
+ ROLE_ACTION_PERMISSIONS = ROLE_ACTION_PERMISSIONS;
1537
+ destroyRef = inject(DestroyRef);
1538
+ roleApi = inject(RoleApiService);
1539
+ actionApi = inject(ActionApiService);
1540
+ permissionApi = inject(PermissionApiService);
1541
+ messageService = inject(MessageService);
1542
+ selectedRoleId = signal(undefined, /* @ts-ignore */
1543
+ ...(ngDevMode ? [{ debugName: "selectedRoleId" }] : /* istanbul ignore next */ []));
1544
+ roles = signal([], /* @ts-ignore */
1545
+ ...(ngDevMode ? [{ debugName: "roles" }] : /* istanbul ignore next */ []));
1546
+ loadDataAbortController = null;
1547
+ constructor() {
1548
+ super();
1549
+ this.destroyRef.onDestroy(() => {
1550
+ this.loadDataAbortController?.abort();
1551
+ });
1552
+ this.loadRoles();
1553
+ effect(() => {
1554
+ const roleId = this.selectedRoleId();
1555
+ this.loadDataAbortController?.abort();
1556
+ if (roleId) {
1557
+ this.loadDataAbortController = new AbortController();
1558
+ this.onRoleChange(this.loadDataAbortController.signal).catch((err) => {
1559
+ if (err.name !== 'AbortError') {
1560
+ this.loading.set(false);
1561
+ }
1562
+ });
1563
+ }
1564
+ else {
1565
+ this.resetState();
1566
+ }
1567
+ });
1568
+ }
1569
+ getDefaultTooltip(isSelected) {
1570
+ if (isSelected) {
1571
+ return `[${this.translate('shared.assigned')}] ${this.translate('iam.tooltip.assigned.to.role')}\n\n[${this.translate('shared.remove')}] ${this.translate('iam.tooltip.click.to.remove.role')}`;
1572
+ }
1573
+ return `[${this.translate('shared.add')}] ${this.translate('iam.tooltip.click.to.assign.role')}`;
1574
+ }
1575
+ async loadRoles() {
1576
+ try {
1577
+ const response = await this.roleApi.getAll({
1578
+ pagination: { currentPage: 0, pageSize: MAX_DROPDOWN_ITEMS },
1579
+ });
1580
+ this.roles.set(response?.data ?? []);
1581
+ }
1582
+ catch { }
1583
+ }
1584
+ async onRoleChange(signal) {
1585
+ const roleId = this.selectedRoleId();
1586
+ if (!roleId)
1587
+ return;
1588
+ this.loading.set(true);
1589
+ try {
1590
+ const actionsResponse = await firstValueFrom(this.actionApi.getActionsForPermission());
1591
+ if (signal.aborted)
1592
+ return;
1593
+ const flatActions = (actionsResponse?.data ?? []).filter((a) => !!a.id);
1594
+ const actionsTree = buildTreeFromFlat(flatActions);
1595
+ this._actionsTree.set(actionsTree);
1596
+ this._actions.set(flatActions);
1597
+ const assignedResponse = await firstValueFrom(this.permissionApi.getRoleActions(roleId));
1598
+ if (signal.aborted)
1599
+ return;
1600
+ const assignedIds = new Set((assignedResponse?.data || []).map((a) => a.actionId));
1601
+ const selMap = this.buildSelectionMap(flatActions, (action) => assignedIds.has(action.id));
1602
+ this._selectionMap.set(selMap);
1603
+ this._initialSelection.set({ ...selMap });
1604
+ }
1605
+ finally {
1606
+ if (!signal.aborted) {
1607
+ this.loading.set(false);
1608
+ }
1609
+ }
1610
+ }
1611
+ async saveChanges() {
1612
+ const roleId = this.selectedRoleId();
1613
+ if (!roleId)
1614
+ return;
1615
+ const invalidActions = this.permissionLogic.getActionsWithUnmetPrerequisites(this.selectionMap(), this.actions());
1616
+ if (invalidActions.length > 0) {
1617
+ this.permissionLogic.showValidationErrorDialog(invalidActions, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
1618
+ return;
1619
+ }
1620
+ const items = this.buildPayloadItems();
1621
+ if (items.length === 0)
1622
+ return;
1623
+ this.saving.set(true);
1624
+ try {
1625
+ const response = await firstValueFrom(this.permissionApi.assignRoleActions({ roleId, items }));
1626
+ this.messageService.add({
1627
+ severity: 'success',
1628
+ summary: this.translate('shared.success'),
1629
+ detail: response?.messageKey
1630
+ ? this.translate(response.messageKey, response.messageVariables)
1631
+ : response?.message,
1632
+ });
1633
+ this._initialSelection.set({ ...this.selectionMap() });
1634
+ }
1635
+ catch {
1636
+ }
1637
+ finally {
1638
+ this.saving.set(false);
1639
+ }
1640
+ }
1641
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RoleActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1642
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: RoleActionSelectorComponent, isStandalone: true, selector: "flusys-role-action-selector", usesInheritance: true, ngImport: i0, template: `
1643
+ <div class="role-action-selector">
1644
+ <div class="mb-4">
1645
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
1646
+ <div>
1647
+ <label class="block font-semibold mb-2">{{
1648
+ 'iam.permission.select.role' | translate
1649
+ }}</label>
1650
+ <f-select
1651
+ [ngModel]="selectedRoleId()"
1652
+ (ngModelChange)="selectedRoleId.set($event)"
1653
+ [options]="roles()"
1654
+ optionLabel="name"
1655
+ optionValue="id"
1656
+ [placeholder]="
1657
+ 'iam.permission.select.role.placeholder' | translate
1658
+ "
1659
+ [showClear]="true"
1660
+ [filter]="true"
1661
+ class="w-full"
1662
+ />
1663
+ </div>
1664
+ </div>
1665
+ </div>
1666
+
1667
+ @if (selectedRoleId()) {
1668
+ @if (loading()) {
1669
+ <div
1670
+ class="surface-card p-5 rounded-border shadow-sm flex justify-center"
1671
+ >
1672
+ <ui-icon name="loader-circle" spin class="text-primary text-5xl"></ui-icon>
1673
+ </div>
1674
+ }
1675
+
1676
+ @if (!loading() && actions().length > 0) {
1677
+ <div class="surface-card p-4 rounded-border shadow-sm">
1678
+ <div
1679
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
1680
+ >
1681
+ <div>
1682
+ <h5 class="m-0 mb-1">
1683
+ {{ 'iam.permission.action.permissions' | translate }}
1684
+ </h5>
1685
+ <p class="text-sm text-muted-color m-0">
1686
+ {{
1687
+ 'iam.permission.actions.available'
1688
+ | translate: { count: actions().length }
1689
+ }}
1690
+ </p>
1691
+ </div>
1692
+ <div class="flex flex-wrap gap-2">
1693
+ <button
1694
+ fButton
1695
+ [outlined]="true"
1696
+ size="small"
1697
+ severity="secondary"
1698
+ (click)="selectAll()"
1699
+ >
1700
+ <ui-icon pButtonIcon name="check"></ui-icon>
1701
+ <span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
1702
+ </button>
1703
+ <button
1704
+ fButton
1705
+ [outlined]="true"
1706
+ size="small"
1707
+ severity="secondary"
1708
+ (click)="deselectAll()"
1709
+ >
1710
+ <ui-icon pButtonIcon name="times"></ui-icon>
1711
+ <span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
1712
+ </button>
1713
+ <button
1714
+ fButton
1715
+ *hasPermission="ROLE_ACTION_PERMISSIONS.ASSIGN"
1716
+ [disabled]="!canSave() || saving()"
1717
+ size="small"
1718
+ (click)="saveChanges()"
1719
+ >
1720
+ @if (saving()) {
1721
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
1722
+ } @else {
1723
+ <ui-icon pButtonIcon name="save"></ui-icon>
1724
+ }
1725
+ <span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
1726
+ </button>
1727
+ </div>
1728
+ </div>
1729
+
1730
+ @if (invalidActionsCount() > 0) {
1731
+ <div class="validation-warning rounded-border p-3 mb-4">
1732
+ <div class="flex items-start gap-2">
1733
+ <ui-icon name="exclamation-triangle" class="text-xl"></ui-icon>
1734
+ <div class="flex-1">
1735
+ <span class="font-semibold"
1736
+ >{{ 'iam.validation.warning.title' | translate }}:</span
1737
+ >
1738
+ <p class="text-sm mt-1 mb-0">
1739
+ {{
1740
+ (invalidActionsCount() > 1
1741
+ ? 'iam.validation.unmet.prerequisites.plural'
1742
+ : 'iam.validation.unmet.prerequisites.singular'
1743
+ ) | translate: { count: invalidActionsCount() }
1744
+ }}
1745
+ </p>
1746
+ </div>
1747
+ </div>
1748
+ </div>
1749
+ }
1750
+
1751
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
1752
+ <f-treetable
1753
+ [value]="treeNodes()"
1754
+ dataKey="id"
1755
+ [tableStyle]="{ 'min-width': '50rem' }"
1756
+ >
1757
+ <ng-template #header>
1758
+ <tr>
1759
+ <th class="w-12">
1760
+ <f-checkbox
1761
+ [ngModel]="allSelected()"
1762
+ [binary]="true"
1763
+ (ngModelChange)="toggleAll()"
1764
+ [fTooltip]="'shared.select.deselect.all' | translate"
1765
+ tooltipPosition="top"
1766
+ />
1767
+ </th>
1768
+ <th>{{ 'shared.name' | translate }}</th>
1769
+ <th class="hidden md:table-cell">
1770
+ {{ 'shared.code' | translate }}
1771
+ </th>
1772
+ <th>{{ 'iam.action.type' | translate }}</th>
1773
+ <th class="hidden lg:table-cell">
1774
+ {{ 'iam.permission.requirements' | translate }}
1775
+ </th>
1776
+ </tr>
1777
+ </ng-template>
1778
+ <ng-template #body let-rowNode let-rowData="rowData">
1779
+ @let hasUnmet = hasUnmetPrerequisites(rowData);
1780
+ @let tooltip = getTooltip(rowData);
1781
+ <tr [class.highlight-warning]="hasUnmet">
1782
+ <td class="w-12">
1783
+ <f-checkbox
1784
+ [ngModel]="selectionMap()[rowData.id]"
1785
+ [binary]="true"
1786
+ (ngModelChange)="onActionToggle(rowData, $event)"
1787
+ [fTooltip]="tooltip"
1788
+ tooltipPosition="top"
1789
+ />
1790
+ </td>
1791
+ <td>
1792
+ <f-treetabletoggler [rowNode]="rowNode" />
1793
+ <span class="inline-flex items-center gap-2">
1794
+ {{ rowData.name }}
1795
+ @if (hasUnmet) {
1796
+ <ui-icon name="exclamation-triangle" class="text-orange-500" [fTooltip]="
1797
+ 'iam.validation.unmet.prerequisites.tooltip'
1798
+ | translate
1799
+ "
1800
+ tooltipPosition="top"></ui-icon>
1801
+ }
1802
+ </span>
1803
+ </td>
1804
+ <td class="hidden md:table-cell">
1805
+ {{ rowData.code || '-' }}
1806
+ </td>
1807
+ <td>
1808
+ <f-tag
1809
+ [value]="rowData.actionType"
1810
+ [severity]="
1811
+ rowData.actionType === 'backend' ? 'info' : 'success'
1812
+ "
1813
+ />
1814
+ </td>
1815
+ <td class="hidden lg:table-cell">
1816
+ @if (rowData.permissionLogic) {
1817
+ <span class="text-sm text-muted-color">{{
1818
+ 'iam.permission.has.prerequisites' | translate
1819
+ }}</span>
1820
+ } @else {
1821
+ <span class="text-muted-color">-</span>
1822
+ }
1823
+ </td>
1824
+ </tr>
1825
+ </ng-template>
1826
+ <ng-template #emptymessage>
1827
+ <tr>
1828
+ <td colspan="5" class="text-center p-4 text-muted-color">
1829
+ @if (loading()) {
1830
+ <ui-icon name="loader-circle" spin></ui-icon>
1831
+ {{ 'shared.loading.actions' | translate }}
1832
+ } @else {
1833
+ {{ 'iam.permission.no.actions.available' | translate }}
1834
+ }
1835
+ </td>
1836
+ </tr>
1837
+ </ng-template>
1838
+ </f-treetable>
1839
+ </div>
1840
+ </div>
1841
+
1842
+ @if (hasChanges()) {
1843
+ <div class="border border-surface rounded-border p-3 mt-4">
1844
+ <div class="flex items-center gap-2 mb-3">
1845
+ <ui-icon name="info-circle" class="text-primary"></ui-icon>
1846
+ <span class="font-bold">{{
1847
+ 'shared.pending.changes' | translate
1848
+ }}</span>
1849
+ </div>
1850
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
1851
+ @if (pendingAdd().length > 0) {
1852
+ <div>
1853
+ <div class="flex items-center gap-2 mb-2">
1854
+ <ui-icon name="plus-circle" class="text-green-500"></ui-icon>
1855
+ <strong class="text-sm"
1856
+ >{{ 'shared.to.add' | translate }} ({{
1857
+ pendingAdd().length
1858
+ }})</strong
1859
+ >
1860
+ </div>
1861
+ <ul class="list-none p-0 m-0 pl-4">
1862
+ @for (action of pendingAdd(); track action.id) {
1863
+ <li class="text-sm mb-1">{{ action.name }}</li>
1864
+ }
1865
+ </ul>
1866
+ </div>
1867
+ }
1868
+ @if (pendingRemove().length > 0) {
1869
+ <div>
1870
+ <div class="flex items-center gap-2 mb-2">
1871
+ <ui-icon name="minus-circle" class="text-red-500"></ui-icon>
1872
+ <strong class="text-sm"
1873
+ >{{ 'shared.to.remove' | translate }} ({{
1874
+ pendingRemove().length
1875
+ }})</strong
1876
+ >
1877
+ </div>
1878
+ <ul class="list-none p-0 m-0 pl-4">
1879
+ @for (action of pendingRemove(); track action.id) {
1880
+ <li class="text-sm mb-1">{{ action.name }}</li>
1881
+ }
1882
+ </ul>
1883
+ </div>
1884
+ }
1885
+ </div>
1886
+ </div>
1887
+ }
1888
+ }
1889
+
1890
+ @if (!loading() && actions().length === 0) {
1891
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
1892
+ <ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
1893
+ <p class="text-muted-color m-0">
1894
+ {{ 'iam.permission.no.actions.for.role' | translate }}
1895
+ </p>
1896
+ </div>
1897
+ }
1898
+ }
1899
+ </div>
1900
+ `, isInline: true, styles: [":host{display:block}.validation-warning{background-color:var(--f-yellow-50, #fefce8);border-left:4px solid var(--f-yellow-500, #eab308);color:var(--f-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--f-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--f-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: 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: UiModule }, { kind: "directive", type: i2.FButtonDirective, selector: "[fButton]", inputs: ["severity", "text", "outlined", "link", "variant", "rounded", "raised", "fluid", "disabled", "loading", "loadingIcon", "size", "label", "icon", "iconPos", "ariaLabel", "plain"] }, { kind: "component", type: i2.TagComponent, selector: "f-tag", inputs: ["value", "severity", "icon", "rounded", "color", "styleClass", "style"] }, { kind: "component", type: i2.CheckboxComponent, selector: "f-checkbox", inputs: ["label", "binary", "disabled", "readonly", "inputId", "invalid", "indeterminate", "variant", "size", "touched", "styleClass", "style", "checked"], outputs: ["onChange", "onBlur", "touch", "checkedChange"] }, { kind: "component", type: i2.SelectComponent, selector: "f-select", inputs: ["options", "optionLabel", "optionValue", "placeholder", "disabled", "filter", "filterBy", "filterMatchMode", "filterLocale", "filterPlaceholder", "resetFilterOnHide", "autofocusFilter", "emptyMessage", "emptyFilterMessage", "showClear", "loading", "loadingIcon", "dropdownIcon", "clearIcon", "inputId", "invalid", "touched", "styleClass", "style", "editable", "readonly", "autofocus", "checkmark", "group", "optionGroupLabel", "optionGroupChildren", "virtualScroll", "virtualScrollItemSize", "lazy"], outputs: ["onFilter", "onChange", "onShow", "onHide", "onBlur", "onFocus", "onClear", "onLazyLoad"] }, { kind: "directive", type: i2.TooltipDirective, selector: "[fTooltip]", inputs: ["fTooltip", "tooltipPosition", "positionStyle", "tooltipStyleClass", "disabled", "tooltipEvent", "showDelay", "hideDelay", "life", "autoHide", "positionTop", "positionLeft", "escape"] }, { kind: "component", type: i2.TreeTableComponent, selector: "f-treetable", inputs: ["value", "loading", "dataKey", "styleClass", "tableStyle", "scrollable", "scrollHeight", "resizableColumns", "lazy", "totalRecords", "paginator", "first", "rows", "rowsPerPageOptions", "showCurrentPageReport", "currentPageReportTemplate", "globalFilterFields", "filterDelay", "selectionMode", "sortMode", "virtualScroll", "virtualScrollItemSize", "sortField", "sortOrder", "multiSortMeta", "filters", "selection", "contextMenuSelection"], outputs: ["sortFieldChange", "sortOrderChange", "multiSortMetaChange", "filtersChange", "selectionChange", "contextMenuSelectionChange", "onLazyLoad", "onSort", "onFilter", "onNodeExpand", "onNodeCollapse", "onNodeSelect", "onNodeUnselect", "onRowContextMenu"] }, { kind: "component", type: i2.TreeTableTogglerComponent, selector: "f-treetabletoggler", inputs: ["rowNode"] }, { kind: "component", type: i2.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "directive", type: HasPermissionDirective, selector: "[hasPermission]", inputs: ["hasPermission"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1901
+ }
1902
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RoleActionSelectorComponent, decorators: [{
1903
+ type: Component,
1904
+ args: [{ selector: 'flusys-role-action-selector', imports: [FormsModule, UiModule, HasPermissionDirective, TranslatePipe], template: `
1905
+ <div class="role-action-selector">
1906
+ <div class="mb-4">
1907
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
1908
+ <div>
1909
+ <label class="block font-semibold mb-2">{{
1910
+ 'iam.permission.select.role' | translate
1911
+ }}</label>
1912
+ <f-select
1913
+ [ngModel]="selectedRoleId()"
1914
+ (ngModelChange)="selectedRoleId.set($event)"
1915
+ [options]="roles()"
1916
+ optionLabel="name"
1917
+ optionValue="id"
1918
+ [placeholder]="
1919
+ 'iam.permission.select.role.placeholder' | translate
1920
+ "
1921
+ [showClear]="true"
1922
+ [filter]="true"
1923
+ class="w-full"
1924
+ />
1925
+ </div>
1926
+ </div>
1927
+ </div>
1928
+
1929
+ @if (selectedRoleId()) {
1930
+ @if (loading()) {
1931
+ <div
1932
+ class="surface-card p-5 rounded-border shadow-sm flex justify-center"
1933
+ >
1934
+ <ui-icon name="loader-circle" spin class="text-primary text-5xl"></ui-icon>
1935
+ </div>
1936
+ }
1937
+
1938
+ @if (!loading() && actions().length > 0) {
1939
+ <div class="surface-card p-4 rounded-border shadow-sm">
1940
+ <div
1941
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
1942
+ >
1943
+ <div>
1944
+ <h5 class="m-0 mb-1">
1945
+ {{ 'iam.permission.action.permissions' | translate }}
1946
+ </h5>
1947
+ <p class="text-sm text-muted-color m-0">
1948
+ {{
1949
+ 'iam.permission.actions.available'
1950
+ | translate: { count: actions().length }
1951
+ }}
1952
+ </p>
1953
+ </div>
1954
+ <div class="flex flex-wrap gap-2">
1955
+ <button
1956
+ fButton
1957
+ [outlined]="true"
1958
+ size="small"
1959
+ severity="secondary"
1960
+ (click)="selectAll()"
1961
+ >
1962
+ <ui-icon pButtonIcon name="check"></ui-icon>
1963
+ <span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
1964
+ </button>
1965
+ <button
1966
+ fButton
1967
+ [outlined]="true"
1968
+ size="small"
1969
+ severity="secondary"
1970
+ (click)="deselectAll()"
1971
+ >
1972
+ <ui-icon pButtonIcon name="times"></ui-icon>
1973
+ <span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
1974
+ </button>
1975
+ <button
1976
+ fButton
1977
+ *hasPermission="ROLE_ACTION_PERMISSIONS.ASSIGN"
1978
+ [disabled]="!canSave() || saving()"
1979
+ size="small"
1980
+ (click)="saveChanges()"
1981
+ >
1982
+ @if (saving()) {
1983
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
1984
+ } @else {
1985
+ <ui-icon pButtonIcon name="save"></ui-icon>
1986
+ }
1987
+ <span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
1988
+ </button>
1989
+ </div>
1990
+ </div>
1991
+
1992
+ @if (invalidActionsCount() > 0) {
1993
+ <div class="validation-warning rounded-border p-3 mb-4">
1994
+ <div class="flex items-start gap-2">
1995
+ <ui-icon name="exclamation-triangle" class="text-xl"></ui-icon>
1996
+ <div class="flex-1">
1997
+ <span class="font-semibold"
1998
+ >{{ 'iam.validation.warning.title' | translate }}:</span
1999
+ >
2000
+ <p class="text-sm mt-1 mb-0">
2001
+ {{
2002
+ (invalidActionsCount() > 1
2003
+ ? 'iam.validation.unmet.prerequisites.plural'
2004
+ : 'iam.validation.unmet.prerequisites.singular'
2005
+ ) | translate: { count: invalidActionsCount() }
2006
+ }}
2007
+ </p>
2008
+ </div>
2009
+ </div>
2010
+ </div>
2011
+ }
2012
+
2013
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
2014
+ <f-treetable
2015
+ [value]="treeNodes()"
2016
+ dataKey="id"
2017
+ [tableStyle]="{ 'min-width': '50rem' }"
2018
+ >
2019
+ <ng-template #header>
2020
+ <tr>
2021
+ <th class="w-12">
2022
+ <f-checkbox
2023
+ [ngModel]="allSelected()"
2024
+ [binary]="true"
2025
+ (ngModelChange)="toggleAll()"
2026
+ [fTooltip]="'shared.select.deselect.all' | translate"
2027
+ tooltipPosition="top"
2028
+ />
2029
+ </th>
2030
+ <th>{{ 'shared.name' | translate }}</th>
2031
+ <th class="hidden md:table-cell">
2032
+ {{ 'shared.code' | translate }}
2033
+ </th>
2034
+ <th>{{ 'iam.action.type' | translate }}</th>
2035
+ <th class="hidden lg:table-cell">
2036
+ {{ 'iam.permission.requirements' | translate }}
2037
+ </th>
2038
+ </tr>
2039
+ </ng-template>
2040
+ <ng-template #body let-rowNode let-rowData="rowData">
2041
+ @let hasUnmet = hasUnmetPrerequisites(rowData);
2042
+ @let tooltip = getTooltip(rowData);
2043
+ <tr [class.highlight-warning]="hasUnmet">
2044
+ <td class="w-12">
2045
+ <f-checkbox
2046
+ [ngModel]="selectionMap()[rowData.id]"
2047
+ [binary]="true"
2048
+ (ngModelChange)="onActionToggle(rowData, $event)"
2049
+ [fTooltip]="tooltip"
2050
+ tooltipPosition="top"
2051
+ />
2052
+ </td>
2053
+ <td>
2054
+ <f-treetabletoggler [rowNode]="rowNode" />
2055
+ <span class="inline-flex items-center gap-2">
2056
+ {{ rowData.name }}
2057
+ @if (hasUnmet) {
2058
+ <ui-icon name="exclamation-triangle" class="text-orange-500" [fTooltip]="
2059
+ 'iam.validation.unmet.prerequisites.tooltip'
2060
+ | translate
2061
+ "
2062
+ tooltipPosition="top"></ui-icon>
2063
+ }
2064
+ </span>
2065
+ </td>
2066
+ <td class="hidden md:table-cell">
2067
+ {{ rowData.code || '-' }}
2068
+ </td>
2069
+ <td>
2070
+ <f-tag
2071
+ [value]="rowData.actionType"
2072
+ [severity]="
2073
+ rowData.actionType === 'backend' ? 'info' : 'success'
2074
+ "
2075
+ />
2076
+ </td>
2077
+ <td class="hidden lg:table-cell">
2078
+ @if (rowData.permissionLogic) {
2079
+ <span class="text-sm text-muted-color">{{
2080
+ 'iam.permission.has.prerequisites' | translate
2081
+ }}</span>
2082
+ } @else {
2083
+ <span class="text-muted-color">-</span>
2084
+ }
2085
+ </td>
2086
+ </tr>
2087
+ </ng-template>
2088
+ <ng-template #emptymessage>
2089
+ <tr>
2090
+ <td colspan="5" class="text-center p-4 text-muted-color">
2091
+ @if (loading()) {
2092
+ <ui-icon name="loader-circle" spin></ui-icon>
2093
+ {{ 'shared.loading.actions' | translate }}
2094
+ } @else {
2095
+ {{ 'iam.permission.no.actions.available' | translate }}
2096
+ }
2097
+ </td>
2098
+ </tr>
2099
+ </ng-template>
2100
+ </f-treetable>
2101
+ </div>
2102
+ </div>
2103
+
2104
+ @if (hasChanges()) {
2105
+ <div class="border border-surface rounded-border p-3 mt-4">
2106
+ <div class="flex items-center gap-2 mb-3">
2107
+ <ui-icon name="info-circle" class="text-primary"></ui-icon>
2108
+ <span class="font-bold">{{
2109
+ 'shared.pending.changes' | translate
2110
+ }}</span>
2111
+ </div>
2112
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
2113
+ @if (pendingAdd().length > 0) {
2114
+ <div>
2115
+ <div class="flex items-center gap-2 mb-2">
2116
+ <ui-icon name="plus-circle" class="text-green-500"></ui-icon>
2117
+ <strong class="text-sm"
2118
+ >{{ 'shared.to.add' | translate }} ({{
2119
+ pendingAdd().length
2120
+ }})</strong
2121
+ >
2122
+ </div>
2123
+ <ul class="list-none p-0 m-0 pl-4">
2124
+ @for (action of pendingAdd(); track action.id) {
2125
+ <li class="text-sm mb-1">{{ action.name }}</li>
2126
+ }
2127
+ </ul>
2128
+ </div>
2129
+ }
2130
+ @if (pendingRemove().length > 0) {
2131
+ <div>
2132
+ <div class="flex items-center gap-2 mb-2">
2133
+ <ui-icon name="minus-circle" class="text-red-500"></ui-icon>
2134
+ <strong class="text-sm"
2135
+ >{{ 'shared.to.remove' | translate }} ({{
2136
+ pendingRemove().length
2137
+ }})</strong
2138
+ >
2139
+ </div>
2140
+ <ul class="list-none p-0 m-0 pl-4">
2141
+ @for (action of pendingRemove(); track action.id) {
2142
+ <li class="text-sm mb-1">{{ action.name }}</li>
2143
+ }
2144
+ </ul>
2145
+ </div>
2146
+ }
2147
+ </div>
2148
+ </div>
2149
+ }
2150
+ }
2151
+
2152
+ @if (!loading() && actions().length === 0) {
2153
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
2154
+ <ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
2155
+ <p class="text-muted-color m-0">
2156
+ {{ 'iam.permission.no.actions.for.role' | translate }}
2157
+ </p>
2158
+ </div>
2159
+ }
2160
+ }
2161
+ </div>
2162
+ `, styles: [":host{display:block}.validation-warning{background-color:var(--f-yellow-50, #fefce8);border-left:4px solid var(--f-yellow-500, #eab308);color:var(--f-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--f-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--f-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"] }]
2163
+ }], ctorParameters: () => [] });
2164
+
2165
+ class CompanyActionSelectorComponent extends BaseActionSelectorComponent {
2166
+ COMPANY_ACTION_PERMISSIONS = COMPANY_ACTION_PERMISSIONS;
2167
+ companyApiProvider = inject(COMPANY_API_PROVIDER);
2168
+ actionApi = inject(ActionApiService);
2169
+ permissionApi = inject(PermissionApiService);
2170
+ messageService = inject(MessageService);
2171
+ confirmationService = inject(ConfirmationService);
2172
+ destroyRef = inject(DestroyRef);
2173
+ selectedCompanyId = signal(undefined, /* @ts-ignore */
2174
+ ...(ngDevMode ? [{ debugName: "selectedCompanyId" }] : /* istanbul ignore next */ []));
2175
+ companies = signal([], /* @ts-ignore */
2176
+ ...(ngDevMode ? [{ debugName: "companies" }] : /* istanbul ignore next */ []));
2177
+ loadDataAbortController = null;
2178
+ constructor() {
2179
+ super();
2180
+ this.destroyRef.onDestroy(() => {
2181
+ this.loadDataAbortController?.abort();
2182
+ });
2183
+ this.loadCompanies();
2184
+ effect(() => {
2185
+ const companyId = this.selectedCompanyId();
2186
+ this.loadDataAbortController?.abort();
2187
+ if (companyId) {
2188
+ this.loadDataAbortController = new AbortController();
2189
+ this.loadData(this.loadDataAbortController.signal).catch((err) => {
2190
+ if (err.name !== 'AbortError') {
2191
+ this.loading.set(false);
2192
+ }
2193
+ });
2194
+ }
2195
+ else {
2196
+ this.resetState();
2197
+ }
2198
+ });
2199
+ }
2200
+ getDefaultTooltip(isSelected) {
2201
+ if (isSelected) {
2202
+ return `[${this.translate('iam.tooltip.selected')}] ${this.translate('iam.tooltip.selected')}\n\n[${this.translate('shared.remove')}] ${this.translate('iam.tooltip.click.to.remove')}`;
2203
+ }
2204
+ return `[${this.translate('shared.add')}] ${this.translate('iam.tooltip.click.to.add')}`;
2205
+ }
2206
+ async loadCompanies() {
2207
+ try {
2208
+ const response = await firstValueFrom(this.companyApiProvider.getCompanies({
2209
+ page: 0,
2210
+ pageSize: MAX_DROPDOWN_ITEMS,
2211
+ }));
2212
+ this.companies.set(response?.data ?? []);
2213
+ }
2214
+ catch { }
2215
+ }
2216
+ async loadData(signal) {
2217
+ const companyId = this.selectedCompanyId();
2218
+ if (!companyId)
2219
+ return;
2220
+ this.loading.set(true);
2221
+ try {
2222
+ const actionsResponse = await firstValueFrom(this.actionApi.getTree(undefined, undefined, false));
2223
+ if (signal.aborted)
2224
+ return;
2225
+ const actionsTree = actionsResponse?.data ?? [];
2226
+ const actionsList = flattenTree(actionsTree).filter((a) => !!a.id);
2227
+ this._actionsTree.set(actionsTree);
2228
+ this._actions.set(actionsList);
2229
+ const assignedResponse = await firstValueFrom(this.permissionApi.getCompanyActions(companyId));
2230
+ if (signal.aborted)
2231
+ return;
2232
+ const assignedIds = new Set((assignedResponse?.data || []).map((a) => a.actionId));
2233
+ const selMap = this.buildSelectionMap(actionsList, (action) => assignedIds.has(action.id));
2234
+ this._selectionMap.set(selMap);
2235
+ this._initialSelection.set({ ...selMap });
2236
+ }
2237
+ finally {
2238
+ if (!signal.aborted) {
2239
+ this.loading.set(false);
2240
+ }
2241
+ }
2242
+ }
2243
+ async saveChanges() {
2244
+ const companyId = this.selectedCompanyId();
2245
+ if (!companyId)
2246
+ return;
2247
+ const invalidActions = this.permissionLogic.getActionsWithUnmetPrerequisites(this.selectionMap(), this.actions());
2248
+ if (invalidActions.length > 0) {
2249
+ this.permissionLogic.showValidationErrorDialog(invalidActions, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
2250
+ return;
2251
+ }
2252
+ const items = this.buildPayloadItems();
2253
+ if (items.length === 0)
2254
+ return;
2255
+ this.saving.set(true);
2256
+ try {
2257
+ const response = await firstValueFrom(this.permissionApi.assignCompanyActions({ companyId, items }));
2258
+ if (response?.data?.prerequisiteErrors &&
2259
+ response.data.prerequisiteErrors.length > 0) {
2260
+ this.handleBackendPrerequisiteErrors(response.data.prerequisiteErrors);
2261
+ return;
2262
+ }
2263
+ this.messageService.add({
2264
+ severity: 'success',
2265
+ summary: this.translate('shared.success'),
2266
+ detail: response?.messageKey
2267
+ ? this.translate(response.messageKey, response.messageVariables)
2268
+ : (response?.message ?? ''),
2269
+ });
2270
+ this._initialSelection.set({ ...this.selectionMap() });
2271
+ }
2272
+ catch {
2273
+ }
2274
+ finally {
2275
+ this.saving.set(false);
2276
+ }
2277
+ }
2278
+ handleBackendPrerequisiteErrors(prerequisiteErrors) {
2279
+ const errorList = prerequisiteErrors
2280
+ .map((error) => {
2281
+ const requiredNames = error.requiredActions
2282
+ .map((a) => a.actionName)
2283
+ .join(', ');
2284
+ return `${error.actionName} ${this.translate('iam.permission.requires')}: ${requiredNames}`;
2285
+ })
2286
+ .join('\n');
2287
+ this.confirmationService.confirm({
2288
+ header: this.translate('iam.permission.prerequisite.validation.failed'),
2289
+ message: `${this.translate('iam.permission.prerequisite.error.message')}\n\n${errorList}\n\n${this.translate('iam.permission.auto.select.prompt')}`,
2290
+ icon: 'exclamation-triangle',
2291
+ acceptLabel: this.translate('iam.permission.auto.select.required'),
2292
+ rejectLabel: this.translate('shared.cancel'),
2293
+ acceptIcon: 'check',
2294
+ rejectIcon: 'times',
2295
+ accept: () => {
2296
+ const selMap = { ...this.selectionMap() };
2297
+ prerequisiteErrors.forEach((error) => {
2298
+ error.requiredActions.forEach((a) => {
2299
+ selMap[a.actionId] = true;
2300
+ });
2301
+ });
2302
+ this._selectionMap.set(selMap);
2303
+ this.messageService.add({
2304
+ severity: 'info',
2305
+ summary: this.translate('iam.permission.actions.selected'),
2306
+ detail: this.translate('iam.permission.auto.selected.prerequisites'),
2307
+ });
2308
+ },
2309
+ reject: () => {
2310
+ this._selectionMap.set({ ...this.initialSelection() });
2311
+ this.messageService.add({
2312
+ severity: 'info',
2313
+ summary: this.translate('iam.permission.changes.reverted'),
2314
+ detail: this.translate('iam.permission.selection.reverted'),
2315
+ });
2316
+ },
2317
+ });
2318
+ }
2319
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: CompanyActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2320
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: CompanyActionSelectorComponent, isStandalone: true, selector: "flusys-company-action-selector", usesInheritance: true, ngImport: i0, template: `
2321
+ <div class="company-action-selector">
2322
+ <div class="mb-4">
2323
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
2324
+ <div>
2325
+ <label class="block font-semibold mb-2">{{
2326
+ 'iam.permission.select.company' | translate
2327
+ }}</label>
2328
+ <f-select
2329
+ [ngModel]="selectedCompanyId()"
2330
+ (ngModelChange)="selectedCompanyId.set($event)"
2331
+ [options]="companies()"
2332
+ optionLabel="name"
2333
+ optionValue="id"
2334
+ [placeholder]="
2335
+ 'iam.permission.select.company.placeholder' | translate
2336
+ "
2337
+ [showClear]="true"
2338
+ [filter]="true"
2339
+ class="w-full"
2340
+ />
2341
+ </div>
2342
+ </div>
2343
+ </div>
2344
+
2345
+ @if (selectedCompanyId()) {
2346
+ @if (loading()) {
2347
+ <div
2348
+ class="surface-card p-5 rounded-border shadow-sm flex justify-center"
2349
+ >
2350
+ <ui-icon name="loader-circle" spin class="text-primary text-5xl"></ui-icon>
2351
+ </div>
2352
+ }
2353
+
2354
+ @if (!loading() && actions().length > 0) {
2355
+ <div class="surface-card p-4 rounded-border shadow-sm">
2356
+ <div
2357
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
2358
+ >
2359
+ <div>
2360
+ <h5 class="m-0 mb-1">
2361
+ {{ 'iam.permission.action.whitelist' | translate }}
2362
+ </h5>
2363
+ <p class="text-sm text-muted-color m-0">
2364
+ {{
2365
+ 'iam.permission.actions.available'
2366
+ | translate: { count: actions().length }
2367
+ }}
2368
+ </p>
2369
+ </div>
2370
+ <div class="flex flex-wrap gap-2">
2371
+ <button
2372
+ fButton
2373
+ [outlined]="true"
2374
+ size="small"
2375
+ severity="secondary"
2376
+ (click)="selectAll()"
2377
+ >
2378
+ <ui-icon pButtonIcon name="check"></ui-icon>
2379
+ <span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
2380
+ </button>
2381
+ <button
2382
+ fButton
2383
+ [outlined]="true"
2384
+ size="small"
2385
+ severity="secondary"
2386
+ (click)="deselectAll()"
2387
+ >
2388
+ <ui-icon pButtonIcon name="times"></ui-icon>
2389
+ <span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
2390
+ </button>
2391
+ <button
2392
+ fButton
2393
+ *hasPermission="COMPANY_ACTION_PERMISSIONS.ASSIGN"
2394
+ [disabled]="!canSave() || saving()"
2395
+ size="small"
2396
+ (click)="saveChanges()"
2397
+ >
2398
+ @if (saving()) {
2399
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
2400
+ } @else {
2401
+ <ui-icon pButtonIcon name="save"></ui-icon>
2402
+ }
2403
+ <span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
2404
+ </button>
2405
+ </div>
2406
+ </div>
2407
+
2408
+ @if (invalidActionsCount() > 0) {
2409
+ <div class="validation-warning rounded-border p-3 mb-4">
2410
+ <div class="flex items-start gap-2">
2411
+ <ui-icon name="exclamation-triangle" class="text-xl"></ui-icon>
2412
+ <div class="flex-1">
2413
+ <span class="font-semibold"
2414
+ >{{ 'iam.validation.warning.title' | translate }}:</span
2415
+ >
2416
+ <p class="text-sm mt-1 mb-0">
2417
+ {{
2418
+ (invalidActionsCount() > 1
2419
+ ? 'iam.validation.unmet.prerequisites.plural'
2420
+ : 'iam.validation.unmet.prerequisites.singular'
2421
+ ) | translate: { count: invalidActionsCount() }
2422
+ }}
2423
+ </p>
2424
+ </div>
2425
+ </div>
2426
+ </div>
2427
+ }
2428
+
2429
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
2430
+ <f-treetable
2431
+ [value]="treeNodes()"
2432
+ dataKey="id"
2433
+ [tableStyle]="{ 'min-width': '50rem' }"
2434
+ >
2435
+ <ng-template #header>
2436
+ <tr>
2437
+ <th class="w-12">
2438
+ <f-checkbox
2439
+ [ngModel]="allSelected()"
2440
+ [binary]="true"
2441
+ (ngModelChange)="toggleAll()"
2442
+ [fTooltip]="'shared.select.deselect.all' | translate"
2443
+ tooltipPosition="top"
2444
+ />
2445
+ </th>
2446
+ <th>{{ 'shared.name' | translate }}</th>
2447
+ <th class="hidden md:table-cell">
2448
+ {{ 'shared.code' | translate }}
2449
+ </th>
2450
+ <th>{{ 'iam.action.type' | translate }}</th>
2451
+ <th class="hidden lg:table-cell">
2452
+ {{ 'shared.description' | translate }}
2453
+ </th>
2454
+ </tr>
2455
+ </ng-template>
2456
+ <ng-template #body let-rowNode let-rowData="rowData">
2457
+ @let hasUnmet = hasUnmetPrerequisites(rowData);
2458
+ <tr [class.highlight-warning]="hasUnmet">
2459
+ <td class="w-12">
2460
+ <f-checkbox
2461
+ [ngModel]="selectionMap()[rowData.id]"
2462
+ [binary]="true"
2463
+ (ngModelChange)="onActionToggle(rowData, $event)"
2464
+ [fTooltip]="getTooltip(rowData)"
2465
+ tooltipPosition="top"
2466
+ />
2467
+ </td>
2468
+ <td>
2469
+ <f-treetabletoggler [rowNode]="rowNode" />
2470
+ <span class="inline-flex items-center gap-2">
2471
+ {{ rowData.name }}
2472
+ @if (hasUnmet) {
2473
+ <ui-icon name="exclamation-triangle" class="text-orange-500" [fTooltip]="
2474
+ 'iam.validation.unmet.prerequisites.tooltip'
2475
+ | translate
2476
+ "
2477
+ tooltipPosition="top"></ui-icon>
2478
+ }
2479
+ </span>
2480
+ </td>
2481
+ <td class="hidden md:table-cell">
2482
+ {{ rowData.code || '-' }}
2483
+ </td>
2484
+ <td>
2485
+ <f-tag
2486
+ [value]="rowData.actionType"
2487
+ [severity]="
2488
+ rowData.actionType === 'backend' ? 'info' : 'success'
2489
+ "
2490
+ />
2491
+ </td>
2492
+ <td class="hidden lg:table-cell">
2493
+ {{ rowData.description || '-' }}
2494
+ </td>
2495
+ </tr>
2496
+ </ng-template>
2497
+ <ng-template #emptymessage>
2498
+ <tr>
2499
+ <td colspan="5" class="text-center p-4 text-muted-color">
2500
+ @if (loading()) {
2501
+ <ui-icon name="loader-circle" spin></ui-icon>
2502
+ {{ 'shared.loading.actions' | translate }}
2503
+ } @else {
2504
+ {{ 'iam.permission.no.actions.available' | translate }}
2505
+ }
2506
+ </td>
2507
+ </tr>
2508
+ </ng-template>
2509
+ </f-treetable>
2510
+ </div>
2511
+ </div>
2512
+
2513
+ @if (hasChanges()) {
2514
+ <div class="border border-surface rounded-border p-3 mt-4">
2515
+ <div class="flex items-center gap-2 mb-3">
2516
+ <ui-icon name="info-circle" class="text-primary"></ui-icon>
2517
+ <span class="font-bold">{{
2518
+ 'shared.pending.changes' | translate
2519
+ }}</span>
2520
+ </div>
2521
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
2522
+ @if (pendingAdd().length > 0) {
2523
+ <div>
2524
+ <div class="flex items-center gap-2 mb-2">
2525
+ <ui-icon name="plus-circle" class="text-green-500"></ui-icon>
2526
+ <strong class="text-sm"
2527
+ >{{ 'shared.to.whitelist' | translate }} ({{
2528
+ pendingAdd().length
2529
+ }})</strong
2530
+ >
2531
+ </div>
2532
+ <ul class="list-none p-0 m-0 pl-4">
2533
+ @for (action of pendingAdd(); track action.id) {
2534
+ <li class="text-sm mb-1">{{ action.name }}</li>
2535
+ }
2536
+ </ul>
2537
+ </div>
2538
+ }
2539
+ @if (pendingRemove().length > 0) {
2540
+ <div>
2541
+ <div class="flex items-center gap-2 mb-2">
2542
+ <ui-icon name="minus-circle" class="text-red-500"></ui-icon>
2543
+ <strong class="text-sm"
2544
+ >{{ 'shared.to.remove' | translate }} ({{
2545
+ pendingRemove().length
2546
+ }})</strong
2547
+ >
2548
+ </div>
2549
+ <ul class="list-none p-0 m-0 pl-4">
2550
+ @for (action of pendingRemove(); track action.id) {
2551
+ <li class="text-sm mb-1">{{ action.name }}</li>
2552
+ }
2553
+ </ul>
2554
+ </div>
2555
+ }
2556
+ </div>
2557
+ </div>
2558
+ }
2559
+ }
2560
+
2561
+ @if (!loading() && actions().length === 0) {
2562
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
2563
+ <ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
2564
+ <p class="text-muted-color m-0">
2565
+ {{ 'iam.permission.no.actions.for.company' | translate }}
2566
+ </p>
2567
+ </div>
2568
+ }
2569
+ }
2570
+ </div>
2571
+ `, isInline: true, styles: [":host{display:block}.validation-warning{background-color:var(--f-yellow-50, #fefce8);border-left:4px solid var(--f-yellow-500, #eab308);color:var(--f-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--f-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--f-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: UiModule }, { kind: "directive", type: i2.FButtonDirective, selector: "[fButton]", inputs: ["severity", "text", "outlined", "link", "variant", "rounded", "raised", "fluid", "disabled", "loading", "loadingIcon", "size", "label", "icon", "iconPos", "ariaLabel", "plain"] }, { kind: "component", type: i2.TagComponent, selector: "f-tag", inputs: ["value", "severity", "icon", "rounded", "color", "styleClass", "style"] }, { kind: "component", type: i2.CheckboxComponent, selector: "f-checkbox", inputs: ["label", "binary", "disabled", "readonly", "inputId", "invalid", "indeterminate", "variant", "size", "touched", "styleClass", "style", "checked"], outputs: ["onChange", "onBlur", "touch", "checkedChange"] }, { kind: "component", type: i2.SelectComponent, selector: "f-select", inputs: ["options", "optionLabel", "optionValue", "placeholder", "disabled", "filter", "filterBy", "filterMatchMode", "filterLocale", "filterPlaceholder", "resetFilterOnHide", "autofocusFilter", "emptyMessage", "emptyFilterMessage", "showClear", "loading", "loadingIcon", "dropdownIcon", "clearIcon", "inputId", "invalid", "touched", "styleClass", "style", "editable", "readonly", "autofocus", "checkmark", "group", "optionGroupLabel", "optionGroupChildren", "virtualScroll", "virtualScrollItemSize", "lazy"], outputs: ["onFilter", "onChange", "onShow", "onHide", "onBlur", "onFocus", "onClear", "onLazyLoad"] }, { kind: "directive", type: i2.TooltipDirective, selector: "[fTooltip]", inputs: ["fTooltip", "tooltipPosition", "positionStyle", "tooltipStyleClass", "disabled", "tooltipEvent", "showDelay", "hideDelay", "life", "autoHide", "positionTop", "positionLeft", "escape"] }, { kind: "component", type: i2.TreeTableComponent, selector: "f-treetable", inputs: ["value", "loading", "dataKey", "styleClass", "tableStyle", "scrollable", "scrollHeight", "resizableColumns", "lazy", "totalRecords", "paginator", "first", "rows", "rowsPerPageOptions", "showCurrentPageReport", "currentPageReportTemplate", "globalFilterFields", "filterDelay", "selectionMode", "sortMode", "virtualScroll", "virtualScrollItemSize", "sortField", "sortOrder", "multiSortMeta", "filters", "selection", "contextMenuSelection"], outputs: ["sortFieldChange", "sortOrderChange", "multiSortMetaChange", "filtersChange", "selectionChange", "contextMenuSelectionChange", "onLazyLoad", "onSort", "onFilter", "onNodeExpand", "onNodeCollapse", "onNodeSelect", "onNodeUnselect", "onRowContextMenu"] }, { kind: "component", type: i2.TreeTableTogglerComponent, selector: "f-treetabletoggler", inputs: ["rowNode"] }, { kind: "component", type: i2.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { 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: "directive", type: HasPermissionDirective, selector: "[hasPermission]", inputs: ["hasPermission"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
2572
+ }
2573
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: CompanyActionSelectorComponent, decorators: [{
2574
+ type: Component,
2575
+ args: [{ selector: 'flusys-company-action-selector', imports: [UiModule, FormsModule, HasPermissionDirective, TranslatePipe], template: `
2576
+ <div class="company-action-selector">
2577
+ <div class="mb-4">
2578
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
2579
+ <div>
2580
+ <label class="block font-semibold mb-2">{{
2581
+ 'iam.permission.select.company' | translate
2582
+ }}</label>
2583
+ <f-select
2584
+ [ngModel]="selectedCompanyId()"
2585
+ (ngModelChange)="selectedCompanyId.set($event)"
2586
+ [options]="companies()"
2587
+ optionLabel="name"
2588
+ optionValue="id"
2589
+ [placeholder]="
2590
+ 'iam.permission.select.company.placeholder' | translate
2591
+ "
2592
+ [showClear]="true"
2593
+ [filter]="true"
2594
+ class="w-full"
2595
+ />
2596
+ </div>
2597
+ </div>
2598
+ </div>
2599
+
2600
+ @if (selectedCompanyId()) {
2601
+ @if (loading()) {
2602
+ <div
2603
+ class="surface-card p-5 rounded-border shadow-sm flex justify-center"
2604
+ >
2605
+ <ui-icon name="loader-circle" spin class="text-primary text-5xl"></ui-icon>
2606
+ </div>
2607
+ }
2608
+
2609
+ @if (!loading() && actions().length > 0) {
2610
+ <div class="surface-card p-4 rounded-border shadow-sm">
2611
+ <div
2612
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
2613
+ >
2614
+ <div>
2615
+ <h5 class="m-0 mb-1">
2616
+ {{ 'iam.permission.action.whitelist' | translate }}
2617
+ </h5>
2618
+ <p class="text-sm text-muted-color m-0">
2619
+ {{
2620
+ 'iam.permission.actions.available'
2621
+ | translate: { count: actions().length }
2622
+ }}
2623
+ </p>
2624
+ </div>
2625
+ <div class="flex flex-wrap gap-2">
2626
+ <button
2627
+ fButton
2628
+ [outlined]="true"
2629
+ size="small"
2630
+ severity="secondary"
2631
+ (click)="selectAll()"
2632
+ >
2633
+ <ui-icon pButtonIcon name="check"></ui-icon>
2634
+ <span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
2635
+ </button>
2636
+ <button
2637
+ fButton
2638
+ [outlined]="true"
2639
+ size="small"
2640
+ severity="secondary"
2641
+ (click)="deselectAll()"
2642
+ >
2643
+ <ui-icon pButtonIcon name="times"></ui-icon>
2644
+ <span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
2645
+ </button>
2646
+ <button
2647
+ fButton
2648
+ *hasPermission="COMPANY_ACTION_PERMISSIONS.ASSIGN"
2649
+ [disabled]="!canSave() || saving()"
2650
+ size="small"
2651
+ (click)="saveChanges()"
2652
+ >
2653
+ @if (saving()) {
2654
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
2655
+ } @else {
2656
+ <ui-icon pButtonIcon name="save"></ui-icon>
2657
+ }
2658
+ <span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
2659
+ </button>
2660
+ </div>
2661
+ </div>
2662
+
2663
+ @if (invalidActionsCount() > 0) {
2664
+ <div class="validation-warning rounded-border p-3 mb-4">
2665
+ <div class="flex items-start gap-2">
2666
+ <ui-icon name="exclamation-triangle" class="text-xl"></ui-icon>
2667
+ <div class="flex-1">
2668
+ <span class="font-semibold"
2669
+ >{{ 'iam.validation.warning.title' | translate }}:</span
2670
+ >
2671
+ <p class="text-sm mt-1 mb-0">
2672
+ {{
2673
+ (invalidActionsCount() > 1
2674
+ ? 'iam.validation.unmet.prerequisites.plural'
2675
+ : 'iam.validation.unmet.prerequisites.singular'
2676
+ ) | translate: { count: invalidActionsCount() }
2677
+ }}
2678
+ </p>
2679
+ </div>
2680
+ </div>
2681
+ </div>
2682
+ }
2683
+
2684
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
2685
+ <f-treetable
2686
+ [value]="treeNodes()"
2687
+ dataKey="id"
2688
+ [tableStyle]="{ 'min-width': '50rem' }"
2689
+ >
2690
+ <ng-template #header>
2691
+ <tr>
2692
+ <th class="w-12">
2693
+ <f-checkbox
2694
+ [ngModel]="allSelected()"
2695
+ [binary]="true"
2696
+ (ngModelChange)="toggleAll()"
2697
+ [fTooltip]="'shared.select.deselect.all' | translate"
2698
+ tooltipPosition="top"
2699
+ />
2700
+ </th>
2701
+ <th>{{ 'shared.name' | translate }}</th>
2702
+ <th class="hidden md:table-cell">
2703
+ {{ 'shared.code' | translate }}
2704
+ </th>
2705
+ <th>{{ 'iam.action.type' | translate }}</th>
2706
+ <th class="hidden lg:table-cell">
2707
+ {{ 'shared.description' | translate }}
2708
+ </th>
2709
+ </tr>
2710
+ </ng-template>
2711
+ <ng-template #body let-rowNode let-rowData="rowData">
2712
+ @let hasUnmet = hasUnmetPrerequisites(rowData);
2713
+ <tr [class.highlight-warning]="hasUnmet">
2714
+ <td class="w-12">
2715
+ <f-checkbox
2716
+ [ngModel]="selectionMap()[rowData.id]"
2717
+ [binary]="true"
2718
+ (ngModelChange)="onActionToggle(rowData, $event)"
2719
+ [fTooltip]="getTooltip(rowData)"
2720
+ tooltipPosition="top"
2721
+ />
2722
+ </td>
2723
+ <td>
2724
+ <f-treetabletoggler [rowNode]="rowNode" />
2725
+ <span class="inline-flex items-center gap-2">
2726
+ {{ rowData.name }}
2727
+ @if (hasUnmet) {
2728
+ <ui-icon name="exclamation-triangle" class="text-orange-500" [fTooltip]="
2729
+ 'iam.validation.unmet.prerequisites.tooltip'
2730
+ | translate
2731
+ "
2732
+ tooltipPosition="top"></ui-icon>
2733
+ }
2734
+ </span>
2735
+ </td>
2736
+ <td class="hidden md:table-cell">
2737
+ {{ rowData.code || '-' }}
2738
+ </td>
2739
+ <td>
2740
+ <f-tag
2741
+ [value]="rowData.actionType"
2742
+ [severity]="
2743
+ rowData.actionType === 'backend' ? 'info' : 'success'
2744
+ "
2745
+ />
2746
+ </td>
2747
+ <td class="hidden lg:table-cell">
2748
+ {{ rowData.description || '-' }}
2749
+ </td>
2750
+ </tr>
2751
+ </ng-template>
2752
+ <ng-template #emptymessage>
2753
+ <tr>
2754
+ <td colspan="5" class="text-center p-4 text-muted-color">
2755
+ @if (loading()) {
2756
+ <ui-icon name="loader-circle" spin></ui-icon>
2757
+ {{ 'shared.loading.actions' | translate }}
2758
+ } @else {
2759
+ {{ 'iam.permission.no.actions.available' | translate }}
2760
+ }
2761
+ </td>
2762
+ </tr>
2763
+ </ng-template>
2764
+ </f-treetable>
2765
+ </div>
2766
+ </div>
2767
+
2768
+ @if (hasChanges()) {
2769
+ <div class="border border-surface rounded-border p-3 mt-4">
2770
+ <div class="flex items-center gap-2 mb-3">
2771
+ <ui-icon name="info-circle" class="text-primary"></ui-icon>
2772
+ <span class="font-bold">{{
2773
+ 'shared.pending.changes' | translate
2774
+ }}</span>
2775
+ </div>
2776
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
2777
+ @if (pendingAdd().length > 0) {
2778
+ <div>
2779
+ <div class="flex items-center gap-2 mb-2">
2780
+ <ui-icon name="plus-circle" class="text-green-500"></ui-icon>
2781
+ <strong class="text-sm"
2782
+ >{{ 'shared.to.whitelist' | translate }} ({{
2783
+ pendingAdd().length
2784
+ }})</strong
2785
+ >
2786
+ </div>
2787
+ <ul class="list-none p-0 m-0 pl-4">
2788
+ @for (action of pendingAdd(); track action.id) {
2789
+ <li class="text-sm mb-1">{{ action.name }}</li>
2790
+ }
2791
+ </ul>
2792
+ </div>
2793
+ }
2794
+ @if (pendingRemove().length > 0) {
2795
+ <div>
2796
+ <div class="flex items-center gap-2 mb-2">
2797
+ <ui-icon name="minus-circle" class="text-red-500"></ui-icon>
2798
+ <strong class="text-sm"
2799
+ >{{ 'shared.to.remove' | translate }} ({{
2800
+ pendingRemove().length
2801
+ }})</strong
2802
+ >
2803
+ </div>
2804
+ <ul class="list-none p-0 m-0 pl-4">
2805
+ @for (action of pendingRemove(); track action.id) {
2806
+ <li class="text-sm mb-1">{{ action.name }}</li>
2807
+ }
2808
+ </ul>
2809
+ </div>
2810
+ }
2811
+ </div>
2812
+ </div>
2813
+ }
2814
+ }
2815
+
2816
+ @if (!loading() && actions().length === 0) {
2817
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
2818
+ <ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
2819
+ <p class="text-muted-color m-0">
2820
+ {{ 'iam.permission.no.actions.for.company' | translate }}
2821
+ </p>
2822
+ </div>
2823
+ }
2824
+ }
2825
+ </div>
2826
+ `, styles: [":host{display:block}.validation-warning{background-color:var(--f-yellow-50, #fefce8);border-left:4px solid var(--f-yellow-500, #eab308);color:var(--f-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--f-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--f-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"] }]
2827
+ }], ctorParameters: () => [] });
2828
+
2829
+ /**
2830
+ * User-Role Selector Component
2831
+ *
2832
+ * Manages user role assignments with branch-level granularity.
2833
+ *
2834
+ * **Architecture:**
2835
+ * - Presentation: DataTable for role list
2836
+ * - Business Logic: Role assignment without dependency validation
2837
+ * - State: Signals with computed dependencies
2838
+ *
2839
+ * **Core Features:**
2840
+ * - User dropdown with async loading
2841
+ * - Branch selector (if company feature enabled)
2842
+ * - Role assignment management (add/remove)
2843
+ * - Real-time change tracking
2844
+ *
2845
+ * **Granularity:**
2846
+ * - Company-wide: branchId = undefined
2847
+ * - Branch-specific: branchId = selected value
2848
+ *
2849
+ * **Performance:**
2850
+ * - Computed signals for reactive updates
2851
+ * - Branch filtering on company context change
2852
+ *
2853
+ * @example
2854
+ * ```html
2855
+ * <flusys-user-role-selector />
2856
+ * ```
2857
+ */
2858
+ class UserRoleSelectorComponent {
2859
+ // Permission constants for template
2860
+ USER_ROLE_PERMISSIONS = USER_ROLE_PERMISSIONS;
2861
+ // Dependencies
2862
+ appConfig = inject(APP_CONFIG);
2863
+ companyContext = inject(LAYOUT_AUTH_STATE);
2864
+ userPermissionProvider = inject(USER_PERMISSION_PROVIDER);
2865
+ roleApi = inject(RoleApiService);
2866
+ permissionApi = inject(PermissionApiService);
2867
+ messageService = inject(MessageService);
2868
+ translateAdapter = inject(TranslateService, {
2869
+ optional: true,
2870
+ });
2871
+ translate(key, vars) {
2872
+ return this.translateAdapter?.translate(key, vars) ?? key;
2873
+ }
2874
+ // State - User/Branch Selection
2875
+ selectedUserId = signal(null, /* @ts-ignore */
2876
+ ...(ngDevMode ? [{ debugName: "selectedUserId" }] : /* istanbul ignore next */ []));
2877
+ selectedBranchId = signal(undefined, /* @ts-ignore */
2878
+ ...(ngDevMode ? [{ debugName: "selectedBranchId" }] : /* istanbul ignore next */ []));
2879
+ branches = signal([], /* @ts-ignore */
2880
+ ...(ngDevMode ? [{ debugName: "branches" }] : /* istanbul ignore next */ []));
2881
+ // State - Loading/Saving
2882
+ loading = signal(false, /* @ts-ignore */
2883
+ ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
2884
+ saving = signal(false, /* @ts-ignore */
2885
+ ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
2886
+ // State - Roles (Business Logic)
2887
+ _roles = signal([], /* @ts-ignore */
2888
+ ...(ngDevMode ? [{ debugName: "_roles" }] : /* istanbul ignore next */ []));
2889
+ roles = this._roles.asReadonly();
2890
+ // State - Selection
2891
+ _selectionMap = signal({}, /* @ts-ignore */
2892
+ ...(ngDevMode ? [{ debugName: "_selectionMap" }] : /* istanbul ignore next */ []));
2893
+ selectionMap = this._selectionMap.asReadonly();
2894
+ _initialSelection = signal({}, /* @ts-ignore */
2895
+ ...(ngDevMode ? [{ debugName: "_initialSelection" }] : /* istanbul ignore next */ []));
2896
+ initialSelection = this._initialSelection.asReadonly();
2897
+ // Computed - UI Flags
2898
+ showBranchSelector = computed(() => isCompanyFeatureEnabled(this.appConfig) === true, /* @ts-ignore */
2899
+ ...(ngDevMode ? [{ debugName: "showBranchSelector" }] : /* istanbul ignore next */ []));
2900
+ filteredBranches = computed(() => {
2901
+ const currentCompanyId = this.companyContext.currentCompanyInfo()?.id || undefined;
2902
+ if (!currentCompanyId)
2903
+ return this.branches();
2904
+ return this.branches().filter((b) => b.companyId === currentCompanyId);
2905
+ }, /* @ts-ignore */
2906
+ ...(ngDevMode ? [{ debugName: "filteredBranches" }] : /* istanbul ignore next */ []));
2907
+ // Computed - Selection State
2908
+ allSelected = computed(() => {
2909
+ const selMap = this.selectionMap();
2910
+ const total = this.roles().length;
2911
+ if (total === 0)
2912
+ return false;
2913
+ const selected = Object.values(selMap).filter((v) => v).length;
2914
+ return selected === total;
2915
+ }, /* @ts-ignore */
2916
+ ...(ngDevMode ? [{ debugName: "allSelected" }] : /* istanbul ignore next */ []));
2917
+ // Computed - Change Detection
2918
+ hasChanges = computed(() => {
2919
+ const current = this.selectionMap();
2920
+ const initial = this.initialSelection();
2921
+ const currentKeys = Object.keys(current);
2922
+ const initialKeys = Object.keys(initial);
2923
+ if (currentKeys.length !== initialKeys.length)
2924
+ return true;
2925
+ return currentKeys.some((key) => current[key] !== initial[key]);
2926
+ }, /* @ts-ignore */
2927
+ ...(ngDevMode ? [{ debugName: "hasChanges" }] : /* istanbul ignore next */ []));
2928
+ // Computed - Pending Changes
2929
+ pendingAdd = computed(() => {
2930
+ const current = this.selectionMap();
2931
+ const initial = this.initialSelection();
2932
+ const allRoles = this.roles();
2933
+ return allRoles.filter((role) => current[role.id] && !initial[role.id]);
2934
+ }, /* @ts-ignore */
2935
+ ...(ngDevMode ? [{ debugName: "pendingAdd" }] : /* istanbul ignore next */ []));
2936
+ pendingRemove = computed(() => {
2937
+ const current = this.selectionMap();
2938
+ const initial = this.initialSelection();
2939
+ const allRoles = this.roles();
2940
+ return allRoles.filter((role) => !current[role.id] && initial[role.id]);
2941
+ }, /* @ts-ignore */
2942
+ ...(ngDevMode ? [{ debugName: "pendingRemove" }] : /* istanbul ignore next */ []));
2943
+ // Computed - Save Button State
2944
+ canSave = computed(() => {
2945
+ return this.hasChanges() && !this.saving();
2946
+ }, /* @ts-ignore */
2947
+ ...(ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []));
2948
+ constructor() {
2949
+ // Effect: Load user branches and data when user changes
2950
+ effect(() => {
2951
+ const userId = this.selectedUserId();
2952
+ if (userId) {
2953
+ if (isCompanyFeatureEnabled(this.appConfig)) {
2954
+ this.loadUserBranches(userId);
2955
+ }
2956
+ this.loadData();
2957
+ }
2958
+ else {
2959
+ this.branches.set([]);
2960
+ this.selectedBranchId.set(undefined);
2961
+ this.resetState();
2962
+ }
2963
+ });
2964
+ // Effect: Reload data when branch changes
2965
+ effect(() => {
2966
+ const userId = this.selectedUserId();
2967
+ const branchId = this.selectedBranchId();
2968
+ if (isCompanyFeatureEnabled(this.appConfig) &&
2969
+ userId &&
2970
+ branchId !== undefined) {
2971
+ this.loadData();
2972
+ }
2973
+ });
2974
+ }
2975
+ /**
2976
+ * Load user's permitted branches
2977
+ */
2978
+ async loadUserBranches(userId) {
2979
+ try {
2980
+ const companyId = this.companyContext.currentCompanyInfo()?.id || undefined;
2981
+ if (!companyId) {
2982
+ this.branches.set([]);
2983
+ return;
2984
+ }
2985
+ const response = await firstValueFrom(this.userPermissionProvider.getUserBranchPermissions(userId));
2986
+ const userBranches = response?.data?.map((ub) => ({
2987
+ id: ub.branchId,
2988
+ name: ub.branchName,
2989
+ companyId: ub.companyId,
2990
+ })) ?? [];
2991
+ this.branches.set(userBranches);
2992
+ }
2993
+ catch {
2994
+ // Error toast handled by global interceptor
2995
+ }
2996
+ }
2997
+ /**
2998
+ * Load roles and assignments for selected user
2999
+ */
3000
+ async loadData() {
3001
+ const userId = this.selectedUserId();
3002
+ if (!userId)
3003
+ return;
3004
+ const branchId = this.selectedBranchId();
3005
+ this.loading.set(true);
3006
+ try {
3007
+ // Load all roles
3008
+ const rolesResponse = await this.roleApi.getAll({
3009
+ pagination: { currentPage: 0, pageSize: MAX_DROPDOWN_ITEMS },
3010
+ });
3011
+ const rolesList = rolesResponse?.data ?? [];
3012
+ this._roles.set(rolesList);
3013
+ // Load existing user-role assignments
3014
+ const query = isCompanyFeatureEnabled(this.appConfig) && branchId
3015
+ ? { branchId }
3016
+ : undefined;
3017
+ const assignedResponse = await firstValueFrom(this.permissionApi.getUserRoles(userId, query));
3018
+ const assignedIds = new Set((assignedResponse?.data || []).map((r) => r.roleId));
3019
+ // Build selection map
3020
+ const selMap = {};
3021
+ rolesList.forEach((role) => {
3022
+ selMap[role.id] = assignedIds.has(role.id);
3023
+ });
3024
+ this._selectionMap.set(selMap);
3025
+ this._initialSelection.set({ ...selMap });
3026
+ }
3027
+ catch {
3028
+ // Error toast handled by global interceptor
3029
+ }
3030
+ finally {
3031
+ this.loading.set(false);
3032
+ }
3033
+ }
3034
+ /**
3035
+ * Get tooltip for role checkbox
3036
+ */
3037
+ getTooltip(role) {
3038
+ const selMap = this.selectionMap();
3039
+ const isCurrentlySelected = selMap[role.id];
3040
+ if (isCurrentlySelected) {
3041
+ return `[${this.translate('shared.assigned')}] ${this.translate('iam.tooltip.assigned.to.user')}\n\n[${this.translate('shared.remove')}] ${this.translate('iam.tooltip.click.to.remove.role.from.user')}`;
3042
+ }
3043
+ return `[${this.translate('shared.add')}] ${this.translate('iam.tooltip.click.to.assign.role.to.user')}`;
3044
+ }
3045
+ /**
3046
+ * Handle role checkbox toggle
3047
+ */
3048
+ onRoleToggle(role, newValue) {
3049
+ const selMap = { ...this.selectionMap() };
3050
+ selMap[role.id] = newValue;
3051
+ this._selectionMap.set(selMap);
3052
+ }
3053
+ /**
3054
+ * Toggle all roles
3055
+ */
3056
+ toggleAll() {
3057
+ this.setAllSelections(!this.allSelected());
3058
+ }
3059
+ /**
3060
+ * Select all roles
3061
+ */
3062
+ selectAll() {
3063
+ this.setAllSelections(true);
3064
+ }
3065
+ /**
3066
+ * Deselect all roles
3067
+ */
3068
+ deselectAll() {
3069
+ this.setAllSelections(false);
3070
+ }
3071
+ /**
3072
+ * Set all role selections to a given value
3073
+ */
3074
+ setAllSelections(value) {
3075
+ const selMap = {};
3076
+ this.roles().forEach((role) => {
3077
+ selMap[role.id] = value;
3078
+ });
3079
+ this._selectionMap.set(selMap);
3080
+ }
3081
+ /**
3082
+ * Save changes to backend
3083
+ */
3084
+ async saveChanges() {
3085
+ const userId = this.selectedUserId();
3086
+ if (!userId)
3087
+ return;
3088
+ const branchId = this.selectedBranchId();
3089
+ const companyId = this.companyContext.currentCompanyInfo()?.id || undefined;
3090
+ // Validate company context if feature enabled
3091
+ if (isCompanyFeatureEnabled(this.appConfig) && !companyId) {
3092
+ this.messageService.add({
3093
+ severity: 'warn',
3094
+ summary: this.translate('shared.warning'),
3095
+ detail: this.translate('iam.permission.company.required'),
3096
+ });
3097
+ return;
3098
+ }
3099
+ // Build payload
3100
+ const items = [];
3101
+ this.pendingAdd().forEach((role) => {
3102
+ items.push({ id: role.id, action: 'add' });
3103
+ });
3104
+ this.pendingRemove().forEach((role) => {
3105
+ items.push({ id: role.id, action: 'remove' });
3106
+ });
3107
+ if (items.length === 0)
3108
+ return;
3109
+ this.saving.set(true);
3110
+ try {
3111
+ const payload = { userId, items };
3112
+ if (isCompanyFeatureEnabled(this.appConfig)) {
3113
+ if (companyId) {
3114
+ payload.companyId = companyId;
3115
+ }
3116
+ if (branchId) {
3117
+ payload.branchId = branchId;
3118
+ }
3119
+ }
3120
+ const response = await firstValueFrom(this.permissionApi.assignUserRoles(payload));
3121
+ this.messageService.add({
3122
+ severity: 'success',
3123
+ summary: this.translate('shared.success'),
3124
+ detail: response?.messageKey
3125
+ ? this.translate(response.messageKey, response.messageVariables)
3126
+ : response?.message,
3127
+ });
3128
+ // Update baseline
3129
+ this._initialSelection.set({ ...this.selectionMap() });
3130
+ }
3131
+ catch {
3132
+ // Error toast handled by global interceptor
3133
+ }
3134
+ finally {
3135
+ this.saving.set(false);
3136
+ }
3137
+ }
3138
+ /**
3139
+ * Reset component state
3140
+ */
3141
+ resetState() {
3142
+ this._roles.set([]);
3143
+ this._selectionMap.set({});
3144
+ this._initialSelection.set({});
3145
+ }
3146
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: UserRoleSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3147
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: UserRoleSelectorComponent, isStandalone: true, selector: "flusys-user-role-selector", ngImport: i0, template: `
3148
+ <div class="user-role-selector">
3149
+ <!-- User and Branch Selectors -->
3150
+ <div class="surface-card p-4 rounded-border mb-4 shadow-sm">
3151
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
3152
+ <div>
3153
+ <label class="block font-semibold mb-2">
3154
+ <ui-icon name="user" class="mr-2 text-primary"></ui-icon>
3155
+ {{ 'iam.permission.select.user' | translate }}
3156
+ </label>
3157
+ <lib-user-select
3158
+ [value]="selectedUserId()"
3159
+ (valueChange)="selectedUserId.set($event)"
3160
+ [isEditMode]="true"
3161
+ [placeHolder]="
3162
+ 'iam.permission.select.user.placeholder' | translate
3163
+ "
3164
+ />
3165
+ </div>
3166
+
3167
+ @if (showBranchSelector()) {
3168
+ <div>
3169
+ <label class="block font-semibold mb-2">
3170
+ <ui-icon name="building" class="mr-2 text-primary"></ui-icon>
3171
+ {{ 'iam.permission.select.branch' | translate }}
3172
+ </label>
3173
+ <f-select
3174
+ [ngModel]="selectedBranchId()"
3175
+ (ngModelChange)="selectedBranchId.set($event)"
3176
+ [options]="filteredBranches()"
3177
+ optionLabel="name"
3178
+ optionValue="id"
3179
+ [placeholder]="
3180
+ 'iam.permission.select.branch.placeholder' | translate
3181
+ "
3182
+ [showClear]="true"
3183
+ [filter]="true"
3184
+ styleClass="w-full"
3185
+ >
3186
+ <ng-template #selectedItem let-branch>
3187
+ @if (branch) {
3188
+ <div class="flex items-center gap-2">
3189
+ <ui-icon name="building" class="text-primary"></ui-icon>
3190
+ <span class="font-semibold">{{ branch.name }}</span>
3191
+ </div>
3192
+ }
3193
+ </ng-template>
3194
+ <ng-template #item let-branch>
3195
+ <div class="flex items-center gap-2">
3196
+ <ui-icon name="building" class="text-muted-color"></ui-icon>
3197
+ <span>{{ branch.name }}</span>
3198
+ </div>
3199
+ </ng-template>
3200
+ </f-select>
3201
+ <small class="text-muted-color block mt-1">
3202
+ {{
3203
+ (filteredBranches().length !== 1
3204
+ ? 'iam.branch.permitted.count.plural'
3205
+ : 'iam.branch.permitted.count'
3206
+ ) | translate: { count: filteredBranches().length }
3207
+ }}
3208
+ </small>
3209
+ </div>
3210
+ }
3211
+ </div>
3212
+ </div>
3213
+
3214
+ @if (selectedUserId()) {
3215
+ <!-- Loading State -->
3216
+ @if (loading()) {
3217
+ <div
3218
+ class="surface-card p-5 rounded-border shadow-sm flex justify-center"
3219
+ >
3220
+ <ui-icon name="loader-circle" spin class="text-primary text-5xl"></ui-icon>
3221
+ </div>
3222
+ }
3223
+
3224
+ <!-- Role List -->
3225
+ @if (!loading() && roles().length > 0) {
3226
+ <div class="surface-card p-4 rounded-border shadow-sm">
3227
+ <div
3228
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
3229
+ >
3230
+ <div>
3231
+ <h5 class="m-0 mb-1">
3232
+ {{ 'iam.permission.role.assignments' | translate }}
3233
+ </h5>
3234
+ <p class="text-sm text-muted-color m-0">
3235
+ {{
3236
+ 'iam.permission.roles.available'
3237
+ | translate: { count: roles().length }
3238
+ }}
3239
+ </p>
3240
+ </div>
3241
+ <div class="flex flex-wrap gap-2">
3242
+ <button
3243
+ fButton
3244
+ [outlined]="true"
3245
+ size="small"
3246
+ severity="secondary"
3247
+ (click)="selectAll()"
3248
+ >
3249
+ <ui-icon pButtonIcon name="check"></ui-icon>
3250
+ <span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
3251
+ </button>
3252
+ <button
3253
+ fButton
3254
+ [outlined]="true"
3255
+ size="small"
3256
+ severity="secondary"
3257
+ (click)="deselectAll()"
3258
+ >
3259
+ <ui-icon pButtonIcon name="times"></ui-icon>
3260
+ <span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
3261
+ </button>
3262
+ <button
3263
+ fButton
3264
+ *hasPermission="USER_ROLE_PERMISSIONS.ASSIGN"
3265
+ [disabled]="!canSave() || saving()"
3266
+ size="small"
3267
+ (click)="saveChanges()"
3268
+ >
3269
+ @if (saving()) {
3270
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
3271
+ } @else {
3272
+ <ui-icon pButtonIcon name="save"></ui-icon>
3273
+ }
3274
+ <span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
3275
+ </button>
3276
+ </div>
3277
+ </div>
3278
+
3279
+ <!-- Role Table -->
3280
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
3281
+ <f-table
3282
+ [value]="roles()"
3283
+ [rows]="10"
3284
+ [paginator]="roles().length > 10"
3285
+ [rowsPerPageOptions]="[10, 20, 50]"
3286
+ [globalFilterFields]="['name', 'code', 'description']"
3287
+ [showCurrentPageReport]="true"
3288
+ [currentPageReportTemplate]="
3289
+ 'iam.pagination.roles.template' | translate
3290
+ "
3291
+ [tableStyle]="{ 'min-width': '35rem' }"
3292
+ >
3293
+ <ng-template #header>
3294
+ <tr>
3295
+ <th style="width: 3rem">
3296
+ <f-checkbox
3297
+ [ngModel]="allSelected()"
3298
+ [binary]="true"
3299
+ (ngModelChange)="toggleAll()"
3300
+ [fTooltip]="'shared.select.deselect.all' | translate"
3301
+ tooltipPosition="top"
3302
+ />
3303
+ </th>
3304
+ <th>{{ 'shared.name' | translate }}</th>
3305
+ <th class="hidden sm:table-cell">
3306
+ {{ 'shared.code' | translate }}
3307
+ </th>
3308
+ <th class="hidden md:table-cell">
3309
+ {{ 'shared.description' | translate }}
3310
+ </th>
3311
+ </tr>
3312
+ </ng-template>
3313
+ <ng-template #body let-role>
3314
+ <tr>
3315
+ <td>
3316
+ <f-checkbox
3317
+ [ngModel]="selectionMap()[role.id]"
3318
+ [binary]="true"
3319
+ (ngModelChange)="onRoleToggle(role, $event)"
3320
+ [fTooltip]="getTooltip(role)"
3321
+ tooltipPosition="top"
3322
+ />
3323
+ </td>
3324
+ <td>{{ role.name }}</td>
3325
+ <td class="hidden sm:table-cell">{{ role.code || '-' }}</td>
3326
+ <td class="hidden md:table-cell">
3327
+ {{ role.description || '-' }}
3328
+ </td>
3329
+ </tr>
3330
+ </ng-template>
3331
+ <ng-template #emptymessage>
3332
+ <tr>
3333
+ <td colspan="4" class="text-center p-4 text-muted-color">
3334
+ @if (loading()) {
3335
+ <ui-icon name="loader-circle" spin></ui-icon>
3336
+ {{ 'shared.loading.roles' | translate }}
3337
+ } @else {
3338
+ {{ 'iam.permission.no.roles.available' | translate }}
3339
+ }
3340
+ </td>
3341
+ </tr>
3342
+ </ng-template>
3343
+ </f-table>
3344
+ </div>
3345
+ </div>
3346
+
3347
+ <!-- Change Summary -->
3348
+ @if (hasChanges()) {
3349
+ <div class="border border-surface rounded-border p-3 mt-4">
3350
+ <div class="flex items-center gap-2 mb-3">
3351
+ <ui-icon name="info-circle" class="text-primary"></ui-icon>
3352
+ <span class="font-bold">{{
3353
+ 'shared.pending.changes' | translate
3354
+ }}</span>
3355
+ </div>
3356
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
3357
+ @if (pendingAdd().length > 0) {
3358
+ <div>
3359
+ <div class="flex items-center gap-2 mb-2">
3360
+ <ui-icon name="plus-circle" class="text-green-500"></ui-icon>
3361
+ <strong class="text-sm"
3362
+ >{{ 'shared.to.assign' | translate }} ({{
3363
+ pendingAdd().length
3364
+ }})</strong
3365
+ >
3366
+ </div>
3367
+ <ul class="list-none p-0 m-0 pl-4">
3368
+ @for (role of pendingAdd(); track role.id) {
3369
+ <li class="text-sm mb-1">{{ role.name }}</li>
3370
+ }
3371
+ </ul>
3372
+ </div>
3373
+ }
3374
+ @if (pendingRemove().length > 0) {
3375
+ <div>
3376
+ <div class="flex items-center gap-2 mb-2">
3377
+ <ui-icon name="minus-circle" class="text-red-500"></ui-icon>
3378
+ <strong class="text-sm"
3379
+ >{{ 'shared.to.remove' | translate }} ({{
3380
+ pendingRemove().length
3381
+ }})</strong
3382
+ >
3383
+ </div>
3384
+ <ul class="list-none p-0 m-0 pl-4">
3385
+ @for (role of pendingRemove(); track role.id) {
3386
+ <li class="text-sm mb-1">{{ role.name }}</li>
3387
+ }
3388
+ </ul>
3389
+ </div>
3390
+ }
3391
+ </div>
3392
+ </div>
3393
+ }
3394
+ }
3395
+
3396
+ @if (!loading() && roles().length === 0) {
3397
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
3398
+ <ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
3399
+ <p class="text-muted-color m-0">
3400
+ {{ 'iam.permission.no.roles.for.user' | translate }}
3401
+ </p>
3402
+ </div>
3403
+ }
3404
+ }
3405
+ </div>
3406
+ `, 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: UiModule }, { kind: "directive", type: i2.FButtonDirective, selector: "[fButton]", inputs: ["severity", "text", "outlined", "link", "variant", "rounded", "raised", "fluid", "disabled", "loading", "loadingIcon", "size", "label", "icon", "iconPos", "ariaLabel", "plain"] }, { kind: "component", type: i2.CheckboxComponent, selector: "f-checkbox", inputs: ["label", "binary", "disabled", "readonly", "inputId", "invalid", "indeterminate", "variant", "size", "touched", "styleClass", "style", "checked"], outputs: ["onChange", "onBlur", "touch", "checkedChange"] }, { kind: "component", type: i2.SelectComponent, selector: "f-select", inputs: ["options", "optionLabel", "optionValue", "placeholder", "disabled", "filter", "filterBy", "filterMatchMode", "filterLocale", "filterPlaceholder", "resetFilterOnHide", "autofocusFilter", "emptyMessage", "emptyFilterMessage", "showClear", "loading", "loadingIcon", "dropdownIcon", "clearIcon", "inputId", "invalid", "touched", "styleClass", "style", "editable", "readonly", "autofocus", "checkmark", "group", "optionGroupLabel", "optionGroupChildren", "virtualScroll", "virtualScrollItemSize", "lazy"], outputs: ["onFilter", "onChange", "onShow", "onHide", "onBlur", "onFocus", "onClear", "onLazyLoad"] }, { kind: "component", type: i2.TableComponent, selector: "f-table", inputs: ["value", "lazy", "loading", "rows", "totalRecords", "rowHover", "dataKey", "scrollable", "scrollHeight", "paginator", "first", "rowsPerPageOptions", "showCurrentPageReport", "currentPageReportTemplate", "tableStyle", "styleClass", "globalFilterFields", "filterDelay", "responsiveLayout", "selectionMode", "sortMode", "rowExpandMode", "resizableColumns", "rowStyleClass", "virtualScroll", "virtualScrollItemSize", "sortField", "sortOrder", "multiSortMeta", "filters", "selection", "expandedRowKeys", "contextMenuSelection"], outputs: ["sortFieldChange", "sortOrderChange", "multiSortMetaChange", "filtersChange", "selectionChange", "expandedRowKeysChange", "contextMenuSelectionChange", "onLazyLoad", "onSort", "onFilter", "onRowSelect", "onRowUnselect", "onSelectAllChange", "onRowExpand", "onRowCollapse", "onColResize", "onRowContextMenu"] }, { kind: "directive", type: i2.TooltipDirective, selector: "[fTooltip]", inputs: ["fTooltip", "tooltipPosition", "positionStyle", "tooltipStyleClass", "disabled", "tooltipEvent", "showDelay", "hideDelay", "life", "autoHide", "positionTop", "positionLeft", "escape"] }, { kind: "component", type: i2.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "directive", type: HasPermissionDirective, selector: "[hasPermission]", inputs: ["hasPermission"] }, { kind: "component", type: UserSelectComponent, selector: "lib-user-select", inputs: ["showClear", "value"], outputs: ["valueChange", "userSelected"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
3407
+ }
3408
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: UserRoleSelectorComponent, decorators: [{
3409
+ type: Component,
3410
+ args: [{ selector: 'flusys-user-role-selector', imports: [
3411
+ CommonModule,
3412
+ FormsModule,
3413
+ UiModule,
3414
+ HasPermissionDirective,
3415
+ UserSelectComponent,
3416
+ TranslatePipe,
3417
+ ], template: `
3418
+ <div class="user-role-selector">
3419
+ <!-- User and Branch Selectors -->
3420
+ <div class="surface-card p-4 rounded-border mb-4 shadow-sm">
3421
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
3422
+ <div>
3423
+ <label class="block font-semibold mb-2">
3424
+ <ui-icon name="user" class="mr-2 text-primary"></ui-icon>
3425
+ {{ 'iam.permission.select.user' | translate }}
3426
+ </label>
3427
+ <lib-user-select
3428
+ [value]="selectedUserId()"
3429
+ (valueChange)="selectedUserId.set($event)"
3430
+ [isEditMode]="true"
3431
+ [placeHolder]="
3432
+ 'iam.permission.select.user.placeholder' | translate
3433
+ "
3434
+ />
3435
+ </div>
3436
+
3437
+ @if (showBranchSelector()) {
3438
+ <div>
3439
+ <label class="block font-semibold mb-2">
3440
+ <ui-icon name="building" class="mr-2 text-primary"></ui-icon>
3441
+ {{ 'iam.permission.select.branch' | translate }}
3442
+ </label>
3443
+ <f-select
3444
+ [ngModel]="selectedBranchId()"
3445
+ (ngModelChange)="selectedBranchId.set($event)"
3446
+ [options]="filteredBranches()"
3447
+ optionLabel="name"
3448
+ optionValue="id"
3449
+ [placeholder]="
3450
+ 'iam.permission.select.branch.placeholder' | translate
3451
+ "
3452
+ [showClear]="true"
3453
+ [filter]="true"
3454
+ styleClass="w-full"
3455
+ >
3456
+ <ng-template #selectedItem let-branch>
3457
+ @if (branch) {
3458
+ <div class="flex items-center gap-2">
3459
+ <ui-icon name="building" class="text-primary"></ui-icon>
3460
+ <span class="font-semibold">{{ branch.name }}</span>
3461
+ </div>
3462
+ }
3463
+ </ng-template>
3464
+ <ng-template #item let-branch>
3465
+ <div class="flex items-center gap-2">
3466
+ <ui-icon name="building" class="text-muted-color"></ui-icon>
3467
+ <span>{{ branch.name }}</span>
3468
+ </div>
3469
+ </ng-template>
3470
+ </f-select>
3471
+ <small class="text-muted-color block mt-1">
3472
+ {{
3473
+ (filteredBranches().length !== 1
3474
+ ? 'iam.branch.permitted.count.plural'
3475
+ : 'iam.branch.permitted.count'
3476
+ ) | translate: { count: filteredBranches().length }
3477
+ }}
3478
+ </small>
3479
+ </div>
3480
+ }
3481
+ </div>
3482
+ </div>
3483
+
3484
+ @if (selectedUserId()) {
3485
+ <!-- Loading State -->
3486
+ @if (loading()) {
3487
+ <div
3488
+ class="surface-card p-5 rounded-border shadow-sm flex justify-center"
3489
+ >
3490
+ <ui-icon name="loader-circle" spin class="text-primary text-5xl"></ui-icon>
3491
+ </div>
3492
+ }
3493
+
3494
+ <!-- Role List -->
3495
+ @if (!loading() && roles().length > 0) {
3496
+ <div class="surface-card p-4 rounded-border shadow-sm">
3497
+ <div
3498
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
3499
+ >
3500
+ <div>
3501
+ <h5 class="m-0 mb-1">
3502
+ {{ 'iam.permission.role.assignments' | translate }}
3503
+ </h5>
3504
+ <p class="text-sm text-muted-color m-0">
3505
+ {{
3506
+ 'iam.permission.roles.available'
3507
+ | translate: { count: roles().length }
3508
+ }}
3509
+ </p>
3510
+ </div>
3511
+ <div class="flex flex-wrap gap-2">
3512
+ <button
3513
+ fButton
3514
+ [outlined]="true"
3515
+ size="small"
3516
+ severity="secondary"
3517
+ (click)="selectAll()"
3518
+ >
3519
+ <ui-icon pButtonIcon name="check"></ui-icon>
3520
+ <span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
3521
+ </button>
3522
+ <button
3523
+ fButton
3524
+ [outlined]="true"
3525
+ size="small"
3526
+ severity="secondary"
3527
+ (click)="deselectAll()"
3528
+ >
3529
+ <ui-icon pButtonIcon name="times"></ui-icon>
3530
+ <span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
3531
+ </button>
3532
+ <button
3533
+ fButton
3534
+ *hasPermission="USER_ROLE_PERMISSIONS.ASSIGN"
3535
+ [disabled]="!canSave() || saving()"
3536
+ size="small"
3537
+ (click)="saveChanges()"
3538
+ >
3539
+ @if (saving()) {
3540
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
3541
+ } @else {
3542
+ <ui-icon pButtonIcon name="save"></ui-icon>
3543
+ }
3544
+ <span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
3545
+ </button>
3546
+ </div>
3547
+ </div>
3548
+
3549
+ <!-- Role Table -->
3550
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
3551
+ <f-table
3552
+ [value]="roles()"
3553
+ [rows]="10"
3554
+ [paginator]="roles().length > 10"
3555
+ [rowsPerPageOptions]="[10, 20, 50]"
3556
+ [globalFilterFields]="['name', 'code', 'description']"
3557
+ [showCurrentPageReport]="true"
3558
+ [currentPageReportTemplate]="
3559
+ 'iam.pagination.roles.template' | translate
3560
+ "
3561
+ [tableStyle]="{ 'min-width': '35rem' }"
3562
+ >
3563
+ <ng-template #header>
3564
+ <tr>
3565
+ <th style="width: 3rem">
3566
+ <f-checkbox
3567
+ [ngModel]="allSelected()"
3568
+ [binary]="true"
3569
+ (ngModelChange)="toggleAll()"
3570
+ [fTooltip]="'shared.select.deselect.all' | translate"
3571
+ tooltipPosition="top"
3572
+ />
3573
+ </th>
3574
+ <th>{{ 'shared.name' | translate }}</th>
3575
+ <th class="hidden sm:table-cell">
3576
+ {{ 'shared.code' | translate }}
3577
+ </th>
3578
+ <th class="hidden md:table-cell">
3579
+ {{ 'shared.description' | translate }}
3580
+ </th>
3581
+ </tr>
3582
+ </ng-template>
3583
+ <ng-template #body let-role>
3584
+ <tr>
3585
+ <td>
3586
+ <f-checkbox
3587
+ [ngModel]="selectionMap()[role.id]"
3588
+ [binary]="true"
3589
+ (ngModelChange)="onRoleToggle(role, $event)"
3590
+ [fTooltip]="getTooltip(role)"
3591
+ tooltipPosition="top"
3592
+ />
3593
+ </td>
3594
+ <td>{{ role.name }}</td>
3595
+ <td class="hidden sm:table-cell">{{ role.code || '-' }}</td>
3596
+ <td class="hidden md:table-cell">
3597
+ {{ role.description || '-' }}
3598
+ </td>
3599
+ </tr>
3600
+ </ng-template>
3601
+ <ng-template #emptymessage>
3602
+ <tr>
3603
+ <td colspan="4" class="text-center p-4 text-muted-color">
3604
+ @if (loading()) {
3605
+ <ui-icon name="loader-circle" spin></ui-icon>
3606
+ {{ 'shared.loading.roles' | translate }}
3607
+ } @else {
3608
+ {{ 'iam.permission.no.roles.available' | translate }}
3609
+ }
3610
+ </td>
3611
+ </tr>
3612
+ </ng-template>
3613
+ </f-table>
3614
+ </div>
3615
+ </div>
3616
+
3617
+ <!-- Change Summary -->
3618
+ @if (hasChanges()) {
3619
+ <div class="border border-surface rounded-border p-3 mt-4">
3620
+ <div class="flex items-center gap-2 mb-3">
3621
+ <ui-icon name="info-circle" class="text-primary"></ui-icon>
3622
+ <span class="font-bold">{{
3623
+ 'shared.pending.changes' | translate
3624
+ }}</span>
3625
+ </div>
3626
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
3627
+ @if (pendingAdd().length > 0) {
3628
+ <div>
3629
+ <div class="flex items-center gap-2 mb-2">
3630
+ <ui-icon name="plus-circle" class="text-green-500"></ui-icon>
3631
+ <strong class="text-sm"
3632
+ >{{ 'shared.to.assign' | translate }} ({{
3633
+ pendingAdd().length
3634
+ }})</strong
3635
+ >
3636
+ </div>
3637
+ <ul class="list-none p-0 m-0 pl-4">
3638
+ @for (role of pendingAdd(); track role.id) {
3639
+ <li class="text-sm mb-1">{{ role.name }}</li>
3640
+ }
3641
+ </ul>
3642
+ </div>
3643
+ }
3644
+ @if (pendingRemove().length > 0) {
3645
+ <div>
3646
+ <div class="flex items-center gap-2 mb-2">
3647
+ <ui-icon name="minus-circle" class="text-red-500"></ui-icon>
3648
+ <strong class="text-sm"
3649
+ >{{ 'shared.to.remove' | translate }} ({{
3650
+ pendingRemove().length
3651
+ }})</strong
3652
+ >
3653
+ </div>
3654
+ <ul class="list-none p-0 m-0 pl-4">
3655
+ @for (role of pendingRemove(); track role.id) {
3656
+ <li class="text-sm mb-1">{{ role.name }}</li>
3657
+ }
3658
+ </ul>
3659
+ </div>
3660
+ }
3661
+ </div>
3662
+ </div>
3663
+ }
3664
+ }
3665
+
3666
+ @if (!loading() && roles().length === 0) {
3667
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
3668
+ <ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
3669
+ <p class="text-muted-color m-0">
3670
+ {{ 'iam.permission.no.roles.for.user' | translate }}
3671
+ </p>
3672
+ </div>
3673
+ }
3674
+ }
3675
+ </div>
3676
+ `, styles: [":host{display:block}\n"] }]
3677
+ }], ctorParameters: () => [] });
3678
+
3679
+ class UserActionSelectorComponent extends BaseActionSelectorComponent {
3680
+ USER_ACTION_PERMISSIONS = USER_ACTION_PERMISSIONS;
3681
+ appConfig = inject(APP_CONFIG);
3682
+ companyContext = inject(LAYOUT_AUTH_STATE);
3683
+ userPermissionProvider = inject(USER_PERMISSION_PROVIDER);
3684
+ actionApi = inject(ActionApiService);
3685
+ permissionApi = inject(PermissionApiService);
3686
+ messageService = inject(MessageService);
3687
+ selectedUserId = signal(null, /* @ts-ignore */
3688
+ ...(ngDevMode ? [{ debugName: "selectedUserId" }] : /* istanbul ignore next */ []));
3689
+ selectedBranchId = signal(undefined, /* @ts-ignore */
3690
+ ...(ngDevMode ? [{ debugName: "selectedBranchId" }] : /* istanbul ignore next */ []));
3691
+ branches = signal([], /* @ts-ignore */
3692
+ ...(ngDevMode ? [{ debugName: "branches" }] : /* istanbul ignore next */ []));
3693
+ isCompanyFeatureActive = computed(() => isCompanyFeatureEnabled(this.appConfig) === true, /* @ts-ignore */
3694
+ ...(ngDevMode ? [{ debugName: "isCompanyFeatureActive" }] : /* istanbul ignore next */ []));
3695
+ showBranchSelector = this.isCompanyFeatureActive;
3696
+ filteredBranches = computed(() => {
3697
+ const currentCompanyId = this.companyContext.currentCompanyInfo()?.id || undefined;
3698
+ if (!currentCompanyId)
3699
+ return this.branches();
3700
+ return this.branches().filter((b) => b.companyId === currentCompanyId);
3701
+ }, /* @ts-ignore */
3702
+ ...(ngDevMode ? [{ debugName: "filteredBranches" }] : /* istanbul ignore next */ []));
3703
+ constructor() {
3704
+ super();
3705
+ effect(() => {
3706
+ const userId = this.selectedUserId();
3707
+ this.selectedBranchId.set(undefined);
3708
+ if (userId) {
3709
+ if (this.isCompanyFeatureActive()) {
3710
+ this.loadUserBranches(userId);
3711
+ }
3712
+ this.loadData();
3713
+ }
3714
+ else {
3715
+ this.branches.set([]);
3716
+ this.resetState();
3717
+ }
3718
+ });
3719
+ effect(() => {
3720
+ const branchId = this.selectedBranchId();
3721
+ const userId = this.selectedUserId();
3722
+ if (this.isCompanyFeatureActive() && userId && branchId !== undefined) {
3723
+ this.loadData();
3724
+ }
3725
+ });
3726
+ }
3727
+ getDefaultTooltip(isSelected) {
3728
+ if (isSelected) {
3729
+ return `[${this.translate('shared.assigned')}] ${this.translate('iam.tooltip.assigned.to.user')}\n\n[${this.translate('shared.remove')}] ${this.translate('iam.tooltip.click.to.remove.user')}`;
3730
+ }
3731
+ return `[${this.translate('shared.add')}] ${this.translate('iam.tooltip.click.to.assign.user')}`;
3732
+ }
3733
+ async loadUserBranches(userId) {
3734
+ const companyId = this.companyContext.currentCompanyInfo()?.id;
3735
+ if (!companyId) {
3736
+ this.branches.set([]);
3737
+ return;
3738
+ }
3739
+ try {
3740
+ const response = await firstValueFrom(this.userPermissionProvider.getUserBranchPermissions(userId));
3741
+ const userBranches = response?.data?.map((ub) => ({
3742
+ id: ub.branchId,
3743
+ name: ub.branchName,
3744
+ companyId: ub.companyId,
3745
+ })) ?? [];
3746
+ this.branches.set(userBranches);
3747
+ }
3748
+ catch { }
3749
+ }
3750
+ async loadData() {
3751
+ const userId = this.selectedUserId();
3752
+ if (!userId)
3753
+ return;
3754
+ const branchId = this.selectedBranchId();
3755
+ this.loading.set(true);
3756
+ try {
3757
+ const actionsResponse = await firstValueFrom(this.actionApi.getActionsForPermission());
3758
+ const flatActions = actionsResponse?.data ?? [];
3759
+ const actionsTree = buildTreeFromFlat(flatActions);
3760
+ this._actionsTree.set(actionsTree);
3761
+ this._actions.set(flatActions);
3762
+ const query = this.isCompanyFeatureActive() && branchId ? { branchId } : undefined;
3763
+ const assignedResponse = await firstValueFrom(this.permissionApi.getUserActions(userId, query));
3764
+ const assignedIds = new Set((assignedResponse?.data || []).map((a) => a.actionId));
3765
+ const selMap = this.buildSelectionMap(flatActions, (action) => assignedIds.has(action.id));
3766
+ this._selectionMap.set(selMap);
3767
+ this._initialSelection.set({ ...selMap });
3768
+ }
3769
+ catch {
3770
+ }
3771
+ finally {
3772
+ this.loading.set(false);
3773
+ }
3774
+ }
3775
+ async saveChanges() {
3776
+ const userId = this.selectedUserId();
3777
+ if (!userId)
3778
+ return;
3779
+ const branchId = this.selectedBranchId();
3780
+ const companyId = this.companyContext.currentCompanyInfo()?.id;
3781
+ if (this.isCompanyFeatureActive() && !companyId) {
3782
+ this.messageService.add({
3783
+ severity: 'warn',
3784
+ summary: this.translate('shared.warning'),
3785
+ detail: this.translate('iam.permission.company.required'),
3786
+ });
3787
+ return;
3788
+ }
3789
+ const invalidActions = this.permissionLogic.getActionsWithUnmetPrerequisites(this.selectionMap(), this.actions());
3790
+ if (invalidActions.length > 0) {
3791
+ this.permissionLogic.showValidationErrorDialog(invalidActions, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
3792
+ return;
3793
+ }
3794
+ const items = this.buildPayloadItems();
3795
+ if (items.length === 0)
3796
+ return;
3797
+ this.saving.set(true);
3798
+ try {
3799
+ const payload = { userId, items };
3800
+ if (this.isCompanyFeatureActive()) {
3801
+ if (companyId)
3802
+ payload.companyId = companyId;
3803
+ if (branchId)
3804
+ payload.branchId = branchId;
3805
+ }
3806
+ const response = await firstValueFrom(this.permissionApi.assignUserActions(payload));
3807
+ this.messageService.add({
3808
+ severity: 'success',
3809
+ summary: this.translate('shared.success'),
3810
+ detail: response?.messageKey
3811
+ ? this.translate(response.messageKey, response.messageVariables)
3812
+ : response?.message,
3813
+ });
3814
+ this._initialSelection.set({ ...this.selectionMap() });
3815
+ }
3816
+ catch {
3817
+ }
3818
+ finally {
3819
+ this.saving.set(false);
3820
+ }
3821
+ }
3822
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: UserActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3823
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: UserActionSelectorComponent, isStandalone: true, selector: "flusys-user-action-selector", usesInheritance: true, ngImport: i0, template: `
3824
+ <div class="user-action-selector">
3825
+ <div class="surface-card p-4 rounded-border mb-4 shadow-sm">
3826
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
3827
+ <div>
3828
+ <label class="block font-semibold mb-2">
3829
+ <ui-icon name="user" class="mr-2 text-primary"></ui-icon>
3830
+ {{ 'iam.permission.select.user' | translate }}
3831
+ </label>
3832
+ <lib-user-select
3833
+ [value]="selectedUserId()"
3834
+ (valueChange)="selectedUserId.set($event)"
3835
+ [isEditMode]="true"
3836
+ [placeHolder]="
3837
+ 'iam.permission.select.user.placeholder' | translate
3838
+ "
3839
+ />
3840
+ </div>
3841
+
3842
+ @if (showBranchSelector()) {
3843
+ <div>
3844
+ <label class="block font-semibold mb-2">
3845
+ <ui-icon name="building" class="mr-2 text-primary"></ui-icon>
3846
+ {{ 'iam.permission.select.branch' | translate }}
3847
+ </label>
3848
+ <f-select
3849
+ [ngModel]="selectedBranchId()"
3850
+ (ngModelChange)="selectedBranchId.set($event)"
3851
+ [options]="filteredBranches()"
3852
+ optionLabel="name"
3853
+ optionValue="id"
3854
+ [placeholder]="
3855
+ 'iam.permission.select.branch.placeholder' | translate
3856
+ "
3857
+ [showClear]="true"
3858
+ [filter]="true"
3859
+ class="w-full"
3860
+ >
3861
+ <ng-template #selectedItem let-branch>
3862
+ @if (branch) {
3863
+ <div class="flex items-center gap-2">
3864
+ <ui-icon name="building" class="text-primary"></ui-icon>
3865
+ <span class="font-semibold">{{ branch.name }}</span>
3866
+ </div>
3867
+ }
3868
+ </ng-template>
3869
+ <ng-template #item let-branch>
3870
+ <div class="flex items-center gap-2">
3871
+ <ui-icon name="building" class="text-muted-color"></ui-icon>
3872
+ <span>{{ branch.name }}</span>
3873
+ </div>
3874
+ </ng-template>
3875
+ </f-select>
3876
+ <small class="text-muted-color block mt-1">
3877
+ {{
3878
+ (filteredBranches().length !== 1
3879
+ ? 'iam.branch.permitted.count.plural'
3880
+ : 'iam.branch.permitted.count'
3881
+ ) | translate: { count: filteredBranches().length }
3882
+ }}
3883
+ </small>
3884
+ </div>
3885
+ }
3886
+ </div>
3887
+ </div>
3888
+
3889
+ @if (selectedUserId()) {
3890
+ @if (loading()) {
3891
+ <div class="flex justify-center p-5">
3892
+ <ui-icon name="loader-circle" spin class="text-4xl"></ui-icon>
3893
+ </div>
3894
+ }
3895
+
3896
+ @if (!loading() && actions().length > 0) {
3897
+ <div class="surface-card p-4 rounded-border shadow-sm">
3898
+ <div
3899
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
3900
+ >
3901
+ <div>
3902
+ <h5 class="m-0 mb-1">
3903
+ {{ 'iam.permission.direct.action.permissions' | translate }}
3904
+ </h5>
3905
+ <p class="text-sm text-muted-color m-0">
3906
+ {{
3907
+ 'iam.permission.actions.available'
3908
+ | translate: { count: actions().length }
3909
+ }}
3910
+ </p>
3911
+ </div>
3912
+ <div class="flex flex-wrap gap-2">
3913
+ <button
3914
+ fButton
3915
+ [outlined]="true"
3916
+ size="small"
3917
+ severity="secondary"
3918
+ (click)="selectAll()"
3919
+ >
3920
+ <ui-icon pButtonIcon name="check"></ui-icon>
3921
+ <span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
3922
+ </button>
3923
+ <button
3924
+ fButton
3925
+ [outlined]="true"
3926
+ size="small"
3927
+ severity="secondary"
3928
+ (click)="deselectAll()"
3929
+ >
3930
+ <ui-icon pButtonIcon name="times"></ui-icon>
3931
+ <span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
3932
+ </button>
3933
+ <button
3934
+ fButton
3935
+ *hasPermission="USER_ACTION_PERMISSIONS.ASSIGN"
3936
+ [disabled]="!canSave() || saving()"
3937
+ size="small"
3938
+ (click)="saveChanges()"
3939
+ >
3940
+ @if (saving()) {
3941
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
3942
+ } @else {
3943
+ <ui-icon pButtonIcon name="save"></ui-icon>
3944
+ }
3945
+ <span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
3946
+ </button>
3947
+ </div>
3948
+ </div>
3949
+
3950
+ @if (invalidActionsCount() > 0) {
3951
+ <div class="validation-warning rounded-border p-3 mb-4">
3952
+ <div class="flex items-start gap-2">
3953
+ <ui-icon name="exclamation-triangle" class="text-xl"></ui-icon>
3954
+ <div class="flex-1">
3955
+ <span class="font-semibold"
3956
+ >{{ 'iam.validation.warning.title' | translate }}:</span
3957
+ >
3958
+ <p class="text-sm mt-1 mb-0">
3959
+ {{
3960
+ (invalidActionsCount() > 1
3961
+ ? 'iam.validation.unmet.prerequisites.plural'
3962
+ : 'iam.validation.unmet.prerequisites.singular'
3963
+ ) | translate: { count: invalidActionsCount() }
3964
+ }}
3965
+ </p>
3966
+ </div>
3967
+ </div>
3968
+ </div>
3969
+ }
3970
+
3971
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
3972
+ <f-treetable
3973
+ [value]="treeNodes()"
3974
+ dataKey="id"
3975
+ [tableStyle]="{ 'min-width': '50rem' }"
3976
+ >
3977
+ <ng-template #header>
3978
+ <tr>
3979
+ <th class="w-12">
3980
+ <f-checkbox
3981
+ [ngModel]="allSelected()"
3982
+ [binary]="true"
3983
+ (ngModelChange)="toggleAll()"
3984
+ [fTooltip]="'shared.select.deselect.all' | translate"
3985
+ tooltipPosition="top"
3986
+ />
3987
+ </th>
3988
+ <th>{{ 'shared.name' | translate }}</th>
3989
+ <th class="hidden md:table-cell">
3990
+ {{ 'shared.code' | translate }}
3991
+ </th>
3992
+ <th>{{ 'iam.action.type' | translate }}</th>
3993
+ <th class="hidden lg:table-cell">
3994
+ {{ 'shared.description' | translate }}
3995
+ </th>
3996
+ </tr>
3997
+ </ng-template>
3998
+ <ng-template #body let-rowNode let-rowData="rowData">
3999
+ @let hasUnmet = hasUnmetPrerequisites(rowData);
4000
+ @let tooltip = getTooltip(rowData);
4001
+ <tr [class.highlight-warning]="hasUnmet">
4002
+ <td class="w-12">
4003
+ <f-checkbox
4004
+ [ngModel]="selectionMap()[rowData.id]"
4005
+ [binary]="true"
4006
+ (ngModelChange)="onActionToggle(rowData, $event)"
4007
+ [fTooltip]="tooltip"
4008
+ tooltipPosition="top"
4009
+ />
4010
+ </td>
4011
+ <td>
4012
+ <f-treetabletoggler [rowNode]="rowNode" />
4013
+ <span class="inline-flex items-center gap-2">
4014
+ {{ rowData.name }}
4015
+ @if (hasUnmet) {
4016
+ <ui-icon name="exclamation-triangle" class="text-orange-500" [fTooltip]="
4017
+ 'iam.validation.unmet.prerequisites.tooltip'
4018
+ | translate
4019
+ "
4020
+ tooltipPosition="top"></ui-icon>
4021
+ }
4022
+ </span>
4023
+ </td>
4024
+ <td class="hidden md:table-cell">
4025
+ {{ rowData.code || '-' }}
4026
+ </td>
4027
+ <td>
4028
+ <f-tag
4029
+ [value]="rowData.actionType"
4030
+ [severity]="
4031
+ rowData.actionType === 'backend' ? 'info' : 'success'
4032
+ "
4033
+ />
4034
+ </td>
4035
+ <td class="hidden lg:table-cell">
4036
+ {{ rowData.description || '-' }}
4037
+ </td>
4038
+ </tr>
4039
+ </ng-template>
4040
+ <ng-template #emptymessage>
4041
+ <tr>
4042
+ <td colspan="5" class="text-center p-4 text-muted-color">
4043
+ @if (loading()) {
4044
+ <ui-icon name="loader-circle" spin></ui-icon>
4045
+ {{ 'shared.loading.actions' | translate }}
4046
+ } @else {
4047
+ {{ 'iam.permission.no.actions.available' | translate }}
4048
+ }
4049
+ </td>
4050
+ </tr>
4051
+ </ng-template>
4052
+ </f-treetable>
4053
+ </div>
4054
+ </div>
4055
+
4056
+ @if (hasChanges()) {
4057
+ <div class="border border-surface rounded-border p-3 mt-4">
4058
+ <div class="flex items-center gap-2 mb-3">
4059
+ <ui-icon name="info-circle" class="text-primary"></ui-icon>
4060
+ <span class="font-bold">{{
4061
+ 'shared.pending.changes' | translate
4062
+ }}</span>
4063
+ </div>
4064
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
4065
+ @if (pendingAdd().length > 0) {
4066
+ <div>
4067
+ <div class="flex items-center gap-2 mb-2">
4068
+ <ui-icon name="plus-circle" class="text-green-500"></ui-icon>
4069
+ <strong class="text-sm"
4070
+ >{{ 'shared.to.assign' | translate }} ({{
4071
+ pendingAdd().length
4072
+ }})</strong
4073
+ >
4074
+ </div>
4075
+ <ul class="list-none p-0 m-0 pl-4">
4076
+ @for (action of pendingAdd(); track action.id) {
4077
+ <li class="text-sm mb-1">{{ action.name }}</li>
4078
+ }
4079
+ </ul>
4080
+ </div>
4081
+ }
4082
+ @if (pendingRemove().length > 0) {
4083
+ <div>
4084
+ <div class="flex items-center gap-2 mb-2">
4085
+ <ui-icon name="minus-circle" class="text-red-500"></ui-icon>
4086
+ <strong class="text-sm"
4087
+ >{{ 'shared.to.remove' | translate }} ({{
4088
+ pendingRemove().length
4089
+ }})</strong
4090
+ >
4091
+ </div>
4092
+ <ul class="list-none p-0 m-0 pl-4">
4093
+ @for (action of pendingRemove(); track action.id) {
4094
+ <li class="text-sm mb-1">{{ action.name }}</li>
4095
+ }
4096
+ </ul>
4097
+ </div>
4098
+ }
4099
+ </div>
4100
+ </div>
4101
+ }
4102
+ }
4103
+
4104
+ @if (!loading() && actions().length === 0) {
4105
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
4106
+ <ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
4107
+ <p class="text-muted-color m-0">
4108
+ {{ 'iam.permission.no.actions.for.user' | translate }}
4109
+ </p>
4110
+ </div>
4111
+ }
4112
+ }
4113
+ </div>
4114
+ `, isInline: true, styles: [":host{display:block}.validation-warning{background-color:var(--f-yellow-50, #fefce8);border-left:4px solid var(--f-yellow-500, #eab308);color:var(--f-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--f-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--f-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: 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: UiModule }, { kind: "directive", type: i2.FButtonDirective, selector: "[fButton]", inputs: ["severity", "text", "outlined", "link", "variant", "rounded", "raised", "fluid", "disabled", "loading", "loadingIcon", "size", "label", "icon", "iconPos", "ariaLabel", "plain"] }, { kind: "component", type: i2.TagComponent, selector: "f-tag", inputs: ["value", "severity", "icon", "rounded", "color", "styleClass", "style"] }, { kind: "component", type: i2.CheckboxComponent, selector: "f-checkbox", inputs: ["label", "binary", "disabled", "readonly", "inputId", "invalid", "indeterminate", "variant", "size", "touched", "styleClass", "style", "checked"], outputs: ["onChange", "onBlur", "touch", "checkedChange"] }, { kind: "component", type: i2.SelectComponent, selector: "f-select", inputs: ["options", "optionLabel", "optionValue", "placeholder", "disabled", "filter", "filterBy", "filterMatchMode", "filterLocale", "filterPlaceholder", "resetFilterOnHide", "autofocusFilter", "emptyMessage", "emptyFilterMessage", "showClear", "loading", "loadingIcon", "dropdownIcon", "clearIcon", "inputId", "invalid", "touched", "styleClass", "style", "editable", "readonly", "autofocus", "checkmark", "group", "optionGroupLabel", "optionGroupChildren", "virtualScroll", "virtualScrollItemSize", "lazy"], outputs: ["onFilter", "onChange", "onShow", "onHide", "onBlur", "onFocus", "onClear", "onLazyLoad"] }, { kind: "directive", type: i2.TooltipDirective, selector: "[fTooltip]", inputs: ["fTooltip", "tooltipPosition", "positionStyle", "tooltipStyleClass", "disabled", "tooltipEvent", "showDelay", "hideDelay", "life", "autoHide", "positionTop", "positionLeft", "escape"] }, { kind: "component", type: i2.TreeTableComponent, selector: "f-treetable", inputs: ["value", "loading", "dataKey", "styleClass", "tableStyle", "scrollable", "scrollHeight", "resizableColumns", "lazy", "totalRecords", "paginator", "first", "rows", "rowsPerPageOptions", "showCurrentPageReport", "currentPageReportTemplate", "globalFilterFields", "filterDelay", "selectionMode", "sortMode", "virtualScroll", "virtualScrollItemSize", "sortField", "sortOrder", "multiSortMeta", "filters", "selection", "contextMenuSelection"], outputs: ["sortFieldChange", "sortOrderChange", "multiSortMetaChange", "filtersChange", "selectionChange", "contextMenuSelectionChange", "onLazyLoad", "onSort", "onFilter", "onNodeExpand", "onNodeCollapse", "onNodeSelect", "onNodeUnselect", "onRowContextMenu"] }, { kind: "component", type: i2.TreeTableTogglerComponent, selector: "f-treetabletoggler", inputs: ["rowNode"] }, { kind: "component", type: i2.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "directive", type: HasPermissionDirective, selector: "[hasPermission]", inputs: ["hasPermission"] }, { kind: "component", type: UserSelectComponent, selector: "lib-user-select", inputs: ["showClear", "value"], outputs: ["valueChange", "userSelected"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
4115
+ }
4116
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: UserActionSelectorComponent, decorators: [{
4117
+ type: Component,
4118
+ args: [{ selector: 'flusys-user-action-selector', imports: [
4119
+ FormsModule,
4120
+ UiModule,
4121
+ HasPermissionDirective,
4122
+ UserSelectComponent,
4123
+ TranslatePipe,
4124
+ ], template: `
4125
+ <div class="user-action-selector">
4126
+ <div class="surface-card p-4 rounded-border mb-4 shadow-sm">
4127
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
4128
+ <div>
4129
+ <label class="block font-semibold mb-2">
4130
+ <ui-icon name="user" class="mr-2 text-primary"></ui-icon>
4131
+ {{ 'iam.permission.select.user' | translate }}
4132
+ </label>
4133
+ <lib-user-select
4134
+ [value]="selectedUserId()"
4135
+ (valueChange)="selectedUserId.set($event)"
4136
+ [isEditMode]="true"
4137
+ [placeHolder]="
4138
+ 'iam.permission.select.user.placeholder' | translate
4139
+ "
4140
+ />
4141
+ </div>
4142
+
4143
+ @if (showBranchSelector()) {
4144
+ <div>
4145
+ <label class="block font-semibold mb-2">
4146
+ <ui-icon name="building" class="mr-2 text-primary"></ui-icon>
4147
+ {{ 'iam.permission.select.branch' | translate }}
4148
+ </label>
4149
+ <f-select
4150
+ [ngModel]="selectedBranchId()"
4151
+ (ngModelChange)="selectedBranchId.set($event)"
4152
+ [options]="filteredBranches()"
4153
+ optionLabel="name"
4154
+ optionValue="id"
4155
+ [placeholder]="
4156
+ 'iam.permission.select.branch.placeholder' | translate
4157
+ "
4158
+ [showClear]="true"
4159
+ [filter]="true"
4160
+ class="w-full"
4161
+ >
4162
+ <ng-template #selectedItem let-branch>
4163
+ @if (branch) {
4164
+ <div class="flex items-center gap-2">
4165
+ <ui-icon name="building" class="text-primary"></ui-icon>
4166
+ <span class="font-semibold">{{ branch.name }}</span>
4167
+ </div>
4168
+ }
4169
+ </ng-template>
4170
+ <ng-template #item let-branch>
4171
+ <div class="flex items-center gap-2">
4172
+ <ui-icon name="building" class="text-muted-color"></ui-icon>
4173
+ <span>{{ branch.name }}</span>
4174
+ </div>
4175
+ </ng-template>
4176
+ </f-select>
4177
+ <small class="text-muted-color block mt-1">
4178
+ {{
4179
+ (filteredBranches().length !== 1
4180
+ ? 'iam.branch.permitted.count.plural'
4181
+ : 'iam.branch.permitted.count'
4182
+ ) | translate: { count: filteredBranches().length }
4183
+ }}
4184
+ </small>
4185
+ </div>
4186
+ }
4187
+ </div>
4188
+ </div>
4189
+
4190
+ @if (selectedUserId()) {
4191
+ @if (loading()) {
4192
+ <div class="flex justify-center p-5">
4193
+ <ui-icon name="loader-circle" spin class="text-4xl"></ui-icon>
4194
+ </div>
4195
+ }
4196
+
4197
+ @if (!loading() && actions().length > 0) {
4198
+ <div class="surface-card p-4 rounded-border shadow-sm">
4199
+ <div
4200
+ class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
4201
+ >
4202
+ <div>
4203
+ <h5 class="m-0 mb-1">
4204
+ {{ 'iam.permission.direct.action.permissions' | translate }}
4205
+ </h5>
4206
+ <p class="text-sm text-muted-color m-0">
4207
+ {{
4208
+ 'iam.permission.actions.available'
4209
+ | translate: { count: actions().length }
4210
+ }}
4211
+ </p>
4212
+ </div>
4213
+ <div class="flex flex-wrap gap-2">
4214
+ <button
4215
+ fButton
4216
+ [outlined]="true"
4217
+ size="small"
4218
+ severity="secondary"
4219
+ (click)="selectAll()"
4220
+ >
4221
+ <ui-icon pButtonIcon name="check"></ui-icon>
4222
+ <span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
4223
+ </button>
4224
+ <button
4225
+ fButton
4226
+ [outlined]="true"
4227
+ size="small"
4228
+ severity="secondary"
4229
+ (click)="deselectAll()"
4230
+ >
4231
+ <ui-icon pButtonIcon name="times"></ui-icon>
4232
+ <span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
4233
+ </button>
4234
+ <button
4235
+ fButton
4236
+ *hasPermission="USER_ACTION_PERMISSIONS.ASSIGN"
4237
+ [disabled]="!canSave() || saving()"
4238
+ size="small"
4239
+ (click)="saveChanges()"
4240
+ >
4241
+ @if (saving()) {
4242
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
4243
+ } @else {
4244
+ <ui-icon pButtonIcon name="save"></ui-icon>
4245
+ }
4246
+ <span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
4247
+ </button>
4248
+ </div>
4249
+ </div>
4250
+
4251
+ @if (invalidActionsCount() > 0) {
4252
+ <div class="validation-warning rounded-border p-3 mb-4">
4253
+ <div class="flex items-start gap-2">
4254
+ <ui-icon name="exclamation-triangle" class="text-xl"></ui-icon>
4255
+ <div class="flex-1">
4256
+ <span class="font-semibold"
4257
+ >{{ 'iam.validation.warning.title' | translate }}:</span
4258
+ >
4259
+ <p class="text-sm mt-1 mb-0">
4260
+ {{
4261
+ (invalidActionsCount() > 1
4262
+ ? 'iam.validation.unmet.prerequisites.plural'
4263
+ : 'iam.validation.unmet.prerequisites.singular'
4264
+ ) | translate: { count: invalidActionsCount() }
4265
+ }}
4266
+ </p>
4267
+ </div>
4268
+ </div>
4269
+ </div>
4270
+ }
4271
+
4272
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
4273
+ <f-treetable
4274
+ [value]="treeNodes()"
4275
+ dataKey="id"
4276
+ [tableStyle]="{ 'min-width': '50rem' }"
4277
+ >
4278
+ <ng-template #header>
4279
+ <tr>
4280
+ <th class="w-12">
4281
+ <f-checkbox
4282
+ [ngModel]="allSelected()"
4283
+ [binary]="true"
4284
+ (ngModelChange)="toggleAll()"
4285
+ [fTooltip]="'shared.select.deselect.all' | translate"
4286
+ tooltipPosition="top"
4287
+ />
4288
+ </th>
4289
+ <th>{{ 'shared.name' | translate }}</th>
4290
+ <th class="hidden md:table-cell">
4291
+ {{ 'shared.code' | translate }}
4292
+ </th>
4293
+ <th>{{ 'iam.action.type' | translate }}</th>
4294
+ <th class="hidden lg:table-cell">
4295
+ {{ 'shared.description' | translate }}
4296
+ </th>
4297
+ </tr>
4298
+ </ng-template>
4299
+ <ng-template #body let-rowNode let-rowData="rowData">
4300
+ @let hasUnmet = hasUnmetPrerequisites(rowData);
4301
+ @let tooltip = getTooltip(rowData);
4302
+ <tr [class.highlight-warning]="hasUnmet">
4303
+ <td class="w-12">
4304
+ <f-checkbox
4305
+ [ngModel]="selectionMap()[rowData.id]"
4306
+ [binary]="true"
4307
+ (ngModelChange)="onActionToggle(rowData, $event)"
4308
+ [fTooltip]="tooltip"
4309
+ tooltipPosition="top"
4310
+ />
4311
+ </td>
4312
+ <td>
4313
+ <f-treetabletoggler [rowNode]="rowNode" />
4314
+ <span class="inline-flex items-center gap-2">
4315
+ {{ rowData.name }}
4316
+ @if (hasUnmet) {
4317
+ <ui-icon name="exclamation-triangle" class="text-orange-500" [fTooltip]="
4318
+ 'iam.validation.unmet.prerequisites.tooltip'
4319
+ | translate
4320
+ "
4321
+ tooltipPosition="top"></ui-icon>
4322
+ }
4323
+ </span>
4324
+ </td>
4325
+ <td class="hidden md:table-cell">
4326
+ {{ rowData.code || '-' }}
4327
+ </td>
4328
+ <td>
4329
+ <f-tag
4330
+ [value]="rowData.actionType"
4331
+ [severity]="
4332
+ rowData.actionType === 'backend' ? 'info' : 'success'
4333
+ "
4334
+ />
4335
+ </td>
4336
+ <td class="hidden lg:table-cell">
4337
+ {{ rowData.description || '-' }}
4338
+ </td>
4339
+ </tr>
4340
+ </ng-template>
4341
+ <ng-template #emptymessage>
4342
+ <tr>
4343
+ <td colspan="5" class="text-center p-4 text-muted-color">
4344
+ @if (loading()) {
4345
+ <ui-icon name="loader-circle" spin></ui-icon>
4346
+ {{ 'shared.loading.actions' | translate }}
4347
+ } @else {
4348
+ {{ 'iam.permission.no.actions.available' | translate }}
4349
+ }
4350
+ </td>
4351
+ </tr>
4352
+ </ng-template>
4353
+ </f-treetable>
4354
+ </div>
4355
+ </div>
4356
+
4357
+ @if (hasChanges()) {
4358
+ <div class="border border-surface rounded-border p-3 mt-4">
4359
+ <div class="flex items-center gap-2 mb-3">
4360
+ <ui-icon name="info-circle" class="text-primary"></ui-icon>
4361
+ <span class="font-bold">{{
4362
+ 'shared.pending.changes' | translate
4363
+ }}</span>
4364
+ </div>
4365
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
4366
+ @if (pendingAdd().length > 0) {
4367
+ <div>
4368
+ <div class="flex items-center gap-2 mb-2">
4369
+ <ui-icon name="plus-circle" class="text-green-500"></ui-icon>
4370
+ <strong class="text-sm"
4371
+ >{{ 'shared.to.assign' | translate }} ({{
4372
+ pendingAdd().length
4373
+ }})</strong
4374
+ >
4375
+ </div>
4376
+ <ul class="list-none p-0 m-0 pl-4">
4377
+ @for (action of pendingAdd(); track action.id) {
4378
+ <li class="text-sm mb-1">{{ action.name }}</li>
4379
+ }
4380
+ </ul>
4381
+ </div>
4382
+ }
4383
+ @if (pendingRemove().length > 0) {
4384
+ <div>
4385
+ <div class="flex items-center gap-2 mb-2">
4386
+ <ui-icon name="minus-circle" class="text-red-500"></ui-icon>
4387
+ <strong class="text-sm"
4388
+ >{{ 'shared.to.remove' | translate }} ({{
4389
+ pendingRemove().length
4390
+ }})</strong
4391
+ >
4392
+ </div>
4393
+ <ul class="list-none p-0 m-0 pl-4">
4394
+ @for (action of pendingRemove(); track action.id) {
4395
+ <li class="text-sm mb-1">{{ action.name }}</li>
4396
+ }
4397
+ </ul>
4398
+ </div>
4399
+ }
4400
+ </div>
4401
+ </div>
4402
+ }
4403
+ }
4404
+
4405
+ @if (!loading() && actions().length === 0) {
4406
+ <div class="surface-card p-5 rounded-border shadow-sm text-center">
4407
+ <ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
4408
+ <p class="text-muted-color m-0">
4409
+ {{ 'iam.permission.no.actions.for.user' | translate }}
4410
+ </p>
4411
+ </div>
4412
+ }
4413
+ }
4414
+ </div>
4415
+ `, styles: [":host{display:block}.validation-warning{background-color:var(--f-yellow-50, #fefce8);border-left:4px solid var(--f-yellow-500, #eab308);color:var(--f-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--f-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--f-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"] }]
4416
+ }], ctorParameters: () => [] });
4417
+
4418
+ // Logic Builder Component
4419
+
4420
+ /**
4421
+ * Profile Permission Provider Adapter
4422
+ *
4423
+ * Implements IProfilePermissionProvider using ng-iam's PermissionApiService.
4424
+ * Allows ng-auth profile page to display user permissions without
4425
+ * depending on ng-iam directly.
4426
+ *
4427
+ * @example
4428
+ * // In app.config.ts
4429
+ * providers: [
4430
+ * ...provideIamProviders()
4431
+ * ]
4432
+ *
4433
+ * // In profile component
4434
+ * private readonly permissionProvider = inject(PROFILE_PERMISSION_PROVIDER, { optional: true });
4435
+ */
4436
+ class ProfilePermissionProviderAdapter {
4437
+ permissionApi = inject(PermissionApiService);
4438
+ getUserRoles(userId, branchId) {
4439
+ return this.permissionApi
4440
+ .getUserRoles(userId, branchId ? { branchId } : undefined)
4441
+ .pipe(map$1((response) => ({
4442
+ success: response.success,
4443
+ message: response.message,
4444
+ data: (response.data ?? []).map((role) => ({
4445
+ id: role.roleId,
4446
+ name: role.roleName,
4447
+ description: null,
4448
+ })),
4449
+ })));
4450
+ }
4451
+ getUserActions(userId, branchId) {
4452
+ return this.permissionApi
4453
+ .getUserActions(userId, branchId ? { branchId } : undefined)
4454
+ .pipe(map$1((response) => ({
4455
+ success: response.success,
4456
+ message: response.message,
4457
+ data: (response.data ?? []).map((action) => ({
4458
+ id: action.actionId,
4459
+ name: action.actionName,
4460
+ code: action.actionCode,
4461
+ description: null,
4462
+ })),
4463
+ })));
4464
+ }
4465
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ProfilePermissionProviderAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4466
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ProfilePermissionProviderAdapter });
4467
+ }
4468
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ProfilePermissionProviderAdapter, decorators: [{
4469
+ type: Injectable
4470
+ }] });
4471
+
4472
+ /** Registers IAM provider adapters for ng-shared interfaces */
4473
+ function provideIamProviders() {
4474
+ return [
4475
+ { provide: PROFILE_PERMISSION_PROVIDER, useClass: ProfilePermissionProviderAdapter },
4476
+ ];
4477
+ }
4478
+
4479
+ /**
4480
+ * IAM Routes Configuration
4481
+ *
4482
+ * Identity and Access Management routing with permission guards.
4483
+ * - Actions: Permission actions management
4484
+ * - Roles: Role management (conditional on RBAC/FULL mode)
4485
+ * - Permissions: User permission assignments
4486
+ */
4487
+ const IAM_ROUTES = [
4488
+ {
4489
+ path: '',
4490
+ resolve: { translations: resolveTranslationModule({ modules: ['iam'], fallbackMessages: { ...IAM_MESSAGES, ...SHARED_MESSAGES } }) },
4491
+ loadComponent: () => import('./flusys-ng-iam-iam-container.component-D0m8u6nu.mjs').then((m) => m.IamContainerComponent),
4492
+ children: [
4493
+ // Actions Management
4494
+ {
4495
+ path: 'actions',
4496
+ canActivate: [permissionGuard(ACTION_PERMISSIONS.READ)],
4497
+ children: [
4498
+ {
4499
+ path: '',
4500
+ loadComponent: () => import('./flusys-ng-iam-action-list-page.component-UGKclpFx.mjs').then((m) => m.ActionListPageComponent),
4501
+ },
4502
+ {
4503
+ path: 'new',
4504
+ loadComponent: () => import('./flusys-ng-iam-action-form-page.component-Vc5Toouu.mjs').then((m) => m.ActionFormPageComponent),
4505
+ },
4506
+ {
4507
+ path: ':id',
4508
+ loadComponent: () => import('./flusys-ng-iam-action-form-page.component-Vc5Toouu.mjs').then((m) => m.ActionFormPageComponent),
4509
+ },
4510
+ ],
4511
+ },
4512
+ // Roles Management
4513
+ {
4514
+ path: 'roles',
4515
+ canActivate: [permissionGuard(ROLE_PERMISSIONS.READ)],
4516
+ children: [
4517
+ {
4518
+ path: '',
4519
+ loadComponent: () => import('./flusys-ng-iam-role-list-page.component-8NGXCeSZ.mjs').then((m) => m.RoleListPageComponent),
4520
+ },
4521
+ {
4522
+ path: 'new',
4523
+ loadComponent: () => import('./flusys-ng-iam-role-form-page.component-InsxdriD.mjs').then((m) => m.RoleFormPageComponent),
4524
+ },
4525
+ {
4526
+ path: ':id',
4527
+ loadComponent: () => import('./flusys-ng-iam-role-form-page.component-InsxdriD.mjs').then((m) => m.RoleFormPageComponent),
4528
+ },
4529
+ ],
4530
+ },
4531
+ // Permissions Management (requires any permission tab access)
4532
+ {
4533
+ path: 'permissions',
4534
+ canActivate: [
4535
+ anyPermissionGuard([
4536
+ ROLE_ACTION_PERMISSIONS.READ,
4537
+ USER_ROLE_PERMISSIONS.READ,
4538
+ USER_ACTION_PERMISSIONS.READ,
4539
+ COMPANY_ACTION_PERMISSIONS.READ,
4540
+ ]),
4541
+ ],
4542
+ loadComponent: () => import('./flusys-ng-iam-permission-page.component-x88X4AgJ.mjs').then((m) => m.PermissionPageComponent),
4543
+ },
4544
+ // Default redirect to actions
4545
+ {
4546
+ path: '',
4547
+ redirectTo: 'actions',
4548
+ pathMatch: 'full',
4549
+ },
4550
+ ],
4551
+ },
4552
+ ];
4553
+
4554
+ // ==================== INTERFACES ====================
4555
+
4556
+ /**
4557
+ * Generated bundle index. Do not edit.
4558
+ */
4559
+
4560
+ export { ActionApiService as A, CompanyActionSelectorComponent as C, IAM_MESSAGES 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, IAM_ROUTES as f, MyPermissionsApiService as g, PermissionStateService as h, ProfilePermissionProviderAdapter as i, provideIamProviders as p };
4561
+ //# sourceMappingURL=flusys-ng-iam-flusys-ng-iam-CrycDULz.mjs.map