Publish from private repository

This commit is contained in:
Gitea Actions
2026-08-01 12:08:49 +00:00
commit 6f7fd61ee9
695 changed files with 69563 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
import { environment } from '../environments/environment';
const api = (environment.apiUrl ?? '').replace(/\/$/, '');
export class ApiRoutes {
static UserRegister = `${api}/api/user/register`;
static UserLogin = `${api}/api/user/login`;
static UserProfile = `${api}/api/user/profile`;
static UserAttach = `${api}/api/user/attach`;
static UserDeattach = `${api}/api/user/deattach`;
static GeneralCurrencies = `${api}/api/general/currencies`;
static SettingsCurrencies = `${api}/api/settings/currencies`;
static SettingsCurrency = `${api}/api/settings/currencies/:id`;
static SettingsCurrenciesRate = `${api}/api/settings/currencies/:id/rate`;
static SettingsAccountCategories = `${api}/api/settings/account-categories`;
static SettingsAccountCategory = `${api}/api/settings/account-categories/:id`;
static SettingsAccounts = `${api}/api/settings/accounts`;
static SettingsAccount = `${api}/api/settings/accounts/:id`;
static SettingsAccountAccesses = `${api}/api/settings/accounts/:id/access`;
static SettingsAccountAccess = `${api}/api/settings/accounts/:id/access/:access`;
static SettingsAccountAccountCategory = `${api}/api/settings/accounts/:id/category/:categoryId`;
static SettingsAccountInvites = `${api}/api/settings/accounts/invites`;
static SettingsAccountInviteAccept = `${api}/api/settings/accounts/invites/:id/accept`;
static SettingsAccountInviteReject = `${api}/api/settings/accounts/invites/:id/reject`;
static SettingsItemCategories = `${api}/api/settings/item-categories`;
static SettingsItemCategory = `${api}/api/settings/item-categories/:id`;
static SettingsItems = `${api}/api/settings/items`;
static SettingsItem = `${api}/api/settings/items/:id`;
static Accounts = `${api}/api/accounts`;
static Account = `${api}/api/accounts/:id`;
static Motions = `${api}/api/accounts/:id/motions`;
static Motion = `${api}/api/accounts/:id/motions/:motionId`;
static Items = `${api}/api/items`;
static Dashboard = `${api}/api/dashboard`;
static DashboardIncome = `${api}/api/dashboard/income`;
static DashboardOutcome = `${api}/api/dashboard/outcome`;
}
@@ -0,0 +1,47 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Page404Component } from './authentication/page404/page404.component';
import { AuthGuard } from './core/guard/auth.guard';
import { AuthLayoutComponent } from './layout/app-layout/auth-layout/auth-layout.component';
import { MainLayoutComponent } from './layout/app-layout/main-layout/main-layout.component';
const routes: Routes = [
{
path: '',
component: MainLayoutComponent,
canActivate: [AuthGuard],
children: [
{ path: '', redirectTo: '/authentication/signin', pathMatch: 'full' },
{
path: 'dashboard',
loadChildren: () =>
import('./dashboard/dashboard.module').then((m) => m.DashboardModule),
},
],
},
{
path: 'authentication',
component: AuthLayoutComponent,
loadChildren: () =>
import('./authentication/authentication.module').then(
(m) => m.AuthenticationModule
),
},
{
path: '',
component: MainLayoutComponent,
canActivate: [AuthGuard],
loadChildren: () =>
import('./pages/pages.module').then(
(m) => m.PagesModule
),
},
{ path: '**', component: Page404Component },
];
@NgModule({
imports: [RouterModule.forRoot(routes, {})],
exports: [RouterModule],
})
export class AppRoutingModule {
}
+2
View File
@@ -0,0 +1,2 @@
<app-page-loader></app-page-loader>
<router-outlet></router-outlet>
@@ -0,0 +1,39 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent',
() => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app',
() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'spire'`,
() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('spire');
});
it('should render title',
() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain('spire app is running!');
});
});
+30
View File
@@ -0,0 +1,30 @@
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Event, Router, RouterOutlet, NavigationStart, NavigationEnd } from '@angular/router';
import { PageLoaderComponent } from './layout/page-loader/page-loader.component';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [RouterOutlet, PageLoaderComponent],
})
export class AppComponent {
currentUrl!: string;
private readonly destroyRef = inject(DestroyRef);
constructor(public _router: Router) {
this._router.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((routerEvent: Event) => {
if (routerEvent instanceof NavigationStart) {
this.currentUrl = routerEvent.url.substring(
routerEvent.url.lastIndexOf('/') + 1
);
}
if (routerEvent instanceof NavigationEnd) {
/* empty */
}
window.scrollTo(0, 0);
});
}
}
+47
View File
@@ -0,0 +1,47 @@
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { LocationStrategy, HashLocationStrategy } from '@angular/common';
import { HTTP_INTERCEPTORS, HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
// libs
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { LoadingBarRouterModule } from '@ngx-loading-bar/router';
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client';
import { NgScrollbarModule } from 'ngx-scrollbar';
// app
import { CoreModule } from './core/core.module';
import { AppRoutingModule } from './app-routing.module';
import { ErrorInterceptor } from './core/interceptor/error.interceptor';
import { UpdateDateHttpInterceptor } from './core/interceptor/update.date.http.interceptor ';
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, 'assets/i18n/', '.json');
}
export const appConfig: ApplicationConfig = {
providers: [
importProvidersFrom(
BrowserAnimationsModule,
AppRoutingModule,
LoadingBarHttpClientModule,
LoadingBarRouterModule,
NgScrollbarModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient],
},
}),
// core & shared
CoreModule,
),
{ provide: LocationStrategy, useClass: HashLocationStrategy },
// Bearer tokens: angular-oauth2-oidc resourceServer (AuthModuleConfig.sendAccessToken)
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: UpdateDateHttpInterceptor, multi: true },
provideHttpClient(withInterceptorsFromDi()),
],
};
@@ -0,0 +1,47 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SigninComponent } from './signin/signin.component';
import { SignupComponent } from './signup/signup.component';
import { ForgotPasswordComponent } from './forgot-password/forgot-password.component';
import { LockedComponent } from './locked/locked.component';
import { Page404Component } from './page404/page404.component';
import { Page500Component } from './page500/page500.component';
const routes: Routes = [
{
path: '',
redirectTo: 'signin',
pathMatch: 'full',
},
{
path: 'signin',
component: SigninComponent,
},
{
path: 'signup',
component: SignupComponent,
},
{
path: 'forgot-password',
component: ForgotPasswordComponent,
},
{
path: 'locked',
component: LockedComponent,
},
{
path: 'page404',
component: Page404Component,
},
{
path: 'page500',
component: Page500Component,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AuthenticationRoutingModule {
}
@@ -0,0 +1,50 @@
// angular
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
// libs
import { GoogleSigninButtonModule } from '@abacritt/angularx-social-login';
// app
import { AuthenticationRoutingModule } from './authentication-routing.module';
import { Page500Component } from './page500/page500.component';
import { Page404Component } from './page404/page404.component';
import { SigninComponent } from './signin/signin.component';
import { SignupComponent } from './signup/signup.component';
import { LockedComponent } from './locked/locked.component';
import { ForgotPasswordComponent } from './forgot-password/forgot-password.component';
@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
AuthenticationRoutingModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatButtonModule,
GoogleSigninButtonModule,
],
declarations: [
Page500Component,
Page404Component,
SigninComponent,
SignupComponent,
LockedComponent,
ForgotPasswordComponent,
],
providers: [
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
})
export class AuthenticationModule {
}
@@ -0,0 +1,45 @@
<div class="auth-container">
<div class="row auth-main">
<div class="col-sm-6 px-0 d-none d-sm-block">
<div class="left-img" style="background-image: url(assets/images/pages/bg-03.png);">
</div>
</div>
<div class="col-sm-6 auth-form-section">
<div class="form-section">
<div class="auth-wrapper">
<h2 class="welcome-msg"> Reset Password </h2>
<p class="auth-signup-text text-muted">Let Us Help You</p>
<form class="validate-form" [formGroup]="authForm" (ngSubmit)="onSubmit()">
<div class="row">
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 mb-2">
<span class="error-subheader2 p-t-20 p-b-15">
Enter your registered email address.
</span>
<mat-form-field class="example-full-width" appearance="outline">
<mat-label>Email</mat-label>
<input matInput formControlName="email" required>
<mat-icon class="material-icons-two-tone color-icon p-3" matSuffix>mail</mat-icon>
<mat-error *ngIf="authForm.get('email')?.hasError('required') || authForm.get('email')?.touched">
Please enter a valid email address
</mat-error>
</mat-form-field>
</div>
</div>
<div class="container-auth-form-btn mt-5">
<button mat-flat-button color="primary" class="auth-form-btn" [disabled]="!authForm.valid " type="submit">
Reset My Password
</button>
</div>
<div class="w-full p-t-25 text-center">
<div>
<a routerLink="/authentication/signin" class="txt1">
Login?
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ForgotPasswordComponent } from './forgot-password.component';
describe('ForgotPasswordComponent',
() => {
let component: ForgotPasswordComponent;
let fixture: ComponentFixture<ForgotPasswordComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ForgotPasswordComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(ForgotPasswordComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,55 @@
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import {
FormBuilder,
FormControl,
FormGroup,
Validators,
} from '@angular/forms';
type ForgotPasswordForm = {
email: FormControl<string | null>;
};
@Component({
selector: 'app-forgot-password',
templateUrl: './forgot-password.component.html',
styleUrls: ['./forgot-password.component.scss'],
})
export class ForgotPasswordComponent implements OnInit {
authForm!: FormGroup<ForgotPasswordForm>;
submitted = false;
returnUrl!: string;
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router
) {
}
ngOnInit() {
this.authForm = this.formBuilder.group({
email: [
'',
[Validators.required, Validators.email, Validators.minLength(5)],
],
});
// get return url from route parameters or default to '/'
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
}
get f() {
return this.authForm.controls;
}
onSubmit() {
this.submitted = true;
// stop here if form is invalid
if (this.authForm.invalid) {
return;
} else {
this.router.navigate(['/dashboard/rests']);
}
}
}
@@ -0,0 +1,58 @@
<div class="auth-container">
<div class="row auth-main">
<div class="col-sm-6 px-0 d-none d-sm-block">
<div class="left-img" style="background-image: url(assets/images/pages/bg-01.png);">
</div>
</div>
<div class="col-sm-6 auth-form-section">
<div class="form-section">
<div class="auth-wrapper">
<form class="validate-form" [formGroup]="authForm" (ngSubmit)="onSubmit()">
<div class="auth-locked">
<div class="image">
<img src={{userImg}} alt="User">
</div>
</div>
<span class="auth-locked-title p-b-34 p-t-27">
{{userFullName}}
</span>
<div class="text-center">
<p class="txt1 p-b-20">
Locked
</p>
</div>
<div class="row">
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 mb-2">
<span class="error-subheader2 p-t-20 p-b-15">
Enter your password here.
</span>
<mat-form-field class="example-full-width" appearance="outline">
<mat-label>Password</mat-label>
<input matInput formControlName="password" [type]="hide ? 'password' : 'text'" required>
<mat-icon matSuffix (click)="hide = !hide">
{{hide ? 'visibility_off' : 'visibility'}}
</mat-icon>
<mat-error *ngIf="authForm.get('password')?.hasError('required')">
Password is required
</mat-error>
</mat-form-field>
</div>
</div>
<div class="container-auth-form-btn mt-5">
<button mat-flat-button color="primary" class="auth-form-btn" [disabled]="!authForm.valid " type="submit">
Reset My Password
</button>
</div>
<div class="w-full p-t-15 p-b-15 text-center">
<div>
<a routerLink="/authentication/signin" class="txt1">
Need Help?
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { LockedComponent } from './locked.component';
describe('LockedComponent',
() => {
let component: LockedComponent;
let fixture: ComponentFixture<LockedComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [LockedComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(LockedComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,55 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { AuthService } from 'src/app/core/service/auth.service';
type LockedForm = {
password: FormControl<string | null>;
};
@Component({
selector: 'app-locked',
templateUrl: './locked.component.html',
styleUrls: ['./locked.component.scss'],
})
export class LockedComponent implements OnInit {
authForm!: FormGroup<LockedForm>;
submitted = false;
userImg!: string;
userFullName!: string;
hide = true;
constructor(
private formBuilder: FormBuilder,
private router: Router,
private authService: AuthService
) {
}
ngOnInit() {
this.authForm = this.formBuilder.group({
password: ['', Validators.required],
});
this.userImg = this.authService.currentUserValue.img || 'assets/images/user/admin.jpg';
this.userFullName =
this.authService.currentUserValue.firstName +
' ' +
this.authService.currentUserValue.lastName;
}
get f() {
return this.authForm.controls;
}
onSubmit() {
this.submitted = true;
// stop here if form is invalid
if (this.authForm.invalid) {
return;
} else {
this.router.navigate(['/dashboard/rests']);
}
}
}
@@ -0,0 +1,37 @@
<div class="auth-container">
<div class="row auth-main">
<div class="col-sm-6 px-0 d-none d-sm-block">
<div class="left-img" style="background-image: url(assets/images/pages/bg-04.png);">
</div>
</div>
<div class="col-sm-6 auth-form-section">
<div class="form-section">
<div class="auth-wrapper">
<form>
<span class="error-header p-b-45">
404
</span>
<span class="error-subheader p-b-5">
Looks Like You're Lost
</span>
<span class="error-subheader2 p-b-5">
The Page You Are Looking For Not Available!
</span>
<div class="container-auth-form-btn mt-5">
<button mat-flat-button color="primary" class="auth-form-btn" type="submit" routerLink="/dashboard">
Go To Home Page
</button>
</div>
<div class="w-full p-t-15 p-b-15 text-center">
<div>
<a routerLink="/authentication/signin" class="txt1">
Need Help?
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { Page404Component } from './page404.component';
describe('Page404Component',
() => {
let component: Page404Component;
let fixture: ComponentFixture<Page404Component>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [Page404Component],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(Page404Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-page404',
templateUrl: './page404.component.html',
styleUrls: ['./page404.component.scss'],
})
export class Page404Component {
constructor() {
// constructor
}
}
@@ -0,0 +1,34 @@
<div class="auth-container">
<div class="row auth-main">
<div class="col-sm-6 px-0 d-none d-sm-block">
<div class="left-img" style="background-image: url(assets/images/pages/bg-05.png);">
</div>
</div>
<div class="col-sm-6 auth-form-section">
<div class="form-section">
<div class="auth-wrapper">
<form>
<span class="error-header p-b-45">
500
</span>
<span class="error-subheader2 p-b-5">
Oops, Something went wrong. Please try after some times.
</span>
<div class="container-auth-form-btn mt-5">
<button mat-flat-button color="primary" class="auth-form-btn" type="submit">
Go To Home Page
</button>
</div>
<div class="w-full p-t-15 p-b-15 text-center">
<div>
<a routerLink="/authentication/signin" class="txt1">
Need Help?
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { Page500Component } from './page500.component';
describe('Page500Component',
() => {
let component: Page500Component;
let fixture: ComponentFixture<Page500Component>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [Page500Component],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(Page500Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-page500',
templateUrl: './page500.component.html',
styleUrls: ['./page500.component.scss'],
})
export class Page500Component {
constructor() {
// constructor
}
}
@@ -0,0 +1,114 @@
<div class="auth-container">
<div class="row auth-main">
<div class="col-sm-6 px-0 d-none d-sm-block">
<div class="left-img" style="background-image: url(assets/images/pages/bg-01.png);">
</div>
</div>
<div class="col-sm-6 auth-form-section">
<div class="form-section">
<div class="auth-wrapper">
<h2 class="welcome-msg">
<div>Welcome to ase.com.ua</div>
<div>angular/asp.net core template</div>
</h2>
<p class="auth-signup-text text-muted">
Need an account?
<a routerLink="/authentication/signup" class="sign-up-link">
Sign Up
</a>
</p>
<h2 class="login-title">Sign in</h2>
<form class="validate-form" [formGroup]="authForm" (ngSubmit)="onSubmit()">
<div class="row">
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 mb-2">
<mat-form-field class="example-full-width" appearance="outline">
<mat-label>Email</mat-label>
<input matInput formControlName="username" type="email" autocomplete="username"/>
<mat-icon class="material-icons-two-tone color-icon p-3" matSuffix>mail</mat-icon>
<mat-error *ngIf="authForm.get('username')?.hasError('required')">
Email is required
</mat-error>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-xl-12col-lg-12 col-md-12 col-sm-12 mb-2">
<mat-form-field class="example-full-width" appearance="outline">
<mat-label>Password</mat-label>
<input matInput [type]="hide ? 'password' : 'text'" formControlName="password">
<a href="#" onClick="return false;" matSuffix (click)="hide = !hide"
[attr.aria-label]="'Hide password'" [attr.aria-pressed]="hide">
<mat-icon class="material-icons-two-tone color-icon m-3" matSuffix>
{{hide ? 'visibility_off' : 'visibility'}}
</mat-icon>
</a>
<mat-error *ngIf="authForm.get('password')?.hasError('required')">
Password is required
</mat-error>
</mat-form-field>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-5">
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" value=""> Remember me
<span class="form-check-sign">
<span class="check"></span>
</span>
</label>
</div>
<a class="txt1" routerLink="/authentication/forgot-password">Forgot Password?</a>
</div>
<div *ngIf="error" class="alert alert-danger mt-3 mb-0">{{error}}</div>
<div class="container-auth-form-btn">
<div style="text-align: center">
<button mat-raised-button color="primary" [class.auth-spinner]="loading" [disabled]="loading"
class="auth-form-btn" [disabled]="!authForm.valid " type="submit">
Login
</button>
</div>
</div>
</form>
<h6 class="social-login-title">OR</h6>
<ul class="list-unstyled social-icon mb-0 mt-3">
<li class="list-inline-item" *ngIf="allowAuth">
<a href="javascript:void(0)" class="rounded" (click)="loginAuth0()">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 135 150">
<title>auth0-glyph</title>
<path d="M131.225 46.364L116.136 0H67.4l15.06 46.364h48.764zM67.4 0H18.664L3.602 46.364H52.34L67.399 0zM3.602 46.364c-8.982 27.639.856 57.918 24.368 75L43.032 75 3.602 46.364zm127.623 0L91.795 75l15.062 46.364c23.512-17.082 33.35-47.361 24.368-75zm-103.255 75L67.4 150l39.43-28.636L67.4 92.727l-39.43 28.637z" fill="#3c4858"/>
</svg>
</a>
</li>
<li class="list-inline-item" *ngIf="allowGoogle">
<!--
<a href="javascript:void(0)" class="rounded" (click)="loginGoogle()">
<i class="fab fa-google"></i>
</a>
-->
<asl-google-signin-button type="icon" size="medium" width="200" shape="rectangular" theme="filled_black" logo_alignment="center" locale=""></asl-google-signin-button>
</li>
<!--
<li class="list-inline-item">
<a href="javascript:void(0)" class="rounded flex-c-m">
<i class="fab fa-facebook-f"></i>
</a>
</li>
<li class="list-inline-item">
<a href="javascript:void(0)" class="rounded">
<i class="fab fa-twitter"></i>
</a>
</li>
<li class="list-inline-item">
<a href="javascript:void(0)" class="rounded">
<i class="fab fa-linkedin-in"></i>
</a>
</li>
-->
</ul>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SigninComponent } from './signin.component';
describe('SigninComponent',
() => {
let component: SigninComponent;
let fixture: ComponentFixture<SigninComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SigninComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(SigninComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,117 @@
// angular
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
// libs
import { SocialAuthService } from '@abacritt/angularx-social-login';
// app
import { AuthService } from 'src/app/core/service/auth.service';
import { UnsubscribeOnDestroyAdapter } from 'src/app/shared/UnsubscribeOnDestroyAdapter';
import { getSafeRedirectUrl } from 'src/app/core/utils/safe-redirect';
import { environment } from '../../../environments/environment';
type SigninForm = {
username: FormControl<string | null>;
password: FormControl<string | null>;
};
@Component({
selector: 'app-signin',
templateUrl: './signin.component.html',
styleUrls: ['./signin.component.scss'],
})
export class SigninComponent extends UnsubscribeOnDestroyAdapter
implements OnInit {
authForm!: FormGroup<SigninForm>;
submitted = false;
loading = false;
error?= '';
hide = true;
allowGoogle: boolean;
allowAuth: boolean;
constructor(
private formBuilder: FormBuilder,
private router: Router,
private route: ActivatedRoute,
private authService: AuthService,
) {
super();
this.allowGoogle = !!(environment.externalLogins &&
environment.externalLogins.google &&
environment.externalLogins.google.clientId);
this.allowAuth = !!(environment.externalLogins &&
environment.externalLogins.auth0 &&
environment.externalLogins.auth0.clientId);
}
ngOnInit() {
this.authForm = this.formBuilder.group({
username: ['', Validators.required],
password: ['', Validators.required],
});
this.subs.sink = this.authService.isAuthenticated$.subscribe(isAuthenticated => {
if (isAuthenticated) {
this.router.navigate([this.getRedirect() || '/dashboard/rests']);
}
});
}
onSubmit() {
if (this.authForm.invalid) {
this.error = 'Username and Password not valid!';
return;
}
this.submitted = true;
this.loading = true;
this.error = '';
var user = {
username: this.authForm.controls.username.value!,
password: this.authForm.controls.password.value!
};
this.subs.sink = this.authService
.login(user)
.subscribe({
next: (resp) => {
if (!resp.success) {
this.error = 'Invalid Login';
if (!resp.success && resp.error?.code === 'invalid_grant') {
this.error = 'Invalid username or password';
}
this.submitted = false;
this.loading = false;
}
},
error: (error) => {
this.error = 'Invalid username or password';
this.submitted = false;
this.loading = false;
},
});
}
loginAuth0() {
this.subs.sink = this.authService.loginAuth0().subscribe(resp => {
if (!resp.success) {
this.error = 'Invalid login';
}
});
}
private getRedirect(): string | undefined {
const raw = this.route.snapshot.queryParams['r'] as string | undefined;
if (!raw) {
return undefined;
}
const safe = getSafeRedirectUrl(raw, '');
return safe || undefined;
}
}
@@ -0,0 +1,128 @@
<div class="auth-container">
<div class="row auth-main">
<div class="col-sm-6 px-0 d-none d-sm-block">
<div class="left-img" style="background-image: url(assets/images/pages/bg-02.png);">
</div>
</div>
<div class="col-sm-6 auth-form-section">
<div class="form-section">
<div class="auth-wrapper">
<h2 class="welcome-msg"> Sign Up </h2>
<p class="auth-signup-text text-muted">Enter details to create your account</p>
<form class="validate-form" [formGroup]="authForm" (ngSubmit)="onSubmit()">
<div class="row">
<div class="col-xl-12col-lg-12 col-md-12 col-sm-12 mb-2">
<mat-form-field class="example-full-width" appearance="outline">
<mat-label>Email</mat-label>
<input matInput formControlName="email" required autocomplete="off">
<mat-icon class="material-icons-two-tone color-icon p-3" matSuffix>mail</mat-icon>
<mat-error *ngIf="authForm.get('email')?.hasError('required') || authForm.get('email')?.touched">
Please enter a valid email address
</mat-error>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-xl-12col-lg-12 col-md-12 col-sm-12 mb-2">
<mat-form-field class="example-full-width" appearance="outline">
<mat-label>Password</mat-label>
<input matInput formControlName="password" [type]="hide ? 'password' : 'text'" required autocomplete="off">
<a href="#" onClick="return false;" matSuffix (click)="hide = !hide"
[attr.aria-label]="'Hide password'" [attr.aria-pressed]="hide">
<mat-icon class="material-icons-two-tone color-icon m-3" matSuffix>
{{hide ? 'visibility_off' : 'visibility'}}
</mat-icon>
</a>
<mat-error *ngIf="authForm.get('password')?.hasError('required')">
Password is required
</mat-error>
<mat-error *ngIf="authForm.get('password')?.hasError('minlength')">
Password must be at least 8 characters
</mat-error>
<mat-error *ngIf="authForm.get('password')?.hasError('pattern')">
Use upper, lower, digit and special character
</mat-error>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-xl-12col-lg-12 col-md-12 col-sm-12 mb-2">
<mat-form-field class="example-full-width" appearance="outline">
<mat-label>Confirm Password</mat-label>
<input matInput formControlName="cpassword" [type]="chide ? 'password' : 'text'" required autocomplete="off">
<a href="#" onClick="return false;" matSuffix (click)="chide = !chide"
[attr.aria-label]="'Hide password'" [attr.aria-pressed]="chide">
<mat-icon class="material-icons-two-tone color-icon m-3" matSuffix>
{{chide ? 'visibility_off' : 'visibility'}}
</mat-icon>
</a>
<mat-error *ngIf="authForm.get('cpassword')?.hasError('required')">
Confirm Password is required
</mat-error>
</mat-form-field>
</div>
</div>
<div class="flex-sb-m w-full p-b-20" *ngIf="authForm.hasError('passwordMismatch') && submitted">
<div class="alert alert-danger">
Passwords do not match
</div>
</div>
<div class="flex-sb-m w-full p-b-20" *ngIf="generalError">
<div *ngFor="let error of generalError.errors" class="alert alert-danger">
{{error.description}}
</div>
</div>
<div class="flex-sb-m w-full p-b-20">
<div>
<span>
Already Registered?
<a routerLink="/authentication/signin">
Login
</a>
</span>
</div>
</div>
<div class="container-auth-form-btn">
<button mat-flat-button color="primary" class="auth-form-btn" [disabled]="!authForm.valid " type="submit">
Register
</button>
</div>
</form>
<h6 class="social-login-title">OR</h6>
<ul class="list-unstyled social-icon mb-0 mt-3">
<li class="list-inline-item">
<a href="javascript:void(0)" class="rounded" (click)="loginAuth0()">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 135 150">
<title>auth0-glyph</title>
<path d="M131.225 46.364L116.136 0H67.4l15.06 46.364h48.764zM67.4 0H18.664L3.602 46.364H52.34L67.399 0zM3.602 46.364c-8.982 27.639.856 57.918 24.368 75L43.032 75 3.602 46.364zm127.623 0L91.795 75l15.062 46.364c23.512-17.082 33.35-47.361 24.368-75zm-103.255 75L67.4 150l39.43-28.636L67.4 92.727l-39.43 28.637z" fill="#3c4858"/>
</svg>
</a>
</li>
<!--
<li class="list-inline-item">
<a href="javascript:void(0)" class="rounded">
<i class="fab fa-google"></i>
</a>
</li>
<li class="list-inline-item">
<a href="javascript:void(0)" class="rounded flex-c-m">
<i class="fab fa-facebook-f"></i>
</a>
</li>
<li class="list-inline-item">
<a href="javascript:void(0)" class="rounded">
<i class="fab fa-twitter"></i>
</a>
</li>
<li class="list-inline-item">
<a href="javascript:void(0)" class="rounded">
<i class="fab fa-linkedin-in"></i>
</a>
</li>
-->
</ul>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SignupComponent } from './signup.component';
describe('SignupComponent',
() => {
let component: SignupComponent;
let fixture: ComponentFixture<SignupComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SignupComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(SignupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,113 @@
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Router, ActivatedRoute } from '@angular/router';
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { GeneralErrorModel } from '../../core/models/general-error.model';
import { AuthService } from '../../core/service/auth.service';
type SignupForm = {
email: FormControl<string | null>;
password: FormControl<string | null>;
cpassword: FormControl<string | null>;
};
@Component({
selector: 'app-signup',
templateUrl: './signup.component.html',
styleUrls: ['./signup.component.scss'],
})
export class SignupComponent implements OnInit {
authForm!: FormGroup<SignupForm>;
submitted = false;
returnUrl!: string;
hide = true;
chide = true;
generalError?: GeneralErrorModel;
private readonly destroyRef = inject(DestroyRef);
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private authService: AuthService
) {
}
ngOnInit() {
this.authForm = this.formBuilder.group({
email: ['', [Validators.required, Validators.email, Validators.minLength(5)]],
password: [
'',
[
Validators.required,
Validators.minLength(8),
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).+$/),
],
],
cpassword: ['', Validators.required],
}, {
validators: (group: AbstractControl) => {
const password = group.get('password')?.value;
const confirm = group.get('cpassword')?.value;
return password && confirm && password !== confirm
? { passwordMismatch: true }
: null;
},
});
// get return url from route parameters or default to '/'
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
}
get f() {
return this.authForm.controls;
}
onSubmit() {
this.submitted = true;
// stop here if form is invalid
if (this.authForm.invalid) {
return;
}
if (this.authForm.hasError('passwordMismatch')) {
return;
}
this.authService.register({
username: this.authForm.get('email')!.value!,
password: this.authForm.get('password')!.value!,
confirmPassword: this.authForm.get('cpassword')!.value!,
}).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(
resp => {
if (!resp.succeeded) {
this.generalError = resp as GeneralErrorModel;
return;
}
// Auto sign-in with email+password after successful register
this.authService.login({
username: this.authForm.get('email')!.value!,
password: this.authForm.get('password')!.value!,
}).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(login => {
if (login.success) {
this.router.navigate([this.returnUrl || '/dashboard/rests']);
} else {
this.router.navigate(['authentication/signin']);
}
});
},
err => {
this.generalError = err.error as GeneralErrorModel;
}
);
}
loginAuth0() {
this.authService.loginAuth0()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => {
});
}
}
@@ -0,0 +1,66 @@
import { SocialAuthServiceConfig } from '@abacritt/angularx-social-login';
import { GoogleLoginProvider } from '@abacritt/angularx-social-login';
import { FacebookLoginProvider } from '@abacritt/angularx-social-login';
import { MicrosoftLoginProvider } from '@abacritt/angularx-social-login';
import { environment } from '../environments/environment';
export class ExternalLoginConfig {
static readonly GOOGLE = 'google';
static readonly AUTH0 = 'auth0';
static readonly FACEBOOK = 'facebook';
static readonly MICROSOFT = 'microsoft';
static getConfiguredProviders() {
return [
{
provider: ExternalLoginConfig.GOOGLE,
name: 'Google',
},
{
provider: ExternalLoginConfig.AUTH0,
name: 'Auth0',
},
/*{
provider: ExternalLoginConfig.FACEBOOK,
name: "FaceBook",
},
{
provider: ExternalLoginConfig.MICROSOFT,
name: "Microsoft",
},*/
];
}
static getSocialConfig(): SocialAuthServiceConfig {
var providers = [];
if (environment.externalLogins &&
environment.externalLogins.google &&
environment.externalLogins.google.clientId
) {
providers.push({
id: GoogleLoginProvider.PROVIDER_ID,
provider: new GoogleLoginProvider(
environment.externalLogins.google.clientId,
{ scopes: 'email', }
)
});
}
return {
autoLogin: false,
providers: providers,
onError: (err) => {
console.error(err);
}
};
}
static getAuth0Config() {
return {
clientId: environment.externalLogins.auth0.clientId,
domain: environment.externalLogins.auth0.domain,
};
}
}
+61
View File
@@ -0,0 +1,61 @@
import { AuthConfig } from 'angular-oauth2-oidc';
import { OAuthModuleConfig } from 'angular-oauth2-oidc';
import { environment } from '../environments/environment';
/** Public origin for same-origin deploys when environment URLs are left empty. */
function publicOrigin(): string {
return window.location.origin;
}
function resolveIdentityServer(): string {
const configured = (environment.identityServer ?? '').trim();
if (configured) {
return configured.endsWith('/') ? configured : `${configured}/`;
}
return `${publicOrigin()}/`;
}
function resolveAllowedUrls(): string[] {
const configured = environment.allowedUrls ?? [];
if (configured.length > 0) {
return configured;
}
const origin = publicOrigin();
const api = (environment.apiUrl ?? '').trim().replace(/\/$/, '');
return api ? [origin, api] : [origin];
}
export const AuthCodeFlowConfig: AuthConfig = {
// Url of the Identity Provider
issuer: resolveIdentityServer(),
// Local/Docker HTTP demos can set environment.requireHttps = false.
requireHttps: (environment as { requireHttps?: boolean }).requireHttps ?? environment.production,
strictDiscoveryDocumentValidation:
(environment as { requireHttps?: boolean }).requireHttps ?? environment.production,
// URL of the SPA to redirect the user to after login
redirectUri: window.location.origin + '/',
// Password/external grants issue refresh tokens; no cookie session for iframe silent refresh.
useSilentRefresh: false,
sessionChecksEnabled: false,
timeoutFactor: 0.75,
clientId: 'angulartemplate_spa',
// Authorization Code + PKCE for interactive OIDC; email/password still uses ROPC (see docs/AUTH.md).
responseType: 'code',
disablePKCE: false,
// offline_access → refresh_token (used by setupAutomaticSilentRefresh)
scope: 'openid profile email api offline_access',
showDebugInformation: !environment.production,
};
export const AuthModuleConfig: OAuthModuleConfig = {
resourceServer: {
allowedUrls: resolveAllowedUrls(),
sendAccessToken: true,
}
};
@@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { InConfiguration } from '../core/models/config.interface';
@Injectable({
providedIn: 'root',
})
export class ConfigService {
configData!: InConfiguration;
constructor() {
this.setConfigData();
}
setConfigData() {
this.configData = {
layout: {
rtl: false, // options: true & false
variant: 'light', // options: light & dark
theme_color: 'white', // options: white, black, purple, blue, cyan, green, orange
logo_bg_color: 'white', // options: white, black, purple, blue, cyan, green, orange
sidebar: {
collapsed: false, // options: true & false
backgroundColor: 'light', // options: light & dark
},
},
};
}
}
+7
View File
@@ -0,0 +1,7 @@
/** Placement of Cancel/Save (and similar) actions in Material dialogs. */
export type ModalButtonsPosition = 'top' | 'bottom';
export const UI_CONFIG = {
/** Dialog action buttons: `top` (next to title) or `bottom` (under content). */
modal_buttons: 'top' as ModalButtonsPosition,
};
+63
View File
@@ -0,0 +1,63 @@
// angular
import { NgModule } from '@angular/core';
import { Optional } from '@angular/core';
import { SkipSelf } from '@angular/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { APP_INITIALIZER } from '@angular/core';
import { CommonModule } from '@angular/common';
// libs
import { OAuthModule, OAuthModuleConfig, OAuthStorage } from 'angular-oauth2-oidc';
import { AuthConfig } from 'angular-oauth2-oidc';
import { AuthModule } from '@auth0/auth0-angular';
import { SocialLoginModule } from '@abacritt/angularx-social-login';
// app
import { AuthGuard } from './guard/auth.guard';
import { RightSidebarService } from './service/rightsidebar.service';
import { AuthService, authAppInitializerFactory } from './service/auth.service';
import { DirectionService } from './service/direction.service';
import { throwIfAlreadyLoaded } from './guard/module-import.guard';
import { OidcHelperService } from './service/oidc-helper.service';
import { AuthCodeFlowConfig, AuthModuleConfig } from '../config.oidc';
import { SubjectExtensions } from './extensions/general.extensions';
import { ExternalLoginConfig } from '../config.external-login';
import { AccountCategoryService } from '../services/account.category.service';
import { AccountService } from '../services/account.service';
export function storageFactory(): OAuthStorage {
return localStorage;
}
@NgModule({
declarations: [],
imports: [
CommonModule,
SocialLoginModule,
OAuthModule.forRoot(),
AuthModule.forRoot(ExternalLoginConfig.getAuth0Config()),
],
providers: [
{ provide: APP_INITIALIZER, useFactory: authAppInitializerFactory, deps: [AuthService], multi: true },
{ provide: AuthConfig, useValue: AuthCodeFlowConfig },
{ provide: OAuthModuleConfig, useValue: AuthModuleConfig },
{ provide: OAuthStorage, useFactory: storageFactory },
{ provide: 'SocialAuthServiceConfig', useValue: ExternalLoginConfig.getSocialConfig() },
RightSidebarService,
AuthGuard,
AuthService,
DirectionService,
OidcHelperService,
SubjectExtensions,
AccountCategoryService,
AccountService,
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
}
@@ -0,0 +1,42 @@
// angular
import { ActivatedRoute } from '@angular/router';
// libs
import { BehaviorSubject, Observable, Subject, of, throwError, from, combineLatest } from 'rxjs';
import { getSafeRedirectUrl } from '../utils/safe-redirect';
export { }
declare global {
interface RouterExtensions {
addDays(days: number): Date;
}
}
export class ExActivatedRoute extends ActivatedRoute {
getCurrentRoute(): string {
const raw = this.snapshot.queryParams['r'] as string | undefined;
return getSafeRedirectUrl(raw);
}
}
interface Action<T> {
(item: T): void;
}
interface Func<T, TResult> {
(item: T): TResult;
}
export class SubjectExtensions {
static start<T>(start: Action<Subject<T>>): Subject<T> {
var result = new Subject<T>();
start(result);
return result;
}
}
@@ -0,0 +1,18 @@
import { ActivatedRoute } from '@angular/router';
import { getSafeRedirectUrl } from '../utils/safe-redirect';
export { }
declare global {
interface RouterExtensions {
addDays(days: number): Date;
}
}
export class ExActivatedRoute extends ActivatedRoute {
getCurrentRoute(): string {
const raw = this.snapshot.queryParams['r'] as string | undefined;
return getSafeRedirectUrl(raw);
}
}
@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../service/auth.service';
@Injectable({
providedIn: 'root',
})
export class AuthGuard {
constructor(
private authService: AuthService,
private router: Router
) {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (this.authService.isAuthenticated) {
return true;
}
this.router.navigate(['/authentication/signin'], { queryParams: { r: encodeURIComponent(state.url) } });
return false;
}
}
@@ -0,0 +1,12 @@
import { CoreModule } from '../core.module';
export function throwIfAlreadyLoaded(
parentModule: CoreModule,
moduleName: string
) {
if (parentModule) {
throw new Error(
`${moduleName} has already been loaded. Import ${moduleName} modules in the AppModule only.`
);
}
}
@@ -0,0 +1,40 @@
import { AuthService } from '../service/auth.service';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(
private authenticationService: AuthService,
private router: Router
) {}
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError((err) => {
if (err.status === 401) {
const returnUrl = this.router.url;
this.authenticationService.logout();
void this.router.navigate(['/authentication/signin'], {
queryParams: returnUrl && returnUrl !== '/'
? { r: returnUrl }
: undefined,
});
}
let error = err.error || err.message || err.statusText;
if (!error) {
console.log('not parsed error', err);
}
return throwError(() => error);
})
);
}
}
@@ -0,0 +1,101 @@
/*import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpResponse,
HttpHandler,
HttpEvent,
HttpInterceptor,
HTTP_INTERCEPTORS,
} from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { UserModel } from '../models/user.model';
const users: UserModel[] = [
{
id: 'fake-user',
img: 'assets/images/user/admin.jpg',
username: 'admin@software.com',
//password: 'admin@123',
firstName: 'Sarah',
lastName: 'Smith',
token: 'admin-token',
},
];
@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
const { url, method, headers, body } = request;
// wrap in delayed observable to simulate server api call
return of(null).pipe(mergeMap(handleRoute));
function handleRoute() {
switch (true) {
case url.endsWith('/authenticate') && method === 'POST':
return authenticate();
default:
// pass through any requests not handled above
return next.handle(request);
}
}
// route functions
function authenticate() {
const { username, password } = body;
const user = users.find(
(x) => x.username === username && x.password === password
);
if (!user) {
return error('Username or password is incorrect');
}
return ok({
id: user.id,
username: user.username,
img: user.img,
firstName: user.firstName,
lastName: user.lastName,
token: user.token,
});
}
// helper functions
function ok(body?: {
id: string;
username: string;
img?: string;
firstName?: string;
lastName?: string;
token?: string;
}) {
return of(new HttpResponse({ status: 200, body }));
}
function error(message: string) {
return throwError({ error: { message } });
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function unauthorized() {
return throwError({ status: 401, error: { message: 'Unauthorised' } });
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function isLoggedIn() {
return headers.get('Authorization') === 'Bearer fake-jwt-token';
}
}
}
export const fakeBackendProvider = {
// use fake backend in place of Http service for backend-less development
provide: HTTP_INTERCEPTORS,
useClass: FakeBackendInterceptor,
multi: true,
};
*/
@@ -0,0 +1,42 @@
// angular
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpInterceptor } from '@angular/common/http';
import { HttpRequest } from '@angular/common/http';
import { HttpHandler } from '@angular/common/http';
import { HttpEvent } from '@angular/common/http';
// libs
import moment from 'moment';
@Injectable()
export class UpdateDateHttpInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (request.method === 'POST' || request.method === 'PUT') {
this.shiftDates(request.body);
}
return next.handle(request);
}
shiftDates(body: any) {
if (body === null || body === undefined) {
return body;
}
if (typeof body !== 'object') {
return body;
}
for (const key of Object.keys(body)) {
const value = body[key];
if (value instanceof Date) {
body[key] = moment(value).utcOffset(0, true).format();
} else if (typeof value === 'object') {
this.shiftDates(value);
}
}
}
}
@@ -0,0 +1,12 @@
export interface InConfiguration {
layout: {
rtl: boolean;
variant: string;
theme_color: string;
logo_bg_color: string;
sidebar: {
collapsed: boolean;
backgroundColor: string;
};
};
}
@@ -0,0 +1,6 @@
export interface GeneralErrorModel {
//succeeded?: boolean;
code?: string;
description?: string;
errors?: GeneralErrorModel[];
}
@@ -0,0 +1,7 @@
import { GeneralErrorModel } from './general-error.model';
export interface GeneralResultModel {
success: boolean;
error?: GeneralErrorModel;
data?: any;
}
@@ -0,0 +1,4 @@
export interface LoginModel {
username: string;
password: string;
}
@@ -0,0 +1,5 @@
export interface RegisterModel {
username: string;
password: string;
confirmPassword: string;
}
@@ -0,0 +1,7 @@
import { SelectableModel } from './selectable.model';
describe('SelectableModel', () => {
it('should create an instance', () => {
expect(new SelectableModel()).toBeTruthy();
});
});
@@ -0,0 +1,4 @@
export interface ISelectableModel<T> {
model: T;
selected: boolean;
}
@@ -0,0 +1,9 @@
export interface UserProfileModel {
id: string;
email?: string;
phone?: string;
firstName?: string;
lastName?: string;
fullName?: string;
currency?: string;
}
@@ -0,0 +1,9 @@
export interface UserModel {
id: string;
userName: string;
img?: string;
firstName?: string;
lastName?: string;
email?: string;
//token?: string;
}
@@ -0,0 +1,18 @@
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
describe('AuthService',
() => {
let service: AuthService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AuthService);
});
it('should be created',
() => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,266 @@
// angular
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
// libs
import { BehaviorSubject } from 'rxjs';
import { Observable } from 'rxjs';
import { Subject } from 'rxjs';
import { of } from 'rxjs';
import { throwError } from 'rxjs';
import { from } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators';
import { catchError } from 'rxjs/operators';
import { AuthService as Auth0Service } from '@auth0/auth0-angular';
import { SocialAuthService } from '@abacritt/angularx-social-login';
import { GoogleLoginProvider } from '@abacritt/angularx-social-login';
// app
import { OidcHelperService } from './oidc-helper.service';
import { UserModel } from '../models/user.model';
import { ApiRoutes } from '../../api-routes';
import { RegisterModel } from '../models/register.model';
import { LoginModel } from '../models/login.model';
import { UserProfileModel } from '../models/user-profile.model';
import { GeneralResultModel } from '../models/general-result.model';
import { getSafeRedirectUrl } from '../utils/safe-redirect';
import { SubjectExtensions } from '../extensions/general.extensions';
export function authAppInitializerFactory(authService: AuthService): () => Promise<void> {
if (window.location.href.indexOf('/silent-refresh.html') !== -1) {
return () => Promise.resolve();
}
return () => authService.runInitialLoginSequence();
}
@Injectable({
providedIn: 'root',
})
export class AuthService {
private currentUserSubject: BehaviorSubject<UserModel>;
currentUser$: Observable<UserModel>;
private isAuthenticatedSubject$ = new BehaviorSubject<boolean>(false);
isAuthenticated$ = this.isAuthenticatedSubject$.asObservable();
constructor(
private http: HttpClient,
private router: Router,
private route: ActivatedRoute,
private oidcHelperService: OidcHelperService,
private auth0Service: Auth0Service,
private externalAuthService: SocialAuthService,
) {
this.currentUserSubject = new BehaviorSubject<UserModel>({} as UserModel);
this.currentUser$ = this.currentUserSubject.asObservable();
// on external social login
this.externalAuthService.authState.subscribe((user) => {
if (user?.idToken) {
oidcHelperService.loginByExternalLogin('google', user.idToken);
}
});
this.oidcHelperService.isDoneLoading$
.pipe(
filter(isDone => isDone),
switchMap(() => this.oidcHelperService.isAuthenticated$)
)
.subscribe(authState => {
let redirectUrl = this.getRedirect();
if (authState.isAuthenticated) {
this.oidcHelperService.loadUserProfile().then(e => {
let resp = (e as any).info;
let userProfile = {
id: resp.id,
email: resp.email,
phone: resp.phone,
firstName: resp.firstName,
lastName: resp.lastName,
fullName: resp.fullName,
};
this.setCurrentUserValue(userProfile.id, userProfile);
if (authState.action === AuthenticatedActionEnum.loggedIn) {
this.router.navigate([redirectUrl || '/dashboard/rests']);
}
});
}
});
}
setCurrentUserValue(id: string, profile?: UserProfileModel) {
this.currentUserSubject.next({
id: id,
img: 'assets/images/user/admin.jpg',
userName: profile?.email || '',
firstName: profile?.firstName || '',
lastName: profile?.lastName || '',
});
}
get currentUserValue(): UserModel {
return this.currentUserSubject.value;
}
login(loginModel: LoginModel): Subject<GeneralResultModel> {
return SubjectExtensions.start<GeneralResultModel>(subject => {
this.oidcHelperService
.login(loginModel.username, loginModel.password)
.subscribe(resp => {
if (resp.access_token) {
this.isAuthenticatedSubject$.next(true);
}
subject.next({ success: !!(resp.access_token) });
},
err => {
subject.next({ success: false, error: err.error });
});
});
}
logout() {
this.currentUserSubject.next({} as UserModel);
this.isAuthenticatedSubject$.next(false);
this.oidcHelperService.logout();
return of({ success: false });
}
register(registerModel: RegisterModel): Observable<any> {
return this.http
.post<RegisterModel>(ApiRoutes.UserRegister, registerModel)
.pipe(
map(resp => resp),
catchError(error => {
return throwError(error);
})
);
}
runInitialLoginSequence() {
return this.oidcHelperService.runInitialLoginSequence();
}
get isAuthenticated(): boolean {
return this.oidcHelperService.hasValidAccessToken();
}
getAccessToken() {
return this.oidcHelperService.getAccessToken();
}
loginGoogle(redirectUrl?: string): Observable<any> {
return from(this.externalAuthService.signIn(GoogleLoginProvider.PROVIDER_ID));
}
attachGoogle(): Observable<any> {
var result = new Subject();
/*this.externalAuthService.signIn(GoogleLoginProvider.PROVIDER_ID).then(function (resp) {
//console.log('attachGoogle', resp);
//return result.next(resp);
//this.attach(idToken.__raw, result);
});*/
return result;
}
loginAuth0(): Observable<GeneralResultModel> {
return SubjectExtensions.start<GeneralResultModel>((subject) => {
var subs = this.auth0Service.idTokenClaims$.subscribe(token => {
if (token && token?.__raw) {
subs.unsubscribe();
this.oidcHelperService.loginByExternalLogin('auth0', token.__raw).subscribe(login => {
if (login.success) {
this.isAuthenticatedSubject$.next(true);
}
subject.next({ success: login.success, error: login.error });
});
} else {
//subject.next({ success: false, error: { description: 'Login failed' } });
}
});
this.auth0Service.getAccessTokenWithPopup().subscribe(popupToken => {
if (!popupToken) {
this.auth0Service.getAccessTokenSilently().subscribe(silently => {
});
}
});
});
}
attachAuth0(): Observable<any> {
return SubjectExtensions.start<GeneralResultModel>((subject) => {
var subs = this.auth0Service.idTokenClaims$.subscribe(token => {
if (token && token?.__raw) {
subs.unsubscribe();
this.attach(token?.__raw).subscribe(
(resp) => subject.next({ success: true, data: token?.__raw }),
(err) => subject.next({ success: false, data: err })
);
} else {
subject.next({ success: false, data: 'Failed' });
}
});
this.auth0Service.getAccessTokenWithPopup().subscribe(popupToken => {
if (!popupToken) {
this.auth0Service.getAccessTokenSilently().subscribe(silently => {
});
}
});
});
}
deattach(provider: string): Observable<any> {
return this.http
.post<any>(ApiRoutes.UserDeattach, { provider: provider })
.pipe(
catchError(error => throwError(() => error))
);
}
private attach(token: string): Observable<any> {
return this.http.post<any>(ApiRoutes.UserAttach, {
provider: 'auth0',
token: token,
});
}
private getRedirect(): string | undefined {
const raw = this.route.snapshot.queryParams['r'] as string | undefined;
if (!raw) {
return undefined;
}
const safe = getSafeRedirectUrl(raw, '');
return safe || undefined;
}
}
export enum AuthenticatedActionEnum {
init,
update,
loggedIn,
loggedOff,
}
export interface IAuthenticatedState {
isAuthenticated: boolean,
action: AuthenticatedActionEnum,
redirectUrl?: string,
}
export enum StateEnum {
undefined,
inited,
completed,
failed,
}
@@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable()
export class DirectionService {
private data = new BehaviorSubject('');
currentData = this.data.asObservable();
constructor() {
//constructor
}
updateDirection(item: string) {
this.data.next(item);
}
}
@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
@Injectable({
providedIn: 'root',
})
export class LanguageService {
languages: string[] = ['en', 'es', 'de', 'ua'];
constructor(public translate: TranslateService) {
let browserLang: string;
translate.addLangs(this.languages);
if (localStorage.getItem('lang')) {
browserLang = localStorage.getItem('lang') as string;
} else {
browserLang = translate.getBrowserLang() as string;
}
translate.use(browserLang.match(/en|es|de|ua/) ? browserLang : 'en');
}
setLanguage(lang: string) {
this.translate.use(lang);
localStorage.setItem('lang', lang);
}
}
@@ -0,0 +1,252 @@
// angular
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { OAuthService } from 'angular-oauth2-oidc';
import { OAuthErrorEvent } from 'angular-oauth2-oidc';
// libs
import { filter, map } from 'rxjs/operators';
import { Subject, Observable, BehaviorSubject, from } from 'rxjs';
// app
import { GeneralResultModel } from '../models/general-result.model';
@Injectable()
export class OidcHelperService {
private isAuthenticatedSubject$ = new BehaviorSubject<IAuthenticatedState>({
isAuthenticated: false,
action: AuthenticatedActionEnum.init
});
isAuthenticated$ = this.isAuthenticatedSubject$.asObservable();
private isDoneLoadingFailed = false;
private isDoneLoadingSubject$ = new BehaviorSubject<boolean>(false);
isDoneLoading$ = this.isDoneLoadingSubject$.asObservable();
constructor(
private oauthService: OAuthService,
private router: Router,
) {
this.oauthService.oidc = false;
// all events handler
this.oauthService.events.subscribe(event => {
if (event instanceof OAuthErrorEvent) {
console.error('OAuthErrorEvent Object:', event);
if (event.type === 'discovery_document_validation_error') {
this.isDoneLoadingFailed = true;
this.setIsAuthenticatedSubject$(false, AuthenticatedActionEnum.update);
}
} else {
var hasValidAccessToken = this.hasValidAccessToken();
var isAuthenticated = this.isAuthenticatedSubject$.getValue().isAuthenticated;
if (isAuthenticated !== hasValidAccessToken) {
this.setIsAuthenticatedSubject$(hasValidAccessToken, AuthenticatedActionEnum.update);
}
}
});
// This is tricky, as it might cause race conditions (where access_token is set in another
// tab before everything is said and done there.
// TODO: Improve this setup. See: https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards/issues/2
window.addEventListener('storage',
(event) => {
// The `key` is `null` if the event was caused by `.clear()`
if (event.key !== 'access_token' && event.key !== null) {
return;
}
console.warn(
'Noticed changes to access_token (most likely from another tab), updating isAuthenticated');
this.setIsAuthenticatedSubject$(this.hasValidAccessToken(), AuthenticatedActionEnum.update);
if (!this.hasValidAccessToken()) {
console.log('storage access_token updated !this.hasValidAccessToken()');
this.logout();
this.navigateToLoginPage();
}
});
// init login stata
this.setIsAuthenticatedSubject$(this.hasValidAccessToken(), AuthenticatedActionEnum.update);
// TODO: some time receive 'message' with data 'error' == 'session_error''
/*window.addEventListener('message', e => {
console.log('message', e);;
if (e.origin === 'https://dev-w08xm0pi.us.auth0.com') {
console.log('message stopPropagation');;
e.stopPropagation();
}
});
this.oauthService.events.subscribe(x => {
console.log('events', x);
});*/
this.oauthService.events
.pipe(filter(e => ['session_terminated', 'session_error'].includes(e.type)))
.subscribe(e => {
console.log('events session_terminated', e, this.hasValidAccessToken());
this.navigateToLoginPage();
});
this.oauthService.setupAutomaticSilentRefresh();
}
private setIsAuthenticatedSubject$(
isAuthenticated: boolean,
action: AuthenticatedActionEnum
) {
var state = this.isAuthenticatedSubject$.getValue();
if (state.isAuthenticated !== isAuthenticated || state.action !== action
) {
this.isAuthenticatedSubject$.next({ isAuthenticated, action });
}
}
private navigateToLoginPage() {
this.router.navigateByUrl('/authentication/signin');
}
logout() {
this.oauthService.logOut(true);
}
refresh() {
const refreshToken = this.oauthService.getRefreshToken();
if (refreshToken) {
this.oauthService.refreshToken();
}
}
hasValidAccessToken() {
return this.oauthService.hasValidAccessToken();
}
getAccessToken() {
return this.oauthService.getAccessToken();
}
login(username: string, password: string) {
//return this.oauthService.fetchTokenUsingPasswordFlow(username, password);
return from(this.oauthService.fetchTokenUsingPasswordFlow(username, password));
}
loginByExternalLogin(
provider: string,
token: string,
redirectUrl?: string
): Observable<GeneralResultModel> {
var result = new Subject<GeneralResultModel>();
let params = {
token: token,
provider: provider,
};
this.oauthService
.fetchTokenUsingGrant('external', params)
.then(x => {
this.setIsAuthenticatedSubject$(this.hasValidAccessToken(), AuthenticatedActionEnum.loggedIn);
result.next({ success: true });
})
.catch(err => {
result.next({ success: false, error: { code: err, description: err } });
});
return result;
}
loadUserProfile(): Promise<object> {
return this.oauthService.loadUserProfile();
}
runInitialLoginSequence(): Promise<void> {
if (location.hash) {
//console.log('Encountered hash fragment, plotting as table...');
//console.table(location.hash.substr(1).split('&').map(kvp => kvp.split('=')));
}
if (this.isDoneLoadingFailed) {
return new Promise<void>(resolve => setTimeout(() => resolve(), 1500));
}
// 0. LOAD CONFIG:
// First we have to check to see how the IdServer is
// currently configured:
return this.oauthService.loadDiscoveryDocument()
// For demo purposes, we pretend the previous call was very slow
.then(() => {
new Promise<void>(resolve => setTimeout(() => resolve(), 1500));
})
// 1. HASH LOGIN:
// Try to log in via hash fragment after redirect back
// from IdServer from initImplicitFlow:
.then(() => {
this.oauthService.tryLogin();
})
.then(() => {
if (this.hasValidAccessToken()) {
return Promise.resolve();
}
// Refresh via refresh_token (no /connect/authorize cookie session).
const refreshToken = this.oauthService.getRefreshToken();
if (!refreshToken) {
return Promise.resolve();
}
return this.oauthService.refreshToken()
.then(() => Promise.resolve())
.catch(result => {
const error =
result?.reason?.error ??
result?.params?.error ??
result?.error;
const softErrors = [
'interaction_required',
'login_required',
'account_selection_required',
'consent_required',
'access_denied',
'invalid_grant',
];
if (error && softErrors.indexOf(error) >= 0) {
console.warn('Token refresh needs user login.', error);
return Promise.resolve();
}
return Promise.reject(result);
});
})
.then(() => {
this.isDoneLoadingSubject$.next(true);
// Check for the strings 'undefined' and 'null' just to be sure. Our current
// login(...) should never have this, but in case someone ever calls
// initImplicitFlow(undefined | null) this could happen.
if (this.oauthService.state &&
this.oauthService.state !== 'undefined' &&
this.oauthService.state !== 'null') {
let stateUrl = this.oauthService.state;
if (stateUrl.startsWith('/') === false) {
stateUrl = decodeURIComponent(stateUrl);
}
console.log(`There was state of ${this.oauthService.state}, so we are sending you to: ${stateUrl}`);
this.router.navigateByUrl(stateUrl);
}
})
.catch(() => this.isDoneLoadingSubject$.next(true));
}
}
export enum AuthenticatedActionEnum {
init,
update,
loggedIn,
loggedOff,
}
export interface IAuthenticatedState {
isAuthenticated: boolean,
action: AuthenticatedActionEnum,
redirectUrl?: string,
}
@@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable()
export class RightSidebarService {
private sidebarSubject: BehaviorSubject<boolean> = new BehaviorSubject(false);
sidebarState = this.sidebarSubject.asObservable();
setRightSidebar = (value: boolean) => {
this.sidebarSubject.next(value);
};
constructor() {
//constructor
}
}
@@ -0,0 +1,22 @@
import { getSafeRedirectUrl } from './safe-redirect';
describe('getSafeRedirectUrl', () => {
it('returns fallback when raw is empty', () => {
expect(getSafeRedirectUrl(null)).toBe('/dashboard/rests');
expect(getSafeRedirectUrl(undefined)).toBe('/dashboard/rests');
expect(getSafeRedirectUrl('')).toBe('/dashboard/rests');
});
it('allows relative in-app paths', () => {
expect(getSafeRedirectUrl('/accounts')).toBe('/accounts');
expect(getSafeRedirectUrl('%2Fdashboard%2Frests')).toBe('/dashboard/rests');
});
it('blocks open redirects', () => {
expect(getSafeRedirectUrl('//evil.com')).toBe('/dashboard/rests');
expect(getSafeRedirectUrl('https://evil.com')).toBe('/dashboard/rests');
expect(getSafeRedirectUrl('http://evil.com')).toBe('/dashboard/rests');
expect(getSafeRedirectUrl('/path?next=https://evil.com')).toBe('/dashboard/rests');
});
});
@@ -0,0 +1,24 @@
/**
* Only allow in-app relative paths (blocks open redirects via ?r=).
*/
export function getSafeRedirectUrl(
raw: string | null | undefined,
fallback = '/dashboard/rests'
): string {
if (!raw) {
return fallback;
}
let url: string;
try {
url = decodeURIComponent(raw).trim();
} catch {
return fallback;
}
if (!url.startsWith('/') || url.startsWith('//') || url.includes('://')) {
return fallback;
}
return url;
}
@@ -0,0 +1,21 @@
import {
FormGroup,
ValidationErrors,
ValidatorFn,
Validators,
} from '@angular/forms';
export const atLeastOne = (validator: ValidatorFn, controls: string[] = []) => (
group: FormGroup,
): ValidationErrors | null => {
if (!controls) {
controls = Object.keys(group.controls);
}
const hasAtLeastOne = group && group.controls && controls
.some(k => !validator(group.controls[k]));
return hasAtLeastOne ? null : {
atLeastOne: true,
};
};
@@ -0,0 +1,24 @@
import {
FormGroup,
ValidationErrors,
ValidatorFn,
Validators,
} from '@angular/forms';
export const atLeastOneNumber = (validator: ValidatorFn, controls: string[] = []) =>
(group: FormGroup,): ValidationErrors | null => {
if (!controls) {
controls = Object.keys(group.controls);
}
const hasAtLeastOne = group && group.controls && controls
.some(k => {
var v = group.controls[k].value;
return !!(v && !isNaN(v) && parseFloat(v) !== 0 && !validator(group.controls[k]));
});
return hasAtLeastOne ? null : {
atLeastOne: true,
};
};
@@ -0,0 +1,37 @@
// angular
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
// app
import { Page404Component } from '../authentication/page404/page404.component';
import { Dashboard2Component } from './dashboard2/dashboard2.component';
import { DashboardIncomeComponent } from './income/dashboard.component';
import { DashboardOutcomeComponent } from './outcome/dashboard.component';
const routes: Routes = [
{
path: '',
redirectTo: 'rests',
pathMatch: 'full',
},
{
path: 'rests',
component: Dashboard2Component,
},
{
path: 'income',
component: DashboardIncomeComponent,
},
{
path: 'outcome',
component: DashboardOutcomeComponent,
},
{ path: '**', component: Page404Component },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class DashboardRoutingModule {
}
@@ -0,0 +1,55 @@
// angular
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DecimalPipe } from '@angular/common';
// libs
import { NgScrollbarModule } from 'ngx-scrollbar';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatTooltipModule } from '@angular/material/tooltip';
import { NgApexchartsModule } from 'ng-apexcharts';
import { TranslateModule } from '@ngx-translate/core';
import { MAT_DATE_LOCALE } from '@angular/material/core';
// app
import { DashboardRoutingModule } from './dashboard-routing.module';
import { Dashboard2Component } from './dashboard2/dashboard2.component';
import { DashboardIncomeComponent } from './income/dashboard.component';
import { DashboardOutcomeComponent } from './outcome/dashboard.component';
import { ComponentsModule } from 'src/app/shared/components/components.module';
import { SharedModule } from '../shared/shared.module';
@NgModule({
declarations: [
Dashboard2Component,
DashboardIncomeComponent,
DashboardOutcomeComponent,
],
imports: [
CommonModule,
DashboardRoutingModule,
NgApexchartsModule,
NgScrollbarModule,
MatIconModule,
MatButtonModule,
MatMenuModule,
MatTooltipModule,
MatCheckboxModule,
DragDropModule,
MatProgressBarModule,
ComponentsModule,
SharedModule,
TranslateModule,
],
providers: [
{ provide: MAT_DATE_LOCALE, useValue: 'en-GB' },
DecimalPipe,
]
})
export class DashboardModule {
}
@@ -0,0 +1,103 @@
<section class="content">
<div class="content-block">
<div class="block-header">
<!-- breadcrumb -->
<app-breadcrumb [title]="'MENUITEMS.DASHBOARD.LIST.DASHBOARD'" [items]="['HOME']" [active_item]="'MENUITEMS.DASHBOARD.LIST.DASHBOARD'"></app-breadcrumb>
</div>
<div class="row">
<div class="col-4 col-sm-4 col-md-4">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h5>{{'BALANCE' | translate}}</h5>
</div>
<h3 class="text-danger">{{dashboardModel?.balance | number: '0.2-2'}}</h3>
</div>
</div>
</div>
</div>
<div class="col-4 col-sm-4 col-md-4">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h5>{{'DEBIT.BALANCE' | translate}}</h5>
<p class="text-muted"></p>
</div>
<h3 class="text-success">{{dashboardModel?.balanceDebit | number: '0.2-2'}}</h3>
</div>
</div>
</div>
</div>
<div class="col-4 col-sm-4 col-md-4">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h5>{{'CREDIT.BALANCE' | translate}}</h5>
</div>
<h3 class="text-danger">{{dashboardModel?.balanceCredit | number: '0.2-2'}}</h3>
</div>
</div>
</div>
</div>
</div>
<div class="row clearfix">
<!-- Bar chart with line -->
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12">
<div class="card">
<div class="header">
<h2>{{'CURRENT.BALANCE' | translate}}</h2>
<button mat-icon-button [matMenuTriggerFor]="menu" class="header-dropdown">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item>Action</button>
<button mat-menu-item>Another action</button>
<button mat-menu-item>Something else here</button>
</mat-menu>
</div>
<div class="body">
<div id="chart">
<apx-chart #chart *ngIf="pieChartOptions" class="apex-pie-center"
[series]="pieChartOptions.series2!"
[chart]="pieChartOptions.chart!"
[labels]="pieChartOptions.labels!"
[responsive]="pieChartOptions.responsive!"
[tooltip]="pieChartOptions.tooltip!"
>
</apx-chart>
</div>
<div class="table-responsive m-t-15">
<table class="table align-items-center">
<tbody>
<tr *ngFor="let item of top10Balance">
<td><i class="fa fa-circle col-cyan msr-2"></i> {{item.name}}</td>
<td class="col-green" style="text-align: right;">{{item.balance | number: '1.2'}} ({{item.currencyShortName}})</td>
<td class="col-green" style="text-align: right;">{{item.balanceAtRate | number: '1.2'}}</td>
</tr>
<tr>
<td><i class="fa fa-circle col-green msr-2"></i> {{'OTHER' | translate}}</td>
<td></td>
<td class="col-green" style="text-align: right;">{{top10BalanceOther | number: '1.2'}}</td>
</tr>
<tr>
<td style="text-align: right; font-size: 16px;"><i class="fa fa-circle col-orange msr-2"></i> {{'TOTAL' | translate}}</td>
<td></td>
<td class="col-green" style="text-align: right; font-size: 16px;">{{top10BalanceTotal | number: '1.2'}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Dashboard2Component } from './dashboard2.component';
describe('Dashboard2Component',
() => {
let component: Dashboard2Component;
let fixture: ComponentFixture<Dashboard2Component>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [Dashboard2Component]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(Dashboard2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,155 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { OnInit } from '@angular/core';
import { ViewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { DecimalPipe } from '@angular/common';
// libs
import {
ApexAxisChartSeries,
ApexChart,
ApexXAxis,
ApexDataLabels,
ApexStroke,
ApexMarkers,
ApexYAxis,
ApexGrid,
ApexTitleSubtitle,
ApexTooltip,
ApexLegend,
ApexFill,
ApexResponsive,
ApexNonAxisChartSeries,
} from 'ng-apexcharts';
import { ChartComponent } from "ng-apexcharts";
// app
import { ApiRoutes } from '../../api-routes';
import { DashboardModel } from '../../model/dashboard.model';
import { DashboardRestModel } from '../../model/dashboard.model';
export type ChartOptions = {
series: ApexAxisChartSeries;
series2: ApexNonAxisChartSeries;
chart: ApexChart;
xaxis: ApexXAxis;
stroke: ApexStroke;
dataLabels: ApexDataLabels;
markers: ApexMarkers;
colors: string[];
yaxis: ApexYAxis;
grid: ApexGrid;
legend: ApexLegend;
tooltip: ApexTooltip;
fill: ApexFill;
title: ApexTitleSubtitle;
responsive: ApexResponsive[];
labels: string[];
};
@Component({
selector: 'app-dashboard2',
templateUrl: './dashboard2.component.html',
styleUrls: ['./dashboard2.component.scss'],
})
export class Dashboard2Component implements OnInit {
@ViewChild("chart") chart!: ChartComponent;
public pieChartOptions!: Partial<ChartOptions>;
top10Balance?: DashboardRestModel[];
top10BalanceOther?: number;
top10BalanceTotal?: number;
dashboardModel?: DashboardModel;
private readonly destroyRef = inject(DestroyRef);
// color: ["#3FA7DC", "#F6A025", "#9BC311"],
constructor(
private httpClient: HttpClient,
private _decimalPipe: DecimalPipe
) {
}
ngOnInit() {
this.httpClient
.get<DashboardModel>(ApiRoutes.Dashboard)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
data.incomeChange = this.calcChanges(data.incomeLast, data.incomePrevious);
data.outcomeChange = this.calcChanges(data.outcomeLast, data.outcomePrevious);
var labels = [];
var series2: number[] = [];
for (var i = 0; i < data.balanceRests.length; i++) {
let item = data.balanceRests[i];
labels.push(item.name);
series2.push(item.balanceAtRate);
}
this.balanceChart(labels, series2);
this.top10Balance = data.balanceRests
.filter(x => x.balanceAtRate > 0)
.sort(x => x.balanceAtRate)
.slice(0, 10);
this.top10BalanceTotal = data.balanceRests.reduce((sum, current) => sum + current.balanceAtRate, 0);
this.top10BalanceOther = this.top10Balance.reduce((sum, current) => sum + current.balanceAtRate, 0);
this.top10BalanceOther = this.top10BalanceTotal - this.top10BalanceOther;
this.dashboardModel = data;
});
}
private calcChanges(newValue: number, oldValue: number): number {
if (newValue > oldValue) {
return (newValue - oldValue) / oldValue * 100;
}
else if (newValue < oldValue) {
return (oldValue - newValue) / oldValue * 100;
}
return 0;
}
private balanceChart(labels: string[], series2: number[]) {
var self = this;
this.pieChartOptions = {
labels: labels,
series2: series2,
chart: {
type: 'donut',
width: 600,
height: 600,
},
legend: {
show: true,
},
dataLabels: {
enabled: false,
},
responsive: [{
breakpoint: 480,
options: {
legend: {
position: 'bottom'
}
},
}],
tooltip: {
x: {
show: false,
},
y: {
formatter: function (value, series) {
return self._decimalPipe.transform(value, '1.2-2') || "";
}
}
}
};
}
}
@@ -0,0 +1,70 @@
<section class="content">
<div class="content-block">
<div class="block-header">
<!-- breadcrumb -->
<app-breadcrumb [title]="'MENUITEMS.DASHBOARD.LIST.INCOME'" [items]="['HOME']" [active_item]="'MENUITEMS.DASHBOARD.LIST.INCOME'"></app-breadcrumb>
</div>
<div class="row">
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12">
<div class="card">
<div class="header">
<h2>{{'PERIOD' | translate}}</h2>
</div>
<div class="body">
<div class="example-container">
<app-date-period [from]="dateFrom" [to]="dateTo" (onChange)="onChangePeriod($event)"></app-date-period>
</div>
</div>
</div>
</div>
</div>
<div class="row clearfix">
<!-- Bar chart with line -->
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12">
<div class="card">
<div class="header">
<h2>{{'INCOME' | translate}}</h2>
<button mat-icon-button [matMenuTriggerFor]="menu" class="header-dropdown">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item>Action</button>
<button mat-menu-item>Another action</button>
<button mat-menu-item>Something else here</button>
</mat-menu>
</div>
<div class="body">
<div id="chart">
<apx-chart #chart *ngIf="chartOptions" class="apex-pie-center" (contextmenu)="onRightClick($event)"
[series]="chartOptions.series!"
[chart]="chartOptions.chart!"
[dataLabels]="chartOptions.dataLabels!"
[plotOptions]="chartOptions.plotOptions!"
[title]="chartOptions.title!"
[legend]="chartOptions.legend!">
</apx-chart>
</div>
<div class="table-responsive m-t-15" *ngIf="dashboardModel">
<table class="table align-items-center">
<tbody>
<tr *ngFor="let item of dashboardModel!.details">
<td><i class="fa fa-circle col-cyan msr-2"></i> {{item.name}}</td>
<td [ngClass]="{ 'col-green': item.value != 0, 'col-red': item.value == 0}" style="text-align: right;">{{item.valueRaw | number: '1.2'}}</td>
<td [ngClass]="{ 'col-green': item.value != 0, 'col-red': item.value == 0}" style="text-align: right;">{{item.value | number: '1.2'}}</td>
</tr>
<tr>
<td class="col-cyan">{{'TOTAL' | translate}}</td>
<td class="col-cyan" style="text-align: right;"></td>
<td class="col-cyan" style="text-align: right;">{{total | number: '1.2'}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Dashboard2Component } from './dashboard2.component';
describe('Dashboard2Component',
() => {
let component: Dashboard2Component;
let fixture: ComponentFixture<Dashboard2Component>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [Dashboard2Component]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(Dashboard2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,146 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { OnInit } from '@angular/core';
import { ViewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpParams } from '@angular/common/http';
import { DecimalPipe } from '@angular/common';
// libs
import {
ChartComponent,
ApexAxisChartSeries,
ApexTitleSubtitle,
ApexDataLabels,
ApexChart,
ApexPlotOptions,
ApexLegend
} from "ng-apexcharts";
import moment from 'moment';
// app
import { ApiRoutes } from '../../api-routes';
import { DashboardInOutModel } from '../../model/dashboard.model';
export type ChartOptions = {
series: ApexAxisChartSeries;
chart: ApexChart;
dataLabels: ApexDataLabels;
title: ApexTitleSubtitle;
plotOptions: ApexPlotOptions;
legend: ApexLegend;
};
@Component({
selector: 'app-dashboard-income',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'],
})
export class DashboardIncomeComponent implements OnInit {
@ViewChild("chart") chart!: ChartComponent;
public chartOptions!: Partial<ChartOptions>;
dashboardModel?: DashboardInOutModel;
total?: number;
public dateFrom: Date = moment(new Date).add(-30, 'days').toDate();
public dateTo: Date = moment(new Date).add(0, 'days').toDate();
colors: string[] = ["#fd7f6f", "#7eb0d5", "#b2e061", "#bd7ebe", "#ffb55a", "#ffee65", "#beb9db", "#fdcce5", "#8bd3c7"];
private readonly destroyRef = inject(DestroyRef);
constructor(
private httpClient: HttpClient,
private _decimalPipe: DecimalPipe
) {
}
ngOnInit() {
this.loadData();
}
onChangePeriod(dates: Date[]) {
this.dateFrom = dates[0];
this.dateTo = dates[1];
this.loadData();
}
loadData(update?: boolean, category?: string) {
let params = new HttpParams()
.set('from', moment(this.dateFrom).format('YYYY-MM-DD'))
.set('to', moment(this.dateTo).format('YYYY-MM-DD'))
.set('category', category || '')
;
this.httpClient
.get<DashboardInOutModel>(ApiRoutes.DashboardIncome, { params: params })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(response => {
this.dashboardModel = response;
var series = response.data.map(x => ({
x: x.name + '(' + (this._decimalPipe.transform(x.value, '1.2-2') || "") + ')',
y: x.value
}));
var min = Math.min(...response.data.map(x => x.value));
var max = Math.max(...response.data.map(x => x.value));
var step = max / 10;
var ranges = [];
for (var i = 0; i < this.colors.length; i++) {
ranges.push({
from: i === 0 ? min : step * i,
to: i === this.colors.length - 1 ? max + 10 : step * (i + 1),
color: this.colors[this.colors.length - i - 1],
});
}
this.total = response.data.reduce((sum, current) => sum + current.value, 0);
if (update) {
this.chart.updateSeries([{
data: series
}]);
} else {
this.setChartOptions(series, ranges);
}
});
}
onRightClick(event?: Event) {
event?.preventDefault();
this.loadData(true);
}
private setChartOptions(data: any[], ranges: any[]) {
var self = this;
this.chartOptions = {
series: [{
data: data
}],
chart: {
type: 'treemap',
width: 600,
height: 600,
events: {
click: function (event, chartContext, config) {
var data = self.dashboardModel!.data[config.dataPointIndex];
self.loadData(true, data.id);
}
}
},
plotOptions: {
treemap: {
enableShades: true,
shadeIntensity: 0.5,
reverseNegativeShade: true,
colorScale: {
ranges: ranges
}
}
},
};
}
}
@@ -0,0 +1,72 @@
<section class="content">
<div class="content-block">
<div class="block-header">
<!-- breadcrumb -->
<app-breadcrumb [title]="'MENUITEMS.DASHBOARD.LIST.OUTCOME'" [items]="['HOME']" [active_item]="'MENUITEMS.DASHBOARD.LIST.OUTCOME'"></app-breadcrumb>
</div>
<div class="row">
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12">
<div class="card">
<div class="header">
<h2>{{'PERIOD' | translate}}</h2>
</div>
<div class="body">
<div class="example-container">
<app-date-period [from]="dateFrom" [to]="dateTo" (onChange)="onChangePeriod($event)"></app-date-period>
</div>
</div>
</div>
</div>
</div>
<div class="row clearfix">
<!-- Bar chart with line -->
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12">
<div class="card">
<div class="header">
<h2>{{'OUTCOME' | translate}}</h2>
<!--
<button mat-icon-button [matMenuTriggerFor]="menu" class="header-dropdown">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item>Action</button>
<button mat-menu-item>Another action</button>
<button mat-menu-item>Something else here</button>
</mat-menu>
-->
</div>
<div class="body">
<apx-chart #chart *ngIf="chartOptions" class="apex-pie-center" (contextmenu)="onRightClick($event)"
[series]="chartOptions.series!"
[chart]="chartOptions.chart!"
[dataLabels]="chartOptions.dataLabels!"
[plotOptions]="chartOptions.plotOptions!"
[title]="chartOptions.title!"
[legend]="chartOptions.legend!">
</apx-chart>
<div class="table-responsive m-t-15" *ngIf="dashboardModel">
<table class="table align-items-center">
<tbody>
<tr *ngFor="let item of dashboardModel!.details">
<td><i class="fa fa-circle col-cyan msr-2"></i> {{item.name}}</td>
<td [ngClass]="{ 'col-green': item.value != 0, 'col-red': item.value == 0}" style="text-align: right;">{{item.valueRaw | number: '1.2'}}</td>
<td [ngClass]="{ 'col-green': item.value != 0, 'col-red': item.value == 0}" style="text-align: right;">{{item.value | number: '1.2'}}</td>
</tr>
<tr>
<td class="col-cyan">{{'TOTAL' | translate}}</td>
<td class="col-cyan" style="text-align: right;"></td>
<td class="col-cyan" style="text-align: right;">{{total | number: '1.2'}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Dashboard2Component } from './dashboard2.component';
describe('Dashboard2Component',
() => {
let component: Dashboard2Component;
let fixture: ComponentFixture<Dashboard2Component>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [Dashboard2Component]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(Dashboard2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,145 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { OnInit } from '@angular/core';
import { ViewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpParams } from '@angular/common/http';
import { DecimalPipe } from '@angular/common';
// libs
import {
ChartComponent,
ApexAxisChartSeries,
ApexTitleSubtitle,
ApexDataLabels,
ApexChart,
ApexPlotOptions,
ApexLegend
} from "ng-apexcharts";
import moment from 'moment';
// app
import { ApiRoutes } from '../../api-routes';
import { DashboardInOutModel } from '../../model/dashboard.model';
export type ChartOptions = {
series: ApexAxisChartSeries;
chart: ApexChart;
dataLabels: ApexDataLabels;
title: ApexTitleSubtitle;
plotOptions: ApexPlotOptions;
legend: ApexLegend;
};
@Component({
selector: 'app-dashboard-outcome',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'],
})
export class DashboardOutcomeComponent implements OnInit {
@ViewChild("chart") chart!: ChartComponent;
public chartOptions!: Partial<ChartOptions>;
dashboardModel?: DashboardInOutModel;
total?: number;
public dateFrom: Date = moment(new Date).add(-30, 'days').toDate();
public dateTo: Date = moment(new Date).add(0, 'days').toDate();
colors: string[] = ["#fd7f6f", "#7eb0d5", "#b2e061", "#bd7ebe", "#ffb55a", "#ffee65", "#beb9db", "#fdcce5", "#8bd3c7"];
private readonly destroyRef = inject(DestroyRef);
constructor(
private httpClient: HttpClient,
private _decimalPipe: DecimalPipe
) {
}
ngOnInit() {
this.loadData();
}
onChangePeriod(dates: Date[]) {
this.dateFrom = dates[0];
this.dateTo = dates[1];
this.loadData();
}
onRightClick(event?: Event) {
event?.preventDefault();
this.loadData(true);
}
private loadData(update?: boolean, category?: string) {
let params = new HttpParams()
.set('from', moment(this.dateFrom).format('YYYY-MM-DD'))
.set('to', moment(this.dateTo).format('YYYY-MM-DD'))
.set('category', category || '')
;
this.httpClient
.get<DashboardInOutModel>(ApiRoutes.DashboardOutcome, { params: params })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(response => {
this.dashboardModel = response;
var series = response.data.map(x => ({
x: x.name + ' (' + (this._decimalPipe.transform(x.value, '1.2-2') || '') + ')',
y: x.value
}));
var min = Math.min(...response.data.map(x => x.value));
var max = Math.max(...response.data.map(x => x.value));
var step = max / 10;
var ranges = [];
for (var i = 0; i < this.colors.length; i++) {
ranges.push({
from: i === 0 ? min : step * i,
to: i === this.colors.length - 1 ? max + 10 : step * (i + 1),
color: this.colors[this.colors.length - i - 1],
});
}
this.total = response.data.reduce((sum, current) => sum + current.value, 0);
if (update) {
this.chart.updateSeries([{
data: series
}]);
} else {
this.setChartOptions(series, ranges);
}
});
}
private setChartOptions(data: any[], ranges: any[]) {
var self = this;
this.chartOptions = {
series: [{
data: data
}],
chart: {
type: 'treemap',
width: 600,
height: 600,
events: {
click: function (event, chartContext, config) {
var data = self.dashboardModel!.data[config.dataPointIndex];
self.loadData(true, data.id);
}
}
},
plotOptions: {
treemap: {
enableShades: true,
shadeIntensity: 0.5,
reverseNegativeShade: true,
colorScale: {
ranges: ranges
}
}
},
};
}
}
@@ -0,0 +1,3 @@
<div [dir]="direction">
<router-outlet></router-outlet>
</div>
@@ -0,0 +1,66 @@
import { BidiModule, Direction } from '@angular/cdk/bidi';
import { Component, DestroyRef, Inject, inject, Renderer2 } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { RouterOutlet } from '@angular/router';
import { InConfiguration } from 'src/app/core/models/config.interface';
import { DirectionService } from 'src/app/core/service/direction.service';
import { ConfigService } from 'src/app/config/config.service';
import { DOCUMENT } from '@angular/common';
@Component({
selector: 'app-auth-layout',
templateUrl: './auth-layout.component.html',
styleUrls: [],
standalone: true,
imports: [BidiModule, RouterOutlet],
})
export class AuthLayoutComponent {
direction!: Direction;
config!: InConfiguration;
private readonly destroyRef = inject(DestroyRef);
constructor(
@Inject(DOCUMENT) private document: Document,
private directoryService: DirectionService,
private configService: ConfigService,
private renderer: Renderer2
) {
this.config = this.configService.configData;
this.directoryService.currentData
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((currentData) => {
if (currentData) {
this.direction = currentData === 'ltr' ? 'ltr' : 'rtl';
} else {
if (localStorage.getItem('isRtl')) {
if (localStorage.getItem('isRtl') === 'true') {
this.direction = 'rtl';
} else if (localStorage.getItem('isRtl') === 'false') {
this.direction = 'ltr';
}
} else {
if (this.config) {
if (this.config.layout.rtl === true) {
this.direction = 'rtl';
localStorage.setItem('isRtl', 'true');
} else {
this.direction = 'ltr';
localStorage.setItem('isRtl', 'false');
}
}
}
}
});
// set theme on startup
if (localStorage.getItem('theme')) {
this.renderer.removeClass(this.document.body, this.config.layout.variant);
this.renderer.addClass(
this.document.body,
localStorage.getItem('theme') as string
);
} else {
this.renderer.addClass(this.document.body, this.config.layout.variant);
}
}
}
@@ -0,0 +1,6 @@
<app-header></app-header>
<app-sidebar></app-sidebar>
<app-right-sidebar></app-right-sidebar>
<div [dir]="direction">
<router-outlet></router-outlet>
</div>
@@ -0,0 +1,55 @@
import { BidiModule, Direction } from '@angular/cdk/bidi';
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { RouterOutlet } from '@angular/router';
import { InConfiguration } from 'src/app/core/models/config.interface';
import { DirectionService } from 'src/app/core/service/direction.service';
import { ConfigService } from 'src/app/config/config.service';
import { HeaderComponent } from '../../header/header.component';
import { SidebarComponent } from '../../sidebar/sidebar.component';
import { RightSidebarComponent } from '../../right-sidebar/right-sidebar.component';
@Component({
selector: 'app-main-layout',
templateUrl: './main-layout.component.html',
styleUrls: [],
standalone: true,
imports: [BidiModule, RouterOutlet, HeaderComponent, SidebarComponent, RightSidebarComponent],
})
export class MainLayoutComponent {
direction!: Direction;
config!: InConfiguration;
private readonly destroyRef = inject(DestroyRef);
constructor(
private directoryService: DirectionService,
private configService: ConfigService
) {
this.config = this.configService.configData;
this.directoryService.currentData
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((currentData) => {
if (currentData) {
this.direction = currentData === 'ltr' ? 'ltr' : 'rtl';
} else {
if (localStorage.getItem('isRtl')) {
if (localStorage.getItem('isRtl') === 'true') {
this.direction = 'rtl';
} else if (localStorage.getItem('isRtl') === 'false') {
this.direction = 'ltr';
}
} else {
if (this.config) {
if (this.config.layout.rtl === true) {
this.direction = 'rtl';
localStorage.setItem('isRtl', 'true');
} else {
this.direction = 'ltr';
localStorage.setItem('isRtl', 'false');
}
}
}
}
});
}
}
@@ -0,0 +1,179 @@
<nav #navbar class="navbar active">
<div class="container-fluid">
<div class="collapse navbar-collapse">
<ul class="float-start collapse-menu-icon">
<li>
<button mat-button (click)="mobileMenuSidebarOpen($event,'overlay-open')" class="sidemenu-collapse">
<app-feather-icons [icon]="'menu'" [class]="'header-icon'"></app-feather-icons>
</button>
</li>
</ul>
<ul class="float-start navbar-nav navbar-left">
<li class="nav-item btnAppList" ngbDropdown>
<button mat-button [matMenuTriggerFor]="appDropdownMenu" class="nav-notification-icons">
<app-feather-icons [icon]="'grid'" [class]="'header-icon'"></app-feather-icons>
</button>
<mat-menu #appDropdownMenu="matMenu" class="notification-dropdown app-dropdown" xPosition="before">
<div class="noti-list">
<ul class="menu">
<li class="nfc-header">
<h5 class="mb-0">Applications</h5>
</li>
<li>
<div class="row g-0 p-1">
<div class="col-3 text-center">
<a routerLink="/apps/chat" class="app-icons">
<img src="assets/images/apps/chat.png">
<p class="tx-12 mb-0">Chat</p>
</a>
</div>
<div class="col-3 text-center">
<a routerLink="/calendar" class="app-icons">
<img src="assets/images/apps/calendar.png">
<p class="tx-12 mb-0">Calendar</p>
</a>
</div>
<div class="col-3 text-center">
<a routerLink="/task" class="app-icons">
<img src="assets/images/apps/task.png">
<p class="tx-12 mb-0">Task</p>
</a>
</div>
<div class="col-3 text-center">
<a routerLink="/email/inbox" class="app-icons">
<img src="assets/images/apps/mail.png">
<p class="tx-12 mb-0">Mail</p>
</a>
</div>
</div>
<div class="row g-0 p-1">
<div class="col-3 text-center">
<a routerLink="/contacts" class="app-icons">
<img src="assets/images/apps/contact.png">
<p class="tx-12 mb-0">Contact</p>
</a>
</div>
<div class="col-3 text-center">
<a routerLink="/apps/support" class="app-icons">
<img src="assets/images/apps/support.png">
<p class="tx-12 mb-0">Support</p>
</a>
</div>
<div class="col-3 text-center">
<a routerLink="/media/gallery" class="app-icons">
<img src="assets/images/apps/gallery.png">
<p class="tx-12 mb-0">Gallery</p>
</a>
</div>
</div>
</li>
</ul>
</div>
</mat-menu>
</li>
</ul>
</div>
<div class="collapse navbar-collapse" [ngClass]="isNavbarCollapsed ? '' : 'show'">
<ul class="nav navbar-nav navbar-right">
<!-- Full Screen Button -->
<li class="fullscreen">
<button mat-button class="nav-notification-icons" (click)="callFullscreen()">
<app-feather-icons [icon]="'maximize'" [class]="'header-icon'"></app-feather-icons>
</button>
</li>
<!-- #END# Full Screen Button -->
<li class="nav-item">
<button mat-button [matMenuTriggerFor]="languagemenu" class="lang-dropdown nav-notification-icons">
<img *ngIf="flagvalue !== undefined" src="{{flagvalue}}" height="16">
<img *ngIf="flagvalue === undefined" src="{{defaultFlag}}" height="16">
</button>
<mat-menu #languagemenu="matMenu" class="lang-item-menu">
<div *ngFor="let item of listLang" class="lang-item">
<button mat-menu-item class="dropdown-item lang-item-list"
(click)="setLanguage(item.text, item.lang, item.flag)"
[ngClass]="{'active': langStoreValue === item.lang}">
<img src="{{item.flag}}" class="flag-img" height="12"> <span class="align-middle">{{item.text}}</span>
</button>
</div>
</mat-menu>
</li>
<!-- #START# Notifications-->
<li>
<button mat-button [matMenuTriggerFor]="notificationMenu" class="nav-notification-icons">
<app-feather-icons [icon]="'bell'" [class]="'header-icon'"></app-feather-icons>
</button>
<mat-menu #notificationMenu="matMenu" class="nfc-menu">
<div class="nfc-header">
<h5 class="mb-0">Notitications</h5>
<a class="nfc-mark-as-read">Mark all as read</a>
</div>
<div class="nfc-dropdown">
<ng-scrollbar style="height: 350px" visibility="hover">
<div class="noti-list header-menu">
<div class="menu">
<div>
<button mat-menu-item *ngFor="let notification of notifications" onClick="return false;"
[ngClass]="[notification.status]">
<span class="table-img msg-user ">
<i class="material-icons-two-tone nfc-type-icon"
[ngClass]="[notification.color]">
{{notification.icon}}
</i>
</span>
<span class="menu-info">
<span class="menu-title">{{notification.message}}</span>
<span class="menu-desc mt-2">
<i class="material-icons">access_time</i> {{notification.time}}
</span>
</span>
<span class="nfc-close">
<app-feather-icons [icon]="'x'" [class]="'user-menu-icons'"></app-feather-icons>
</span>
</button>
</div>
</div>
</div>
</ng-scrollbar>
</div>
<div class="nfc-footer">
<a class="nfc-read-all">
Read
All Notifications
</a>
</div>
</mat-menu>
</li>
<!-- #END# Notifications-->
<li class="nav-item user_profile">
<button mat-button [matMenuTriggerFor]="profilemenu">
<div class="chip dropdown-toggle" ngbDropdownToggle class="">
<span>{{userName}}</span>
<img src="{{userImg}}" class="user_img" width="32" height="32" alt="User">
</div>
</button>
<mat-menu #profilemenu="matMenu" class="profile-menu">
<div class="noti-list">
<div class="menu ">
<div class="user_dw_menu">
<button mat-menu-item routerLink="/user/profile">
<app-feather-icons [icon]="'user'" [class]="'user-menu-icons'" routerlink="/user/profile"></app-feather-icons>Account
</button>
<button mat-menu-item>
<app-feather-icons [icon]="'mail'" [class]="'user-menu-icons'"></app-feather-icons>Inbox
</button>
<button mat-menu-item>
<app-feather-icons [icon]="'settings'" [class]="'user-menu-icons'"></app-feather-icons>Settings
</button>
<button mat-menu-item (click)="logout()">
<app-feather-icons [icon]="'log-out'" [class]="'user-menu-icons'"></app-feather-icons>Logout
</button>
</div>
</div>
</div>
</mat-menu>
</li>
<!-- #END# Tasks -->
</ul>
</div>
</div>
</nav>
@@ -0,0 +1 @@
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
describe('HeaderComponent',
() => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [HeaderComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,264 @@
// angular
import { DOCUMENT } from '@angular/common';
import { Component } from '@angular/core';
import { Inject } from '@angular/core';
import { ElementRef } from '@angular/core';
import { OnInit } from '@angular/core';
import { Renderer2 } from '@angular/core';
import { AfterViewInit } from '@angular/core';
import { Router } from '@angular/router';
// libs
import { NgScrollbarModule } from 'ngx-scrollbar';
// app
import { ConfigService } from 'src/app/config/config.service';
import { InConfiguration } from 'src/app/core/models/config.interface';
import { AuthService } from 'src/app/core/service/auth.service';
import { LanguageService } from 'src/app/core/service/language.service';
import { UnsubscribeOnDestroyAdapter } from 'src/app/shared/UnsubscribeOnDestroyAdapter';
import { SharedModule } from 'src/app/shared/shared.module';
interface Notifications {
message: string;
time: string;
icon: string;
color: string;
status: string;
}
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
standalone: true,
imports: [SharedModule, NgScrollbarModule],
})
export class HeaderComponent extends UnsubscribeOnDestroyAdapter implements OnInit, AfterViewInit {
config!: InConfiguration;
userImg?: string;
userName?: string;
homePage?: string;
isNavbarCollapsed = true;
flagvalue: string | string[] | undefined;
countryName: string | string[] = [];
langStoreValue?: string;
defaultFlag?: string;
isOpenSidebar?: boolean;
docElement: HTMLElement | undefined;
isFullScreen = false;
constructor(
@Inject(DOCUMENT) private document: Document,
private renderer: Renderer2,
public elementRef: ElementRef,
private configService: ConfigService,
private authService: AuthService,
private router: Router,
public languageService: LanguageService
) {
super();
this.subs.sink = this.authService.currentUser$.subscribe(() => {
this.setUser();
});
}
listLang = [
{ text: 'English', flag: 'assets/images/flags/us.jpg', lang: 'en' },
{ text: 'Spanish', flag: 'assets/images/flags/spain.jpg', lang: 'es' },
{ text: 'German', flag: 'assets/images/flags/germany.jpg', lang: 'de' },
{ text: 'Ukraine', flag: 'assets/images/flags/ukraine.png', lang: 'ua' },
];
notifications: Notifications[] = [
{
message: 'Please check your mail',
time: '14 mins ago',
icon: 'mail',
color: 'nfc-green',
status: 'msg-unread',
},
{
message: 'New Employee Added..',
time: '22 mins ago',
icon: 'person_add',
color: 'nfc-blue',
status: 'msg-read',
},
{
message: 'Your leave is approved!! ',
time: '3 hours ago',
icon: 'event_available',
color: 'nfc-orange',
status: 'msg-read',
},
{
message: 'Lets break for lunch...',
time: '5 hours ago',
icon: 'lunch_dining',
color: 'nfc-blue',
status: 'msg-read',
},
{
message: 'Employee report generated',
time: '14 mins ago',
icon: 'description',
color: 'nfc-green',
status: 'msg-read',
},
{
message: 'Please check your mail',
time: '22 mins ago',
icon: 'mail',
color: 'nfc-red',
status: 'msg-read',
},
{
message: 'Salary credited...',
time: '3 hours ago',
icon: 'paid',
color: 'nfc-purple',
status: 'msg-read',
},
];
ngOnInit() {
this.config = this.configService.configData;
this.setUser();
this.homePage = 'dashboard/rests';
this.langStoreValue = localStorage.getItem('lang') as string;
const val = this.listLang.filter((x) => x.lang === this.langStoreValue);
this.countryName = val.map((element) => element.text);
if (val.length === 0) {
if (this.flagvalue === undefined) {
this.defaultFlag = 'assets/images/flags/us.jpg';
}
} else {
this.flagvalue = val.map((element) => element.flag);
}
}
ngAfterViewInit() {
// set theme on startup
if (localStorage.getItem('theme')) {
this.renderer.removeClass(this.document.body, this.config.layout.variant);
this.renderer.addClass(
this.document.body,
localStorage.getItem('theme') as string
);
} else {
this.renderer.addClass(this.document.body, this.config.layout.variant);
}
if (localStorage.getItem('menuOption')) {
this.renderer.addClass(
this.document.body,
localStorage.getItem('menuOption') as string
);
} else {
this.renderer.addClass(
this.document.body,
'menu_' + this.config.layout.sidebar.backgroundColor
);
}
if (localStorage.getItem('choose_logoheader')) {
this.renderer.addClass(
this.document.body,
localStorage.getItem('choose_logoheader') as string
);
} else {
this.renderer.addClass(
this.document.body,
'logo-' + this.config.layout.logo_bg_color
);
}
if (localStorage.getItem('sidebar_status')) {
if (localStorage.getItem('sidebar_status') === 'close') {
this.renderer.addClass(this.document.body, 'side-closed');
this.renderer.addClass(this.document.body, 'submenu-closed');
} else {
this.renderer.removeClass(this.document.body, 'side-closed');
this.renderer.removeClass(this.document.body, 'submenu-closed');
}
} else {
if (this.config.layout.sidebar.collapsed === true) {
this.renderer.addClass(this.document.body, 'side-closed');
this.renderer.addClass(this.document.body, 'submenu-closed');
}
}
}
callFullscreen() {
if (!this.isFullScreen) {
this.docElement?.requestFullscreen();
} else {
document.exitFullscreen();
}
this.isFullScreen = !this.isFullScreen;
}
setLanguage(text: string, lang: string, flag: string) {
this.countryName = text;
this.flagvalue = flag;
this.langStoreValue = lang;
this.languageService.setLanguage(lang);
}
mobileMenuSidebarOpen(event: Event, className: string) {
const hasClass = (event.target as HTMLInputElement).classList.contains(
className
);
if (hasClass) {
this.renderer.removeClass(this.document.body, className);
} else {
this.renderer.addClass(this.document.body, className);
}
const hasClass2 = this.document.body.classList.contains('side-closed');
if (hasClass2) {
// this.renderer.removeClass(this.document.body, "side-closed");
this.renderer.removeClass(this.document.body, 'submenu-closed');
} else {
// this.renderer.addClass(this.document.body, "side-closed");
this.renderer.addClass(this.document.body, 'submenu-closed');
}
}
callSidemenuCollapse() {
const hasClass = this.document.body.classList.contains('side-closed');
if (hasClass) {
this.renderer.removeClass(this.document.body, 'side-closed');
this.renderer.removeClass(this.document.body, 'submenu-closed');
} else {
this.renderer.addClass(this.document.body, 'side-closed');
this.renderer.addClass(this.document.body, 'submenu-closed');
}
}
logout() {
this.subs.sink = this.authService.logout().subscribe((res) => {
if (!res.success) {
this.router.navigate(['/authentication/signin']);
}
});
}
private setUser() {
this.userImg = this.authService.currentUserValue.img;
this.userName = [
this.authService.currentUserValue.firstName,
this.authService.currentUserValue.lastName
].join(' ').trim();
this.userName = this.userName || this.authService.currentUserValue.userName;
}
}
@@ -0,0 +1,2 @@
<ngx-loading-bar color="#3173D6" ref="router"></ngx-loading-bar>
<ngx-loading-bar color="#31d662" ref="http"></ngx-loading-bar>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PageLoaderComponent } from './page-loader.component';
describe('PageLoaderComponent',
() => {
let component: PageLoaderComponent;
let fixture: ComponentFixture<PageLoaderComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [PageLoaderComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(PageLoaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,15 @@
import { Component } from '@angular/core';
import { LoadingBarModule } from '@ngx-loading-bar/core';
@Component({
selector: 'app-page-loader',
templateUrl: './page-loader.component.html',
styleUrls: ['./page-loader.component.scss'],
standalone: true,
imports: [LoadingBarModule],
})
export class PageLoaderComponent {
constructor() {
// constructor
}
}
@@ -0,0 +1,48 @@
<div class="settingSidebar" [ngClass]="isOpenSidebar ? 'showSettingPanel' : ''">
<a href="javascript:void(0)" class="settingPanelToggle" (click)="toggleRightSidebar()">
<app-feather-icons [icon]="'settings'" [class]="'setting-sidebar-icon'"></app-feather-icons>
</a>
<ng-scrollbar [style.height]="maxHeight + 'px'" visibility="hover">
<div class="settingSidebar-body ps-container ps-theme-default">
<div class=" fade show active">
<div class="setting-panel-header">
Setting Panel
</div>
<div class="p-15 border-bottom">
<h6 class="font-medium m-b-10">Select Layout</h6>
<div class="flex flex-wrap hiddenradio">
<div class="flex flex-col">
<label [class.layout-selected]="!isDarTheme" (click)="lightThemeBtnClick()">
<input type="radio" name="layoutTheme" value="light" [checked]="!isDarTheme"
tabindex="-1" (click)="$event.stopPropagation()">
<img src="assets/images/light.png" alt="Light layout">
</label>
<div class="mt-1 text-md text-center"> Light </div>
</div>
<div class="flex flex-col mt-3">
<label [class.layout-selected]="isDarTheme" (click)="darkThemeBtnClick()">
<input type="radio" name="layoutTheme" value="dark" [checked]="isDarTheme"
tabindex="-1" (click)="$event.stopPropagation()">
<img src="assets/images/dark.png" alt="Dark layout">
</label>
<div class="mt-1 text-md text-center"> Dark </div>
</div>
</div>
</div>
<div class="rightSetting">
<h6 class="font-medium m-b-10">Sidebar Menu Color</h6>
<mat-button-toggle-group class="mt-2"
hideSingleSelectionIndicator
[value]="isDarkSidebar ? 'dark' : 'light'">
<mat-button-toggle (click)="lightSidebarBtnClick()" value="light">Light</mat-button-toggle>
<mat-button-toggle (click)="darkSidebarBtnClick()" value="dark">Dark</mat-button-toggle>
</mat-button-toggle-group>
</div>
<div class="rightSetting">
<h6 class="font-medium m-b-10">RTL Layout</h6>
<mat-slide-toggle class="mt-2" [checked]="isRtl" (change)="switchDirection($event)"></mat-slide-toggle>
</div>
</div>
</div>
</ng-scrollbar>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RightSidebarComponent } from './right-sidebar.component';
describe('RightSidebarComponent',
() => {
let component: RightSidebarComponent;
let fixture: ComponentFixture<RightSidebarComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [RightSidebarComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(RightSidebarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,258 @@
import { DOCUMENT } from '@angular/common';
import {
Component,
Inject,
ElementRef,
OnInit,
AfterViewInit,
Renderer2,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import { ConfigService } from 'src/app/config/config.service';
import { RightSidebarService } from 'src/app/core/service/rightsidebar.service';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { UnsubscribeOnDestroyAdapter } from 'src/app/shared/UnsubscribeOnDestroyAdapter';
import { DirectionService } from 'src/app/core/service/direction.service';
import { InConfiguration } from 'src/app/core/models/config.interface';
import { SharedModule } from 'src/app/shared/shared.module';
import { NgScrollbarModule } from 'ngx-scrollbar';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-right-sidebar',
templateUrl: './right-sidebar.component.html',
styleUrls: ['./right-sidebar.component.scss'],
standalone: true,
imports: [SharedModule, NgScrollbarModule],
})
export class RightSidebarComponent
extends UnsubscribeOnDestroyAdapter
implements OnInit, AfterViewInit {
selectedBgColor = 'white';
maxHeight!: string;
maxWidth!: string;
showpanel = false;
isOpenSidebar!: boolean;
isDarkSidebar = false;
isDarTheme = false;
innerHeight?: number;
headerHeight = 60;
isRtl = false;
config!: InConfiguration;
constructor(
@Inject(DOCUMENT) private document: Document,
private renderer: Renderer2,
public elementRef: ElementRef,
private rightSidebarService: RightSidebarService,
private configService: ConfigService,
private directionService: DirectionService,
private cdr: ChangeDetectorRef
) {
super();
}
ngOnInit() {
this.config = this.configService.configData;
this.subs.sink = this.rightSidebarService.sidebarState.subscribe(
(isRunning) => {
this.isOpenSidebar = isRunning;
}
);
this.setRightSidebarWindowHeight();
}
ngAfterViewInit() {
// Light/dark skins only (clamp legacy color-skin values).
const storedSkin = localStorage.getItem('choose_skin_active');
const skin = storedSkin === 'black' || this.config.layout.theme_color === 'black'
? 'black'
: 'white';
this.selectedBgColor = skin;
this.renderer.addClass(this.document.body, 'theme-' + skin);
localStorage.setItem('choose_skin', 'theme-' + skin);
localStorage.setItem('choose_skin_active', skin);
if (localStorage.getItem('menuOption')) {
if (localStorage.getItem('menuOption') === 'menu_dark') {
this.isDarkSidebar = true;
} else if (localStorage.getItem('menuOption') === 'menu_light') {
this.isDarkSidebar = false;
} else {
this.isDarkSidebar =
this.config.layout.sidebar.backgroundColor === 'dark' ? true : false;
}
} else {
this.isDarkSidebar =
this.config.layout.sidebar.backgroundColor === 'dark' ? true : false;
}
if (localStorage.getItem('theme')) {
if (localStorage.getItem('theme') === 'dark') {
this.isDarTheme = true;
} else if (localStorage.getItem('theme') === 'light') {
this.isDarTheme = false;
} else {
this.isDarTheme = this.config.layout.variant === 'dark' ? true : false;
}
} else {
this.isDarTheme = this.config.layout.variant === 'dark' ? true : false;
}
// Content styles use body.dark; keep in sync with the layout toggle (theme-black alone is chrome-only).
if (this.isDarTheme) {
this.renderer.removeClass(this.document.body, 'light');
this.renderer.addClass(this.document.body, 'dark');
} else {
this.renderer.removeClass(this.document.body, 'dark');
this.renderer.addClass(this.document.body, 'light');
}
if (localStorage.getItem('isRtl')) {
if (localStorage.getItem('isRtl') === 'true') {
this.setRTLSettings();
} else if (localStorage.getItem('isRtl') === 'false') {
this.setLTRSettings();
}
} else {
if (this.config.layout.rtl == true) {
this.setRTLSettings();
} else {
this.setLTRSettings();
}
}
this.cdr.markForCheck();
}
lightSidebarBtnClick() {
this.renderer.removeClass(this.document.body, 'menu_dark');
this.renderer.removeClass(this.document.body, 'logo-black');
this.renderer.addClass(this.document.body, 'menu_light');
this.renderer.addClass(this.document.body, 'logo-white');
this.isDarkSidebar = false;
localStorage.setItem('choose_logoheader', 'logo-white');
localStorage.setItem('menuOption', 'menu_light');
this.cdr.markForCheck();
}
darkSidebarBtnClick() {
this.renderer.removeClass(this.document.body, 'menu_light');
this.renderer.removeClass(this.document.body, 'logo-white');
this.renderer.addClass(this.document.body, 'menu_dark');
this.renderer.addClass(this.document.body, 'logo-black');
this.isDarkSidebar = true;
localStorage.setItem('choose_logoheader', 'logo-black');
localStorage.setItem('menuOption', 'menu_dark');
this.cdr.markForCheck();
}
lightThemeBtnClick() {
this.removeBodyThemeClasses();
this.renderer.removeClass(this.document.body, 'dark');
this.renderer.removeClass(this.document.body, 'menu_dark');
this.renderer.removeClass(this.document.body, 'logo-black');
this.renderer.addClass(this.document.body, 'light');
this.renderer.addClass(this.document.body, 'submenu-closed');
this.renderer.addClass(this.document.body, 'menu_light');
this.renderer.addClass(this.document.body, 'logo-white');
this.renderer.addClass(this.document.body, 'theme-white');
this.selectedBgColor = 'white';
this.isDarkSidebar = false;
this.isDarTheme = false;
localStorage.setItem('choose_logoheader', 'logo-white');
localStorage.setItem('choose_skin', 'theme-white');
localStorage.setItem('choose_skin_active', 'white');
localStorage.setItem('theme', 'light');
localStorage.setItem('menuOption', 'menu_light');
this.cdr.markForCheck();
}
darkThemeBtnClick() {
this.removeBodyThemeClasses();
this.renderer.removeClass(this.document.body, 'light');
this.renderer.removeClass(this.document.body, 'menu_light');
this.renderer.removeClass(this.document.body, 'logo-white');
this.renderer.addClass(this.document.body, 'dark');
this.renderer.addClass(this.document.body, 'submenu-closed');
this.renderer.addClass(this.document.body, 'menu_dark');
this.renderer.addClass(this.document.body, 'logo-black');
this.renderer.addClass(this.document.body, 'theme-black');
this.selectedBgColor = 'black';
this.isDarkSidebar = true;
this.isDarTheme = true;
localStorage.setItem('choose_logoheader', 'logo-black');
localStorage.setItem('choose_skin', 'theme-black');
localStorage.setItem('choose_skin_active', 'black');
localStorage.setItem('theme', 'dark');
localStorage.setItem('menuOption', 'menu_dark');
this.cdr.markForCheck();
}
/** Clears light/dark theme-* classes before applying a layout. */
private removeBodyThemeClasses() {
['theme-white', 'theme-black', 'theme-purple', 'theme-orange', 'theme-cyan', 'theme-green', 'theme-blue']
.forEach((themeClass) => this.renderer.removeClass(this.document.body, themeClass));
}
setRightSidebarWindowHeight() {
this.innerHeight = window.innerHeight;
const height = this.innerHeight - this.headerHeight;
this.maxHeight = height + '';
this.maxWidth = '500px';
}
onClickedOutside(event: Event) {
const button = event.target as HTMLButtonElement;
if (button.id !== 'settingBtn') {
if (this.isOpenSidebar === true) {
this.toggleRightSidebar();
}
}
}
toggleRightSidebar(): void {
this.rightSidebarService.setRightSidebar(
(this.isOpenSidebar = !this.isOpenSidebar)
);
}
switchDirection(event: MatSlideToggleChange) {
const isrtl = String(event.checked);
if (
isrtl === 'false' &&
document.getElementsByTagName('html')[0].hasAttribute('dir')
) {
document.getElementsByTagName('html')[0].removeAttribute('dir');
this.renderer.removeClass(this.document.body, 'rtl');
this.directionService.updateDirection('ltr');
} else if (
isrtl === 'true' &&
!document.getElementsByTagName('html')[0].hasAttribute('dir')
) {
document.getElementsByTagName('html')[0].setAttribute('dir', 'rtl');
this.renderer.addClass(this.document.body, 'rtl');
this.directionService.updateDirection('rtl');
}
localStorage.setItem('isRtl', isrtl);
this.isRtl = event.checked;
}
setRTLSettings() {
document.getElementsByTagName('html')[0].setAttribute('dir', 'rtl');
this.renderer.addClass(this.document.body, 'rtl');
this.isRtl = true;
localStorage.setItem('isRtl', 'true');
}
setLTRSettings() {
document.getElementsByTagName('html')[0].removeAttribute('dir');
this.renderer.removeClass(this.document.body, 'rtl');
this.isRtl = false;
localStorage.setItem('isRtl', 'false');
}
}
@@ -0,0 +1,142 @@
import { RouteInfo } from './sidebar.metadata';
export const ROUTES: RouteInfo[] = [
{
path: '',
title: 'MENUITEMS.MAIN.TEXT',
iconType: '',
icon: '',
class: '',
groupTitle: true,
badge: '',
badgeClass: '',
submenu: [],
},
{
path: '',
title: 'MENUITEMS.DASHBOARD.TEXT',
iconType: 'feather',
icon: 'home',
class: 'menu-toggle',
groupTitle: false,
badge: '',
badgeClass: '',
submenu: [
{
path: 'dashboard/rests',
title: 'MENUITEMS.DASHBOARD.LIST.DASHBOARD',
iconType: '',
icon: '',
class: 'ml-menu',
groupTitle: false,
badge: '',
badgeClass: '',
submenu: [],
},
{
path: 'dashboard/income',
title: 'MENUITEMS.DASHBOARD.LIST.INCOME',
iconType: '',
icon: '',
class: 'ml-menu',
groupTitle: false,
badge: '',
badgeClass: '',
submenu: [],
},
{
path: 'dashboard/outcome',
title: 'MENUITEMS.DASHBOARD.LIST.OUTCOME',
iconType: '',
icon: '',
class: 'ml-menu',
groupTitle: false,
badge: '',
badgeClass: '',
submenu: [],
},
],
},
// Common Modules
{
id: 'accounts',
path: '',
title: 'MENUITEMS.ACCOUNTS.TEXT',
iconType: 'feather',
icon: 'chevrons-down',
class: 'menu-toggle',
groupTitle: false,
badge: '',
badgeClass: '',
submenu: [
],
},
{
path: '',
title: 'MENUITEMS.SETTINGS.TEXT',
iconType: 'feather',
icon: 'settings',
class: 'menu-toggle',
groupTitle: false,
badge: '',
badgeClass: '',
submenu: [
{
path: '/settings/currencies',
title: 'MENUITEMS.SETTINGS.LIST.CURRENCIES',
iconType: '',
icon: '',
class: 'ml-menu',
groupTitle: false,
badge: '',
badgeClass: '',
submenu: [],
},
{
path: '/settings/account-categories',
title: 'MENUITEMS.SETTINGS.LIST.ACCOUNTCATEGORIES',
iconType: '',
icon: '',
class: 'ml-menu',
groupTitle: false,
badge: '',
badgeClass: '',
submenu: [],
},
{
path: '/settings/accounts',
title: 'MENUITEMS.SETTINGS.LIST.ACCOUNTS',
iconType: '',
icon: '',
class: 'ml-menu',
groupTitle: false,
badge: '',
badgeClass: '',
submenu: [],
},
{
path: '/settings/item-categories',
title: 'MENUITEMS.SETTINGS.LIST.ITEMCATEGORIES',
iconType: '',
icon: '',
class: 'ml-menu',
groupTitle: false,
badge: '',
badgeClass: '',
submenu: [],
},
{
path: '/settings/items',
title: 'MENUITEMS.SETTINGS.LIST.ITEMS',
iconType: '',
icon: '',
class: 'ml-menu',
groupTitle: false,
badge: '',
badgeClass: '',
submenu: [],
},
],
},
];
@@ -0,0 +1,75 @@
<div>
<!-- Left Sidebar -->
<aside id="leftsidebar" class="sidebar" (mouseenter)="mouseHover()" (mouseleave)="mouseOut()">
<div class="navbar-header">
<ul class="nav navbar-nav flex-row">
<li class="nav-item logo">
<a class="navbar-brand" routerLink="dashboard/rests">
<img src="assets/images/logo.png" alt=""/>
<span class="logo-name">ase.com.ua</span>
</a>
</li>
<li class="nav-item nav-toggle">
<button mat-icon-button (click)="callSidemenuCollapse()" class="sidemenu-collapse">
<mat-icon [ngStyle]="{'color':'#8F8C91'}" class="menuIcon">{{menuIcon}}</mat-icon>
</button>
</li>
</ul>
</div>
<!-- Menu -->
<div class="menu">
<ng-scrollbar [style.height]="listMaxHeight + 'px'" visibility="hover">
<ul class="list">
<!-- Top Most level menu -->
<li *ngFor="let sidebarItem of sidebarItems"
[routerLinkActive]="sidebarItem.submenu.length !== 0 ? 'active' : 'active-top'">
<div class="header" *ngIf="sidebarItem.groupTitle === true">{{sidebarItem.title | translate}}</div>
<a [routerLink]="sidebarItem.class === '' ? [sidebarItem.path] : null" *ngIf="!sidebarItem.groupTitle;"
[ngClass]="[sidebarItem.class]" (click)="callToggleMenu($event, sidebarItem.submenu.length)"
class="menu-top">
<i-feather [name]="sidebarItem.icon" class="sidebarIcon"></i-feather>
<span class="hide-menu">
{{sidebarItem.title | translate}}
</span>
<span *ngIf="sidebarItem.badge !== '' " [ngClass]="[sidebarItem.badgeClass]">{{sidebarItem.badge}}</span>
</a>
<!-- First level menu -->
<ul class="ml-menu" *ngIf="sidebarItem.submenu.length > 0">
<li *ngFor="let sidebarSubItem1 of sidebarItem.submenu"
[routerLinkActive]="sidebarSubItem1.submenu.length > 0 ? '' : 'active'">
<a [routerLink]="sidebarSubItem1.submenu.length > 0 ? null : [sidebarSubItem1.path]"
(click)="callToggleMenu($event,sidebarSubItem1.submenu.length)" [ngClass]="[sidebarSubItem1.class]">
{{sidebarSubItem1.title | translate}}
</a>
<!-- Second level menu -->
<ul class="ml-menu-2" *ngIf="sidebarSubItem1.submenu.length > 0">
<li *ngFor="let sidebarSubItem2 of sidebarSubItem1.submenu"
[routerLinkActive]="sidebarSubItem2.submenu.length > 0 ? '' : 'active'">
<a [routerLink]="sidebarSubItem2.submenu.length > 0 ? null : [sidebarSubItem2.path]"
(click)="callToggleMenu($event,sidebarSubItem2.submenu.length)"
[ngClass]="[sidebarSubItem2.class]">
{{sidebarSubItem2.title | translate}}
</a>
<!-- Third level menu -->
<ul class="ml-menu-3" *ngIf="sidebarSubItem2.submenu.length > 0">
<li *ngFor="let sidebarSubItem3 of sidebarSubItem2.submenu"
[routerLinkActive]="sidebarSubItem3.submenu.length > 0 ? '' : 'active'">
<a [routerLink]="sidebarSubItem3.submenu.length > 0 ? null : [sidebarSubItem3.path]"
(click)="callToggleMenu($event,sidebarSubItem3.submenu.length)"
[ngClass]="[sidebarSubItem3.class]">
{{sidebarSubItem3.title | translate}}
</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</ng-scrollbar>
</div>
<!-- #Menu -->
</aside>
<!-- #END# Left Sidebar -->
</div>
@@ -0,0 +1 @@
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SidebarComponent } from './sidebar.component';
describe('SidebarComponent',
() => {
let component: SidebarComponent;
let fixture: ComponentFixture<SidebarComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SidebarComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(SidebarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,208 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// angular
import { Router, NavigationEnd } from '@angular/router';
import { DOCUMENT } from '@angular/common';
import { Component, DestroyRef, Inject, ElementRef, OnInit, Renderer2, HostListener, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { filter } from 'rxjs/operators';
// libs
import { NgScrollbarModule } from 'ngx-scrollbar';
import { TranslateModule } from '@ngx-translate/core';
// app
import { ROUTES } from './sidebar-items';
import { AuthService } from 'src/app/core/service/auth.service';
import { AccountCategoryService } from '../../services/account.category.service';
import { RouteInfo } from './sidebar.metadata';
import { SharedModule } from 'src/app/shared/shared.module';
@Component({
selector: 'app-sidebar',
templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.scss'],
standalone: true,
imports: [SharedModule, NgScrollbarModule, TranslateModule],
})
export class SidebarComponent implements OnInit {
sidebarItems!: RouteInfo[];
innerHeight?: number;
bodyTag!: HTMLElement;
listMaxHeight?: string;
listMaxWidth?: string;
userFullName?: string;
userImg?: string;
userType?: string;
headerHeight = 60;
currentRoute?: string;
menuIcon = 'radio_button_checked';
private readonly destroyRef = inject(DestroyRef);
constructor(
@Inject(DOCUMENT) private document: Document,
private renderer: Renderer2,
public elementRef: ElementRef,
private authService: AuthService,
private router: Router,
private accountCategoryService: AccountCategoryService,
) {
this.elementRef.nativeElement.closest('body');
this.router.events
.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => {
// close sidebar on mobile screen after menu select
this.renderer.removeClass(this.document.body, 'overlay-open');
});
}
@HostListener('window:resize', ['$event'])
windowResizecall() {
this.setMenuHeight();
this.checkStatuForResize(false);
}
@HostListener('document:mousedown', ['$event'])
onGlobalClick(event: Event): void {
if (!this.elementRef.nativeElement.contains(event.target)) {
this.renderer.removeClass(this.document.body, 'overlay-open');
}
}
callToggleMenu(event: Event, length: number) {
if (length > 0) {
const parentElement = (event.target as HTMLInputElement).closest('li');
const activeClass = parentElement?.classList.contains('active');
if (activeClass) {
this.renderer.removeClass(parentElement, 'active');
} else {
this.renderer.addClass(parentElement, 'active');
}
}
}
ngOnInit() {
if (this.authService.currentUserValue) {
this.userFullName =
this.authService.currentUserValue.firstName +
' ' +
this.authService.currentUserValue.lastName;
this.userImg = this.authService.currentUserValue.img;
this.userType = 'Admin';
this.sidebarItems = ROUTES.filter((sidebarItem) => sidebarItem);
this.accountCategoryService
.getCategories()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(categories => {
var accounts = this.sidebarItems.filter(x => x.id === 'accounts');
accounts[0].submenu = [];
for (var category of categories) {
accounts[0].submenu.push({
path: '/account-category/' + category.id,
title: category.name!,
iconType: '',
icon: '',
class: 'ml-menu',
groupTitle: false,
badge: '',
badgeClass: '',
submenu: [],
});
}
});
}
// this.sidebarItems = ROUTES.filter((sidebarItem) => sidebarItem);
this.initLeftSidebar();
this.bodyTag = this.document.body;
}
initLeftSidebar() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
// Set menu height
_this.setMenuHeight();
_this.checkStatuForResize(true);
}
setMenuHeight() {
this.innerHeight = window.innerHeight;
const height = this.innerHeight - this.headerHeight;
this.listMaxHeight = height + '';
this.listMaxWidth = '500px';
}
isOpen() {
return this.bodyTag.classList.contains('overlay-open');
}
checkStatuForResize(firstTime: boolean) {
if (window.innerWidth < 1170) {
this.renderer.addClass(this.document.body, 'ls-closed');
} else {
this.renderer.removeClass(this.document.body, 'ls-closed');
}
}
mouseHover() {
const body = this.elementRef.nativeElement.closest('body');
if (body.classList.contains('submenu-closed')) {
this.renderer.addClass(this.document.body, 'side-closed-hover');
this.renderer.removeClass(this.document.body, 'submenu-closed');
}
}
mouseOut() {
const body = this.elementRef.nativeElement.closest('body');
if (body.classList.contains('side-closed-hover')) {
this.renderer.removeClass(this.document.body, 'side-closed-hover');
this.renderer.addClass(this.document.body, 'submenu-closed');
}
}
mobileMenuSidebarOpen(event: Event, className: string) {
const hasClass = (event.target as HTMLInputElement).classList.contains(
className
);
if (hasClass) {
this.renderer.removeClass(this.document.body, className);
} else {
this.renderer.addClass(this.document.body, className);
}
}
callSidemenuCollapse() {
const hasClass = this.document.body.classList.contains('side-closed');
if (hasClass) {
this.renderer.removeClass(this.document.body, 'side-closed');
this.renderer.removeClass(this.document.body, 'submenu-closed');
this.menuIcon = 'radio_button_checked';
} else {
this.renderer.addClass(this.document.body, 'side-closed');
this.renderer.addClass(this.document.body, 'submenu-closed');
this.menuIcon = 'radio_button_unchecked';
}
const sideClosedHover =
this.document.body.classList.contains('side-closed');
if (sideClosedHover) {
this.renderer.removeClass(this.document.body, 'side-closed-hover');
}
}
logout() {
this.authService.logout()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((res) => {
if (!res.success) {
console.log('sidebar.logout');
this.router.navigate(['/authentication/signin']);
}
});
}
}
@@ -0,0 +1,13 @@
// Sidebar route metadata
export interface RouteInfo {
id?: string;
path: string;
title: string;
iconType: string;
icon: string;
class: string;
groupTitle: boolean;
badge: string;
badgeClass: string;
submenu: RouteInfo[];
}

Some files were not shown because too many files have changed in this diff Show More