carga inicial

Carga inicial
This commit is contained in:
IvanAS94
2025-12-26 17:19:37 -08:00
parent d6ba066e85
commit 9261cc3abd
2030 changed files with 127782 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
// Angular
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
// Components
import { BaseComponent } from './views/theme/base/base.component';
import { ErrorPageComponent } from './views/theme/content/error-page/error-page.component';
// Auth
import { AuthGuard } from './core/auth';
const routes: Routes = [
{path: 'auth', loadChildren: () => import('../app/views/pages/auth/auth.module').then(m => m.AuthModule)},
{
path: '',
component: BaseComponent,
canActivate: [AuthGuard],
children: [
{
path: 'inicio',
loadChildren: () => import('../app/views/pages/dashboard/dashboard.module').then(m => m.DashboardModule)
},
{
path: 'catalogos',
loadChildren: () => import('../app/views/pages/catalogos/catalogos.module').then(m => m.CatalogosModule)
},
{
path: 'servicios',
loadChildren: () => import('../app/views/pages/servicios/servicios.module').then(m => m.ServiciosModule)
},
{
path: 'agenda',
loadChildren: () => import('../app/views/pages/agenda/agenda.module').then(m => m.AgendaModule)
},
{
path: 'reportes',
loadChildren: () => import('../app/views/pages/reportes/reportes.module').then(m => m.ReportesModule)
},
{
path: 'encuesta',
loadChildren: () => import('../app/views/pages/encuesta/encuesta.module').then(m => m.EncuestaModule)
},
{
path: 'error/403',
component: ErrorPageComponent,
data: {
type: 'error-v6',
code: 403,
title: '403... Access forbidden',
desc: 'Looks like you don\'t have permission to access for requested page.<br> Please, contact administrator'
}
},
{path: 'error/:type', component: ErrorPageComponent},
{path: '', redirectTo: 'inicio', pathMatch: 'full'},
{path: '**', redirectTo: 'inicio', pathMatch: 'full'}
]
},
{path: '', redirectTo: 'inicio', pathMatch: 'full'},
{path: '**', redirectTo: 'error/403', pathMatch: 'full'},
];
@NgModule({
imports: [
RouterModule.forRoot(routes)
],
exports: [RouterModule]
})
export class AppRoutingModule {
}

View File

@@ -0,0 +1,2 @@
<kt-splash-screen *ngIf="loader"></kt-splash-screen>
<router-outlet></router-outlet>

View File

@@ -0,0 +1,4 @@
:host {
height: 100%;
margin: 0;
}

102
src/app/app.component.ts Normal file
View File

@@ -0,0 +1,102 @@
import { Subscription } from 'rxjs';
// Angular
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
// Layout
import { LayoutConfigService, SplashScreenService, TranslationService } from './core/_base/layout';
// language list
import { locale as enLang } from './core/_config/i18n/en';
import { locale as chLang } from './core/_config/i18n/ch';
import { locale as esLang } from './core/_config/i18n/es';
import { locale as jpLang } from './core/_config/i18n/jp';
import { locale as deLang } from './core/_config/i18n/de';
import { locale as frLang } from './core/_config/i18n/fr';
import {ApiService} from "./core/api/api.service";
import {MatSnackBar} from "@angular/material";
@Component({
// tslint:disable-next-line:component-selector
selector: 'body[kt-root]',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements OnInit, OnDestroy {
// Public properties
title = 'Metronic';
loader: boolean;
private unsubscribe: Subscription[] = []; // Read more: => https://brianflove.com/2016/12/11/anguar-2-unsubscribe-observables/
/**
* Component constructor
*
* @param translationService: TranslationService
* @param router: Router
* @param layoutConfigService: LayoutCongifService
* @param splashScreenService: SplashScreenService
*/
constructor(private translationService: TranslationService,
private router: Router,
private api: ApiService,
public snackBar: MatSnackBar,
private layoutConfigService: LayoutConfigService,
private splashScreenService: SplashScreenService) {
// register translations
this.translationService.loadTranslations(enLang, chLang, esLang, jpLang, deLang, frLang);
}
/**
* @ Lifecycle sequences => https://angular.io/guide/lifecycle-hooks
*/
/**
* On init
*/
ngOnInit(): void {
this.api.error.subscribe((res: any) => {
if (res.status == 401 && res.error == 'token_expired'){
this.api.login.refreshToken().subscribe(res=>{
location.reload();
})
return;
}
for (let msg of res.msg) {
this.snackBar.open(msg, '',{
duration: 5000,
verticalPosition: 'bottom',
horizontalPosition: 'right',
panelClass: [res.status]
});
}
});
// enable/disable loader
this.loader = this.layoutConfigService.getConfig('loader.enabled');
const routerSubscription = this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
// hide splash screen
this.splashScreenService.hide();
// scroll to top on every route change
window.scrollTo(0, 0);
// to display back the body content
setTimeout(() => {
document.body.classList.add('kt-page--loaded');
}, 500);
}
});
this.unsubscribe.push(routerSubscription);
}
/**
* On Destroy
*/
ngOnDestroy() {
this.unsubscribe.forEach(sb => sb.unsubscribe());
}
}

187
src/app/app.module.ts Normal file
View File

@@ -0,0 +1,187 @@
// Angular
import {BrowserModule, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {APP_INITIALIZER, LOCALE_ID, NgModule} from '@angular/core';
import {TranslateModule} from '@ngx-translate/core';
import {HttpClientModule} from '@angular/common/http';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {
GestureConfig,
MAT_DATE_LOCALE,
MatNativeDateModule,
MatPaginatorIntl,
MatProgressSpinnerModule,
MatSelectModule
} from '@angular/material';
import {OverlayModule} from '@angular/cdk/overlay';
// Angular in memory
import {HttpClientInMemoryWebApiModule} from 'angular-in-memory-web-api';
// Perfect Scroll bar
import {PERFECT_SCROLLBAR_CONFIG, PerfectScrollbarConfigInterface} from 'ngx-perfect-scrollbar';
// SVG inline
import {InlineSVGModule} from 'ng-inline-svg';
// Env
import {environment} from '../environments/environment';
// Hammer JS
import 'hammerjs';
// NGX Permissions
import {NgxPermissionsModule, NgxPermissionsService} from 'ngx-permissions';
// NGRX
import {StoreModule} from '@ngrx/store';
import {EffectsModule} from '@ngrx/effects';
import {StoreRouterConnectingModule} from '@ngrx/router-store';
import {StoreDevtoolsModule} from '@ngrx/store-devtools';
// State
import {metaReducers, reducers} from './core/reducers';
// Copmponents
import {AppComponent} from './app.component';
// Modules
import {AppRoutingModule} from './app-routing.module';
import {CoreModule} from './core/core.module';
import {ThemeModule} from './views/theme/theme.module';
// Partials
import {PartialsModule} from './views/partials/partials.module';
// Layout Services
import {
DataTableService,
FakeApiService,
KtDialogService,
LayoutConfigService,
LayoutRefService,
MenuAsideService,
MenuConfigService,
MenuHorizontalService,
PageConfigService,
SplashScreenService,
SubheaderService
} from './core/_base/layout';
// Auth
import {AuthModule} from './views/pages/auth/auth.module';
import {AuthService} from './core/auth';
// CRUD
import {HttpUtilsService, LayoutUtilsService, TypesUtilsService} from './core/_base/crud';
// Config
import {LayoutConfig} from './core/_config/layout.config';
// Highlight JS
import {HIGHLIGHT_OPTIONS, HighlightLanguage} from 'ngx-highlightjs';
import * as typescript from 'highlight.js/lib/languages/typescript';
import * as scss from 'highlight.js/lib/languages/scss';
import * as xml from 'highlight.js/lib/languages/xml';
import * as json from 'highlight.js/lib/languages/json';
import {TokenStorage} from "./core/auth/_services/token-storage.service";
import {PaginatorConfig} from './paginator-config';
import {CalendarModule, DateAdapter} from 'angular-calendar';
import {adapterFactory} from 'angular-calendar/date-adapters/date-fns';
import localeEs from '@angular/common/locales/es-MX';
import {registerLocaleData} from "@angular/common";
// tslint:disable-next-line:class-name
const DEFAULT_PERFECT_SCROLLBAR_CONFIG: PerfectScrollbarConfigInterface = {
wheelSpeed: 0.5,
swipeEasing: true,
minScrollbarLength: 40,
maxScrollbarLength: 300,
};
export function initializeLayoutConfig(appConfig: LayoutConfigService) {
// initialize app by loading default demo layout config
return () => {
if (appConfig.getConfig() === null) {
appConfig.loadConfigs(new LayoutConfig().configs);
}
};
}
export function hljsLanguages(): HighlightLanguage[] {
return [
{name: 'typescript', func: typescript},
{name: 'scss', func: scss},
{name: 'xml', func: xml},
{name: 'json', func: json}
];
}
registerLocaleData(localeEs);
@NgModule({
declarations: [AppComponent],
imports: [
BrowserAnimationsModule,
BrowserModule,
AppRoutingModule,
HttpClientModule,
environment.isMockEnabled ? HttpClientInMemoryWebApiModule.forRoot(FakeApiService, {
passThruUnknownUrl: true,
dataEncapsulation: false
}) : [],
NgxPermissionsModule.forRoot(),
PartialsModule,
CoreModule,
OverlayModule,
MatNativeDateModule,
StoreModule.forRoot(reducers, {metaReducers}),
EffectsModule.forRoot([]),
StoreRouterConnectingModule.forRoot({stateKey: 'router'}),
StoreDevtoolsModule.instrument(),
AuthModule.forRoot(),
TranslateModule.forRoot(),
MatProgressSpinnerModule,
InlineSVGModule.forRoot(),
ThemeModule,
MatSelectModule,
CalendarModule.forRoot({provide: DateAdapter, useFactory: adapterFactory}),
],
providers: [
AuthService,
TokenStorage,
LayoutConfigService,
LayoutRefService,
MenuConfigService,
PageConfigService,
KtDialogService,
DataTableService,
SplashScreenService,
{
provide: PERFECT_SCROLLBAR_CONFIG,
useValue: DEFAULT_PERFECT_SCROLLBAR_CONFIG
},
{
provide: HAMMER_GESTURE_CONFIG,
useClass: GestureConfig
},
{
// layout config initializer
provide: APP_INITIALIZER,
useFactory: (initializeLayoutConfig, ps: NgxPermissionsService) => function () {
return ps.loadPermissions([])
},
deps: [LayoutConfigService, NgxPermissionsService],
multi: true
},
{
// layout config initializer
provide: APP_INITIALIZER,
useFactory: (initializeLayoutConfig),
deps: [LayoutConfigService, NgxPermissionsService],
multi: true
},
{
provide: HIGHLIGHT_OPTIONS,
useValue: {languages: hljsLanguages}
},
// template services
SubheaderService,
MenuHorizontalService,
MenuAsideService,
HttpUtilsService,
TypesUtilsService,
LayoutUtilsService,
{provide: MatPaginatorIntl, useValue: PaginatorConfig()},
{provide: MAT_DATE_LOCALE, useValue: 'es-MX'},
{provide: LOCALE_ID, useValue: 'es-MX'}
],
bootstrap: [AppComponent]
})
export class AppModule {
}

View File

@@ -0,0 +1,11 @@
// Models
export { BaseModel } from './models/_base.model';
export { BaseDataSource } from './models/_base.datasource';
export { QueryParamsModel } from './models/query-models/query-params.model';
export { QueryResultsModel } from './models/query-models/query-results.model';
export { HttpExtenstionsModel } from './models/http-extentsions-model';
// Utils
export { HttpUtilsService } from './utils/http-utils.service';
export { TypesUtilsService } from './utils/types-utils.service';
export { InterceptService } from './utils/intercept.service';
export { LayoutUtilsService, MessageType } from './utils/layout-utils.service';

View File

@@ -0,0 +1,67 @@
// Angular
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
// RxJS
import { Observable, BehaviorSubject, combineLatest, Subscription, of } from 'rxjs';
// CRUD
import { HttpExtenstionsModel } from './http-extentsions-model';
import { QueryParamsModel } from './query-models/query-params.model';
import { QueryResultsModel } from './query-models/query-results.model';
import { BaseModel } from './_base.model';
import { skip, distinctUntilChanged } from 'rxjs/operators';
// Why not use MatTableDataSource?
/* In this example, we will not be using the built-in MatTableDataSource because its designed for filtering,
sorting and pagination of a client - side data array.
Read the article: 'https://blog.angular-university.io/angular-material-data-table/'
**/
export class BaseDataSource implements DataSource<BaseModel> {
entitySubject = new BehaviorSubject<any[]>([]);
hasItems = true; // Need to show message: 'No records found'
// Loading | Progress bar
loading$: Observable<boolean>;
isPreloadTextViewed$: Observable<boolean> = of(true);
// Paginator | Paginators count
paginatorTotalSubject = new BehaviorSubject<number>(0);
paginatorTotal$: Observable<number>;
subscriptions: Subscription[] = [];
constructor() {
this.paginatorTotal$ = this.paginatorTotalSubject.asObservable();
// subscribe hasItems to (entitySubject.length==0)
const hasItemsSubscription = this.paginatorTotal$.pipe(
distinctUntilChanged(),
skip(1)
).subscribe(res => this.hasItems = res > 0);
this.subscriptions.push(hasItemsSubscription);
}
connect(collectionViewer: CollectionViewer): Observable<any[]> {
// Connecting data source
return this.entitySubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
// Disonnecting data source
this.entitySubject.complete();
this.paginatorTotalSubject.complete();
this.subscriptions.forEach(sb => sb.unsubscribe());
}
baseFilter(_entities: any[], _queryParams: QueryParamsModel, _filtrationFields: string[] = []): QueryResultsModel {
const httpExtention = new HttpExtenstionsModel();
return httpExtention.baseFilter(_entities, _queryParams, _filtrationFields);
}
sortArray(_incomingArray: any[], _sortField: string = '', _sortOrder: string = 'asc'): any[] {
const httpExtention = new HttpExtenstionsModel();
return httpExtention.sortArray(_incomingArray, _sortField, _sortOrder);
}
searchInArray(_incomingArray: any[], _queryObj: any, _filtrationFields: string[] = []): any[] {
const httpExtention = new HttpExtenstionsModel();
return httpExtention.searchInArray(_incomingArray, _queryObj, _filtrationFields);
}
}

View File

@@ -0,0 +1,8 @@
export class BaseModel {
// Edit
_isEditMode = false;
// Log
_userId = 0; // Admin
_createdDate: string;
_updatedDate: string;
}

View File

@@ -0,0 +1,124 @@
// CRUD
import { QueryParamsModel } from './query-models/query-params.model';
import { QueryResultsModel } from './query-models/query-results.model';
export class HttpExtenstionsModel {
/**
* Filtration with sorting
* First do Sort then filter
*
* @param _entities: any[]
* @param _queryParams: QueryParamsModel
* @param _filtrationFields: string[]
*/
baseFilter(_entities: any[], _queryParams: QueryParamsModel, _filtrationFields: string[] = []): QueryResultsModel {
// Filtration
let entitiesResult = this.searchInArray(_entities, _queryParams.filter, _filtrationFields);
// Sorting
// start
if (_queryParams.sortField) {
entitiesResult = this.sortArray(entitiesResult, _queryParams.sortField, _queryParams.sortOrder);
}
// end
// Paginator
// start
const totalCount = entitiesResult.length;
const initialPos = _queryParams.pageNumber * _queryParams.pageSize;
entitiesResult = entitiesResult.slice(initialPos, initialPos + _queryParams.pageSize);
// end
const queryResults = new QueryResultsModel();
queryResults.items = entitiesResult;
queryResults.totalCount = totalCount;
return queryResults;
}
/**
* Sort array by field name and order-type
* @param _incomingArray: any[]
* @param _sortField: string
* @param _sortOrder: string
*/
sortArray(_incomingArray: any[], _sortField: string = '', _sortOrder: string = 'asc'): any[] {
if (!_sortField) {
return _incomingArray;
}
let result: any[] = [];
result = _incomingArray.sort((a, b) => {
if (a[_sortField] < b[_sortField]) {
return _sortOrder === 'asc' ? -1 : 1;
}
if (a[_sortField] > b[_sortField]) {
return _sortOrder === 'asc' ? 1 : -1;
}
return 0;
});
return result;
}
/**
* Filter array by some fields
*
* @param _incomingArray: any[]
* @param _queryObj: any
* @param _filtrationFields: string[]
*/
searchInArray(_incomingArray: any[], _queryObj: any, _filtrationFields: string[] = []): any[] {
const result: any[] = [];
let resultBuffer: any[] = [];
const indexes: number[] = [];
let firstIndexes: number[] = [];
let doSearch = false;
_filtrationFields.forEach(item => {
if (item in _queryObj) {
_incomingArray.forEach((element, index) => {
if (element[item] === _queryObj[item]) {
firstIndexes.push(index);
}
});
firstIndexes.forEach(element => {
resultBuffer.push(_incomingArray[element]);
});
_incomingArray = resultBuffer.slice(0);
resultBuffer = [].slice(0);
firstIndexes = [].slice(0);
}
});
Object.keys(_queryObj).forEach(key => {
const searchText = _queryObj[key].toString().trim().toLowerCase();
if (key && !_filtrationFields.some(e => e === key) && searchText) {
doSearch = true;
try {
_incomingArray.forEach((element, index) => {
if (element[key]) {
const _val = element[key].toString().trim().toLowerCase();
if (_val.indexOf(searchText) > -1 && indexes.indexOf(index) === -1) {
indexes.push(index);
}
}
});
} catch (ex) {
console.log(ex, key, searchText);
}
}
});
if (!doSearch) {
return _incomingArray;
}
indexes.forEach(re => {
result.push(_incomingArray[re]);
});
return result;
}
}

View File

@@ -0,0 +1,21 @@
export class QueryParamsModel {
// fields
filter: any;
sortOrder: string; // asc || desc
sortField: string;
pageNumber: number;
pageSize: number;
// constructor overrides
constructor(_filter: any,
_sortOrder: string = 'asc',
_sortField: string = '',
_pageNumber: number = 0,
_pageSize: number = 10) {
this.filter = _filter;
this.sortOrder = _sortOrder;
this.sortField = _sortField;
this.pageNumber = _pageNumber;
this.pageSize = _pageSize;
}
}

View File

@@ -0,0 +1,11 @@
export class QueryResultsModel {
// fields
items: any[];
totalCount: number;
errorMessage: string;
constructor(_items: any[] = [], _totalCount: number = 0, _errorMessage: string = '') {
this.items = _items;
this.totalCount = _totalCount;
}
}

View File

@@ -0,0 +1,49 @@
// Angular
import { Injectable } from '@angular/core';
import { HttpParams, HttpHeaders } from '@angular/common/http';
// CRUD
import { QueryResultsModel } from '../models/query-models/query-results.model';
import { QueryParamsModel } from '../models/query-models/query-params.model';
import { HttpExtenstionsModel } from '../../crud/models/http-extentsions-model';
@Injectable()
export class HttpUtilsService {
/**
* Prepare query http params
* @param queryParams: QueryParamsModel
*/
getFindHTTPParams(queryParams): HttpParams {
const params = new HttpParams()
.set('lastNamefilter', queryParams.filter)
.set('sortOrder', queryParams.sortOrder)
.set('sortField', queryParams.sortField)
.set('pageNumber', queryParams.pageNumber.toString())
.set('pageSize', queryParams.pageSize.toString());
return params;
}
/**
* get standard content-type
*/
getHTTPHeaders(): HttpHeaders {
const result = new HttpHeaders();
result.set('Content-Type', 'application/json');
return result;
}
baseFilter(_entities: any[], _queryParams: QueryParamsModel, _filtrationFields: string[] = []): QueryResultsModel {
const httpExtention = new HttpExtenstionsModel();
return httpExtention.baseFilter(_entities, _queryParams, _filtrationFields);
}
sortArray(_incomingArray: any[], _sortField: string = '', _sortOrder: string = 'asc'): any[] {
const httpExtention = new HttpExtenstionsModel();
return httpExtention.sortArray(_incomingArray, _sortField, _sortOrder);
}
searchInArray(_incomingArray: any[], _queryObj: any, _filtrationFields: string[] = []): any[] {
const httpExtention = new HttpExtenstionsModel();
return httpExtention.searchInArray(_incomingArray, _queryObj, _filtrationFields);
}
}

View File

@@ -0,0 +1,51 @@
// Angular
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http';
// RxJS
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { debug } from 'util';
/**
* More information there => https://medium.com/@MetonymyQT/angular-http-interceptors-what-are-they-and-how-to-use-them-52e060321088
*/
@Injectable()
export class InterceptService implements HttpInterceptor {
// intercept request and add token
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
// tslint:disable-next-line:no-debugger
// modify request
// request = request.clone({
// setHeaders: {
// Authorization: `Bearer ${localStorage.getItem('accessToken')}`
// }
// });
// console.log('----request----');
// console.log(request);
// console.log('--- end of request---');
return next.handle(request).pipe(
tap(
event => {
if (event instanceof HttpResponse) {
// console.log('all looks good');
// http response status code
// console.log(event.status);
}
},
error => {
// http response status code
// console.log('----response----');
// console.error('status code:');
// tslint:disable-next-line:no-debugger
console.error(error.status);
console.error(error.message);
// console.log('--- end of response---');
}
)
);
}
}

View File

@@ -0,0 +1,122 @@
// Angular
import { Injectable } from '@angular/core';
import { MatSnackBar, MatDialog } from '@angular/material';
// Partials for CRUD
import { ActionNotificationComponent,
DeleteEntityDialogComponent,
FetchEntityDialogComponent,
UpdateStatusDialogComponent
} from '../../../../views/partials/content/crud';
export enum MessageType {
Create,
Read,
Update,
Delete
}
@Injectable()
export class LayoutUtilsService {
/**
* Service constructor
*
* @param snackBar: MatSnackBar
* @param dialog: MatDialog
*/
constructor(private snackBar: MatSnackBar,
private dialog: MatDialog) { }
/**
* Showing (Mat-Snackbar) Notification
*
* @param _message
* @param _type
* @param _duration
* @param _showCloseButton
* @param _showUndoButton
* @param _undoButtonDuration
* @param _verticalPosition
*/
showActionNotification(
_message: string,
_type: MessageType = MessageType.Create,
_duration: number = 10000,
_showCloseButton: boolean = true,
_showUndoButton: boolean = false,
_undoButtonDuration: number = 3000,
_verticalPosition: 'top' | 'bottom' = 'bottom'
) {
const _data = {
message: _message,
snackBar: this.snackBar,
showCloseButton: _showCloseButton,
showUndoButton: _showUndoButton,
undoButtonDuration: _undoButtonDuration,
verticalPosition: _verticalPosition,
type: _type,
action: 'Undo'
};
return this.snackBar.openFromComponent(ActionNotificationComponent, {
duration: _duration,
data: _data,
verticalPosition: _verticalPosition
});
}
/**
* Showing Confirmation (Mat-Dialog) before Entity Removing
*
* @param title: stirng
* @param description: stirng
* @param waitDesciption: string
* @param btnText
*/
deleteElement(title: string = '', description: string = '', waitDesciption: string = '', btnText: string = '') {
return this.dialog.open(DeleteEntityDialogComponent, {
data: { title, description, waitDesciption, btnText
},
width: '440px',
});
}
/**
* Showing Confirmation (Mat-Dialog) before Entity Removing
*
* @param title: stirng
* @param description: stirng
* @param waitDesciption: string
* @param btnText
*/
customElement(title: string = '', description: string = '', waitDesciption: string = '', btnText: string = 'Aceptar') {
return this.dialog.open(DeleteEntityDialogComponent, {
data: { title, description, waitDesciption, btnText },
width: '440px'
});
}
/**
* Showing Fetching Window(Mat-Dialog)
*
* @param _data: any
*/
fetchElements(_data) {
return this.dialog.open(FetchEntityDialogComponent, {
data: _data,
width: '400px'
});
}
/**
* Showing Update Status for Entites Window
*
* @param title: string
* @param statuses: string[]
* @param messages: string[]
*/
updateStatusForEntities(title, statuses, messages) {
return this.dialog.open(UpdateStatusDialogComponent, {
data: { title, statuses, messages },
width: '480px'
});
}
}

View File

@@ -0,0 +1,129 @@
/** Angular */
import { Injectable } from '@angular/core';
@Injectable()
export class TypesUtilsService {
/**
* Convert number to string and addinng '0' before
*
* @param value: number
*/
padNumber(value: number) {
if (this.isNumber(value)) {
return `0${value}`.slice(-2);
} else {
return '';
}
}
/**
* Checking value type equals to Number
*
* @param value: any
*/
isNumber(value: any): boolean {
return !isNaN(this.toInteger(value));
}
/**
* Covert value to number
*
* @param value: any
*/
toInteger(value: any): number {
return parseInt(`${value}`, 10);
}
/**
* Convert date to string with 'MM/dd/yyyy' format
*
* @param date: Date
*/
dateFormat(date: Date): string {
const month = date.getMonth() + 1;
const day = date.getDate();
const year = date.getFullYear();
if (date) {
return `${month}/${day}/${year}`;
}
return '';
}
/**
* Convert Date to string with custom format 'MM/dd/yyyy'
*
* @param date: any
*/
dateCustomFormat(date: any): string {
let stringDate = '';
if (date) {
stringDate += this.isNumber(date.month) ? this.padNumber(date.month) + '/' : '';
stringDate += this.isNumber(date.day) ? this.padNumber(date.day) + '/' : '';
stringDate += date.year;
}
return stringDate;
}
/**
* Convert string to DateFormatter (For Reactive Forms Validators)
*
* @param dateInStr: string (format => 'MM/dd/yyyy')
*/
getDateFormatterFromString(dateInStr: string): any {
if (dateInStr && dateInStr.length > 0) {
const dateParts = dateInStr.trim().split('/');
return [
{
year: this.toInteger(dateParts[2]),
month: this.toInteger(dateParts[0]),
day: this.toInteger(dateParts[1])
}
];
}
const _date = new Date();
return [
{
year: _date.getFullYear(),
month: _date.getMonth() + 1,
day: _date.getDay()
}
];
}
/**
* Convert string to Date
*
* @param dateInStr: string (format => 'MM/dd/yyyy')
*/
getDateFromString(dateInStr: string = ''): Date {
if (dateInStr && dateInStr.length > 0) {
const dateParts = dateInStr.trim().split('/');
const year = this.toInteger(dateParts[2]);
const month = this.toInteger(dateParts[0]);
const day = this.toInteger(dateParts[1]);
// tslint:disable-next-line:prefer-const
let result = new Date();
result.setDate(day);
result.setMonth(month - 1);
result.setFullYear(year);
return result;
}
return new Date();
}
/**
* Convert Date to string with format 'MM/dd/yyyy'
* @param _date: Date?
*/
getDateStringFromDate(_date: Date = new Date()): string {
const month = _date.getMonth() + 1;
const year = _date.getFullYear();
const date = _date.getDate();
return `${month}/${date}/${year}`;
}
}

