@flusys/ng-layout 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2233 @@
1
+ import * as i1 from '@angular/common';
2
+ import { isPlatformBrowser, CommonModule } from '@angular/common';
3
+ import * as i0 from '@angular/core';
4
+ import { inject, PLATFORM_ID, Injectable, DOCUMENT, signal, computed, effect, Component, InjectionToken, DestroyRef, input, Renderer2, ViewChild } from '@angular/core';
5
+ import * as i2 from '@angular/forms';
6
+ import { FormsModule } from '@angular/forms';
7
+ import * as i1$2 from '@angular/router';
8
+ import { Router, RouterModule, NavigationEnd } from '@angular/router';
9
+ import { updatePreset, updateSurfacePalette, $t, definePreset } from '@primeuix/themes';
10
+ import Aura from '@primeuix/themes/aura';
11
+ import Lara from '@primeuix/themes/lara';
12
+ import Nora from '@primeuix/themes/nora';
13
+ import { PrimeNG } from 'primeng/config';
14
+ import * as i3 from 'primeng/selectbutton';
15
+ import { SelectButtonModule } from 'primeng/selectbutton';
16
+ import * as i2$2 from '@flusys/ng-shared';
17
+ import { evaluateLogicNode, PermissionValidatorService, PlatformService, AngularModule, IconComponent } from '@flusys/ng-shared';
18
+ import { Subject, filter as filter$1 } from 'rxjs';
19
+ import * as i1$1 from 'primeng/button';
20
+ import { ButtonModule } from 'primeng/button';
21
+ import * as i2$1 from 'primeng/styleclass';
22
+ import { StyleClassModule } from 'primeng/styleclass';
23
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
24
+ import { APP_CONFIG, isCompanyFeatureEnabled } from '@flusys/ng-core';
25
+ import { MessageService } from 'primeng/api';
26
+ import * as i4 from 'primeng/select';
27
+ import { SelectModule } from 'primeng/select';
28
+ import * as i2$3 from 'primeng/ripple';
29
+ import { RippleModule } from 'primeng/ripple';
30
+ import { filter } from 'rxjs/operators';
31
+ import Material from '@primeuix/themes/material';
32
+
33
+ /** Filter launcher apps based on user permission codes */
34
+ function filterAppsByPermissions(apps, permissionCodes) {
35
+ return apps.filter((app) => !app.permissionLogic ||
36
+ evaluateLogicNode(app.permissionLogic, permissionCodes));
37
+ }
38
+
39
+ /**
40
+ * Filter menu items based on permission and role checker
41
+ *
42
+ * @param items - Array of menu items to filter
43
+ * @param hasPermission - Function to check if user has permission
44
+ * @param hasRole - Function to check if user has role (optional, defaults to always false)
45
+ * @returns Filtered menu items array
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const filteredMenu = filterMenuByPermissions(
50
+ * menuItems,
51
+ * (actionCode) => permissionState.hasAction(actionCode)
52
+ * );
53
+ * ```
54
+ */
55
+ function filterMenuByPermissions(items, permissionCode) {
56
+ return items
57
+ .map((item) => filterMenuItem(item, permissionCode))
58
+ .filter((item) => item !== null);
59
+ }
60
+ /**
61
+ * Filter a single menu item and its children recursively
62
+ *
63
+ * @param item - Menu item to filter
64
+ * @param hasPermission - Function to check if user has permission
65
+ * @param hasRole - Function to check if user has role
66
+ * @returns Filtered menu item or null if user doesn't have permission
67
+ */
68
+ function filterMenuItem(item, permissionCode) {
69
+ // Skip separators - they have no permission checks
70
+ if (item.separator) {
71
+ return item;
72
+ }
73
+ // Check permission logic tree
74
+ if (item.permissionLogic) {
75
+ const hasAccess = evaluateLogicNode(item.permissionLogic, permissionCode);
76
+ if (!hasAccess) {
77
+ return null; // User doesn't pass permission logic
78
+ }
79
+ }
80
+ // Filter child items recursively
81
+ if (item.children && item.children.length > 0) {
82
+ const filteredChildren = filterMenuByPermissions(item.children, permissionCode);
83
+ // If parent has no visible children, hide parent too
84
+ if (filteredChildren.length === 0) {
85
+ return null;
86
+ }
87
+ return { ...item, children: filteredChildren };
88
+ }
89
+ return item;
90
+ }
91
+
92
+ const STORAGE_KEY = 'flusys.layout.config';
93
+ const STORAGE_VERSION = 1;
94
+ /**
95
+ * Service for persisting layout configuration to localStorage.
96
+ * Handles loading, saving, and validation of layout preferences with SSR-safe checks.
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const persistence = inject(LayoutPersistenceService);
101
+ *
102
+ * // Load saved config
103
+ * const config = persistence.load();
104
+ *
105
+ * // Save config
106
+ * persistence.save({ darkTheme: true, preset: 'Aura' });
107
+ *
108
+ * // Clear saved config
109
+ * persistence.clear();
110
+ * ```
111
+ */
112
+ class LayoutPersistenceService {
113
+ platformId = inject(PLATFORM_ID);
114
+ isBrowser = isPlatformBrowser(this.platformId);
115
+ validPresets = ['Aura', 'Lara', 'Nora'];
116
+ validMenuModes = ['static', 'overlay'];
117
+ /**
118
+ * Load configuration from localStorage.
119
+ * Returns null if no saved config or invalid data.
120
+ * SSR-safe (returns null on server).
121
+ *
122
+ * @returns Validated layout configuration or null
123
+ */
124
+ load() {
125
+ if (!this.isBrowser)
126
+ return null;
127
+ try {
128
+ const stored = localStorage.getItem(STORAGE_KEY);
129
+ if (!stored)
130
+ return null;
131
+ const parsed = JSON.parse(stored);
132
+ return this.validateConfig(parsed);
133
+ }
134
+ catch (error) {
135
+ console.warn('[LayoutPersistence] Failed to load config:', error);
136
+ this.clear();
137
+ return null;
138
+ }
139
+ }
140
+ /**
141
+ * Save configuration to localStorage.
142
+ * SSR-safe (no-op on server).
143
+ *
144
+ * @param config Layout configuration to save
145
+ */
146
+ save(config) {
147
+ if (!this.isBrowser)
148
+ return;
149
+ try {
150
+ const toStore = {
151
+ ...config,
152
+ _version: STORAGE_VERSION,
153
+ };
154
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
155
+ }
156
+ catch (error) {
157
+ console.warn('[LayoutPersistence] Failed to save config:', error);
158
+ }
159
+ }
160
+ /**
161
+ * Clear stored configuration.
162
+ * SSR-safe (no-op on server).
163
+ */
164
+ clear() {
165
+ if (!this.isBrowser)
166
+ return;
167
+ localStorage.removeItem(STORAGE_KEY);
168
+ }
169
+ /**
170
+ * Validate and sanitize configuration.
171
+ * Returns null if version mismatch.
172
+ *
173
+ * @param config Configuration to validate
174
+ * @returns Validated configuration or null
175
+ */
176
+ validateConfig(config) {
177
+ // Version mismatch - clear and return null
178
+ if (config._version && config._version !== STORAGE_VERSION) {
179
+ console.warn('[LayoutPersistence] Version mismatch, clearing config');
180
+ this.clear();
181
+ return null;
182
+ }
183
+ // Validate preset
184
+ if (config.preset && !this.validPresets.includes(config.preset)) {
185
+ console.warn(`[LayoutPersistence] Invalid preset "${config.preset}", using Aura`);
186
+ config.preset = 'Aura';
187
+ }
188
+ // Validate menuMode
189
+ if (config.menuMode && !this.validMenuModes.includes(config.menuMode)) {
190
+ console.warn(`[LayoutPersistence] Invalid menuMode "${config.menuMode}", using static`);
191
+ config.menuMode = 'static';
192
+ }
193
+ // Validate darkTheme
194
+ if (typeof config.darkTheme !== 'boolean') {
195
+ config.darkTheme = false;
196
+ }
197
+ // Remove version field from returned config
198
+ const { _version, ...cleanConfig } = config;
199
+ return cleanConfig;
200
+ }
201
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: LayoutPersistenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
202
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: LayoutPersistenceService, providedIn: 'root' });
203
+ }
204
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: LayoutPersistenceService, decorators: [{
205
+ type: Injectable,
206
+ args: [{
207
+ providedIn: 'root',
208
+ }]
209
+ }] });
210
+
211
+ /**
212
+ * Service managing layout configuration and state.
213
+ * Provides signals for reactive layout updates.
214
+ */
215
+ class LayoutService {
216
+ document = inject(DOCUMENT);
217
+ platformId = inject(PLATFORM_ID);
218
+ isBrowser = isPlatformBrowser(this.platformId);
219
+ persistence = inject(LayoutPersistenceService);
220
+ defaultConfig = {
221
+ preset: 'Aura',
222
+ primary: 'emerald',
223
+ surface: null,
224
+ darkTheme: false,
225
+ menuMode: 'static',
226
+ };
227
+ // Load persisted config merged with defaults
228
+ initialConfig = (() => {
229
+ const persisted = this.persistence.load();
230
+ return persisted
231
+ ? { ...this.defaultConfig, ...persisted }
232
+ : this.defaultConfig;
233
+ })();
234
+ defaultState = {
235
+ staticMenuDesktopInactive: false,
236
+ overlayMenuActive: false,
237
+ configSidebarVisible: false,
238
+ staticMenuMobileActive: false,
239
+ menuHoverActive: false,
240
+ };
241
+ // Signals
242
+ layoutConfig = signal(this.initialConfig, ...(ngDevMode ? [{ debugName: "layoutConfig" }] : []));
243
+ layoutState = signal(this.defaultState, ...(ngDevMode ? [{ debugName: "layoutState" }] : []));
244
+ transitionComplete = signal(false, ...(ngDevMode ? [{ debugName: "transitionComplete" }] : []));
245
+ // User Profile Signals
246
+ userProfile = signal(null, ...(ngDevMode ? [{ debugName: "userProfile" }] : []));
247
+ companyProfile = signal(null, ...(ngDevMode ? [{ debugName: "companyProfile" }] : []));
248
+ appName = signal('FLUSYS', ...(ngDevMode ? [{ debugName: "appName" }] : []));
249
+ // App Launcher Signals
250
+ _rawApps = signal([], ...(ngDevMode ? [{ debugName: "_rawApps" }] : []));
251
+ /**
252
+ * Filtered launcher apps based on user permissions.
253
+ * Automatically recomputes when raw apps or permissions change.
254
+ */
255
+ apps = computed(() => {
256
+ const raw = this._rawApps();
257
+ const permission = this.permissionValidator.permissions();
258
+ return filterAppsByPermissions(raw, permission);
259
+ }, ...(ngDevMode ? [{ debugName: "apps" }] : []));
260
+ // Menu Signals
261
+ _rawMenu = signal([], ...(ngDevMode ? [{ debugName: "_rawMenu" }] : []));
262
+ permissionValidator = inject(PermissionValidatorService);
263
+ /**
264
+ * Filtered menu items based on user permissions.
265
+ * Automatically recomputes when raw menu or permission checker changes.
266
+ * Role checker is optional - if not set, role-based permissions will be denied.
267
+ */
268
+ menu = computed(() => {
269
+ const raw = this._rawMenu(); // Track permission changes
270
+ const permission = this.permissionValidator.permissions();
271
+ return filterMenuByPermissions(raw, permission);
272
+ }, ...(ngDevMode ? [{ debugName: "menu" }] : []));
273
+ // Computed signals - Layout
274
+ theme = computed(() => this.layoutConfig()?.darkTheme ? 'light' : 'dark', ...(ngDevMode ? [{ debugName: "theme" }] : []));
275
+ isSidebarActive = computed(() => this.layoutState().overlayMenuActive ||
276
+ this.layoutState().staticMenuMobileActive, ...(ngDevMode ? [{ debugName: "isSidebarActive" }] : []));
277
+ isDarkTheme = computed(() => this.layoutConfig().darkTheme, ...(ngDevMode ? [{ debugName: "isDarkTheme" }] : []));
278
+ getPrimary = computed(() => this.layoutConfig().primary, ...(ngDevMode ? [{ debugName: "getPrimary" }] : []));
279
+ getSurface = computed(() => this.layoutConfig().surface, ...(ngDevMode ? [{ debugName: "getSurface" }] : []));
280
+ isOverlay = computed(() => this.layoutConfig().menuMode === 'overlay', ...(ngDevMode ? [{ debugName: "isOverlay" }] : []));
281
+ // Computed signals - User Profile
282
+ userName = computed(() => this.userProfile()?.name ?? 'User', ...(ngDevMode ? [{ debugName: "userName" }] : []));
283
+ userEmail = computed(() => this.userProfile()?.email ?? '', ...(ngDevMode ? [{ debugName: "userEmail" }] : []));
284
+ userProfilePictureUrl = computed(() => this.userProfile()?.profilePictureUrl ?? null, ...(ngDevMode ? [{ debugName: "userProfilePictureUrl" }] : []));
285
+ companyName = computed(() => this.companyProfile()?.name ?? this.appName(), ...(ngDevMode ? [{ debugName: "companyName" }] : []));
286
+ companyLogoUrl = computed(() => this.companyProfile()?.logoUrl ?? null, ...(ngDevMode ? [{ debugName: "companyLogoUrl" }] : []));
287
+ isAuthenticated = computed(() => !!this.userProfile(), ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : []));
288
+ // Computed signals - App Launcher
289
+ hasApps = computed(() => this.apps().length > 0, ...(ngDevMode ? [{ debugName: "hasApps" }] : []));
290
+ // RxJS Subjects for event communication
291
+ configUpdate = new Subject();
292
+ overlayOpen = new Subject();
293
+ menuSource = new Subject();
294
+ resetSource = new Subject();
295
+ menuSource$ = this.menuSource.asObservable();
296
+ resetSource$ = this.resetSource.asObservable();
297
+ configUpdate$ = this.configUpdate.asObservable();
298
+ overlayOpen$ = this.overlayOpen.asObservable();
299
+ initialized = false;
300
+ constructor() {
301
+ effect(() => {
302
+ const config = this.layoutConfig();
303
+ if (config) {
304
+ this.onConfigUpdate();
305
+ }
306
+ });
307
+ effect(() => {
308
+ const config = this.layoutConfig();
309
+ if (!this.initialized || !config) {
310
+ this.initialized = true;
311
+ return;
312
+ }
313
+ this.handleDarkModeTransition(config);
314
+ });
315
+ // Auto-save configuration changes to localStorage
316
+ effect(() => {
317
+ const config = this.layoutConfig();
318
+ if (config && this.initialized) {
319
+ this.persistence.save(config);
320
+ }
321
+ });
322
+ }
323
+ handleDarkModeTransition(config) {
324
+ if (this.document.startViewTransition) {
325
+ this.startViewTransition(config);
326
+ }
327
+ else {
328
+ this.toggleDarkMode(config);
329
+ this.onTransitionEnd();
330
+ }
331
+ }
332
+ startViewTransition(config) {
333
+ const transition = this.document.startViewTransition(() => {
334
+ this.toggleDarkMode(config);
335
+ });
336
+ transition.ready.then(() => this.onTransitionEnd()).catch(() => { });
337
+ }
338
+ toggleDarkMode(config) {
339
+ const _config = config || this.layoutConfig();
340
+ if (_config.darkTheme) {
341
+ this.document.documentElement.classList.add('app-dark');
342
+ }
343
+ else {
344
+ this.document.documentElement.classList.remove('app-dark');
345
+ }
346
+ }
347
+ onTransitionEnd() {
348
+ this.transitionComplete.set(true);
349
+ setTimeout(() => this.transitionComplete.set(false));
350
+ }
351
+ onMenuToggle() {
352
+ if (this.isOverlay()) {
353
+ this.layoutState.update((prev) => ({
354
+ ...prev,
355
+ overlayMenuActive: !this.layoutState().overlayMenuActive,
356
+ }));
357
+ if (this.layoutState().overlayMenuActive) {
358
+ this.overlayOpen.next();
359
+ }
360
+ }
361
+ if (this.isDesktop()) {
362
+ this.layoutState.update((prev) => ({
363
+ ...prev,
364
+ staticMenuDesktopInactive: !this.layoutState().staticMenuDesktopInactive,
365
+ }));
366
+ }
367
+ else {
368
+ this.layoutState.update((prev) => ({
369
+ ...prev,
370
+ staticMenuMobileActive: !this.layoutState().staticMenuMobileActive,
371
+ }));
372
+ if (this.layoutState().staticMenuMobileActive) {
373
+ this.overlayOpen.next();
374
+ }
375
+ }
376
+ }
377
+ isDesktop() {
378
+ return this.isBrowser ? window.innerWidth > 991 : true;
379
+ }
380
+ isMobile() {
381
+ return !this.isDesktop();
382
+ }
383
+ onConfigUpdate() {
384
+ this.configUpdate.next(this.layoutConfig());
385
+ }
386
+ onMenuStateChange(event) {
387
+ this.menuSource.next(event);
388
+ }
389
+ reset() {
390
+ this.resetSource.next(true);
391
+ }
392
+ // ==========================================================================
393
+ // User Profile Methods
394
+ // ==========================================================================
395
+ /**
396
+ * Set the current user profile for display in layout.
397
+ * Called by auth integration to sync user data.
398
+ */
399
+ setUserProfile(profile) {
400
+ this.userProfile.set(profile);
401
+ }
402
+ /**
403
+ * Set the current company profile for display in layout.
404
+ * Called by auth integration to sync company data.
405
+ */
406
+ setCompanyProfile(profile) {
407
+ this.companyProfile.set(profile);
408
+ }
409
+ // ==========================================================================
410
+ // Menu Methods
411
+ // ==========================================================================
412
+ /**
413
+ * Set the raw menu items (unfiltered).
414
+ * Menu will be automatically filtered based on permission checker.
415
+ * Called by app initialization to set menu from IAM or other source.
416
+ */
417
+ setMenu(items) {
418
+ this._rawMenu.set(items);
419
+ }
420
+ /**
421
+ * Clear menu and permission/role checkers.
422
+ * Called on logout.
423
+ */
424
+ clearMenu() {
425
+ this._rawMenu.set([]);
426
+ }
427
+ // ==========================================================================
428
+ // App Launcher Methods
429
+ // ==========================================================================
430
+ /**
431
+ * Set launcher apps for display in header.
432
+ * Apps will be automatically filtered based on user permissions.
433
+ * If empty after filtering, the app launcher button is hidden.
434
+ */
435
+ setApps(apps) {
436
+ this._rawApps.set(apps);
437
+ }
438
+ /**
439
+ * Clear launcher apps.
440
+ * Called on logout.
441
+ */
442
+ clearApps() {
443
+ this._rawApps.set([]);
444
+ }
445
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: LayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
446
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: LayoutService, providedIn: 'root' });
447
+ }
448
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: LayoutService, decorators: [{
449
+ type: Injectable,
450
+ args: [{
451
+ providedIn: 'root',
452
+ }]
453
+ }], ctorParameters: () => [] });
454
+
455
+ const presets = {
456
+ Aura,
457
+ Lara,
458
+ Nora,
459
+ };
460
+ class AppConfigurator {
461
+ router = inject(Router);
462
+ config = inject(PrimeNG);
463
+ layoutService = inject(LayoutService);
464
+ platformService = inject(PlatformService);
465
+ primeng = inject(PrimeNG);
466
+ presets = Object.keys(presets);
467
+ showMenuModeButton = signal(!this.router.url.includes('auth'), ...(ngDevMode ? [{ debugName: "showMenuModeButton" }] : []));
468
+ menuModeOptions = [
469
+ { label: 'Static', value: 'static' },
470
+ { label: 'Overlay', value: 'overlay' },
471
+ ];
472
+ ngOnInit() {
473
+ if (!this.platformService.isServer) {
474
+ this.onPresetChange(this.layoutService.layoutConfig().preset);
475
+ }
476
+ }
477
+ surfaces = [
478
+ {
479
+ name: 'slate',
480
+ palette: {
481
+ 0: '#ffffff',
482
+ 50: '#f8fafc',
483
+ 100: '#f1f5f9',
484
+ 200: '#e2e8f0',
485
+ 300: '#cbd5e1',
486
+ 400: '#94a3b8',
487
+ 500: '#64748b',
488
+ 600: '#475569',
489
+ 700: '#334155',
490
+ 800: '#1e293b',
491
+ 900: '#0f172a',
492
+ 950: '#020617',
493
+ },
494
+ },
495
+ {
496
+ name: 'gray',
497
+ palette: {
498
+ 0: '#ffffff',
499
+ 50: '#f9fafb',
500
+ 100: '#f3f4f6',
501
+ 200: '#e5e7eb',
502
+ 300: '#d1d5db',
503
+ 400: '#9ca3af',
504
+ 500: '#6b7280',
505
+ 600: '#4b5563',
506
+ 700: '#374151',
507
+ 800: '#1f2937',
508
+ 900: '#111827',
509
+ 950: '#030712',
510
+ },
511
+ },
512
+ {
513
+ name: 'zinc',
514
+ palette: {
515
+ 0: '#ffffff',
516
+ 50: '#fafafa',
517
+ 100: '#f4f4f5',
518
+ 200: '#e4e4e7',
519
+ 300: '#d4d4d8',
520
+ 400: '#a1a1aa',
521
+ 500: '#71717a',
522
+ 600: '#52525b',
523
+ 700: '#3f3f46',
524
+ 800: '#27272a',
525
+ 900: '#18181b',
526
+ 950: '#09090b',
527
+ },
528
+ },
529
+ {
530
+ name: 'neutral',
531
+ palette: {
532
+ 0: '#ffffff',
533
+ 50: '#fafafa',
534
+ 100: '#f5f5f5',
535
+ 200: '#e5e5e5',
536
+ 300: '#d4d4d4',
537
+ 400: '#a3a3a3',
538
+ 500: '#737373',
539
+ 600: '#525252',
540
+ 700: '#404040',
541
+ 800: '#262626',
542
+ 900: '#171717',
543
+ 950: '#0a0a0a',
544
+ },
545
+ },
546
+ {
547
+ name: 'stone',
548
+ palette: {
549
+ 0: '#ffffff',
550
+ 50: '#fafaf9',
551
+ 100: '#f5f5f4',
552
+ 200: '#e7e5e4',
553
+ 300: '#d6d3d1',
554
+ 400: '#a8a29e',
555
+ 500: '#78716c',
556
+ 600: '#57534e',
557
+ 700: '#44403c',
558
+ 800: '#292524',
559
+ 900: '#1c1917',
560
+ 950: '#0c0a09',
561
+ },
562
+ },
563
+ {
564
+ name: 'soho',
565
+ palette: {
566
+ 0: '#ffffff',
567
+ 50: '#ececec',
568
+ 100: '#dedfdf',
569
+ 200: '#c4c4c6',
570
+ 300: '#adaeb0',
571
+ 400: '#97979b',
572
+ 500: '#7f8084',
573
+ 600: '#6a6b70',
574
+ 700: '#55565b',
575
+ 800: '#3f4046',
576
+ 900: '#2c2c34',
577
+ 950: '#16161d',
578
+ },
579
+ },
580
+ {
581
+ name: 'viva',
582
+ palette: {
583
+ 0: '#ffffff',
584
+ 50: '#f3f3f3',
585
+ 100: '#e7e7e8',
586
+ 200: '#cfd0d0',
587
+ 300: '#b7b8b9',
588
+ 400: '#9fa1a1',
589
+ 500: '#87898a',
590
+ 600: '#6e7173',
591
+ 700: '#565a5b',
592
+ 800: '#3e4244',
593
+ 900: '#262b2c',
594
+ 950: '#0e1315',
595
+ },
596
+ },
597
+ {
598
+ name: 'ocean',
599
+ palette: {
600
+ 0: '#ffffff',
601
+ 50: '#fbfcfc',
602
+ 100: '#F7F9F8',
603
+ 200: '#EFF3F2',
604
+ 300: '#DADEDD',
605
+ 400: '#B1B7B6',
606
+ 500: '#828787',
607
+ 600: '#5F7274',
608
+ 700: '#415B61',
609
+ 800: '#29444E',
610
+ 900: '#183240',
611
+ 950: '#0c1920',
612
+ },
613
+ },
614
+ ];
615
+ selectedPrimaryColor = computed(() => {
616
+ return this.layoutService.layoutConfig().primary;
617
+ }, ...(ngDevMode ? [{ debugName: "selectedPrimaryColor" }] : []));
618
+ selectedSurfaceColor = computed(() => this.layoutService.layoutConfig().surface, ...(ngDevMode ? [{ debugName: "selectedSurfaceColor" }] : []));
619
+ selectedPreset = computed(() => this.layoutService.layoutConfig().preset, ...(ngDevMode ? [{ debugName: "selectedPreset" }] : []));
620
+ menuMode = computed(() => this.layoutService.layoutConfig().menuMode, ...(ngDevMode ? [{ debugName: "menuMode" }] : []));
621
+ primaryColors = computed(() => {
622
+ const presetPalette = presets[this.layoutService.layoutConfig().preset].primitive;
623
+ const colors = [
624
+ 'emerald',
625
+ 'green',
626
+ 'lime',
627
+ 'orange',
628
+ 'amber',
629
+ 'yellow',
630
+ 'teal',
631
+ 'cyan',
632
+ 'sky',
633
+ 'blue',
634
+ 'indigo',
635
+ 'violet',
636
+ 'purple',
637
+ 'fuchsia',
638
+ 'pink',
639
+ 'rose',
640
+ ];
641
+ const palettes = [{ name: 'noir', palette: {} }];
642
+ colors.forEach((color) => {
643
+ palettes.push({
644
+ name: color,
645
+ palette: presetPalette?.[color],
646
+ });
647
+ });
648
+ return palettes;
649
+ }, ...(ngDevMode ? [{ debugName: "primaryColors" }] : []));
650
+ getPresetExt() {
651
+ const color = this.primaryColors().find((c) => c.name === this.selectedPrimaryColor()) || {};
652
+ const preset = this.layoutService.layoutConfig().preset;
653
+ if (color.name === 'noir') {
654
+ return {
655
+ semantic: {
656
+ primary: {
657
+ 50: '{surface.50}',
658
+ 100: '{surface.100}',
659
+ 200: '{surface.200}',
660
+ 300: '{surface.300}',
661
+ 400: '{surface.400}',
662
+ 500: '{surface.500}',
663
+ 600: '{surface.600}',
664
+ 700: '{surface.700}',
665
+ 800: '{surface.800}',
666
+ 900: '{surface.900}',
667
+ 950: '{surface.950}',
668
+ },
669
+ colorScheme: {
670
+ light: {
671
+ primary: {
672
+ color: '{primary.950}',
673
+ contrastColor: '#ffffff',
674
+ hoverColor: '{primary.800}',
675
+ activeColor: '{primary.700}',
676
+ },
677
+ highlight: {
678
+ background: '{primary.950}',
679
+ focusBackground: '{primary.700}',
680
+ color: '#ffffff',
681
+ focusColor: '#ffffff',
682
+ },
683
+ },
684
+ dark: {
685
+ primary: {
686
+ color: '{primary.50}',
687
+ contrastColor: '{primary.950}',
688
+ hoverColor: '{primary.200}',
689
+ activeColor: '{primary.300}',
690
+ },
691
+ highlight: {
692
+ background: '{primary.50}',
693
+ focusBackground: '{primary.300}',
694
+ color: '{primary.950}',
695
+ focusColor: '{primary.950}',
696
+ },
697
+ },
698
+ },
699
+ },
700
+ };
701
+ }
702
+ else {
703
+ if (preset === 'Nora') {
704
+ return {
705
+ semantic: {
706
+ primary: color.palette,
707
+ colorScheme: {
708
+ light: {
709
+ primary: {
710
+ color: '{primary.600}',
711
+ contrastColor: '#ffffff',
712
+ hoverColor: '{primary.700}',
713
+ activeColor: '{primary.800}',
714
+ },
715
+ highlight: {
716
+ background: '{primary.600}',
717
+ focusBackground: '{primary.700}',
718
+ color: '#ffffff',
719
+ focusColor: '#ffffff',
720
+ },
721
+ },
722
+ dark: {
723
+ primary: {
724
+ color: '{primary.500}',
725
+ contrastColor: '{surface.900}',
726
+ hoverColor: '{primary.400}',
727
+ activeColor: '{primary.300}',
728
+ },
729
+ highlight: {
730
+ background: '{primary.500}',
731
+ focusBackground: '{primary.400}',
732
+ color: '{surface.900}',
733
+ focusColor: '{surface.900}',
734
+ },
735
+ },
736
+ },
737
+ },
738
+ };
739
+ }
740
+ else {
741
+ return {
742
+ semantic: {
743
+ primary: color.palette,
744
+ colorScheme: {
745
+ light: {
746
+ primary: {
747
+ color: '{primary.500}',
748
+ contrastColor: '#ffffff',
749
+ hoverColor: '{primary.600}',
750
+ activeColor: '{primary.700}',
751
+ },
752
+ highlight: {
753
+ background: '{primary.50}',
754
+ focusBackground: '{primary.100}',
755
+ color: '{primary.700}',
756
+ focusColor: '{primary.800}',
757
+ },
758
+ },
759
+ dark: {
760
+ primary: {
761
+ color: '{primary.400}',
762
+ contrastColor: '{surface.900}',
763
+ hoverColor: '{primary.300}',
764
+ activeColor: '{primary.200}',
765
+ },
766
+ highlight: {
767
+ background: 'color-mix(in srgb, {primary.400}, transparent 84%)',
768
+ focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)',
769
+ color: 'rgba(255,255,255,.87)',
770
+ focusColor: 'rgba(255,255,255,.87)',
771
+ },
772
+ },
773
+ },
774
+ },
775
+ };
776
+ }
777
+ }
778
+ }
779
+ updateColors(event, type, color) {
780
+ if (type === 'primary') {
781
+ this.layoutService.layoutConfig.update((state) => ({
782
+ ...state,
783
+ primary: color.name,
784
+ }));
785
+ }
786
+ else if (type === 'surface') {
787
+ this.layoutService.layoutConfig.update((state) => ({
788
+ ...state,
789
+ surface: color.name,
790
+ }));
791
+ }
792
+ this.applyTheme(type, color);
793
+ event.stopPropagation();
794
+ }
795
+ applyTheme(type, color) {
796
+ if (type === 'primary') {
797
+ updatePreset(this.getPresetExt());
798
+ }
799
+ else if (type === 'surface') {
800
+ updateSurfacePalette(color.palette);
801
+ }
802
+ }
803
+ onPresetChange(event) {
804
+ this.layoutService.layoutConfig.update((state) => ({
805
+ ...state,
806
+ preset: event,
807
+ }));
808
+ const preset = presets[event];
809
+ const surfacePalette = this.surfaces.find((s) => s.name === this.selectedSurfaceColor())?.palette;
810
+ $t()
811
+ .preset(preset)
812
+ .preset(this.getPresetExt())
813
+ .surfacePalette(surfacePalette)
814
+ .use({ useDefaultOptions: true });
815
+ }
816
+ onMenuModeChange(event) {
817
+ this.layoutService.layoutConfig.update((prev) => ({
818
+ ...prev,
819
+ menuMode: event,
820
+ }));
821
+ }
822
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppConfigurator, deps: [], target: i0.ɵɵFactoryTarget.Component });
823
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: AppConfigurator, isStandalone: true, selector: "app-configurator", host: { styleAttribute: "background-color: var(--surface-overlay)", classAttribute: "hidden absolute top-[3.25rem] right-0 w-72 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]" }, ngImport: i0, template: `
824
+ <div class="flex flex-col gap-4">
825
+ <div>
826
+ <span class="text-sm text-muted-color font-semibold">Primary</span>
827
+ <div class="pt-2 flex gap-2 flex-wrap justify-start">
828
+ @for (primaryColor of primaryColors(); track primaryColor.name) {
829
+ <button
830
+ type="button"
831
+ [title]="primaryColor.name"
832
+ (click)="updateColors($event, 'primary', primaryColor)"
833
+ [ngClass]="{
834
+ 'outline-primary': primaryColor.name === selectedPrimaryColor()
835
+ }"
836
+ class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
837
+ [style]="{
838
+ 'background-color': primaryColor?.name === 'noir' ? 'var(--text-color)' : primaryColor?.palette?.['500']
839
+ }"
840
+ ></button>
841
+ }
842
+ </div>
843
+ </div>
844
+ <div>
845
+ <span class="text-sm text-muted-color font-semibold">Surface</span>
846
+ <div class="pt-2 flex gap-2 flex-wrap justify-start">
847
+ @for (surface of surfaces; track surface.name) {
848
+ <button
849
+ type="button"
850
+ [title]="surface.name"
851
+ (click)="updateColors($event, 'surface', surface)"
852
+ [ngClass]="{
853
+ 'outline-primary': selectedSurfaceColor()
854
+ ? selectedSurfaceColor() === surface.name
855
+ : layoutService.layoutConfig().darkTheme
856
+ ? surface.name === 'zinc'
857
+ : surface.name === 'slate'
858
+ }"
859
+ class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
860
+ [style]="{
861
+ 'background-color': surface?.name === 'noir' ? 'var(--text-color)' : surface?.palette?.['500']
862
+ }"
863
+ ></button>
864
+ }
865
+ </div>
866
+ </div>
867
+ <div class="flex flex-col gap-2">
868
+ <span class="text-sm text-muted-color font-semibold">Presets</span>
869
+ <p-selectbutton
870
+ [options]="presets"
871
+ [ngModel]="selectedPreset()"
872
+ (ngModelChange)="onPresetChange($event)"
873
+ [allowEmpty]="false"
874
+ size="small"
875
+ />
876
+ </div>
877
+ @if (showMenuModeButton()) {
878
+ <div class="flex flex-col gap-2">
879
+ <span class="text-sm text-muted-color font-semibold">Menu Mode</span>
880
+ <p-selectbutton
881
+ [ngModel]="menuMode()"
882
+ (ngModelChange)="onMenuModeChange($event)"
883
+ [options]="menuModeOptions"
884
+ [allowEmpty]="false"
885
+ size="small"
886
+ />
887
+ </div>
888
+ }
889
+ </div>
890
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: SelectButtonModule }, { kind: "component", type: i3.SelectButton, selector: "p-selectButton, p-selectbutton, p-select-button", inputs: ["options", "optionLabel", "optionValue", "optionDisabled", "unselectable", "tabindex", "multiple", "allowEmpty", "styleClass", "ariaLabelledBy", "dataKey", "autofocus", "size", "fluid"], outputs: ["onOptionClick", "onChange"] }] });
891
+ }
892
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppConfigurator, decorators: [{
893
+ type: Component,
894
+ args: [{
895
+ selector: 'app-configurator',
896
+ standalone: true,
897
+ imports: [CommonModule, FormsModule, SelectButtonModule],
898
+ template: `
899
+ <div class="flex flex-col gap-4">
900
+ <div>
901
+ <span class="text-sm text-muted-color font-semibold">Primary</span>
902
+ <div class="pt-2 flex gap-2 flex-wrap justify-start">
903
+ @for (primaryColor of primaryColors(); track primaryColor.name) {
904
+ <button
905
+ type="button"
906
+ [title]="primaryColor.name"
907
+ (click)="updateColors($event, 'primary', primaryColor)"
908
+ [ngClass]="{
909
+ 'outline-primary': primaryColor.name === selectedPrimaryColor()
910
+ }"
911
+ class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
912
+ [style]="{
913
+ 'background-color': primaryColor?.name === 'noir' ? 'var(--text-color)' : primaryColor?.palette?.['500']
914
+ }"
915
+ ></button>
916
+ }
917
+ </div>
918
+ </div>
919
+ <div>
920
+ <span class="text-sm text-muted-color font-semibold">Surface</span>
921
+ <div class="pt-2 flex gap-2 flex-wrap justify-start">
922
+ @for (surface of surfaces; track surface.name) {
923
+ <button
924
+ type="button"
925
+ [title]="surface.name"
926
+ (click)="updateColors($event, 'surface', surface)"
927
+ [ngClass]="{
928
+ 'outline-primary': selectedSurfaceColor()
929
+ ? selectedSurfaceColor() === surface.name
930
+ : layoutService.layoutConfig().darkTheme
931
+ ? surface.name === 'zinc'
932
+ : surface.name === 'slate'
933
+ }"
934
+ class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
935
+ [style]="{
936
+ 'background-color': surface?.name === 'noir' ? 'var(--text-color)' : surface?.palette?.['500']
937
+ }"
938
+ ></button>
939
+ }
940
+ </div>
941
+ </div>
942
+ <div class="flex flex-col gap-2">
943
+ <span class="text-sm text-muted-color font-semibold">Presets</span>
944
+ <p-selectbutton
945
+ [options]="presets"
946
+ [ngModel]="selectedPreset()"
947
+ (ngModelChange)="onPresetChange($event)"
948
+ [allowEmpty]="false"
949
+ size="small"
950
+ />
951
+ </div>
952
+ @if (showMenuModeButton()) {
953
+ <div class="flex flex-col gap-2">
954
+ <span class="text-sm text-muted-color font-semibold">Menu Mode</span>
955
+ <p-selectbutton
956
+ [ngModel]="menuMode()"
957
+ (ngModelChange)="onMenuModeChange($event)"
958
+ [options]="menuModeOptions"
959
+ [allowEmpty]="false"
960
+ size="small"
961
+ />
962
+ </div>
963
+ }
964
+ </div>
965
+ `,
966
+ host: {
967
+ class: 'hidden absolute top-[3.25rem] right-0 w-72 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]',
968
+ style: 'background-color: var(--surface-overlay)',
969
+ },
970
+ }]
971
+ }] });
972
+
973
+ class AppFloatingConfigurator {
974
+ LayoutService = inject(LayoutService);
975
+ isDarkTheme = computed(() => this.LayoutService.layoutConfig().darkTheme, ...(ngDevMode ? [{ debugName: "isDarkTheme" }] : []));
976
+ toggleDarkMode() {
977
+ this.LayoutService.layoutConfig.update((state) => ({ ...state, darkTheme: !state.darkTheme }));
978
+ }
979
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppFloatingConfigurator, deps: [], target: i0.ɵɵFactoryTarget.Component });
980
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: AppFloatingConfigurator, isStandalone: true, selector: "app-floating-configurator", ngImport: i0, template: `
981
+ <div class="fixed flex flex-col md:flex-row gap-4 top-8 right-0 md:right-8 z-99">
982
+ <p-button type="button" (onClick)="toggleDarkMode()" [rounded]="true" [icon]="isDarkTheme() ? 'pi pi-moon' : 'pi pi-sun'" severity="secondary" />
983
+ <div class="relative">
984
+ <p-button icon="pi pi-palette" pStyleClass="@next" enterFromClass="hidden" enterActiveClass="animate-scalein" leaveToClass="hidden" leaveActiveClass="animate-fadeout" [hideOnOutsideClick]="true" type="button" rounded />
985
+ <app-configurator />
986
+ </div>
987
+ </div>
988
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i1$1.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }, { kind: "component", type: AppConfigurator, selector: "app-configurator" }] });
989
+ }
990
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppFloatingConfigurator, decorators: [{
991
+ type: Component,
992
+ args: [{
993
+ selector: 'app-floating-configurator',
994
+ imports: [ButtonModule, StyleClassModule, AppConfigurator],
995
+ template: `
996
+ <div class="fixed flex flex-col md:flex-row gap-4 top-8 right-0 md:right-8 z-99">
997
+ <p-button type="button" (onClick)="toggleDarkMode()" [rounded]="true" [icon]="isDarkTheme() ? 'pi pi-moon' : 'pi pi-sun'" severity="secondary" />
998
+ <div class="relative">
999
+ <p-button icon="pi pi-palette" pStyleClass="@next" enterFromClass="hidden" enterActiveClass="animate-scalein" leaveToClass="hidden" leaveActiveClass="animate-fadeout" [hideOnOutsideClick]="true" type="button" rounded />
1000
+ <app-configurator />
1001
+ </div>
1002
+ </div>
1003
+ `
1004
+ }]
1005
+ }] });
1006
+
1007
+ const LAYOUT_AUTH_STATE = new InjectionToken('LAYOUT_AUTH_STATE');
1008
+ const LAYOUT_AUTH_API = new InjectionToken('LAYOUT_AUTH_API');
1009
+
1010
+ class AppFooter {
1011
+ authState = inject(LAYOUT_AUTH_STATE, { optional: true });
1012
+ companyName = computed(() => {
1013
+ return this.authState?.currentCompanyInfo()?.name ?? 'Flusys';
1014
+ }, ...(ngDevMode ? [{ debugName: "companyName" }] : []));
1015
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppFooter, deps: [], target: i0.ɵɵFactoryTarget.Component });
1016
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: AppFooter, isStandalone: true, selector: "app-footer", ngImport: i0, template: `<div class="layout-footer">
1017
+ {{ companyName() }} by
1018
+ <a href="https://flusys.vercel.app" target="_blank" rel="noopener noreferrer" class="text-primary font-bold hover:underline">Flusys</a>
1019
+ </div>`, isInline: true });
1020
+ }
1021
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppFooter, decorators: [{
1022
+ type: Component,
1023
+ args: [{
1024
+ standalone: true,
1025
+ selector: 'app-footer',
1026
+ template: `<div class="layout-footer">
1027
+ {{ companyName() }} by
1028
+ <a href="https://flusys.vercel.app" target="_blank" rel="noopener noreferrer" class="text-primary font-bold hover:underline">Flusys</a>
1029
+ </div>`,
1030
+ }]
1031
+ }] });
1032
+
1033
+ /** Company/branch switcher displayed in top bar */
1034
+ class AppCompanyBranchSelector {
1035
+ destroyRef = inject(DestroyRef);
1036
+ authState = inject(LAYOUT_AUTH_STATE, { optional: true });
1037
+ authApi = inject(LAYOUT_AUTH_API, { optional: true });
1038
+ messageService = inject(MessageService);
1039
+ currentCompanyName = computed(() => this.authState?.currentCompanyInfo()?.name ?? 'No Company', ...(ngDevMode ? [{ debugName: "currentCompanyName" }] : []));
1040
+ currentBranchName = computed(() => this.authState?.currentBranchInfo()?.name ?? null, ...(ngDevMode ? [{ debugName: "currentBranchName" }] : []));
1041
+ companies = signal([], ...(ngDevMode ? [{ debugName: "companies" }] : []));
1042
+ branches = signal([], ...(ngDevMode ? [{ debugName: "branches" }] : []));
1043
+ selectedCompanyId = signal(null, ...(ngDevMode ? [{ debugName: "selectedCompanyId" }] : []));
1044
+ selectedBranchId = signal(null, ...(ngDevMode ? [{ debugName: "selectedBranchId" }] : []));
1045
+ isLoadingCompanies = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingCompanies" }] : []));
1046
+ isLoadingBranches = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingBranches" }] : []));
1047
+ isSwitching = signal(false, ...(ngDevMode ? [{ debugName: "isSwitching" }] : []));
1048
+ canSwitch = computed(() => {
1049
+ const selectedCompany = this.selectedCompanyId();
1050
+ if (!selectedCompany)
1051
+ return false;
1052
+ const currentCompanyId = this.authState?.currentCompanyInfo()?.id;
1053
+ const currentBranchId = this.authState?.currentBranchInfo()?.id;
1054
+ const selectedBranch = this.selectedBranchId();
1055
+ return selectedCompany !== currentCompanyId || selectedBranch !== currentBranchId;
1056
+ }, ...(ngDevMode ? [{ debugName: "canSwitch" }] : []));
1057
+ onPanelOpen() {
1058
+ if (this.companies().length === 0) {
1059
+ this.loadCompanies();
1060
+ }
1061
+ }
1062
+ loadCompanies() {
1063
+ if (!this.authApi)
1064
+ return;
1065
+ this.isLoadingCompanies.set(true);
1066
+ this.authApi
1067
+ .getUserCompanies()
1068
+ .pipe(takeUntilDestroyed(this.destroyRef))
1069
+ .subscribe({
1070
+ next: (companies) => {
1071
+ this.companies.set(companies);
1072
+ this.isLoadingCompanies.set(false);
1073
+ },
1074
+ error: (err) => {
1075
+ this.isLoadingCompanies.set(false);
1076
+ this.messageService.add({
1077
+ severity: 'error',
1078
+ summary: 'Error',
1079
+ detail: err?.message || 'Failed to load companies',
1080
+ });
1081
+ },
1082
+ });
1083
+ }
1084
+ onCompanyChange(companyId) {
1085
+ this.selectedCompanyId.set(companyId);
1086
+ if (!companyId) {
1087
+ this.branches.set([]);
1088
+ this.selectedBranchId.set(null);
1089
+ return;
1090
+ }
1091
+ this.loadBranches(companyId);
1092
+ }
1093
+ onBranchChange(branchId) {
1094
+ this.selectedBranchId.set(branchId);
1095
+ }
1096
+ loadBranches(companyId) {
1097
+ if (!this.authApi)
1098
+ return;
1099
+ this.isLoadingBranches.set(true);
1100
+ this.selectedBranchId.set(null);
1101
+ this.authApi
1102
+ .getCompanyBranches(companyId)
1103
+ .pipe(takeUntilDestroyed(this.destroyRef))
1104
+ .subscribe({
1105
+ next: (branches) => {
1106
+ this.branches.set(branches);
1107
+ this.isLoadingBranches.set(false);
1108
+ // Auto-select if only one branch available
1109
+ if (branches.length === 1) {
1110
+ this.selectedBranchId.set(branches[0].id);
1111
+ }
1112
+ },
1113
+ error: (err) => {
1114
+ this.isLoadingBranches.set(false);
1115
+ this.messageService.add({
1116
+ severity: 'error',
1117
+ summary: 'Error',
1118
+ detail: err?.message || 'Failed to load branches',
1119
+ });
1120
+ },
1121
+ });
1122
+ }
1123
+ onSwitch() {
1124
+ const selectedCompany = this.selectedCompanyId();
1125
+ const selectedBranch = this.selectedBranchId();
1126
+ if (!this.authApi || !selectedCompany)
1127
+ return;
1128
+ if (this.branches().length > 0 && !selectedBranch) {
1129
+ this.messageService.add({
1130
+ severity: 'warn',
1131
+ summary: 'Branch Required',
1132
+ detail: 'Please select a branch',
1133
+ });
1134
+ return;
1135
+ }
1136
+ this.isSwitching.set(true);
1137
+ this.authApi
1138
+ .switchCompany(selectedCompany, selectedBranch || '')
1139
+ .pipe(takeUntilDestroyed(this.destroyRef))
1140
+ .subscribe({
1141
+ next: () => { },
1142
+ error: (err) => {
1143
+ this.isSwitching.set(false);
1144
+ this.messageService.add({
1145
+ severity: 'error',
1146
+ summary: 'Error',
1147
+ detail: err?.message || 'Failed to switch company',
1148
+ });
1149
+ },
1150
+ });
1151
+ }
1152
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppCompanyBranchSelector, deps: [], target: i0.ɵɵFactoryTarget.Component });
1153
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: AppCompanyBranchSelector, isStandalone: true, selector: "app-company-branch-selector", host: { classAttribute: "relative" }, ngImport: i0, template: `
1154
+ <button
1155
+ type="button"
1156
+ class="layout-topbar-action"
1157
+ pStyleClass="@next"
1158
+ enterFromClass="hidden"
1159
+ enterActiveClass="animate-scalein"
1160
+ leaveToClass="hidden"
1161
+ leaveActiveClass="animate-fadeout"
1162
+ [hideOnOutsideClick]="true"
1163
+ (click)="onPanelOpen()"
1164
+ >
1165
+ <i class="pi pi-building"></i>
1166
+ <span>{{ currentCompanyName() }}</span>
1167
+ @if (currentBranchName()) {
1168
+ <span class="text-xs opacity-70"> | {{ currentBranchName() }}</span>
1169
+ }
1170
+ </button>
1171
+ <div
1172
+ class="hidden absolute top-[3.25rem] right-0 z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]"
1173
+ style="background-color: var(--surface-overlay)"
1174
+ >
1175
+ <div class="flex flex-col gap-4 min-w-[280px]">
1176
+ <span class="text-sm text-muted-color font-semibold"
1177
+ >Switch Company & Branch</span
1178
+ >
1179
+
1180
+ <!-- Company Selector -->
1181
+ <div class="flex flex-col gap-2">
1182
+ <label class="text-sm text-muted-color font-semibold">Company</label>
1183
+ <p-select
1184
+ [options]="companies()"
1185
+ [ngModel]="selectedCompanyId()"
1186
+ (ngModelChange)="onCompanyChange($event)"
1187
+ optionLabel="name"
1188
+ optionValue="id"
1189
+ placeholder="Select Company"
1190
+ class="w-full"
1191
+ [loading]="isLoadingCompanies()"
1192
+ />
1193
+ </div>
1194
+
1195
+ <!-- Branch Selector -->
1196
+ @if (selectedCompanyId() && branches().length > 0) {
1197
+ <div class="flex flex-col gap-2">
1198
+ <label class="text-sm text-muted-color font-semibold">Branch</label>
1199
+ <p-select
1200
+ [options]="branches()"
1201
+ [ngModel]="selectedBranchId()"
1202
+ (ngModelChange)="onBranchChange($event)"
1203
+ optionLabel="name"
1204
+ optionValue="id"
1205
+ placeholder="Select Branch"
1206
+ class="w-full"
1207
+ [loading]="isLoadingBranches()"
1208
+ />
1209
+ </div>
1210
+ }
1211
+
1212
+ <!-- Actions -->
1213
+ <div class="flex gap-2 pt-2">
1214
+ <p-button
1215
+ label="Switch"
1216
+ icon="pi pi-sync"
1217
+ styleClass="flex-1"
1218
+ [loading]="isSwitching()"
1219
+ [disabled]="!canSwitch()"
1220
+ (onClick)="onSwitch()"
1221
+ />
1222
+ </div>
1223
+ </div>
1224
+ </div>
1225
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i1$1.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i4.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }] });
1226
+ }
1227
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppCompanyBranchSelector, decorators: [{
1228
+ type: Component,
1229
+ args: [{
1230
+ selector: 'app-company-branch-selector',
1231
+ standalone: true,
1232
+ imports: [AngularModule, StyleClassModule, ButtonModule, SelectModule],
1233
+ host: { class: 'relative' },
1234
+ template: `
1235
+ <button
1236
+ type="button"
1237
+ class="layout-topbar-action"
1238
+ pStyleClass="@next"
1239
+ enterFromClass="hidden"
1240
+ enterActiveClass="animate-scalein"
1241
+ leaveToClass="hidden"
1242
+ leaveActiveClass="animate-fadeout"
1243
+ [hideOnOutsideClick]="true"
1244
+ (click)="onPanelOpen()"
1245
+ >
1246
+ <i class="pi pi-building"></i>
1247
+ <span>{{ currentCompanyName() }}</span>
1248
+ @if (currentBranchName()) {
1249
+ <span class="text-xs opacity-70"> | {{ currentBranchName() }}</span>
1250
+ }
1251
+ </button>
1252
+ <div
1253
+ class="hidden absolute top-[3.25rem] right-0 z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]"
1254
+ style="background-color: var(--surface-overlay)"
1255
+ >
1256
+ <div class="flex flex-col gap-4 min-w-[280px]">
1257
+ <span class="text-sm text-muted-color font-semibold"
1258
+ >Switch Company & Branch</span
1259
+ >
1260
+
1261
+ <!-- Company Selector -->
1262
+ <div class="flex flex-col gap-2">
1263
+ <label class="text-sm text-muted-color font-semibold">Company</label>
1264
+ <p-select
1265
+ [options]="companies()"
1266
+ [ngModel]="selectedCompanyId()"
1267
+ (ngModelChange)="onCompanyChange($event)"
1268
+ optionLabel="name"
1269
+ optionValue="id"
1270
+ placeholder="Select Company"
1271
+ class="w-full"
1272
+ [loading]="isLoadingCompanies()"
1273
+ />
1274
+ </div>
1275
+
1276
+ <!-- Branch Selector -->
1277
+ @if (selectedCompanyId() && branches().length > 0) {
1278
+ <div class="flex flex-col gap-2">
1279
+ <label class="text-sm text-muted-color font-semibold">Branch</label>
1280
+ <p-select
1281
+ [options]="branches()"
1282
+ [ngModel]="selectedBranchId()"
1283
+ (ngModelChange)="onBranchChange($event)"
1284
+ optionLabel="name"
1285
+ optionValue="id"
1286
+ placeholder="Select Branch"
1287
+ class="w-full"
1288
+ [loading]="isLoadingBranches()"
1289
+ />
1290
+ </div>
1291
+ }
1292
+
1293
+ <!-- Actions -->
1294
+ <div class="flex gap-2 pt-2">
1295
+ <p-button
1296
+ label="Switch"
1297
+ icon="pi pi-sync"
1298
+ styleClass="flex-1"
1299
+ [loading]="isSwitching()"
1300
+ [disabled]="!canSwitch()"
1301
+ (onClick)="onSwitch()"
1302
+ />
1303
+ </div>
1304
+ </div>
1305
+ </div>
1306
+ `,
1307
+ }]
1308
+ }] });
1309
+
1310
+ class AppLauncher {
1311
+ layoutService = inject(LayoutService);
1312
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppLauncher, deps: [], target: i0.ɵɵFactoryTarget.Component });
1313
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: AppLauncher, isStandalone: true, selector: "app-launcher", host: { classAttribute: "relative" }, ngImport: i0, template: `
1314
+ <button
1315
+ type="button"
1316
+ class="layout-topbar-action"
1317
+ pStyleClass="@next"
1318
+ enterFromClass="hidden"
1319
+ enterActiveClass="animate-scalein"
1320
+ leaveToClass="hidden"
1321
+ leaveActiveClass="animate-fadeout"
1322
+ [hideOnOutsideClick]="true"
1323
+ >
1324
+ <i class="pi pi-th-large"></i>
1325
+ <span>Apps</span>
1326
+ </button>
1327
+ <div
1328
+ class="hidden absolute top-[3.25rem] right-0 z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]"
1329
+ style="background-color: var(--surface-overlay)"
1330
+ >
1331
+ <div class="flex flex-col gap-2 min-w-[240px]">
1332
+ <span class="text-sm text-muted-color font-semibold">Applications</span>
1333
+ <div class="grid grid-cols-3 gap-2">
1334
+ @for (app of layoutService.apps(); track app.id) {
1335
+ <a
1336
+ class="group flex flex-col items-center w-20 p-2 rounded-border cursor-pointer no-underline transition-all duration-200 hover:bg-emphasis"
1337
+ [href]="app.url"
1338
+ target="_blank"
1339
+ rel="noopener noreferrer"
1340
+ >
1341
+ <div
1342
+ class="flex items-center justify-center w-10 h-10 rounded-border bg-emphasis group-hover:bg-primary transition-all duration-200"
1343
+ >
1344
+ <lib-icon
1345
+ [icon]="app.icon"
1346
+ [iconType]="app.iconType"
1347
+ class="text-xl text-primary group-hover:text-primary-contrast transition-colors duration-200"
1348
+ />
1349
+ </div>
1350
+ <span
1351
+ class="text-xs mt-2 text-center text-muted-color group-hover:text-color transition-colors duration-200"
1352
+ >{{ app.name }}</span
1353
+ >
1354
+ </a>
1355
+ }
1356
+ </div>
1357
+ </div>
1358
+ </div>
1359
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }, { kind: "component", type: IconComponent, selector: "lib-icon", inputs: ["icon", "iconType"] }] });
1360
+ }
1361
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppLauncher, decorators: [{
1362
+ type: Component,
1363
+ args: [{
1364
+ selector: 'app-launcher',
1365
+ standalone: true,
1366
+ imports: [StyleClassModule, IconComponent],
1367
+ host: { class: 'relative' },
1368
+ template: `
1369
+ <button
1370
+ type="button"
1371
+ class="layout-topbar-action"
1372
+ pStyleClass="@next"
1373
+ enterFromClass="hidden"
1374
+ enterActiveClass="animate-scalein"
1375
+ leaveToClass="hidden"
1376
+ leaveActiveClass="animate-fadeout"
1377
+ [hideOnOutsideClick]="true"
1378
+ >
1379
+ <i class="pi pi-th-large"></i>
1380
+ <span>Apps</span>
1381
+ </button>
1382
+ <div
1383
+ class="hidden absolute top-[3.25rem] right-0 z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]"
1384
+ style="background-color: var(--surface-overlay)"
1385
+ >
1386
+ <div class="flex flex-col gap-2 min-w-[240px]">
1387
+ <span class="text-sm text-muted-color font-semibold">Applications</span>
1388
+ <div class="grid grid-cols-3 gap-2">
1389
+ @for (app of layoutService.apps(); track app.id) {
1390
+ <a
1391
+ class="group flex flex-col items-center w-20 p-2 rounded-border cursor-pointer no-underline transition-all duration-200 hover:bg-emphasis"
1392
+ [href]="app.url"
1393
+ target="_blank"
1394
+ rel="noopener noreferrer"
1395
+ >
1396
+ <div
1397
+ class="flex items-center justify-center w-10 h-10 rounded-border bg-emphasis group-hover:bg-primary transition-all duration-200"
1398
+ >
1399
+ <lib-icon
1400
+ [icon]="app.icon"
1401
+ [iconType]="app.iconType"
1402
+ class="text-xl text-primary group-hover:text-primary-contrast transition-colors duration-200"
1403
+ />
1404
+ </div>
1405
+ <span
1406
+ class="text-xs mt-2 text-center text-muted-color group-hover:text-color transition-colors duration-200"
1407
+ >{{ app.name }}</span
1408
+ >
1409
+ </a>
1410
+ }
1411
+ </div>
1412
+ </div>
1413
+ </div>
1414
+ `,
1415
+ }]
1416
+ }] });
1417
+
1418
+ class AppProfile {
1419
+ destroyRef = inject(DestroyRef);
1420
+ authState = inject(LAYOUT_AUTH_STATE, { optional: true });
1421
+ authApi = inject(LAYOUT_AUTH_API, { optional: true });
1422
+ messageService = inject(MessageService);
1423
+ userName = computed(() => this.authState?.loginUserData()?.name ?? 'Guest', ...(ngDevMode ? [{ debugName: "userName" }] : []));
1424
+ userEmail = computed(() => this.authState?.loginUserData()?.email ?? '', ...(ngDevMode ? [{ debugName: "userEmail" }] : []));
1425
+ profilePicture = computed(() => this.authState?.loginUserData()?.profilePicture?.url ?? '', ...(ngDevMode ? [{ debugName: "profilePicture" }] : []));
1426
+ logout() {
1427
+ if (!this.authApi)
1428
+ return;
1429
+ this.authApi
1430
+ .logOut()
1431
+ .pipe(takeUntilDestroyed(this.destroyRef))
1432
+ .subscribe({
1433
+ next: (result) => {
1434
+ this.messageService.add({
1435
+ severity: 'success',
1436
+ summary: 'Success!',
1437
+ detail: result.message,
1438
+ });
1439
+ this.authApi?.navigateLogin(false);
1440
+ },
1441
+ error: (err) => {
1442
+ this.messageService.add({
1443
+ severity: 'error',
1444
+ summary: 'Error',
1445
+ detail: err?.message || 'Failed to logout',
1446
+ });
1447
+ },
1448
+ });
1449
+ }
1450
+ copySignUpLink() {
1451
+ const slug = this.authState?.currentCompanyInfo()?.slug ?? '';
1452
+ const signUpLink = `${window.location.origin}/auth/register?companySlug=${slug}`;
1453
+ navigator.clipboard
1454
+ .writeText(signUpLink)
1455
+ .then(() => {
1456
+ this.messageService.add({
1457
+ severity: 'success',
1458
+ summary: 'Link Copied',
1459
+ detail: 'Sign Up Link copied.',
1460
+ });
1461
+ })
1462
+ .catch((err) => {
1463
+ console.error('Failed to copy signup link: ', err);
1464
+ this.messageService.add({
1465
+ severity: 'error',
1466
+ summary: 'Sorry',
1467
+ detail: 'Failed to copy signup link.',
1468
+ });
1469
+ });
1470
+ }
1471
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppProfile, deps: [], target: i0.ɵɵFactoryTarget.Component });
1472
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: AppProfile, isStandalone: true, selector: "app-profile", host: { classAttribute: "relative" }, ngImport: i0, template: `
1473
+ <button
1474
+ type="button"
1475
+ class="layout-topbar-action"
1476
+ pStyleClass="@next"
1477
+ enterFromClass="hidden"
1478
+ enterActiveClass="animate-scalein"
1479
+ leaveToClass="hidden"
1480
+ leaveActiveClass="animate-fadeout"
1481
+ [hideOnOutsideClick]="true"
1482
+ >
1483
+ <i class="pi pi-user"></i>
1484
+ <span>Profile</span>
1485
+ </button>
1486
+ <div
1487
+ class="hidden absolute top-[3.25rem] right-0 z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]"
1488
+ style="background-color: var(--surface-overlay)"
1489
+ >
1490
+ <div class="flex flex-col gap-3 min-w-[240px]">
1491
+ <!-- User Info -->
1492
+ <a
1493
+ class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline hover:bg-emphasis transition-all duration-200"
1494
+ routerLink="/profile"
1495
+ >
1496
+ @if (profilePicture()) {
1497
+ <img
1498
+ [src]="profilePicture()"
1499
+ class="w-12 h-12 rounded-border object-cover"
1500
+ alt="Profile"
1501
+ />
1502
+ } @else {
1503
+ <div
1504
+ class="w-12 h-12 rounded-border bg-primary flex items-center justify-center"
1505
+ >
1506
+ <i class="pi pi-user text-xl text-primary-contrast"></i>
1507
+ </div>
1508
+ }
1509
+ <div class="flex flex-col">
1510
+ <span class="font-semibold text-color">{{ userName() }}</span>
1511
+ <span class="text-sm text-muted-color">{{ userEmail() }}</span>
1512
+ </div>
1513
+ </a>
1514
+
1515
+ <!-- Divider -->
1516
+ <div class="border-t border-surface"></div>
1517
+
1518
+ <!-- Menu Items -->
1519
+ <div class="flex flex-col gap-1">
1520
+ <a
1521
+ class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1522
+ (click)="copySignUpLink()"
1523
+ >
1524
+ <i class="pi pi-link text-muted-color"></i>
1525
+ <span>Copy SignUp Link</span>
1526
+ </a>
1527
+ <a
1528
+ class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1529
+ (click)="logout()"
1530
+ >
1531
+ <i class="pi pi-sign-out text-muted-color"></i>
1532
+ <span>Logout</span>
1533
+ </a>
1534
+ </div>
1535
+ </div>
1536
+ </div>
1537
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i2$2.IsEmptyImageDirective, selector: "img", inputs: ["src"] }, { kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }] });
1538
+ }
1539
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppProfile, decorators: [{
1540
+ type: Component,
1541
+ args: [{
1542
+ selector: 'app-profile',
1543
+ standalone: true,
1544
+ imports: [AngularModule, StyleClassModule],
1545
+ host: { class: 'relative' },
1546
+ template: `
1547
+ <button
1548
+ type="button"
1549
+ class="layout-topbar-action"
1550
+ pStyleClass="@next"
1551
+ enterFromClass="hidden"
1552
+ enterActiveClass="animate-scalein"
1553
+ leaveToClass="hidden"
1554
+ leaveActiveClass="animate-fadeout"
1555
+ [hideOnOutsideClick]="true"
1556
+ >
1557
+ <i class="pi pi-user"></i>
1558
+ <span>Profile</span>
1559
+ </button>
1560
+ <div
1561
+ class="hidden absolute top-[3.25rem] right-0 z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]"
1562
+ style="background-color: var(--surface-overlay)"
1563
+ >
1564
+ <div class="flex flex-col gap-3 min-w-[240px]">
1565
+ <!-- User Info -->
1566
+ <a
1567
+ class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline hover:bg-emphasis transition-all duration-200"
1568
+ routerLink="/profile"
1569
+ >
1570
+ @if (profilePicture()) {
1571
+ <img
1572
+ [src]="profilePicture()"
1573
+ class="w-12 h-12 rounded-border object-cover"
1574
+ alt="Profile"
1575
+ />
1576
+ } @else {
1577
+ <div
1578
+ class="w-12 h-12 rounded-border bg-primary flex items-center justify-center"
1579
+ >
1580
+ <i class="pi pi-user text-xl text-primary-contrast"></i>
1581
+ </div>
1582
+ }
1583
+ <div class="flex flex-col">
1584
+ <span class="font-semibold text-color">{{ userName() }}</span>
1585
+ <span class="text-sm text-muted-color">{{ userEmail() }}</span>
1586
+ </div>
1587
+ </a>
1588
+
1589
+ <!-- Divider -->
1590
+ <div class="border-t border-surface"></div>
1591
+
1592
+ <!-- Menu Items -->
1593
+ <div class="flex flex-col gap-1">
1594
+ <a
1595
+ class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1596
+ (click)="copySignUpLink()"
1597
+ >
1598
+ <i class="pi pi-link text-muted-color"></i>
1599
+ <span>Copy SignUp Link</span>
1600
+ </a>
1601
+ <a
1602
+ class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1603
+ (click)="logout()"
1604
+ >
1605
+ <i class="pi pi-sign-out text-muted-color"></i>
1606
+ <span>Logout</span>
1607
+ </a>
1608
+ </div>
1609
+ </div>
1610
+ </div>
1611
+ `,
1612
+ }]
1613
+ }] });
1614
+
1615
+ class AppTopbar {
1616
+ authState = inject(LAYOUT_AUTH_STATE, { optional: true });
1617
+ appConfig = inject(APP_CONFIG);
1618
+ layoutService = inject(LayoutService);
1619
+ companyName = computed(() => {
1620
+ return this.authState?.currentCompanyInfo()?.name ?? 'Flusys';
1621
+ }, ...(ngDevMode ? [{ debugName: "companyName" }] : []));
1622
+ enableCompanyFeature = computed(() => {
1623
+ return isCompanyFeatureEnabled(this.appConfig);
1624
+ }, ...(ngDevMode ? [{ debugName: "enableCompanyFeature" }] : []));
1625
+ toggleDarkMode() {
1626
+ this.layoutService.layoutConfig.update((state) => ({
1627
+ ...state,
1628
+ darkTheme: !state.darkTheme,
1629
+ }));
1630
+ }
1631
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppTopbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
1632
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: AppTopbar, isStandalone: true, selector: "app-topbar", ngImport: i0, template: ` <div class="layout-topbar">
1633
+ <div class="layout-topbar-logo-container">
1634
+ <button
1635
+ class="layout-menu-button layout-topbar-action"
1636
+ (click)="layoutService.onMenuToggle()"
1637
+ >
1638
+ <i class="pi pi-bars"></i>
1639
+ </button>
1640
+ <a class="layout-topbar-logo" routerLink="/">
1641
+ <span>{{ companyName() }}</span>
1642
+ </a>
1643
+ </div>
1644
+
1645
+ <div class="layout-topbar-actions">
1646
+ <div class="layout-config-menu">
1647
+ <button
1648
+ type="button"
1649
+ class="layout-topbar-action"
1650
+ (click)="toggleDarkMode()"
1651
+ >
1652
+ <i
1653
+ [ngClass]="{
1654
+ 'pi ': true,
1655
+ 'pi-moon': layoutService.isDarkTheme(),
1656
+ 'pi-sun': !layoutService.isDarkTheme(),
1657
+ }"
1658
+ ></i>
1659
+ </button>
1660
+ <div class="relative">
1661
+ <button
1662
+ class="layout-topbar-action layout-topbar-action-highlight"
1663
+ pStyleClass="@next"
1664
+ enterFromClass="hidden"
1665
+ enterActiveClass="animate-scalein"
1666
+ leaveToClass="hidden"
1667
+ leaveActiveClass="animate-fadeout"
1668
+ [hideOnOutsideClick]="true"
1669
+ >
1670
+ <i class="pi pi-palette"></i>
1671
+ </button>
1672
+ <app-configurator />
1673
+ </div>
1674
+ </div>
1675
+
1676
+ <button
1677
+ class="layout-topbar-menu-button layout-topbar-action"
1678
+ pStyleClass="@next"
1679
+ enterFromClass="hidden"
1680
+ enterActiveClass="animate-scalein"
1681
+ leaveToClass="hidden"
1682
+ leaveActiveClass="animate-fadeout"
1683
+ [hideOnOutsideClick]="true"
1684
+ >
1685
+ <i class="pi pi-ellipsis-v"></i>
1686
+ </button>
1687
+
1688
+ <div class="layout-topbar-menu hidden lg:block">
1689
+ <div class="layout-topbar-menu-content">
1690
+ @if (layoutService.hasApps()) {
1691
+ <app-launcher />
1692
+ }
1693
+ @if (enableCompanyFeature()) {
1694
+ <app-company-branch-selector />
1695
+ }
1696
+ <app-profile />
1697
+ </div>
1698
+ </div>
1699
+ </div>
1700
+ </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }, { kind: "component", type: AppConfigurator, selector: "app-configurator" }, { kind: "component", type: AppProfile, selector: "app-profile" }, { kind: "component", type: AppCompanyBranchSelector, selector: "app-company-branch-selector" }, { kind: "component", type: AppLauncher, selector: "app-launcher" }] });
1701
+ }
1702
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppTopbar, decorators: [{
1703
+ type: Component,
1704
+ args: [{
1705
+ selector: 'app-topbar',
1706
+ standalone: true,
1707
+ imports: [
1708
+ RouterModule,
1709
+ CommonModule,
1710
+ StyleClassModule,
1711
+ AppConfigurator,
1712
+ AppProfile,
1713
+ AppCompanyBranchSelector,
1714
+ AppLauncher,
1715
+ ],
1716
+ template: ` <div class="layout-topbar">
1717
+ <div class="layout-topbar-logo-container">
1718
+ <button
1719
+ class="layout-menu-button layout-topbar-action"
1720
+ (click)="layoutService.onMenuToggle()"
1721
+ >
1722
+ <i class="pi pi-bars"></i>
1723
+ </button>
1724
+ <a class="layout-topbar-logo" routerLink="/">
1725
+ <span>{{ companyName() }}</span>
1726
+ </a>
1727
+ </div>
1728
+
1729
+ <div class="layout-topbar-actions">
1730
+ <div class="layout-config-menu">
1731
+ <button
1732
+ type="button"
1733
+ class="layout-topbar-action"
1734
+ (click)="toggleDarkMode()"
1735
+ >
1736
+ <i
1737
+ [ngClass]="{
1738
+ 'pi ': true,
1739
+ 'pi-moon': layoutService.isDarkTheme(),
1740
+ 'pi-sun': !layoutService.isDarkTheme(),
1741
+ }"
1742
+ ></i>
1743
+ </button>
1744
+ <div class="relative">
1745
+ <button
1746
+ class="layout-topbar-action layout-topbar-action-highlight"
1747
+ pStyleClass="@next"
1748
+ enterFromClass="hidden"
1749
+ enterActiveClass="animate-scalein"
1750
+ leaveToClass="hidden"
1751
+ leaveActiveClass="animate-fadeout"
1752
+ [hideOnOutsideClick]="true"
1753
+ >
1754
+ <i class="pi pi-palette"></i>
1755
+ </button>
1756
+ <app-configurator />
1757
+ </div>
1758
+ </div>
1759
+
1760
+ <button
1761
+ class="layout-topbar-menu-button layout-topbar-action"
1762
+ pStyleClass="@next"
1763
+ enterFromClass="hidden"
1764
+ enterActiveClass="animate-scalein"
1765
+ leaveToClass="hidden"
1766
+ leaveActiveClass="animate-fadeout"
1767
+ [hideOnOutsideClick]="true"
1768
+ >
1769
+ <i class="pi pi-ellipsis-v"></i>
1770
+ </button>
1771
+
1772
+ <div class="layout-topbar-menu hidden lg:block">
1773
+ <div class="layout-topbar-menu-content">
1774
+ @if (layoutService.hasApps()) {
1775
+ <app-launcher />
1776
+ }
1777
+ @if (enableCompanyFeature()) {
1778
+ <app-company-branch-selector />
1779
+ }
1780
+ <app-profile />
1781
+ </div>
1782
+ </div>
1783
+ </div>
1784
+ </div>`,
1785
+ }]
1786
+ }] });
1787
+
1788
+ class AppMenuitem {
1789
+ // Signal inputs
1790
+ item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
1791
+ index = input.required(...(ngDevMode ? [{ debugName: "index" }] : []));
1792
+ parentKey = input('', ...(ngDevMode ? [{ debugName: "parentKey" }] : []));
1793
+ // Injected services
1794
+ router = inject(Router);
1795
+ layoutService = inject(LayoutService);
1796
+ authState = inject(LAYOUT_AUTH_STATE, { optional: true });
1797
+ destroyRef = inject(DestroyRef);
1798
+ // State signals
1799
+ active = signal(false, ...(ngDevMode ? [{ debugName: "active" }] : []));
1800
+ // Computed signals
1801
+ key = computed(() => {
1802
+ const parent = this.parentKey();
1803
+ return parent ? `${parent}-${this.index()}` : String(this.index());
1804
+ }, ...(ngDevMode ? [{ debugName: "key" }] : []));
1805
+ routerLink = computed(() => {
1806
+ const menuItem = this.item();
1807
+ return menuItem.routerLink ?? [];
1808
+ }, ...(ngDevMode ? [{ debugName: "routerLink" }] : []));
1809
+ // Computed options for routerLinkActive - use 'exact' for root, 'subset' for others
1810
+ routerLinkActiveOptions = computed(() => {
1811
+ const link = this.routerLink();
1812
+ const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
1813
+ return {
1814
+ paths: pathsMatch,
1815
+ queryParams: 'ignored',
1816
+ matrixParams: 'ignored',
1817
+ fragment: 'ignored',
1818
+ };
1819
+ }, ...(ngDevMode ? [{ debugName: "routerLinkActiveOptions" }] : []));
1820
+ constructor() {
1821
+ // Subscribe to menu source changes
1822
+ this.layoutService.menuSource$
1823
+ .pipe(takeUntilDestroyed(this.destroyRef))
1824
+ .subscribe((value) => {
1825
+ Promise.resolve(null).then(() => {
1826
+ const currentKey = this.key();
1827
+ if (value.routeEvent) {
1828
+ this.active.set(value.key === currentKey ||
1829
+ value.key?.startsWith(currentKey + '-'));
1830
+ }
1831
+ else if (value.key !== currentKey &&
1832
+ !value.key?.startsWith(currentKey + '-')) {
1833
+ this.active.set(false);
1834
+ }
1835
+ });
1836
+ });
1837
+ // Subscribe to menu reset
1838
+ this.layoutService.resetSource$
1839
+ .pipe(takeUntilDestroyed(this.destroyRef))
1840
+ .subscribe(() => this.active.set(false));
1841
+ // Subscribe to navigation events
1842
+ this.router.events
1843
+ .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
1844
+ .subscribe(() => {
1845
+ if (this.item().routerLink) {
1846
+ this.updateActiveStateFromRoute();
1847
+ }
1848
+ });
1849
+ // Effect to update active state on init when item has routerLink
1850
+ effect(() => {
1851
+ const menuItem = this.item();
1852
+ if (menuItem.routerLink) {
1853
+ this.updateActiveStateFromRoute();
1854
+ }
1855
+ });
1856
+ }
1857
+ updateActiveStateFromRoute() {
1858
+ const link = this.routerLink();
1859
+ if (!link.length)
1860
+ return;
1861
+ // Use 'exact' for root path to avoid matching all routes
1862
+ // Use 'subset' for all other paths to match child routes
1863
+ const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
1864
+ const isActive = this.router.isActive(link[0], {
1865
+ paths: pathsMatch,
1866
+ queryParams: 'ignored',
1867
+ matrixParams: 'ignored',
1868
+ fragment: 'ignored',
1869
+ });
1870
+ if (isActive) {
1871
+ this.layoutService.onMenuStateChange({
1872
+ key: this.key(),
1873
+ routeEvent: true,
1874
+ });
1875
+ }
1876
+ }
1877
+ itemClick() {
1878
+ if (this.item().children) {
1879
+ this.active.update((prev) => !prev);
1880
+ }
1881
+ this.layoutService.onMenuStateChange({ key: this.key() });
1882
+ }
1883
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppMenuitem, deps: [], target: i0.ɵɵFactoryTarget.Component });
1884
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: AppMenuitem, isStandalone: true, selector: "[app-menuitem]", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, parentKey: { classPropertyName: "parentKey", publicName: "parentKey", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.active-menuitem": "active()" } }, ngImport: i0, template: `
1885
+ <ng-container>
1886
+ @if (item().children?.length) {
1887
+ <a (click)="itemClick()" tabindex="0" pRipple>
1888
+ @if (item().icon) {
1889
+ <lib-icon
1890
+ [icon]="item().icon!"
1891
+ [iconType]="item().iconType"
1892
+ class="layout-menuitem-icon"
1893
+ />
1894
+ }
1895
+ <span class="layout-menuitem-text">{{ item().label }}</span>
1896
+ @if (item().children) {
1897
+ <i class="pi pi-fw pi-angle-down layout-submenu-toggler"></i>
1898
+ }
1899
+ </a>
1900
+ }
1901
+ @if (item().routerLink && !item().children?.length) {
1902
+ <a
1903
+ (click)="itemClick()"
1904
+ [routerLink]="routerLink()"
1905
+ routerLinkActive="active-route"
1906
+ [routerLinkActiveOptions]="routerLinkActiveOptions()"
1907
+ tabindex="0"
1908
+ pRipple
1909
+ >
1910
+ @if (item().icon) {
1911
+ <lib-icon
1912
+ [icon]="item().icon!"
1913
+ [iconType]="item().iconType"
1914
+ class="layout-menuitem-icon"
1915
+ />
1916
+ }
1917
+ <span class="layout-menuitem-text">{{ item().label }}</span>
1918
+ </a>
1919
+ }
1920
+ @if (item().children) {
1921
+ <ul
1922
+ [class.submenu-expanded]="active()"
1923
+ [class.submenu-collapsed]="!active()"
1924
+ >
1925
+ @for (
1926
+ child of item().children;
1927
+ track 'menu' + child.id + '' + i;
1928
+ let i = $index
1929
+ ) {
1930
+ <li
1931
+ app-menuitem
1932
+ [item]="child"
1933
+ [index]="i"
1934
+ [parentKey]="key()"
1935
+ ></li>
1936
+ }
1937
+ </ul>
1938
+ }
1939
+ </ng-container>
1940
+ `, isInline: true, styles: [":host ul{overflow:hidden;transition:max-height .4s cubic-bezier(.86,0,.07,1)}:host ul.submenu-collapsed{max-height:0}:host ul.submenu-expanded{max-height:1000px}\n"], dependencies: [{ kind: "component", type: AppMenuitem, selector: "[app-menuitem]", inputs: ["item", "index", "parentKey"] }, { kind: "component", type: IconComponent, selector: "lib-icon", inputs: ["icon", "iconType"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i1$2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "ngmodule", type: RippleModule }, { kind: "directive", type: i2$3.Ripple, selector: "[pRipple]" }] });
1941
+ }
1942
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppMenuitem, decorators: [{
1943
+ type: Component,
1944
+ args: [{ selector: '[app-menuitem]', standalone: true, imports: [IconComponent, RouterModule, RippleModule], template: `
1945
+ <ng-container>
1946
+ @if (item().children?.length) {
1947
+ <a (click)="itemClick()" tabindex="0" pRipple>
1948
+ @if (item().icon) {
1949
+ <lib-icon
1950
+ [icon]="item().icon!"
1951
+ [iconType]="item().iconType"
1952
+ class="layout-menuitem-icon"
1953
+ />
1954
+ }
1955
+ <span class="layout-menuitem-text">{{ item().label }}</span>
1956
+ @if (item().children) {
1957
+ <i class="pi pi-fw pi-angle-down layout-submenu-toggler"></i>
1958
+ }
1959
+ </a>
1960
+ }
1961
+ @if (item().routerLink && !item().children?.length) {
1962
+ <a
1963
+ (click)="itemClick()"
1964
+ [routerLink]="routerLink()"
1965
+ routerLinkActive="active-route"
1966
+ [routerLinkActiveOptions]="routerLinkActiveOptions()"
1967
+ tabindex="0"
1968
+ pRipple
1969
+ >
1970
+ @if (item().icon) {
1971
+ <lib-icon
1972
+ [icon]="item().icon!"
1973
+ [iconType]="item().iconType"
1974
+ class="layout-menuitem-icon"
1975
+ />
1976
+ }
1977
+ <span class="layout-menuitem-text">{{ item().label }}</span>
1978
+ </a>
1979
+ }
1980
+ @if (item().children) {
1981
+ <ul
1982
+ [class.submenu-expanded]="active()"
1983
+ [class.submenu-collapsed]="!active()"
1984
+ >
1985
+ @for (
1986
+ child of item().children;
1987
+ track 'menu' + child.id + '' + i;
1988
+ let i = $index
1989
+ ) {
1990
+ <li
1991
+ app-menuitem
1992
+ [item]="child"
1993
+ [index]="i"
1994
+ [parentKey]="key()"
1995
+ ></li>
1996
+ }
1997
+ </ul>
1998
+ }
1999
+ </ng-container>
2000
+ `, host: {
2001
+ '[class.active-menuitem]': 'active()',
2002
+ }, styles: [":host ul{overflow:hidden;transition:max-height .4s cubic-bezier(.86,0,.07,1)}:host ul.submenu-collapsed{max-height:0}:host ul.submenu-expanded{max-height:1000px}\n"] }]
2003
+ }], ctorParameters: () => [], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: true }] }], parentKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentKey", required: false }] }] } });
2004
+
2005
+ /**
2006
+ * Main menu component that displays filtered menu items.
2007
+ * Menu is automatically filtered based on user permissions using the permission checker (logicNode pattern).
2008
+ * The filtering happens internally in LayoutService via computed signal.
2009
+ */
2010
+ class AppMenu {
2011
+ layoutService = inject(LayoutService);
2012
+ /**
2013
+ * Filtered menu items from layout service.
2014
+ * This signal is computed in LayoutService and automatically updates when:
2015
+ * - Raw menu changes (setMenu called)
2016
+ * - Permission state changes (user permissions updated)
2017
+ */
2018
+ menuItems = this.layoutService.menu;
2019
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
2020
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: AppMenu, isStandalone: true, selector: "app-menu", ngImport: i0, template: `<div class="layout-menu">
2021
+ <ul>
2022
+ @for (
2023
+ item of menuItems();
2024
+ track 'menu' + item.id + '' + i;
2025
+ let i = $index
2026
+ ) {
2027
+ <li app-menuitem [item]="item" [index]="i"></li>
2028
+ }
2029
+ </ul>
2030
+ </div>`, isInline: true, dependencies: [{ kind: "component", type: AppMenuitem, selector: "[app-menuitem]", inputs: ["item", "index", "parentKey"] }, { kind: "ngmodule", type: RouterModule }] });
2031
+ }
2032
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppMenu, decorators: [{
2033
+ type: Component,
2034
+ args: [{
2035
+ selector: 'app-menu',
2036
+ standalone: true,
2037
+ imports: [AppMenuitem, RouterModule],
2038
+ template: `<div class="layout-menu">
2039
+ <ul>
2040
+ @for (
2041
+ item of menuItems();
2042
+ track 'menu' + item.id + '' + i;
2043
+ let i = $index
2044
+ ) {
2045
+ <li app-menuitem [item]="item" [index]="i"></li>
2046
+ }
2047
+ </ul>
2048
+ </div>`,
2049
+ }]
2050
+ }] });
2051
+
2052
+ class AppSidebar {
2053
+ el;
2054
+ constructor(el) {
2055
+ this.el = el;
2056
+ }
2057
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppSidebar, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
2058
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: AppSidebar, isStandalone: true, selector: "app-sidebar", ngImport: i0, template: ` <div class="layout-sidebar">
2059
+ <app-menu></app-menu>
2060
+ </div>`, isInline: true, dependencies: [{ kind: "component", type: AppMenu, selector: "app-menu" }] });
2061
+ }
2062
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppSidebar, decorators: [{
2063
+ type: Component,
2064
+ args: [{
2065
+ selector: 'app-sidebar',
2066
+ standalone: true,
2067
+ imports: [AppMenu],
2068
+ template: ` <div class="layout-sidebar">
2069
+ <app-menu></app-menu>
2070
+ </div>`
2071
+ }]
2072
+ }], ctorParameters: () => [{ type: i0.ElementRef }] });
2073
+
2074
+ class AppLayout {
2075
+ destroyRef = inject(DestroyRef);
2076
+ document = inject(DOCUMENT);
2077
+ layoutService = inject(LayoutService);
2078
+ renderer = inject(Renderer2);
2079
+ router = inject(Router);
2080
+ menuOutsideClickListener = null;
2081
+ appSidebar;
2082
+ appTopBar;
2083
+ constructor() {
2084
+ this.layoutService.overlayOpen$
2085
+ .pipe(takeUntilDestroyed(this.destroyRef))
2086
+ .subscribe(() => {
2087
+ if (!this.menuOutsideClickListener) {
2088
+ this.menuOutsideClickListener = this.renderer.listen(this.document, 'click', (event) => {
2089
+ if (this.isOutsideClicked(event)) {
2090
+ this.hideMenu();
2091
+ }
2092
+ });
2093
+ }
2094
+ if (this.layoutService.layoutState().staticMenuMobileActive) {
2095
+ this.blockBodyScroll();
2096
+ }
2097
+ });
2098
+ this.router.events
2099
+ .pipe(filter$1((event) => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
2100
+ .subscribe(() => this.hideMenu());
2101
+ }
2102
+ isOutsideClicked(event) {
2103
+ const sidebarEl = this.document.querySelector('.layout-sidebar');
2104
+ const topbarEl = this.document.querySelector('.layout-menu-button');
2105
+ const eventTarget = event.target;
2106
+ return !(sidebarEl?.isSameNode(eventTarget) ||
2107
+ sidebarEl?.contains(eventTarget) ||
2108
+ topbarEl?.isSameNode(eventTarget) ||
2109
+ topbarEl?.contains(eventTarget));
2110
+ }
2111
+ hideMenu() {
2112
+ this.layoutService.layoutState.update((prev) => ({
2113
+ ...prev,
2114
+ overlayMenuActive: false,
2115
+ staticMenuMobileActive: false,
2116
+ menuHoverActive: false,
2117
+ }));
2118
+ if (this.menuOutsideClickListener) {
2119
+ this.menuOutsideClickListener();
2120
+ this.menuOutsideClickListener = null;
2121
+ }
2122
+ this.unblockBodyScroll();
2123
+ }
2124
+ blockBodyScroll() {
2125
+ this.document.body.classList.add('blocked-scroll');
2126
+ }
2127
+ unblockBodyScroll() {
2128
+ this.document.body.classList.remove('blocked-scroll');
2129
+ }
2130
+ get containerClass() {
2131
+ const config = this.layoutService.layoutConfig();
2132
+ const state = this.layoutService.layoutState();
2133
+ return {
2134
+ 'layout-overlay': config.menuMode === 'overlay',
2135
+ 'layout-static': config.menuMode === 'static',
2136
+ 'layout-static-inactive': state.staticMenuDesktopInactive && config.menuMode === 'static',
2137
+ 'layout-overlay-active': state.overlayMenuActive,
2138
+ 'layout-mobile-active': state.staticMenuMobileActive,
2139
+ };
2140
+ }
2141
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
2142
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: AppLayout, isStandalone: true, selector: "app-layout", viewQueries: [{ propertyName: "appSidebar", first: true, predicate: AppSidebar, descendants: true }, { propertyName: "appTopBar", first: true, predicate: AppTopbar, descendants: true }], ngImport: i0, template: `
2143
+ <div class="layout-wrapper" [ngClass]="containerClass">
2144
+ <app-topbar></app-topbar>
2145
+ <app-sidebar></app-sidebar>
2146
+ <div class="layout-main-container">
2147
+ <div class="layout-main">
2148
+ <router-outlet></router-outlet>
2149
+ </div>
2150
+ <app-footer></app-footer>
2151
+ </div>
2152
+ <div class="layout-mask animate-fadein"></div>
2153
+ </div>
2154
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: AppTopbar, selector: "app-topbar" }, { kind: "component", type: AppSidebar, selector: "app-sidebar" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$2.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: AppFooter, selector: "app-footer" }] });
2155
+ }
2156
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AppLayout, decorators: [{
2157
+ type: Component,
2158
+ args: [{
2159
+ selector: 'app-layout',
2160
+ standalone: true,
2161
+ imports: [CommonModule, AppTopbar, AppSidebar, RouterModule, AppFooter],
2162
+ template: `
2163
+ <div class="layout-wrapper" [ngClass]="containerClass">
2164
+ <app-topbar></app-topbar>
2165
+ <app-sidebar></app-sidebar>
2166
+ <div class="layout-main-container">
2167
+ <div class="layout-main">
2168
+ <router-outlet></router-outlet>
2169
+ </div>
2170
+ <app-footer></app-footer>
2171
+ </div>
2172
+ <div class="layout-mask animate-fadein"></div>
2173
+ </div>
2174
+ `,
2175
+ }]
2176
+ }], ctorParameters: () => [], propDecorators: { appSidebar: [{
2177
+ type: ViewChild,
2178
+ args: [AppSidebar]
2179
+ }], appTopBar: [{
2180
+ type: ViewChild,
2181
+ args: [AppTopbar]
2182
+ }] } });
2183
+
2184
+ const GreenTheme = definePreset(Material, {
2185
+ semantic: {
2186
+ colorScheme: {
2187
+ light: {
2188
+ primary: {
2189
+ color: '#01712c',
2190
+ inverseColor: '#119744',
2191
+ hoverColor: '#119744',
2192
+ activeColor: '#119744',
2193
+ },
2194
+ highlight: {
2195
+ background: '#e2e8f0',
2196
+ focusBackground: '#e2e8f0',
2197
+ color: '#01712c',
2198
+ focusColor: '#01712c',
2199
+ },
2200
+ },
2201
+ },
2202
+ },
2203
+ });
2204
+
2205
+ const NavyBlueTheme = definePreset(Material, {
2206
+ semantic: {
2207
+ colorScheme: {
2208
+ light: {
2209
+ primary: {
2210
+ color: '#3535cd',
2211
+ inverseColor: '#0707a9',
2212
+ hoverColor: '#0707a9',
2213
+ activeColor: '#0707a9',
2214
+ },
2215
+ highlight: {
2216
+ background: '#e2e8f0',
2217
+ focusBackground: '#e2e8f0',
2218
+ color: '#3535cd',
2219
+ focusColor: '#3535cd',
2220
+ },
2221
+ },
2222
+ },
2223
+ },
2224
+ });
2225
+
2226
+ // Components
2227
+
2228
+ /**
2229
+ * Generated bundle index. Do not edit.
2230
+ */
2231
+
2232
+ export { AppCompanyBranchSelector, AppConfigurator, AppFloatingConfigurator, AppFooter, AppLauncher, AppLayout, AppMenu, AppMenuitem, AppProfile, AppSidebar, AppTopbar, GreenTheme, LAYOUT_AUTH_API, LAYOUT_AUTH_STATE, LayoutPersistenceService, LayoutService, NavyBlueTheme, filterAppsByPermissions, filterMenuByPermissions };
2233
+ //# sourceMappingURL=flusys-ng-layout.mjs.map