@zambon-dev/ui 0.0.0-stage → 0.0.1-bootstrap.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +2325 -0
  2. package/README.md +81 -3
  3. package/fesm2022/zambon-dev-ui-framework.mjs +3997 -0
  4. package/fesm2022/zambon-dev-ui-framework.mjs.map +1 -0
  5. package/fesm2022/zambon-dev-ui-library.mjs +4280 -0
  6. package/fesm2022/zambon-dev-ui-library.mjs.map +1 -0
  7. package/fesm2022/zambon-dev-ui-shared.mjs +1802 -0
  8. package/fesm2022/zambon-dev-ui-shared.mjs.map +1 -0
  9. package/fesm2022/zambon-dev-ui-testing.mjs +80 -0
  10. package/fesm2022/zambon-dev-ui-testing.mjs.map +1 -0
  11. package/fesm2022/zambon-dev-ui.mjs +8 -0
  12. package/fesm2022/zambon-dev-ui.mjs.map +1 -0
  13. package/i18n/framework/en.json +63 -0
  14. package/i18n/framework/pt.json +63 -0
  15. package/i18n/shared/external-content/en.json +12 -0
  16. package/i18n/shared/external-content/pt.json +12 -0
  17. package/i18n/shared/language-selector/en.json +5 -0
  18. package/i18n/shared/language-selector/pt.json +5 -0
  19. package/i18n/shared/login/en.json +14 -0
  20. package/i18n/shared/login/pt.json +14 -0
  21. package/i18n/shared/operations-history/en.json +5 -0
  22. package/i18n/shared/operations-history/pt.json +5 -0
  23. package/i18n/shared/services-history/en.json +14 -0
  24. package/i18n/shared/services-history/pt.json +14 -0
  25. package/i18n/shared/top-bar/en.json +7 -0
  26. package/i18n/shared/top-bar/pt.json +7 -0
  27. package/package.json +106 -4
  28. package/styles/common.scss +10 -0
  29. package/styles/components/button.scss +101 -0
  30. package/styles/components/card.scss +13 -0
  31. package/styles/components/dropdown-static.scss +35 -0
  32. package/styles/components/input-group.scss +44 -0
  33. package/styles/components/input.scss +99 -0
  34. package/styles/components/label.scss +3 -0
  35. package/styles/components/row.scss +48 -0
  36. package/styles/mixins/background.scss +31 -0
  37. package/styles/theme.css +75 -0
  38. package/styles/variables.scss +60 -0
  39. package/types/zambon-dev-ui-framework.d.ts +1066 -0
  40. package/types/zambon-dev-ui-library.d.ts +1233 -0
  41. package/types/zambon-dev-ui-shared.d.ts +655 -0
  42. package/types/zambon-dev-ui-testing.d.ts +56 -0
  43. package/types/zambon-dev-ui.d.ts +3 -0