View File

@@ -0,0 +1,80 @@
// Angular
import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core';
import { animate, AnimationBuilder, AnimationPlayer, style } from '@angular/animations';
import { NavigationEnd, Router } from '@angular/router';
// RxJS
import { Subscription } from 'rxjs';
/**
* Page load animation
*
*/
@Directive({
selector: '[ktContentAnimate]'
})
export class ContentAnimateDirective implements OnInit, OnDestroy {
// Public properties
player: AnimationPlayer;
// Private properties
private events: Subscription;
/**
* Directive Consturctor
* @param el: ElementRef
* @param router: Router
* @param animationBuilder: AnimationBuilder
*/
constructor(
private el: ElementRef,
private router: Router,
private animationBuilder: AnimationBuilder) {
}
/**
* @ Lifecycle sequences => https://angular.io/guide/lifecycle-hooks
*/
/**
* On init
*/
ngOnInit(): void {
// animate the content
this.initAnimate();
// animate page load
this.events = this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.player.play();
}
});
}
/**
* On destroy
*/
ngOnDestroy(): void {
this.events.unsubscribe();
this.player.destroy();
}
/**
* Animate page load
*/
initAnimate() {
this.player = this.animationBuilder
.build([
// style({opacity: 0, transform: 'translateY(15px)'}),
// animate(500, style({opacity: 1, transform: 'translateY(0)'})),
// style({transform: 'none'}),
style({
transform: 'translateY(-3%)',
opacity: 0,
position: 'static'
}),
animate(
'0.5s ease-in-out',
style({transform: 'translateY(0%)', opacity: 1})
)
])
.create(this.el.nativeElement);
}
}

View File

@@ -0,0 +1,68 @@
// Angular
import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core';
// ObjectPath
import * as objectPath from 'object-path';
export interface HeaderOptions {
classic?: any;
offset?: any;
minimize?: any;
}
/**
* Configure Header
*/
@Directive({
selector: '[ktHeader]',
exportAs: 'ktHeader',
})
export class HeaderDirective implements AfterViewInit {
@Input() options: HeaderOptions = {};
/**
* Directive Constructor
* @param el: ElementRef
*/
constructor(private el: ElementRef) {
}
/**
* @ Lifecycle sequences => https://angular.io/guide/lifecycle-hooks
*/
/**
* After view init
*/
ngAfterViewInit(): void {
this.setupOptions();
const header = new KTHeader(this.el.nativeElement, this.options);
}
/**
* Setup options to header
*/
private setupOptions() {
this.options = {
classic: {
desktop: true,
mobile: false
},
};
if (this.el.nativeElement.getAttribute('data-ktheader-minimize') == '1') {
objectPath.set(this.options, 'minimize', {
desktop: {
on: 'kt-header--minimize'
},
mobile: {
on: 'kt-header--minimize'
}
});
objectPath.set(this.options, 'offset', {
desktop: 200,
mobile: 150
});
}
}
}

View File

@@ -0,0 +1,66 @@
// Angular
import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core';
// Object-Path
import * as objectPath from 'object-path';
export interface MenuOptions {
scroll?: any;
submenu?: any;
accordion?: any;
dropdown?: any;
}
/**
* Configure menu
*/
@Directive({
selector: '[ktMenu]',
exportAs: 'ktMenu',
})
export class MenuDirective implements AfterViewInit {
// Public propeties
@Input() options: MenuOptions;
// Private properites
private menu: any;
/**
* Directive Constructor
* @param el: ElementRef
*/
constructor(private el: ElementRef) {
}
/**
* @ Lifecycle sequences => https://angular.io/guide/lifecycle-hooks
*/
/**
* After view init
*/
ngAfterViewInit(): void {
this.setupOptions();
this.menu = new KTMenu(this.el.nativeElement, this.options);
}
/**
* Return the menu
*/
getMenu() {
return this.menu;
}
/**
* Setup menu options
*/
private setupOptions() {
// init aside menu
let menuDesktopMode = 'accordion';
if (this.el.nativeElement.getAttribute('data-ktmenu-dropdown') === '1') {
menuDesktopMode = 'dropdown';
}
if (typeof objectPath.get(this.options, 'submenu.desktop') === 'object') {
objectPath.set(this.options, 'submenu.desktop', menuDesktopMode);
}
}
}

View File

@@ -0,0 +1,49 @@
// Angular
import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core';
export interface OffcanvasOptions {
baseClass: string;
overlay?: boolean;
closeBy: string;
toggleBy?: any;
}
/**
* Setup off Convas
*/
@Directive({
selector: '[ktOffcanvas]',
exportAs: 'ktOffcanvas',
})
export class OffcanvasDirective implements AfterViewInit {
// Public properties
@Input() options: OffcanvasOptions;
// Private properties
private offcanvas: any;
/**
* Directive Constructor
* @param el: ElementRef
*/
constructor(private el: ElementRef) { }
/**
* @ Lifecycle sequences => https://angular.io/guide/lifecycle-hooks
*/
/**
* After view init
*/
ngAfterViewInit(): void {
setTimeout(() => {
this.offcanvas = new KTOffcanvas(this.el.nativeElement, this.options);
});
}
/**
* Returns the offCanvas
*/
getOffcanvas() {
return this.offcanvas;
}
}

View File

@@ -0,0 +1,44 @@
// Angular
import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core';
export interface ScrollTopOptions {
offset: number;
speed: number;
}
/**
* Scroll to top
*/
@Directive({
selector: '[ktScrollTop]'
})
export class ScrollTopDirective implements AfterViewInit {
// Public properties
@Input() options: ScrollTopOptions;
// Private properites
private scrollTop: any;
/**
* Directive Conctructor
* @param el: ElementRef
*/
constructor(private el: ElementRef) { }
/**
* @ Lifecycle sequences => https://angular.io/guide/lifecycle-hooks
*/
/**
* After view init
*/
ngAfterViewInit(): void {
this.scrollTop = new KTScrolltop(this.el.nativeElement, this.options);
}
/**
* Returns ScrollTop
*/
getScrollTop() {
return this.scrollTop;
}
}

View File

@@ -0,0 +1,161 @@
// Angular
import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core';
// Chart
import { Chart } from 'chart.js/dist/Chart.min.js';
// LayoutConfig
import { LayoutConfigService } from '../../layout/services/layout-config.service';
export interface SparklineChartOptions {
// array of numbers
data: number[];
// chart line color
color: string;
// chart line size
border: number;
fill?: boolean;
tooltip?: boolean;
}
/**
* Configure sparkline chart
*/
@Directive({
selector: '[ktSparklineChart]',
exportAs: 'ktSparklineChart'
})
export class SparklineChartDirective implements AfterViewInit {
// Public properties
@Input() options: SparklineChartOptions;
// Private properties
private chart: Chart;
/**
* Directive Constructor
*
* @param el: ElementRef
* @param layoutConfigService: LayoutConfigService
*/
constructor(private el: ElementRef, private layoutConfigService: LayoutConfigService) {
}
/**
* @ Lifecycle sequences => https://angular.io/guide/lifecycle-hooks
*/
/**
* After view init
*/
ngAfterViewInit(): void {
this.initChart(this.el.nativeElement, this.options.data, this.options.color, this.options.border, this.options.fill, this.options.tooltip);
}
/**
* Init chart
* @param src: any
* @param data: any
* @param color: any
* @param border: any
* @param fill: any
* @param tooltip: any
*/
initChart(src, data, color, border, fill, tooltip) {
if (src.length === 0) {
return;
}
// set default values
fill = (typeof fill !== 'undefined') ? fill : false;
tooltip = (typeof tooltip !== 'undefined') ? tooltip : false;
const config = {
type: 'line',
data: {
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October'],
datasets: [{
label: '',
borderColor: color,
borderWidth: border,
pointHoverRadius: 4,
pointHoverBorderWidth: 12,
pointBackgroundColor: Chart.helpers.color('#000000').alpha(0).rgbString(),
pointBorderColor: Chart.helpers.color('#000000').alpha(0).rgbString(),
pointHoverBackgroundColor: this.layoutConfigService.getConfig('colors.state.danger'),
pointHoverBorderColor: Chart.helpers.color('#000000').alpha(0.1).rgbString(),
fill: false,
data,
}]
},
options: {
title: {
display: false,
},
tooltips: {
enabled: false,
intersect: false,
mode: 'nearest',
xPadding: 10,
yPadding: 10,
caretPadding: 10
},
legend: {
display: false,
labels: {
usePointStyle: false
}
},
responsive: true,
maintainAspectRatio: true,
hover: {
mode: 'index'
},
scales: {
xAxes: [{
display: false,
gridLines: false,
scaleLabel: {
display: true,
labelString: 'Month'
}
}],
yAxes: [{
display: false,
gridLines: false,
scaleLabel: {
display: true,
labelString: 'Value'
},
ticks: {
beginAtZero: true
}
}]
},
elements: {
point: {
radius: 4,
borderWidth: 12
},
},
layout: {
padding: {
left: 0,
right: 10,
top: 5,
bottom: 0
}
}
}
};
this.chart = new Chart(src, config);
}
/**
* Returns the chart
*/
getChart() {
return this.chart;
}
}

View File

@@ -0,0 +1,316 @@
import { AfterViewInit, Directive, ElementRef, HostBinding, HostListener, Inject, Input, isDevMode, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { animationFrame } from 'rxjs/internal/scheduler/animationFrame';
import { filter, map, share, startWith, takeUntil, throttleTime } from 'rxjs/operators';
/**
* Extended version of "Sticky Directive for Angular 2+"
* https://github.com/w11k/angular-sticky-things
*/
export interface StickyPositions {
offsetY: number;
bottomBoundary: number | null;
}
export interface StickyStatus {
isSticky: boolean;
reachedLowerEdge: boolean;
marginTop: number;
marginBottom: number;
}
@Directive({
selector: '[ktSticky]'
})
export class StickyDirective implements OnInit, AfterViewInit, OnDestroy {
filterGate = false;
marginTop$ = new BehaviorSubject(0);
marginBottom$ = new BehaviorSubject(0);
enable$ = new BehaviorSubject(true);
@Input() scrollContainer: string | HTMLElement;
@Input('spacerElement') spacerElement: HTMLElement | undefined;
@Input('boundaryElement') boundaryElement: HTMLElement | undefined;
@HostBinding('class.is-sticky') sticky = false;
@HostBinding('class.boundary-reached') boundaryReached = false;
/**
* The field represents some position values in normal (not sticky) mode.
* If the browser size or the content of the page changes, this value must be recalculated.
*/
private scroll$ = new Subject<number>();
private scrollThrottled$: Observable<number>;
private resize$ = new Subject<void>();
private resizeThrottled$: Observable<void>;
private extraordinaryChange$ = new BehaviorSubject<void>(undefined);
private status$: Observable<StickyStatus>;
private componentDestroyed = new Subject<void>();
constructor(private stickyElement: ElementRef, @Inject(PLATFORM_ID) private platformId: string) {
/** Throttle the scroll to animation frame (around 16.67ms) */
this.scrollThrottled$ = this.scroll$
.pipe(
throttleTime(0, animationFrame),
share()
);
/** Throttle the resize to animation frame (around 16.67ms) */
this.resizeThrottled$ = this.resize$
.pipe(
throttleTime(0, animationFrame),
// emit once since we are currently using combineLatest
startWith(null),
share()
);
this.status$ = combineLatest(
this.enable$,
this.scrollThrottled$,
this.marginTop$,
this.marginBottom$,
this.extraordinaryChange$,
this.resizeThrottled$,
)
.pipe(
filter(([enabled]) => this.checkEnabled(enabled)),
map(([enabled, pageYOffset, marginTop, marginBottom]) => this.determineStatus(this.determineElementOffsets(), pageYOffset, marginTop, marginBottom, enabled)),
share(),
);
}
@Input() set marginTop(value: number) {
this.marginTop$.next(value);
}
@Input() set marginBottom(value: number) {
this.marginBottom$.next(value);
}
@Input() set enable(value: boolean) {
this.enable$.next(value);
}
ngAfterViewInit(): void {
this.status$
.pipe(takeUntil(this.componentDestroyed))
.subscribe((status) => this.setSticky(status));
}
public recalculate(): void {
if (isPlatformBrowser(this.platformId)) {
// Make sure to be in the next tick by using timeout
setTimeout(() => {
this.extraordinaryChange$.next(undefined);
}, 0);
}
}
/**
* This is nasty code that should be refactored at some point.
*
* The Problem is, we filter for enabled. So that the code doesn't run
* if @Input enabled = false. But if the user disables, we need exactly 1
* emit in order to reset and call removeSticky. So this method basically
* turns the filter in "filter, but let the first pass".
*/
checkEnabled(enabled: boolean): boolean {
if (!isPlatformBrowser(this.platformId)) {
return false;
}
if (enabled) {
// reset the gate
this.filterGate = false;
return true;
} else {
if (this.filterGate) {
// gate closed, first emit has happened
return false;
} else {
// this is the first emit for enabled = false,
// let it pass, and activate the gate
// so the next wont pass.
this.filterGate = true;
return true;
}
}
}
@HostListener('window:resize', [])
onWindowResize(): void {
if (isPlatformBrowser(this.platformId)) {
this.resize$.next();
}
}
setupListener(): void {
if (isPlatformBrowser(this.platformId)) {
const target = this.getScrollTarget();
target.addEventListener('scroll', this.listener);
}
}
removeListener() {
if (isPlatformBrowser(this.platformId)) {
const target = this.getScrollTarget();
target.removeEventListener('scroll', this.listener);
}
}
listener = (e: Event) => {
const upperScreenEdgeAt = (e.target as HTMLElement).scrollTop || window.pageYOffset;
this.scroll$.next(upperScreenEdgeAt);
}
ngOnInit(): void {
// this.checkSetup();
this.setupListener();
}
ngOnDestroy(): void {
this.componentDestroyed.next();
this.removeListener();
}
getComputedStyle(el: HTMLElement): ClientRect | DOMRect {
return el.getBoundingClientRect();
}
private getScrollTarget(): Element | Window {
let target: Element | Window;
if (this.scrollContainer && typeof this.scrollContainer === 'string') {
target = document.querySelector(this.scrollContainer);
} else if (this.scrollContainer && this.scrollContainer instanceof HTMLElement) {
target = this.scrollContainer;
} else {
target = window;
}
return target;
}
private determineStatus(originalVals: StickyPositions, pageYOffset: number, marginTop: number, marginBottom: number, enabled: boolean): StickyStatus {
const stickyElementHeight = this.getComputedStyle(this.stickyElement.nativeElement).height;
const reachedLowerEdge = this.boundaryElement && window.pageYOffset + stickyElementHeight + marginBottom >= (originalVals.bottomBoundary - marginTop);
return {
isSticky: enabled && pageYOffset > originalVals.offsetY,
reachedLowerEdge,
marginBottom,
marginTop,
};
}
/**
* Gets the offset for element. If the element
* currently is sticky, it will get removed
* to access the original position. Other
* wise this would just be 0 for fixed elements.
*/
private determineElementOffsets(): StickyPositions {
if (this.sticky) {
this.removeSticky();
}
let bottomBoundary: number | null = null;
if (this.boundaryElement) {
const boundaryElementHeight = this.getComputedStyle(this.boundaryElement).height;
const boundaryElementOffset = getPosition(this.boundaryElement).y;
bottomBoundary = boundaryElementHeight + boundaryElementOffset;
}
return {offsetY: (getPosition(this.stickyElement.nativeElement).y - this.marginTop$.value), bottomBoundary};
}
private makeSticky(boundaryReached: boolean = false, marginTop: number, marginBottom: number): void {
this.boundaryReached = boundaryReached;
// do this before setting it to pos:fixed
const {width, height, left} = this.getComputedStyle(this.stickyElement.nativeElement);
const offSet = boundaryReached ? (this.getComputedStyle(this.boundaryElement).bottom - height - this.marginBottom$.value) : this.marginTop$.value;
this.sticky = true;
this.stickyElement.nativeElement.style.position = 'sticky';
this.stickyElement.nativeElement.style.backgroundColor = '#fff';
this.stickyElement.nativeElement.style.top = this.marginTop$.value + 'px';
// this.stickyElement.nativeElement.style.left = left + 'px';
this.stickyElement.nativeElement.style.width = `${width}px`;
this.stickyElement.nativeElement.style.zIndex = `2`;
if (this.spacerElement) {
const spacerHeight = marginBottom + height + marginTop;
this.spacerElement.style.height = `${spacerHeight}px`;
}
}
private checkSetup() {
if (isDevMode() && !this.spacerElement) {
console.warn(`******There might be an issue with your sticky directive!******
You haven't specified a spacer element. This will cause the page to jump.
Best practise is to provide a spacer element (e.g. a div) right before/after the sticky element.
Then pass the spacer element as input:
<div #spacer></div>
<div stickyThing="" [spacer]="spacer">
I am sticky!
</div>`);
}
}
private setSticky(status: StickyStatus): void {
if (status.isSticky) {
this.makeSticky(status.reachedLowerEdge, status.marginTop, status.marginBottom);
} else {
this.removeSticky();
}
}
private removeSticky(): void {
this.boundaryReached = false;
this.sticky = false;
this.stickyElement.nativeElement.style.position = '';
this.stickyElement.nativeElement.style.width = 'auto';
this.stickyElement.nativeElement.style.left = 'auto';
this.stickyElement.nativeElement.style.top = 'auto';
if (this.spacerElement) {
this.spacerElement.style.height = '0';
}
}
}
// Thanks to https://stanko.github.io/javascript-get-element-offset/
function getPosition(el) {
let top = 0;
let left = 0;
let element = el;
// Loop through the DOM tree
// and add it's parent's offset to get page offset
do {
top += element.offsetTop || 0;
left += element.offsetLeft || 0;
element = element.offsetParent;
} while (element);
return {
y: top,
x: left,
};
}

View File

@@ -0,0 +1,35 @@
import { Directive, ElementRef, HostListener, Renderer2 } from '@angular/core';
/**
* Listen Tab click
*/
@Directive({
selector: '[ktTabClickEvent]'
})
export class TabClickEventDirective {
/**
* Directive Constructor
* @param el: ElementRef
* @param render: Renderer2
*/
constructor(private el: ElementRef, private render: Renderer2) { }
/**
* A directive handler the tab click event for active tab
* @param target
*/
@HostListener('click', ['$event.target'])
onClick(target: HTMLElement) {
// remove previous active tab
const parent = target.closest('[role="tablist"]');
const activeLink = parent.querySelector('[role="tab"].active');
if (activeLink) {
this.render.removeClass(activeLink, 'active');
}
// set active tab
const link = target.closest('[role="tab"]');
if (link) {
this.render.addClass(link, 'active');
}
}
}

View File

@@ -0,0 +1,38 @@
// Angular
import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core';
export interface ToggleOptions {
target?: string;
targetState?: string;
togglerState?: string;
}
/**
* Toggle
*/
@Directive({
selector: '[ktToggle]',
exportAs: 'ktToggle'
})
export class ToggleDirective implements AfterViewInit {
// Public properties
@Input() options: ToggleOptions;
toggle: any;
/**
* Directive constructor
* @param el: ElementRef
*/
constructor(private el: ElementRef) { }
/**
* @ Lifecycle sequences => https://angular.io/guide/lifecycle-hooks
*/
/**
* After view init
*/
ngAfterViewInit(): void {
this.toggle = new KTToggle(this.el.nativeElement, this.options);
}
}

View File

@@ -0,0 +1,45 @@
// Directives
export { SparklineChartOptions, SparklineChartDirective } from './directives/sparkline-chart.directive';
export { OffcanvasDirective } from './directives/offcanvas.directive';
export { ScrollTopDirective } from './directives/scroll-top.directive';
export { TabClickEventDirective } from './directives/tab-click-event.directive';
export { ToggleDirective } from './directives/toggle.directive';
export { ContentAnimateDirective } from './directives/content-animate.directive';
export { HeaderDirective } from './directives/header.directive';
export { MenuDirective } from './directives/menu.directive';
export { StickyDirective } from './directives/sticky.directive';
// Models
export { DataTableItemModel } from './models/datatable-item.model';
export { ExternalCodeExample } from './models/external-code-example';
export { OffcanvasOptions } from './directives/offcanvas.directive';
export { ScrollTopOptions } from './directives/scroll-top.directive';
export { ToggleOptions } from './directives/toggle.directive';
export { LayoutConfigModel } from './models/layout-config.model';
export { MenuOptions } from './directives/menu.directive';
// Pipes
export { FirstLetterPipe } from './pipes/first-letter.pipe';
export { GetObjectPipe } from './pipes/get-object.pipe';
export { JoinPipe } from './pipes/join.pipe';
export { SafePipe } from './pipes/safe.pipe';
export { TimeElapsedPipe } from './pipes/time-elapsed.pipe';
// Services
export { DataTableService } from './services/datatable.service';
export { TranslationService } from './services/translation.service';
export { LayoutConfigService } from './services/layout-config.service';
export { LayoutRefService } from './services/layout-ref.service';
export { MenuAsideService } from './services/menu-aside.service';
export { MenuConfigService } from './services/menu-config.service';
export { MenuHorizontalService } from './services/menu-horizontal.service';
export { PageConfigService } from './services/page-config.service';
export { SplashScreenService } from './services/splash-screen.service';
export { SubheaderService } from './services/subheader.service';
export { KtDialogService } from './services/kt-dialog.service';
// Server
export { FakeApiService } from './server/fake-api/fake-api.service';

View File

@@ -0,0 +1,13 @@
export class DataTableItemModel {
id: number;
cModel: string;
cManufacture: string;
cModelYear: number;
cMileage: number;
cDescription: string;
cColor: string;
cPrice: number;
cCondition: number;
cStatus: number;
cVINCode: string;
}

View File

@@ -0,0 +1,15 @@
export class ExternalCodeExample {
beforeCodeTitle: string;
beforeCodeDescription: string;
htmlCode: string;
tsCode: string;
cssCode: string;
scssCode: string;
viewCode: string;
codeTitle: string;
afterCodeTitle: string;
afterCodeDescription: string;
sourceLink: string;
isCodeVisible = true;
isExampleExpanded: boolean;
}

View File

@@ -0,0 +1,168 @@
export interface LayoutConfigModel {
demo: string;
self: {
layout?: string;
body?: {
'background-image'?: string,
'class'?: string,
'background-position'?: string,
'background-size'?: string
};
logo: any | string;
};
portlet?: {
sticky: {
offset: number
}
};
loader: {
enabled: boolean;
type?: string | 'default' | 'spinner-message' | 'spinner-logo';
logo?: string;
message?: string;
};
colors: {
state?: any;
base: {
label: Array<string>;
shape: Array<string>
}
};
width?: string;
header: {
self: {
skin?: string;
width?: string;
fixed: {
desktop: any;
mobile: boolean
}
};
// not implemented yet
topbar?: {
self?: {
width?: string;
}
search?: {
display: boolean;
layout: 'offcanvas' | 'dropdown';
dropdown?: {
style: 'light' | 'dark';
}
};
notifications?: {
display: boolean;
layout: 'offcanvas' | 'dropdown';
dropdown: {
style: 'light' | 'dark';
}
};
'quick-actions'?: {
display: boolean;
layout: 'offcanvas' | 'dropdown';
dropdown: {
style: 'light' | 'dark';
}
};
user?: {
display: boolean;
layout: 'offcanvas' | 'dropdown';
dropdown: {
style: 'light' | 'dark';
}
};
languages?: {
display: boolean
};
cart?: {
display: boolean
};
'my-cart'?: any
'quick-panel'?: {
display: boolean
}
};
search?: {
display: boolean
};
menu?: {
self: {
display: boolean;
layout?: string;
'root-arrow'?: boolean;
width?: string;
};
desktop: {
arrow: boolean;
toggle: string;
submenu: {
skin?: string;
arrow: boolean
}
};
mobile: {
submenu: {
skin: string;
accordion: boolean
}
}
}
};
brand?: {
self: {
skin: string
}
};
aside?: {
self: {
skin?: string;
display: boolean;
fixed?: boolean | any;
minimize?: {
toggle: boolean;
default: boolean
}
};
footer?: {
self: {
display: boolean
}
};
menu: {
'root-arrow'?: boolean;
dropdown: boolean;
scroll: boolean;
submenu: {
accordion: boolean;
dropdown: {
arrow: boolean;
'hover-timeout': number
}
}
}
};
'aside-secondary'?: {
self: {
display: boolean;
layout?: string;
expanded?: boolean;
}
};
subheader?: {
display: boolean;
fixed?: boolean;
width?: string;
layout?: string;
style?: 'light' | 'solid' | 'transparent';
daterangepicker?: {
display: boolean
}
};
content?: any;
footer?: {
self?: any;
};
'quick-panel'?: {
display?: boolean
};
}

View File

@@ -0,0 +1,21 @@
// Angular
import { Pipe, PipeTransform } from '@angular/core';
/**
* Returns only first letter of string
*/
@Pipe({
name: 'firstLetter'
})
export class FirstLetterPipe implements PipeTransform {
/**
* Transform
*
* @param value: any
* @param args: any
*/
transform(value: any, args?: any): any {
return value.split(' ').map(n => n[0]).join('');
}
}

View File

@@ -0,0 +1,22 @@
// Angular
import { Pipe, PipeTransform } from '@angular/core';
// Object-Path
import * as objectPath from 'object-path';
/**
* Returns object from parent object
*/
@Pipe({
name: 'getObject'
})
export class GetObjectPipe implements PipeTransform {
/**
* Transform
*
* @param value: any
* @param args: any
*/
transform(value: any, args?: any): any {
return objectPath.get(value, args);
}
}

View File

@@ -0,0 +1,23 @@
// Angular
import { Pipe, PipeTransform } from '@angular/core';
/**
* Returns string from Array
*/
@Pipe({
name: 'join'
})
export class JoinPipe implements PipeTransform {
/**
* Transform
*
* @param value: any
* @param args: any
*/
transform(value: any, args?: any): any {
if (Array.isArray(value)) {
return value.join(' ');
}
return value;
}
}

View File

@@ -0,0 +1,42 @@
// Angular
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml, SafeStyle, SafeScript, SafeUrl, SafeResourceUrl } from '@angular/platform-browser';
/**
* Sanitize HTML
*/
@Pipe({
name: 'safe'
})
export class SafePipe implements PipeTransform {
/**
* Pipe Constructor
*
* @param _sanitizer: DomSanitezer
*/
constructor(protected _sanitizer: DomSanitizer) { }
/**
* Transform
*
* @param value: string
* @param type: string
*/
transform(value: string, type: string): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl {
switch (type) {
case 'html':
return this._sanitizer.bypassSecurityTrustHtml(value);
case 'style':
return this._sanitizer.bypassSecurityTrustStyle(value);
case 'script':
return this._sanitizer.bypassSecurityTrustScript(value);
case 'url':
return this._sanitizer.bypassSecurityTrustUrl(value);
case 'resourceUrl':
return this._sanitizer.bypassSecurityTrustResourceUrl(value);
default:
return this._sanitizer.bypassSecurityTrustHtml(value);
}
}
}

