Initial commit

This commit is contained in:
Gagan7900
2026-07-13 11:57:09 +05:30
commit ac629ef540
26616 changed files with 795848 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
import { computed, Injectable, inject, signal } from '@angular/core';
import { BehaviorSubject, Observable, catchError, finalize, map, shareReplay, throwError } from 'rxjs';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { LoginRequest, LoginResponse, UserProfile } from '../../models/auth/auth.model';
import { TokenStorageService } from './token-storage.service';
import { AppContextService } from '../../services/context/app-context.service';
import { AUTH_ENDPOINTS } from '../../../core/end-points/auth/auth.endpoints';
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly http = inject(HttpClient);
private readonly tokenStorage = inject(TokenStorageService);
private readonly appContextService = inject(AppContextService);
private readonly userSubject = new BehaviorSubject<UserProfile | null>(null);
readonly user$ = this.userSubject.asObservable();
readonly currentUserSignal = signal<UserProfile | null>(null);
private readonly accessTokenSignal = signal<string | null>(null);
readonly isAuthenticatedSignal = computed(() => !!this.accessTokenSignal() && !!this.currentUserSignal());
private refreshRequest$: Observable<LoginResponse> | null = null;
constructor() {
this.restoreAuthState();
}
login(payload: LoginRequest, rememberMe: boolean): Observable<LoginResponse> {
return this.http.post<LoginResponse>(`${AUTH_ENDPOINTS.login}`, payload).pipe(
map((response) => {
const user = this.normalizeUserProfile(response);
this.tokenStorage.saveAuth(response, rememberMe);
this.setAuthState(user, this.tokenStorage.getAccessToken());
return response;
})
);
}
refreshAccessToken(): Observable<LoginResponse> {
if (this.refreshRequest$) {
return this.refreshRequest$;
}
const refreshToken = this.tokenStorage.getRefreshToken();
if (!refreshToken) {
this.logout();
return throwError(() => new HttpErrorResponse({ status: 401, statusText: 'Refresh token missing' }));
}
this.refreshRequest$ = this.http.post<LoginResponse>(`${AUTH_ENDPOINTS.refresh}`, { refreshToken }).pipe(
map((response) => {
const user = this.normalizeUserProfile(response, this.currentUserSignal());
const storageType = this.tokenStorage.getStorageType();
const rememberMe = storageType === 'local';
this.tokenStorage.saveAuth(response, rememberMe);
this.setAuthState(user, this.tokenStorage.getAccessToken());
return response;
}),
catchError((error) => {
this.logout();
return throwError(() => error);
}),
finalize(() => {
this.refreshRequest$ = null;
}),
shareReplay(1)
);
return this.refreshRequest$;
}
logout(): void {
this.tokenStorage.clearAuth();
this.appContextService.clearContext();
this.setAuthState(null, null);
this.refreshRequest$ = null;
}
get accessToken(): string | null {
return this.tokenStorage.getAccessToken();
}
get refreshToken(): string | null {
return this.tokenStorage.getRefreshToken();
}
get isAuthenticated(): boolean {
return this.isAuthenticatedSignal();
}
get isLoggedIn(): boolean {
return this.isAuthenticatedSignal();
}
get currentUser(): UserProfile | null {
return this.currentUserSignal();
}
private restoreAuthState(): void {
const storedUser = this.tokenStorage.getUser();
const storedAccessToken = this.tokenStorage.getAccessToken();
if (storedUser && storedAccessToken) {
this.setAuthState(storedUser, storedAccessToken);
return;
}
this.setAuthState(null, storedAccessToken ?? null);
}
private setAuthState(user: UserProfile | null, token: string | null): void {
this.userSubject.next(user);
this.currentUserSignal.set(user);
this.accessTokenSignal.set(token);
}
private normalizeUserProfile(response: LoginResponse, fallbackUser: UserProfile | null = null): UserProfile | null {
if (response.user) {
return response.user;
}
const userId = response.userId?.trim();
const email = response.email?.trim();
if (userId || email) {
return {
id: userId ?? fallbackUser?.id ?? '',
email: email ?? fallbackUser?.email ?? '',
roles: response.roles,
};
}
return fallbackUser;
}
}
@@ -0,0 +1,154 @@
import { DOCUMENT } from '@angular/common';
import { DestroyRef, effect, inject, Injectable, OnDestroy, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Router } from '@angular/router';
import { merge, fromEvent, Subscription } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
import { environment } from '../../../../environments/environment';
import { AuthService } from '../auth/auth.service';
@Injectable()
export class SessionTimeoutService implements OnDestroy {
private readonly document = inject(DOCUMENT);
private readonly router = inject(Router);
private readonly authService = inject(AuthService);
private readonly destroyRef = inject(DestroyRef);
readonly showWarning = signal(false);
readonly remainingSeconds = signal(0);
private readonly warningAfterMs = environment.sessionTimeout?.warningAfterMs ?? 25 * 60 * 1000;
private readonly logoutAfterMs = environment.sessionTimeout?.logoutAfterMs ?? 30 * 60 * 1000;
private activitySubscription: Subscription | null = null;
private warningTimer: ReturnType<typeof setTimeout> | null = null;
private logoutTimer: ReturnType<typeof setTimeout> | null = null;
private countdownTimer: ReturnType<typeof setInterval> | null = null;
private logoutDeadline = 0;
private trackingEnabled = false;
constructor() {
effect(() => {
if (this.authService.currentUserSignal()) {
this.start();
return;
}
this.stop();
});
}
start(): void {
if (this.trackingEnabled) {
this.resetTimers();
return;
}
this.trackingEnabled = true;
this.bindActivityTracking();
this.resetTimers();
}
stop(): void {
this.trackingEnabled = false;
this.showWarning.set(false);
this.remainingSeconds.set(0);
this.clearTimers();
this.activitySubscription?.unsubscribe();
this.activitySubscription = null;
}
staySignedIn(): void {
if (!this.trackingEnabled) {
return;
}
this.resetTimers();
}
logoutNow(): void {
this.handleTimeoutLogout();
}
ngOnDestroy(): void {
this.stop();
}
private bindActivityTracking(): void {
if (this.activitySubscription) {
return;
}
this.activitySubscription = merge(
fromEvent(this.document, 'mousemove'),
fromEvent(this.document, 'keydown'),
fromEvent(this.document, 'click'),
fromEvent(window, 'scroll')
)
.pipe(throttleTime(1000), takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
if (!this.trackingEnabled || !this.authService.currentUserSignal()) {
return;
}
this.resetTimers();
});
}
private resetTimers(): void {
if (!this.trackingEnabled) {
return;
}
this.clearTimers();
this.showWarning.set(false);
this.remainingSeconds.set(0);
const safeWarningDelay = Math.max(Math.min(this.warningAfterMs, this.logoutAfterMs), 0);
const safeLogoutDelay = Math.max(this.logoutAfterMs, 0);
this.warningTimer = setTimeout(() => {
this.showWarning.set(true);
this.logoutDeadline = Date.now() + Math.max(safeLogoutDelay - safeWarningDelay, 0);
this.updateRemainingSeconds();
this.countdownTimer = setInterval(() => {
this.updateRemainingSeconds();
}, 1000);
}, safeWarningDelay);
this.logoutTimer = setTimeout(() => {
this.handleTimeoutLogout();
}, safeLogoutDelay);
}
private updateRemainingSeconds(): void {
const remainingMs = Math.max(this.logoutDeadline - Date.now(), 0);
this.remainingSeconds.set(Math.ceil(remainingMs / 1000));
}
private handleTimeoutLogout(): void {
const returnUrl = this.router.url.startsWith('/auth') ? '/' : this.router.url;
this.stop();
this.authService.logout();
void this.router.navigate(['/auth/login'], {
queryParams: returnUrl && returnUrl !== '/' ? { returnUrl } : undefined,
});
}
private clearTimers(): void {
if (this.warningTimer) {
clearTimeout(this.warningTimer);
this.warningTimer = null;
}
if (this.logoutTimer) {
clearTimeout(this.logoutTimer);
this.logoutTimer = null;
}
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
}
}
@@ -0,0 +1,152 @@
import { Injectable } from '@angular/core';
import { LoginResponse, UserProfile } from '../../models/auth/auth.model';
type StorageType = 'local' | 'session';
@Injectable({ providedIn: 'root' })
export class TokenStorageService {
private readonly accessTokenKey = 'master-admin-access-token';
private readonly refreshTokenKey = 'master-admin-refresh-token';
private readonly accessTokenExpiresOnKey = 'master-admin-access-token-expires-on';
private readonly refreshTokenExpiresOnKey = 'master-admin-refresh-token-expires-on';
private readonly userKey = 'master-admin-user';
saveAuth(response: LoginResponse, rememberMe: boolean): void {
this.clearAuth();
const selectedStorage = rememberMe ? localStorage : sessionStorage;
if (response.accessToken) {
selectedStorage.setItem(this.accessTokenKey, response.accessToken);
}
if (response.refreshToken) {
selectedStorage.setItem(this.refreshTokenKey, response.refreshToken);
}
if (response.accessTokenExpiresOn) {
selectedStorage.setItem(this.accessTokenExpiresOnKey, response.accessTokenExpiresOn);
}
if (response.refreshTokenExpiresOn) {
selectedStorage.setItem(this.refreshTokenExpiresOnKey, response.refreshTokenExpiresOn);
}
const user = this.buildUserProfile(response);
if (user) {
selectedStorage.setItem(this.userKey, JSON.stringify(user));
}
}
clearAuth(): void {
this.removeFromStorage(localStorage);
this.removeFromStorage(sessionStorage);
}
getAccessToken(): string | null {
return this.getByPriority(this.accessTokenKey);
}
getRefreshToken(): string | null {
return this.getByPriority(this.refreshTokenKey);
}
getAccessTokenExpiresOn(): string | null {
return this.getByPriority(this.accessTokenExpiresOnKey);
}
getRefreshTokenExpiresOn(): string | null {
return this.getByPriority(this.refreshTokenExpiresOnKey);
}
getUser(): UserProfile | null {
const raw = this.getByPriority(this.userKey);
if (!raw) {
return null;
}
try {
return JSON.parse(raw) as UserProfile;
} catch {
this.clearAuth();
return null;
}
}
getStorageType(): 'local' | 'session' | null {
if (this.hasAnyAuthKey(sessionStorage)) {
return 'session';
}
if (this.hasAnyAuthKey(localStorage)) {
return 'local';
}
return null;
}
isAccessTokenExpired(): boolean {
return this.isExpired(this.getAccessTokenExpiresOn());
}
isRefreshTokenExpired(): boolean {
return this.isExpired(this.getRefreshTokenExpiresOn());
}
private getByPriority(key: string): string | null {
const fromSession = sessionStorage.getItem(key);
if (fromSession !== null) {
return fromSession;
}
return localStorage.getItem(key);
}
private hasAnyAuthKey(storage: Storage): boolean {
return [
this.accessTokenKey,
this.refreshTokenKey,
this.accessTokenExpiresOnKey,
this.refreshTokenExpiresOnKey,
this.userKey,
].some((key) => storage.getItem(key) !== null);
}
private removeFromStorage(storage: Storage): void {
storage.removeItem(this.accessTokenKey);
storage.removeItem(this.refreshTokenKey);
storage.removeItem(this.accessTokenExpiresOnKey);
storage.removeItem(this.refreshTokenExpiresOnKey);
storage.removeItem(this.userKey);
}
private isExpired(expiresOn: string | null): boolean {
debugger;
if (!expiresOn) {
return true;
}
const parsedExpiry = Date.parse(expiresOn);
if (Number.isNaN(parsedExpiry)) {
return true;
}
return parsedExpiry <= Date.now();
}
private buildUserProfile(response: LoginResponse): UserProfile | null {
if (response.user) {
return response.user;
}
if (response.userId || response.email) {
return {
id: response.userId ?? '',
email: response.email ?? '',
roles: response.roles,
};
}
return null;
}
}
@@ -0,0 +1,321 @@
import { DOCUMENT, ElementRef, inject, Injectable, Renderer2 } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
interface StateType {
direction: string;
theme: string;
navigationStyles: string, // vertical, horizontal
menuStyles: string, // menu-click, menu-hover, icon-click, icon-hover
layoutStyles: string, // double-menu, detached, icon-overlay, icontext-menu, closed-menu, default-menu
pageStyles: string, // regular, classic, modern
widthStyles: string, // fullwidth, boxed
menuPosition: string, // fixed, scrollable
headerPosition: string, // fixed, scrollable
menuColor: string, // light, dark, color, gradient, transparent
headerColor: string, // light, dark, color, gradient, transparent
themePrimary: string, // '58, 88, 146', '92, 144, 163', '161, 90, 223', '78, 172, 76', '223, 90, 90'
themeBackground: string,
backgroundImage: string,
};
@Injectable({
providedIn: 'root'
})
export class AppStateService {
private readonly localStorageKey = 'Ynex-ng'; // Customize this key
private initialState: StateType = {
theme: 'light', // light, dark
direction: 'ltr', // ltr, rtl
navigationStyles: 'vertical', // vertical, horizontal
menuStyles: '', // menu-click, menu-hover, icon-click, icon-hover
layoutStyles: 'default', // double-menu, detached, icon-overlay, icontext-menu, closed-menu, default-menu
pageStyles: 'regular', // regular, classic, modern
widthStyles: 'fullwidth', // fullwidth, boxed
menuPosition: 'fixed', // fixed, scrollable
headerPosition: 'fixed', // fixed, scrollable
menuColor: 'dark', // light, dark, color, gradient, transparent
headerColor: 'light', // light, dark, color, gradient, transparent
themePrimary: '', // '58, 88, 146', '92, 144, 163', '161, 90, 223', '78, 172, 76', '223, 90, 90'
themeBackground: '',
backgroundImage: '', // bgimg1, bgimg2, bgimg3, bgimg4, bgimg5
} // Store initial state
private stateSubject = new BehaviorSubject<StateType>(this.initialState); // Use any for initial null value
state$ = this.stateSubject.asObservable();
private document = inject(DOCUMENT);
navigationStyles: any;
private html = this.document.documentElement;
constructor() {
const initialState: StateType = this.getInitialStateFromLocalStorage();
// this.initializeState();
this.stateSubject.next(initialState);
}
private getInitialStateFromLocalStorage(): StateType {
try {
const storedState = localStorage.getItem(this.localStorageKey);
if (storedState) {
return JSON.parse(storedState);
}
} catch (error) {
console.error('Error retrieving initial state from local storage:', error);
}
return this.initialState;
}
getupdateState() {
const currentState = this.stateSubject.getValue();
return currentState
}
updateState(newState?: Partial<any>) { // Use any for partial updates
const currentState = this.stateSubject.getValue(); // Get current state
if (!currentState) {
// Handle initial update case (no state emitted yet)
this.updateStateAndEmit(newState);
return;
}
if (newState) {
const updatedState = { ...currentState, ...newState }; // Merge updates
this.updateStateAndEmit(updatedState); // Update and emit combined state
} else {
this.updateStateAndEmit(currentState);
return;
}
}
private state: { [key: string]: any } = {};
getState(menuStyles: string): any {
return this.state[menuStyles];
}
private applyThemeBackgroundSpecificChanges(background: any) {
this.html?.style.setProperty('--color-bodybg', background.main);
this.html?.style.setProperty('--color-bodybg2', background.secondary);
this.html?.style.setProperty('--color-light', background.accent);
this.html?.style.setProperty('--color-formcontrolbg', `rgba(${background.accent})`);
this.html?.style.setProperty('--color-inputborder', background.overlay);
this.html?.style.setProperty('--color-gray3', background.primary);
this.applythemeSpecificChanges(background.theme);
}
private applyDirectionSpecificChanges(direction: string) {
this.html?.setAttribute('dir', direction);
}
private applythemeSpecificChanges(theme: string) {
this.html?.setAttribute('class', theme); //setting theme style
this.html?.setAttribute('data-header-styles', theme); //setting header style
}
private applyNavigationStylesSpecificChanges(navigationStyles: string) {
this.html?.setAttribute('data-nav-layout', navigationStyles);
if (navigationStyles == 'horizontal') {
this.html?.setAttribute('data-nav-style', 'menu-click');
this.html?.removeAttribute('data-vertical-style');
}
}
private applyMenuStylesSpecificChanges(menuStyles: string) {
this.html?.setAttribute('data-nav-style', menuStyles);
this.html?.setAttribute('data-toggled', menuStyles + '-closed');
this.html?.removeAttribute('data-vertical-style');
}
private applyLayoutStylesSpecificChanges(layoutStyles: string) {
this.html?.setAttribute('data-vertical-style', layoutStyles);
this.html?.removeAttribute('data-nav-style');
switch (layoutStyles) {
case 'default':
this.html?.setAttribute('data-vertical-style', 'overlay');
this.html?.setAttribute('data-toggled', '');
break;
case 'closed':
this.html?.setAttribute('data-toggled', 'close-menu-close');
break;
case 'icontext':
this.html?.setAttribute('data-toggled', 'icon-text-close');
break;
case 'overlay':
this.html?.setAttribute('data-toggled', 'icon-overlay-close');
break;
case 'detached':
this.html?.setAttribute('data-toggled', 'detached-close');
break;
case 'doublemenu':
this.html?.setAttribute('data-toggled', 'double-menu-open');
break;
}
if (layoutStyles === 'icon-text') {
this.html?.setAttribute('icon-text', 'open');
} else {
// If not 'icon-text', remove the icon-text attribute
this.html?.removeAttribute('icon-text');
}
}
private applypageStylesSpecificChanges(pageStyles: string) {
this.html?.setAttribute('data-page-style', pageStyles);
const slideRight = document.querySelector('.slide-right') as HTMLElement | null;
if (slideRight) {
// If the element exists, toggle the 'd-none' class
if (slideRight.classList.contains('d-none')) {
slideRight.classList.remove('d-none');
} else {
slideRight.classList.add('d-none');
}
} else {
// If the element does not exist (is null), create a safe fallback by adding 'd-none'
const dummySlideRight = document.createElement('div');
dummySlideRight.classList.add('slide-right', 'd-none'); // Add classes to the new element
document.body.appendChild(dummySlideRight); // Append it to the DOM as a fallback
}
}
private applywidthStylesSpecificChanges(widthStyles: string) {
this.html?.setAttribute('data-width', widthStyles);
}
private applymenuPositionSpecificChanges(menuPosition: string) {
this.html?.setAttribute('data-menu-position', menuPosition);
}
private applyheaderPositionSpecificChanges(headerPosition: string) {
this.html?.setAttribute('data-header-position', headerPosition);
}
private applyheaderColorSpecificChanges(headerColor: string) {
this.html?.setAttribute('data-header-styles', headerColor);
}
private applymenuColorSpecificChanges(menuColor: string) {
this.html?.setAttribute('data-menu-styles', menuColor);
}
private applyPrimarySpecificChanges(primary: string) {
this.html?.style.setProperty('--color-primaryrgb', primary);
this.html?.style.setProperty('--color-primary', primary);
}
private applybackgroundImageSpecificChanges(backgroundImage: string) {
this.html?.setAttribute('bg-img', backgroundImage);
}
public applyReset() {
if (this.html) {
this.html?.style.removeProperty('--color-bodybg');
this.html?.style.removeProperty('--color-gray3');
this.html?.style.removeProperty('--color-bodybg2');
this.html?.style.removeProperty('--color-light');
this.html?.style.removeProperty('--color-formcontrolbg');
this.html?.style.removeProperty('--color-inputborder');
this.html?.style.removeProperty('--color-primary');
this.html?.style.removeProperty('--color-primaryrgb');
}
this.html?.removeAttribute('bg-img');
this.html?.setAttribute('data-vertical-style', 'overlay');
this.stateSubject.next(this.initialState);
this.updateStateAndEmit(this.initialState);
localStorage.clear();
if (window.innerWidth <= 992) {
this.html?.setAttribute('data-toggled', 'close');
}
}
private updateStateAndEmit(state: any) {
// Conditional logic based on direction changes
const currentState = this.stateSubject.getValue(); // Get current state
// Conditional logic based on theme changes
if (state['theme']) {
this.applythemeSpecificChanges(state['theme']);
}
if (state['direction']) {
this.applyDirectionSpecificChanges(state['direction']);
}
// Conditional logic based on theme changes
if (state['navigationStyles']) {
this.applyNavigationStylesSpecificChanges(state['navigationStyles']);
}
// Conditional logic based on theme changes
if (state['menuStyles'] && !state['layoutStyles']) {
this.applyMenuStylesSpecificChanges(state['menuStyles']);
}
if (state['layoutStyles'] && !state['menuStyles']) {
this.applyLayoutStylesSpecificChanges(state['layoutStyles']);
}
if (state['pageStyles']) {
this.applypageStylesSpecificChanges(state['pageStyles']);
}
if (state['widthStyles']) {
this.applywidthStylesSpecificChanges(state['widthStyles']);
}
if (state['menuPosition']) {
this.applymenuPositionSpecificChanges(state['menuPosition']);
}
if (state['headerPosition']) {
this.applyheaderPositionSpecificChanges(state['headerPosition']);
}
if (state['themePrimary']) {
this.applyPrimarySpecificChanges(state['themePrimary']);
}
if (state['themeBackground']) {
this.applyThemeBackgroundSpecificChanges(state['themeBackground']);
}
if (state['headerColor']) {
this.applyheaderColorSpecificChanges(state['headerColor']);
}
if (state['menuColor']) {
this.applymenuColorSpecificChanges(state['menuColor']);
}
if (state['backgroundImage']) {
this.applybackgroundImageSpecificChanges(state['backgroundImage']);
}
this.stateSubject.next(state);
this.updateLocalStorage(state);
}
private updateLocalStorage(state: any) {
try {
localStorage.setItem(this.localStorageKey, JSON.stringify(state));
} catch (error) {
console.error('Error saving state to local storage:', error);
}
}
}
@@ -0,0 +1,33 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { API_CONFIG } from '../../config/api.config';
@Injectable({ providedIn: 'root' })
export class BaseApiService {
private readonly http = inject(HttpClient);
protected get<T>(resource: string, params?: Record<string, string | number | boolean>): Observable<T> {
let httpParams = new HttpParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
httpParams = httpParams.set(key, String(value));
});
}
return this.http.get<T>(`${API_CONFIG.baseUrl}${resource}`, { params: httpParams });
}
protected post<T, R = unknown>(resource: string, body: T): Observable<R> {
return this.http.post<R>(`${API_CONFIG.baseUrl}${resource}`, body);
}
protected put<T, R = unknown>(resource: string, body: T): Observable<R> {
return this.http.put<R>(`${API_CONFIG.baseUrl}${resource}`, body);
}
protected delete<R = unknown>(resource: string): Observable<R> {
return this.http.delete<R>(`${API_CONFIG.baseUrl}${resource}`);
}
}
@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { AngularFireModule } from '@angular/fire/compat';
import { AngularFirestoreModule } from '@angular/fire/compat/firestore';
import { AngularFireDatabaseModule } from '@angular/fire/compat/database';
import { AngularFireAuthModule } from '@angular/fire/compat/auth';
import { environment } from '../../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class FirebaseService {
constructor() {
AngularFireModule.initializeApp(environment.firebase);
}
getFirestore() {
return AngularFirestoreModule;
}
getDatabase() {
return AngularFireDatabaseModule;
}
getAuth() {
return AngularFireAuthModule;
}
}
@@ -0,0 +1,24 @@
import { Injectable, signal} from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class LoadingService {
private requestCount = 0;
readonly isLoading = signal(false);
show(): void {
this.requestCount++;
this.isLoading.set(true);
}
hide(): void {
this.requestCount--;
if (this.requestCount <= 0) {
this.requestCount = 0;
this.isLoading.set(false);
}
}
}
+78
View File
@@ -0,0 +1,78 @@
import { MenuContext } from '../../models/context/context.model';
export const SAAS_MENU_DATA: MenuContext = {
defaultLandingPage: '/dashboards/crm',
items: [
{ headTitle: 'MAIN' },
{
title: 'Dashboards',
icon: '<i class="bx bx-home side-menu__icon"></i>',
type: 'sub',
active: false,
selected: false,
dirchange: false,
children: [
{ path: '/dashboards/crm', title: 'CRM', type: 'link', dirchange: false },
],
},
{ headTitle: 'SAAS ADMIN' },
{
title: 'Management',
icon: '<i class="bx bx-buildings side-menu__icon"></i>',
type: 'sub',
active: false,
selected: false,
dirchange: false,
children: [
{ path: '/tenants', title: 'Tenants', type: 'link', dirchange: false },
{ path: '/users', title: 'Users', type: 'link', dirchange: false },
{
title: 'Global Master',
type: 'sub',
active: false,
selected: false,
dirchange: false,
children: [
{ path: '/global-masters/countries', title: 'Country', type: 'link', dirchange: false },
{ path: '/global-masters/states', title: 'State', type: 'link', dirchange: false },
{ path: '/global-masters/cities', title: 'City', type: 'link', dirchange: false },
],
},
{
title: 'Configuration',
type: 'sub',
active: false,
selected: false,
dirchange: false,
children: [
{ path: '/localization', title: 'Localization', type: 'link', dirchange: false },
{
title: 'Branding',
type: 'sub',
active: false,
selected: false,
dirchange: false,
children: [
{ path: '/theming', title: 'Theming', type: 'link', dirchange: false },
{ path: '/platform', title: 'Platform', type: 'link', dirchange: false },
],
},
],
},
],
},
{
title: 'Operations',
icon: '<i class="bx bx-line-chart side-menu__icon"></i>',
type: 'sub',
active: false,
selected: false,
dirchange: false,
children: [
{ path: '/billing', title: 'Billing', type: 'link', dirchange: false },
{ path: '/monitoring', title: 'Monitoring', type: 'link', dirchange: false },
],
},
],
};
@@ -0,0 +1,42 @@
import { Injectable, signal } from '@angular/core';
import { Observable, of, tap } from 'rxjs';
import { MenuContext } from '../../models/context/context.model';
import { SAAS_MENU_DATA } from './menu.data';
import { Menu } from '../../../core/services/common/nav.service';
@Injectable({ providedIn: 'root' })
export class MenuService {
readonly menuContext = signal<MenuContext | null>(null);
loadMenu(): Observable<MenuContext> {
const context = this.cloneMenuContext(SAAS_MENU_DATA);
return of(context).pipe(
tap((menuContext) => {
this.menuContext.set(menuContext);
})
);
}
getNavigationMenu(): Menu[] {
return this.cloneMenuItems(this.menuContext()?.items ?? []);
}
getDefaultLandingPage(): string | null {
return this.menuContext()?.defaultLandingPage ?? null;
}
clear(): void {
this.menuContext.set(null);
}
private cloneMenuContext(context: MenuContext): MenuContext {
return {
defaultLandingPage: context.defaultLandingPage,
items: this.cloneMenuItems(context.items),
};
}
private cloneMenuItems(items: Menu[]): Menu[] {
return JSON.parse(JSON.stringify(items)) as Menu[];
}
}
+102
View File
@@ -0,0 +1,102 @@
import { Injectable, OnDestroy } from '@angular/core';
import { Subject, BehaviorSubject, fromEvent } from 'rxjs';
import { takeUntil, debounceTime } from 'rxjs/operators';
import { Router } from '@angular/router';
// Menu
export interface Menu {
headTitle?: string;
headTitle2?: string;
path?: string;
title?: string;
icon?: string;
type?: string;
badgeValue?: string;
badgeClass?: string;
badgeText?: string;
active?: boolean;
selected?: boolean;
bookmark?: boolean;
children?: Menu[];
children2?: Menu[];
Menusub?: boolean;
target?: boolean;
menutype?: string,
dirchange?: boolean,
nochild?: any
}
@Injectable({
providedIn: 'root',
})
export class NavService implements OnDestroy {
private unsubscriber: Subject<any> = new Subject();
public screenWidth: BehaviorSubject<number> = new BehaviorSubject(
window.innerWidth
);
// Search Box
public search = false;
// Language
public language = false;
// Mega Menu
public megaMenu = false;
public levelMenu = false;
public megaMenuColapse: boolean = window.innerWidth < 1199 ? true : false;
// Collapse Sidebar
public collapseSidebar: boolean = window.innerWidth < 991 ? true : false;
// For Horizontal Layout Mobile
public horizontal: boolean = window.innerWidth < 991 ? false : true;
// Full screen
public fullScreen = false;
active: any;
constructor(private router: Router) {
this.setScreenWidth(window.innerWidth);
fromEvent(window, 'resize')
.pipe(debounceTime(1000), takeUntil(this.unsubscriber))
.subscribe((evt: any) => {
this.setScreenWidth(evt.target.innerWidth);
if (evt.target.innerWidth < 991) {
this.collapseSidebar = true;
this.megaMenu = false;
this.levelMenu = false;
}
if (evt.target.innerWidth < 1199) {
this.megaMenuColapse = true;
}
});
if (window.innerWidth < 991) {
// Detect Route change sidebar close
this.router.events.subscribe((event) => {
this.collapseSidebar = true;
this.megaMenu = false;
this.levelMenu = false;
});
}
}
ngOnDestroy() {
this.unsubscriber.next;
this.unsubscriber.complete();
}
private setScreenWidth(width: number): void {
this.screenWidth.next(width);
}
items = new BehaviorSubject<Menu[]>([]);
setMenuItems(menuItems: Menu[]): void {
this.items.next(menuItems);
}
clearMenuItems(): void {
this.items.next([]);
}
}
@@ -0,0 +1,33 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject, signal } from '@angular/core';
import { map, Observable, tap } from 'rxjs';
import { PermissionContext } from '../../models/context/context.model';
interface PermissionResponse {
roles?: string[];
permissions?: string[];
defaultLandingPage?: string;
}
@Injectable({ providedIn: 'root' })
export class PermissionService {
private readonly http = inject(HttpClient);
readonly permissionContext = signal<PermissionContext | null>(null);
loadPermissions(): Observable<PermissionContext> {
return this.http.get<PermissionResponse>(``).pipe(
map((response) => ({
roles: response.roles ?? [],
permissions: response.permissions ?? [],
defaultLandingPage: response.defaultLandingPage?.trim() || undefined,
})),
tap((context) => {
this.permissionContext.set(context);
})
);
}
clear(): void {
this.permissionContext.set(null);
}
}
@@ -0,0 +1,104 @@
import { Injectable, inject } from '@angular/core';
import { finalize, forkJoin, Observable, of, shareReplay, tap } from 'rxjs';
import { AppContextState, MenuContext } from '../../models/context/context.model';
import { MenuService } from '../common/menu.service';
import { PermissionService } from '../common/permission.service';
import { TenantContextService } from './tenant-context.service';
import { UserContextService } from './user-context.service';
import { NavService } from '../common/nav.service';
@Injectable({ providedIn: 'root' })
export class AppContextService {
private readonly userContextService = inject(UserContextService);
private readonly tenantContextService = inject(TenantContextService);
private readonly permissionService = inject(PermissionService);
private readonly menuService = inject(MenuService);
private readonly navService = inject(NavService);
private loadRequest$: Observable<AppContextState> | null = null;
private menuLoadRequest$: Observable<MenuContext> | null = null;
private loaded = false;
ensureMenuInitialized(forceReload = false): Observable<MenuContext> {
const currentMenu = this.menuService.menuContext();
if (currentMenu && !forceReload) {
this.navService.setMenuItems(this.menuService.getNavigationMenu());
return of(currentMenu);
}
if (this.menuLoadRequest$ && !forceReload) {
return this.menuLoadRequest$;
}
this.menuLoadRequest$ = this.menuService.loadMenu().pipe(
tap(() => {
this.navService.setMenuItems(this.menuService.getNavigationMenu());
}),
finalize(() => {
this.menuLoadRequest$ = null;
}),
shareReplay(1)
);
return this.menuLoadRequest$;
}
loadAppContext(forceReload = false): Observable<AppContextState> {
if (this.loaded && !forceReload) {
return of(this.getCurrentState());
}
if (this.loadRequest$ && !forceReload) {
return this.loadRequest$;
}
this.loadRequest$ = forkJoin({
user: this.userContextService.loadCurrentUserProfile(),
tenant: this.tenantContextService.loadTenantContext(),
permissions: this.permissionService.loadPermissions(),
menu: this.menuService.loadMenu(),
}).pipe(
tap((context) => {
this.navService.setMenuItems(this.menuService.getNavigationMenu());
this.loaded = true;
}),
finalize(() => {
this.loadRequest$ = null;
}),
shareReplay(1)
);
return this.loadRequest$;
}
ensureContextLoaded(): Observable<AppContextState> {
return this.loadAppContext();
}
clearContext(): void {
this.userContextService.clear();
this.tenantContextService.clear();
this.permissionService.clear();
this.menuService.clear();
this.navService.clearMenuItems();
this.loaded = false;
this.loadRequest$ = null;
}
getDefaultLandingPage(): string {
return this.userContextService.currentUserContext()?.defaultLandingPage
?? this.permissionService.permissionContext()?.defaultLandingPage
?? this.tenantContextService.tenantContext()?.defaultLandingPage
?? this.menuService.getDefaultLandingPage()
?? '/dashboards/crm';
}
private getCurrentState(): AppContextState {
return {
user: this.userContextService.currentUserContext() ?? { id: '', email: '', roles: [] },
tenant: this.tenantContextService.tenantContext() ?? {},
permissions: this.permissionService.permissionContext() ?? { roles: [], permissions: [] },
menu: this.menuService.menuContext() ?? { items: [] },
};
}
}
@@ -0,0 +1,22 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject, signal } from '@angular/core';
import { Observable, tap } from 'rxjs';
import { TenantContext } from '../../models/context/context.model';
@Injectable({ providedIn: 'root' })
export class TenantContextService {
private readonly http = inject(HttpClient);
readonly tenantContext = signal<TenantContext | null>(null);
loadTenantContext(): Observable<TenantContext> {
return this.http.get<TenantContext>(``).pipe(
tap((context) => {
this.tenantContext.set(context);
})
);
}
clear(): void {
this.tenantContext.set(null);
}
}
@@ -0,0 +1,31 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject, signal } from '@angular/core';
import { map, Observable, tap } from 'rxjs';
import { CurrentUserContext } from '../../models/context/context.model';
@Injectable({ providedIn: 'root' })
export class UserContextService {
private readonly http = inject(HttpClient);
readonly currentUserContext = signal<CurrentUserContext | null>(null);
loadCurrentUserProfile(): Observable<CurrentUserContext> {
return this.http.get<Partial<CurrentUserContext>>(``).pipe(
map((response) => ({
id: response.id?.trim() ?? '',
email: response.email?.trim() ?? '',
displayName: response.displayName?.trim() || undefined,
fullName: response.fullName?.trim() || undefined,
tenantId: response.tenantId?.trim() || undefined,
roles: response.roles ?? [],
defaultLandingPage: response.defaultLandingPage?.trim() || undefined,
})),
tap((profile) => {
this.currentUserContext.set(profile);
})
);
}
clear(): void {
this.currentUserContext.set(null);
}
}
@@ -0,0 +1,17 @@
import { HttpClient } from "@angular/common/http";
import { Injectable, inject } from "@angular/core";
import { COUNTRY_ENDPOINTS } from "../../../core/end-points/country/country.endpoints";
import { Observable } from "rxjs";
import { DataTableQuery, DataTableResult } from "../../../shared/components/data-table/data-table.types";
@Injectable({
providedIn: 'root'
})
export class CountryService {
private readonly http = inject(HttpClient);
getCountryDataTable(query: DataTableQuery): Observable<DataTableResult<any>> {
return this.http.post<DataTableResult<any>>(`${COUNTRY_ENDPOINTS.dataTable}`, query);
}
}
@@ -0,0 +1,17 @@
import { HttpClient } from "@angular/common/http";
import { Injectable, inject } from "@angular/core";
import { STATE_ENDPOINTS } from "../../end-points/state/state.endpoints"
import { Observable } from "rxjs";
import { DataTableQuery, DataTableResult } from "../../../shared/components/data-table/data-table.types";
@Injectable({
providedIn: 'root'
})
export class StateService {
private readonly http = inject(HttpClient);
getStateDataTable(query: DataTableQuery, countryId: string): Observable<DataTableResult<any>> {
return this.http.post<DataTableResult<any>>(`${STATE_ENDPOINTS.dataTable}`, { ...query, countryId });
}
}