@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.
- package/assets/images/asset-access.svg +46 -0
- package/assets/images/asset-error.svg +74 -0
- package/assets/images/default/default-audio.png +0 -0
- package/assets/images/default/default-code.png +0 -0
- package/assets/images/default/default-file.png +0 -0
- package/assets/images/default/default-image.jpg +0 -0
- package/assets/images/default/default-pdf.png +0 -0
- package/assets/images/default/default-txt.png +0 -0
- package/assets/images/default/default-video.png +0 -0
- package/assets/images/default/default-zip.png +0 -0
- package/assets/images/logo.svg +377 -0
- package/assets/layout/_core.scss +24 -0
- package/assets/layout/_footer.scss +8 -0
- package/assets/layout/_forms.scss +54 -0
- package/assets/layout/_main.scss +13 -0
- package/assets/layout/_menu.scss +181 -0
- package/assets/layout/_mixins.scss +15 -0
- package/assets/layout/_preloading.scss +47 -0
- package/assets/layout/_responsive.scss +110 -0
- package/assets/layout/_topbar.scss +160 -0
- package/assets/layout/_typography.scss +68 -0
- package/assets/layout/_utils.scss +25 -0
- package/assets/layout/layout.scss +14 -0
- package/assets/layout/variables/_common.scss +20 -0
- package/assets/layout/variables/_dark.scss +5 -0
- package/assets/layout/variables/_light.scss +5 -0
- package/assets/style.scss +39 -0
- package/fesm2022/flusys-ng-layout.mjs +2233 -0
- package/fesm2022/flusys-ng-layout.mjs.map +1 -0
- package/package.json +28 -0
- package/types/flusys-ng-layout.d.ts +525 -0
|
@@ -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
|