View File

@@ -0,0 +1,123 @@
// Angular
import { Pipe, PipeTransform, OnDestroy, ChangeDetectorRef, NgZone } from '@angular/core';
/**
* https://github.com/AndrewPoyntz/time-ago-pipe
* An Angular pipe for converting a date string into a time ago
*/
@Pipe({
name: 'kTimeElapsed'
})
export class TimeElapsedPipe implements PipeTransform, OnDestroy {
// Private properties
private timer: number;
/**
* Pipe Constructor
*
* @param changeDetectorRef: ChangeDetectorRef
* @param ngZone: NgZone
*/
constructor(
private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone
) {}
/**
* @ Lifecycle sequences => https://angular.io/guide/lifecycle-hooks
*/
/**
* On destroy
*/
ngOnDestroy(): void {
this.removeTimer();
}
/**
* Transform
*
* @param value: string
*/
transform(value: string) {
this.removeTimer();
const d = new Date(value);
const now = new Date();
const seconds = Math.round(
Math.abs((now.getTime() - d.getTime()) / 1000)
);
const timeToUpdate = this.getSecondsUntilUpdate(seconds) * 1000;
this.timer = this.ngZone.runOutsideAngular(() => {
if (typeof window !== 'undefined') {
return window.setTimeout(() => {
this.ngZone.run(() =>
this.changeDetectorRef.markForCheck()
);
}, timeToUpdate);
}
return null;
});
const minutes = Math.round(Math.abs(seconds / 60));
const hours = Math.round(Math.abs(minutes / 60));
const days = Math.round(Math.abs(hours / 24));
const months = Math.round(Math.abs(days / 30.416));
const years = Math.round(Math.abs(days / 365));
if (seconds <= 45) {
return 'just now';
} else if (seconds <= 90) {
return '1 min';
} else if (minutes <= 45) {
return minutes + ' mins';
} else if (minutes <= 90) {
return '1 hr';
} else if (hours <= 22) {
return hours + ' hrs';
} else if (hours <= 36) {
return '1 day';
} else if (days <= 25) {
return days + ' days';
} else if (days <= 45) {
return '1 month';
} else if (days <= 345) {
return months + ' months';
} else if (days <= 545) {
return '1 year';
} else {
// (days > 545)
return years + ' years';
}
}
/**
* Remove timer
*/
private removeTimer() {
if (this.timer) {
window.clearTimeout(this.timer);
this.timer = null;
}
}
/**
* Returns Seconds Until Update
* @param seconds: number
*/
private getSecondsUntilUpdate(seconds: number) {
const min = 60;
const hr = min * 60;
const day = hr * 24;
if (seconds < min) {
// less than 1 min, update ever 2 secs
return 2;
} else if (seconds < hr) {
// less than an hour, update every 30 secs
return 30;
} else if (seconds < day) {
// less then a day, update every 5 mins
return 300;
} else {
// update every hour
return 3600;
}
}
}

View File

@@ -0,0 +1,48 @@
// Angular
import { Injectable } from '@angular/core';
// Angular in memory
import { InMemoryDbService } from 'angular-in-memory-web-api';
// RxJS
import { Observable } from 'rxjs';
// Auth
import { AuthDataContext } from '../../../../auth';
// ECommerce
import { ECommerceDataContext } from '../../../../e-commerce';
// Models
import { CarsDb } from './fake-db/cars';
@Injectable()
export class FakeApiService implements InMemoryDbService {
/**
* Service Constructore
*/
constructor() {}
/**
* Create Fake DB and API
*/
createDb(): {} | Observable<{}> {
// tslint:disable-next-line:class-name
const db = {
// auth module
users: AuthDataContext.users,
roles: AuthDataContext.roles,
permissions: AuthDataContext.permissions,
// e-commerce
// customers
customers: ECommerceDataContext.customers,
// products
products: ECommerceDataContext.cars,
productRemarks: ECommerceDataContext.remarks,
productSpecs: ECommerceDataContext.carSpecs,
// orders
orders: ECommerceDataContext.orders,
// data-table
cars: CarsDb.cars
};
return db;
}
}

View File

@@ -0,0 +1,481 @@
export class CarsDb {
public static cars: any = [
{
id: 1,
cModel: 'Elise',
cManufacture: 'Lotus',
cModelYear: 2004,
cMileage: 116879,
// tslint:disable-next-line:max-line-length
cDescription: `The Lotus Elise first appeared in 1996 and revolutionised small sports car design with its lightweight extruded aluminium chassis and composite body. There have been many variations, but the basic principle remain the same.`,
cColor: 'Red',
cPrice: 18347,
cCondition: 1,
createdDate: '09/30/2017',
cStatus: 0,
cVINCode: '1FTWX3D52AE575540',
}, {
id: 2,
cModel: 'Sunbird',
cManufacture: 'Pontiac',
cModelYear: 1984,
cMileage: 99515,
// tslint:disable-next-line:max-line-length
cDescription: `The Pontiac Sunbird is an automobile that was produced by Pontiac, initially as a subcompact for the 1976 to 1980 cModel years,and later as a compact for the 1982 to 1994 cModel years. The Sunbird badge ran for 18 years (with a hiatus during the 1981 and 1982 cModel years, as the 1982 cModel was called J2000) and was then replaced in 1995 by the Pontiac Sunfire. Through the years the Sunbird was available in notchback coupé, sedan, hatchback, station wagon, and convertible body styles.`,
cColor: 'Khaki',
cPrice: 165956,
cCondition: 0,
createdDate: '03/22/2018',
cStatus: 1,
cVINCode: 'JM1NC2EF8A0293556'
}, {
id: 3,
cModel: 'Amigo',
cManufacture: 'Isuzu',
cModelYear: 1993,
cMileage: 138027,
// tslint:disable-next-line:max-line-length
cDescription: `The Isuzu MU is a mid-size SUV that was produced by the Japan-based cManufacturer Isuzu. The three-door MU was introduced in 1989, followed in 1990 by the five-door version called Isuzu MU Wizard, both of which stopped production in 1998 to be replaced by a second generation. This time, the five-door version dropped the "MU" prefix, to become the Isuzu Wizard. The acronym "MU" is short for "Mysterious Utility". Isuzu cManufactured several variations to the MU and its derivates for sale in other countries.`,
cColor: 'Aquamarine',
cPrice: 45684,
cCondition: 0,
createdDate: '03/06/2018',
cStatus: 0,
cVINCode: '1G6DG8E56C0973889'
}, {
id: 4,
cModel: 'LS',
cManufacture: 'Lexus',
cModelYear: 2004,
cMileage: 183068,
// tslint:disable-next-line:max-line-length
cDescription: `The Lexus LS (Japanese: レクサス・LS, Rekusasu LS) is a full-size luxury car (F-segment in Europe) serving as the flagship cModel of Lexus, the luxury division of Toyota. For the first four generations, all LS cModels featured V8 engines and were predominantly rear-wheel-drive, with Lexus also offering all-wheel-drive, hybrid, and long-wheelbase variants. The fifth generation changed to using a V6 engine with no V8 option, and the long wheelbase variant was removed entirely.`,
cColor: 'Mauv',
cPrice: 95410,
cCondition: 1,
createdDate: '02/03/2018',
cStatus: 1,
cVINCode: '2T1BU4EE6DC859114'
}, {
id: 5,
cModel: 'Paseo',
cManufacture: 'Toyota',
cModelYear: 1997,
cMileage: 74884,
// tslint:disable-next-line:max-line-length
cDescription: `The Toyota Paseo (known as the Cynos in Japan and other regions) is a sports styled compact car sold from 19911999 and was loosely based on the Tercel. It was available as a coupe and in later cModels as a convertible. Toyota stopped selling the car in the United States in 1997, however the car continued to be sold in Canada, Europe and Japan until 1999, but had no direct replacement. The Paseo, like the Tercel, shares a platform with the Starlet. Several parts are interchangeable between the three`,
cColor: 'Pink',
cPrice: 24796,
cCondition: 1,
createdDate: '08/13/2017',
cStatus: 0,
cVINCode: '1D7RB1GP0AS597432'
}, {
id: 6,
cModel: 'M',
cManufacture: 'Infiniti',
cModelYear: 2009,
cMileage: 194846,
// tslint:disable-next-line:max-line-length
cDescription: `The Infiniti M is a line of mid-size luxury (executive) cars from the Infiniti luxury division of Nissan.\r\nThe first iteration was the M30 Coupe/Convertible, which were rebadged JDM Nissan Leopard.\r\nAfter a long hiatus, the M nameplate was used for Infiniti's mid-luxury sedans (executive cars). First was the short-lived M45 sedan, a rebadged version of the Japanese-spec Nissan Gloria. The next generations, the M35/45 and M37/56/35h/30d, became the flagship of the Infiniti brand and are based on the JDM Nissan Fuga.`,
cColor: 'Puce',
cPrice: 30521,
cCondition: 1,
createdDate: '01/27/2018',
cStatus: 0,
cVINCode: 'YV1940AS1D1542424'
}, {
id: 7,
cModel: 'Phantom',
cManufacture: 'Rolls-Royce',
cModelYear: 2008,
cMileage: 164124,
// tslint:disable-next-line:max-line-length
cDescription: `The Rolls-Royce Phantom VIII is a luxury saloon car cManufactured by Rolls-Royce Motor Cars. It is the eighth and current generation of Rolls-Royce Phantom, and the second launched by Rolls-Royce under BMW ownership. It is offered in two wheelbase lengths`,
cColor: 'Purple',
cPrice: 196247,
cCondition: 1,
createdDate: '09/28/2017',
cStatus: 1,
cVINCode: '3VWML7AJ1DM234625'
}, {
id: 8,
cModel: 'QX',
cManufacture: 'Infiniti',
cModelYear: 2002,
cMileage: 57410,
// tslint:disable-next-line:max-line-length
cDescription: `The Infiniti QX80 (called the Infiniti QX56 until 2013) is a full-size luxury SUV built by Nissan Motor Company's Infiniti division. The naming convention originally adhered to the current trend of using a numeric designation derived from the engine's displacement, thus QX56 since the car has a 5.6-liter engine. From the 2014 cModel year, the car was renamed the QX80, as part of Infiniti's cModel name rebranding. The new name carries no meaning beyond suggesting that the vehicle is larger than smaller cModels such as the QX60`,
cColor: 'Green',
cPrice: 185775,
cCondition: 1,
createdDate: '11/15/2017',
cStatus: 0,
cVINCode: 'WDDHF2EB9CA161524'
}, {
id: 9,
cModel: 'Daytona',
cManufacture: 'Dodge',
cModelYear: 1993,
cMileage: 4444,
// tslint:disable-next-line:max-line-length
cDescription: `The Dodge Daytona was an automobile which was produced by the Chrysler Corporation under their Dodge division from 1984 to 1993. It was a front-wheel drive hatchback based on the Chrysler G platform, which was derived from the Chrysler K platform. The Chrysler Laser was an upscale rebadged version of the Daytona. The Daytona was restyled for 1987, and again for 1992. It replaced the Mitsubishi Galant-based Challenger, and slotted between the Charger and the Conquest. The Daytona was replaced by the 1995 Dodge Avenger, which was built by Mitsubishi Motors. The Daytona derives its name mainly from the Dodge Charger Daytona, which itself was named after the Daytona 500 race in Daytona Beach, Florida.`,
cColor: 'Maroon',
cPrice: 171898,
cCondition: 0,
createdDate: '12/24/2017',
cStatus: 1,
cVINCode: 'WBAET37422N752051'
}, {
id: 10,
cModel: '1500 Silverado',
cManufacture: 'Chevrolet',
cModelYear: 1999,
cMileage: 195310,
// tslint:disable-next-line:max-line-length
cDescription: `The Chevrolet Silverado, and its mechanically identical cousin, the GMC Sierra, are a series of full-size and heavy-duty pickup trucks cManufactured by General Motors and introduced in 1998 as the successor to the long-running Chevrolet C/K line. The Silverado name was taken from a trim level previously used on its predecessor, the Chevrolet C/K pickup truck from 1975 through 1998. General Motors continues to offer a GMC-badged variant of the Chevrolet full-size pickup under the GMC Sierra name, first used in 1987 for its variant of the GMT400 platform trucks.`,
cColor: 'Blue',
cPrice: 25764,
cCondition: 0,
createdDate: '08/30/2017',
cStatus: 1,
cVINCode: '1N6AF0LX6EN590806'
}, {
id: 11,
cModel: 'CTS',
cManufacture: 'Cadillac',
cModelYear: 2012,
cMileage: 170862,
// tslint:disable-next-line:max-line-length
cDescription: `The Cadillac CTS is a mid-size luxury car / executive car designed, engineered, cManufactured and marketed by General Motors, and now in its third generation. \r\nInitially available only as a 4-door sedan on the GM Sigma platform, GM had offered the second generation CTS in three body styles: 4-door sedan, 2-door coupe, and 5-door sport wagon also using the Sigma platform — and the third generation in coupe and sedan configurations, using a stretched version of the GM Alpha platform.\r\nWayne Cherry and Kip Wasenko designed the exterior of the first generation CTS, marking the production debut of a design language (marketed as "Art and Science") first seen on the Evoq concept car. Bob Boniface and Robin Krieg designed the exterior of the third generation CTS`,
cColor: 'Crimson',
cPrice: 80588,
cCondition: 0,
createdDate: '02/15/2018',
cStatus: 0,
cVINCode: '1G4HR54KX4U506530'
}, {
id: 12,
cModel: 'Astro',
cManufacture: 'Chevrolet',
cModelYear: 1995,
cMileage: 142137,
// tslint:disable-next-line:max-line-length
cDescription: `The Chevrolet Astro was a rear-wheel drive van/minivan cManufactured and marketed by the American automaker Chevrolet from 1985 to 2005 and over two build generations. Along with its rebadged variant, the GMC Safari, the Astro was marketed in passenger as well as cargo and livery configurations—featuring a V6 engine, unibody construction with a separate front engine/suspension sub-frame, leaf-spring rear suspension, rear bi-parting doors, and a seating capacity of up to eight passengers`,
cColor: 'Teal',
cPrice: 72430,
cCondition: 1,
createdDate: '07/31/2017',
cStatus: 0,
cVINCode: 'KMHGH4JH2DU676107'
}, {
id: 13,
cModel: 'XL7',
cManufacture: 'Suzuki',
cModelYear: 2009,
cMileage: 165165,
// tslint:disable-next-line:max-line-length
cDescription: `The Suzuki XL-7 (styled as XL7 for the second generation) is Suzuki's mid-sized SUV that was made from 1998 to 2009, over two generations. It was slotted above the Grand Vitara in Suzuki's lineup.`,
cColor: 'Puce',
cPrice: 118667,
cCondition: 0,
createdDate: '02/04/2018',
cStatus: 0,
cVINCode: '1N6AF0LX9EN733005'
}, {
id: 14,
cModel: 'SJ 410',
cManufacture: 'Suzuki',
cModelYear: 1984,
cMileage: 176074,
// tslint:disable-next-line:max-line-length
cDescription: `The SJ-Series was introduced to the United States (Puerto Rico (SJ-410) and Canada earlier) in 1985 for the 1986 cModel year. It was cPriced at $6200 and 47,000 were sold in its first year. The Samurai had a 1.3 liter, 63 hp (47 kW), 4-cylinder engine and was available as a convertible or a hardtop, and with or without a rear seat. The Suzuki Samurai became intensely popular within the serious 4WD community for its good off-road performance and reliability compared to other 4WDs of the time. This is due to the fact that while very compact and light, it is a real 4WD vehicle equipped with a transfer case, switchable 4WD and low range. Its lightness makes it a very nimble off-roader less prone to sinking in softer ground than heavier types. It is also considered a great beginner off-roader due to its simple design and ease of engine and suspension modifications.`,
cColor: 'Orange',
cPrice: 84325,
cCondition: 0,
createdDate: '12/22/2017',
cStatus: 0,
cVINCode: '2C3CDYBT6DH183756'
}, {
id: 15,
cModel: 'F-Series',
cManufacture: 'Ford',
cModelYear: 1995,
cMileage: 53030,
// tslint:disable-next-line:max-line-length
cDescription: `The Ford F-Series is a series of light-duty trucks and medium-duty trucks (Class 2-7) that have been marketed and cManufactured by Ford Motor Company since 1948. While most variants of the F-Series trucks are full-size pickup trucks, the F-Series also includes chassis cab trucks and commercial vehicles. The Ford F-Series has been the best-selling vehicle in the United States since 1986 and the best-selling pickup since 1977.[1][2] It is also the best selling vehicle in Canada.[3] As of the 2018 cModel year, the F-Series generates $41.0 billion in annual revenue for Ford, making the F-Series brand more valuable than Coca-Cola and Nike.`,
cColor: 'Aquamarine',
cPrice: 77108,
cCondition: 0,
createdDate: '01/09/2018',
cStatus: 0,
cVINCode: 'WBAVB33526P873481'
}, {
id: 16,
cModel: 'HS',
cManufacture: 'Lexus',
cModelYear: 2011,
cMileage: 84718,
// tslint:disable-next-line:max-line-length
cDescription: `The Lexus HS (Japanese: レクサス・HS, Rekusasu HS) is a dedicated hybrid vehicle introduced by Lexus as a new entry-level luxury compact sedan in 2009.[2] Built on the Toyota New MC platform,[3] it is classified as a compact under Japanese regulations concerning vehicle exterior dimensions and engine displacement. Unveiled at the North American International Auto Show in January 2009, the HS 250h went on sale in July 2009 in Japan, followed by the United States in August 2009 as a 2010 cModel. The HS 250h represented the first dedicated hybrid vehicle in the Lexus lineup, as well as the first offered with an inline-four gasoline engine.[4] Bioplastic materials are used for the vehicle interior.[5] With a total length of 184.8 inches, the Lexus HS is slightly larger than the Lexus IS, but still smaller than the mid-size Lexus ES.`,
cColor: 'Purple',
cPrice: 140170,
cCondition: 0,
createdDate: '11/14/2017',
cStatus: 1,
cVINCode: '1FTWF3A56AE545514'
}, {
id: 17,
cModel: 'Land Cruiser',
cManufacture: 'Toyota',
cModelYear: 2008,
cMileage: 157019,
// tslint:disable-next-line:max-line-length
cDescription: `Production of the first generation Land Cruiser began in 1951 (90 units) as Toyota's version of a Jeep-like vehicle.[2][3] The Land Cruiser has been produced in convertible, hardtop, station wagon and cab chassis versions. The Land Cruiser's reliability and longevity has led to huge popularity, especially in Australia where it is the best-selling body-on-frame, four-wheel drive vehicle.[4] Toyota also extensively tests the Land Cruiser in the Australian outback considered to be one of the toughest operating environments in both temperature and terrain. In Japan, the Land Cruiser is exclusive to Toyota Japanese dealerships called Toyota Store.`,
cColor: 'Crimson',
cPrice: 72638,
cCondition: 1,
createdDate: '08/08/2017',
cStatus: 1,
cVINCode: '3C3CFFDR2FT957799'
}, {
id: 18,
cModel: 'Wrangler',
cManufacture: 'Jeep',
cModelYear: 1994,
cMileage: 55857,
// tslint:disable-next-line:max-line-length
cDescription: `The Jeep Wrangler is a series of compact and mid-size (Wrangler Unlimited and Wrangler 4-door JL) four-wheel drive off-road vehicle cModels, cManufactured by Jeep since 1986, and currently migrating from its third into its fourth generation. The Wrangler JL was unveiled in late 2017 and will be produced at Jeep's Toledo Complex.`,
cColor: 'Red',
cPrice: 193523,
cCondition: 0,
createdDate: '02/28/2018',
cStatus: 1,
cVINCode: '3C4PDCAB7FT652291'
}, {
id: 19,
cModel: 'Sunbird',
cManufacture: 'Pontiac',
cModelYear: 1994,
cMileage: 165202,
// tslint:disable-next-line:max-line-length
cDescription: `The Pontiac Sunbird is an automobile that was produced by Pontiac, initially as a subcompact for the 1976 to 1980 cModel years, and later as a compact for the 1982 to 1994 cModel years. The Sunbird badge ran for 18 years (with a hiatus during the 1981 and 1982 cModel years, as the 1982 cModel was called J2000) and was then replaced in 1995 by the Pontiac Sunfire. Through the years the Sunbird was available in notchback coupé, sedan, hatchback, station wagon, and convertible body styles.`,
cColor: 'Blue',
cPrice: 198739,
cCondition: 0,
createdDate: '05/13/2017',
cStatus: 1,
cVINCode: '1GD22XEG9FZ103872'
}, {
id: 20,
cModel: 'A4',
cManufacture: 'Audi',
cModelYear: 1998,
cMileage: 117958,
// tslint:disable-next-line:max-line-length
cDescription: `The A4 has been built in five generations and is based on the Volkswagen Group B platform. The first generation A4 succeeded the Audi 80. The automaker's internal numbering treats the A4 as a continuation of the Audi 80 lineage, with the initial A4 designated as the B5-series, followed by the B6, B7, B8 and the B9. The B8 and B9 versions of the A4 are built on the Volkswagen Group MLB platform shared with many other Audi cModels and potentially one Porsche cModel within Volkswagen Group`,
cColor: 'Yellow',
cPrice: 159377,
cCondition: 0,
createdDate: '12/15/2017',
cStatus: 1,
cVINCode: '2C3CDXCT2FH350366'
}, {
id: 21,
cModel: 'Camry Solara',
cManufacture: 'Toyota',
cModelYear: 2006,
cMileage: 22436,
// tslint:disable-next-line:max-line-length
cDescription: `The Toyota Camry Solara, popularly known as the Toyota Solara, is a mid-size coupe/convertible built by Toyota. The Camry Solara is mechanically based on the Toyota Camry and effectively replaced the discontinued Camry Coupe (XV10); however, in contrast with its predecessor's conservative design, the Camry Solara was designed with a greater emphasis on sportiness, with more rakish styling, and uprated suspension and engine tuning intended to provide a sportier feel.[5] The coupe was launched in late 1998 as a 1999 cModel.[1] In 2000, the convertible was introduced, effectively replacing the Celica convertible in Toyota's North American lineup`,
cColor: 'Green',
cPrice: 122562,
cCondition: 0,
createdDate: '07/11/2017',
cStatus: 0,
cVINCode: '3C3CFFHH6DT874066'
}, {
id: 22,
cModel: 'Tribeca',
cManufacture: 'Subaru',
cModelYear: 2007,
cMileage: 127958,
// tslint:disable-next-line:max-line-length
cDescription: `The Subaru Tribeca is a mid-size crossover SUV made from 2005 to 2014. Released in some markets, including Canada, as the Subaru B9 Tribeca, the name "Tribeca" derives from the Tribeca neighborhood of New York City.[1] Built on the Subaru Legacy platform and sold in five- and seven-seat configurations, the Tribeca was intended to be sold alongside a slightly revised version known as the Saab 9-6. Saab, at the time a subsidiary of General Motors (GM), abandoned the 9-6 program just prior to its release subsequent to GM's 2005 divestiture of its 20 percent stake in FHI.`,
cColor: 'Yellow',
cPrice: 90221,
cCondition: 1,
createdDate: '11/12/2017',
cStatus: 0,
cVINCode: 'WVWGU7AN9AE957575'
}, {
id: 23,
cModel: '1500 Club Coupe',
cManufacture: 'GMC',
cModelYear: 1997,
cMileage: 95783,
// tslint:disable-next-line:max-line-length
cDescription: `GMC (General Motors Truck Company), formally the GMC Division of General Motors LLC, is a division of the American automobile cManufacturer General Motors (GM) that primarily focuses on trucks and utility vehicles. GMC sells pickup and commercial trucks, buses, vans, military vehicles, and sport utility vehicles marketed worldwide by General Motors.`,
cColor: 'Teal',
cPrice: 64376,
cCondition: 1,
createdDate: '06/28/2017',
cStatus: 0,
cVINCode: 'SCFBF04BX7G920997'
}, {
id: 24,
cModel: 'Firebird',
cManufacture: 'Pontiac',
cModelYear: 2002,
cMileage: 74063,
// tslint:disable-next-line:max-line-length
cDescription: `The Pontiac Firebird is an American automobile built by Pontiac from the 1967 to the 2002 cModel years. Designed as a pony car to compete with the Ford Mustang, it was introduced 23 February 1967, the same cModel year as GM's Chevrolet division platform-sharing Camaro.[1] This also coincided with the release of the 1967 Mercury Cougar, Ford's upscale, platform-sharing version of the Mustang. The name "Firebird" was also previously used by GM for the General Motors Firebird 1950s and early-1960s`,
cColor: 'Puce',
cPrice: 94178,
cCondition: 1,
createdDate: '09/13/2017',
cStatus: 0,
cVINCode: '3C63D2JL5CG563879'
}, {
id: 25,
cModel: 'RAV4',
cManufacture: 'Toyota',
cModelYear: 1996,
cMileage: 99461,
// tslint:disable-next-line:max-line-length
cDescription: `The Toyota RAV4 (Japanese: トヨタ RAV4 Toyota Ravufō) is a compact crossover SUV (sport utility vehicle) produced by the Japanese automobile cManufacturer Toyota. This was the first compact crossover SUV;[1] it made its debut in Japan and Europe in 1994,[2] and in North America in 1995. The vehicle was designed for consumers wanting a vehicle that had most of the benefits of SUVs, such as increased cargo room, higher visibility, and the option of full-time four-wheel drive, along with the maneuverability and fuel economy of a compact car. Although not all RAV4s are four-wheel-drive, RAV4 stands for "Recreational Activity Vehicle: 4-wheel drive", because the aforementioned equipment is an option in select countries`,
cColor: 'Goldenrod',
cPrice: 48342,
cCondition: 0,
createdDate: '12/29/2017',
cStatus: 0,
cVINCode: '2C4RDGDG6DR836144'
}, {
id: 26,
cModel: 'Amanti / Opirus',
cManufacture: 'Kia',
cModelYear: 2007,
cMileage: 189651,
// tslint:disable-next-line:max-line-length
cDescription: `The Kia Opirus was an executive car cManufactured and marketed by Kia Motors that was launched in April 2003 and was marketed globally under various nameplates, prominently as the Amanti. It was considered to be Kia's flagship vehicle.`,
cColor: 'Indigo',
cPrice: 44292,
cCondition: 1,
createdDate: '09/01/2017',
cStatus: 1,
cVINCode: '1C4SDHCT2CC055294'
}, {
id: 27,
cModel: 'S60',
cManufacture: 'Volvo',
cModelYear: 2001,
cMileage: 78963,
// tslint:disable-next-line:max-line-length
cDescription: `First introduced in 2004, Volvo's S60 R used a Haldex all-wheel-drive system mated to a 300 PS (221 kW; 296 hp) / 400 N⋅m (300 lbf⋅ft) inline-5. The 20042005 cModels came with a 6-speed manual transmission, or an available 5-speed automatic which allowed only 258 lb⋅ft (350 N⋅m) torque in 1st and 2nd gears. The 20062007 cModels came with a 6-speed manual or 6-speed automatic transmission (which was no longer torque-restricted)`,
cColor: 'Goldenrod',
cPrice: 9440,
cCondition: 0,
createdDate: '11/06/2017',
cStatus: 0,
cVINCode: '3C6TD5CT5CG316067',
}, {
id: 28,
cModel: 'Grand Marquis',
cManufacture: 'Mercury',
cModelYear: 1984,
cMileage: 153027,
// tslint:disable-next-line:max-line-length
cDescription: `The Mercury Grand Marquis is an automobile that was sold by the Mercury division of Ford Motor Company from 1975 to 2011. From 1975 to 1982, it was the premium cModel of the Mercury Marquis cModel line, becoming a standalone cModel line in 1983. For its entire production run, the Grand Marquis served as the flagship of the Mercury line, with the Ford (LTD) Crown Victoria serving as its Ford counterpart. In addition, from 1979 to 2011, the Grand Marquis shared the rear-wheel drive Panther platform alongside the Lincoln Town Car`,
cColor: 'Goldenrod',
cPrice: 76027,
cCondition: 0,
createdDate: '12/16/2017',
cStatus: 1,
cVINCode: '3C3CFFJH2DT871398'
}, {
id: 29,
cModel: 'Talon',
cManufacture: 'Eagle',
cModelYear: 1991,
cMileage: 111234,
// tslint:disable-next-line:max-line-length
cDescription: `Cosmetically, differences between the three were found in wheels, availability of cColors, tail lights, front and rear bumpers, and spoilers. The Talon featured two-tone body cColor with a black 'greenhouse' (roof, pillars, door-mounted mirrors) regardless of the body cColor. The variants featured 5-speed manual or 4-speed automatic transmissions and a hood bulge on the left-hand side of the car in order for camshaft clearance on the 4G63 engine. The base cModel DL did not use this engine but still had a bulge as evident in the 1992 Talon brochure. 2nd Generation cars all had such a bulge, even with the inclusion of the 420A engine`,
cColor: 'Teal',
cPrice: 157216,
cCondition: 0,
createdDate: '05/08/2017',
cStatus: 1,
cVINCode: 'YV1902FH1D2957659'
}, {
id: 30,
cModel: 'Passport',
cManufacture: 'Honda',
cModelYear: 2002,
cMileage: 3812,
// tslint:disable-next-line:max-line-length
cDescription: `The Passport was a part of a partnership between Isuzu and Honda in the 1990s, which saw an exchange of passenger vehicles from Honda to Isuzu, such as the Isuzu Oasis, and trucks from Isuzu to Honda, such as the Passport and Acura SLX. This arrangement was convenient for both companies, as Isuzu discontinued passenger car production in 1993 after a corporate restructuring, and Honda was in desperate need a SUV, a segment that was growing in popularity in North America as well as Japan during the 1990s. The partnership ended in 2002 with the discontinuation of the Passport in favor of the Honda-engineered Pilot`,
cColor: 'Puce',
cPrice: 41299,
cCondition: 1,
createdDate: '03/08/2018',
cStatus: 0,
cVINCode: 'WVWEU9AN4AE524071'
}, {
id: 31,
cModel: 'H3',
cManufacture: 'Hummer',
cModelYear: 2006,
cMileage: 196321,
// tslint:disable-next-line:max-line-length
cDescription: `The Hummer H3 is a sport utility vehicle/off-road vehicle from Hummer that was produced from 2005 to 2010. Introduced for the 2006 cModel year, it was based on a highly modified GMT355 underpinning the Chevrolet cColorado/GMC Canyon compact pickup trucks that were also built at GM's Shreveport Operations in Shreveport, Louisiana and the Port Elizabeth plant in South Africa. The H3 was actually the smallest among the Hummer cModels. It was available either as a traditional midsize SUV or as a midsize pickup known as the H3T`,
cColor: 'Pink',
cPrice: 186964,
cCondition: 1,
createdDate: '06/04/2017',
cStatus: 1,
cVINCode: '4T1BF1FK4FU746230',
}, {
id: 32,
cModel: 'Comanche',
cManufacture: 'Jeep',
cModelYear: 1992,
cMileage: 72285,
// tslint:disable-next-line:max-line-length
cDescription: `The Jeep Comanche (designated MJ) is a pickup truck variant of the Cherokee compact SUV (19842001)[3] cManufactured and marketed by Jeep for cModel years 1986-1992 in rear wheel (RWD) and four-wheel drive (4WD) cModels as well as two cargo bed lengths: six-feet (1.83 metres) and seven-feet (2.13 metres)`,
cColor: 'Mauv',
cPrice: 145971,
cCondition: 1,
createdDate: '09/01/2017',
cStatus: 0,
cVINCode: '1J4PN2GK1BW745045'
}, {
id: 33,
cModel: 'Blazer',
cManufacture: 'Chevrolet',
cModelYear: 1993,
cMileage: 189804,
// tslint:disable-next-line:max-line-length
cDescription: `The 2014 2nd generation, MY14 Duramax 2.8L diesel engines have several new parts, namely a new water-cooled variable-geometry turbocharger, a new high-pressure common-rail fuel delivery system, a new exhaust gas recirculation (EGR) system, a new intake manifold, a new cylinder head, a new cylinder block, a new balance shaft unit and a new Engine Control Module (ECM). and now produce 197 hp and 369 Ft/Lbs of torque`,
cColor: 'Indigo',
cPrice: 154594,
cCondition: 0,
createdDate: '09/13/2017',
cStatus: 0,
cVINCode: '1G6KD57Y43U482896'
}, {
id: 34,
cModel: 'Envoy XUV',
cManufacture: 'GMC',
cModelYear: 2004,
cMileage: 187960,
// tslint:disable-next-line:max-line-length
cDescription: `The GMC Envoy is a mid-size SUV that was produced by General Motors. It was introduced for the 1998 cModel year. After the first generation Envoy was discontinued after the 2000 cModel year, but the Envoy was reintroduced and redesigned for the 2002 cModel year, and it was available in the GMC line of vehicles from the 2002 to 2009 cModel years`,
cColor: 'Turquoise',
cPrice: 185103,
cCondition: 1,
createdDate: '12/07/2017',
cStatus: 0,
cVINCode: '5GAER23D09J658030'
}
];
}

