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