@@ -0,0 +1,1802 @@
1
+ import * as i0 from '@angular/core';
2
+ import { ChangeDetectionStrategy, Component, InjectionToken, inject, Injectable, viewChild, signal, HostListener, input, output, NgModule, ViewChild, DOCUMENT, computed, Input, Pipe, model } from '@angular/core';
3
+ import * as i1 from '@angular/router';
4
+ import { RouterModule, Router, ActivatedRoute } from '@angular/router';
5
+ import { AuthService, APP_CONFIG, TabService, Tab, TabsComponent, ButtonFiltersComponent, TabViewBase, ButtonComponent, FrameworkViewType, DefaultTabViewComponent, FRAMEWORK_VIEW_TYPE, ModalBase, ChildList, ButtonRefreshComponent } from '@zambon-dev/ui/framework';
6
+ import { SidebarService, toSidebarMenuOpenMode, SidebarMenuOpenMode, ModalComponent, SidebarComponent, DataGridDataset, RibbonGroupComponent, DataGridComponent, CatalogSelectComponent, FormGroupComponent, FormInputGroupComponent, DataProviderService } from '@zambon-dev/ui/library';
7
+ import { TranslateService, TranslatePipe, TranslateLoader, provideTranslateService } from '@ngx-translate/core';
8
+ import { map, catchError, interval, mergeMap, tap, of, take, BehaviorSubject, Subject, takeUntil, finalize, shareReplay, switchMap } from 'rxjs';
9
+ import { HttpClient, HttpBackend } from '@angular/common/http';
10
+ import { JwtHelperService, JwtInterceptor } from '@auth0/angular-jwt';
11
+ import { HubConnectionState, HubConnectionBuilder } from '@microsoft/signalr';
12
+ import { AsyncPipe, JsonPipe, DatePipe } from '@angular/common';
13
+ import { toSignal } from '@angular/core/rxjs-interop';
14
+ import * as i1$1 from '@angular/forms';
15
+ import { FormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
16
+ import { DomSanitizer } from '@angular/platform-browser';
17
+ import { MultiTranslateHttpLoader } from 'ngx-translate-multi-http-loader';
18
+
19
+ class LoginLayoutComponent {
20
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: LoginLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
21
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: LoginLayoutComponent, isStandalone: true, selector: "shared-login-layout", ngImport: i0, template: "<div class=\"background\"></div>\r\n\r\n<div class=\"container\">\r\n <router-outlet></router-outlet>\r\n</div>\r\n", styles: ["@layer properties;:host{display:block;height:100vh}.background{position:fixed;z-index:-50;height:100vh;width:100%;overflow:hidden}.background,.background:before{background-image:url(/background.jpg);background-size:cover;background-attachment:fixed}.background:before,.background:after{position:absolute;inset:0;--tw-content: \"\";content:var(--tw-content)}.background:before{--tw-blur: blur(var(--blur-sm, 8px));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.background:after{background-color:color-mix(in srgb,#fff 30%,transparent)}@supports (color: color-mix(in lab,red,red)){.background:after{background-color:color-mix(in oklab,var(--color-white, #fff) 30%,transparent)}}.background *{z-index:10}.container{position:absolute;top:50%;left:50%;--tw-translate-x: -50% ;--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}@property --tw-content{syntax: \"*\"; inherits: false; initial-value: \"\";}@property --tw-blur{syntax: \"*\"; inherits: false;}@property --tw-brightness{syntax: \"*\"; inherits: false;}@property --tw-contrast{syntax: \"*\"; inherits: false;}@property --tw-grayscale{syntax: \"*\"; inherits: false;}@property --tw-hue-rotate{syntax: \"*\"; inherits: false;}@property --tw-invert{syntax: \"*\"; inherits: false;}@property --tw-opacity{syntax: \"*\"; inherits: false;}@property --tw-saturate{syntax: \"*\"; inherits: false;}@property --tw-sepia{syntax: \"*\"; inherits: false;}@property --tw-drop-shadow{syntax: \"*\"; inherits: false;}@property --tw-drop-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-drop-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-drop-shadow-size{syntax: \"*\"; inherits: false;}@property --tw-translate-x{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-y{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-z{syntax: \"*\"; inherits: false; initial-value: 0;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-content: \"\";--tw-blur: initial;--tw-brightness: initial;--tw-contrast: initial;--tw-grayscale: initial;--tw-hue-rotate: initial;--tw-invert: initial;--tw-opacity: initial;--tw-saturate: initial;--tw-sepia: initial;--tw-drop-shadow: initial;--tw-drop-shadow-color: initial;--tw-drop-shadow-alpha: 100%;--tw-drop-shadow-size: initial;--tw-translate-x: 0;--tw-translate-y: 0;--tw-translate-z: 0}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
22
+ }
23
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: LoginLayoutComponent, decorators: [{
24
+ type: Component,
25
+ args: [{ selector: 'shared-login-layout', imports: [RouterModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"background\"></div>\r\n\r\n<div class=\"container\">\r\n <router-outlet></router-outlet>\r\n</div>\r\n", styles: ["@layer properties;:host{display:block;height:100vh}.background{position:fixed;z-index:-50;height:100vh;width:100%;overflow:hidden}.background,.background:before{background-image:url(/background.jpg);background-size:cover;background-attachment:fixed}.background:before,.background:after{position:absolute;inset:0;--tw-content: \"\";content:var(--tw-content)}.background:before{--tw-blur: blur(var(--blur-sm, 8px));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.background:after{background-color:color-mix(in srgb,#fff 30%,transparent)}@supports (color: color-mix(in lab,red,red)){.background:after{background-color:color-mix(in oklab,var(--color-white, #fff) 30%,transparent)}}.background *{z-index:10}.container{position:absolute;top:50%;left:50%;--tw-translate-x: -50% ;--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}@property --tw-content{syntax: \"*\"; inherits: false; initial-value: \"\";}@property --tw-blur{syntax: \"*\"; inherits: false;}@property --tw-brightness{syntax: \"*\"; inherits: false;}@property --tw-contrast{syntax: \"*\"; inherits: false;}@property --tw-grayscale{syntax: \"*\"; inherits: false;}@property --tw-hue-rotate{syntax: \"*\"; inherits: false;}@property --tw-invert{syntax: \"*\"; inherits: false;}@property --tw-opacity{syntax: \"*\"; inherits: false;}@property --tw-saturate{syntax: \"*\"; inherits: false;}@property --tw-sepia{syntax: \"*\"; inherits: false;}@property --tw-drop-shadow{syntax: \"*\"; inherits: false;}@property --tw-drop-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-drop-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-drop-shadow-size{syntax: \"*\"; inherits: false;}@property --tw-translate-x{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-y{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-z{syntax: \"*\"; inherits: false; initial-value: 0;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-content: \"\";--tw-blur: initial;--tw-brightness: initial;--tw-contrast: initial;--tw-grayscale: initial;--tw-hue-rotate: initial;--tw-invert: initial;--tw-opacity: initial;--tw-saturate: initial;--tw-sepia: initial;--tw-drop-shadow: initial;--tw-drop-shadow-color: initial;--tw-drop-shadow-alpha: 100%;--tw-drop-shadow-size: initial;--tw-translate-x: 0;--tw-translate-y: 0;--tw-translate-z: 0}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
26
+ }] });
27
+
28
+ /** First segment of the route that hosts embedded external content. */
29
+ const EXTERNAL_CONTENT_ROUTE_PATH = 'external-content';
30
+ class ExternalContentConfigs {
31
+ /**
32
+ * Origins an embedded menu item may point at, for example `['https://reports.example.com']`.
33
+ * Empty — the default — allows any `http`/`https` origin.
34
+ *
35
+ * Populating it is the single highest-value control here after the scheme check: it narrows a
36
+ * compromised menu row from "frame anything on the internet" to "frame one of our report hosts".
37
+ */
38
+ allowedOrigins = [];
39
+ /** Milliseconds to wait for the frame's first `load` before hinting that framing may be refused. */
40
+ slowFrameHintDelay = 5000;
41
+ constructor(options = {}) {
42
+ Object.assign(this, options);
43
+ }
44
+ }
45
+ const EXTERNAL_CONTENT_CONFIGS = new InjectionToken('Embedded external content configuration', {
46
+ providedIn: 'root',
47
+ factory: () => new ExternalContentConfigs(),
48
+ });
49
+
50
+ class AuthenticationService extends AuthService {
51
+ //#region ViewChilds, Inputs, Outputs
52
+ //#endregion
53
+ //#region Variables
54
+ adminAction = 'AdministrativeMaster';
55
+ config = inject(APP_CONFIG);
56
+ BASE_URL = `${this.config.BASE_URL}/Authentication`;
57
+ http = inject(HttpClient);
58
+ //#endregion
59
+ //#region Properties
60
+ //#endregion
61
+ //#region Constructor and Angular life cycle methods
62
+ constructor() {
63
+ super(inject(JwtHelperService), inject(TabService));
64
+ }
65
+ //#endregion
66
+ //#region Event handlers
67
+ //#endregion
68
+ //#region Public methods
69
+ authenticate(model) {
70
+ return this.http.post(`${this.BASE_URL}/SignIn`, model).pipe(map((res) => {
71
+ this.setStorage('username', res.username, model.rememberMe);
72
+ this.setStorage('token', res.token, model.rememberMe);
73
+ this.setStorage('refreshToken', res.refreshToken, model.rememberMe);
74
+ this.setStorage('userInfo', window.btoa(JSON.stringify(res)), model.rememberMe);
75
+ }), catchError((error) => interval(1000).pipe(mergeMap(() => {
76
+ switch (error.status) {
77
+ case 401:
78
+ throw 'InvalidUsernamePassword';
79
+ default:
80
+ throw 'InternalServerError';
81
+ }
82
+ }))));
83
+ }
84
+ getActions() {
85
+ return this.http.post(`${this.BASE_URL}/GetActions`, {});
86
+ }
87
+ getUserInfo() {
88
+ const userInfo = this.userInfo;
89
+ if (userInfo) {
90
+ return JSON.parse(window.atob(userInfo));
91
+ }
92
+ return null;
93
+ }
94
+ tryRefreshToken() {
95
+ return this.http
96
+ .post(`${this.BASE_URL}/RefreshToken`, {
97
+ username: this.username ?? '',
98
+ refreshToken: this.refreshToken ?? '',
99
+ })
100
+ .pipe(tap((res) => {
101
+ this.setStorage('token', res.token);
102
+ this.setStorage('refreshToken', res.refreshToken);
103
+ this.setStorage('userInfo', window.btoa(JSON.stringify(res)));
104
+ }), map((res) => res.token ?? ''));
105
+ }
106
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: AuthenticationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
107
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: AuthenticationService, providedIn: 'root' });
108
+ }
109
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: AuthenticationService, decorators: [{
110
+ type: Injectable,
111
+ args: [{
112
+ providedIn: 'root',
113
+ }]
114
+ }], ctorParameters: () => [] });
115
+
116
+ /**
117
+ * Remembers which external destination sits behind an embedded tab's `/external-content/:menuID`
118
+ * route, and resolves it back when the view is created.
119
+ *
120
+ * The destination never travels in the route, so the id has to be resolved to a URL somehow. That
121
+ * happens in three steps, cheapest first:
122
+ *
123
+ * 1. the in-memory registry — the item was clicked in this browsing session;
124
+ * 2. `sessionStorage` — the user pressed F5 on the embedded tab;
125
+ * 3. the application's `SidebarService.getMenuFromUrl()` — a cold deep link.
126
+ *
127
+ * Step 3 reuses the abstract hook every application already implements, so nothing new is required
128
+ * of a consumer: an application whose menu endpoint cannot resolve `/external-content/:id` simply
129
+ * gets the unavailable state on a cold deep link, which is the documented behaviour.
130
+ */
131
+ class ExternalContentService {
132
+ //#region Variables
133
+ maxEntries = 20;
134
+ storageKey = 'zambon.externalContent';
135
+ registry = new Map();
136
+ sidebarService = inject(SidebarService);
137
+ //#endregion
138
+ //#region Properties
139
+ //#endregion
140
+ //#region Constructor and Angular life cycle methods
141
+ //#endregion
142
+ //#region Public methods
143
+ /** Resolves the destination behind an embedded tab, or `undefined` when it cannot be recovered. */
144
+ find(menuID) {
145
+ const known = this.registry.get(menuID) ?? this.readFromStorage(menuID);
146
+ if (known) {
147
+ return of(known);
148
+ }
149
+ return this.sidebarService.getMenuFromUrl(`/${EXTERNAL_CONTENT_ROUTE_PATH}/${menuID}`).pipe(take(1), map((menu) => (!!menu && !!menu.url ? this.toEntry(menu) : undefined)),
150
+ // A menu endpoint that does not know this route shape answers 404, which is an expected
151
+ // outcome here rather than a failure: the view falls back to its unavailable state.
152
+ catchError(() => of(undefined)));
153
+ }
154
+ /** Remembers a menu so its embedded tab survives a page refresh. */
155
+ register(menu) {
156
+ const entry = this.toEntry(menu);
157
+ this.registry.set(entry.id, entry);
158
+ this.writeToStorage(entry);
159
+ }
160
+ //#endregion
161
+ //#region Private methods
162
+ readAllFromStorage() {
163
+ try {
164
+ const raw = window.sessionStorage.getItem(this.storageKey);
165
+ if (!raw) {
166
+ return [];
167
+ }
168
+ const parsed = JSON.parse(raw);
169
+ return Array.isArray(parsed) ? parsed : [];
170
+ }
171
+ catch {
172
+ // A poisoned or unavailable storage entry must never take the application down; the view
173
+ // degrades to asking the user to reopen the item from the menu.
174
+ return [];
175
+ }
176
+ }
177
+ readFromStorage(menuID) {
178
+ const entry = this.readAllFromStorage().find((candidate) => candidate?.id === menuID);
179
+ if (!!entry && !!entry.url) {
180
+ this.registry.set(entry.id, entry);
181
+ return entry;
182
+ }
183
+ return undefined;
184
+ }
185
+ toEntry(menu) {
186
+ return {
187
+ id: menu.id,
188
+ label: menu.label,
189
+ url: menu.url ?? '',
190
+ };
191
+ }
192
+ writeToStorage(entry) {
193
+ try {
194
+ const entries = [
195
+ entry,
196
+ ...this.readAllFromStorage().filter((candidate) => candidate?.id !== entry.id),
197
+ ].slice(0, this.maxEntries);
198
+ window.sessionStorage.setItem(this.storageKey, JSON.stringify(entries));
199
+ }
200
+ catch {
201
+ // Storage can be unavailable (private mode, blocked site data). Losing refresh support is
202
+ // acceptable; failing the click is not.
203
+ }
204
+ }
205
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ExternalContentService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
206
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ExternalContentService, providedIn: 'root' });
207
+ }
208
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ExternalContentService, decorators: [{
209
+ type: Injectable,
210
+ args: [{
211
+ providedIn: 'root',
212
+ }]
213
+ }] });
214
+
215
+ /**
216
+ * Resolves the runtime placeholders in an external menu URL, and vets the result before it is
217
+ * handed to the browser.
218
+ *
219
+ * Placeholder syntax is `{name}`, case-sensitive, drawn from a **closed** set:
220
+ *
221
+ * | Token | Source |
222
+ * |--------------|-----------------------------------------------------|
223
+ * | `{email}` | `ICurrentUserInfo.email` |
224
+ * | `{language}` | `TranslateService.currentLang`, then `defaultLang` |
225
+ * | `{userId}` | `ICurrentUserInfo.userID` |
226
+ * | `{userName}` | `ICurrentUserInfo.username` |
227
+ *
228
+ * The set is closed by construction rather than reflected off the stored user info, and that is a
229
+ * security property, not a style choice: `AuthenticationService` persists the entire sign-in
230
+ * response under `userInfo`, tokens included, so a reflective implementation would let a menu URL
231
+ * configured as `?t={token}` hand the JWT to a third party. No authentication token is ever
232
+ * substituted.
233
+ */
234
+ class ExternalUrlResolverService {
235
+ //#region Variables
236
+ authenticationService = inject(AuthenticationService);
237
+ translate = inject(TranslateService);
238
+ /** Placeholder grammar, used only to *detect* tokens this version does not know. */
239
+ unknownPlaceholderPattern = /\{[A-Za-z][A-Za-z0-9_]*\}/g;
240
+ //#endregion
241
+ //#region Properties
242
+ //#endregion
243
+ //#region Constructor and Angular life cycle methods
244
+ //#endregion
245
+ //#region Public methods
246
+ /**
247
+ * Whether `url` is an absolute `http`/`https` address.
248
+ *
249
+ * Rejects relative paths, malformed values, protocol-relative `//host/path` (which throws
250
+ * without a base), and the schemes that would execute rather than navigate: `javascript:`,
251
+ * `data:`, `blob:`, `file:`.
252
+ */
253
+ isAllowed(url) {
254
+ let parsed;
255
+ try {
256
+ parsed = new URL(url);
257
+ }
258
+ catch {
259
+ return false;
260
+ }
261
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
262
+ }
263
+ /**
264
+ * Substitutes every known placeholder, URL-encoding each value.
265
+ *
266
+ * Because every value is percent-encoded, a placeholder must occupy one *whole* value — a full
267
+ * path segment, or a full query-parameter value. Encoding is what stops a display name that
268
+ * contains `&` from injecting an extra query parameter into the destination.
269
+ *
270
+ * A known placeholder with no value becomes an empty string; an unrecognized `{…}` is left
271
+ * exactly as configured, so a report URL that legitimately contains braces is not corrupted.
272
+ */
273
+ resolve(url) {
274
+ if (!url || url.indexOf('{') === -1) {
275
+ return url;
276
+ }
277
+ const values = this.getPlaceholderValues();
278
+ let resolved = url;
279
+ values.forEach((value, token) => {
280
+ if (resolved.indexOf(token) === -1) {
281
+ return;
282
+ }
283
+ if (value.length === 0) {
284
+ console.warn(`External URL placeholder ${token} has no value for the current user; substituting an empty string.`, url);
285
+ }
286
+ // split/join rather than String.replace: replace() interprets `$&` and `$1` in the
287
+ // replacement text, and without a global regex it would only swap the first occurrence.
288
+ resolved = resolved.split(token).join(encodeURIComponent(value));
289
+ });
290
+ this.warnAboutUnknownPlaceholders(url, resolved);
291
+ return resolved;
292
+ }
293
+ //#endregion
294
+ //#region Private methods
295
+ getPlaceholderValues() {
296
+ const user = this.authenticationService.getUserInfo();
297
+ return new Map([
298
+ ['{email}', user?.email ?? ''],
299
+ ['{language}', this.translate.currentLang || this.translate.defaultLang || ''],
300
+ ['{userId}', user?.userID?.toString() ?? ''],
301
+ ['{userName}', user?.username ?? ''],
302
+ ]);
303
+ }
304
+ warnAboutUnknownPlaceholders(url, resolved) {
305
+ const unknown = resolved.match(this.unknownPlaceholderPattern) ?? [];
306
+ if (unknown.length > 0) {
307
+ console.warn(`External URL contains unrecognized placeholders and was left as configured: ${unknown.join(', ')}`, url);
308
+ }
309
+ }
310
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ExternalUrlResolverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
311
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ExternalUrlResolverService, providedIn: 'root' });
312
+ }
313
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ExternalUrlResolverService, decorators: [{
314
+ type: Injectable,
315
+ args: [{
316
+ providedIn: 'root',
317
+ }]
318
+ }] });
319
+
320
+ /**
321
+ * Streams top-bar notifications from a SignalR hub.
322
+ *
323
+ * The hub is configured via `AppConfig.notificationsEnabled` and `AppConfig.notificationsUrl`.
324
+ * When enabled, {@link start} opens a connection (authenticated with the current JWT) and
325
+ * listens for the server to push the notification list via the `ReceiveNotifications` client
326
+ * method. Consuming apps do not interact with this service directly — the top bar drives it.
327
+ */
328
+ class NotificationsService {
329
+ //#region Variables
330
+ authService = inject(AuthService);
331
+ config = inject(APP_CONFIG);
332
+ connection;
333
+ notifications$ = new BehaviorSubject([]);
334
+ //#endregion
335
+ //#region Properties
336
+ /** Whether the notifications feature is enabled (toggled on and given a hub URL). */
337
+ get isEnabled() {
338
+ return this.config.notificationsEnabled && this.config.notificationsUrl.length > 0;
339
+ }
340
+ //#endregion
341
+ //#region Public methods
342
+ /** Emits the current list of notifications. */
343
+ getNotifications() {
344
+ return this.notifications$.asObservable();
345
+ }
346
+ /** Emits the number of unread notifications. */
347
+ getUnreadCount() {
348
+ return this.notifications$.pipe(map((notifications) => notifications.filter((n) => !n.isRead).length));
349
+ }
350
+ /** Marks every notification as read and notifies the hub. */
351
+ markAllAsRead() {
352
+ this.notifications$.next(this.notifications$.value.map((n) => ({ ...n, isRead: true })));
353
+ if (this.connection?.state === HubConnectionState.Connected) {
354
+ this.connection.invoke('MarkAllAsRead').catch((error) => {
355
+ console.error('Failed to mark notifications as read on the hub.', error);
356
+ });
357
+ }
358
+ }
359
+ /** Marks a single notification as read (optimistically; the server reconciles on the next push). */
360
+ markAsRead(notification) {
361
+ this.notifications$.next(this.notifications$.value.map((n) => (n === notification ? { ...n, isRead: true } : n)));
362
+ }
363
+ /** Opens the SignalR connection and starts receiving notifications. Idempotent and a no-op when disabled. */
364
+ start() {
365
+ if (!this.isEnabled || !!this.connection) {
366
+ return;
367
+ }
368
+ this.connection = new HubConnectionBuilder()
369
+ .withUrl(this.config.notificationsUrl, {
370
+ accessTokenFactory: () => this.authService.token ?? '',
371
+ })
372
+ .withAutomaticReconnect()
373
+ .build();
374
+ this.connection.on('ReceiveNotifications', (notifications) => {
375
+ this.notifications$.next(notifications ?? []);
376
+ });
377
+ this.connection.start().catch((error) => {
378
+ console.error('Failed to connect to the notifications hub.', error);
379
+ });
380
+ }
381
+ /** Closes the SignalR connection. */
382
+ async stop() {
383
+ if (this.connection) {
384
+ await this.connection.stop();
385
+ this.connection = undefined;
386
+ }
387
+ }
388
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NotificationsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
389
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NotificationsService, providedIn: 'root' });
390
+ }
391
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NotificationsService, decorators: [{
392
+ type: Injectable,
393
+ args: [{
394
+ providedIn: 'root',
395
+ }]
396
+ }] });
397
+
398
+ class OperationsHistoryService {
399
+ //#region ViewChilds, Inputs, Outputs
400
+ //#endregion
401
+ //#region Variables
402
+ config = inject(APP_CONFIG);
403
+ http = inject(HttpClient);
404
+ //#endregion
405
+ //#region Properties
406
+ //#endregion
407
+ //#region Constructor and Angular life cycle methods
408
+ //#endregion
409
+ //#region Event handlers
410
+ //#endregion
411
+ //#region Public methods
412
+ list(controllerName, entityID, serviceHistoryID, parameters) {
413
+ return this.http.post(`${this.config.BASE_URL}/${controllerName}/${entityID}/Audit/${serviceHistoryID}`, parameters);
414
+ }
415
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: OperationsHistoryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
416
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: OperationsHistoryService, providedIn: 'root' });
417
+ }
418
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: OperationsHistoryService, decorators: [{
419
+ type: Injectable,
420
+ args: [{
421
+ providedIn: 'root',
422
+ }]
423
+ }] });
424
+
425
+ class ServicesHistoryService {
426
+ //#region ViewChilds, Inputs, Outputs
427
+ //#endregion
428
+ //#region Variables
429
+ config = inject(APP_CONFIG);
430
+ http = inject(HttpClient);
431
+ //#endregion
432
+ //#region Properties
433
+ //#endregion
434
+ //#region Constructor and Angular life cycle methods
435
+ //#endregion
436
+ //#region Event handlers
437
+ //#endregion
438
+ //#region Public methods
439
+ list(controllerName, entityID, parameters) {
440
+ return this.http.post(`${this.config.BASE_URL}/${controllerName}/${entityID}/Audit`, parameters);
441
+ }
442
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ServicesHistoryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
443
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ServicesHistoryService, providedIn: 'root' });
444
+ }
445
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ServicesHistoryService, decorators: [{
446
+ type: Injectable,
447
+ args: [{
448
+ providedIn: 'root',
449
+ }]
450
+ }] });
451
+
452
+ class BrandComponent {
453
+ //#region Variables
454
+ config = inject(APP_CONFIG);
455
+ //#endregion
456
+ //#region Properties
457
+ get appName() {
458
+ return this.config.appName;
459
+ }
460
+ get companyName() {
461
+ return this.config.companyName;
462
+ }
463
+ get logoUrl() {
464
+ return this.config.logoUrl;
465
+ }
466
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: BrandComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
467
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: BrandComponent, isStandalone: true, selector: "shared-brand", ngImport: i0, template: "<div class=\"brand\">\r\n @if (logoUrl) {\r\n <img class=\"brand-logo\" [src]=\"logoUrl\" [alt]=\"appName\" />\r\n }\r\n\r\n <div class=\"brand-text\">\r\n <span class=\"brand-app\">{{ appName }}</span>\r\n @if (companyName) {\r\n <span class=\"brand-company\">{{ companyName }}</span>\r\n }\r\n </div>\r\n</div>\r\n", styles: ["@layer properties;.brand{display:flex;min-width:0px;align-items:center;gap:calc(var(--spacing, .25rem) * 2)}.brand .brand-app{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}.brand .brand-company{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));color:var(--color-stone-500, oklch(55.3% .013 58.071))}.brand .brand-logo{height:calc(var(--spacing, .25rem) * 8);width:calc(var(--spacing, .25rem) * 8);flex-shrink:0;object-fit:contain}.brand .brand-text{display:flex;min-width:0px;flex-direction:column;--tw-leading: var(--leading-tight, 1.25);line-height:var(--leading-tight, 1.25)}@media(max-width:767px){.brand .brand-text{display:none}}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-leading{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-leading: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
468
+ }
469
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: BrandComponent, decorators: [{
470
+ type: Component,
471
+ args: [{ selector: 'shared-brand', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"brand\">\r\n @if (logoUrl) {\r\n <img class=\"brand-logo\" [src]=\"logoUrl\" [alt]=\"appName\" />\r\n }\r\n\r\n <div class=\"brand-text\">\r\n <span class=\"brand-app\">{{ appName }}</span>\r\n @if (companyName) {\r\n <span class=\"brand-company\">{{ companyName }}</span>\r\n }\r\n </div>\r\n</div>\r\n", styles: ["@layer properties;.brand{display:flex;min-width:0px;align-items:center;gap:calc(var(--spacing, .25rem) * 2)}.brand .brand-app{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}.brand .brand-company{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));color:var(--color-stone-500, oklch(55.3% .013 58.071))}.brand .brand-logo{height:calc(var(--spacing, .25rem) * 8);width:calc(var(--spacing, .25rem) * 8);flex-shrink:0;object-fit:contain}.brand .brand-text{display:flex;min-width:0px;flex-direction:column;--tw-leading: var(--leading-tight, 1.25);line-height:var(--leading-tight, 1.25)}@media(max-width:767px){.brand .brand-text{display:none}}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-leading{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-leading: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
472
+ }] });
473
+
474
+ class EnvironmentBadgeComponent {
475
+ //#region Variables
476
+ config = inject(APP_CONFIG);
477
+ //#endregion
478
+ //#region Properties
479
+ /** CSS modifier class for the current environment, falling back to a neutral style. */
480
+ get badgeClass() {
481
+ const colorByEnvironment = {
482
+ DEV: 'env-dev',
483
+ QA: 'env-qa',
484
+ STG: 'env-stg',
485
+ };
486
+ return colorByEnvironment[this.environment] ?? 'env-neutral';
487
+ }
488
+ /** Normalized environment key (uppercase, trimmed). */
489
+ get environment() {
490
+ return (this.config.environment ?? '').trim().toUpperCase();
491
+ }
492
+ /** The badge is hidden in production and when no environment is configured. */
493
+ get isVisible() {
494
+ return this.environment.length > 0 && this.environment !== 'PROD';
495
+ }
496
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: EnvironmentBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
497
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: EnvironmentBadgeComponent, isStandalone: true, selector: "shared-environment-badge", ngImport: i0, template: "@if (isVisible) {\r\n <span class=\"env-badge {{ badgeClass }}\">\r\n <span class=\"env-dot\"></span>\r\n <span class=\"env-label\">{{ environment }}</span>\r\n </span>\r\n}\r\n", styles: ["@layer properties;.env-badge{display:inline-flex;align-items:center;gap:calc(var(--spacing, .25rem) * 1.5);border-radius:calc(infinity * 1px);padding-inline:calc(var(--spacing, .25rem) * 2.5);padding-block:.325rem;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);--tw-tracking: var(--tracking-wide, .025em);letter-spacing:var(--tracking-wide, .025em);text-transform:uppercase;--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-inset: inset}.env-badge .env-dot{height:calc(var(--spacing, .25rem) * 1.5);width:calc(var(--spacing, .25rem) * 1.5);flex-shrink:0;border-radius:calc(infinity * 1px)}.env-badge.env-dev{background-color:var(--color-blue-50, oklch(97% .014 254.604));color:var(--color-blue-700, oklch(48.8% .243 264.376));--tw-ring-color: color-mix(in srgb, oklch(54.6% .245 262.881) 20%, transparent)}@supports (color: color-mix(in lab,red,red)){.env-badge.env-dev{--tw-ring-color: color-mix(in oklab, var(--color-blue-600, oklch(54.6% .245 262.881)) 20%, transparent)}}.env-badge.env-dev .env-dot{background-color:var(--color-blue-500, oklch(62.3% .214 259.815))}.env-badge.env-neutral{background-color:var(--color-stone-100, oklch(97% .001 106.424));color:var(--color-stone-600, oklch(44.4% .011 73.639));--tw-ring-color: color-mix(in srgb, oklch(55.3% .013 58.071) 20%, transparent)}@supports (color: color-mix(in lab,red,red)){.env-badge.env-neutral{--tw-ring-color: color-mix(in oklab, var(--color-stone-500, oklch(55.3% .013 58.071)) 20%, transparent)}}.env-badge.env-neutral .env-dot{background-color:var(--color-stone-500, oklch(55.3% .013 58.071))}.env-badge.env-qa{background-color:var(--color-amber-50, oklch(98.7% .022 95.277));color:var(--color-amber-700, oklch(55.5% .163 48.998));--tw-ring-color: color-mix(in srgb, oklch(66.6% .179 58.318) 20%, transparent)}@supports (color: color-mix(in lab,red,red)){.env-badge.env-qa{--tw-ring-color: color-mix(in oklab, var(--color-amber-600, oklch(66.6% .179 58.318)) 20%, transparent)}}.env-badge.env-qa .env-dot{background-color:var(--color-amber-500, oklch(76.9% .188 70.08))}.env-badge.env-stg{background-color:var(--color-violet-50, oklch(96.9% .016 293.756));color:var(--color-violet-700, oklch(49.1% .27 292.581));--tw-ring-color: color-mix(in srgb, oklch(54.1% .281 293.009) 20%, transparent)}@supports (color: color-mix(in lab,red,red)){.env-badge.env-stg{--tw-ring-color: color-mix(in oklab, var(--color-violet-600, oklch(54.1% .281 293.009)) 20%, transparent)}}.env-badge.env-stg .env-dot{background-color:var(--color-violet-500, oklch(60.6% .25 292.717))}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-tracking{syntax: \"*\"; inherits: false;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-tracking: initial;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
498
+ }
499
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: EnvironmentBadgeComponent, decorators: [{
500
+ type: Component,
501
+ args: [{ selector: 'shared-environment-badge', changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (isVisible) {\r\n <span class=\"env-badge {{ badgeClass }}\">\r\n <span class=\"env-dot\"></span>\r\n <span class=\"env-label\">{{ environment }}</span>\r\n </span>\r\n}\r\n", styles: ["@layer properties;.env-badge{display:inline-flex;align-items:center;gap:calc(var(--spacing, .25rem) * 1.5);border-radius:calc(infinity * 1px);padding-inline:calc(var(--spacing, .25rem) * 2.5);padding-block:.325rem;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);--tw-tracking: var(--tracking-wide, .025em);letter-spacing:var(--tracking-wide, .025em);text-transform:uppercase;--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-inset: inset}.env-badge .env-dot{height:calc(var(--spacing, .25rem) * 1.5);width:calc(var(--spacing, .25rem) * 1.5);flex-shrink:0;border-radius:calc(infinity * 1px)}.env-badge.env-dev{background-color:var(--color-blue-50, oklch(97% .014 254.604));color:var(--color-blue-700, oklch(48.8% .243 264.376));--tw-ring-color: color-mix(in srgb, oklch(54.6% .245 262.881) 20%, transparent)}@supports (color: color-mix(in lab,red,red)){.env-badge.env-dev{--tw-ring-color: color-mix(in oklab, var(--color-blue-600, oklch(54.6% .245 262.881)) 20%, transparent)}}.env-badge.env-dev .env-dot{background-color:var(--color-blue-500, oklch(62.3% .214 259.815))}.env-badge.env-neutral{background-color:var(--color-stone-100, oklch(97% .001 106.424));color:var(--color-stone-600, oklch(44.4% .011 73.639));--tw-ring-color: color-mix(in srgb, oklch(55.3% .013 58.071) 20%, transparent)}@supports (color: color-mix(in lab,red,red)){.env-badge.env-neutral{--tw-ring-color: color-mix(in oklab, var(--color-stone-500, oklch(55.3% .013 58.071)) 20%, transparent)}}.env-badge.env-neutral .env-dot{background-color:var(--color-stone-500, oklch(55.3% .013 58.071))}.env-badge.env-qa{background-color:var(--color-amber-50, oklch(98.7% .022 95.277));color:var(--color-amber-700, oklch(55.5% .163 48.998));--tw-ring-color: color-mix(in srgb, oklch(66.6% .179 58.318) 20%, transparent)}@supports (color: color-mix(in lab,red,red)){.env-badge.env-qa{--tw-ring-color: color-mix(in oklab, var(--color-amber-600, oklch(66.6% .179 58.318)) 20%, transparent)}}.env-badge.env-qa .env-dot{background-color:var(--color-amber-500, oklch(76.9% .188 70.08))}.env-badge.env-stg{background-color:var(--color-violet-50, oklch(96.9% .016 293.756));color:var(--color-violet-700, oklch(49.1% .27 292.581));--tw-ring-color: color-mix(in srgb, oklch(54.1% .281 293.009) 20%, transparent)}@supports (color: color-mix(in lab,red,red)){.env-badge.env-stg{--tw-ring-color: color-mix(in oklab, var(--color-violet-600, oklch(54.1% .281 293.009)) 20%, transparent)}}.env-badge.env-stg .env-dot{background-color:var(--color-violet-500, oklch(60.6% .25 292.717))}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-tracking{syntax: \"*\"; inherits: false;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-tracking: initial;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
502
+ }] });
503
+
504
+ class NotificationsComponent {
505
+ //#region ViewChilds, Inputs, Outputs
506
+ dropdown = viewChild('dropdown', /* @ts-ignore */
507
+ ...(ngDevMode ? [{ debugName: "dropdown" }] : /* istanbul ignore next */ []));
508
+ //#endregion
509
+ //#region Variables
510
+ notificationsService = inject(NotificationsService);
511
+ router = inject(Router);
512
+ notifications$ = this.notificationsService.getNotifications();
513
+ // A signal rather than a field: the panel is also closed from a window click handler, which
514
+ // is not a binding on this view and so marks nothing.
515
+ showDropdown = signal(false, /* @ts-ignore */
516
+ ...(ngDevMode ? [{ debugName: "showDropdown" }] : /* istanbul ignore next */ []));
517
+ unreadCount$ = this.notificationsService.getUnreadCount();
518
+ //#endregion
519
+ //#region Properties
520
+ /** Whether the notifications feature is enabled; when false the bell is not rendered. */
521
+ get isEnabled() {
522
+ return this.notificationsService.isEnabled;
523
+ }
524
+ //#endregion
525
+ //#region Constructor and Angular life cycle methods
526
+ ngOnInit() {
527
+ this.notificationsService.start();
528
+ }
529
+ //#endregion
530
+ //#region Event handlers
531
+ onDropdownClick() {
532
+ this.showDropdown.update((isShown) => !isShown);
533
+ }
534
+ onMarkAllAsRead() {
535
+ this.notificationsService.markAllAsRead();
536
+ }
537
+ onNotificationClick(notification) {
538
+ this.notificationsService.markAsRead(notification);
539
+ this.showDropdown.set(false);
540
+ const url = notification.callToActionUrl;
541
+ if (!url) {
542
+ return;
543
+ }
544
+ if (/^https?:\/\//i.test(url)) {
545
+ window.open(url, '_blank', 'noopener');
546
+ }
547
+ else {
548
+ this.router.navigateByUrl(url);
549
+ }
550
+ }
551
+ //#endregion
552
+ //#region Private methods
553
+ documentClick(event) {
554
+ if (this.showDropdown()) {
555
+ let target = event.target;
556
+ while (target !== null && target.tagName?.toUpperCase() !== 'BODY') {
557
+ if (target === this.dropdown()?.nativeElement) {
558
+ return;
559
+ }
560
+ target = target.parentElement;
561
+ }
562
+ this.showDropdown.set(false);
563
+ }
564
+ }
565
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NotificationsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
566
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: NotificationsComponent, isStandalone: true, selector: "shared-notifications", host: { listeners: { "window:click": "documentClick($event)" } }, viewQueries: [{ propertyName: "dropdown", first: true, predicate: ["dropdown"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (isEnabled) {\r\n <div #dropdown class=\"notifications\">\r\n <button\r\n class=\"btn gray-300 notifications-toggle\"\r\n type=\"button\"\r\n (click)=\"onDropdownClick()\"\r\n [attr.aria-label]=\"'TopBar-Notifications-Title' | translate\"\r\n >\r\n <i class=\"fa-solid fa-bell\"></i>\r\n @if (unreadCount$ | async; as count) {\r\n <span class=\"notifications-badge\">{{ count }}</span>\r\n }\r\n </button>\r\n\r\n @if (showDropdown()) {\r\n <div class=\"notifications-panel\">\r\n @let items = (notifications$ | async) ?? [];\r\n\r\n <div class=\"notifications-header\">\r\n <span>{{ 'TopBar-Notifications-Title' | translate }}</span>\r\n @if (items.length > 0) {\r\n <button type=\"button\" class=\"notifications-mark-all\" (click)=\"onMarkAllAsRead()\">\r\n {{ 'TopBar-Notifications-MarkAllRead' | translate }}\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (items.length > 0) {\r\n <ul class=\"notifications-list\">\r\n @for (item of items; track $index) {\r\n <li class=\"notifications-item\" [class.unread]=\"!item.isRead\">\r\n <button\r\n type=\"button\"\r\n class=\"notifications-item-btn\"\r\n [class.has-cta]=\"!!item.callToActionUrl\"\r\n (click)=\"onNotificationClick(item)\"\r\n >\r\n <span class=\"notifications-item-icon\"><i class=\"{{ item.icon }}\"></i></span>\r\n <span class=\"notifications-item-body\">\r\n <span class=\"notifications-item-title\">{{ item.title }}</span>\r\n <span class=\"notifications-item-description\">{{ item.description }}</span>\r\n </span>\r\n @if (item.callToActionUrl) {\r\n <i class=\"notifications-item-cta fa-solid fa-chevron-right\"></i>\r\n }\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n } @else {\r\n <div class=\"notifications-empty\">{{ 'TopBar-Notifications-Empty' | translate }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n}\r\n", styles: ["@layer properties;.notifications{position:relative}.notifications .notifications-badge{position:absolute;top:calc(var(--spacing, .25rem) * -1);right:calc(var(--spacing, .25rem) * -1);height:calc(var(--spacing, .25rem) * 4);min-width:calc(var(--spacing, .25rem) * 4);border-radius:calc(infinity * 1px);padding-inline:var(--spacing, .25rem);display:flex;align-items:center;justify-content:center;font-size:10px;--tw-leading: 1;line-height:1;--tw-font-weight: var(--font-weight-bold, 700);font-weight:var(--font-weight-bold, 700);background-color:var(--color-red-600, oklch(57.7% .245 27.325));color:var(--color-white, #fff)}.notifications .notifications-empty{padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 6);text-align:center;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-stone-500, oklch(55.3% .013 58.071))}.notifications .notifications-header{display:flex;align-items:center;justify-content:space-between;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-stone-200, oklch(92.3% .003 48.717));padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}.notifications .notifications-list{max-height:calc(var(--spacing, .25rem) * 80);overflow-y:auto}.notifications .notifications-list .notifications-item{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-stone-100, oklch(97% .001 106.424))}.notifications .notifications-list .notifications-item:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0px}.notifications .notifications-list .notifications-item.unread{background-color:color-mix(in srgb,#e6f4ff 60%,transparent)}@supports (color: color-mix(in lab,red,red)){.notifications .notifications-list .notifications-item.unread{background-color:color-mix(in oklab,var(--color-primary-50, #e6f4ff) 60%,transparent)}}.notifications .notifications-list .notifications-item .notifications-item-body{display:flex;min-width:0px;flex:1;flex-direction:column}.notifications .notifications-list .notifications-item .notifications-item-btn{display:flex;width:100%;cursor:pointer;align-items:flex-start;gap:calc(var(--spacing, .25rem) * 2.5);padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2);text-align:left}@media(hover:hover){.notifications .notifications-list .notifications-item .notifications-item-btn:hover{background-color:var(--color-stone-50, oklch(98.5% .001 106.423))}}.notifications .notifications-list .notifications-item .notifications-item-cta{margin-top:var(--spacing, .25rem);flex-shrink:0;font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));color:var(--color-stone-400, oklch(70.9% .01 56.259))}.notifications .notifications-list .notifications-item .notifications-item-description{font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));color:var(--color-stone-500, oklch(55.3% .013 58.071))}.notifications .notifications-list .notifications-item .notifications-item-icon{margin-top:calc(var(--spacing, .25rem) * .5);display:flex;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);flex-shrink:0;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);background-color:var(--color-stone-100, oklch(97% .001 106.424));font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));color:var(--color-stone-600, oklch(44.4% .011 73.639))}.notifications .notifications-list .notifications-item .notifications-item-title{font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-stone-800, oklch(26.8% .007 34.298))}.notifications .notifications-mark-all{font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-primary-400, #006bb6)}@media(hover:hover){.notifications .notifications-mark-all:hover{color:var(--color-primary-500, #005794)}}.notifications .notifications-panel{position:absolute;top:100%;right:0;z-index:50;margin-top:var(--spacing, .25rem);width:calc(var(--spacing, .25rem) * 80);max-width:90vw;border-radius:var(--radius-md, .375rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-stone-200, oklch(92.3% .003 48.717));background-color:var(--color-white, #fff);--tw-shadow: 0 0 4px 0 var(--tw-shadow-color, rgba(0,0,0,.2)), 0 3px 20px 0 var(--tw-shadow-color, rgba(0,0,0,.19));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.notifications .notifications-toggle{position:relative}.notifications .notifications-toggle i{margin-right:0}@property --tw-leading{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-leading: initial;--tw-font-weight: initial;--tw-border-style: solid;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
567
+ }
568
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NotificationsComponent, decorators: [{
569
+ type: Component,
570
+ args: [{ selector: 'shared-notifications', imports: [AsyncPipe, TranslatePipe], template: "@if (isEnabled) {\r\n <div #dropdown class=\"notifications\">\r\n <button\r\n class=\"btn gray-300 notifications-toggle\"\r\n type=\"button\"\r\n (click)=\"onDropdownClick()\"\r\n [attr.aria-label]=\"'TopBar-Notifications-Title' | translate\"\r\n >\r\n <i class=\"fa-solid fa-bell\"></i>\r\n @if (unreadCount$ | async; as count) {\r\n <span class=\"notifications-badge\">{{ count }}</span>\r\n }\r\n </button>\r\n\r\n @if (showDropdown()) {\r\n <div class=\"notifications-panel\">\r\n @let items = (notifications$ | async) ?? [];\r\n\r\n <div class=\"notifications-header\">\r\n <span>{{ 'TopBar-Notifications-Title' | translate }}</span>\r\n @if (items.length > 0) {\r\n <button type=\"button\" class=\"notifications-mark-all\" (click)=\"onMarkAllAsRead()\">\r\n {{ 'TopBar-Notifications-MarkAllRead' | translate }}\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (items.length > 0) {\r\n <ul class=\"notifications-list\">\r\n @for (item of items; track $index) {\r\n <li class=\"notifications-item\" [class.unread]=\"!item.isRead\">\r\n <button\r\n type=\"button\"\r\n class=\"notifications-item-btn\"\r\n [class.has-cta]=\"!!item.callToActionUrl\"\r\n (click)=\"onNotificationClick(item)\"\r\n >\r\n <span class=\"notifications-item-icon\"><i class=\"{{ item.icon }}\"></i></span>\r\n <span class=\"notifications-item-body\">\r\n <span class=\"notifications-item-title\">{{ item.title }}</span>\r\n <span class=\"notifications-item-description\">{{ item.description }}</span>\r\n </span>\r\n @if (item.callToActionUrl) {\r\n <i class=\"notifications-item-cta fa-solid fa-chevron-right\"></i>\r\n }\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n } @else {\r\n <div class=\"notifications-empty\">{{ 'TopBar-Notifications-Empty' | translate }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n}\r\n", styles: ["@layer properties;.notifications{position:relative}.notifications .notifications-badge{position:absolute;top:calc(var(--spacing, .25rem) * -1);right:calc(var(--spacing, .25rem) * -1);height:calc(var(--spacing, .25rem) * 4);min-width:calc(var(--spacing, .25rem) * 4);border-radius:calc(infinity * 1px);padding-inline:var(--spacing, .25rem);display:flex;align-items:center;justify-content:center;font-size:10px;--tw-leading: 1;line-height:1;--tw-font-weight: var(--font-weight-bold, 700);font-weight:var(--font-weight-bold, 700);background-color:var(--color-red-600, oklch(57.7% .245 27.325));color:var(--color-white, #fff)}.notifications .notifications-empty{padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 6);text-align:center;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-stone-500, oklch(55.3% .013 58.071))}.notifications .notifications-header{display:flex;align-items:center;justify-content:space-between;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-stone-200, oklch(92.3% .003 48.717));padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}.notifications .notifications-list{max-height:calc(var(--spacing, .25rem) * 80);overflow-y:auto}.notifications .notifications-list .notifications-item{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-stone-100, oklch(97% .001 106.424))}.notifications .notifications-list .notifications-item:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0px}.notifications .notifications-list .notifications-item.unread{background-color:color-mix(in srgb,#e6f4ff 60%,transparent)}@supports (color: color-mix(in lab,red,red)){.notifications .notifications-list .notifications-item.unread{background-color:color-mix(in oklab,var(--color-primary-50, #e6f4ff) 60%,transparent)}}.notifications .notifications-list .notifications-item .notifications-item-body{display:flex;min-width:0px;flex:1;flex-direction:column}.notifications .notifications-list .notifications-item .notifications-item-btn{display:flex;width:100%;cursor:pointer;align-items:flex-start;gap:calc(var(--spacing, .25rem) * 2.5);padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2);text-align:left}@media(hover:hover){.notifications .notifications-list .notifications-item .notifications-item-btn:hover{background-color:var(--color-stone-50, oklch(98.5% .001 106.423))}}.notifications .notifications-list .notifications-item .notifications-item-cta{margin-top:var(--spacing, .25rem);flex-shrink:0;font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));color:var(--color-stone-400, oklch(70.9% .01 56.259))}.notifications .notifications-list .notifications-item .notifications-item-description{font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));color:var(--color-stone-500, oklch(55.3% .013 58.071))}.notifications .notifications-list .notifications-item .notifications-item-icon{margin-top:calc(var(--spacing, .25rem) * .5);display:flex;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);flex-shrink:0;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);background-color:var(--color-stone-100, oklch(97% .001 106.424));font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));color:var(--color-stone-600, oklch(44.4% .011 73.639))}.notifications .notifications-list .notifications-item .notifications-item-title{font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-stone-800, oklch(26.8% .007 34.298))}.notifications .notifications-mark-all{font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-primary-400, #006bb6)}@media(hover:hover){.notifications .notifications-mark-all:hover{color:var(--color-primary-500, #005794)}}.notifications .notifications-panel{position:absolute;top:100%;right:0;z-index:50;margin-top:var(--spacing, .25rem);width:calc(var(--spacing, .25rem) * 80);max-width:90vw;border-radius:var(--radius-md, .375rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-stone-200, oklch(92.3% .003 48.717));background-color:var(--color-white, #fff);--tw-shadow: 0 0 4px 0 var(--tw-shadow-color, rgba(0,0,0,.2)), 0 3px 20px 0 var(--tw-shadow-color, rgba(0,0,0,.19));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.notifications .notifications-toggle{position:relative}.notifications .notifications-toggle i{margin-right:0}@property --tw-leading{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-leading: initial;--tw-font-weight: initial;--tw-border-style: solid;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
571
+ }], propDecorators: { dropdown: [{ type: i0.ViewChild, args: ['dropdown', { isSignal: true }] }], documentClick: [{
572
+ type: HostListener,
573
+ args: ['window:click', ['$event']]
574
+ }] } });
575
+
576
+ class LanguageSelectorComponent {
577
+ //#region ViewChilds, Inputs, Outputs
578
+ dropdown = viewChild('dropdown', /* @ts-ignore */
579
+ ...(ngDevMode ? [{ debugName: "dropdown" }] : /* istanbul ignore next */ []));
580
+ /** When true, the toggle shows the current language's flag (light-gray button) instead of text. */
581
+ showFlag = input(false, /* @ts-ignore */
582
+ ...(ngDevMode ? [{ debugName: "showFlag" }] : /* istanbul ignore next */ []));
583
+ title = input('LanguageSelector-Title', /* @ts-ignore */
584
+ ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
585
+ //#endregion
586
+ //#region Variables
587
+ translate = inject(TranslateService);
588
+ languages = this.translate.getLangs();
589
+ // A signal rather than a field: the list is also closed from a window click handler, which
590
+ // is not a binding on this view and so marks nothing.
591
+ showDropdown = signal(false, /* @ts-ignore */
592
+ ...(ngDevMode ? [{ debugName: "showDropdown" }] : /* istanbul ignore next */ []));
593
+ /**
594
+ * `TranslateService.currentLang` is a plain field. The flag on the toggle and the tick beside
595
+ * the chosen entry are read from it, and switching language changed neither until something
596
+ * else happened to refresh this view. Mirrored from the service's own event instead.
597
+ */
598
+ currentLang = toSignal(this.translate.onLangChange.pipe(map((event) => event.lang)), { initialValue: this.translate.currentLang });
599
+ //#endregion
600
+ //#region Properties
601
+ get selectedLanguage() {
602
+ return this.currentLang();
603
+ }
604
+ //#endregion
605
+ //#region Constructor and Angular life cycle methods
606
+ //#endregion
607
+ //#region Event handlers
608
+ onDropdownClick() {
609
+ this.showDropdown.update((isShown) => !isShown);
610
+ }
611
+ onSelectLanguage(language) {
612
+ localStorage.setItem('language', language);
613
+ this.translate.use(language);
614
+ this.showDropdown.set(false);
615
+ }
616
+ //#endregion
617
+ //#region Public methods
618
+ //#endregion
619
+ //#region Private methods
620
+ documentClick(event) {
621
+ if (this.showDropdown()) {
622
+ let target = event.target;
623
+ while (target !== null && target.tagName?.toUpperCase() !== 'BODY') {
624
+ if (target === this.dropdown()?.nativeElement) {
625
+ return;
626
+ }
627
+ target = target.parentElement;
628
+ }
629
+ this.showDropdown.set(false);
630
+ }
631
+ }
632
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: LanguageSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
633
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: LanguageSelectorComponent, isStandalone: true, selector: "shared-language-selector", inputs: { showFlag: { classPropertyName: "showFlag", publicName: "showFlag", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "window:click": "documentClick($event)" } }, viewQueries: [{ propertyName: "dropdown", first: true, predicate: ["dropdown"], descendants: true, isSignal: true }], ngImport: i0, template: "<div #dropdown class=\"dropdown right\" [class.show]=\"showDropdown()\">\r\n <button\r\n class=\"btn\"\r\n type=\"button\"\r\n (click)=\"onDropdownClick()\"\r\n [class.gray-300]=\"showFlag()\"\r\n [class.lang-flag-toggle]=\"showFlag()\"\r\n [attr.aria-label]=\"showFlag() ? (title() | translate) : null\"\r\n >\r\n @if (showFlag()) {\r\n <span class=\"flag flag-{{ selectedLanguage }}\"></span>\r\n } @else {\r\n {{ title() | translate }}\r\n }\r\n </button>\r\n\r\n <ul>\r\n @for (lang of languages; track lang) {\r\n <li>\r\n <button\r\n type=\"button\"\r\n class=\"dropdown-item\"\r\n [class.selected]=\"lang === selectedLanguage\"\r\n (click)=\"onSelectLanguage(lang)\"\r\n >\r\n <span class=\"flag flag-{{ lang }}\"></span>\r\n <span class=\"dropdown-label\">{{ 'Language-' + lang | translate }}</span>\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n</div>\r\n", styles: ["@layer properties;.dropdown .btn{width:100%;--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500)}.dropdown .flag{display:inline-block;height:calc(var(--spacing, .25rem) * 4);width:calc(var(--spacing, .25rem) * 6);flex-shrink:0;border-radius:var(--radius-xs, .125rem);background-size:cover;background-position:center;--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color: color-mix(in srgb, #000 10%, transparent)}@supports (color: color-mix(in lab,red,red)){.dropdown .flag{--tw-ring-color: color-mix(in oklab, var(--color-black, #000) 10%, transparent)}}.dropdown .flag.flag-en{background-image:url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 76 40\"%3E%3Crect width=\"76\" height=\"40\" fill=\"%23b22234\"/%3E%3Cg fill=\"%23fff\"%3E%3Crect y=\"3\" width=\"76\" height=\"3\"/%3E%3Crect y=\"9\" width=\"76\" height=\"3\"/%3E%3Crect y=\"15\" width=\"76\" height=\"3\"/%3E%3Crect y=\"22\" width=\"76\" height=\"3\"/%3E%3Crect y=\"28\" width=\"76\" height=\"3\"/%3E%3Crect y=\"34\" width=\"76\" height=\"3\"/%3E%3C/g%3E%3Crect width=\"30\" height=\"22\" fill=\"%233c3b6e\"/%3E%3C/svg%3E')}.dropdown .flag.flag-pt{background-image:url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 70 49\"%3E%3Crect width=\"70\" height=\"49\" fill=\"%23009c3b\"/%3E%3Cpath d=\"M35 6L64 24.5L35 43L6 24.5Z\" fill=\"%23ffdf00\"/%3E%3Ccircle cx=\"35\" cy=\"24.5\" r=\"9.8\" fill=\"%23002776\"/%3E%3C/svg%3E')}.dropdown .lang-flag-toggle{display:flex;height:calc(var(--spacing, .25rem) * 9);width:auto;align-items:center;justify-content:center;border-style:var(--tw-border-style);border-width:0px;padding-inline:calc(var(--spacing, .25rem) * 2.5)}.dropdown .lang-flag-toggle:after{border-top-color:currentColor;margin-left:.325rem}.dropdown ul{z-index:50;margin-top:var(--spacing, .25rem);min-width:11rem;padding:var(--spacing, .25rem);border-radius:var(--radius-md, .375rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-stone-200, oklch(92.3% .003 48.717));background-color:var(--color-white, #fff);--tw-backdrop-blur: ;-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);--tw-shadow: 0 0 4px 0 var(--tw-shadow-color, rgba(0,0,0,.2)), 0 3px 20px 0 var(--tw-shadow-color, rgba(0,0,0,.19));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dropdown ul .dropdown-item{display:flex;width:100%;cursor:pointer;align-items:center;gap:calc(var(--spacing, .25rem) * 2);border-radius:var(--radius-sm, .25rem);padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2);-webkit-user-select:none;user-select:none;text-align:left;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-stone-700, oklch(37.4% .01 67.558))}@media(hover:hover){.dropdown ul .dropdown-item:hover{background-color:var(--color-stone-100, oklch(97% .001 106.424))}}@media(hover:hover){.dropdown ul .dropdown-item:hover{color:var(--color-stone-700, oklch(37.4% .01 67.558))}}.dropdown ul .dropdown-item:focus{background-color:var(--color-stone-100, oklch(97% .001 106.424))}.dropdown ul .dropdown-item:focus{--tw-outline-style: none;outline-style:none}@media(forced-colors:active){.dropdown ul .dropdown-item:focus{outline:2px solid transparent;outline-offset:2px}}.dropdown ul .dropdown-item:active{background-color:var(--color-stone-200, oklch(92.3% .003 48.717))}.dropdown ul .dropdown-item:active{color:var(--color-stone-700, oklch(37.4% .01 67.558))}.dropdown ul .dropdown-item.selected{background-color:var(--color-primary-50, #e6f4ff);--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:var(--color-primary-700, #002f50)}.dropdown ul .dropdown-item.selected:hover{background-color:var(--color-primary-100, #9acaed)}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-backdrop-blur{syntax: \"*\"; inherits: false;}@property --tw-backdrop-brightness{syntax: \"*\"; inherits: false;}@property --tw-backdrop-contrast{syntax: \"*\"; inherits: false;}@property --tw-backdrop-grayscale{syntax: \"*\"; inherits: false;}@property --tw-backdrop-hue-rotate{syntax: \"*\"; inherits: false;}@property --tw-backdrop-invert{syntax: \"*\"; inherits: false;}@property --tw-backdrop-opacity{syntax: \"*\"; inherits: false;}@property --tw-backdrop-saturate{syntax: \"*\"; inherits: false;}@property --tw-backdrop-sepia{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-border-style: solid;--tw-backdrop-blur: initial;--tw-backdrop-brightness: initial;--tw-backdrop-contrast: initial;--tw-backdrop-grayscale: initial;--tw-backdrop-hue-rotate: initial;--tw-backdrop-invert: initial;--tw-backdrop-opacity: initial;--tw-backdrop-saturate: initial;--tw-backdrop-sepia: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }] });
634
+ }
635
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: LanguageSelectorComponent, decorators: [{
636
+ type: Component,
637
+ args: [{ selector: 'shared-language-selector', imports: [TranslatePipe], template: "<div #dropdown class=\"dropdown right\" [class.show]=\"showDropdown()\">\r\n <button\r\n class=\"btn\"\r\n type=\"button\"\r\n (click)=\"onDropdownClick()\"\r\n [class.gray-300]=\"showFlag()\"\r\n [class.lang-flag-toggle]=\"showFlag()\"\r\n [attr.aria-label]=\"showFlag() ? (title() | translate) : null\"\r\n >\r\n @if (showFlag()) {\r\n <span class=\"flag flag-{{ selectedLanguage }}\"></span>\r\n } @else {\r\n {{ title() | translate }}\r\n }\r\n </button>\r\n\r\n <ul>\r\n @for (lang of languages; track lang) {\r\n <li>\r\n <button\r\n type=\"button\"\r\n class=\"dropdown-item\"\r\n [class.selected]=\"lang === selectedLanguage\"\r\n (click)=\"onSelectLanguage(lang)\"\r\n >\r\n <span class=\"flag flag-{{ lang }}\"></span>\r\n <span class=\"dropdown-label\">{{ 'Language-' + lang | translate }}</span>\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n</div>\r\n", styles: ["@layer properties;.dropdown .btn{width:100%;--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500)}.dropdown .flag{display:inline-block;height:calc(var(--spacing, .25rem) * 4);width:calc(var(--spacing, .25rem) * 6);flex-shrink:0;border-radius:var(--radius-xs, .125rem);background-size:cover;background-position:center;--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color: color-mix(in srgb, #000 10%, transparent)}@supports (color: color-mix(in lab,red,red)){.dropdown .flag{--tw-ring-color: color-mix(in oklab, var(--color-black, #000) 10%, transparent)}}.dropdown .flag.flag-en{background-image:url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 76 40\"%3E%3Crect width=\"76\" height=\"40\" fill=\"%23b22234\"/%3E%3Cg fill=\"%23fff\"%3E%3Crect y=\"3\" width=\"76\" height=\"3\"/%3E%3Crect y=\"9\" width=\"76\" height=\"3\"/%3E%3Crect y=\"15\" width=\"76\" height=\"3\"/%3E%3Crect y=\"22\" width=\"76\" height=\"3\"/%3E%3Crect y=\"28\" width=\"76\" height=\"3\"/%3E%3Crect y=\"34\" width=\"76\" height=\"3\"/%3E%3C/g%3E%3Crect width=\"30\" height=\"22\" fill=\"%233c3b6e\"/%3E%3C/svg%3E')}.dropdown .flag.flag-pt{background-image:url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 70 49\"%3E%3Crect width=\"70\" height=\"49\" fill=\"%23009c3b\"/%3E%3Cpath d=\"M35 6L64 24.5L35 43L6 24.5Z\" fill=\"%23ffdf00\"/%3E%3Ccircle cx=\"35\" cy=\"24.5\" r=\"9.8\" fill=\"%23002776\"/%3E%3C/svg%3E')}.dropdown .lang-flag-toggle{display:flex;height:calc(var(--spacing, .25rem) * 9);width:auto;align-items:center;justify-content:center;border-style:var(--tw-border-style);border-width:0px;padding-inline:calc(var(--spacing, .25rem) * 2.5)}.dropdown .lang-flag-toggle:after{border-top-color:currentColor;margin-left:.325rem}.dropdown ul{z-index:50;margin-top:var(--spacing, .25rem);min-width:11rem;padding:var(--spacing, .25rem);border-radius:var(--radius-md, .375rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-stone-200, oklch(92.3% .003 48.717));background-color:var(--color-white, #fff);--tw-backdrop-blur: ;-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);--tw-shadow: 0 0 4px 0 var(--tw-shadow-color, rgba(0,0,0,.2)), 0 3px 20px 0 var(--tw-shadow-color, rgba(0,0,0,.19));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dropdown ul .dropdown-item{display:flex;width:100%;cursor:pointer;align-items:center;gap:calc(var(--spacing, .25rem) * 2);border-radius:var(--radius-sm, .25rem);padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2);-webkit-user-select:none;user-select:none;text-align:left;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-stone-700, oklch(37.4% .01 67.558))}@media(hover:hover){.dropdown ul .dropdown-item:hover{background-color:var(--color-stone-100, oklch(97% .001 106.424))}}@media(hover:hover){.dropdown ul .dropdown-item:hover{color:var(--color-stone-700, oklch(37.4% .01 67.558))}}.dropdown ul .dropdown-item:focus{background-color:var(--color-stone-100, oklch(97% .001 106.424))}.dropdown ul .dropdown-item:focus{--tw-outline-style: none;outline-style:none}@media(forced-colors:active){.dropdown ul .dropdown-item:focus{outline:2px solid transparent;outline-offset:2px}}.dropdown ul .dropdown-item:active{background-color:var(--color-stone-200, oklch(92.3% .003 48.717))}.dropdown ul .dropdown-item:active{color:var(--color-stone-700, oklch(37.4% .01 67.558))}.dropdown ul .dropdown-item.selected{background-color:var(--color-primary-50, #e6f4ff);--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:var(--color-primary-700, #002f50)}.dropdown ul .dropdown-item.selected:hover{background-color:var(--color-primary-100, #9acaed)}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-backdrop-blur{syntax: \"*\"; inherits: false;}@property --tw-backdrop-brightness{syntax: \"*\"; inherits: false;}@property --tw-backdrop-contrast{syntax: \"*\"; inherits: false;}@property --tw-backdrop-grayscale{syntax: \"*\"; inherits: false;}@property --tw-backdrop-hue-rotate{syntax: \"*\"; inherits: false;}@property --tw-backdrop-invert{syntax: \"*\"; inherits: false;}@property --tw-backdrop-opacity{syntax: \"*\"; inherits: false;}@property --tw-backdrop-saturate{syntax: \"*\"; inherits: false;}@property --tw-backdrop-sepia{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-border-style: solid;--tw-backdrop-blur: initial;--tw-backdrop-brightness: initial;--tw-backdrop-contrast: initial;--tw-backdrop-grayscale: initial;--tw-backdrop-hue-rotate: initial;--tw-backdrop-invert: initial;--tw-backdrop-opacity: initial;--tw-backdrop-saturate: initial;--tw-backdrop-sepia: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
638
+ }], propDecorators: { dropdown: [{ type: i0.ViewChild, args: ['dropdown', { isSignal: true }] }], showFlag: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFlag", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], documentClick: [{
639
+ type: HostListener,
640
+ args: ['window:click', ['$event']]
641
+ }] } });
642
+
643
+ class UserProfileComponent {
644
+ //#region Variables
645
+ authenticationService = inject(AuthenticationService);
646
+ // A signal rather than a field: the top bar renders the name and the avatar, and this is
647
+ // only filled in ngOnInit -- after the first pass has already been and gone.
648
+ user = signal(null, /* @ts-ignore */
649
+ ...(ngDevMode ? [{ debugName: "user" }] : /* istanbul ignore next */ []));
650
+ //#endregion
651
+ //#region Properties
652
+ /** Up to two uppercase initials derived from the user's name, used when no picture is set. */
653
+ get initials() {
654
+ const parts = this.name
655
+ .trim()
656
+ .split(/\s+/)
657
+ .filter((p) => p.length > 0);
658
+ if (parts.length === 0) {
659
+ return '';
660
+ }
661
+ const first = parts[0].charAt(0);
662
+ const last = parts.length > 1 ? parts[parts.length - 1].charAt(0) : '';
663
+ return (first + last).toUpperCase();
664
+ }
665
+ get name() {
666
+ return this.user()?.name ?? '';
667
+ }
668
+ get pictureUrl() {
669
+ return this.user()?.pictureUrl;
670
+ }
671
+ get position() {
672
+ return this.user()?.position ?? '';
673
+ }
674
+ //#endregion
675
+ //#region Constructor and Angular life cycle methods
676
+ ngOnInit() {
677
+ this.user.set(this.authenticationService.getUserInfo());
678
+ }
679
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UserProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
680
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UserProfileComponent, isStandalone: true, selector: "shared-user-profile", ngImport: i0, template: "<div class=\"user-profile\">\r\n <div class=\"user-text\">\r\n <span class=\"user-name\">{{ name }}</span>\r\n @if (position) {\r\n <span class=\"user-position\">{{ position }}</span>\r\n }\r\n </div>\r\n\r\n <div class=\"user-avatar\">\r\n @if (pictureUrl) {\r\n <img class=\"user-avatar-img\" [src]=\"pictureUrl\" [alt]=\"name\" />\r\n } @else {\r\n <span class=\"user-avatar-initials\">{{ initials }}</span>\r\n }\r\n </div>\r\n</div>\r\n", styles: ["@layer properties;@media(max-width:767px){:host{display:none}}.user-profile{display:flex;height:calc(var(--spacing, .25rem) * 9);min-width:0px;align-items:center;gap:calc(var(--spacing, .25rem) * 2);border-radius:var(--radius-sm, .25rem);padding-inline:calc(var(--spacing, .25rem) * 2);-webkit-user-select:none;user-select:none;background-color:color-mix(in srgb,#000 5%,transparent);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s))}@supports (color: color-mix(in lab,red,red)){.user-profile{background-color:color-mix(in oklab,var(--color-black, #000) 5%,transparent)}}@media(hover:hover){.user-profile:hover{background-color:color-mix(in srgb,#000 10%,transparent)}@supports (color: color-mix(in lab,red,red)){.user-profile:hover{background-color:color-mix(in oklab,var(--color-black, #000) 10%,transparent)}}}.user-profile .user-avatar{display:flex;height:calc(var(--spacing, .25rem) * 7);width:calc(var(--spacing, .25rem) * 7);flex-shrink:0;align-items:center;justify-content:center;overflow:hidden;border-radius:calc(infinity * 1px);background-color:var(--color-primary-500, #005794);color:var(--color-white, #fff)}.user-profile .user-avatar-img{height:100%;width:100%;object-fit:cover}.user-profile .user-avatar-initials{font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);text-transform:uppercase}.user-profile .user-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-leading: calc(var(--spacing, .25rem) * 4);line-height:calc(var(--spacing, .25rem) * 4);--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-stone-800, oklch(26.8% .007 34.298))}.user-profile .user-position{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));--tw-leading: calc(var(--spacing, .25rem) * 4);line-height:calc(var(--spacing, .25rem) * 4);color:var(--color-stone-500, oklch(55.3% .013 58.071))}.user-profile .user-text{display:flex;min-width:0px;flex-direction:column;text-align:right}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-leading{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-leading: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] });
681
+ }
682
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UserProfileComponent, decorators: [{
683
+ type: Component,
684
+ args: [{ selector: 'shared-user-profile', template: "<div class=\"user-profile\">\r\n <div class=\"user-text\">\r\n <span class=\"user-name\">{{ name }}</span>\r\n @if (position) {\r\n <span class=\"user-position\">{{ position }}</span>\r\n }\r\n </div>\r\n\r\n <div class=\"user-avatar\">\r\n @if (pictureUrl) {\r\n <img class=\"user-avatar-img\" [src]=\"pictureUrl\" [alt]=\"name\" />\r\n } @else {\r\n <span class=\"user-avatar-initials\">{{ initials }}</span>\r\n }\r\n </div>\r\n</div>\r\n", styles: ["@layer properties;@media(max-width:767px){:host{display:none}}.user-profile{display:flex;height:calc(var(--spacing, .25rem) * 9);min-width:0px;align-items:center;gap:calc(var(--spacing, .25rem) * 2);border-radius:var(--radius-sm, .25rem);padding-inline:calc(var(--spacing, .25rem) * 2);-webkit-user-select:none;user-select:none;background-color:color-mix(in srgb,#000 5%,transparent);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s))}@supports (color: color-mix(in lab,red,red)){.user-profile{background-color:color-mix(in oklab,var(--color-black, #000) 5%,transparent)}}@media(hover:hover){.user-profile:hover{background-color:color-mix(in srgb,#000 10%,transparent)}@supports (color: color-mix(in lab,red,red)){.user-profile:hover{background-color:color-mix(in oklab,var(--color-black, #000) 10%,transparent)}}}.user-profile .user-avatar{display:flex;height:calc(var(--spacing, .25rem) * 7);width:calc(var(--spacing, .25rem) * 7);flex-shrink:0;align-items:center;justify-content:center;overflow:hidden;border-radius:calc(infinity * 1px);background-color:var(--color-primary-500, #005794);color:var(--color-white, #fff)}.user-profile .user-avatar-img{height:100%;width:100%;object-fit:cover}.user-profile .user-avatar-initials{font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);text-transform:uppercase}.user-profile .user-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-leading: calc(var(--spacing, .25rem) * 4);line-height:calc(var(--spacing, .25rem) * 4);--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-stone-800, oklch(26.8% .007 34.298))}.user-profile .user-position{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));--tw-leading: calc(var(--spacing, .25rem) * 4);line-height:calc(var(--spacing, .25rem) * 4);color:var(--color-stone-500, oklch(55.3% .013 58.071))}.user-profile .user-text{display:flex;min-width:0px;flex-direction:column;text-align:right}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-leading{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-leading: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
685
+ }] });
686
+
687
+ class TopBarComponent {
688
+ //#region ViewChilds, Inputs, Outputs
689
+ /** Emitted when the logout button is clicked. */
690
+ logout = output();
691
+ //#endregion
692
+ //#region Event handlers
693
+ onLogoutClick() {
694
+ this.logout.emit();
695
+ }
696
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: TopBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
697
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: TopBarComponent, isStandalone: true, selector: "shared-top-bar", outputs: { logout: "logout" }, ngImport: i0, template: "<div class=\"top-bar\">\r\n <div class=\"top-bar-section top-bar-start\">\r\n <shared-brand></shared-brand>\r\n <shared-environment-badge></shared-environment-badge>\r\n </div>\r\n\r\n <div class=\"top-bar-section top-bar-end\">\r\n <shared-notifications></shared-notifications>\r\n <shared-language-selector [showFlag]=\"true\"></shared-language-selector>\r\n\r\n <!-- Theme switch slot (deferred \u2014 see 2026-07-23-top-bar-update-design.md) -->\r\n\r\n <shared-user-profile></shared-user-profile>\r\n\r\n <button\r\n class=\"btn gray-300 top-bar-logout\"\r\n type=\"button\"\r\n (click)=\"onLogoutClick()\"\r\n [attr.aria-label]=\"'Main-Logout' | translate\"\r\n >\r\n <i class=\"fa-solid fa-arrow-right-from-bracket\"></i>\r\n </button>\r\n </div>\r\n</div>\r\n", styles: [".top-bar{display:flex;width:100%;align-items:center;justify-content:space-between;gap:calc(var(--spacing, .25rem) * 3)}.top-bar .top-bar-end{flex-shrink:0}.top-bar .top-bar-section{display:flex;min-width:0px;align-items:center;gap:calc(var(--spacing, .25rem) * 3)}.top-bar .top-bar-start{min-width:0px}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: BrandComponent, selector: "shared-brand" }, { kind: "component", type: EnvironmentBadgeComponent, selector: "shared-environment-badge" }, { kind: "component", type: LanguageSelectorComponent, selector: "shared-language-selector", inputs: ["showFlag", "title"] }, { kind: "component", type: NotificationsComponent, selector: "shared-notifications" }, { kind: "component", type: UserProfileComponent, selector: "shared-user-profile" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
698
+ }
699
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: TopBarComponent, decorators: [{
700
+ type: Component,
701
+ args: [{ selector: 'shared-top-bar', imports: [
702
+ BrandComponent,
703
+ EnvironmentBadgeComponent,
704
+ LanguageSelectorComponent,
705
+ NotificationsComponent,
706
+ TranslatePipe,
707
+ UserProfileComponent,
708
+ ], template: "<div class=\"top-bar\">\r\n <div class=\"top-bar-section top-bar-start\">\r\n <shared-brand></shared-brand>\r\n <shared-environment-badge></shared-environment-badge>\r\n </div>\r\n\r\n <div class=\"top-bar-section top-bar-end\">\r\n <shared-notifications></shared-notifications>\r\n <shared-language-selector [showFlag]=\"true\"></shared-language-selector>\r\n\r\n <!-- Theme switch slot (deferred \u2014 see 2026-07-23-top-bar-update-design.md) -->\r\n\r\n <shared-user-profile></shared-user-profile>\r\n\r\n <button\r\n class=\"btn gray-300 top-bar-logout\"\r\n type=\"button\"\r\n (click)=\"onLogoutClick()\"\r\n [attr.aria-label]=\"'Main-Logout' | translate\"\r\n >\r\n <i class=\"fa-solid fa-arrow-right-from-bracket\"></i>\r\n </button>\r\n </div>\r\n</div>\r\n", styles: [".top-bar{display:flex;width:100%;align-items:center;justify-content:space-between;gap:calc(var(--spacing, .25rem) * 3)}.top-bar .top-bar-end{flex-shrink:0}.top-bar .top-bar-section{display:flex;min-width:0px;align-items:center;gap:calc(var(--spacing, .25rem) * 3)}.top-bar .top-bar-start{min-width:0px}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
709
+ }], propDecorators: { logout: [{ type: i0.Output, args: ["logout"] }] } });
710
+
711
+ class MainLayoutComponent {
712
+ //#region ViewChilds, Inputs, Outputs
713
+ logoutModal = viewChild('logoutModal', /* @ts-ignore */
714
+ ...(ngDevMode ? [{ debugName: "logoutModal" }] : /* istanbul ignore next */ []));
715
+ //#endregion
716
+ //#region Variables
717
+ authenticationService = inject(AuthenticationService);
718
+ destroy$ = new Subject();
719
+ externalContentService = inject(ExternalContentService);
720
+ externalUrlResolverService = inject(ExternalUrlResolverService);
721
+ router = inject(Router);
722
+ sidebarService = inject(SidebarService);
723
+ tabService = inject(TabService);
724
+ /** Application version (from AppConfig) projected into the sidebar footer. */
725
+ appVersion = inject(APP_CONFIG).version;
726
+ //#endregion
727
+ //#region Properties
728
+ //#endregion
729
+ //#region Constructor and Angular life cycle methods
730
+ ngOnDestroy() {
731
+ this.destroy$.next(true);
732
+ this.destroy$.complete();
733
+ }
734
+ ngOnInit() {
735
+ const url = this.router.url;
736
+ if (url !== '/' && !this.tabService.isUrlOpen(url)) {
737
+ this.sidebarService
738
+ .getMenuFromUrl(url)
739
+ .pipe(take(1))
740
+ .subscribe({
741
+ next: (item) => {
742
+ if (item) {
743
+ this.tabService.updateTabTitle(url, item.label);
744
+ }
745
+ },
746
+ // A deep-linked title is best-effort. Menu endpoints answer 404 for a URL they do not
747
+ // know -- the embedded-content route among them -- and that must not surface as an
748
+ // unhandled rejection.
749
+ error: () => undefined,
750
+ });
751
+ }
752
+ this.sidebarService.menuExternalUrlSelected
753
+ .pipe(takeUntil(this.destroy$))
754
+ .subscribe((item) => this.openExternalMenu(item));
755
+ this.sidebarService.menuUrlSelected.pipe(takeUntil(this.destroy$)).subscribe((item) => {
756
+ const tab = new Tab({
757
+ title: item.label,
758
+ url: item.url,
759
+ });
760
+ this.tabService.openTab(tab);
761
+ });
762
+ }
763
+ //#endregion
764
+ //#region Event handlers
765
+ //#endregion
766
+ //#region Public methods
767
+ onLogoutClick() {
768
+ this.logoutModal()?.toggleModal();
769
+ }
770
+ onLogoutConfirm() {
771
+ this.authenticationService.signOut();
772
+ this.router.navigate(['/login']);
773
+ }
774
+ //#endregion
775
+ //#region Private methods
776
+ openExternalMenu(item) {
777
+ const rawUrl = item.url ?? '';
778
+ if (rawUrl.length === 0) {
779
+ return;
780
+ }
781
+ if (toSidebarMenuOpenMode(item.openMode) === SidebarMenuOpenMode.ExternalEmbedded) {
782
+ // The tab has to be a real Angular route, so the destination travels by menu id and is
783
+ // resolved by ExternalContentComponent. A `?url=` query string would collapse every
784
+ // embedded tab into one -- TabService and CustomReuseStrategy key tabs on path segments
785
+ // only -- and would also let anyone frame an arbitrary site inside our own chrome.
786
+ this.externalContentService.register(item);
787
+ const tab = new Tab({
788
+ title: item.label,
789
+ url: `/${EXTERNAL_CONTENT_ROUTE_PATH}/${item.id}`,
790
+ });
791
+ this.tabService.openTab(tab);
792
+ return;
793
+ }
794
+ const url = this.externalUrlResolverService.resolve(rawUrl);
795
+ if (!this.externalUrlResolverService.isAllowed(url)) {
796
+ console.error(`Sidebar menu "${item.label}" points to an unsupported external address and was not opened.`, url);
797
+ return;
798
+ }
799
+ window.open(url, '_blank', 'noopener,noreferrer');
800
+ }
801
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: MainLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
802
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: MainLayoutComponent, isStandalone: true, selector: "shared-main-layout", viewQueries: [{ propertyName: "logoutModal", first: true, predicate: ["logoutModal"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"main-container\">\r\n <div class=\"sidebar-region\">\r\n <lib-sidebar>\r\n @if (appVersion) {\r\n <span>v{{ appVersion }}</span>\r\n }\r\n </lib-sidebar>\r\n </div>\r\n\r\n <shared-top-bar class=\"toolbar\" (logout)=\"onLogoutClick()\"></shared-top-bar>\r\n\r\n <div class=\"content\">\r\n <framework-tabs>\r\n <router-outlet></router-outlet>\r\n </framework-tabs>\r\n </div>\r\n</div>\r\n\r\n<lib-modal #logoutModal position=\"top\" size=\"xl\" [closeButtonText]=\"'Main-Logout-Modal-Message-Cancel' | translate\">\r\n <div body>\r\n <h5>{{ 'Main-Logout-Modal-Message' | translate }}</h5>\r\n </div>\r\n <div footer>\r\n <button type=\"button\" class=\"btn red-600\" (click)=\"onLogoutConfirm()\">\r\n {{ 'Main-Logout-Modal-Message-Confirm' | translate }}\r\n </button>\r\n </div>\r\n</lib-modal>\r\n", styles: ["@charset \"UTF-8\";@layer properties;.main-container{display:grid;height:100vh;max-height:100vh;width:100vw;max-width:100%;background:var(--app-backdrop);grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto minmax(0,1fr);grid-template-areas:\"toolbar toolbar\" \"sidebar content\"}.main-container .sidebar-region{padding:calc(var(--spacing, .25rem) * 3);grid-area:sidebar}.main-container .toolbar{display:flex;justify-content:space-between;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-stone-300, oklch(86.9% .005 56.366));background-color:color-mix(in srgb,#fff 90%,transparent);padding:calc(var(--spacing, .25rem) * 2);--tw-shadow: 0 0 4px 0 var(--tw-shadow-color, rgba(0,0,0,.2)), 0 3px 20px 0 var(--tw-shadow-color, rgba(0,0,0,.19));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);grid-area:toolbar}@supports (color: color-mix(in lab,red,red)){.main-container .toolbar{background-color:color-mix(in oklab,var(--color-white, #fff) 90%,transparent)}}.main-container .content{padding-inline:calc(var(--spacing, .25rem) * 3);padding-top:calc(var(--spacing, .25rem) * 3);padding-bottom:calc(var(--spacing, .25rem) * 2);grid-area:content}.main-container framework-tabs{height:100%}lib-modal h5{margin-bottom:calc(var(--spacing, .25rem) * 2);font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-leading: calc(var(--spacing, .25rem) * 5);line-height:calc(var(--spacing, .25rem) * 5);--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-leading{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-leading: initial;--tw-font-weight: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: ModalComponent, selector: "lib-modal", inputs: ["closeButtonText", "dialog", "modalProcessing", "position", "size", "title"], outputs: ["closeButtonTextChange", "modalProcessingChange", "closed"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: SidebarComponent, selector: "lib-sidebar" }, { kind: "component", type: TabsComponent, selector: "framework-tabs" }, { kind: "component", type: TopBarComponent, selector: "shared-top-bar", outputs: ["logout"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
803
+ }
804
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: MainLayoutComponent, decorators: [{
805
+ type: Component,
806
+ args: [{ selector: 'shared-main-layout', imports: [ModalComponent, RouterModule, SidebarComponent, TabsComponent, TopBarComponent, TranslatePipe], template: "<div class=\"main-container\">\r\n <div class=\"sidebar-region\">\r\n <lib-sidebar>\r\n @if (appVersion) {\r\n <span>v{{ appVersion }}</span>\r\n }\r\n </lib-sidebar>\r\n </div>\r\n\r\n <shared-top-bar class=\"toolbar\" (logout)=\"onLogoutClick()\"></shared-top-bar>\r\n\r\n <div class=\"content\">\r\n <framework-tabs>\r\n <router-outlet></router-outlet>\r\n </framework-tabs>\r\n </div>\r\n</div>\r\n\r\n<lib-modal #logoutModal position=\"top\" size=\"xl\" [closeButtonText]=\"'Main-Logout-Modal-Message-Cancel' | translate\">\r\n <div body>\r\n <h5>{{ 'Main-Logout-Modal-Message' | translate }}</h5>\r\n </div>\r\n <div footer>\r\n <button type=\"button\" class=\"btn red-600\" (click)=\"onLogoutConfirm()\">\r\n {{ 'Main-Logout-Modal-Message-Confirm' | translate }}\r\n </button>\r\n </div>\r\n</lib-modal>\r\n", styles: ["@charset \"UTF-8\";@layer properties;.main-container{display:grid;height:100vh;max-height:100vh;width:100vw;max-width:100%;background:var(--app-backdrop);grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto minmax(0,1fr);grid-template-areas:\"toolbar toolbar\" \"sidebar content\"}.main-container .sidebar-region{padding:calc(var(--spacing, .25rem) * 3);grid-area:sidebar}.main-container .toolbar{display:flex;justify-content:space-between;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-stone-300, oklch(86.9% .005 56.366));background-color:color-mix(in srgb,#fff 90%,transparent);padding:calc(var(--spacing, .25rem) * 2);--tw-shadow: 0 0 4px 0 var(--tw-shadow-color, rgba(0,0,0,.2)), 0 3px 20px 0 var(--tw-shadow-color, rgba(0,0,0,.19));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);grid-area:toolbar}@supports (color: color-mix(in lab,red,red)){.main-container .toolbar{background-color:color-mix(in oklab,var(--color-white, #fff) 90%,transparent)}}.main-container .content{padding-inline:calc(var(--spacing, .25rem) * 3);padding-top:calc(var(--spacing, .25rem) * 3);padding-bottom:calc(var(--spacing, .25rem) * 2);grid-area:content}.main-container framework-tabs{height:100%}lib-modal h5{margin-bottom:calc(var(--spacing, .25rem) * 2);font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-leading: calc(var(--spacing, .25rem) * 5);line-height:calc(var(--spacing, .25rem) * 5);--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-leading{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-leading: initial;--tw-font-weight: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
807
+ }], propDecorators: { logoutModal: [{ type: i0.ViewChild, args: ['logoutModal', { isSignal: true }] }] } });
808
+
809
+ class LoginComponent {
810
+ //#region ViewChilds, Inputs, Outputs
811
+ username = viewChild('username', /* @ts-ignore */
812
+ ...(ngDevMode ? [{ debugName: "username" }] : /* istanbul ignore next */ []));
813
+ //#endregion
814
+ //#region Variables
815
+ form;
816
+ // A signal holding one object, replaced rather than edited: every one of these is rendered --
817
+ // the validation banner, the button's spinner, its success tick -- and each is written from
818
+ // inside the authentication stream, where nothing marks this view.
819
+ formState = signal({
820
+ error: null,
821
+ loading: false,
822
+ success: false,
823
+ }, /* @ts-ignore */
824
+ ...(ngDevMode ? [{ debugName: "formState" }] : /* istanbul ignore next */ []));
825
+ authenticationService = inject(AuthenticationService);
826
+ formBuilder = inject(FormBuilder);
827
+ router = inject(Router);
828
+ //#endregion
829
+ //#region Properties
830
+ //#endregion
831
+ //#region Constructor and Angular life cycle methods
832
+ ngOnInit() {
833
+ this.form = this.formBuilder.group({
834
+ password: ['', Validators.required],
835
+ rememberMe: [false, { nonNullable: true }],
836
+ username: ['', Validators.required],
837
+ });
838
+ }
839
+ //#endregion
840
+ //#region Event handlers
841
+ onSubmit() {
842
+ this.form.markAllAsTouched();
843
+ if (this.form.valid) {
844
+ this.form.disable();
845
+ this.formState.update((state) => ({ ...state, error: null, loading: true }));
846
+ this.authenticationService
847
+ .authenticate({
848
+ username: this.form.get('username')?.value.toString() ?? '',
849
+ password: this.form.get('password')?.value.toString() ?? '',
850
+ rememberMe: this.form.get('rememberMe')?.value ?? false,
851
+ })
852
+ .pipe(tap(() => this.formState.update((state) => ({ ...state, success: true }))), finalize(() => this.formState.update((state) => ({ ...state, loading: false }))))
853
+ .subscribe({
854
+ next: () => {
855
+ const params = new URLSearchParams(window.location.search);
856
+ this.router.navigate([params.get('returnUrl') ?? '/']);
857
+ },
858
+ error: (e) => {
859
+ this.form.enable();
860
+ this.form.controls['password'].setValue('');
861
+ this.form.controls['password'].markAsUntouched();
862
+ this.username()?.nativeElement.focus();
863
+ const error = e === 'InvalidUsernamePassword' ? 'invalid' : 'internalServerError';
864
+ this.formState.update((state) => ({ ...state, error }));
865
+ },
866
+ });
867
+ }
868
+ }
869
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: LoginComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
870
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: LoginComponent, isStandalone: true, selector: "shared-login", viewQueries: [{ propertyName: "username", first: true, predicate: ["username"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"content\">\r\n <div class=\"login\">\r\n <div class=\"logo-container\">\r\n <div class=\"logo\">\r\n <img src=\"/logo-login.png\" alt=\"Logo\" />\r\n </div>\r\n </div>\r\n\r\n <h3 class=\"title\">{{ 'Login-Title' | translate }}</h3>\r\n\r\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\r\n <div class=\"validations\" [class.hidden]=\"!formState().error\">\r\n @if (formState().error === 'invalid') {\r\n <div>\r\n {{ 'Login-Errors-Invalid' | translate }}\r\n </div>\r\n }\r\n @if (formState().error === 'internalServerError') {\r\n <div>\r\n {{ 'Login-Errors-InternalServerError' | translate }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <div class=\"input-group inline\">\r\n <input\r\n type=\"text\"\r\n #username\r\n id=\"username\"\r\n formControlName=\"username\"\r\n class=\"form-input peer\"\r\n [placeholder]=\"'Login-Username' | translate\"\r\n />\r\n\r\n <div class=\"input-group-text\">\r\n <label for=\"username\">\r\n <i class=\"fas fa-user fa-fw\"></i>\r\n </label>\r\n </div>\r\n\r\n <div class=\"validations\">\r\n @if (form.get('username')?.hasError('required')) {\r\n <span>{{ 'Login-Username-Required' | translate }}</span>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"input-group inline\">\r\n <input\r\n type=\"password\"\r\n id=\"password\"\r\n formControlName=\"password\"\r\n class=\"form-input\"\r\n [placeholder]=\"'Login-Password' | translate\"\r\n />\r\n\r\n <div class=\"input-group-text\">\r\n <label for=\"password\">\r\n <i class=\"fas fa-key fa-fw\"></i>\r\n </label>\r\n </div>\r\n\r\n <div class=\"validations\">\r\n @if (form.get('password')?.hasError('required')) {\r\n <span>{{ 'Login-Password-Required' | translate }}</span>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"options\">\r\n <div>\r\n <input class=\"form-input\" id=\"rememberMe\" formControlName=\"rememberMe\" type=\"checkbox\" />\r\n <label for=\"rememberMe\">{{ 'Login-RememberMe' | translate }}</label>\r\n </div>\r\n\r\n <shared-language-selector></shared-language-selector>\r\n </div>\r\n\r\n <div class=\"buttons\">\r\n <button\r\n type=\"submit\"\r\n class=\"btn\"\r\n [class.loading]=\"formState().loading && !formState().success && !formState().error\"\r\n [class.error]=\"formState().error\"\r\n [class.success]=\"formState().success\"\r\n >\r\n @if (!formState().loading) {\r\n <i class=\"fas fa-sign-in-alt fa-fw\"></i>\r\n }\r\n {{ 'Login-SignIn' | translate }}\r\n </button>\r\n </div>\r\n </form>\r\n </div>\r\n\r\n <div class=\"footer\">\r\n <span [innerHTML]=\"'Login-Copyright' | translate\"></span>\r\n <span class=\"design\">{{ 'Login-DesignBy' | translate }}</span>\r\n </div>\r\n</div>\r\n", styles: ["@layer properties;:host{display:block}.content{margin-inline:auto;width:calc(10 / 12 * 100%);border-radius:var(--radius-lg, .5rem);--tw-shadow: 0 4px 8px 0 var(--tw-shadow-color, rgba(0,0,0,.2)), 0 6px 20px 0 var(--tw-shadow-color, rgba(0,0,0,.19));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(width>=48rem){.content{width:calc(8 / 12 * 100%)}}@media(width>=80rem){.content{width:calc(5 / 12 * 100%)}}:where(.content>:not(:last-child)){--tw-divide-y-reverse: 0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.content>:not(:last-child)){border-color:var(--color-zinc-400, oklch(70.5% .015 286.067))}.content .login{position:relative;z-index:10;margin-top:calc(var(--spacing, .25rem) * 20);border-top-left-radius:var(--radius-lg, .5rem);border-top-right-radius:var(--radius-lg, .5rem);background-color:color-mix(in srgb,#fff 60%,transparent);padding-inline:calc(var(--spacing, .25rem) * 6);padding-top:calc(var(--spacing, .25rem) * 20);padding-bottom:calc(var(--spacing, .25rem) * 6);--tw-backdrop-blur: blur(var(--blur-md, 12px));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}@supports (color: color-mix(in lab,red,red)){.content .login{background-color:color-mix(in oklab,var(--color-white, #fff) 60%,transparent)}}.content .login .logo-container{margin-top:calc(var(--spacing, .25rem) * -40);margin-right:auto;margin-bottom:calc(var(--spacing, .25rem) * 4);margin-left:auto;width:fit-content;border-radius:var(--radius-3xl, 1.5rem);padding:calc(var(--spacing, .25rem) * 2);background-color:var(--color-white, #fff);--tw-shadow: 0 4px 8px 0 var(--tw-shadow-color, rgba(0,0,0,.2)), 0 6px 20px 0 var(--tw-shadow-color, rgba(0,0,0,.19));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.content .login .logo-container .logo{width:fit-content;border-radius:var(--radius-3xl, 1.5rem);padding:calc(var(--spacing, .25rem) * 2);border-style:var(--tw-border-style);border-width:2px;border-color:var(--color-amber-300, oklch(87.9% .169 91.605))}.content .login .logo-container .logo img{height:calc(var(--spacing, .25rem) * 40);min-width:fit-content}.content .login .title{padding-bottom:calc(var(--spacing, .25rem) * 5);text-align:center;font-size:var(--text-3xl, 1.875rem);line-height:var(--tw-leading, var(--text-3xl--line-height, 1.2 ));--tw-font-weight: var(--font-weight-bold, 700);font-weight:var(--font-weight-bold, 700)}.content .login form>.validations{text-align:center;--tw-font-weight: var(--font-weight-bold, 700);font-weight:var(--font-weight-bold, 700);color:var(--color-red-500, oklch(63.7% .237 25.331))}.content .login form>.validations:after{margin-inline:auto;margin-bottom:calc(var(--spacing, .25rem) * 2);display:block;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-red-500, oklch(63.7% .237 25.331));--tw-content: \"\";content:var(--tw-content);animation:separator 1s ease-in-out forwards}.content .login form .options{margin-bottom:calc(var(--spacing, .25rem) * 3);display:flex;flex-direction:row;flex-wrap:wrap-reverse;justify-content:space-between}.content .login form .options shared-language-selector{margin-bottom:calc(var(--spacing, .25rem) * 3);width:100%}@media(width>=40rem){.content .login form .options shared-language-selector{margin-bottom:0}}@media(width>=40rem){.content .login form .options shared-language-selector{width:auto}}.content .login form .buttons{display:flex;justify-content:center;gap:calc(var(--spacing, .25rem) * 4)}.content .footer{border-bottom-right-radius:var(--radius-lg, .5rem);border-bottom-left-radius:var(--radius-lg, .5rem);padding-inline:calc(var(--spacing, .25rem) * 2);padding-block:calc(var(--spacing, .25rem) * .5);background-color:color-mix(in srgb,oklch(92.9% .013 255.508) 60%,transparent);--tw-backdrop-blur: blur(var(--blur-md, 12px));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);--tw-tracking: var(--tracking-tight, -.025em);letter-spacing:var(--tracking-tight, -.025em);display:flex;flex-wrap:wrap;justify-content:space-between}@supports (color: color-mix(in lab,red,red)){.content .footer{background-color:color-mix(in oklab,var(--color-slate-200, oklch(92.9% .013 255.508)) 60%,transparent)}}@keyframes separator{0%{width:0}to{width:100%}}@property --tw-divide-y-reverse{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-backdrop-blur{syntax: \"*\"; inherits: false;}@property --tw-backdrop-brightness{syntax: \"*\"; inherits: false;}@property --tw-backdrop-contrast{syntax: \"*\"; inherits: false;}@property --tw-backdrop-grayscale{syntax: \"*\"; inherits: false;}@property --tw-backdrop-hue-rotate{syntax: \"*\"; inherits: false;}@property --tw-backdrop-invert{syntax: \"*\"; inherits: false;}@property --tw-backdrop-opacity{syntax: \"*\"; inherits: false;}@property --tw-backdrop-saturate{syntax: \"*\"; inherits: false;}@property --tw-backdrop-sepia{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-content{syntax: \"*\"; inherits: false; initial-value: \"\";}@property --tw-tracking{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-divide-y-reverse: 0;--tw-border-style: solid;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-backdrop-blur: initial;--tw-backdrop-brightness: initial;--tw-backdrop-contrast: initial;--tw-backdrop-grayscale: initial;--tw-backdrop-hue-rotate: initial;--tw-backdrop-invert: initial;--tw-backdrop-opacity: initial;--tw-backdrop-saturate: initial;--tw-backdrop-sepia: initial;--tw-font-weight: initial;--tw-content: \"\";--tw-tracking: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: LanguageSelectorComponent, selector: "shared-language-selector", inputs: ["showFlag", "title"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: RouterModule }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
871
+ }
872
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: LoginComponent, decorators: [{
873
+ type: Component,
874
+ args: [{ selector: 'shared-login', imports: [LanguageSelectorComponent, ReactiveFormsModule, RouterModule, TranslatePipe], template: "<div class=\"content\">\r\n <div class=\"login\">\r\n <div class=\"logo-container\">\r\n <div class=\"logo\">\r\n <img src=\"/logo-login.png\" alt=\"Logo\" />\r\n </div>\r\n </div>\r\n\r\n <h3 class=\"title\">{{ 'Login-Title' | translate }}</h3>\r\n\r\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\r\n <div class=\"validations\" [class.hidden]=\"!formState().error\">\r\n @if (formState().error === 'invalid') {\r\n <div>\r\n {{ 'Login-Errors-Invalid' | translate }}\r\n </div>\r\n }\r\n @if (formState().error === 'internalServerError') {\r\n <div>\r\n {{ 'Login-Errors-InternalServerError' | translate }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <div class=\"input-group inline\">\r\n <input\r\n type=\"text\"\r\n #username\r\n id=\"username\"\r\n formControlName=\"username\"\r\n class=\"form-input peer\"\r\n [placeholder]=\"'Login-Username' | translate\"\r\n />\r\n\r\n <div class=\"input-group-text\">\r\n <label for=\"username\">\r\n <i class=\"fas fa-user fa-fw\"></i>\r\n </label>\r\n </div>\r\n\r\n <div class=\"validations\">\r\n @if (form.get('username')?.hasError('required')) {\r\n <span>{{ 'Login-Username-Required' | translate }}</span>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"input-group inline\">\r\n <input\r\n type=\"password\"\r\n id=\"password\"\r\n formControlName=\"password\"\r\n class=\"form-input\"\r\n [placeholder]=\"'Login-Password' | translate\"\r\n />\r\n\r\n <div class=\"input-group-text\">\r\n <label for=\"password\">\r\n <i class=\"fas fa-key fa-fw\"></i>\r\n </label>\r\n </div>\r\n\r\n <div class=\"validations\">\r\n @if (form.get('password')?.hasError('required')) {\r\n <span>{{ 'Login-Password-Required' | translate }}</span>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"options\">\r\n <div>\r\n <input class=\"form-input\" id=\"rememberMe\" formControlName=\"rememberMe\" type=\"checkbox\" />\r\n <label for=\"rememberMe\">{{ 'Login-RememberMe' | translate }}</label>\r\n </div>\r\n\r\n <shared-language-selector></shared-language-selector>\r\n </div>\r\n\r\n <div class=\"buttons\">\r\n <button\r\n type=\"submit\"\r\n class=\"btn\"\r\n [class.loading]=\"formState().loading && !formState().success && !formState().error\"\r\n [class.error]=\"formState().error\"\r\n [class.success]=\"formState().success\"\r\n >\r\n @if (!formState().loading) {\r\n <i class=\"fas fa-sign-in-alt fa-fw\"></i>\r\n }\r\n {{ 'Login-SignIn' | translate }}\r\n </button>\r\n </div>\r\n </form>\r\n </div>\r\n\r\n <div class=\"footer\">\r\n <span [innerHTML]=\"'Login-Copyright' | translate\"></span>\r\n <span class=\"design\">{{ 'Login-DesignBy' | translate }}</span>\r\n </div>\r\n</div>\r\n", styles: ["@layer properties;:host{display:block}.content{margin-inline:auto;width:calc(10 / 12 * 100%);border-radius:var(--radius-lg, .5rem);--tw-shadow: 0 4px 8px 0 var(--tw-shadow-color, rgba(0,0,0,.2)), 0 6px 20px 0 var(--tw-shadow-color, rgba(0,0,0,.19));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(width>=48rem){.content{width:calc(8 / 12 * 100%)}}@media(width>=80rem){.content{width:calc(5 / 12 * 100%)}}:where(.content>:not(:last-child)){--tw-divide-y-reverse: 0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.content>:not(:last-child)){border-color:var(--color-zinc-400, oklch(70.5% .015 286.067))}.content .login{position:relative;z-index:10;margin-top:calc(var(--spacing, .25rem) * 20);border-top-left-radius:var(--radius-lg, .5rem);border-top-right-radius:var(--radius-lg, .5rem);background-color:color-mix(in srgb,#fff 60%,transparent);padding-inline:calc(var(--spacing, .25rem) * 6);padding-top:calc(var(--spacing, .25rem) * 20);padding-bottom:calc(var(--spacing, .25rem) * 6);--tw-backdrop-blur: blur(var(--blur-md, 12px));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}@supports (color: color-mix(in lab,red,red)){.content .login{background-color:color-mix(in oklab,var(--color-white, #fff) 60%,transparent)}}.content .login .logo-container{margin-top:calc(var(--spacing, .25rem) * -40);margin-right:auto;margin-bottom:calc(var(--spacing, .25rem) * 4);margin-left:auto;width:fit-content;border-radius:var(--radius-3xl, 1.5rem);padding:calc(var(--spacing, .25rem) * 2);background-color:var(--color-white, #fff);--tw-shadow: 0 4px 8px 0 var(--tw-shadow-color, rgba(0,0,0,.2)), 0 6px 20px 0 var(--tw-shadow-color, rgba(0,0,0,.19));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.content .login .logo-container .logo{width:fit-content;border-radius:var(--radius-3xl, 1.5rem);padding:calc(var(--spacing, .25rem) * 2);border-style:var(--tw-border-style);border-width:2px;border-color:var(--color-amber-300, oklch(87.9% .169 91.605))}.content .login .logo-container .logo img{height:calc(var(--spacing, .25rem) * 40);min-width:fit-content}.content .login .title{padding-bottom:calc(var(--spacing, .25rem) * 5);text-align:center;font-size:var(--text-3xl, 1.875rem);line-height:var(--tw-leading, var(--text-3xl--line-height, 1.2 ));--tw-font-weight: var(--font-weight-bold, 700);font-weight:var(--font-weight-bold, 700)}.content .login form>.validations{text-align:center;--tw-font-weight: var(--font-weight-bold, 700);font-weight:var(--font-weight-bold, 700);color:var(--color-red-500, oklch(63.7% .237 25.331))}.content .login form>.validations:after{margin-inline:auto;margin-bottom:calc(var(--spacing, .25rem) * 2);display:block;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-red-500, oklch(63.7% .237 25.331));--tw-content: \"\";content:var(--tw-content);animation:separator 1s ease-in-out forwards}.content .login form .options{margin-bottom:calc(var(--spacing, .25rem) * 3);display:flex;flex-direction:row;flex-wrap:wrap-reverse;justify-content:space-between}.content .login form .options shared-language-selector{margin-bottom:calc(var(--spacing, .25rem) * 3);width:100%}@media(width>=40rem){.content .login form .options shared-language-selector{margin-bottom:0}}@media(width>=40rem){.content .login form .options shared-language-selector{width:auto}}.content .login form .buttons{display:flex;justify-content:center;gap:calc(var(--spacing, .25rem) * 4)}.content .footer{border-bottom-right-radius:var(--radius-lg, .5rem);border-bottom-left-radius:var(--radius-lg, .5rem);padding-inline:calc(var(--spacing, .25rem) * 2);padding-block:calc(var(--spacing, .25rem) * .5);background-color:color-mix(in srgb,oklch(92.9% .013 255.508) 60%,transparent);--tw-backdrop-blur: blur(var(--blur-md, 12px));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);--tw-tracking: var(--tracking-tight, -.025em);letter-spacing:var(--tracking-tight, -.025em);display:flex;flex-wrap:wrap;justify-content:space-between}@supports (color: color-mix(in lab,red,red)){.content .footer{background-color:color-mix(in oklab,var(--color-slate-200, oklch(92.9% .013 255.508)) 60%,transparent)}}@keyframes separator{0%{width:0}to{width:100%}}@property --tw-divide-y-reverse{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-backdrop-blur{syntax: \"*\"; inherits: false;}@property --tw-backdrop-brightness{syntax: \"*\"; inherits: false;}@property --tw-backdrop-contrast{syntax: \"*\"; inherits: false;}@property --tw-backdrop-grayscale{syntax: \"*\"; inherits: false;}@property --tw-backdrop-hue-rotate{syntax: \"*\"; inherits: false;}@property --tw-backdrop-invert{syntax: \"*\"; inherits: false;}@property --tw-backdrop-opacity{syntax: \"*\"; inherits: false;}@property --tw-backdrop-saturate{syntax: \"*\"; inherits: false;}@property --tw-backdrop-sepia{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-content{syntax: \"*\"; inherits: false; initial-value: \"\";}@property --tw-tracking{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-divide-y-reverse: 0;--tw-border-style: solid;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-backdrop-blur: initial;--tw-backdrop-brightness: initial;--tw-backdrop-contrast: initial;--tw-backdrop-grayscale: initial;--tw-backdrop-hue-rotate: initial;--tw-backdrop-invert: initial;--tw-backdrop-opacity: initial;--tw-backdrop-saturate: initial;--tw-backdrop-sepia: initial;--tw-font-weight: initial;--tw-content: \"\";--tw-tracking: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
875
+ }], propDecorators: { username: [{ type: i0.ViewChild, args: ['username', { isSignal: true }] }] } });
876
+
877
+ const routes = [
878
+ {
879
+ path: '',
880
+ component: LoginLayoutComponent,
881
+ children: [
882
+ { path: '', redirectTo: 'login', pathMatch: 'full' },
883
+ { path: 'login', component: LoginComponent },
884
+ ],
885
+ },
886
+ ];
887
+ class SharedAuthModule {
888
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SharedAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
889
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: SharedAuthModule, imports: [i1.RouterModule] });
890
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SharedAuthModule, imports: [RouterModule.forChild(routes)] });
891
+ }
892
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SharedAuthModule, decorators: [{
893
+ type: NgModule,
894
+ args: [{
895
+ imports: [RouterModule.forChild(routes)],
896
+ }]
897
+ }] });
898
+
899
+ class AuthGuard {
900
+ //#region ViewChilds, Inputs, Outputs
901
+ //#endregion
902
+ //#region Variables
903
+ authenticationService = inject(AuthenticationService);
904
+ router = inject(Router);
905
+ //#endregion
906
+ //#region Properties
907
+ //#endregion
908
+ //#region Constructor and Angular life cycle methods
909
+ //#endregion
910
+ //#region Event handlers
911
+ //#endregion
912
+ //#region Public methods
913
+ canActivate(_, state) {
914
+ if (!this.authenticationService.isAuthenticated) {
915
+ this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
916
+ return false;
917
+ }
918
+ return true;
919
+ }
920
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: AuthGuard, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
921
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: AuthGuard, providedIn: 'root' });
922
+ }
923
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: AuthGuard, decorators: [{
924
+ type: Injectable,
925
+ args: [{
926
+ providedIn: 'root',
927
+ }]
928
+ }] });
929
+
930
+ class AuthInterceptor {
931
+ //#region ViewChilds, Inputs, Outputs
932
+ //#endregion
933
+ //#region Variables
934
+ authenticationService = inject(AuthenticationService);
935
+ config = inject(APP_CONFIG);
936
+ jwtInterceptor = inject(JwtInterceptor);
937
+ refreshToken$;
938
+ router = inject(Router);
939
+ //#endregion
940
+ //#region Properties
941
+ //#endregion
942
+ //#region Constructor and Angular life cycle methods
943
+ //#endregion
944
+ //#region Event handlers
945
+ //#endregion
946
+ //#region Public methods
947
+ intercept(request, next) {
948
+ if (request.url.indexOf(this.config.BASE_URL) >= 0 &&
949
+ !this.jwtInterceptor.isDisallowedRoute(request) &&
950
+ this.authenticationService.isTokenExpired) {
951
+ if (!this.refreshToken$) {
952
+ this.refreshToken$ = this.authenticationService.tryRefreshToken().pipe(shareReplay(), tap(() => {
953
+ this.refreshToken$ = undefined;
954
+ }));
955
+ }
956
+ return this.refreshToken$.pipe(switchMap((token) => {
957
+ return next.handle(request.clone({ setHeaders: { authorization: `Bearer ${token}` } }));
958
+ }), catchError((err) => {
959
+ if (err.status === 401) {
960
+ this.authenticationService.signOut();
961
+ this.router.navigate(['/login']);
962
+ }
963
+ throw err;
964
+ }));
965
+ }
966
+ return next.handle(request);
967
+ }
968
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: AuthInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
969
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: AuthInterceptor });
970
+ }
971
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: AuthInterceptor, decorators: [{
972
+ type: Injectable
973
+ }] });
974
+
975
+ // No strategy on purpose: Angular never inherits `changeDetection`, so one declared here would
976
+ // describe a class that is never instantiated, while every subclass compiles to whatever it
977
+ // declares -- which in v22 means OnPush. Correctness comes from the state below notifying
978
+ // Angular, never from this decorator. See docs/guides/reactivity.md.
979
+ class FiltersBase {
980
+ //#region ViewChilds, Inputs, Outputs
981
+ buttonFilters;
982
+ //#endregion
983
+ //#region Variables
984
+ destroy$ = new Subject();
985
+ filterForm;
986
+ formBuilder = inject(FormBuilder);
987
+ //#endregion
988
+ //#region Properties
989
+ //#endregion
990
+ //#region Constructor and Angular life cycle methods
991
+ ngOnDestroy() {
992
+ this.destroy$.next(true);
993
+ this.destroy$.complete();
994
+ }
995
+ ngOnInit() {
996
+ this.filterForm = this.formSetup();
997
+ }
998
+ //#endregion
999
+ //#region Event handlers
1000
+ //#endregion
1001
+ //#region Public methods
1002
+ setFilters(filters) {
1003
+ this.filterForm.patchValue(filters);
1004
+ setTimeout(() => {
1005
+ if (this.buttonFilters) {
1006
+ this.buttonFilters.setFilters(filters);
1007
+ }
1008
+ });
1009
+ }
1010
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FiltersBase, deps: [], target: i0.ɵɵFactoryTarget.Component });
1011
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: FiltersBase, isStandalone: true, selector: "ng-component", viewQueries: [{ propertyName: "buttonFilters", first: true, predicate: ButtonFiltersComponent, descendants: true }], ngImport: i0, template: '', isInline: true });
1012
+ }
1013
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FiltersBase, decorators: [{
1014
+ type: Component,
1015
+ args: [{ template: '' }]
1016
+ }], propDecorators: { buttonFilters: [{
1017
+ type: ViewChild,
1018
+ args: [ButtonFiltersComponent]
1019
+ }] } });
1020
+
1021
+ class OperationsHistoryDataset extends DataGridDataset {
1022
+ //#region ViewChilds, Inputs, Outputs
1023
+ //#endregion
1024
+ //#region Variables
1025
+ columns = [{ field: '', headerName: '' }];
1026
+ controllerName;
1027
+ operationsHistoryService = inject(OperationsHistoryService);
1028
+ _serviceID;
1029
+ //#endregion
1030
+ //#region Properties
1031
+ set serviceId(value) {
1032
+ if (this._serviceID !== value) {
1033
+ this._serviceID = value;
1034
+ this.refresh();
1035
+ }
1036
+ }
1037
+ //#endregion
1038
+ //#region Constructor and Angular life cycle methods
1039
+ constructor() {
1040
+ super();
1041
+ this.configs.hideColumnHeaders = true;
1042
+ this.configs.showMessageOnEmpty = false;
1043
+ }
1044
+ //#endregion
1045
+ //#region Event handlers
1046
+ //#endregion
1047
+ //#region Public methods
1048
+ getData(params) {
1049
+ this.configs.showMessageOnEmpty = !!this._serviceID;
1050
+ return !!this.parentEntityId && !!this.controllerName && !!this._serviceID
1051
+ ? this.operationsHistoryService.list(this.controllerName, this.parentEntityId, this._serviceID, params)
1052
+ : of([]);
1053
+ }
1054
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: OperationsHistoryDataset, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1055
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: OperationsHistoryDataset });
1056
+ }
1057
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: OperationsHistoryDataset, decorators: [{
1058
+ type: Injectable
1059
+ }], ctorParameters: () => [] });
1060
+
1061
+ class ServicesHistoryDataset extends DataGridDataset {
1062
+ //#region ViewChilds, Inputs, Outputs
1063
+ //#endregion
1064
+ //#region Variables
1065
+ columns = [{ field: '', headerName: '' }];
1066
+ controllerName;
1067
+ servicesHistoryService = inject(ServicesHistoryService);
1068
+ //#endregion
1069
+ //#region Properties
1070
+ //#endregion
1071
+ //#region Constructor and Angular life cycle methods
1072
+ constructor() {
1073
+ super();
1074
+ this.configs.hideColumnHeaders = true;
1075
+ this.configs.rowHeight = 52;
1076
+ }
1077
+ //#endregion
1078
+ //#region Event handlers
1079
+ //#endregion
1080
+ //#region Public methods
1081
+ getData(params) {
1082
+ return !!this.parentEntityId && !!this.controllerName
1083
+ ? this.servicesHistoryService.list(this.controllerName, this.parentEntityId, this.withUtcPeriod(params))
1084
+ : of([]);
1085
+ }
1086
+ //#endregion
1087
+ //#region Private methods
1088
+ /**
1089
+ * Widens the picked dates to whole local days and sends them as UTC instants.
1090
+ *
1091
+ * A date input yields `YYYY-MM-DD` -- no time, no zone -- while the history stores UTC instants
1092
+ * and the backend compares what it is given. Sent as they are, "from the 5th to the 5th" is the
1093
+ * single instant at midnight and matches nothing, and in a zone behind UTC it does not even
1094
+ * cover the right day.
1095
+ *
1096
+ * Done here rather than in the filter form because the form is what the modal shows: reopening
1097
+ * it patches these values straight back into the fields, which have to keep reading as the dates
1098
+ * the person picked. This is the boundary where the request is built, so it is also the last
1099
+ * point that sees every caller.
1100
+ */
1101
+ withUtcPeriod(params) {
1102
+ const filters = params.filters;
1103
+ if (!filters?.['changedOnFrom'] && !filters?.['changedOnTo']) {
1104
+ return params;
1105
+ }
1106
+ const converted = { ...filters };
1107
+ if (filters['changedOnFrom']) {
1108
+ converted['changedOnFrom'] = ServicesHistoryDataset.toUtcInstant(filters['changedOnFrom'], 'start');
1109
+ }
1110
+ if (filters['changedOnTo']) {
1111
+ converted['changedOnTo'] = ServicesHistoryDataset.toUtcInstant(filters['changedOnTo'], 'end');
1112
+ }
1113
+ return { ...params, filters: converted };
1114
+ }
1115
+ /**
1116
+ * The UTC wall clock of one end of a local day, written without a zone designator.
1117
+ *
1118
+ * Without one on purpose. `DateTime.TryParse` on the server reads a trailing `Z` as an instant
1119
+ * and hands back the *machine's* local time for it, so the same request would mean different
1120
+ * things on a server in UTC and one in UTC-3, and the stored column is plain UTC either way.
1121
+ * Dropping the designator leaves the numbers alone, which is what the column holds.
1122
+ */
1123
+ static toUtcInstant(value, edge) {
1124
+ const [year, month, day] = value.split('-').map(Number);
1125
+ // Left alone when it is not the date input's own format: a value set from somewhere else is
1126
+ // already whatever its caller meant.
1127
+ if ([year, month, day].some((part) => Number.isNaN(part))) {
1128
+ return value;
1129
+ }
1130
+ const instant = edge === 'start' ? new Date(year, month - 1, day, 0, 0, 0, 0) : new Date(year, month - 1, day, 23, 59, 59, 999);
1131
+ return instant.toISOString().replace(/Z$/, '');
1132
+ }
1133
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ServicesHistoryDataset, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1134
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ServicesHistoryDataset });
1135
+ }
1136
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ServicesHistoryDataset, decorators: [{
1137
+ type: Injectable
1138
+ }], ctorParameters: () => [] });
1139
+
1140
+ /**
1141
+ * Displays an external destination inside an application tab.
1142
+ *
1143
+ * Routed as `/external-content/:menuID` — the destination itself never travels in the URL, so no
1144
+ * one can hand-craft a link that makes the application frame an arbitrary site.
1145
+ *
1146
+ * A `TabViewBase` like any other screen, so its actions live in the ribbon rather than in a
1147
+ * bar of its own: it must be routed under `DefaultTabViewComponent`, which is what renders the
1148
+ * `#ribbon` template this view publishes. Use the exported `externalContentRoutes`.
1149
+ */
1150
+ class ExternalContentComponent extends TabViewBase {
1151
+ //#region ViewChilds, Inputs, Outputs
1152
+ //#endregion
1153
+ //#region Variables
1154
+ // Signals, every one: this whole screen is decided after the first paint, by a lookup that
1155
+ // resolves, a frame that loads or fails to, and a timeout that gives up waiting. None of
1156
+ // those is a binding on this view, so as fields they changed nothing on screen.
1157
+ frameUrl = signal(undefined, /* @ts-ignore */
1158
+ ...(ngDevMode ? [{ debugName: "frameUrl" }] : /* istanbul ignore next */ []));
1159
+ isBlocked = signal(false, /* @ts-ignore */
1160
+ ...(ngDevMode ? [{ debugName: "isBlocked" }] : /* istanbul ignore next */ []));
1161
+ isFrameLoading = signal(false, /* @ts-ignore */
1162
+ ...(ngDevMode ? [{ debugName: "isFrameLoading" }] : /* istanbul ignore next */ []));
1163
+ isInsecure = signal(false, /* @ts-ignore */
1164
+ ...(ngDevMode ? [{ debugName: "isInsecure" }] : /* istanbul ignore next */ []));
1165
+ isSlow = signal(false, /* @ts-ignore */
1166
+ ...(ngDevMode ? [{ debugName: "isSlow" }] : /* istanbul ignore next */ []));
1167
+ isUnavailable = signal(false, /* @ts-ignore */
1168
+ ...(ngDevMode ? [{ debugName: "isUnavailable" }] : /* istanbul ignore next */ []));
1169
+ label = signal('', /* @ts-ignore */
1170
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1171
+ resolvedUrl = signal('', /* @ts-ignore */
1172
+ ...(ngDevMode ? [{ debugName: "resolvedUrl" }] : /* istanbul ignore next */ []));
1173
+ activatedRoute = inject(ActivatedRoute);
1174
+ configs = inject(EXTERNAL_CONTENT_CONFIGS);
1175
+ document = inject(DOCUMENT);
1176
+ externalContentService = inject(ExternalContentService);
1177
+ externalUrlResolverService = inject(ExternalUrlResolverService);
1178
+ sanitizer = inject(DomSanitizer);
1179
+ slowHintTimeout;
1180
+ tabService = inject(TabService);
1181
+ //#endregion
1182
+ //#region Properties
1183
+ //#endregion
1184
+ //#region Constructor and Angular life cycle methods
1185
+ constructor() {
1186
+ super();
1187
+ }
1188
+ ngOnDestroy() {
1189
+ this.clearSlowHint();
1190
+ super.ngOnDestroy();
1191
+ }
1192
+ ngOnInit() {
1193
+ const menuID = Number(this.activatedRoute.snapshot.paramMap.get('menuID'));
1194
+ if (!menuID) {
1195
+ this.isUnavailable.set(true);
1196
+ this.loading = false;
1197
+ return;
1198
+ }
1199
+ this.externalContentService
1200
+ .find(menuID)
1201
+ .pipe(take(1), takeUntil(this.destroy$))
1202
+ .subscribe((entry) => this.show(entry));
1203
+ }
1204
+ //#endregion
1205
+ //#region Event handlers
1206
+ onFrameLoad() {
1207
+ this.clearSlowHint();
1208
+ this.isFrameLoading.set(false);
1209
+ this.isSlow.set(false);
1210
+ }
1211
+ onOpenInNewTab() {
1212
+ window.open(this.resolvedUrl(), '_blank', 'noopener,noreferrer');
1213
+ }
1214
+ onReload() {
1215
+ const url = this.frameUrl();
1216
+ if (!url || this.isFrameLoading()) {
1217
+ return;
1218
+ }
1219
+ // The frame is cross-origin, so its location cannot be touched from here: destroying the
1220
+ // element and building a new one is the only way to make it navigate again. That also clears
1221
+ // the previous render, which is the point -- otherwise a reload leaves the old content on
1222
+ // screen and the user cannot tell whether anything happened.
1223
+ //
1224
+ // The rebuild has to wait for a macrotask, not a microtask. Angular coalesces changes within a
1225
+ // change-detection cycle, so setting frameUrl back before the next tick means @if sees the
1226
+ // value go A -> undefined -> A and never toggles: the element is never destroyed, nothing
1227
+ // navigates, no `load` ever fires, and the view spins until the timeout gives up. Yielding to
1228
+ // a timeout lets a cycle run with the frame unmounted, which is what really tears it down.
1229
+ this.frameUrl.set(undefined);
1230
+ this.isFrameLoading.set(true);
1231
+ setTimeout(() => this.displayFrame(url));
1232
+ }
1233
+ //#endregion
1234
+ //#region Public methods
1235
+ //#endregion
1236
+ //#region Private methods
1237
+ clearSlowHint() {
1238
+ if (this.slowHintTimeout !== undefined) {
1239
+ clearTimeout(this.slowHintTimeout);
1240
+ this.slowHintTimeout = undefined;
1241
+ }
1242
+ }
1243
+ isMixedContent(url) {
1244
+ // A browser refuses to embed an http:// frame in an https:// page, unconditionally: an iframe
1245
+ // is active mixed content. Nothing on this side can permit it -- no attribute, no header, and
1246
+ // not CSP, which only ever restricts further. Left alone the frame renders empty, so this is
1247
+ // checked up front to say so instead.
1248
+ //
1249
+ // Unlike a site refusing to be framed, this one IS knowable before trying: both protocols are
1250
+ // in hand. `new URL` cannot throw here -- isAllowed has already parsed the same string -- and
1251
+ // it normalises the scheme's case, which a prefix test would not.
1252
+ return this.document.location.protocol === 'https:' && new URL(url).protocol === 'http:';
1253
+ }
1254
+ isOriginAllowed(url) {
1255
+ if (this.configs.allowedOrigins.length === 0) {
1256
+ return true;
1257
+ }
1258
+ try {
1259
+ return this.configs.allowedOrigins.indexOf(new URL(url).origin) !== -1;
1260
+ }
1261
+ catch {
1262
+ return false;
1263
+ }
1264
+ }
1265
+ show(entry) {
1266
+ // TabViewBase starts every screen loading so its ribbon buttons begin disabled; every exit
1267
+ // from here has to clear it or they never become usable.
1268
+ this.loading = false;
1269
+ if (!entry || !entry.url) {
1270
+ this.isUnavailable.set(true);
1271
+ return;
1272
+ }
1273
+ this.label.set(entry.label);
1274
+ // MainLayoutComponent only resolves a deep-linked title through getMenuFromUrl, and a Tab
1275
+ // starts with isTitleLoading true, so a tab re-created by TabsComponent after a refresh would
1276
+ // spin forever if nothing set its title. Setting it here is idempotent.
1277
+ this.tabService.updateActiveTabRootTitle(entry.label);
1278
+ // Order matters: resolve, then validate, then trust. What we vet has to be exactly what the
1279
+ // browser receives, and a placeholder is substituted before the scheme can be inspected.
1280
+ const url = this.externalUrlResolverService.resolve(entry.url);
1281
+ if (!this.externalUrlResolverService.isAllowed(url) || !this.isOriginAllowed(url)) {
1282
+ console.error(`Embedded menu item "${entry.label}" points to an address that is not allowed and was not displayed.`, url);
1283
+ this.isBlocked.set(true);
1284
+ return;
1285
+ }
1286
+ // Set before the mixed-content check, not after: that state still offers 'open in a new
1287
+ // browser tab', and the button reads this to decide whether it has anywhere to go.
1288
+ this.resolvedUrl.set(url);
1289
+ if (this.isMixedContent(url)) {
1290
+ this.isInsecure.set(true);
1291
+ return;
1292
+ }
1293
+ // Trusted once, into a field. From a getter or a pipe this would hand back a new
1294
+ // SafeResourceUrl on every change-detection pass, and Angular would re-set the iframe's src
1295
+ // and reload the destination each time.
1296
+ this.displayFrame(this.sanitizer.bypassSecurityTrustResourceUrl(url));
1297
+ }
1298
+ displayFrame(url) {
1299
+ this.isFrameLoading.set(true);
1300
+ this.frameUrl.set(url);
1301
+ this.startSlowHint();
1302
+ }
1303
+ startSlowHint() {
1304
+ this.clearSlowHint();
1305
+ // Not detection, and it must never be turned into one: whether a site refuses to be framed
1306
+ // (X-Frame-Options, CSP frame-ancestors) is not observable from JavaScript. A refused frame
1307
+ // usually fires `load` immediately and renders the browser's own error page, in which case
1308
+ // this hint never appears -- the ribbon's "open in a new browser tab" button is the actual
1309
+ // way out, and it is always present.
1310
+ this.slowHintTimeout = setTimeout(() => {
1311
+ // Stop claiming it is loading. A destination that never reports `load` -- a refused frame
1312
+ // among them -- would otherwise spin forever and keep the actions disabled; hand the
1313
+ // controls back and let the hint do the explaining.
1314
+ this.isFrameLoading.set(false);
1315
+ this.isSlow.set(true);
1316
+ }, this.configs.slowFrameHintDelay);
1317
+ }
1318
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ExternalContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1319
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: ExternalContentComponent, isStandalone: true, selector: "shared-external-content", usesInheritance: true, ngImport: i0, template: "<!-- Published to DefaultTabViewComponent through TabViewService, so the actions sit in the\r\n application's ribbon like every other screen's. -->\r\n<ng-template #ribbon>\r\n <lib-ribbon-group [label]=\"'RibbonGroup-Page' | translate\">\r\n <!-- Same icon and label as framework-button-refresh, so reloading a report reads as the\r\n same action as refreshing a grid. `loading` is what spins it while the frame loads. -->\r\n <framework-button\r\n icon=\"fa-sync-alt\"\r\n label=\"Button-Refresh\"\r\n [disabled]=\"!resolvedUrl() || isInsecure()\"\r\n [loading]=\"isFrameLoading()\"\r\n (action)=\"onReload()\"\r\n >\r\n </framework-button>\r\n\r\n <framework-button\r\n icon=\"fa-arrow-up-right-from-square\"\r\n label=\"ExternalContent-OpenInNewTab\"\r\n [disabled]=\"!resolvedUrl()\"\r\n (action)=\"onOpenInNewTab()\"\r\n >\r\n </framework-button>\r\n </lib-ribbon-group>\r\n</ng-template>\r\n\r\n@if (isUnavailable()) {\r\n <div class=\"external-content-message\">\r\n <i class=\"fa-solid fa-link-slash\"></i>\r\n <span class=\"external-content-message-title\">{{ 'ExternalContent-Unavailable-Title' | translate }}</span>\r\n <span>{{ 'ExternalContent-Unavailable-Message' | translate }}</span>\r\n </div>\r\n} @else if (isInsecure()) {\r\n <div class=\"external-content-message\">\r\n <i class=\"fa-solid fa-shield-halved\"></i>\r\n <span class=\"external-content-message-title\">{{ 'ExternalContent-Insecure-Title' | translate }}</span>\r\n <span>{{ 'ExternalContent-Insecure-Message' | translate }}</span>\r\n </div>\r\n} @else if (isBlocked()) {\r\n <div class=\"external-content-message\">\r\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\r\n <span class=\"external-content-message-title\">{{ 'ExternalContent-Blocked-Title' | translate }}</span>\r\n <span>{{ 'ExternalContent-Blocked-Message' | translate }}</span>\r\n </div>\r\n} @else {\r\n @if (isSlow()) {\r\n <div class=\"external-content-hint\">{{ 'ExternalContent-SlowHint' | translate }}</div>\r\n }\r\n @if (frameUrl() || isFrameLoading()) {\r\n <!--\r\n The container stays mounted across a reload, which briefly clears frameUrl, so the panel\r\n does not flicker out and back. What goes away is the frame itself, on purpose.\r\n\r\n sandbox and referrerpolicy are STATIC attributes on purpose. Angular rejects `sandbox`\r\n as a binding on an <iframe> outright (NG0910) \u2014 it may only be a static attribute \u2014 and\r\n the whole element then fails to render.\r\n\r\n The token list is fixed for every destination, and deliberately not configurable per\r\n menu item:\r\n - allow-same-origin keeps the frame in the destination's own origin so its cookies and\r\n storage work; without it an SSO'd report will not render. It grants no access to ours,\r\n provided the destination is cross-origin -- never point an embedded item at this\r\n application's own origin, use an internal route for that.\r\n - allow-top-navigation is absent on purpose: a framed site must not be able to navigate\r\n the whole application away.\r\n - allow-popups-to-escape-sandbox keeps print and download popups usable.\r\n -->\r\n <div class=\"external-content-frame-container\">\r\n @if (isFrameLoading()) {\r\n <div class=\"external-content-loading\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\r\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\r\n <path\r\n class=\"opacity-75\"\r\n fill=\"currentColor\"\r\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\r\n ></path>\r\n </svg>\r\n\r\n <span>{{ 'Loading' | translate }}</span>\r\n </div>\r\n }\r\n @if (frameUrl()) {\r\n <iframe\r\n class=\"external-content-frame\"\r\n sandbox=\"allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads\"\r\n referrerpolicy=\"strict-origin-when-cross-origin\"\r\n [src]=\"frameUrl()\"\r\n [attr.title]=\"label() | translate\"\r\n (load)=\"onFrameLoad()\"\r\n >\r\n </iframe>\r\n }\r\n </div>\r\n }\r\n}\r\n", styles: ["@layer properties;:host{display:flex;flex-grow:1;flex-direction:column;gap:calc(var(--spacing, .25rem) * 2);overflow:hidden;min-height:0}.external-content-hint{flex:none;border-radius:var(--radius-lg, .5rem);border-style:var(--tw-border-style);border-width:1px;padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));border-color:var(--color-amber-300, oklch(87.9% .169 91.605));background-color:var(--color-amber-100, oklch(96.2% .059 95.617));color:var(--color-amber-900, oklch(41.4% .112 45.904))}.external-content-frame-container{border-radius:var(--radius-lg, .5rem);border-style:var(--tw-border-style);border-width:1px;border-color:color-mix(in srgb,oklch(87.2% .01 258.338) 90%,transparent);background-color:var(--color-white, #fff);position:relative;display:flex;flex-grow:1;overflow:hidden;min-height:0}@supports (color: color-mix(in lab,red,red)){.external-content-frame-container{border-color:color-mix(in oklab,var(--color-gray-300, oklch(87.2% .01 258.338)) 90%,transparent)}}.external-content-loading{position:absolute;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;gap:calc(var(--spacing, .25rem) * 2);background-color:var(--color-white, #fff);color:var(--color-gray-500, oklch(55.1% .027 264.364))}.external-content-loading svg{height:calc(var(--spacing, .25rem) * 6);min-height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);min-width:calc(var(--spacing, .25rem) * 6);animation:var(--animate-spin, spin 1s linear infinite)}.external-content-frame{width:100%;flex-grow:1;border-style:var(--tw-border-style);border-width:0px;min-height:0}.external-content-message{border-radius:var(--radius-lg, .5rem);border-style:var(--tw-border-style);border-width:1px;border-color:color-mix(in srgb,oklch(87.2% .01 258.338) 90%,transparent);background-color:var(--color-white, #fff);display:flex;flex-grow:1;flex-direction:column;align-items:center;justify-content:center;gap:calc(var(--spacing, .25rem) * 2);padding:calc(var(--spacing, .25rem) * 6);text-align:center}@supports (color: color-mix(in lab,red,red)){.external-content-message{border-color:color-mix(in oklab,var(--color-gray-300, oklch(87.2% .01 258.338)) 90%,transparent)}}.external-content-message i{font-size:var(--text-4xl, 2.25rem);line-height:var(--tw-leading, var(--text-4xl--line-height, calc(2.5 / 2.25)));opacity:40%}.external-content-message .external-content-message-title{font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@keyframes spin{to{transform:rotate(360deg)}}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-font-weight: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "framework-button", inputs: ["color", "icon", "label"], outputs: ["action"] }, { kind: "component", type: RibbonGroupComponent, selector: "lib-ribbon-group", inputs: ["label"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1320
+ }
1321
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ExternalContentComponent, decorators: [{
1322
+ type: Component,
1323
+ args: [{ selector: 'shared-external-content', imports: [ButtonComponent, RibbonGroupComponent, TranslatePipe], template: "<!-- Published to DefaultTabViewComponent through TabViewService, so the actions sit in the\r\n application's ribbon like every other screen's. -->\r\n<ng-template #ribbon>\r\n <lib-ribbon-group [label]=\"'RibbonGroup-Page' | translate\">\r\n <!-- Same icon and label as framework-button-refresh, so reloading a report reads as the\r\n same action as refreshing a grid. `loading` is what spins it while the frame loads. -->\r\n <framework-button\r\n icon=\"fa-sync-alt\"\r\n label=\"Button-Refresh\"\r\n [disabled]=\"!resolvedUrl() || isInsecure()\"\r\n [loading]=\"isFrameLoading()\"\r\n (action)=\"onReload()\"\r\n >\r\n </framework-button>\r\n\r\n <framework-button\r\n icon=\"fa-arrow-up-right-from-square\"\r\n label=\"ExternalContent-OpenInNewTab\"\r\n [disabled]=\"!resolvedUrl()\"\r\n (action)=\"onOpenInNewTab()\"\r\n >\r\n </framework-button>\r\n </lib-ribbon-group>\r\n</ng-template>\r\n\r\n@if (isUnavailable()) {\r\n <div class=\"external-content-message\">\r\n <i class=\"fa-solid fa-link-slash\"></i>\r\n <span class=\"external-content-message-title\">{{ 'ExternalContent-Unavailable-Title' | translate }}</span>\r\n <span>{{ 'ExternalContent-Unavailable-Message' | translate }}</span>\r\n </div>\r\n} @else if (isInsecure()) {\r\n <div class=\"external-content-message\">\r\n <i class=\"fa-solid fa-shield-halved\"></i>\r\n <span class=\"external-content-message-title\">{{ 'ExternalContent-Insecure-Title' | translate }}</span>\r\n <span>{{ 'ExternalContent-Insecure-Message' | translate }}</span>\r\n </div>\r\n} @else if (isBlocked()) {\r\n <div class=\"external-content-message\">\r\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\r\n <span class=\"external-content-message-title\">{{ 'ExternalContent-Blocked-Title' | translate }}</span>\r\n <span>{{ 'ExternalContent-Blocked-Message' | translate }}</span>\r\n </div>\r\n} @else {\r\n @if (isSlow()) {\r\n <div class=\"external-content-hint\">{{ 'ExternalContent-SlowHint' | translate }}</div>\r\n }\r\n @if (frameUrl() || isFrameLoading()) {\r\n <!--\r\n The container stays mounted across a reload, which briefly clears frameUrl, so the panel\r\n does not flicker out and back. What goes away is the frame itself, on purpose.\r\n\r\n sandbox and referrerpolicy are STATIC attributes on purpose. Angular rejects `sandbox`\r\n as a binding on an <iframe> outright (NG0910) \u2014 it may only be a static attribute \u2014 and\r\n the whole element then fails to render.\r\n\r\n The token list is fixed for every destination, and deliberately not configurable per\r\n menu item:\r\n - allow-same-origin keeps the frame in the destination's own origin so its cookies and\r\n storage work; without it an SSO'd report will not render. It grants no access to ours,\r\n provided the destination is cross-origin -- never point an embedded item at this\r\n application's own origin, use an internal route for that.\r\n - allow-top-navigation is absent on purpose: a framed site must not be able to navigate\r\n the whole application away.\r\n - allow-popups-to-escape-sandbox keeps print and download popups usable.\r\n -->\r\n <div class=\"external-content-frame-container\">\r\n @if (isFrameLoading()) {\r\n <div class=\"external-content-loading\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\r\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\r\n <path\r\n class=\"opacity-75\"\r\n fill=\"currentColor\"\r\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\r\n ></path>\r\n </svg>\r\n\r\n <span>{{ 'Loading' | translate }}</span>\r\n </div>\r\n }\r\n @if (frameUrl()) {\r\n <iframe\r\n class=\"external-content-frame\"\r\n sandbox=\"allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads\"\r\n referrerpolicy=\"strict-origin-when-cross-origin\"\r\n [src]=\"frameUrl()\"\r\n [attr.title]=\"label() | translate\"\r\n (load)=\"onFrameLoad()\"\r\n >\r\n </iframe>\r\n }\r\n </div>\r\n }\r\n}\r\n", styles: ["@layer properties;:host{display:flex;flex-grow:1;flex-direction:column;gap:calc(var(--spacing, .25rem) * 2);overflow:hidden;min-height:0}.external-content-hint{flex:none;border-radius:var(--radius-lg, .5rem);border-style:var(--tw-border-style);border-width:1px;padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));border-color:var(--color-amber-300, oklch(87.9% .169 91.605));background-color:var(--color-amber-100, oklch(96.2% .059 95.617));color:var(--color-amber-900, oklch(41.4% .112 45.904))}.external-content-frame-container{border-radius:var(--radius-lg, .5rem);border-style:var(--tw-border-style);border-width:1px;border-color:color-mix(in srgb,oklch(87.2% .01 258.338) 90%,transparent);background-color:var(--color-white, #fff);position:relative;display:flex;flex-grow:1;overflow:hidden;min-height:0}@supports (color: color-mix(in lab,red,red)){.external-content-frame-container{border-color:color-mix(in oklab,var(--color-gray-300, oklch(87.2% .01 258.338)) 90%,transparent)}}.external-content-loading{position:absolute;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;gap:calc(var(--spacing, .25rem) * 2);background-color:var(--color-white, #fff);color:var(--color-gray-500, oklch(55.1% .027 264.364))}.external-content-loading svg{height:calc(var(--spacing, .25rem) * 6);min-height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);min-width:calc(var(--spacing, .25rem) * 6);animation:var(--animate-spin, spin 1s linear infinite)}.external-content-frame{width:100%;flex-grow:1;border-style:var(--tw-border-style);border-width:0px;min-height:0}.external-content-message{border-radius:var(--radius-lg, .5rem);border-style:var(--tw-border-style);border-width:1px;border-color:color-mix(in srgb,oklch(87.2% .01 258.338) 90%,transparent);background-color:var(--color-white, #fff);display:flex;flex-grow:1;flex-direction:column;align-items:center;justify-content:center;gap:calc(var(--spacing, .25rem) * 2);padding:calc(var(--spacing, .25rem) * 6);text-align:center}@supports (color: color-mix(in lab,red,red)){.external-content-message{border-color:color-mix(in oklab,var(--color-gray-300, oklch(87.2% .01 258.338)) 90%,transparent)}}.external-content-message i{font-size:var(--text-4xl, 2.25rem);line-height:var(--tw-leading, var(--text-4xl--line-height, calc(2.5 / 2.25)));opacity:40%}.external-content-message .external-content-message-title{font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@keyframes spin{to{transform:rotate(360deg)}}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-font-weight: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
1324
+ }], ctorParameters: () => [] });
1325
+
1326
+ /**
1327
+ * Route table for embedded external content. Spread it into `MainLayoutComponent`'s children:
1328
+ *
1329
+ * ```ts
1330
+ * { path: '', component: MainLayoutComponent, canActivate: [AuthGuard], children: [
1331
+ * ...externalContentRoutes,
1332
+ * // the application's own features
1333
+ * ] }
1334
+ * ```
1335
+ *
1336
+ * Three things about the shape are load-bearing and must not be "simplified" away:
1337
+ *
1338
+ * - The path is nested one segment per route. `RouteHelper.getRouteURL` and
1339
+ * `CustomReuseStrategy.getUrlFromRoute` collect segments walking up the parent chain and then
1340
+ * reverse the flat list, so a route declared `'external-content/:menuID'` rebuilds as
1341
+ * `/:menuID/external-content` and every tab URL is wrong.
1342
+ * - `DefaultTabViewComponent` hosts the view. It is what renders the `#ribbon` template the
1343
+ * component publishes, so dropping it leaves the Reload and Open-in-a-new-browser-tab actions
1344
+ * with nowhere to appear.
1345
+ * - `FRAMEWORK_VIEW_TYPE` must be present. `TabsComponent` re-creates the tab after a page refresh
1346
+ * by looking for a `Details` or `List` view in the activated route tree, and navigates to `/`
1347
+ * when it finds neither — the embedded tab would vanish on F5.
1348
+ */
1349
+ const externalContentRoutes = [
1350
+ {
1351
+ path: EXTERNAL_CONTENT_ROUTE_PATH,
1352
+ children: [
1353
+ {
1354
+ path: ':menuID',
1355
+ component: DefaultTabViewComponent,
1356
+ data: { [FRAMEWORK_VIEW_TYPE]: FrameworkViewType.List },
1357
+ children: [{ path: '', component: ExternalContentComponent }],
1358
+ },
1359
+ ],
1360
+ },
1361
+ ];
1362
+
1363
+ class OperationsHistoryModalComponent extends ModalBase {
1364
+ //#region ViewChilds, Inputs, Outputs
1365
+ modal = viewChild(ModalComponent, /* @ts-ignore */
1366
+ ...(ngDevMode ? [{ debugName: "modal" }] : /* istanbul ignore next */ []));
1367
+ newValues = input(/* @ts-ignore */
1368
+ ...(ngDevMode ? [undefined, { debugName: "newValues" }] : /* istanbul ignore next */ []));
1369
+ oldValues = input(/* @ts-ignore */
1370
+ ...(ngDevMode ? [undefined, { debugName: "oldValues" }] : /* istanbul ignore next */ []));
1371
+ //#endregion
1372
+ //#region Variables
1373
+ //#endregion
1374
+ //#region Properties
1375
+ /** Delegates to the inner modal; a signal query, so it re-runs once that child resolves. */
1376
+ isModalShown = computed(() => this.modal()?.isOpen() ?? false, /* @ts-ignore */
1377
+ ...(ngDevMode ? [{ debugName: "isModalShown" }] : /* istanbul ignore next */ []));
1378
+ //#endregion
1379
+ //#region Constructor and Angular life cycle methods
1380
+ //#endregion
1381
+ //#region Event handlers
1382
+ //#endregion
1383
+ //#region Public methods
1384
+ toggle() {
1385
+ this.modal()?.toggleModal();
1386
+ }
1387
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: OperationsHistoryModalComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1388
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.7", type: OperationsHistoryModalComponent, isStandalone: true, selector: "shared-operations-history-modal", inputs: { newValues: { classPropertyName: "newValues", publicName: "newValues", isSignal: true, isRequired: false, transformFunction: null }, oldValues: { classPropertyName: "oldValues", publicName: "oldValues", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "modal", first: true, predicate: ModalComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<lib-modal size=\"6xl\" [title]=\"'OperationsHistory-Modal-Title' | translate\">\r\n <div body>\r\n <pre>{{ oldValues() | json }}</pre>\r\n <pre>{{ newValues() | json }}</pre>\r\n </div>\r\n</lib-modal>\r\n", styles: ["div[body]{display:flex;flex-direction:row}div[body] pre{width:50%;flex-grow:1;overflow-x:auto;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)))}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: ModalComponent, selector: "lib-modal", inputs: ["closeButtonText", "dialog", "modalProcessing", "position", "size", "title"], outputs: ["closeButtonTextChange", "modalProcessingChange", "closed"] }, { kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1389
+ }
1390
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: OperationsHistoryModalComponent, decorators: [{
1391
+ type: Component,
1392
+ args: [{ selector: 'shared-operations-history-modal', imports: [JsonPipe, ModalComponent, TranslatePipe], template: "<lib-modal size=\"6xl\" [title]=\"'OperationsHistory-Modal-Title' | translate\">\r\n <div body>\r\n <pre>{{ oldValues() | json }}</pre>\r\n <pre>{{ newValues() | json }}</pre>\r\n </div>\r\n</lib-modal>\r\n", styles: ["div[body]{display:flex;flex-direction:row}div[body] pre{width:50%;flex-grow:1;overflow-x:auto;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)))}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
1393
+ }], propDecorators: { modal: [{ type: i0.ViewChild, args: [i0.forwardRef(() => ModalComponent), { isSignal: true }] }], newValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "newValues", required: false }] }], oldValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "oldValues", required: false }] }] } });
1394
+
1395
+ class OperationsHistoryChildListComponent extends ChildList {
1396
+ //#region ViewChilds, Inputs, Outputs
1397
+ modal = viewChild(OperationsHistoryModalComponent, /* @ts-ignore */
1398
+ ...(ngDevMode ? [{ debugName: "modal" }] : /* istanbul ignore next */ []));
1399
+ operationTemplate = viewChild('operationTemplate', /* @ts-ignore */
1400
+ ...(ngDevMode ? [{ debugName: "operationTemplate" }] : /* istanbul ignore next */ []));
1401
+ set controllerName(value) {
1402
+ this.dataGridDataset.controllerName = value;
1403
+ }
1404
+ /**
1405
+ * Filters to send with the list.
1406
+ *
1407
+ * The audit view applies its filters to the services grid and hands the same set here, because
1408
+ * one of them -- whether to list only the audited record's own operations -- belongs to this
1409
+ * list. The rest mean nothing to it, and the backend reads only what it recognises.
1410
+ *
1411
+ * `setFilters` refreshes the grid itself. With no service selected the dataset answers with an
1412
+ * empty list rather than calling the backend, so a refresh then costs nothing.
1413
+ */
1414
+ set filters(value) {
1415
+ this.dataGridDataset.setFilters(value);
1416
+ }
1417
+ set serviceID(value) {
1418
+ this.dataGridDataset.serviceId = value;
1419
+ }
1420
+ //#endregion
1421
+ //#region Variables
1422
+ // Signals: both are bound into the modal and are filled when a row is picked, which the grid
1423
+ // reports through a callback rather than a binding on this view.
1424
+ selectedNewValues = signal(null, /* @ts-ignore */
1425
+ ...(ngDevMode ? [{ debugName: "selectedNewValues" }] : /* istanbul ignore next */ []));
1426
+ selectedOldValues = signal(null, /* @ts-ignore */
1427
+ ...(ngDevMode ? [{ debugName: "selectedOldValues" }] : /* istanbul ignore next */ []));
1428
+ //#endregion
1429
+ //#region Properties
1430
+ //#endregion
1431
+ //#region Constructor and Angular life cycle methods
1432
+ ngAfterViewInit() {
1433
+ this.dataGridDataset.columns[0].template = this.operationTemplate();
1434
+ }
1435
+ //#endregion
1436
+ //#region Event handlers
1437
+ openModal() {
1438
+ const modal = this.modal();
1439
+ if (modal && !modal.isModalShown()) {
1440
+ modal.toggle();
1441
+ }
1442
+ }
1443
+ //#endregion
1444
+ //#region Public methods
1445
+ selectedRowChanged() {
1446
+ super.selectedRowChanged();
1447
+ if (this.dataGridDataset.hasSelectedRows) {
1448
+ const key = this.dataGridDataset.selectedRowKeys[0];
1449
+ const data = this.dataGridDataset.getRowData(key);
1450
+ this.selectedOldValues.set(JSON.parse(data.oldValues?.trim() ?? ''));
1451
+ this.selectedNewValues.set(JSON.parse(data.newValues?.trim() ?? ''));
1452
+ }
1453
+ }
1454
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: OperationsHistoryChildListComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1455
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.7", type: OperationsHistoryChildListComponent, isStandalone: true, selector: "shared-operations-history-child-list", inputs: { controllerName: "controllerName", filters: "filters", serviceID: "serviceID" }, providers: [{ provide: DataGridDataset, useClass: OperationsHistoryDataset }], viewQueries: [{ propertyName: "modal", first: true, predicate: OperationsHistoryModalComponent, descendants: true, isSignal: true }, { propertyName: "operationTemplate", first: true, predicate: ["operationTemplate"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<lib-data-grid [lazyLoadRows]=\"true\"> </lib-data-grid>\r\n\r\n<ng-template #operationTemplate let-rowData=\"rowData\">\r\n <div\r\n class=\"operation-content\"\r\n role=\"button\"\r\n tabindex=\"0\"\r\n (click)=\"openModal()\"\r\n (keydown.enter)=\"openModal()\"\r\n (keydown.space)=\"openModal(); $event.preventDefault()\"\r\n >\r\n <span\r\n class=\"operation\"\r\n [class.added]=\"rowData.operationType === 'Added'\"\r\n [class.deleted]=\"rowData.operationType === 'Deleted'\"\r\n [class.modified]=\"rowData.operationType === 'Modified'\"\r\n [class.unchanged]=\"rowData.operationType === 'Unchanged'\"\r\n >\r\n {{ rowData.operationType }}\r\n </span>\r\n {{ rowData.entityName }}\r\n </div>\r\n</ng-template>\r\n\r\n<shared-operations-history-modal [newValues]=\"selectedNewValues()\" [oldValues]=\"selectedOldValues()\">\r\n</shared-operations-history-modal>\r\n", styles: ["@layer properties;:host{width:100%;max-width:100%;min-width:100%}lib-data-grid{height:100%}lib-data-grid ::ng-deep .body lib-data-grid-row{padding:0!important}.operation-content{padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2)}.operation-content .operation{font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}.operation-content .operation.added{color:var(--color-green-700, oklch(52.7% .154 150.069))}.operation-content .operation.deleted{color:var(--color-red-700, oklch(50.5% .213 27.518))}.operation-content .operation.modified{color:var(--color-blue-700, oklch(48.8% .243 264.376))}.operation-content .operation.unchanged{color:var(--color-gray-700, oklch(37.3% .034 259.733))}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: DataGridComponent, selector: "lib-data-grid", inputs: ["disabled", "lazyLoadRows", "showButtons"] }, { kind: "component", type: OperationsHistoryModalComponent, selector: "shared-operations-history-modal", inputs: ["newValues", "oldValues"] }] });
1456
+ }
1457
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: OperationsHistoryChildListComponent, decorators: [{
1458
+ type: Component,
1459
+ args: [{ selector: 'shared-operations-history-child-list', imports: [DataGridComponent, OperationsHistoryModalComponent], providers: [{ provide: DataGridDataset, useClass: OperationsHistoryDataset }], template: "<lib-data-grid [lazyLoadRows]=\"true\"> </lib-data-grid>\r\n\r\n<ng-template #operationTemplate let-rowData=\"rowData\">\r\n <div\r\n class=\"operation-content\"\r\n role=\"button\"\r\n tabindex=\"0\"\r\n (click)=\"openModal()\"\r\n (keydown.enter)=\"openModal()\"\r\n (keydown.space)=\"openModal(); $event.preventDefault()\"\r\n >\r\n <span\r\n class=\"operation\"\r\n [class.added]=\"rowData.operationType === 'Added'\"\r\n [class.deleted]=\"rowData.operationType === 'Deleted'\"\r\n [class.modified]=\"rowData.operationType === 'Modified'\"\r\n [class.unchanged]=\"rowData.operationType === 'Unchanged'\"\r\n >\r\n {{ rowData.operationType }}\r\n </span>\r\n {{ rowData.entityName }}\r\n </div>\r\n</ng-template>\r\n\r\n<shared-operations-history-modal [newValues]=\"selectedNewValues()\" [oldValues]=\"selectedOldValues()\">\r\n</shared-operations-history-modal>\r\n", styles: ["@layer properties;:host{width:100%;max-width:100%;min-width:100%}lib-data-grid{height:100%}lib-data-grid ::ng-deep .body lib-data-grid-row{padding:0!important}.operation-content{padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2)}.operation-content .operation{font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}.operation-content .operation.added{color:var(--color-green-700, oklch(52.7% .154 150.069))}.operation-content .operation.deleted{color:var(--color-red-700, oklch(50.5% .213 27.518))}.operation-content .operation.modified{color:var(--color-blue-700, oklch(48.8% .243 264.376))}.operation-content .operation.unchanged{color:var(--color-gray-700, oklch(37.3% .034 259.733))}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
1460
+ }], propDecorators: { modal: [{ type: i0.ViewChild, args: [i0.forwardRef(() => OperationsHistoryModalComponent), { isSignal: true }] }], operationTemplate: [{ type: i0.ViewChild, args: ['operationTemplate', { isSignal: true }] }], controllerName: [{
1461
+ type: Input
1462
+ }], filters: [{
1463
+ type: Input
1464
+ }], serviceID: [{
1465
+ type: Input
1466
+ }] } });
1467
+
1468
+ /**
1469
+ * Pipe that bypasses Angular's built-in HTML sanitization.
1470
+ *
1471
+ * **WARNING: This pipe introduces XSS (Cross-Site Scripting) risk.**
1472
+ * It marks the provided HTML string as trusted, meaning Angular will
1473
+ * render it without any sanitization. Malicious content such as
1474
+ * `<script>` tags, inline event handlers (`onerror`, `onclick`, etc.),
1475
+ * and `javascript:` URLs will be executed in the user's browser.
1476
+ *
1477
+ * ### Safe usage
1478
+ * - Server-rendered HTML from **trusted, internal APIs** that never
1479
+ * include user-generated content (e.g., admin-authored rich text
1480
+ * that is sanitized server-side before storage).
1481
+ * - Static HTML snippets defined in application code.
1482
+ *
1483
+ * ### Unsafe usage (DO NOT use for these)
1484
+ * - Any value that originates from user input (form fields, query
1485
+ * params, URL fragments, file uploads).
1486
+ * - Third-party API responses whose content you do not control.
1487
+ * - Database fields that may contain unsanitized user submissions.
1488
+ *
1489
+ * @example
1490
+ * // Safe — trusted server content
1491
+ * <div [innerHTML]="trustedServerHtml | bypassHtmlSanitizer"></div>
1492
+ *
1493
+ * // UNSAFE — user-provided content (use Angular’s default sanitization instead)
1494
+ * <div [innerHTML]="comment.body | bypassHtmlSanitizer"></div> // ❌ XSS risk
1495
+ * <div [innerHTML]="comment.body"></div> // ✅ Angular sanitizes automatically
1496
+ */
1497
+ class BypassHtmlSanitizerPipe {
1498
+ sanitizer = inject(DomSanitizer);
1499
+ transform(html) {
1500
+ return this.sanitizer.bypassSecurityTrustHtml(html);
1501
+ }
1502
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: BypassHtmlSanitizerPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1503
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: BypassHtmlSanitizerPipe, isStandalone: true, name: "bypassHtmlSanitizer" });
1504
+ }
1505
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: BypassHtmlSanitizerPipe, decorators: [{
1506
+ type: Pipe,
1507
+ args: [{
1508
+ name: 'bypassHtmlSanitizer',
1509
+ }]
1510
+ }] });
1511
+
1512
+ class EnumLabelPipe {
1513
+ transform(value, enumType) {
1514
+ if (value === undefined || enumType === undefined) {
1515
+ return '';
1516
+ }
1517
+ const keys = Object.keys(enumType).filter((key) => !isNaN(Number(enumType[key])));
1518
+ const values = keys.map((key) => enumType[key]);
1519
+ const index = values.indexOf(value);
1520
+ if (index >= 0) {
1521
+ return keys[index];
1522
+ }
1523
+ return '';
1524
+ }
1525
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: EnumLabelPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1526
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: EnumLabelPipe, isStandalone: true, name: "enumLabel" });
1527
+ }
1528
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: EnumLabelPipe, decorators: [{
1529
+ type: Pipe,
1530
+ args: [{
1531
+ name: 'enumLabel',
1532
+ }]
1533
+ }] });
1534
+
1535
+ class UtcDatePipe {
1536
+ static timeZoneMarker = /(?:Z|[+-]\d{2}:?\d{2})$/i;
1537
+ transform(value) {
1538
+ if (!value) {
1539
+ return null;
1540
+ }
1541
+ const newDate = new Date(value);
1542
+ if (Number.isNaN(newDate.getTime())) {
1543
+ return null;
1544
+ }
1545
+ // A value that declares its zone has already been parsed as the right instant.
1546
+ if (typeof value === 'string' && UtcDatePipe.timeZoneMarker.test(value.trim())) {
1547
+ return newDate;
1548
+ }
1549
+ // The API serializes UTC values without a timezone marker, so Date parses them as local time.
1550
+ // Shifting by the offset re-reads those parts as UTC, which keeps both the time and the day
1551
+ // correct on either side of Greenwich.
1552
+ return new Date(newDate.getTime() - newDate.getTimezoneOffset() * 60000);
1553
+ }
1554
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UtcDatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1555
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: UtcDatePipe, isStandalone: true, name: "utcDate" });
1556
+ }
1557
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UtcDatePipe, decorators: [{
1558
+ type: Pipe,
1559
+ args: [{
1560
+ name: 'utcDate',
1561
+ }]
1562
+ }] });
1563
+
1564
+ class ServicesHistoryChildListComponent extends ChildList {
1565
+ //#region ViewChilds, Inputs, Outputs
1566
+ serviceTemplate = viewChild('serviceTemplate', /* @ts-ignore */
1567
+ ...(ngDevMode ? [{ debugName: "serviceTemplate" }] : /* istanbul ignore next */ []));
1568
+ set controllerName(value) {
1569
+ this.dataGridDataset.controllerName = value;
1570
+ }
1571
+ /** Re-emits the grid's own filter changes, for a sibling list that has to follow them. */
1572
+ filtersChanged = output();
1573
+ //#endregion
1574
+ //#region Variables
1575
+ // A signal: it is bound into the operations list beside this one, and both the row callback
1576
+ // and the grid's load event write it from outside any binding on this view.
1577
+ selectedServiceID = signal(undefined, /* @ts-ignore */
1578
+ ...(ngDevMode ? [{ debugName: "selectedServiceID" }] : /* istanbul ignore next */ []));
1579
+ //#endregion
1580
+ //#region Properties
1581
+ //#endregion
1582
+ //#region Constructor and Angular life cycle methods
1583
+ ngAfterViewInit() {
1584
+ this.dataGridDataset.columns[0].template = this.serviceTemplate();
1585
+ }
1586
+ ngOnInit() {
1587
+ super.ngOnInit();
1588
+ this.dataGridDataset.filtersChanged
1589
+ .pipe(takeUntil(this.destroy$))
1590
+ .subscribe((filters) => this.filtersChanged.emit(filters));
1591
+ // Reloading clears the grid's selection but would leave the last chosen service behind, and
1592
+ // the operations list beside it still showing that service's rows -- a service the new filters
1593
+ // may not even list any more. Dropped here so the two panels never disagree.
1594
+ this.dataGridDataset.loadStarted.pipe(takeUntil(this.destroy$)).subscribe(() => {
1595
+ this.selectedServiceID.set(undefined);
1596
+ });
1597
+ }
1598
+ //#endregion
1599
+ //#region Event handlers
1600
+ //#endregion
1601
+ //#region Public methods
1602
+ selectedRowChanged() {
1603
+ super.selectedRowChanged();
1604
+ if (this.dataGridDataset.hasSelectedRows) {
1605
+ const selectedKey = this.dataGridDataset.selectedRowKeys[0];
1606
+ this.selectedServiceID.set(this.dataGridDataset.getRowID(selectedKey));
1607
+ }
1608
+ }
1609
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ServicesHistoryChildListComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1610
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.7", type: ServicesHistoryChildListComponent, isStandalone: true, selector: "shared-services-history-child-list", inputs: { controllerName: "controllerName" }, outputs: { filtersChanged: "filtersChanged" }, viewQueries: [{ propertyName: "serviceTemplate", first: true, predicate: ["serviceTemplate"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<lib-data-grid [lazyLoadRows]=\"true\"> </lib-data-grid>\r\n\r\n<ng-template #serviceTemplate let-rowData=\"rowData\">\r\n <div class=\"service-content\">\r\n <div class=\"icon\">\r\n <i class=\"fa-solid fa-circle fa-lg\"></i>\r\n </div>\r\n <div class=\"information\">\r\n <div class=\"service-name\">\r\n {{ rowData.name }}\r\n </div>\r\n <div class=\"details\">\r\n <span class=\"datetime\"\r\n ><i class=\"fa-solid fa-clock\"></i>\r\n {{ rowData.changedOn | utcDate | date: ('Format-DateTime' | translate) }}</span\r\n >\r\n <span class=\"user\"><i class=\"fa-solid fa-user\"></i> {{ rowData.changedByName }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n</ng-template>\r\n", styles: ["@layer properties;:host{width:100%;max-width:100%;min-width:100%}lib-data-grid{height:100%}lib-data-grid ::ng-deep .body lib-data-grid-row{padding:0!important}.service-content{display:flex;flex-grow:1;overflow:hidden;padding-inline:calc(var(--spacing, .25rem) * 3)}.service-content .icon{position:relative;margin-right:calc(var(--spacing, .25rem) * 3);display:flex;align-items:center;color:var(--color-blue-500, oklch(62.3% .214 259.815))}.service-content .icon:before,.service-content .icon:after{position:absolute;left:50%;margin-left:calc(var(--spacing, .25rem) * -1);height:50%;width:calc(var(--spacing, .25rem) * 2);background-color:color-mix(in srgb,oklch(86.9% .022 252.894) 50%,transparent);--tw-content: \"\";content:var(--tw-content)}@supports (color: color-mix(in lab,red,red)){.service-content .icon:before,.service-content .icon:after{background-color:color-mix(in oklab,var(--color-slate-300, oklch(86.9% .022 252.894)) 50%,transparent)}}.service-content .icon:before{top:0}.service-content .icon:after{top:50%}.service-content .icon i{z-index:1}.service-content .information{display:flex;flex-grow:1;flex-direction:column;overflow:hidden;padding-block:var(--spacing, .25rem)}.service-content .information .service-name{--tw-font-weight: var(--font-weight-bold, 700);font-weight:var(--font-weight-bold, 700)}.service-content .information .details{display:flex;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)))}.service-content .information .details i{color:var(--color-slate-400, oklch(70.4% .04 256.788))}.service-content .information .details .datetime{margin-right:calc(var(--spacing, .25rem) * 3)}.service-content .information .details .user{overflow:hidden;text-overflow:ellipsis}@property --tw-content{syntax: \"*\"; inherits: false; initial-value: \"\";}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-content: \"\";--tw-font-weight: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: DataGridComponent, selector: "lib-data-grid", inputs: ["disabled", "lazyLoadRows", "showButtons"] }, { kind: "pipe", type: DatePipe, name: "date" }, { kind: "pipe", type: UtcDatePipe, name: "utcDate" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1611
+ }
1612
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ServicesHistoryChildListComponent, decorators: [{
1613
+ type: Component,
1614
+ args: [{ selector: 'shared-services-history-child-list', imports: [DataGridComponent, DatePipe, UtcDatePipe, TranslatePipe], template: "<lib-data-grid [lazyLoadRows]=\"true\"> </lib-data-grid>\r\n\r\n<ng-template #serviceTemplate let-rowData=\"rowData\">\r\n <div class=\"service-content\">\r\n <div class=\"icon\">\r\n <i class=\"fa-solid fa-circle fa-lg\"></i>\r\n </div>\r\n <div class=\"information\">\r\n <div class=\"service-name\">\r\n {{ rowData.name }}\r\n </div>\r\n <div class=\"details\">\r\n <span class=\"datetime\"\r\n ><i class=\"fa-solid fa-clock\"></i>\r\n {{ rowData.changedOn | utcDate | date: ('Format-DateTime' | translate) }}</span\r\n >\r\n <span class=\"user\"><i class=\"fa-solid fa-user\"></i> {{ rowData.changedByName }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n</ng-template>\r\n", styles: ["@layer properties;:host{width:100%;max-width:100%;min-width:100%}lib-data-grid{height:100%}lib-data-grid ::ng-deep .body lib-data-grid-row{padding:0!important}.service-content{display:flex;flex-grow:1;overflow:hidden;padding-inline:calc(var(--spacing, .25rem) * 3)}.service-content .icon{position:relative;margin-right:calc(var(--spacing, .25rem) * 3);display:flex;align-items:center;color:var(--color-blue-500, oklch(62.3% .214 259.815))}.service-content .icon:before,.service-content .icon:after{position:absolute;left:50%;margin-left:calc(var(--spacing, .25rem) * -1);height:50%;width:calc(var(--spacing, .25rem) * 2);background-color:color-mix(in srgb,oklch(86.9% .022 252.894) 50%,transparent);--tw-content: \"\";content:var(--tw-content)}@supports (color: color-mix(in lab,red,red)){.service-content .icon:before,.service-content .icon:after{background-color:color-mix(in oklab,var(--color-slate-300, oklch(86.9% .022 252.894)) 50%,transparent)}}.service-content .icon:before{top:0}.service-content .icon:after{top:50%}.service-content .icon i{z-index:1}.service-content .information{display:flex;flex-grow:1;flex-direction:column;overflow:hidden;padding-block:var(--spacing, .25rem)}.service-content .information .service-name{--tw-font-weight: var(--font-weight-bold, 700);font-weight:var(--font-weight-bold, 700)}.service-content .information .details{display:flex;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)))}.service-content .information .details i{color:var(--color-slate-400, oklch(70.4% .04 256.788))}.service-content .information .details .datetime{margin-right:calc(var(--spacing, .25rem) * 3)}.service-content .information .details .user{overflow:hidden;text-overflow:ellipsis}@property --tw-content{syntax: \"*\"; inherits: false; initial-value: \"\";}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-content: \"\";--tw-font-weight: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
1615
+ }], propDecorators: { serviceTemplate: [{ type: i0.ViewChild, args: ['serviceTemplate', { isSignal: true }] }], controllerName: [{
1616
+ type: Input
1617
+ }], filtersChanged: [{ type: i0.Output, args: ["filtersChanged"] }] } });
1618
+
1619
+ class ServicesHistoryFilterComponent extends FiltersBase {
1620
+ //#region ViewChilds, Inputs, Outputs
1621
+ /**
1622
+ * Catalog that resolves the people who can appear as the author of a change.
1623
+ *
1624
+ * There is no such catalog in this library -- who a user is belongs to the application -- so the
1625
+ * author filter is offered only when one is supplied.
1626
+ */
1627
+ usersCatalogEndpoint = input(/* @ts-ignore */
1628
+ ...(ngDevMode ? [undefined, { debugName: "usersCatalogEndpoint" }] : /* istanbul ignore next */ []));
1629
+ //#endregion
1630
+ //#region Variables
1631
+ //#endregion
1632
+ //#region Properties
1633
+ hasUsersCatalog = computed(() => !!this.usersCatalogEndpoint(), /* @ts-ignore */
1634
+ ...(ngDevMode ? [{ debugName: "hasUsersCatalog" }] : /* istanbul ignore next */ []));
1635
+ get changedOnFromControl() {
1636
+ return this.filterForm.get('changedOnFrom');
1637
+ }
1638
+ get changedOnToControl() {
1639
+ return this.filterForm.get('changedOnTo');
1640
+ }
1641
+ //#endregion
1642
+ //#region Constructor and Angular life cycle methods
1643
+ ngOnInit() {
1644
+ super.ngOnInit();
1645
+ // A range that reads backwards returns nothing and explains nothing, so the bound that did not
1646
+ // just change follows the one that did, rather than leaving the person to spot it.
1647
+ this.changedOnFromControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((from) => {
1648
+ const to = this.changedOnToControl.value;
1649
+ if (!!from && !!to && to < from) {
1650
+ this.changedOnToControl.setValue(from);
1651
+ }
1652
+ });
1653
+ this.changedOnToControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((to) => {
1654
+ const from = this.changedOnFromControl.value;
1655
+ if (!!from && !!to && to < from) {
1656
+ this.changedOnFromControl.setValue(to);
1657
+ }
1658
+ });
1659
+ }
1660
+ //#endregion
1661
+ //#region Event handlers
1662
+ //#endregion
1663
+ //#region Public methods
1664
+ //#endregion
1665
+ //#region Private methods
1666
+ /**
1667
+ * The control names are the filter names the backend reads, case insensitively: `name`,
1668
+ * `changedByID`, `changedOnFrom`, `changedOnTo` and `onlyCurrentEntity` reach `AuditFilters` in
1669
+ * ZWebAPI. The two dates are sent as picked and turned into UTC instants by
1670
+ * {@link ServicesHistoryDataset}, which is the boundary where the request is built.
1671
+ */
1672
+ formSetup() {
1673
+ return this.formBuilder.group({
1674
+ changedByID: [null],
1675
+ changedOnFrom: [null],
1676
+ changedOnTo: [null],
1677
+ name: [null],
1678
+ onlyCurrentEntity: [false, { nonNullable: true }],
1679
+ });
1680
+ }
1681
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ServicesHistoryFilterComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1682
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: ServicesHistoryFilterComponent, isStandalone: true, selector: "shared-services-history-filter", inputs: { usersCatalogEndpoint: { classPropertyName: "usersCatalogEndpoint", publicName: "usersCatalogEndpoint", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: "<framework-button-filters modalSize=\"2xl\" modalTitle=\"ServicesHistory-Filters-Title\" [formGroup]=\"filterForm\">\r\n <lib-form-group label=\"ServicesHistory-FormGroup-Service\">\r\n <lib-form-input-group controlName=\"name\" label=\"ServicesHistory-Field-Name\" [autofocus]=\"true\" [maxLength]=\"200\">\r\n </lib-form-input-group>\r\n\r\n @if (hasUsersCatalog()) {\r\n <lib-catalog-select\r\n controlName=\"changedByID\"\r\n displayControlName=\"changedByName\"\r\n label=\"ServicesHistory-Field-ChangedBy\"\r\n [readOnly]=\"false\"\r\n [searchEndpoint]=\"usersCatalogEndpoint()!\"\r\n >\r\n </lib-catalog-select>\r\n }\r\n </lib-form-group>\r\n\r\n <lib-form-group label=\"ServicesHistory-FormGroup-Period\">\r\n <lib-form-input-group controlName=\"changedOnFrom\" label=\"ServicesHistory-Field-ChangedOnFrom\" [type]=\"'date'\">\r\n </lib-form-input-group>\r\n\r\n <lib-form-input-group controlName=\"changedOnTo\" label=\"ServicesHistory-Field-ChangedOnTo\" [type]=\"'date'\">\r\n </lib-form-input-group>\r\n </lib-form-group>\r\n\r\n <lib-form-group label=\"ServicesHistory-FormGroup-Operations\">\r\n <lib-form-input-group\r\n controlName=\"onlyCurrentEntity\"\r\n label=\"ServicesHistory-Field-OnlyCurrentEntity\"\r\n notes=\"ServicesHistory-Field-OnlyCurrentEntity-Notes\"\r\n [type]=\"'checkbox'\"\r\n >\r\n </lib-form-input-group>\r\n </lib-form-group>\r\n</framework-button-filters>\r\n", styles: [""], dependencies: [{ kind: "component", type: ButtonFiltersComponent, selector: "framework-button-filters", inputs: ["modalSize", "modalTitle", "validateFormFunction"] }, { kind: "component", type: CatalogSelectComponent, selector: "lib-catalog-select", inputs: ["autofocus", "controlName", "displayControlName", "displayProperty", "entriesList", "filters", "label", "maxEntries", "minimumLengthSearch", "notes", "readOnly", "searchable", "searchEndpoint", "validations", "valueProperty"] }, { kind: "component", type: FormGroupComponent, selector: "lib-form-group", inputs: ["shouldExpand", "label"] }, { kind: "component", type: FormInputGroupComponent, selector: "lib-form-input-group", inputs: ["autofocus", "alignContent", "controlName", "fixedValue", "format", "label", "isDisabled", "isFullHeight", "maxLength", "min", "notes", "readOnly", "rows", "showLabel", "step", "template", "type", "validations"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
1683
+ }
1684
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ServicesHistoryFilterComponent, decorators: [{
1685
+ type: Component,
1686
+ args: [{ selector: 'shared-services-history-filter', imports: [
1687
+ ButtonFiltersComponent,
1688
+ CatalogSelectComponent,
1689
+ FormGroupComponent,
1690
+ FormInputGroupComponent,
1691
+ FormsModule,
1692
+ ReactiveFormsModule,
1693
+ ], template: "<framework-button-filters modalSize=\"2xl\" modalTitle=\"ServicesHistory-Filters-Title\" [formGroup]=\"filterForm\">\r\n <lib-form-group label=\"ServicesHistory-FormGroup-Service\">\r\n <lib-form-input-group controlName=\"name\" label=\"ServicesHistory-Field-Name\" [autofocus]=\"true\" [maxLength]=\"200\">\r\n </lib-form-input-group>\r\n\r\n @if (hasUsersCatalog()) {\r\n <lib-catalog-select\r\n controlName=\"changedByID\"\r\n displayControlName=\"changedByName\"\r\n label=\"ServicesHistory-Field-ChangedBy\"\r\n [readOnly]=\"false\"\r\n [searchEndpoint]=\"usersCatalogEndpoint()!\"\r\n >\r\n </lib-catalog-select>\r\n }\r\n </lib-form-group>\r\n\r\n <lib-form-group label=\"ServicesHistory-FormGroup-Period\">\r\n <lib-form-input-group controlName=\"changedOnFrom\" label=\"ServicesHistory-Field-ChangedOnFrom\" [type]=\"'date'\">\r\n </lib-form-input-group>\r\n\r\n <lib-form-input-group controlName=\"changedOnTo\" label=\"ServicesHistory-Field-ChangedOnTo\" [type]=\"'date'\">\r\n </lib-form-input-group>\r\n </lib-form-group>\r\n\r\n <lib-form-group label=\"ServicesHistory-FormGroup-Operations\">\r\n <lib-form-input-group\r\n controlName=\"onlyCurrentEntity\"\r\n label=\"ServicesHistory-Field-OnlyCurrentEntity\"\r\n notes=\"ServicesHistory-Field-OnlyCurrentEntity-Notes\"\r\n [type]=\"'checkbox'\"\r\n >\r\n </lib-form-input-group>\r\n </lib-form-group>\r\n</framework-button-filters>\r\n" }]
1694
+ }], propDecorators: { usersCatalogEndpoint: [{ type: i0.Input, args: [{ isSignal: true, alias: "usersCatalogEndpoint", required: false }] }] } });
1695
+
1696
+ class ServicesHistoryViewComponent extends TabViewBase {
1697
+ //#region ViewChilds, Inputs, Outputs
1698
+ // Models rather than inputs: the constructor fills the first two from route data when a host
1699
+ // has not bound them, and the entity id arrives from the data provider's stream.
1700
+ controllerName = model('', /* @ts-ignore */
1701
+ ...(ngDevMode ? [{ debugName: "controllerName" }] : /* istanbul ignore next */ []));
1702
+ entityID = model(/* @ts-ignore */
1703
+ ...(ngDevMode ? [undefined, { debugName: "entityID" }] : /* istanbul ignore next */ []));
1704
+ /**
1705
+ * Catalog resolving the people who can appear as the author of a change, for the filter.
1706
+ *
1707
+ * Supplied through the route's `usersCatalogEndpoint` data, or bound directly. Without it the
1708
+ * author filter is not offered -- see {@link ServicesHistoryFilterComponent}.
1709
+ */
1710
+ usersCatalogEndpoint = model(/* @ts-ignore */
1711
+ ...(ngDevMode ? [undefined, { debugName: "usersCatalogEndpoint" }] : /* istanbul ignore next */ []));
1712
+ //#endregion
1713
+ //#region Variables
1714
+ /**
1715
+ * The filters applied to the services grid, forwarded to the operations grid.
1716
+ *
1717
+ * Only `onlyCurrentEntity` means anything to the operations list, and the backend reads only what
1718
+ * it recognises, so the set is passed whole rather than picked apart here: a filter added later
1719
+ * then reaches both lists without this having to learn about it.
1720
+ */
1721
+ operationsFilters = signal(undefined, /* @ts-ignore */
1722
+ ...(ngDevMode ? [{ debugName: "operationsFilters" }] : /* istanbul ignore next */ []));
1723
+ activatedRoute = inject(ActivatedRoute);
1724
+ dataProviderService = inject(DataProviderService, { optional: true });
1725
+ //#endregion
1726
+ //#region Properties
1727
+ //#endregion
1728
+ //#region Constructor and Angular life cycle methods
1729
+ constructor() {
1730
+ super();
1731
+ if (this.activatedRoute.snapshot.data['controllerName']) {
1732
+ this.controllerName.set(this.activatedRoute.snapshot.data['controllerName']);
1733
+ }
1734
+ if (this.activatedRoute.snapshot.data['usersCatalogEndpoint']) {
1735
+ this.usersCatalogEndpoint.set(this.activatedRoute.snapshot.data['usersCatalogEndpoint']);
1736
+ }
1737
+ if (this.dataProviderService) {
1738
+ this.dataProviderService
1739
+ .getModel$()
1740
+ .pipe(take(1))
1741
+ .subscribe((model) => {
1742
+ this.entityID.set(model.id);
1743
+ });
1744
+ }
1745
+ }
1746
+ //#endregion
1747
+ //#region Event handlers
1748
+ onFiltersChanged(filters) {
1749
+ this.operationsFilters.set(filters);
1750
+ }
1751
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ServicesHistoryViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1752
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: ServicesHistoryViewComponent, isStandalone: true, selector: "shared-services-history-view", inputs: { controllerName: { classPropertyName: "controllerName", publicName: "controllerName", isSignal: true, isRequired: false, transformFunction: null }, entityID: { classPropertyName: "entityID", publicName: "entityID", isSignal: true, isRequired: false, transformFunction: null }, usersCatalogEndpoint: { classPropertyName: "usersCatalogEndpoint", publicName: "usersCatalogEndpoint", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { controllerName: "controllerNameChange", entityID: "entityIDChange", usersCatalogEndpoint: "usersCatalogEndpointChange" }, providers: [{ provide: DataGridDataset, useClass: ServicesHistoryDataset }], usesInheritance: true, ngImport: i0, template: "<ng-template #ribbon>\r\n <lib-ribbon-group [label]=\"'RibbonGroup-General' | translate\">\r\n <framework-button-refresh></framework-button-refresh>\r\n\r\n <shared-services-history-filter [usersCatalogEndpoint]=\"usersCatalogEndpoint()\"> </shared-services-history-filter>\r\n </lib-ribbon-group>\r\n</ng-template>\r\n\r\n<div class=\"history-container\">\r\n <div class=\"service-history-container\">\r\n <shared-services-history-child-list\r\n #service\r\n [controllerName]=\"controllerName()\"\r\n (filtersChanged)=\"onFiltersChanged($event)\"\r\n >\r\n </shared-services-history-child-list>\r\n </div>\r\n\r\n <div class=\"operations-history-container\">\r\n <shared-operations-history-child-list\r\n [controllerName]=\"controllerName()\"\r\n [filters]=\"operationsFilters()\"\r\n [serviceID]=\"service.selectedServiceID()\"\r\n >\r\n </shared-operations-history-child-list>\r\n </div>\r\n</div>\r\n", styles: [":host{display:flex;flex-grow:1;overflow:hidden}.history-container{display:flex;flex-grow:1;overflow:hidden;padding:calc(var(--spacing, .25rem) * 2)}.history-container .service-history-container,.history-container .operations-history-container{display:flex;flex-grow:1;overflow:hidden}.history-container .service-history-container lib-data-grid,.history-container .operations-history-container lib-data-grid{flex-grow:1}.history-container .service-history-container{width:calc(2 / 6 * 100%);padding-right:var(--spacing, .25rem)}.history-container .service-history-container ::ng-deep lib-data-grid-row:first-child .icon:before{display:none}.history-container .service-history-container ::ng-deep lib-data-grid-row:last-child .icon:after{display:none}.history-container .operations-history-container{width:calc(4 / 6 * 100%);padding-left:var(--spacing, .25rem)}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: ButtonRefreshComponent, selector: "framework-button-refresh" }, { kind: "component", type: OperationsHistoryChildListComponent, selector: "shared-operations-history-child-list", inputs: ["controllerName", "filters", "serviceID"] }, { kind: "component", type: RibbonGroupComponent, selector: "lib-ribbon-group", inputs: ["label"] }, { kind: "component", type: ServicesHistoryChildListComponent, selector: "shared-services-history-child-list", inputs: ["controllerName"], outputs: ["filtersChanged"] }, { kind: "component", type: ServicesHistoryFilterComponent, selector: "shared-services-history-filter", inputs: ["usersCatalogEndpoint"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1753
+ }
1754
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ServicesHistoryViewComponent, decorators: [{
1755
+ type: Component,
1756
+ args: [{ selector: 'shared-services-history-view', imports: [
1757
+ ButtonRefreshComponent,
1758
+ OperationsHistoryChildListComponent,
1759
+ RibbonGroupComponent,
1760
+ ServicesHistoryChildListComponent,
1761
+ ServicesHistoryFilterComponent,
1762
+ TranslatePipe,
1763
+ ], providers: [{ provide: DataGridDataset, useClass: ServicesHistoryDataset }], template: "<ng-template #ribbon>\r\n <lib-ribbon-group [label]=\"'RibbonGroup-General' | translate\">\r\n <framework-button-refresh></framework-button-refresh>\r\n\r\n <shared-services-history-filter [usersCatalogEndpoint]=\"usersCatalogEndpoint()\"> </shared-services-history-filter>\r\n </lib-ribbon-group>\r\n</ng-template>\r\n\r\n<div class=\"history-container\">\r\n <div class=\"service-history-container\">\r\n <shared-services-history-child-list\r\n #service\r\n [controllerName]=\"controllerName()\"\r\n (filtersChanged)=\"onFiltersChanged($event)\"\r\n >\r\n </shared-services-history-child-list>\r\n </div>\r\n\r\n <div class=\"operations-history-container\">\r\n <shared-operations-history-child-list\r\n [controllerName]=\"controllerName()\"\r\n [filters]=\"operationsFilters()\"\r\n [serviceID]=\"service.selectedServiceID()\"\r\n >\r\n </shared-operations-history-child-list>\r\n </div>\r\n</div>\r\n", styles: [":host{display:flex;flex-grow:1;overflow:hidden}.history-container{display:flex;flex-grow:1;overflow:hidden;padding:calc(var(--spacing, .25rem) * 2)}.history-container .service-history-container,.history-container .operations-history-container{display:flex;flex-grow:1;overflow:hidden}.history-container .service-history-container lib-data-grid,.history-container .operations-history-container lib-data-grid{flex-grow:1}.history-container .service-history-container{width:calc(2 / 6 * 100%);padding-right:var(--spacing, .25rem)}.history-container .service-history-container ::ng-deep lib-data-grid-row:first-child .icon:before{display:none}.history-container .service-history-container ::ng-deep lib-data-grid-row:last-child .icon:after{display:none}.history-container .operations-history-container{width:calc(4 / 6 * 100%);padding-left:var(--spacing, .25rem)}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
1764
+ }], ctorParameters: () => [], propDecorators: { controllerName: [{ type: i0.Input, args: [{ isSignal: true, alias: "controllerName", required: false }] }, { type: i0.Output, args: ["controllerNameChange"] }], entityID: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityID", required: false }] }, { type: i0.Output, args: ["entityIDChange"] }], usersCatalogEndpoint: [{ type: i0.Input, args: [{ isSignal: true, alias: "usersCatalogEndpoint", required: false }] }, { type: i0.Output, args: ["usersCatalogEndpointChange"] }] } });
1765
+
1766
+ const ZAMBON_SHARED_I18N_ASSET_PATH = 'assets/i18n/zambon-dev/shared';
1767
+ const ZAMBON_SHARED_I18N_ASSET = {
1768
+ glob: '**/*.json',
1769
+ input: 'node_modules/@zambon-dev/ui/i18n/shared',
1770
+ output: ZAMBON_SHARED_I18N_ASSET_PATH,
1771
+ };
1772
+ const ZAMBON_SHARED_I18N_RESOURCES = [
1773
+ { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/external-content/`, suffix: '.json' },
1774
+ { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/language-selector/`, suffix: '.json' },
1775
+ { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/login/`, suffix: '.json' },
1776
+ { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/operations-history/`, suffix: '.json' },
1777
+ { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/services-history/`, suffix: '.json' },
1778
+ { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/top-bar/`, suffix: '.json' },
1779
+ ];
1780
+ function createZambonSharedTranslateLoader(httpBackend, appResources = []) {
1781
+ return new MultiTranslateHttpLoader(httpBackend, [...ZAMBON_SHARED_I18N_RESOURCES, ...appResources]);
1782
+ }
1783
+ function provideZambonSharedTranslateLoader(appResources = []) {
1784
+ return {
1785
+ provide: TranslateLoader,
1786
+ useFactory: (httpBackend) => createZambonSharedTranslateLoader(httpBackend, appResources),
1787
+ deps: [HttpBackend],
1788
+ };
1789
+ }
1790
+ function provideZambonSharedTranslateService(config = {}) {
1791
+ return provideTranslateService({
1792
+ defaultLanguage: config.defaultLanguage,
1793
+ loader: provideZambonSharedTranslateLoader(config.appResources),
1794
+ });
1795
+ }
1796
+
1797
+ /**
1798
+ * Generated bundle index. Do not edit.
1799
+ */
1800
+
1801
+ export { AuthGuard, AuthInterceptor, AuthenticationService, BrandComponent, BypassHtmlSanitizerPipe, EXTERNAL_CONTENT_CONFIGS, EXTERNAL_CONTENT_ROUTE_PATH, EnumLabelPipe, EnvironmentBadgeComponent, ExternalContentComponent, ExternalContentConfigs, ExternalContentService, ExternalUrlResolverService, FiltersBase, LoginLayoutComponent, MainLayoutComponent, NotificationsComponent, NotificationsService, OperationsHistoryChildListComponent, OperationsHistoryDataset, OperationsHistoryModalComponent, OperationsHistoryService, ServicesHistoryChildListComponent, ServicesHistoryDataset, ServicesHistoryFilterComponent, ServicesHistoryService, ServicesHistoryViewComponent, SharedAuthModule, TopBarComponent, UserProfileComponent, UtcDatePipe, ZAMBON_SHARED_I18N_ASSET, ZAMBON_SHARED_I18N_ASSET_PATH, ZAMBON_SHARED_I18N_RESOURCES, createZambonSharedTranslateLoader, externalContentRoutes, provideZambonSharedTranslateLoader, provideZambonSharedTranslateService };
1802
+ //# sourceMappingURL=zambon-dev-ui-shared.mjs.map