View File

@@ -0,0 +1,26 @@
// Angular
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
// RxJS
import { Observable } from 'rxjs';
// Models
import { DataTableItemModel } from '../models/datatable-item.model';
const API_DATATABLE_URL = 'api/cars';
@Injectable()
export class DataTableService {
/**
* Service Constructor
*
* @param http: HttpClient
*/
constructor(private http: HttpClient) { }
/**
* Returns data from fake server
*/
getAllItems(): Observable<DataTableItemModel[]> {
return this.http.get<DataTableItemModel[]>(API_DATATABLE_URL);
}
}

View File

@@ -0,0 +1,29 @@
// Angular
import { Injectable } from '@angular/core';
// RxJS
import { BehaviorSubject } from 'rxjs';
@Injectable()
export class KtDialogService {
private ktDialog: any;
private currentState: BehaviorSubject<boolean> = new BehaviorSubject(false);
// Public properties
constructor() {
this.ktDialog = new KTDialog({type: 'loader', placement: 'top center', message: 'Loading ...'});
}
show() {
this.currentState.next(true);
this.ktDialog.show();
}
hide() {
this.currentState.next(false);
this.ktDialog.hide();
}
checkIsShown() {
return this.currentState.value;
}
}

View File

@@ -0,0 +1,144 @@
// Angular
import { Injectable } from '@angular/core';
// RxJS
import { Subject } from 'rxjs';
// Object-Path
import * as objectPath from 'object-path';
// Lodash
import { merge } from 'lodash';
// Models
import { LayoutConfigModel } from '../models/layout-config.model';
@Injectable()
export class LayoutConfigService {
// Public properties
onConfigUpdated$: Subject<LayoutConfigModel>;
layoutConfig: LayoutConfigModel;
/**
* Servcie constructor
*/
constructor() {
// register on config changed event and set default config
this.onConfigUpdated$ = new Subject();
}
/**
* Save layout config to the local storage
* @param layoutConfig
*/
saveConfig(layoutConfig: LayoutConfigModel): void {
if (layoutConfig) {
localStorage.setItem('layoutConfigDrenax', JSON.stringify(layoutConfig));
}
}
/**
* Get layout config from local storage
*/
getSavedConfig(): LayoutConfigModel {
const config = localStorage.getItem('layoutConfigDrenax');
try {
return JSON.parse(config);
} catch (e) {
}
}
/**
* Remove saved layout config and revert back to default
*/
resetConfig(): void {
localStorage.removeItem('layoutConfigDrenax');
}
/**
* Get all config or by object path
* @param path | object path separated by dot
*/
getConfig(path?: string): LayoutConfigModel | any {
// merge default layout config with the saved config from layout storage
// @todo; known issue; viewing 2 or more demos at the time in different browser's tabs, can cause conflict to the layout config
this.layoutConfig = this.getSavedConfig();
if (path) {
// if path is specified, get the value within object
return objectPath.get(this.layoutConfig, path);
}
return this.layoutConfig;
}
/**
* Set existing config with a new value
* @param value
* @param save
*/
setConfig(value: any, save?: boolean): void {
this.layoutConfig = merge(this.layoutConfig, value);
if (save) {
this.saveConfig(this.layoutConfig);
}
// fire off an event that all subscribers will listen
this.onConfigUpdated$.next(this.layoutConfig);
}
/**
* Get brand logo
*/
getLogo(): string {
const menuAsideLeftSkin = objectPath.get(this.layoutConfig, 'brand.self.skin');
// set brand logo
const logoObject = objectPath.get(this.layoutConfig, 'self.logo');
let logo;
if (typeof logoObject === 'string') {
logo = logoObject;
}
if (typeof logoObject === 'object') {
logo = objectPath.get(logoObject, menuAsideLeftSkin + '');
}
if (typeof logo === 'undefined') {
try {
const logos = objectPath.get(this.layoutConfig, 'self.logo');
logo = logos[Object.keys(logos)[0]];
} catch (e) {
}
}
return logo;
}
/**
* Returns sticky logo
*/
getStickyLogo(): string {
let logo = objectPath.get(this.layoutConfig, 'self.logo.sticky');
if (typeof logo === 'undefined') {
logo = this.getLogo();
}
return logo + '';
}
/**
* Initialize layout config
* @param config
*/
loadConfigs(config: LayoutConfigModel) {
this.layoutConfig = this.getSavedConfig();
// use saved config as priority, or load new config if demo does not matched
if (!this.layoutConfig || objectPath.get(this.layoutConfig, 'demo') !== config.demo) {
this.layoutConfig = config;
}
this.saveConfig(this.layoutConfig);
}
/**
* Reload current layout config to the state of latest saved config
*/
reloadConfigs(): LayoutConfigModel {
this.layoutConfig = this.getSavedConfig();
this.onConfigUpdated$.next(this.layoutConfig);
return this.layoutConfig;
}
}

View File

@@ -0,0 +1,25 @@
// Angular
import { Injectable } from '@angular/core';
// RxJS
import { BehaviorSubject } from 'rxjs';
@Injectable()
export class LayoutRefService {
// Public properties
layoutRefs$: BehaviorSubject<any> = new BehaviorSubject<any>({});
layoutRefs: any = {};
/**
* Add element to Ref
*
* @param name: any
* @param element: any
*/
addElement(name, element) {
const obj = {};
obj[name] = element;
this.layoutRefs = Object.assign({}, this.layoutRefs, obj);
this.layoutRefs$.next(this.layoutRefs);
}
}

View File

@@ -0,0 +1,32 @@
// Angular
import { Injectable } from '@angular/core';
// RxJS
import { BehaviorSubject } from 'rxjs';
// Object path
import * as objectPath from 'object-path';
// Services
import { MenuConfigService } from './menu-config.service';
@Injectable()
export class MenuAsideService {
// Public properties
menuList$: BehaviorSubject<any[]> = new BehaviorSubject<any[]>([]);
/**
* Service constructor
*
* @param menuConfigService: MenuConfigService
*/
constructor(private menuConfigService: MenuConfigService) {
this.loadMenu();
}
/**
* Load menu list
*/
loadMenu() {
// get menu list
const menuItems: any[] = objectPath.get(this.menuConfigService.getMenus(), 'aside.items');
this.menuList$.next(menuItems);
}
}

View File

@@ -0,0 +1,37 @@
// Angular
import { Injectable } from '@angular/core';
// RxJS
import { Subject } from 'rxjs';
@Injectable()
export class MenuConfigService {
// Public properties
onConfigUpdated$: Subject<any>;
// Private properties
private menuConfig: any;
/**
* Service Constructor
*/
constructor() {
// register on config changed event and set default config
this.onConfigUpdated$ = new Subject();
}
/**
* Returns the menuConfig
*/
getMenus() {
return this.menuConfig;
}
/**
* Load config
*
* @param config: any
*/
loadConfigs(config: any) {
this.menuConfig = config;
this.onConfigUpdated$.next(this.menuConfig);
}
}

View File

@@ -0,0 +1,32 @@
// Angular
import { Injectable } from '@angular/core';
// RxJS
import { BehaviorSubject } from 'rxjs';
// Object path
import * as objectPath from 'object-path';
// Services
import { MenuConfigService } from './menu-config.service';
@Injectable()
export class MenuHorizontalService {
// Public properties
menuList$: BehaviorSubject<any[]> = new BehaviorSubject<any[]>([]);
/**
* Service constructor
*
* @param menuConfigService: MenuConfigService
*/
constructor(private menuConfigService: MenuConfigService) {
this.loadMenu();
}
/**
* Load menu list
*/
loadMenu() {
// get menu list
const menuItems: any[] = objectPath.get(this.menuConfigService.getMenus(), 'header.items');
this.menuList$.next(menuItems);
}
}

View File

@@ -0,0 +1,92 @@
// Angular
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
// RxJS
import { Subject } from 'rxjs';
// Object-Path
import * as objectPath from 'object-path';
// Lodash
import { merge } from 'lodash';
@Injectable()
export class PageConfigService {
// Public properties
onConfigUpdated$: Subject<any>;
pageConfig: any;
/**
* Service Constructor
*
* @param router: Router
*/
constructor(private router: Router) {
// register on config changed event and set default config
this.onConfigUpdated$ = new Subject();
}
/**
* Get current page config based on route
*/
getCurrentPageConfig(path?: string): any {
let configPath = this.cleanUrl(this.router.url);
if (path) {
configPath += '.' + path;
}
// get page config by path
return objectPath.get(this.pageConfig, configPath);
}
/**
* Set existing config with a new value
* @param value: any
* @param sav: boolean?
*/
setConfig(value: any, save?: boolean): void {
this.pageConfig = merge(this.pageConfig, value);
if (save) {
// not implemented
}
// fire off an event that all subscribers will listen
this.onConfigUpdated$.next(this.pageConfig);
}
/**
* Load confgis
*
* @param config: any
*/
loadConfigs(config: any) {
this.pageConfig = config;
this.onConfigUpdated$.next(this.pageConfig);
}
/**
* Remove unnecessary params from URL
* @param url
*/
cleanUrl(url: string): string {
// remove first route (demo name) from url router
if (new RegExp(/^\/demo/).test(url)) {
const urls = url.split('/');
urls.splice(0, 2);
url = urls.join('/');
}
if (url.charAt(0) == '/') {
url = url.substr(1);
}
// we get the page title from config, using url path.
// we need to remove query from url ?id=1 before we use the path to search in array config.
let finalUrl = url.replace(/\//g, '.');
if (finalUrl.indexOf('?') !== -1) {
finalUrl = finalUrl.substring(0, finalUrl.indexOf('?'));
}
return finalUrl;
}
}

View File

@@ -0,0 +1,52 @@
// Angular
import { ElementRef, Injectable } from '@angular/core';
import { animate, AnimationBuilder, style } from '@angular/animations';
@Injectable()
export class SplashScreenService {
// Private properties
private el: ElementRef;
private stopped: boolean;
/**
* Service constctuctor
*
* @param animationBuilder: AnimationBuilder
*/
constructor(private animationBuilder: AnimationBuilder) {
}
/**
* Init
*
* @param element: ElementRef
*/
init(element: ElementRef) {
this.el = element;
}
/**
* Hide
*/
hide() {
if (this.stopped) {
return;
}
const player = this.animationBuilder.build([
style({opacity: '1'}),
animate(800, style({opacity: '0'}))
]).create(this.el.nativeElement);
player.onDone(() => {
if (typeof this.el.nativeElement.remove === 'function') {
this.el.nativeElement.remove();
} else {
this.el.nativeElement.style.display = 'none';
}
this.stopped = true;
});
setTimeout(() => player.play(), 300);
}
}

View File

@@ -0,0 +1,197 @@
// Angular
import { Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
// RxJS
import { BehaviorSubject, Subject } from 'rxjs';
import { filter } from 'rxjs/operators';
// Object-Path
import * as objectPath from 'object-path';
// Services
import { PageConfigService } from './page-config.service';
import { MenuConfigService } from './menu-config.service';
export interface Breadcrumb {
title: string;
page: string | any;
}
export interface BreadcrumbTitle {
title: string;
desc?: string;
}
@Injectable()
export class SubheaderService {
// Public properties
title$: BehaviorSubject<BreadcrumbTitle> = new BehaviorSubject<BreadcrumbTitle>({title: '', desc: ''});
breadcrumbs$: BehaviorSubject<Breadcrumb[]> = new BehaviorSubject<Breadcrumb[]>([]);
disabled$: Subject<boolean> = new Subject<boolean>();
// Private properties
private manualBreadcrumbs: any = {};
private appendingBreadcrumbs: any = {};
private manualTitle: any = {};
private asideMenus: any;
private headerMenus: any;
private pageConfig: any;
/**
* Service Constructor
*
* @param router: Router
* @param pageConfigService: PageConfigServie
* @param menuConfigService: MenuConfigService
*/
constructor(
private router: Router,
private pageConfigService: PageConfigService,
private menuConfigService: MenuConfigService) {
const initBreadcrumb = () => {
// get updated title current page config
this.pageConfig = this.pageConfigService.getCurrentPageConfig();
this.headerMenus = objectPath.get(this.menuConfigService.getMenus(), 'header');
this.asideMenus = objectPath.get(this.menuConfigService.getMenus(), 'aside');
// update breadcrumb on initial page load
this.updateBreadcrumbs();
if (objectPath.get(this.manualTitle, this.router.url)) {
this.setTitle(this.manualTitle[this.router.url]);
} else {
// get updated page title on every route changed
this.title$.next(objectPath.get(this.pageConfig, 'page'));
// subheader enable/disable
const hideSubheader = objectPath.get(this.pageConfig, 'page.subheader');
this.disabled$.next(typeof hideSubheader !== 'undefined' && !hideSubheader);
if (objectPath.get(this.manualBreadcrumbs, this.router.url)) {
// breadcrumbs was set manually
this.setBreadcrumbs(this.manualBreadcrumbs[this.router.url]);
} else {
// get updated breadcrumbs on every route changed
this.updateBreadcrumbs();
// breadcrumbs was appended before, reuse it for this page
if (objectPath.get(this.appendingBreadcrumbs, this.router.url)) {
this.appendBreadcrumbs(this.appendingBreadcrumbs[this.router.url]);
}
}
}
};
initBreadcrumb();
// subscribe to router events
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(initBreadcrumb);
}
/**
* Update breadCrumbs
*/
updateBreadcrumbs() {
// get breadcrumbs from header menu
let breadcrumbs = this.getBreadcrumbs(this.headerMenus);
// if breadcrumbs empty from header menu
if (breadcrumbs.length === 0) {
// get breadcrumbs from aside menu
breadcrumbs = this.getBreadcrumbs(this.asideMenus);
}
if (
// if breadcrumb has only 1 item
breadcrumbs.length === 1 &&
// and breadcrumb title is same as current page title
breadcrumbs[0].title.indexOf(objectPath.get(this.pageConfig, 'page.title')) !== -1) {
// no need to display on frontend
breadcrumbs = [];
}
this.breadcrumbs$.next(breadcrumbs);
}
/**
* Manually set full breadcrumb paths
*/
setBreadcrumbs(breadcrumbs: Breadcrumb[] | any[]) {
this.manualBreadcrumbs[this.router.url] = breadcrumbs;
this.breadcrumbs$.next(breadcrumbs);
}
/**
* Append breadcrumb to the last existing breadcrumbs
* @param breadcrumbs
*/
appendBreadcrumbs(breadcrumbs: Breadcrumb[] | any[]) {
this.appendingBreadcrumbs[this.router.url] = breadcrumbs;
const prev = this.breadcrumbs$.getValue();
this.breadcrumbs$.next(prev.concat(breadcrumbs));
}
/**
* Get breadcrumbs from menu items
* @param menus
*/
getBreadcrumbs(menus: any) {
let url = this.pageConfigService.cleanUrl(this.router.url);
url = url.replace(new RegExp(/\./, 'g'), '/');
const breadcrumbs = [];
const menuPath = this.getPath(menus, url) || [];
menuPath.forEach(key => {
menus = menus[key];
if (typeof menus !== 'undefined' && menus.title) {
breadcrumbs.push(menus);
}
});
return breadcrumbs;
}
/**
* Set title
*
* @param title: string
*/
setTitle(title: string) {
this.manualTitle[this.router.url] = title;
this.title$.next({title});
}
/**
* Get object path by value
* @param obj
* @param value
*/
getPath(obj, value) {
if (typeof obj !== 'object') {
return;
}
const path = [];
let found = false;
const search = (haystack) => {
// tslint:disable-next-line:forin
for (const key in haystack) {
path.push(key);
if (haystack[key] === value) {
found = true;
break;
}
if (typeof haystack[key] === 'object') {
search(haystack[key]);
if (found) {
break;
}
}
path.pop();
}
};
search(obj);
return path;
}
}

View File

@@ -0,0 +1,71 @@
// Angular
import { Injectable } from '@angular/core';
// Tranlsation
import { TranslateService } from '@ngx-translate/core';
export interface Locale {
lang: string;
// tslint:disable-next-line:ban-types
data: Object;
}
@Injectable({
providedIn: 'root'
})
export class TranslationService {
// Private properties
private langIds: any = [];
/**
* Service Constructor
*
* @param translate: TranslateService
*/
constructor(private translate: TranslateService) {
// add new langIds to the list
this.translate.addLangs(['en']);
// this language will be used as a fallback when a translation isn't found in the current language
this.translate.setDefaultLang('en');
}
/**
* Load Translation
*
* @param args: Locale[]
*/
loadTranslations(...args: Locale[]): void {
const locales = [...args];
locales.forEach(locale => {
// use setTranslation() with the third argument set to true
// to append translations instead of replacing them
this.translate.setTranslation(locale.lang, locale.data, true);
this.langIds.push(locale.lang);
});
// add new languages to the list
this.translate.addLangs(this.langIds);
}
/**
* Setup language
*
* @param lang: any
*/
setLanguage(lang) {
if (lang) {
this.translate.use(this.translate.getDefaultLang());
this.translate.use(lang);
localStorage.setItem('languageDrenax', lang);
}
}
/**
* Returns selected language
*/
getSelectedLanguage(): any {
return localStorage.getItem('languageDrenax') || this.translate.getDefaultLang();
}
}

View File

@@ -0,0 +1,105 @@
// China
export const locale = {
lang: 'ch',
data: {
TRANSLATOR: {
SELECT: '选择你的语言',
},
MENU: {
NEW: '新',
ACTIONS: '行动',
CREATE_POST: '创建新帖子',
PAGES: 'Pages',
FEATURES: '特征',
APPS: '应用',
DASHBOARD: '仪表板',
},
AUTH: {
GENERAL: {
OR: '要么',
SUBMIT_BUTTON: '提交',
NO_ACCOUNT: '没有账号?',
SIGNUP_BUTTON: '注册',
FORGOT_BUTTON: '忘记密码',
BACK_BUTTON: '背部',
PRIVACY: '隐私',
LEGAL: '法律',
CONTACT: '联系',
},
LOGIN: {
TITLE: '创建帐号',
BUTTON: '签到',
},
FORGOT: {
TITLE: 'Forgotten Password?',
DESC: 'Enter your email to reset your password',
SUCCESS: 'Your account has been successfully reset.'
},
REGISTER: {
TITLE: 'Sign Up',
DESC: 'Enter your details to create your account',
SUCCESS: 'Your account has been successfuly registered.'
},
INPUT: {
EMAIL: 'Email',
FULLNAME: 'Fullname',
PASSWORD: 'Password',
CONFIRM_PASSWORD: 'Confirm Password',
USERNAME: '用戶名'
},
VALIDATION: {
INVALID: '{{name}} is not valid',
REQUIRED: '{{name}} is required',
MIN_LENGTH: '{{name}} minimum length is {{min}}',
AGREEMENT_REQUIRED: 'Accepting terms & conditions are required',
NOT_FOUND: 'The requested {{name}} is not found',
INVALID_LOGIN: 'The login detail is incorrect',
REQUIRED_FIELD: 'Required field',
MIN_LENGTH_FIELD: 'Minimum field length:',
MAX_LENGTH_FIELD: 'Maximum field length:',
INVALID_FIELD: 'Field is not valid',
}
},
ECOMMERCE: {
COMMON: {
SELECTED_RECORDS_COUNT: 'Selected records count: ',
ALL: 'All',
SUSPENDED: 'Suspended',
ACTIVE: 'Active',
FILTER: 'Filter',
BY_STATUS: 'by Status',
BY_TYPE: 'by Type',
BUSINESS: 'Business',
INDIVIDUAL: 'Individual',
SEARCH: 'Search',
IN_ALL_FIELDS: 'in all fields'
},
ECOMMERCE: 'eCommerce',
CUSTOMERS: {
CUSTOMERS: '顾客',
CUSTOMERS_LIST: '客户名单',
NEW_CUSTOMER: 'New Customer',
DELETE_CUSTOMER_SIMPLE: {
TITLE: 'Customer Delete',
DESCRIPTION: 'Are you sure to permanently delete this customer?',
WAIT_DESCRIPTION: 'Customer is deleting...',
MESSAGE: 'Customer has been deleted'
},
DELETE_CUSTOMER_MULTY: {
TITLE: 'Customers Delete',
DESCRIPTION: 'Are you sure to permanently delete selected customers?',
WAIT_DESCRIPTION: 'Customers are deleting...',
MESSAGE: 'Selected customers have been deleted'
},
UPDATE_STATUS: {
TITLE: 'Status has been updated for selected customers',
MESSAGE: 'Selected customers status have successfully been updated'
},
EDIT: {
UPDATE_MESSAGE: 'Customer has been updated',
ADD_MESSAGE: 'Customer has been created'
}
}
}
}
};

View File

@@ -0,0 +1,105 @@
// Germany
export const locale = {
lang: 'de',
data: {
TRANSLATOR: {
SELECT: 'Wähle deine Sprache',
},
MENU: {
NEW: 'Neu',
ACTIONS: 'Aktionen',
CREATE_POST: 'Erstellen Sie einen neuen Beitrag',
PAGES: 'Pages',
FEATURES: 'Eigenschaften',
APPS: 'Apps',
DASHBOARD: 'Instrumententafel'
},
AUTH: {
GENERAL: {
OR: 'Oder',
SUBMIT_BUTTON: 'einreichen',
NO_ACCOUNT: 'Hast du kein Konto?',
SIGNUP_BUTTON: 'Anmelden',
FORGOT_BUTTON: 'Passwort vergessen',
BACK_BUTTON: 'Zurück',
PRIVACY: 'Privatsphäre',
LEGAL: 'Legal',
CONTACT: 'Kontakt',
},
LOGIN: {
TITLE: 'Create Account',
BUTTON: 'Sign In',
},
FORGOT: {
TITLE: 'Forgotten Password?',
DESC: 'Enter your email to reset your password',
SUCCESS: 'Your account has been successfully reset.'
},
REGISTER: {
TITLE: 'Sign Up',
DESC: 'Enter your details to create your account',
SUCCESS: 'Your account has been successfuly registered.'
},
INPUT: {
EMAIL: 'Email',
FULLNAME: 'Fullname',
PASSWORD: 'Password',
CONFIRM_PASSWORD: 'Confirm Password',
USERNAME: 'Nutzername'
},
VALIDATION: {
INVALID: '{{name}} is not valid',
REQUIRED: '{{name}} is required',
MIN_LENGTH: '{{name}} minimum length is {{min}}',
AGREEMENT_REQUIRED: 'Accepting terms & conditions are required',
NOT_FOUND: 'The requested {{name}} is not found',
INVALID_LOGIN: 'The login detail is incorrect',
REQUIRED_FIELD: 'Required field',
MIN_LENGTH_FIELD: 'Minimum field length:',
MAX_LENGTH_FIELD: 'Maximum field length:',
INVALID_FIELD: 'Field is not valid',
}
},
ECOMMERCE: {
COMMON: {
SELECTED_RECORDS_COUNT: 'Selected records count: ',
ALL: 'All',
SUSPENDED: 'Suspended',
ACTIVE: 'Active',
FILTER: 'Filter',
BY_STATUS: 'by Status',
BY_TYPE: 'by Type',
BUSINESS: 'Business',
INDIVIDUAL: 'Individual',
SEARCH: 'Search',
IN_ALL_FIELDS: 'in all fields'
},
ECOMMERCE: 'eCommerce',
CUSTOMERS: {
CUSTOMERS: 'Customers',
CUSTOMERS_LIST: 'Customers list',
NEW_CUSTOMER: 'New Customer',
DELETE_CUSTOMER_SIMPLE: {
TITLE: 'Customer Delete',
DESCRIPTION: 'Are you sure to permanently delete this customer?',
WAIT_DESCRIPTION: 'Customer is deleting...',
MESSAGE: 'Customer has been deleted'
},
DELETE_CUSTOMER_MULTY: {
TITLE: 'Customers Delete',
DESCRIPTION: 'Are you sure to permanently delete selected customers?',
WAIT_DESCRIPTION: 'Customers are deleting...',
MESSAGE: 'Selected customers have been deleted'
},
UPDATE_STATUS: {
TITLE: 'Status has been updated for selected customers',
MESSAGE: 'Selected customers status have successfully been updated'
},
EDIT: {
UPDATE_MESSAGE: 'Customer has been updated',
ADD_MESSAGE: 'Customer has been created'
}
}
}
}
};

View File

@@ -0,0 +1,105 @@
// USA
export const locale = {
lang: 'en',
data: {
TRANSLATOR: {
SELECT: 'Select your language',
},
MENU: {
NEW: 'new',
ACTIONS: 'Actions',
CREATE_POST: 'Create New Post',
PAGES: 'Pages',
FEATURES: 'Features',
APPS: 'Apps',
DASHBOARD: 'Dashboard',
},
AUTH: {
GENERAL: {
OR: 'Or',
SUBMIT_BUTTON: 'Submit',
NO_ACCOUNT: 'Don\'t have an account?',
SIGNUP_BUTTON: 'Sign Up',
FORGOT_BUTTON: 'Forgot Password',
BACK_BUTTON: 'Back',
PRIVACY: 'Privacy',
LEGAL: 'Legal',
CONTACT: 'Contact',
},
LOGIN: {
TITLE: 'Login Account',
BUTTON: 'Sign In',
},
FORGOT: {
TITLE: 'Forgotten Password?',
DESC: 'Enter your email to reset your password',
SUCCESS: 'Your account has been successfully reset.'
},
REGISTER: {
TITLE: 'Sign Up',
DESC: 'Enter your details to create your account',
SUCCESS: 'Your account has been successfuly registered.'
},
INPUT: {
EMAIL: 'Email',
FULLNAME: 'Fullname',
PASSWORD: 'Password',
CONFIRM_PASSWORD: 'Confirm Password',
USERNAME: 'Username'
},
VALIDATION: {
INVALID: '{{name}} is not valid',
REQUIRED: '{{name}} is required',
MIN_LENGTH: '{{name}} minimum length is {{min}}',
AGREEMENT_REQUIRED: 'Accepting terms & conditions are required',
NOT_FOUND: 'The requested {{name}} is not found',
INVALID_LOGIN: 'The login detail is incorrect',
REQUIRED_FIELD: 'Required field',
MIN_LENGTH_FIELD: 'Minimum field length:',
MAX_LENGTH_FIELD: 'Maximum field length:',
INVALID_FIELD: 'Field is not valid',
}
},
ECOMMERCE: {
COMMON: {
SELECTED_RECORDS_COUNT: 'Selected records count: ',
ALL: 'All',
SUSPENDED: 'Suspended',
ACTIVE: 'Active',
FILTER: 'Filter',
BY_STATUS: 'by Status',
BY_TYPE: 'by Type',
BUSINESS: 'Business',
INDIVIDUAL: 'Individual',
SEARCH: 'Search',
IN_ALL_FIELDS: 'in all fields'
},
ECOMMERCE: 'eCommerce',
CUSTOMERS: {
CUSTOMERS: 'Customers',
CUSTOMERS_LIST: 'Customers list',
NEW_CUSTOMER: 'New Customer',
DELETE_CUSTOMER_SIMPLE: {
TITLE: 'Customer Delete',
DESCRIPTION: 'Are you sure to permanently delete this customer?',
WAIT_DESCRIPTION: 'Customer is deleting...',
MESSAGE: 'Customer has been deleted'
},
DELETE_CUSTOMER_MULTY: {
TITLE: 'Customers Delete',
DESCRIPTION: 'Are you sure to permanently delete selected customers?',
WAIT_DESCRIPTION: 'Customers are deleting...',
MESSAGE: 'Selected customers have been deleted'
},
UPDATE_STATUS: {
TITLE: 'Status has been updated for selected customers',
MESSAGE: 'Selected customers status have successfully been updated'
},
EDIT: {
UPDATE_MESSAGE: 'Customer has been updated',
ADD_MESSAGE: 'Customer has been created'
}
}
}
}
};

View File

@@ -0,0 +1,105 @@
// Spain
export const locale = {
lang: 'es',
data: {
TRANSLATOR: {
SELECT: 'Elige tu idioma',
},
MENU: {
NEW: 'nuevo',
ACTIONS: 'Comportamiento',
CREATE_POST: 'Crear nueva publicación',
PAGES: 'Pages',
FEATURES: 'Caracteristicas',
APPS: 'Aplicaciones',
DASHBOARD: 'Tablero'
},
AUTH: {
GENERAL: {
OR: 'O',
SUBMIT_BUTTON: 'Enviar',
NO_ACCOUNT: 'No tienes una cuenta?',
SIGNUP_BUTTON: 'Regístrate',
FORGOT_BUTTON: 'Se te olvidó tu contraseña',
BACK_BUTTON: 'Espalda',
PRIVACY: 'Intimidad',
LEGAL: 'Legal',
CONTACT: 'Contacto',
},
LOGIN: {
TITLE: 'Crear una cuenta',
BUTTON: 'Registrarse',
},
FORGOT: {
TITLE: 'Contraseña olvidada?',
DESC: 'Ingrese su correo electrónico para restablecer su contraseña',
SUCCESS: 'Your account has been successfully reset.'
},
REGISTER: {
TITLE: 'Sign Up',
DESC: 'Enter your details to create your account',
SUCCESS: 'Your account has been successfuly registered.'
},
INPUT: {
EMAIL: 'Email',
FULLNAME: 'Fullname',
PASSWORD: 'Password',
CONFIRM_PASSWORD: 'Confirm Password',
USERNAME: 'Usuario'
},
VALIDATION: {
INVALID: '{{name}} is not valid',
REQUIRED: '{{name}} is required',
MIN_LENGTH: '{{name}} minimum length is {{min}}',
AGREEMENT_REQUIRED: 'Accepting terms & conditions are required',
NOT_FOUND: 'The requested {{name}} is not found',
INVALID_LOGIN: 'The login detail is incorrect',
REQUIRED_FIELD: 'Required field',
MIN_LENGTH_FIELD: 'Minimum field length:',
MAX_LENGTH_FIELD: 'Maximum field length:',
INVALID_FIELD: 'Field is not valid',
}
},
ECOMMERCE: {
COMMON: {
SELECTED_RECORDS_COUNT: 'Selected records count: ',
ALL: 'All',
SUSPENDED: 'Suspended',
ACTIVE: 'Active',
FILTER: 'Filter',
BY_STATUS: 'by Status',
BY_TYPE: 'by Type',
BUSINESS: 'Business',
INDIVIDUAL: 'Individual',
SEARCH: 'Search',
IN_ALL_FIELDS: 'in all fields'
},
ECOMMERCE: 'eCommerce',
CUSTOMERS: {
CUSTOMERS: 'Customers',
CUSTOMERS_LIST: 'Customers list',
NEW_CUSTOMER: 'New Customer',
DELETE_CUSTOMER_SIMPLE: {
TITLE: 'Customer Delete',
DESCRIPTION: 'Are you sure to permanently delete this customer?',
WAIT_DESCRIPTION: 'Customer is deleting...',
MESSAGE: 'Customer has been deleted'
},
DELETE_CUSTOMER_MULTY: {
TITLE: 'Customers Delete',
DESCRIPTION: 'Are you sure to permanently delete selected customers?',
WAIT_DESCRIPTION: 'Customers are deleting...',
MESSAGE: 'Selected customers have been deleted'
},
UPDATE_STATUS: {
TITLE: 'Status has been updated for selected customers',
MESSAGE: 'Selected customers status have successfully been updated'
},
EDIT: {
UPDATE_MESSAGE: 'Customer has been updated',
ADD_MESSAGE: 'Customer has been created'
}
}
}
}
};

View File

@@ -0,0 +1,105 @@
// France
export const locale = {
lang: 'fr',
data: {
TRANSLATOR: {
SELECT: 'choisissez votre langue',
},
MENU: {
NEW: 'Nouveau',
ACTIONS: 'Actes',
CREATE_POST: 'Créer un nouveau Post',
PAGES: 'Pages',
FEATURES: 'Fonctionnalités',
APPS: 'Applications',
DASHBOARD: 'Tableau de Bord',
},
AUTH: {
GENERAL: {
OR: 'Ou',
SUBMIT_BUTTON: 'Soumettre',
NO_ACCOUNT: 'Ne pas avoir de compte?',
SIGNUP_BUTTON: 'Registre',
FORGOT_BUTTON: 'Mot de passe oublié',
BACK_BUTTON: 'Back',
PRIVACY: 'Privacy',
LEGAL: 'Legal',
CONTACT: 'Contact',
},
LOGIN: {
TITLE: 'Créer un compte',
BUTTON: 'Sign In',
},
FORGOT: {
TITLE: 'Forgotten Password?',
DESC: 'Enter your email to reset your password',
SUCCESS: 'Your account has been successfully reset.'
},
REGISTER: {
TITLE: 'Sign Up',
DESC: 'Enter your details to create your account',
SUCCESS: 'Your account has been successfuly registered.'
},
INPUT: {
EMAIL: 'Email',
FULLNAME: 'Fullname',
PASSWORD: 'Mot de passe',
CONFIRM_PASSWORD: 'Confirm Password',
USERNAME: 'Nom d\'utilisateur'
},
VALIDATION: {
INVALID: '{{name}} n\'est pas valide',
REQUIRED: '{{name}} est requis',
MIN_LENGTH: '{{name}} minimum length is {{min}}',
AGREEMENT_REQUIRED: 'Accepting terms & conditions are required',
NOT_FOUND: 'The requested {{name}} is not found',
INVALID_LOGIN: 'The login detail is incorrect',
REQUIRED_FIELD: 'Required field',
MIN_LENGTH_FIELD: 'Minimum field length:',
MAX_LENGTH_FIELD: 'Maximum field length:',
INVALID_FIELD: 'Field is not valid',
}
},
ECOMMERCE: {
COMMON: {
SELECTED_RECORDS_COUNT: 'Nombre d\'enregistrements sélectionnés: ',
ALL: 'All',
SUSPENDED: 'Suspended',
ACTIVE: 'Active',
FILTER: 'Filter',
BY_STATUS: 'by Status',
BY_TYPE: 'by Type',
BUSINESS: 'Business',
INDIVIDUAL: 'Individual',
SEARCH: 'Search',
IN_ALL_FIELDS: 'in all fields'
},
ECOMMERCE: 'éCommerce',
CUSTOMERS: {
CUSTOMERS: 'Les clients',
CUSTOMERS_LIST: 'Liste des clients',
NEW_CUSTOMER: 'Nouveau client',
DELETE_CUSTOMER_SIMPLE: {
TITLE: 'Suppression du client',
DESCRIPTION: 'Êtes-vous sûr de supprimer définitivement ce client?',
WAIT_DESCRIPTION: 'Le client est en train de supprimer ...',
MESSAGE: 'Le client a été supprimé'
},
DELETE_CUSTOMER_MULTY: {
TITLE: 'Supprimer les clients',
DESCRIPTION: 'Êtes-vous sûr de supprimer définitivement les clients sélectionnés?',
WAIT_DESCRIPTION: 'Les clients suppriment ...',
MESSAGE: 'Les clients sélectionnés ont été supprimés'
},
UPDATE_STATUS: {
TITLE: 'Le statut a été mis à jour pour les clients sélectionnés',
MESSAGE: 'Le statut des clients sélectionnés a été mis à jour avec succès'
},
EDIT: {
UPDATE_MESSAGE: 'Le client a été mis à jour',
ADD_MESSAGE: 'Le client a été créé'
}
}
}
}
};

View File

@@ -0,0 +1,105 @@
// Japan
export const locale = {
lang: 'jp',
data: {
TRANSLATOR: {
SELECT: 'あなたが使う言語を選んでください',
},
MENU: {
NEW: '新しい',
ACTIONS: '行動',
CREATE_POST: '新しい投稿を作成',
PAGES: 'Pages',
FEATURES: '特徴',
APPS: 'アプリ',
DASHBOARD: 'ダッシュボード',
},
AUTH: {
GENERAL: {
OR: 'または',
SUBMIT_BUTTON: '提出する',
NO_ACCOUNT: 'アカウントを持っていない?',
SIGNUP_BUTTON: 'サインアップ',
FORGOT_BUTTON: 'パスワードをお忘れですか',
BACK_BUTTON: 'バック',
PRIVACY: 'プライバシー',
LEGAL: '法的',
CONTACT: '接触',
},
LOGIN: {
TITLE: 'Create Account',
BUTTON: 'Sign In',
},
FORGOT: {
TITLE: 'Forgotten Password?',
DESC: 'Enter your email to reset your password',
SUCCESS: 'Your account has been successfully reset.'
},
REGISTER: {
TITLE: 'Sign Up',
DESC: 'Enter your details to create your account',
SUCCESS: 'Your account has been successfuly registered.'
},
INPUT: {
EMAIL: 'Email',
FULLNAME: 'Fullname',
PASSWORD: 'Password',
CONFIRM_PASSWORD: 'Confirm Password',
USERNAME: 'ユーザー名'
},
VALIDATION: {
INVALID: '{{name}} is not valid',
REQUIRED: '{{name}} is required',
MIN_LENGTH: '{{name}} minimum length is {{min}}',
AGREEMENT_REQUIRED: 'Accepting terms & conditions are required',
NOT_FOUND: 'The requested {{name}} is not found',
INVALID_LOGIN: 'The login detail is incorrect',
REQUIRED_FIELD: 'Required field',
MIN_LENGTH_FIELD: 'Minimum field length:',
MAX_LENGTH_FIELD: 'Maximum field length:',
INVALID_FIELD: 'Field is not valid',
}
},
ECOMMERCE: {
COMMON: {
SELECTED_RECORDS_COUNT: 'Selected records count: ',
ALL: 'All',
SUSPENDED: 'Suspended',
ACTIVE: 'Active',
FILTER: 'Filter',
BY_STATUS: 'by Status',
BY_TYPE: 'by Type',
BUSINESS: 'Business',
INDIVIDUAL: 'Individual',
SEARCH: 'Search',
IN_ALL_FIELDS: 'in all fields'
},
ECOMMERCE: 'eCommerce',
CUSTOMERS: {
CUSTOMERS: 'Customers',
CUSTOMERS_LIST: 'Customers list',
NEW_CUSTOMER: 'New Customer',
DELETE_CUSTOMER_SIMPLE: {
TITLE: 'Customer Delete',
DESCRIPTION: 'Are you sure to permanently delete this customer?',
WAIT_DESCRIPTION: 'Customer is deleting...',
MESSAGE: 'Customer has been deleted'
},
DELETE_CUSTOMER_MULTY: {
TITLE: 'Customers Delete',
DESCRIPTION: 'Are you sure to permanently delete selected customers?',
WAIT_DESCRIPTION: 'Customers are deleting...',
MESSAGE: 'Selected customers have been deleted'
},
UPDATE_STATUS: {
TITLE: 'Status has been updated for selected customers',
MESSAGE: 'Selected customers status have successfully been updated'
},
EDIT: {
UPDATE_MESSAGE: 'Customer has been updated',
ADD_MESSAGE: 'Customer has been created'
}
}
}
}
};

View File

@@ -0,0 +1,133 @@
import {LayoutConfigModel} from '../_base/layout';
export class LayoutConfig {
public defaults: LayoutConfigModel = {
demo: 'demo6',
// == Base Layout
self: {
layout: 'fluid', // fluid|boxed
body: {
'background-image': './assets/media/misc/bg-1.jpg',
},
logo: './assets/media/logos/logo-6.png',
},
// == Page Splash Screen loading
loader: {
enabled: true,
type: 'spinner-logo',
logo: './assets/media/logos/logo-mini-md.png',
message: 'Please wait...',
},
// == Colors for javascript
colors: {
state: {
brand: '#22b9ff',
light: '#19385E',
dark: '#282a3c',
primary: '#5867dd',
success: '#34bfa3',
info: '#36a3f7',
warning: '#ffb822',
danger: '#fd3995',
},
base: {
label: [
'#c5cbe3',
'#a1a8c3',
'#3d4465',
'#3e4466',
],
shape: [
'#f0f3ff',
'#d9dffa',
'#afb4d4',
'#646c9a',
],
},
},
header: {
self: {
skin: 'light',
fixed: {
desktop: true,
mobile: true,
},
},
menu: {
self: {
display: true,
layout: 'default',
'root-arrow': true,
},
desktop: {
arrow: true,
toggle: 'click',
submenu: {
skin: 'light',
arrow: true,
},
},
mobile: {
submenu: {
skin: 'dark',
accordion: true,
},
},
},
},
subheader: {
display: true,
layout: 'subheader-v1',
fixed: true,
width: 'fluid',
style: 'solid',
},
content: {
width: 'fluid',
},
brand: {
self: {
skin: 'narvy',
},
},
aside: {
self: {
display: true,
fixed: true,
minimize: {
toggle: false,
default: true,
},
},
footer: {
self: {
display: true,
},
},
menu: {
dropdown: false,
scroll: false,
submenu: {
accordion: false,
dropdown: {
arrow: true,
'hover-timeout': 500,
},
},
},
},
footer: {
self: {
width: 'fluid',
fixed: false,
},
},
};
/**
* Good place for getting the remote config
*/
public get configs(): LayoutConfigModel {
return this.defaults;
}
}

View File

@@ -0,0 +1,197 @@
export class MenuConfig {
public defaults: any = {
aside: {
self: {},
items: [
{
title: 'Inicio',
root: true,
icon: 'flaticon2-architecture-and-city',
page: '/inicio',
bullet: 'dot',
},
{section: 'Catálogos'},
{
title: 'Catálogos',
root: true,
icon: 'fa fa-book',
permissions: [
'administrador.usuarios.index',
'administrador.roles.index',
'administrador.clientes.index',
'administrador.estatus_servicios.index',
'administrador.formas_pagos.index',
'administrador.tipos_servicios.index',
'administrador.servicios.index',
'administrador.tipos_vehiculos.index',
'administrador.vehiculos.index',
'administrador.sucursales.index'
],
submenu: [
{
title: 'Usuarios',
icon: 'fa fa-user',
page: '/catalogos/usuarios',
permissions: ['administrador.usuarios.index'],
},
{
title: 'Roles',
icon: 'fa fa-users',
page: '/catalogos/roles',
permissions: ['administrador.roles.index'],
},
{
title: 'Clientes',
icon: 'fa fa-handshake-o',
page: '/catalogos/clientes',
permissions: ['administrador.clientes.index'],
},
{
title: 'Estados de servicio',
icon: 'fa fa-signal',
page: '/catalogos/estados-servicio',
permissions: ['administrador.estatus_servicios.index'],
},
{
title: 'Formas de pago',
icon: 'fa fa-money',
page: '/catalogos/formas-pago',
permissions: ['administrador.formas_pagos.index'],
},
{
title: 'Tipos de servicio',
icon: 'fa fa-building',
page: '/catalogos/tipos-servicio',
permissions: ['administrador.tipos_servicios.index'],
},
{
title: 'Servicios',
icon: 'fa fa-cogs',
page: '/catalogos/servicios',
permissions: ['administrador.servicios.index'],
},
{
title: 'Tipos de vehículo',
icon: 'fa fa-truck',
page: '/catalogos/tipos-vehiculo',
permissions: ['administrador.tipos_vehiculos.index'],
},
{
title: 'Vehículos',
icon: 'fa fa-car',
page: '/catalogos/vehiculos',
permissions: ['administrador.vehiculos.index'],
},
{
title: 'Sucursales',
icon: 'fa fa-home',
page: '/catalogos/sucursales',
permissions: ['administrador.sucursales.index'],
},
]
},
{section: 'Servicios'},
{
title: 'Servicios',
root: true,
icon: 'fas fa-phone',
permissions: ['atencion_clientes.solicitud_servicios.index'],
submenu: [
{
title: 'Solicitudes de servicios',
icon: 'far fa-calendar',
page: '/servicios/solicitudes-servicio',
permissions: ['atencion_clientes.solicitud_servicios.index']
}
]
},
{
title: 'Agenda',
root: true,
icon: 'flaticon2-calendar',
page: '/agenda',
bullet: 'dot',
permissions: ['atencion_clientes.servicios.agenda'],
},
{section: 'Reportes'},
{
title: 'Reportes',
root: true,
icon: 'fa fa-line-chart',
permissions: [
'reportes.servicios',
'reportes.productividad.atencionclientes',
'reportes.servicios.asesores',
'reportes.servicios.clientes',
'reportes.servicios.capacidad',
'reportes.servicios.semanal',
'reportes.servicios.mensual'
],
submenu: [
{
title: 'Servicios',
icon: 'fa fa-file-excel-o',
page: '/reportes/servicios',
permissions: ['reportes.servicios']
},
{
title: 'Productividad atención a clientes',
icon: 'fa fa-file-excel-o',
page: '/reportes/productividad',
permissions: ['reportes.productividad.atencionclientes']
},
{
title: 'Servicios otorgados por asesor de operaciones',
icon: 'fa fa-file-excel-o',
page: '/reportes/servicios-asesor',
permissions: ['reportes.servicios.asesores']
},
{
title: 'Servicios por cliente',
icon: 'fa fa-file-excel-o',
page: '/reportes/servicios-cliente',
permissions: ['reportes.servicios.clientes']
},
{
title: 'Capacidad aprovechada',
icon: 'fa fa-file-excel-o',
page: '/reportes/capacidad-aprovechada',
permissions: ['reportes.servicios.capacidad']
},
{
title: 'Semanal',
icon: 'fa fa-file-excel-o',
page: '/reportes/semanal',
permissions: ['reportes.servicios.semanal']
},
{
title: 'Mensual',
icon: 'fa fa-file-excel-o',
page: '/reportes/mensual',
permissions: ['reportes.servicios.mensual']
}
]
},
{section: 'Encuesta'},
{
title: 'Encuesta',
root: true,
icon: 'fa fa-bar-chart-o',
permissions: ['reportes.servicios.encuesta'],
submenu: [
{
title: 'Encuesta General',
icon: 'glyphicon glyphicon-list-alt',
page: '/encuesta/encuesta-general',
permissions: ['reportes.servicios.encuesta']
}
]
},
]
},
};
public get configs(): any {
return this.defaults;
}
}

View File

@@ -0,0 +1,43 @@
export class PageConfig {
public defaults: any = {
dashboard: {
page: {
title: 'Dashboard',
desc: 'Latest updates and statistic charts'
},
},
catalogos: {
'usuarios': {page: {title: 'Usuarios', desc: ''}},
'productos': {page: {title: 'Productos', desc: ''}}
},
builder: {
page: {title: 'Layout Builder', desc: ''}
},
header: {
actions: {
page: {title: 'Actions', desc: 'Actions example page'}
}
},
profile: {
page: {title: 'User Profile', desc: ''}
},
error: {
404: {
page: {title: '404 Not Found', desc: '', subheader: false}
},
403: {
page: {title: '403 Access Forbidden', desc: '', subheader: false}
}
},
wizard: {
'wizard-1': {page: {title: 'Wizard 1', desc: ''}},
'wizard-2': {page: {title: 'Wizard 2', desc: ''}},
'wizard-3': {page: {title: 'Wizard 3', desc: ''}},
'wizard-4': {page: {title: 'Wizard 4', desc: ''}},
},
};
public get configs(): any {
return this.defaults;
}
}

View File

@@ -0,0 +1,192 @@
import {AbstractControl, FormControl, ValidatorFn} from '@angular/forms';
export class CustomValidators {
static isNullOrUndefined<T>(obj: T | null | undefined): obj is null | undefined {
return typeof obj === 'undefined' || obj === null;
}
static email2(control: FormControl): any {
const exp: any = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'email': true, 'currentValue': control.value};
}
return null;
}
static password(control: FormControl): any {
const exp: any = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*)[A-Za-z0-9\d$]{8,16}$/;
if (!(typeof control.value === 'undefined' || control.value === null) && control.value !== "" && !exp.test(control.value)) {
return {'password': true, 'currentValue': control.value};
}
return null;
}
static fechamx(control: FormControl): any {
// let exp: any = /^(d{1,2})[/](d{1,2})[/](d{2}|d{4})$/; // Basic validation
const exp: any = /^(?:(0[1-9]|[12][0-9]|3[01])[\/.](0[1-9]|1[012])[\/.](19|20)[0-9]{2})$/;
return exp.test(control.value) ? null : {
fechamx: {
valid: true
}
};
}
static fechaus(control: FormControl): any {
// let exp: any = /^(d{1,2})[/](d{1,2})[/](d{2}|d{4})$/; // Basic validation
const exp: any = /^(?:(19|20)[0-9]{2}[\-.](0[1-9]|1[012])[\-.](0[1-9]|[12][0-9]|3[01]))$/;
return exp.test(control.value) ? null : {
fechaus: {
valid: true
}
};
}
static telefono2(control: FormControl): any {
const exp: any = /^\+\d{2,3}\s\d{10}$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'telefono': true, 'currentValue': control.value};
}
return null;
}
static formatoNumero(control: FormControl): any {
const exp: any = /^[0-9]+$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'formatoNumero': true, 'currentValue': control.value};
}
return null;
}
static formatoDecimal(control: FormControl): any {
const exp: any = /^(\d+\.?\d{0,9}|\.\d{1,9})$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'formatoDecimal': true, 'currentValue': control.value};
}
return null;
}
static formatoHora(control: FormControl): any {
const exp: any = /^(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'formatoHora': true, 'currentValue': control.value};
}
return null;
}
static email(control: FormControl): any {
const exp: any = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'email': true, 'currentValue': control.value};
}
return null;
}
static urlYoutube(control: FormControl): any {
const exp: any = /^(https:\/\/(www.)?youtube.com\/embed\/)/;
if (control.value && !exp.test(control.value)) {
return {'deUrl': true, 'currentValue': control.value};
}
return null;
}
static urlSpotify(control: FormControl): any {
const exp: any = /^(https:\/\/open.spotify.com\/embed\/)/;
if (control.value && !exp.test(control.value)) {
return {'deUrl': true, 'currentValue': control.value};
}
return null;
}
static telefono(control: FormControl): any {
const exp: any = /^\+\d{2,3}\s\d{10}$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'telefono': true, 'currentValue': control.value};
}
return null;
}
static numeric(control: FormControl): any {
const exp: any = /^[0-9]+$/;
if (control.value && !exp.test(control.value)) {
return {'numeric': true, 'currentValue': control.value};
}
return null;
}
static deUrl(control: FormControl): any {
const exp: any = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'deUrl': true, 'currentValue': control.value};
}
return null;
}
static deUrlImagen(control: FormControl): any {
const exp: any = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'deUrlImagen': true, 'currentValue': control.value};
}
return null;
}
// VALIDA QUE LA CONTRASEÑA NO CONTENGA ESPACIOS
static noWhiteSpace(control: FormControl): any {
const exp: any = /^\S+$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'nowhitespace': true, 'currentValue': control.value};
}
return null;
}
// VALIDA QUE EL USUARIO NO CONTENGA ESPACIOS
static nbUsuario(control: FormControl): any {
const exp: any = /^\S+$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'nbUsuario': true, 'currentValue': control.value};
}
return null;
}
// VALIDA QUE LA CONTRASEÑA NO CONTENGA ESPACIOS
static dePassword(control: FormControl): any {
const exp: any = /^\S+$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'dePassword': true, 'currentValue': control.value};
}
return null;
}
static precio(control: FormControl): any {
const exp: any = /^([0-9])*[.]?[0-9]*$/;
if (control.value !== undefined && !exp.test(control.value)) {
return {'precio': true, 'currentValue': control.value};
}
return null;
}
static preciomayor0(control: FormControl): any {
const exp: any = /^([0-9])*[.]?[0-9]*$/;
if (parseInt(control.value) == 0) {
return {'precio': true, 'currentValue': control.value};
}
if (control.value !== undefined && !exp.test(control.value) && parseInt(control.value) == 0) {
return {'precio': true, 'currentValue': control.value};
}
return null;
}
static minValue(min: Number): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => {
const input = control.value,
isValid = input < min;
if (isValid) {
return {'minValue': {min}};
} else {
return null;
}
};
}
}

View File

@@ -0,0 +1,47 @@
import {EventEmitter, Output} from '@angular/core';
import {HttpErrorResponse} from '@angular/common/http';
import {Observable, throwError} from 'rxjs';
export class ApiBase {
@Output() errorEvent: EventEmitter<any> = new EventEmitter(true);
protected handleError<T>(operation = 'operation', result?: T) {
return (error: HttpErrorResponse): Observable<T> => {
console.log("HANDLE ERROR", error.status)
// console.error(`${operation} failed henry: ${error.message}`);
let msg: any = [];
let status = error.status;
//if(!error.status){
// return throwError(null);
//}
if (error.status === 401) {
msg[0] = 'Usuario y/o contraseña incorrecta, intente de nuevo';
//status = error.status;
} else if (error.status === 403) {
msg[0] = 'No cuentas con los suficientes permisos.';
//status = error.status;
} else if (error.status === 422) {
let i = 0;
for (let e of error.error.errors) {
msg[i] = e;
i++;
}
//status = 'warning-snackbar';
} else if (error.status === 404) {
msg[0] = 'La ruta no se encuentra disponible.';
//status = 'error-snackbar';
} else {
msg[0] = 'Algo salió mal, intente de nuevo';
//status = 'error-snackbar';
}
let info = {msg: msg, status: status, error: error.error.error};
console.log('MENSAJE=> ', info);
this.errorEvent.emit(info);
return throwError(msg);
};
}
}

View File

@@ -0,0 +1,80 @@
import {EventEmitter, Injectable, Output} from '@angular/core';
import {AuthService} from '../auth/_services';
import {TokenStorage} from '../auth/_services/token-storage.service';
import {UsuariosService} from '../../views/pages/catalogos/usuarios/usuarios.service';
import {RolesService} from '../../views/pages/catalogos/roles/roles.service';
import {MatSnackBar} from "@angular/material/snack-bar";
import {EstadosServicioService} from '../../views/pages/catalogos/estados-servicio/estados-servicio.service';
import {FormasPagoService} from '../../views/pages/catalogos/formas-pago/formas-pago.service';
import {TiposServicioService} from '../../views/pages/catalogos/tipos-servicio/tipos-servicio.service';
import {ServiciosService} from '../../views/pages/catalogos/servicios/servicios.service';
import {TiposVehiculoService} from '../../views/pages/catalogos/tipos-vehiculo/tipos-vehiculo.service';
import {VehiculosService} from '../../views/pages/catalogos/vehiculos/vehiculos.service';
import {SucursalesService} from '../../views/pages/catalogos/sucursales/sucursales.service';
import {ClientesService} from '../../views/pages/catalogos/clientes/clientes.service';
import { SolicitudesServicioService } from '../../views/pages/servicios/solicitudes-servicio/solicitudes-servicio.service';
import { ReportesService } from '../../views/pages/reportes/reportes.service';
import {ClientesDomiciliosService} from '../../views/pages/catalogos/clientes/clientes-domicilios.service';
import {AgendaService} from "../../views/pages/agenda/agenda.service";
@Injectable({
providedIn: 'root'
})
export class ApiService {
@Output() error: EventEmitter<any> = new EventEmitter(true);
constructor(
public login: AuthService,
public usuarios: UsuariosService,
public roles: RolesService,
public estadosServicio: EstadosServicioService,
public formasPago: FormasPagoService,
public tiposServicio: TiposServicioService,
public servicios: ServiciosService,
public tiposVehiculos: TiposVehiculoService,
public vehiculos: VehiculosService,
public sucursales: SucursalesService,
public clientes: ClientesService,
public clientesDomicilios: ClientesDomiciliosService,
public solicitudesServicioService: SolicitudesServicioService,
public reportesService: ReportesService,
public tokenStorage: TokenStorage,
public snackBar: MatSnackBar,
public agenda: AgendaService
) {
this.login.errorEvent.subscribe((res: any) => this.error.emit(res));
this.usuarios.errorEvent.subscribe((res: any) => this.error.emit(res));
this.roles.errorEvent.subscribe((res: any) => this.error.emit(res));
this.estadosServicio.errorEvent.subscribe((res: any) => this.error.emit(res));
this.formasPago.errorEvent.subscribe((res: any) => this.error.emit(res));
this.tiposServicio.errorEvent.subscribe((res: any) => this.error.emit(res));
this.servicios.errorEvent.subscribe((res: any) => this.error.emit(res));
this.tiposVehiculos.errorEvent.subscribe((res: any) => this.error.emit(res));
this.vehiculos.errorEvent.subscribe((res: any) => this.error.emit(res));
this.sucursales.errorEvent.subscribe((res: any) => this.error.emit(res));
this.clientes.errorEvent.subscribe((res: any) => this.error.emit(res));
this.clientesDomicilios.errorEvent.subscribe((res: any) => this.error.emit(res));
this.solicitudesServicioService.errorEvent.subscribe((res: any) => this.error.emit(res));
this.reportesService.errorEvent.subscribe((res: any) => this.error.emit(res));
this.agenda.errorEvent.subscribe((res: any) => this.error.emit(res));
}
/**
* Get access token
* @description Should return access token in Observable from e.g. localStorage
* @returns {Observable<string>}
*/
public getAccessToken(): string {
return this.tokenStorage.getAccessToken();
}
public snackbar(msg: string, duration: number = 3000) {
this.snackBar.open(msg, '', {
duration: duration,
verticalPosition: 'bottom',
horizontalPosition: 'right',
});
}
}

View File

@@ -0,0 +1,37 @@
import {Component, Input} from '@angular/core';
@Component({
styles : [`
.my-mat-hint {
line-height: 1.2em;
transform: translateY(30px);
font-size: 75%;
}
`],
selector : 'app-feedback-validator-reset',
template :
`<div *ngIf="submitted && errors">
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.required">El campo {{attr}} es requerido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.minlength">El {{attr}} debe ser más grande que {{errors.minlength.requiredLength}} caracteres.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.nowhitespace">El campo {{attr}} tiene caracteres de espacio en blanco no válido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.size">El campo {{attr}} debe tener {{errors.size.requiredSize}} caracteres.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.numeric">El campo {{attr}} debe ser numérico.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.email">El campo {{attr}} debe ser un correo electrónico válido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.asyncproductvalidator">El campo seleccionado {{attr}} es inválido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.asyncepcvalidator">El campo seleccionado {{attr}} is inválido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.decimal">El campo seleccionado {{attr}} es inválido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.max">El número máximo de caracteres para el campo {{attr}} es {{errors.max.length}}.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.min">El número mínimo de caracteres para el campo {{attr}} es {{errors.min.length}}.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.formatoHora">La hora debe ser formato 24 horas (HH:MM:SS).</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.telefono">El campo {{attr}} debe tener 10 números.</div>
<!--<pre>{{errors | json}}</pre>-->
</div>`
})
export class FeedbackValidatorsResetComponent {
@Input() submitted;
@Input() errors;
@Input() attr = 'Field';
constructor() {}
}

View File

@@ -0,0 +1,37 @@
import {Component, Input} from '@angular/core';
@Component({
styles : [`
.my-mat-hint {
line-height: 1.2em;
transform: translateY(30px);
font-size: 75%;
}
`],
selector : 'app-feedback-validator',
template :
`<div *ngIf="submitted && errors">
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.required">El campo {{attr}} es requerido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.minlength">El {{attr}} debe ser más grande que {{errors.minlength.requiredLength}} caracteres.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.nowhitespace">El campo {{attr}} tiene caracteres de espacio en blanco no válido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.size">El campo {{attr}} debe tener {{errors.size.requiredSize}} caracteres.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.numeric">El campo {{attr}} debe ser numérico.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.email">El campo {{attr}} debe ser un correo electrónico válido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.asyncproductvalidator">El campo seleccionado {{attr}} es inválido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.asyncepcvalidator">El campo seleccionado {{attr}} is inválido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.decimal">El campo seleccionado {{attr}} es inválido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.max">El número máximo de caracteres para el campo {{attr}} es {{errors.max.length}}.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.min">El número mínimo de caracteres para el campo {{attr}} es {{errors.min.length}}.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.formatoHora">La hora debe ser formato 24 horas (HH:MM:SS).</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.telefono">El campo {{attr}} debe tener 10 números.</div>
<!--<pre>{{errors | json}}</pre>-->
</div>`
})
export class FeedbackValidatorsComponent {
@Input() submitted;
@Input() errors;
@Input() attr = 'Field';
constructor() {}
}

View File

@@ -0,0 +1,9 @@
/** Error when invalid control is dirty or touched*/
import {ErrorStateMatcher} from '@angular/material';
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
return !!(control && control.invalid && (control.dirty || control.touched));
}
}

View File

@@ -0,0 +1,66 @@
import {EventEmitter, Output} from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
import {Observable} from 'rxjs/index';
import {isNullOrUndefined} from 'util';
import {catchError} from 'rxjs/internal/operators';
import {ApiBase} from './api-base';
import {environment} from '../../../environments/environment';
export class ResourceBase extends ApiBase {
baseUrl = environment.API;
private url = '';
constructor(public http: HttpClient, private urlReceive: string) {
super();
this.url = this.baseUrl + urlReceive;
}
@Output() errorEvent: EventEmitter<any> = new EventEmitter(true);
public index(page?: string, searchText?: string, sortBy?: string, order?: string, perPage?: string, visible?: string, deleted_at?: string): Observable<any> {
const params = new HttpParams()
.set('sortBy', isNullOrUndefined(sortBy) ? '' : sortBy)
.set('order', isNullOrUndefined(order) ? '' : order)
.set('page', isNullOrUndefined(page) ? '1' : page)
.set('perPage', isNullOrUndefined(perPage) ? '10' : perPage)
.set('visible', isNullOrUndefined(visible) ? '1' : visible)
.set('deleted', isNullOrUndefined(deleted_at) ? '0' : deleted_at)
.set('query', searchText ? searchText : '');
const options = {params: params};
return this.http.get(this.url, options).pipe(
catchError(this.handleError('index'))
);
}
public create(body: any): Observable<any> {
return this.http.post(this.url, body).pipe(
catchError(this.handleError('create'))
);
}
public delete(id: number, reactivate = false): Observable<any> {
let deleted = reactivate ? '?deleted=1' : '';
return this.http.delete(`${this.url}/${id}`+ deleted).pipe(
catchError(this.handleError('delete'))
);
}
public show(id: number): Observable<any> {
return this.http.get(`${this.url}/${id}`).pipe(
catchError(this.handleError('show'))
);
}
public update(id: number, body: any): Observable<any> {
return this.http.put(`${this.url}/${id}`, body).pipe(
catchError(this.handleError('update'))
);
}
}

View File

@@ -0,0 +1,25 @@
import {Injectable} from '@angular/core';
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {Observable} from 'rxjs/index';
import {ApiService} from "./api.service";
@Injectable({
providedIn: 'root'
})
export class UrlInterceptorService implements HttpInterceptor {
constructor(private api: ApiService) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const authToken: string = this.api.getAccessToken();
const authReq = req.clone({
headers: req.headers.set('Authorization', 'Bearer ' + authToken)
.set('Application', 'WEB'),
url: `${req.url}`
});
return next.handle(authReq);
}
}

View File

@@ -0,0 +1,40 @@
import {Component, Input} from '@angular/core';
@Component({
styles : [`
.my-mat-hint {
line-height: 1.2em;
transform: translateY(30px);
font-size: 75%;
}
`],
selector : 'app-validator',
template :
`<div *ngIf="submitted && errors">
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.required">El campo {{attr}} es requerido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.minlength">El {{attr}} debe ser más grande que {{errors.minlength.requiredLength}} caracteres.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.nowhitespace">El campo {{attr}} tiene caracteres de espacio en blanco no válido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.size">El campo {{attr}} debe tener {{errors.size.requiredSize}} caracteres.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.numeric">El campo {{attr}} debe ser numérico.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.email">El campo {{attr}} debe ser un correo electrónico válido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.asyncproductvalidator">El campo seleccionado {{attr}} es inválido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.asyncepcvalidator">El campo seleccionado {{attr}} is inválido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.decimal">El campo seleccionado {{attr}} es inválido.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.max">El número máximo de caracteres para el campo {{attr}} es {{errors.max.length}}.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.min">El número mínimo de caracteres para el campo {{attr}} es {{errors.min.length}}.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.formatoHora">La hora debe ser formato 24 horas (HH:MM:SS).</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.telefono">El campo {{attr}} debe tener 10 números.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.deUrl">El campo {{attr}} debe ser una url correcta.</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.urlYoutube">El campo {{attr}} debe ser una url de Youtube correcta..</div>
<div class="text-danger margin-bottom-thin my-mat-hint" *ngIf="errors.precio">El campo {{attr}} debe ser un precio correcto.</div>
<!--<pre>{{errors | json}}</pre>-->
</div>`
})
export class ValidatorsComponent {
@Input() submitted;
@Input() errors;
@Input() attr = 'Field';
constructor() {}
}

View File

@@ -0,0 +1,38 @@
import { Action } from '@ngrx/store';
import { User } from '../_models/user.model';
export enum AuthActionTypes {
Login = '[Login] Action',
Logout = '[Logout] Action',
Register = '[Register] Action',
UserRequested = '[Request User] Action',
UserLoaded = '[Load User] Auth API'
}
export class Login implements Action {
readonly type = AuthActionTypes.Login;
constructor(public payload: { authToken: string }) { }
}
export class Logout implements Action {
readonly type = AuthActionTypes.Logout;
}
export class Register implements Action {
readonly type = AuthActionTypes.Register;
constructor(public payload: { authToken: string }) { }
}
export class UserRequested implements Action {
readonly type = AuthActionTypes.UserRequested;
}
export class UserLoaded implements Action {
readonly type = AuthActionTypes.UserLoaded;
constructor(public payload: { user: User }) { }
}
export type AuthActions = Login | Logout | Register | UserRequested | UserLoaded;

View File

@@ -0,0 +1,20 @@
// NGRX
import { Action } from '@ngrx/store';
// Models
import { Permission } from '../_models/permission.model';
export enum PermissionActionTypes {
AllPermissionsRequested = '[Init] All Permissions Requested',
AllPermissionsLoaded = '[Init] All Permissions Loaded'
}
export class AllPermissionsRequested implements Action {
readonly type = PermissionActionTypes.AllPermissionsRequested;
}
export class AllPermissionsLoaded implements Action {
readonly type = PermissionActionTypes.AllPermissionsLoaded;
constructor(public payload: { permissions: Permission[] }) { }
}
export type PermissionActions = AllPermissionsRequested | AllPermissionsLoaded;

View File

@@ -0,0 +1,89 @@
// NGRX
import { Action } from '@ngrx/store';
import { Update } from '@ngrx/entity';
// CRUD
import { QueryParamsModel } from '../../_base/crud';
// Models
import { Role } from '../_models/role.model';
export enum RoleActionTypes {
AllRolesRequested = '[Roles Home Page] All Roles Requested',
AllRolesLoaded = '[Roles API] All Roles Loaded',
RoleOnServerCreated = '[Edit Role Dialog] Role On Server Created',
RoleCreated = '[Edit Roles Dialog] Roles Created',
RoleUpdated = '[Edit Role Dialog] Role Updated',
RoleDeleted = '[Roles List Page] Role Deleted',
RolesPageRequested = '[Roles List Page] Roles Page Requested',
RolesPageLoaded = '[Roles API] Roles Page Loaded',
RolesPageCancelled = '[Roles API] Roles Page Cancelled',
RolesPageToggleLoading = '[Roles page] Roles Page Toggle Loading',
RolesActionToggleLoading = '[Roles] Roles Action Toggle Loading'
}
export class RoleOnServerCreated implements Action {
readonly type = RoleActionTypes.RoleOnServerCreated;
constructor(public payload: { role: Role }) { }
}
export class RoleCreated implements Action {
readonly type = RoleActionTypes.RoleCreated;
constructor(public payload: { role: Role }) { }
}
export class RoleUpdated implements Action {
readonly type = RoleActionTypes.RoleUpdated;
constructor(public payload: {
partialrole: Update<Role>,
role: Role
}) { }
}
export class RoleDeleted implements Action {
readonly type = RoleActionTypes.RoleDeleted;
constructor(public payload: { id: number }) {}
}
export class RolesPageRequested implements Action {
readonly type = RoleActionTypes.RolesPageRequested;
constructor(public payload: { page: QueryParamsModel }) { }
}
export class RolesPageLoaded implements Action {
readonly type = RoleActionTypes.RolesPageLoaded;
constructor(public payload: { roles: Role[], totalCount: number, page: QueryParamsModel }) { }
}
export class RolesPageCancelled implements Action {
readonly type = RoleActionTypes.RolesPageCancelled;
}
export class AllRolesRequested implements Action {
readonly type = RoleActionTypes.AllRolesRequested;
}
export class AllRolesLoaded implements Action {
readonly type = RoleActionTypes.AllRolesLoaded;
constructor(public payload: { roles: Role[] }) { }
}
export class RolesPageToggleLoading implements Action {
readonly type = RoleActionTypes.RolesPageToggleLoading;
constructor(public payload: { isLoading: boolean }) { }
}
export class RolesActionToggleLoading implements Action {
readonly type = RoleActionTypes.RolesActionToggleLoading;
constructor(public payload: { isLoading: boolean }) { }
}
export type RoleActions = RoleCreated
| RoleUpdated
| RoleDeleted
| RolesPageRequested
| RolesPageLoaded
| RolesPageCancelled
| AllRolesLoaded
| AllRolesRequested
| RoleOnServerCreated
| RolesPageToggleLoading
| RolesActionToggleLoading;

View File

@@ -0,0 +1,80 @@
// NGRX
import { Action } from '@ngrx/store';
import { Update } from '@ngrx/entity';
// CRUD
import { User } from '../_models/user.model';
// Models
import { QueryParamsModel } from '../../_base/crud';
export enum UserActionTypes {
AllUsersRequested = '[Users Module] All Users Requested',
AllUsersLoaded = '[Users API] All Users Loaded',
UserOnServerCreated = '[Edit User Component] User On Server Created',
UserCreated = '[Edit User Dialog] User Created',
UserUpdated = '[Edit User Dialog] User Updated',
UserDeleted = '[Users List Page] User Deleted',
UsersPageRequested = '[Users List Page] Users Page Requested',
UsersPageLoaded = '[Users API] Users Page Loaded',
UsersPageCancelled = '[Users API] Users Page Cancelled',
UsersPageToggleLoading = '[Users] Users Page Toggle Loading',
UsersActionToggleLoading = '[Users] Users Action Toggle Loading'
}
export class UserOnServerCreated implements Action {
readonly type = UserActionTypes.UserOnServerCreated;
constructor(public payload: { user: User }) { }
}
export class UserCreated implements Action {
readonly type = UserActionTypes.UserCreated;
constructor(public payload: { user: User }) { }
}
export class UserUpdated implements Action {
readonly type = UserActionTypes.UserUpdated;
constructor(public payload: {
partialUser: Update<User>,
user: User
}) { }
}
export class UserDeleted implements Action {
readonly type = UserActionTypes.UserDeleted;
constructor(public payload: { id: number }) {}
}
export class UsersPageRequested implements Action {
readonly type = UserActionTypes.UsersPageRequested;
constructor(public payload: { page: QueryParamsModel }) { }
}
export class UsersPageLoaded implements Action {
readonly type = UserActionTypes.UsersPageLoaded;
constructor(public payload: { users: User[], totalCount: number, page: QueryParamsModel }) { }
}
export class UsersPageCancelled implements Action {
readonly type = UserActionTypes.UsersPageCancelled;
}
export class UsersPageToggleLoading implements Action {
readonly type = UserActionTypes.UsersPageToggleLoading;
constructor(public payload: { isLoading: boolean }) { }
}
export class UsersActionToggleLoading implements Action {
readonly type = UserActionTypes.UsersActionToggleLoading;
constructor(public payload: { isLoading: boolean }) { }
}
export type UserActions = UserCreated
| UserUpdated
| UserDeleted
| UserOnServerCreated
| UsersPageLoaded
| UsersPageCancelled
| UsersPageToggleLoading
| UsersPageRequested
| UsersActionToggleLoading;

View File

@@ -0,0 +1,33 @@
// RxJS
import { of } from 'rxjs';
import { catchError, finalize, tap, debounceTime, delay, distinctUntilChanged } from 'rxjs/operators';
// NGRX
import { Store, select } from '@ngrx/store';
// CRUD
import { BaseDataSource, QueryResultsModel } from '../../_base/crud';
// State
import { AppState } from '../../../core/reducers';
// Selectirs
import { selectQueryResult, selectRolesPageLoading, selectRolesShowInitWaitingMessage } from '../_selectors/role.selectors';
export class RolesDataSource extends BaseDataSource {
constructor(private store: Store<AppState>) {
super();
this.loading$ = this.store.pipe(
select(selectRolesPageLoading)
);
this.isPreloadTextViewed$ = this.store.pipe(
select(selectRolesShowInitWaitingMessage)
);
this.store.pipe(
select(selectQueryResult)
).subscribe((response: QueryResultsModel) => {
this.paginatorTotalSubject.next(response.totalCount);
this.entitySubject.next(response.items);
});
}
}

View File

@@ -0,0 +1,32 @@
// RxJS
import { of } from 'rxjs';
import { catchError, finalize, tap, debounceTime, delay, distinctUntilChanged } from 'rxjs/operators';
// NGRX
import { Store, select } from '@ngrx/store';
// CRUD
import { BaseDataSource, QueryResultsModel } from '../../_base/crud';
// State
import { AppState } from '../../../core/reducers';
import { selectUsersInStore, selectUsersPageLoading, selectUsersShowInitWaitingMessage } from '../_selectors/user.selectors';
export class UsersDataSource extends BaseDataSource {
constructor(private store: Store<AppState>) {
super();
this.loading$ = this.store.pipe(
select(selectUsersPageLoading)
);
this.isPreloadTextViewed$ = this.store.pipe(
select(selectUsersShowInitWaitingMessage)
);
this.store.pipe(
select(selectUsersInStore)
).subscribe((response: QueryResultsModel) => {
this.paginatorTotalSubject.next(response.totalCount);
this.entitySubject.next(response.items);
});
}
}

View File

@@ -0,0 +1,84 @@
// Angular
import { Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
// RxJS
import { filter, mergeMap, tap, withLatestFrom } from 'rxjs/operators';
import { defer, Observable, of } from 'rxjs';
// NGRX
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Action, select, Store } from '@ngrx/store';
// Auth actions
import { AuthActionTypes, Login, Logout, Register, UserLoaded, UserRequested } from '../_actions/auth.actions';
import { AuthService } from '../_services/index';
import { AppState } from '../../reducers';
import { environment } from '../../../../environments/environment';
import { isUserLoaded } from '../_selectors/auth.selectors';
@Injectable()
export class AuthEffects {
@Effect({dispatch: false})
login$ = this.actions$.pipe(
ofType<Login>(AuthActionTypes.Login),
tap(action => {
localStorage.setItem(environment.authTokenKey, action.payload.authToken);
this.store.dispatch(new UserRequested());
}),
);
@Effect({dispatch: false})
logout$ = this.actions$.pipe(
ofType<Logout>(AuthActionTypes.Logout),
tap(() => {
localStorage.removeItem(environment.authTokenKey);
this.router.navigate(['/auth/login'], {queryParams: {returnUrl: this.returnUrl}});
})
);
@Effect({dispatch: false})
register$ = this.actions$.pipe(
ofType<Register>(AuthActionTypes.Register),
tap(action => {
localStorage.setItem(environment.authTokenKey, action.payload.authToken);
})
);
@Effect({dispatch: false})
loadUser$ = this.actions$
.pipe(
ofType<UserRequested>(AuthActionTypes.UserRequested),
withLatestFrom(this.store.pipe(select(isUserLoaded))),
filter(([action, _isUserLoaded]) => !_isUserLoaded),
mergeMap(([action, _isUserLoaded]) => this.auth.getUserByToken()),
tap(_user => {
if (_user) {
this.store.dispatch(new UserLoaded({ user: _user }));
} else {
this.store.dispatch(new Logout());
}
})
);
@Effect()
init$: Observable<Action> = defer(() => {
const userToken = localStorage.getItem(environment.authTokenKey);
let observableResult = of({type: 'NO_ACTION'});
if (userToken) {
observableResult = of(new Login({ authToken: userToken }));
}
return observableResult;
});
private returnUrl: string;
constructor(private actions$: Actions,
private router: Router,
private auth: AuthService,
private store: Store<AppState>) {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.returnUrl = event.url;
}
});
}
}

View File

@@ -0,0 +1,40 @@
// Angular
import { Injectable } from '@angular/core';
// RxJS
import { mergeMap, map, tap } from 'rxjs/operators';
import { defer, Observable, of } from 'rxjs';
// NGRX
import { Effect, Actions, ofType } from '@ngrx/effects';
import { Action } from '@ngrx/store';
// Services
import { AuthService } from '../_services';
// Actions
import {
AllPermissionsLoaded,
AllPermissionsRequested,
PermissionActionTypes
} from '../_actions/permission.actions';
// Models
import { Permission } from '../_models/permission.model';
@Injectable()
export class PermissionEffects {
@Effect()
loadAllPermissions$ = this.actions$
.pipe(
ofType<AllPermissionsRequested>(PermissionActionTypes.AllPermissionsRequested),
mergeMap(() => this.auth.getAllPermissions()),
map((result: Permission[]) => {
return new AllPermissionsLoaded({
permissions: result
});
})
);
@Effect()
init$: Observable<Action> = defer(() => {
return of(new AllPermissionsRequested());
});
constructor(private actions$: Actions, private auth: AuthService) { }
}

View File

@@ -0,0 +1,126 @@
// Angular
import { Injectable } from '@angular/core';
// RxJS
import { of, Observable, defer, forkJoin } from 'rxjs';
import { mergeMap, map, withLatestFrom, filter, tap } from 'rxjs/operators';
// NGRX
import { Effect, Actions, ofType } from '@ngrx/effects';
import { Store, select, Action } from '@ngrx/store';
// CRUD
import { QueryResultsModel, QueryParamsModel } from '../../_base/crud';
// Services
import { AuthService } from '../_services';
// State
import { AppState } from '../../../core/reducers';
// Selectors
import { allRolesLoaded } from '../_selectors/role.selectors';
// Actions
import {
AllRolesLoaded,
AllRolesRequested,
RoleActionTypes,
RolesPageRequested,
RolesPageLoaded,
RoleUpdated,
RolesPageToggleLoading,
RoleDeleted,
RoleOnServerCreated,
RoleCreated,
RolesActionToggleLoading
} from '../_actions/role.actions';
@Injectable()
export class RoleEffects {
showPageLoadingDistpatcher = new RolesPageToggleLoading({ isLoading: true });
hidePageLoadingDistpatcher = new RolesPageToggleLoading({ isLoading: false });
showActionLoadingDistpatcher = new RolesActionToggleLoading({ isLoading: true });
hideActionLoadingDistpatcher = new RolesActionToggleLoading({ isLoading: false });
@Effect()
loadAllRoles$ = this.actions$
.pipe(
ofType<AllRolesRequested>(RoleActionTypes.AllRolesRequested),
withLatestFrom(this.store.pipe(select(allRolesLoaded))),
filter(([action, isAllRolesLoaded]) => !isAllRolesLoaded),
mergeMap(() => this.auth.getAllRoles()),
map(roles => {
return new AllRolesLoaded({roles});
})
);
@Effect()
loadRolesPage$ = this.actions$
.pipe(
ofType<RolesPageRequested>(RoleActionTypes.RolesPageRequested),
mergeMap(( { payload } ) => {
this.store.dispatch(this.showPageLoadingDistpatcher);
const requestToServer = this.auth.findRoles(payload.page);
const lastQuery = of(payload.page);
return forkJoin(requestToServer, lastQuery);
}),
map(response => {
const result: QueryResultsModel = response[0];
const lastQuery: QueryParamsModel = response[1];
this.store.dispatch(this.hidePageLoadingDistpatcher);
return new RolesPageLoaded({
roles: result.items,
totalCount: result.totalCount,
page: lastQuery
});
}),
);
@Effect()
deleteRole$ = this.actions$
.pipe(
ofType<RoleDeleted>(RoleActionTypes.RoleDeleted),
mergeMap(( { payload } ) => {
this.store.dispatch(this.showActionLoadingDistpatcher);
return this.auth.deleteRole(payload.id);
}
),
map(() => {
return this.hideActionLoadingDistpatcher;
}),
);
@Effect()
updateRole$ = this.actions$
.pipe(
ofType<RoleUpdated>(RoleActionTypes.RoleUpdated),
mergeMap(( { payload } ) => {
this.store.dispatch(this.showActionLoadingDistpatcher);
return this.auth.updateRole(payload.role);
}),
map(() => {
return this.hideActionLoadingDistpatcher;
}),
);
@Effect()
createRole$ = this.actions$
.pipe(
ofType<RoleOnServerCreated>(RoleActionTypes.RoleOnServerCreated),
mergeMap(( { payload } ) => {
this.store.dispatch(this.showActionLoadingDistpatcher);
return this.auth.createRole(payload.role).pipe(
tap(res => {
this.store.dispatch(new RoleCreated({ role: res }));
})
);
}),
map(() => {
return this.hideActionLoadingDistpatcher;
}),
);
@Effect()
init$: Observable<Action> = defer(() => {
return of(new AllRolesRequested());
});
constructor(private actions$: Actions, private auth: AuthService, private store: Store<AppState>) { }
}

View File

@@ -0,0 +1,101 @@
// Angular
import { Injectable } from '@angular/core';
// RxJS
import { mergeMap, map, tap } from 'rxjs/operators';
import { Observable, defer, of, forkJoin } from 'rxjs';
// NGRX
import { Effect, Actions, ofType } from '@ngrx/effects';
import { Store, select, Action } from '@ngrx/store';
// CRUD
import { QueryResultsModel, QueryParamsModel } from '../../_base/crud';
// Services
import { AuthService } from '../../../core/auth/_services';
// State
import { AppState } from '../../../core/reducers';
import {
UserActionTypes,
UsersPageRequested,
UsersPageLoaded,
UserCreated,
UserDeleted,
UserUpdated,
UserOnServerCreated,
UsersActionToggleLoading,
UsersPageToggleLoading
} from '../_actions/user.actions';
@Injectable()
export class UserEffects {
showPageLoadingDistpatcher = new UsersPageToggleLoading({ isLoading: true });
hidePageLoadingDistpatcher = new UsersPageToggleLoading({ isLoading: false });
showActionLoadingDistpatcher = new UsersActionToggleLoading({ isLoading: true });
hideActionLoadingDistpatcher = new UsersActionToggleLoading({ isLoading: false });
@Effect()
loadUsersPage$ = this.actions$
.pipe(
ofType<UsersPageRequested>(UserActionTypes.UsersPageRequested),
mergeMap(( { payload } ) => {
this.store.dispatch(this.showPageLoadingDistpatcher);
const requestToServer = this.auth.findUsers(payload.page);
const lastQuery = of(payload.page);
return forkJoin(requestToServer, lastQuery);
}),
map(response => {
const result: QueryResultsModel = response[0];
const lastQuery: QueryParamsModel = response[1];
return new UsersPageLoaded({
users: result.items,
totalCount: result.totalCount,
page: lastQuery
});
}),
);
@Effect()
deleteUser$ = this.actions$
.pipe(
ofType<UserDeleted>(UserActionTypes.UserDeleted),
mergeMap(( { payload } ) => {
this.store.dispatch(this.showActionLoadingDistpatcher);
return this.auth.deleteUser(payload.id);
}
),
map(() => {
return this.hideActionLoadingDistpatcher;
}),
);
@Effect()
updateUser$ = this.actions$
.pipe(
ofType<UserUpdated>(UserActionTypes.UserUpdated),
mergeMap(( { payload } ) => {
this.store.dispatch(this.showActionLoadingDistpatcher);
return this.auth.updateUser(payload.user);
}),
map(() => {
return this.hideActionLoadingDistpatcher;
}),
);
@Effect()
createUser$ = this.actions$
.pipe(
ofType<UserOnServerCreated>(UserActionTypes.UserOnServerCreated),
mergeMap(( { payload } ) => {
this.store.dispatch(this.showActionLoadingDistpatcher);
return this.auth.createUser(payload.user).pipe(
tap(res => {
this.store.dispatch(new UserCreated({ user: res }));
})
);
}),
map(() => {
return this.hideActionLoadingDistpatcher;
}),
);
constructor(private actions$: Actions, private auth: AuthService, private store: Store<AppState>) { }
}

View File

@@ -0,0 +1,34 @@
import {Injectable} from '@angular/core';
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot, Router,
} from '@angular/router';
import {NgxPermissionsService} from "ngx-permissions";
import { AuthService } from '../_services';
@Injectable({providedIn: 'root'})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router, private pService: NgxPermissionsService) {
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
let user = this.authService.getUser();
if(user){
let permissions = this.authService.getPermission();
let result = [];
for(let i in permissions){
if(permissions[i]){
result.push(i);
}
}
this.pService.loadPermissions(result);
}
if(!user){
this.router.navigateByUrl('/auth/login');
}
return !!user;
}
}

View File

@@ -0,0 +1,42 @@
// Angular
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
// RxJS
import { Observable, of } from 'rxjs';
import { tap, map } from 'rxjs/operators';
// NGRX
import { select, Store } from '@ngrx/store';
// Module reducers and selectors
import { AppState} from '../../../core/reducers/';
import { currentUserPermissions } from '../_selectors/auth.selectors';
import { Permission } from '../_models/permission.model';
import { find } from 'lodash';
@Injectable()
export class ModuleGuard implements CanActivate {
constructor(private store: Store<AppState>, private router: Router) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const moduleName = route.data.moduleName as string;
if (!moduleName) {
return of(false);
}
return this.store
.pipe(
select(currentUserPermissions),
map((permissions: Permission[]) => {
const _perm = find(permissions, (elem: Permission) => {
return elem.title.toLocaleLowerCase() === moduleName.toLocaleLowerCase();
});
return _perm ? true : false;
}),
tap(hasAccess => {
if (!hasAccess) {
this.router.navigateByUrl('/error/403');
}
})
);
}
}

View File

@@ -0,0 +1,9 @@
// Samplce access data
// {"id":1,"username":"admin","password":"demo","email":"admin@demo.com","accessToken":"access-token-0.022563452858263444","refreshToken":"access-token-0.9348573301432961","roles":["ADMIN"],"pic":"./assets/app/media/img/users/user4.jpg","fullname":"Mark Andre"}
export interface AccessData {
token: string;
refreshToken: string;
role: any;
user: any;
permissions: any;
}

View File

@@ -0,0 +1,13 @@
export class Address {
addressLine: string;
city: string;
state: string;
postCode: string;
clear() {
this.addressLine = '';
this.city = '';
this.state = '';
this.postCode = '';
}
}

View File

@@ -0,0 +1,21 @@
import { BaseModel } from '../../_base/crud';
export class Permission extends BaseModel {
id: number;
title: string;
level: number;
parentId: number;
isSelected: boolean;
name: string;
_children: Permission[];
clear(): void {
this.id = undefined;
this.title = '';
this.level = 1;
this.parentId = undefined;
this.isSelected = false;
this.name = '';
this._children = [];
}
}

View File

@@ -0,0 +1,15 @@
import { BaseModel } from '../../_base/crud';
export class Role extends BaseModel {
id: number;
title: string;
permissions: number[];
isCoreRole = false;
clear(): void {
this.id = undefined;
this.title = '';
this.permissions = [];
this.isCoreRole = false;
}
}

View File

@@ -0,0 +1,13 @@
export class SocialNetworks {
linkedIn: string;
facebook: string;
twitter: string;
instagram: string;
clear() {
this.linkedIn = '';
this.facebook = '';
this.twitter = '';
this.instagram = '';
}
}

View File

@@ -0,0 +1,39 @@
import { BaseModel } from '../../_base/crud';
import { Address } from './address.model';
import { SocialNetworks } from './social-networks.model';
export class User extends BaseModel {
id: number;
username: string;
password: string;
email: string;
accessToken: string;
refreshToken: string;
roles: number[];
pic: string;
fullname: string;
occupation: string;
companyName: string;
phone: string;
address: Address;
socialNetworks: SocialNetworks;
clear(): void {
this.id = undefined;
this.username = '';
this.password = '';
this.email = '';
this.roles = [];
this.fullname = '';
this.accessToken = 'access-token-' + Math.random();
this.refreshToken = 'access-token-' + Math.random();
this.pic = './assets/media/users/default.jpg';
this.occupation = '';
this.companyName = '';
this.phone = '';
this.address = new Address();
this.address.clear();
this.socialNetworks = new SocialNetworks();
this.socialNetworks.clear();
}
}

View File

@@ -0,0 +1,57 @@
// Actions
import { AuthActions, AuthActionTypes } from '../_actions/auth.actions';
// Models
import { User } from '../_models/user.model';
export interface AuthState {
loggedIn: boolean;
authToken: string;
user: User;
isUserLoaded: boolean;
}
export const initialAuthState: AuthState = {
loggedIn: false,
authToken: undefined,
user: undefined,
isUserLoaded: false
};
export function authReducer(state = initialAuthState, action: AuthActions): AuthState {
switch (action.type) {
case AuthActionTypes.Login: {
const _token: string = action.payload.authToken;
return {
loggedIn: true,
authToken: _token,
user: undefined,
isUserLoaded: false
};
}
case AuthActionTypes.Register: {
const _token: string = action.payload.authToken;
return {
loggedIn: true,
authToken: _token,
user: undefined,
isUserLoaded: false
};
}
case AuthActionTypes.Logout:
return initialAuthState;
case AuthActionTypes.UserLoaded: {
const _user: User = action.payload.user;
return {
...state,
user: _user,
isUserLoaded: true
};
}
default:
return state;
}
}

View File

@@ -0,0 +1,42 @@
// NGRX
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
import { createFeatureSelector } from '@ngrx/store';
// Actions
import { PermissionActionTypes, PermissionActions } from '../_actions/permission.actions';
// Models
import { Permission } from '../_models/permission.model';
export interface PermissionsState extends EntityState<Permission> {
_isAllPermissionsLoaded: boolean;
}
export const adapter: EntityAdapter<Permission> = createEntityAdapter<Permission>();
export const initialPermissionsState: PermissionsState = adapter.getInitialState({
_isAllPermissionsLoaded: false
});
export function permissionsReducer(state = initialPermissionsState, action: PermissionActions): PermissionsState {
switch (action.type) {
case PermissionActionTypes.AllPermissionsRequested:
return {...state,
_isAllPermissionsLoaded: false
};
case PermissionActionTypes.AllPermissionsLoaded:
return adapter.addAll(action.payload.permissions, {
...state,
_isAllPermissionsLoaded: true
});
default:
return state;
}
}
export const getRoleState = createFeatureSelector<PermissionsState>('permissions');
export const {
selectAll,
selectEntities,
selectIds,
selectTotal
} = adapter.getSelectors();

View File

@@ -0,0 +1,72 @@
// NGRX
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
// Actions
import { RoleActions, RoleActionTypes } from '../_actions/role.actions';
// Models
import { Role } from '../_models/role.model';
import { QueryParamsModel } from '../../_base/crud';
export interface RolesState extends EntityState<Role> {
isAllRolesLoaded: boolean;
queryRowsCount: number;
queryResult: Role[];
lastCreatedRoleId: number;
listLoading: boolean;
actionsloading: boolean;
lastQuery: QueryParamsModel;
showInitWaitingMessage: boolean;
}
export const adapter: EntityAdapter<Role> = createEntityAdapter<Role>();
export const initialRolesState: RolesState = adapter.getInitialState({
isAllRolesLoaded: false,
queryRowsCount: 0,
queryResult: [],
lastCreatedRoleId: undefined,
listLoading: false,
actionsloading: false,
lastQuery: new QueryParamsModel({}),
showInitWaitingMessage: true
});
export function rolesReducer(state = initialRolesState, action: RoleActions): RolesState {
switch (action.type) {
case RoleActionTypes.RolesPageToggleLoading: return {
...state, listLoading: action.payload.isLoading, lastCreatedRoleId: undefined
};
case RoleActionTypes.RolesActionToggleLoading: return {
...state, actionsloading: action.payload.isLoading
};
case RoleActionTypes.RoleOnServerCreated: return {
...state
};
case RoleActionTypes.RoleCreated: return adapter.addOne(action.payload.role, {
...state, lastCreatedRoleId: action.payload.role.id
});
case RoleActionTypes.RoleUpdated: return adapter.updateOne(action.payload.partialrole, state);
case RoleActionTypes.RoleDeleted: return adapter.removeOne(action.payload.id, state);
case RoleActionTypes.AllRolesLoaded: return adapter.addAll(action.payload.roles, {
...state, isAllRolesLoaded: true
});
case RoleActionTypes.RolesPageCancelled: return {
...state, listLoading: false, queryRowsCount: 0, queryResult: [], lastQuery: new QueryParamsModel({})
};
case RoleActionTypes.RolesPageLoaded: return adapter.addMany(action.payload.roles, {
...initialRolesState,
listLoading: false,
queryRowsCount: action.payload.totalCount,
queryResult: action.payload.roles,
lastQuery: action.payload.page,
showInitWaitingMessage: false
});
default: return state;
}
}
export const {
selectAll,
selectEntities,
selectIds,
selectTotal
} = adapter.getSelectors();

View File

@@ -0,0 +1,71 @@
// NGRX
import { createFeatureSelector } from '@ngrx/store';
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
// Actions
import { UserActions, UserActionTypes } from '../_actions/user.actions';
// CRUD
import { QueryParamsModel } from '../../_base/crud';
// Models
import { User } from '../_models/user.model';
// tslint:disable-next-line:no-empty-interface
export interface UsersState extends EntityState<User> {
listLoading: boolean;
actionsloading: boolean;
totalCount: number;
lastCreatedUserId: number;
lastQuery: QueryParamsModel;
showInitWaitingMessage: boolean;
}
export const adapter: EntityAdapter<User> = createEntityAdapter<User>();
export const initialUsersState: UsersState = adapter.getInitialState({
listLoading: false,
actionsloading: false,
totalCount: 0,
lastQuery: new QueryParamsModel({}),
lastCreatedUserId: undefined,
showInitWaitingMessage: true
});
export function usersReducer(state = initialUsersState, action: UserActions): UsersState {
switch (action.type) {
case UserActionTypes.UsersPageToggleLoading: return {
...state, listLoading: action.payload.isLoading, lastCreatedUserId: undefined
};
case UserActionTypes.UsersActionToggleLoading: return {
...state, actionsloading: action.payload.isLoading
};
case UserActionTypes.UserOnServerCreated: return {
...state
};
case UserActionTypes.UserCreated: return adapter.addOne(action.payload.user, {
...state, lastCreatedUserId: action.payload.user.id
});
case UserActionTypes.UserUpdated: return adapter.updateOne(action.payload.partialUser, state);
case UserActionTypes.UserDeleted: return adapter.removeOne(action.payload.id, state);
case UserActionTypes.UsersPageCancelled: return {
...state, listLoading: false, lastQuery: new QueryParamsModel({})
};
case UserActionTypes.UsersPageLoaded: {
return adapter.addMany(action.payload.users, {
...initialUsersState,
totalCount: action.payload.totalCount,
lastQuery: action.payload.page,
listLoading: false,
showInitWaitingMessage: false
});
}
default: return state;
}
}
export const getUserState = createFeatureSelector<UsersState>('users');
export const {
selectAll,
selectEntities,
selectIds,
selectTotal
} = adapter.getSelectors();

View File

@@ -0,0 +1,100 @@
// NGRX
import { createSelector } from '@ngrx/store';
// Lodash
import { each, find, some } from 'lodash';
// Selectors
import { selectAllRoles } from './role.selectors';
import { selectAllPermissions } from './permission.selectors';
// Models
import { Role } from '../_models/role.model';
import { Permission } from '../_models/permission.model';
export const selectAuthState = state => state.auth;
export const isLoggedIn = createSelector(
selectAuthState,
auth => auth.loggedIn
);
export const isLoggedOut = createSelector(
isLoggedIn,
loggedIn => !loggedIn
);
export const currentAuthToken = createSelector(
selectAuthState,
auth => auth.authToken
);
export const isUserLoaded = createSelector(
selectAuthState,
auth => auth.isUserLoaded
);
export const currentUser = createSelector(
selectAuthState,
auth => auth.user
);
export const currentUserRoleIds = createSelector(
currentUser,
user => {
if (!user) {
return [];
}
return user.roles;
}
);
export const currentUserPermissionsIds = createSelector(
currentUserRoleIds,
selectAllRoles,
(userRoleIds: number[], allRoles: Role[]) => {
const result = getPermissionsIdsFrom(userRoleIds, allRoles);
return result;
}
);
export const checkHasUserPermission = (permissionId: number) => createSelector(
currentUserPermissionsIds,
(ids: number[]) => {
return ids.some(id => id === permissionId);
}
);
export const currentUserPermissions = createSelector(
currentUserPermissionsIds,
selectAllPermissions,
(permissionIds: number[], allPermissions: Permission[]) => {
const result: Permission[] = [];
each(permissionIds, id => {
const userPermission = find(allPermissions, elem => elem.id === id);
if (userPermission) {
result.push(userPermission);
}
});
return result;
}
);
function getPermissionsIdsFrom(userRolesIds: number[] = [], allRoles: Role[] = []): number[] {
const userRoles: Role[] = [];
each(userRolesIds, (_id: number) => {
const userRole = find(allRoles, (_role: Role) => _role.id === _id);
if (userRole) {
userRoles.push(userRole);
}
});
const result: number[] = [];
each(userRoles, (_role: Role) => {
each(_role.permissions, id => {
if (!some(result, _id => _id === id)) {
result.push(id);
}
});
});
return result;
}

View File

@@ -0,0 +1,27 @@
// NGRX
import { createFeatureSelector, createSelector } from '@ngrx/store';
// State
import { PermissionsState } from '../_reducers/permission.reducers';
import * as fromPermissions from '../_reducers/permission.reducers';
export const selectPermissionsState = createFeatureSelector<PermissionsState>('permissions');
export const selectPermissionById = (permissionId: number) => createSelector(
selectPermissionsState,
ps => ps.entities[permissionId]
);
export const selectAllPermissions = createSelector(
selectPermissionsState,
fromPermissions.selectAll
);
export const selectAllPermissionsIds = createSelector(
selectPermissionsState,
fromPermissions.selectIds
);
export const allPermissionsLoaded = createSelector(
selectPermissionsState,
ps => ps._isAllPermissionsLoaded
);

View File

@@ -0,0 +1,68 @@
import { Role } from './../_models/role.model';
// NGRX
import { createFeatureSelector, createSelector } from '@ngrx/store';
// CRUD
import { QueryResultsModel, HttpExtenstionsModel } from '../../_base/crud';
// State
import { RolesState } from '../_reducers/role.reducers';
import * as fromRole from '../_reducers/role.reducers';
import { each } from 'lodash';
export const selectRolesState = createFeatureSelector<RolesState>('roles');
export const selectRoleById = (roleId: number) => createSelector(
selectRolesState,
rolesState => rolesState.entities[roleId]
);
export const selectAllRoles = createSelector(
selectRolesState,
fromRole.selectAll
);
export const selectAllRolesIds = createSelector(
selectRolesState,
fromRole.selectIds
);
export const allRolesLoaded = createSelector(
selectRolesState,
rolesState => rolesState.isAllRolesLoaded
);
export const selectRolesPageLoading = createSelector(
selectRolesState,
rolesState => rolesState.listLoading
);
export const selectRolesActionLoading = createSelector(
selectRolesState,
rolesState => rolesState.actionsloading
);
export const selectLastCreatedRoleId = createSelector(
selectRolesState,
rolesState => rolesState.lastCreatedRoleId
);
export const selectRolesShowInitWaitingMessage = createSelector(
selectRolesState,
rolesState => rolesState.showInitWaitingMessage
);
export const selectQueryResult = createSelector(
selectRolesState,
rolesState => {
const items: Role[] = [];
each(rolesState.entities, element => {
items.push(element);
});
const httpExtension = new HttpExtenstionsModel();
const result: Role[] = httpExtension.sortArray(items, rolesState.lastQuery.sortField, rolesState.lastQuery.sortOrder);
return new QueryResultsModel(rolesState.queryResult, rolesState.queryRowsCount);
}
);

View File

@@ -0,0 +1,67 @@
// NGRX
import { createFeatureSelector, createSelector } from '@ngrx/store';
// CRUD
import { QueryResultsModel, HttpExtenstionsModel } from '../../_base/crud';
// State
import { UsersState } from '../_reducers/user.reducers';
import { each } from 'lodash';
import { User } from '../_models/user.model';
export const selectUsersState = createFeatureSelector<UsersState>('users');
export const selectUserById = (userId: number) => createSelector(
selectUsersState,
usersState => usersState.entities[userId]
);
export const selectUsersPageLoading = createSelector(
selectUsersState,
usersState => {
return usersState.listLoading;
}
);
export const selectUsersActionLoading = createSelector(
selectUsersState,
usersState => usersState.actionsloading
);
export const selectLastCreatedUserId = createSelector(
selectUsersState,
usersState => usersState.lastCreatedUserId
);
export const selectUsersPageLastQuery = createSelector(
selectUsersState,
usersState => usersState.lastQuery
);
export const selectUsersInStore = createSelector(
selectUsersState,
usersState => {
const items: User[] = [];
each(usersState.entities, element => {
items.push(element);
});
const httpExtension = new HttpExtenstionsModel();
const result: User[] = httpExtension.sortArray(items, usersState.lastQuery.sortField, usersState.lastQuery.sortOrder);
return new QueryResultsModel(result, usersState.totalCount, '');
}
);
export const selectUsersShowInitWaitingMessage = createSelector(
selectUsersState,
usersState => usersState.showInitWaitingMessage
);
export const selectHasUsersInStore = createSelector(
selectUsersState,
queryResult => {
if (!queryResult.totalCount) {
return false;
}
return true;
}
);

View File

@@ -0,0 +1,10 @@
import { UsersTable } from './users.table';
import { PermissionsTable } from './permissions.table';
import { RolesTable } from './roles.table';
// Wrapper class
export class AuthDataContext {
public static users: any = UsersTable.users;
public static roles: any = RolesTable.roles;
public static permissions = PermissionsTable.permissions;
}

View File

@@ -0,0 +1,85 @@
export class PermissionsTable {
public static permissions: any = [
{
id: 1,
name: 'accessToECommerceModule',
level: 1,
title: 'eCommerce module'
},
{
id: 2,
name: 'accessToAuthModule',
level: 1,
title: 'Users Management module'
},
{
id: 3,
name: 'accessToMailModule',
level: 1,
title: 'Mail module'
},
{
id: 4,
name: 'canReadECommerceData',
level: 2,
parentId: 1,
title: 'Read'
},
{
id: 5,
name: 'canEditECommerceData',
level: 2,
parentId: 1,
title: 'Edit'
},
{
id: 6,
name: 'canDeleteECommerceData',
level: 2,
parentId: 1,
title: 'Delete'
},
{
id: 7,
name: 'canReadAuthData',
level: 2,
parentId: 2,
title: 'Read'
},
{
id: 8,
name: 'canEditAuthData',
level: 2,
parentId: 2,
title: 'Edit'
},
{
id: 9,
name: 'canDeleteAuthData',
level: 2,
parentId: 2,
title: 'Delete'
},
{
id: 10,
name: 'canReadMailData',
level: 2,
parentId: 3,
title: 'Read'
},
{
id: 11,
name: 'canEditMailData',
level: 2,
parentId: 3,
title: 'Edit'
},
{
id: 12,
name: 'canDeleteMailData',
level: 2,
parentId: 3,
title: 'Delete'
},
];
}

View File

@@ -0,0 +1,22 @@
export class RolesTable {
public static roles: any = [
{
id: 1,
title: 'Administrator',
isCoreRole: true,
permissions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
},
{
id: 2,
title: 'Manager',
isCoreRole: false,
permissions: [3, 4, 10]
},
{
id: 3,
title: 'Guest',
isCoreRole: false,
permissions: []
}
];
}

View File

@@ -0,0 +1,90 @@
export class UsersTable {
public static users: any = [
{
id: 1,
username: 'admin',
password: 'demo',
email: 'admin@demo.com',
accessToken: 'access-token-8f3ae836da744329a6f93bf20594b5cc',
refreshToken: 'access-token-f8c137a2c98743f48b643e71161d90aa',
roles: [1], // Administrator
pic: './assets/media/users/300_25.jpg',
fullname: 'Sean',
occupation: 'CEO',
companyName: 'Keenthemes',
phone: '456669067890',
address: {
addressLine: 'L-12-20 Vertex, Cybersquare',
city: 'San Francisco',
state: 'California',
postCode: '45000'
},
socialNetworks: {
linkedIn: 'https://linkedin.com/admin',
facebook: 'https://facebook.com/admin',
twitter: 'https://twitter.com/admin',
instagram: 'https://instagram.com/admin'
}
},
{
id: 2,
username: 'user',
password: 'demo',
email: 'user@demo.com',
accessToken: 'access-token-6829bba69dd3421d8762-991e9e806dbf',
refreshToken: 'access-token-f8e4c61a318e4d618b6c199ef96b9e55',
roles: [2], // Manager
pic: './assets/media/users/100_2.jpg',
fullname: 'Megan',
occupation: 'Deputy Head of Keenthemes in New York office',
companyName: 'Keenthemes',
phone: '456669067891',
address: {
addressLine: '3487 Ingram Road',
city: 'Greensboro',
state: 'North Carolina',
postCode: '27409'
},
socialNetworks: {
linkedIn: 'https://linkedin.com/user',
facebook: 'https://facebook.com/user',
twitter: 'https://twitter.com/user',
instagram: 'https://instagram.com/user'
}
},
{
id: 3,
username: 'guest',
password: 'demo',
email: 'guest@demo.com',
accessToken: 'access-token-d2dff7b82f784de584b60964abbe45b9',
refreshToken: 'access-token-c999ccfe74aa40d0aa1a64c5e620c1a5',
roles: [3], // Guest
pic: './assets/media/users/default.jpg',
fullname: 'Ginobili Maccari',
occupation: 'CFO',
companyName: 'Keenthemes',
phone: '456669067892',
address: {
addressLine: '1467 Griffin Street',
city: 'Phoenix',
state: 'Arizona',
postCode: '85012'
},
socialNetworks: {
linkedIn: 'https://linkedin.com/guest',
facebook: 'https://facebook.com/guest',
twitter: 'https://twitter.com/guest',
instagram: 'https://instagram.com/guest'
}
}
];
public static tokens: any = [
{
id: 1,
accessToken: 'access-token-' + Math.random(),
refreshToken: 'access-token-' + Math.random()
}
];
}

View File

@@ -0,0 +1,220 @@
import {EventEmitter, Injectable, Output} from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { User } from '../_models/user.model';
import { Permission } from '../_models/permission.model';
import { Role } from '../_models/role.model';
import {catchError, map, tap} from 'rxjs/operators';
import { QueryParamsModel, QueryResultsModel } from '../../_base/crud';
import { environment } from '../../../../environments/environment';
import {ApiBase} from "../../api/api-base";
import {AccessData} from "../_models/access-data";
import {TokenStorage} from "./token-storage.service";
const API_USERS_URL = 'api/users';
const API_PERMISSION_URL = 'api/permissions';
const API_ROLES_URL = 'api/roles';
@Injectable()
export class AuthService extends ApiBase {
@Output() errorEvent: EventEmitter<any> = new EventEmitter(true);
constructor(private http: HttpClient, private tokenStorage: TokenStorage) {
super();
}
/**
* Get user roles
* @returns {Observable<any>}
*/
public getUser(): any {
return this.tokenStorage.getUser();
}
public getPermission() : any {
return this.tokenStorage.getUserRoles()
}
/**
* Logout
*/
public logout(refresh?: boolean): void {
this.tokenStorage.clear();
if (refresh) {
location.reload();
}
}
// Authentication/Authorization
login(email: string, password: string): Observable<any> {
let headers = new HttpHeaders({
'Application': 'WEB' });
let options = { headers: headers };
return this.http.post(environment.API + 'login', {email: email, password: password, options}).pipe(
map((result: any) => {
if (result instanceof Array) {
return result.pop();
}
return result;
}),
tap(this.saveAccessData.bind(this)),
catchError(this.handleError('login', []))
);
}
private saveAccessData(accessData: AccessData) {
if (typeof accessData !== 'undefined') {
this.tokenStorage
.setAccessToken(accessData.token)
.setRefreshToken(accessData.token)
.setUserRoles(accessData.role.permissions)
.setUser(accessData.user);
}
}
getUserByToken(): Observable<User> {
const userToken = localStorage.getItem(environment.authTokenKey);
const httpHeaders = new HttpHeaders();
httpHeaders.set('Authorization', 'Bearer ' + userToken);
return this.http.get<User>(API_USERS_URL, { headers: httpHeaders });
}
refreshToken(): Observable<User> {
const userToken = localStorage.getItem('accessTokenDrenax');
const httpHeaders = new HttpHeaders();
httpHeaders.set('Authorization', 'Bearer ' + userToken);
return this.http.get(environment.API + 'refresh', { headers: httpHeaders }).pipe(
map((result: any) => {
if (result instanceof Array) {
return result.pop();
}
return result;
}),
tap(this.setToken.bind(this)),
catchError(this.handleError('refresh', []))
);
}
setToken(accessData: AccessData){
if (typeof accessData !== 'undefined') {
this.tokenStorage
.setAccessToken(accessData.token)
.setRefreshToken(accessData.token);
}
}
register(user: User): Observable<any> {
const httpHeaders = new HttpHeaders();
httpHeaders.set('Content-Type', 'application/json');
return this.http.post<User>(API_USERS_URL, user, { headers: httpHeaders })
.pipe(
map((res: User) => {
return res;
}),
catchError(err => {
return null;
})
);
}
/*
* Submit forgot password request
*
* @param {string} email
* @returns {Observable<any>}
*/
public requestPassword(email: string): Observable<any> {
return this.http.get(API_USERS_URL + '/forgot?=' + email)
.pipe(catchError(this.handleError('forgot-password', []))
);
}
getAllUsers(): Observable<User[]> {
return this.http.get<User[]>(API_USERS_URL);
}
getUserById(userId: number): Observable<User> {
return this.http.get<User>(API_USERS_URL + `/${userId}`);
}
// DELETE => delete the user from the server
deleteUser(userId: number) {
const url = `${API_USERS_URL}/${userId}`;
return this.http.delete(url);
}
// UPDATE => PUT: update the user on the server
updateUser(_user: User): Observable<any> {
const httpHeaders = new HttpHeaders();
httpHeaders.set('Content-Type', 'application/json');
return this.http.put(API_USERS_URL, _user, { headers: httpHeaders });
}
// CREATE => POST: add a new user to the server
createUser(user: User): Observable<User> {
const httpHeaders = new HttpHeaders();
httpHeaders.set('Content-Type', 'application/json');
return this.http.post<User>(API_USERS_URL, user, { headers: httpHeaders});
}
// Method from server should return QueryResultsModel(items: any[], totalsCount: number)
// items => filtered/sorted result
findUsers(queryParams: QueryParamsModel): Observable<QueryResultsModel> {
const httpHeaders = new HttpHeaders();
httpHeaders.set('Content-Type', 'application/json');
return this.http.post<QueryResultsModel>(API_USERS_URL + '/findUsers', queryParams, { headers: httpHeaders});
}
// Permission
getAllPermissions(): Observable<Permission[]> {
return this.http.get<Permission[]>(API_PERMISSION_URL);
}
getRolePermissions(roleId: number): Observable<Permission[]> {
return this.http.get<Permission[]>(API_PERMISSION_URL + '/getRolePermission?=' + roleId);
}
// Roles
getAllRoles(): Observable<Role[]> {
return this.http.get<Role[]>(API_ROLES_URL);
}
getRoleById(roleId: number): Observable<Role> {
return this.http.get<Role>(API_ROLES_URL + `/${roleId}`);
}
// CREATE => POST: add a new role to the server
createRole(role: Role): Observable<Role> {
// Note: Add headers if needed (tokens/bearer)
const httpHeaders = new HttpHeaders();
httpHeaders.set('Content-Type', 'application/json');
return this.http.post<Role>(API_ROLES_URL, role, { headers: httpHeaders});
}
// UPDATE => PUT: update the role on the server
updateRole(role: Role): Observable<any> {
const httpHeaders = new HttpHeaders();
httpHeaders.set('Content-Type', 'application/json');
return this.http.put(API_ROLES_URL, role, { headers: httpHeaders });
}
// DELETE => delete the role from the server
deleteRole(roleId: number): Observable<Role> {
const url = `${API_ROLES_URL}/${roleId}`;
return this.http.delete<Role>(url);
}
// Check Role Before deletion
isRoleAssignedToUsers(roleId: number): Observable<boolean> {
return this.http.get<boolean>(API_ROLES_URL + '/checkIsRollAssignedToUser?roleId=' + roleId);
}
findRoles(queryParams: QueryParamsModel): Observable<QueryResultsModel> {
// This code imitates server calls
const httpHeaders = new HttpHeaders();
httpHeaders.set('Content-Type', 'application/json');
return this.http.post<QueryResultsModel>(API_ROLES_URL + '/findRoles', queryParams, { headers: httpHeaders});
}
}

View File

@@ -0,0 +1,2 @@
export { AuthService } from './auth.service'; // You have to comment this, when your real back-end is done
// export { AuthService } from './auth.service'; // You have to uncomment this, when your real back-end is done

View File

@@ -0,0 +1,104 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
@Injectable()
export class TokenStorage {
/**
* Get access token
* @returns {Observable<string>}
*/
public getAccessToken(): string {
const token: any = localStorage.getItem('accessTokenDrenax');
try {
return token;
} catch (e) {}
}
/**
* Get refresh token
* @returns {Observable<string>}
*/
public getRefreshToken(): Observable<string> {
const token: string = <string>localStorage.getItem('refreshTokenDrenax');
return of(token);
}
/**
* Get user roles in JSON string
* @returns {Observable<any>}
*/
public getUserRoles(): Observable<any> {
const roles: any = localStorage.getItem('userRolesDrenax');
try {
return JSON.parse(roles)
} catch (e) {}
}
/**
* Get user in JSON string
* @returns {Observable<any>}
*/
public getUser(): any {
const user: any = localStorage.getItem('userDrenax');
try {
return JSON.parse(user);
} catch (e) {}
}
/**
* Set access token
* @returns {TokenStorage}
*/
public setAccessToken(token: string): TokenStorage {
localStorage.setItem('accessTokenDrenax', token);
return this;
}
/**
* Set refresh token
* @returns {TokenStorage}
*/
public setRefreshToken(token: string): TokenStorage {
localStorage.setItem('refreshTokenDrenax', token);
return this;
}
/**
* Set user roles
* @param roles
* @returns {TokenStorage}
*/
public setUserRoles(roles: any): any {
if (roles != null) {
localStorage.setItem('userRolesDrenax', JSON.stringify(roles));
}
return this;
}
/**
* Set user
* @param user
* @returns {TokenStorage}
*/
public setUser(user: any): any {
if (user != null) {
localStorage.setItem('userDrenax', JSON.stringify(user));
}
return this;
}
/**
* Remove tokens
*/
public clear() {
localStorage.removeItem('accessTokenDrenax');
localStorage.removeItem('refreshTokenDrenax');
localStorage.removeItem('userRolesDrenax');
localStorage.removeItem('userDrenax');
}
}

View File

@@ -0,0 +1,4 @@
export interface AuthNotice {
type?: string;
message: string;
}

View File

@@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { AuthNotice } from './auth-notice.interface';
@Injectable({
providedIn: 'root'
})
export class AuthNoticeService {
onNoticeChanged$: BehaviorSubject<AuthNotice>;
constructor() {
this.onNoticeChanged$ = new BehaviorSubject(null);
}
setNotice(message: string, type?: string) {
const notice: AuthNotice = {
message,
type
};
this.onNoticeChanged$.next(notice);
}
}

114
src/app/core/auth/index.ts Normal file
View File

@@ -0,0 +1,114 @@
// SERVICES
export { AuthService } from './_services';
export { AuthNoticeService } from './auth-notice/auth-notice.service';
// DATA SOURCERS
export { RolesDataSource } from './_data-sources/roles.datasource';
export { UsersDataSource } from './_data-sources/users.datasource';
// ACTIONS
export {
Login,
Logout,
Register,
UserRequested,
UserLoaded,
AuthActionTypes,
AuthActions
} from './_actions/auth.actions';
export {
AllPermissionsRequested,
AllPermissionsLoaded,
PermissionActionTypes,
PermissionActions
} from './_actions/permission.actions';
export {
RoleOnServerCreated,
RoleCreated,
RoleUpdated,
RoleDeleted,
RolesPageRequested,
RolesPageLoaded,
RolesPageCancelled,
AllRolesLoaded,
AllRolesRequested,
RoleActionTypes,
RoleActions
} from './_actions/role.actions';
export {
UserCreated,
UserUpdated,
UserDeleted,
UserOnServerCreated,
UsersPageLoaded,
UsersPageCancelled,
UsersPageToggleLoading,
UsersPageRequested,
UsersActionToggleLoading
} from './_actions/user.actions';
// EFFECTS
export { AuthEffects } from './_effects/auth.effects';
export { PermissionEffects } from './_effects/permission.effects';
export { RoleEffects } from './_effects/role.effects';
export { UserEffects } from './_effects/user.effects';
// REDUCERS
export { authReducer } from './_reducers/auth.reducers';
export { permissionsReducer } from './_reducers/permission.reducers';
export { rolesReducer } from './_reducers/role.reducers';
export { usersReducer } from './_reducers/user.reducers';
// SELECTORS
export {
isLoggedIn,
isLoggedOut,
isUserLoaded,
currentAuthToken,
currentUser,
currentUserRoleIds,
currentUserPermissionsIds,
currentUserPermissions,
checkHasUserPermission
} from './_selectors/auth.selectors';
export {
selectPermissionById,
selectAllPermissions,
selectAllPermissionsIds,
allPermissionsLoaded
} from './_selectors/permission.selectors';
export {
selectRoleById,
selectAllRoles,
selectAllRolesIds,
allRolesLoaded,
selectLastCreatedRoleId,
selectRolesPageLoading,
selectQueryResult,
selectRolesActionLoading,
selectRolesShowInitWaitingMessage
} from './_selectors/role.selectors';
export {
selectUserById,
selectUsersPageLoading,
selectLastCreatedUserId,
selectUsersInStore,
selectHasUsersInStore,
selectUsersPageLastQuery,
selectUsersActionLoading,
selectUsersShowInitWaitingMessage
} from './_selectors/user.selectors';
// GUARDS
export { AuthGuard } from './_guards/auth.guard';
export { ModuleGuard } from './_guards/module.guard';
// MODELS
export { User } from './_models/user.model';
export { Permission } from './_models/permission.model';
export { Role } from './_models/role.model';
export { Address } from './_models/address.model';
export { SocialNetworks } from './_models/social-networks.model';
export { AuthNotice } from './auth-notice/auth-notice.interface';
export { AuthDataContext } from './_server/auth.data-context';

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