[HIMA-161] Dashboard desing update

- Dashboard books updated
- Dashboard kpi updated
This commit is contained in:
Milos Jovanov
2019-05-15 12:12:23 +02:00
parent f6f64fbf9a
commit 90c08fabe2
26 changed files with 293 additions and 91 deletions

View File

@@ -35,6 +35,7 @@ import { SsoAuthorizationInterceptor, HttpErrorHandlerInterceptor } from './core
import { DatePipe } from '@angular/common';
import { CountryState } from './core/store/state/countries.state';
import { HimaSalesErrorHandler } from './core/error/hima-sales.error-handler';
import { NgCircleProgressModule } from 'ng-circle-progress';
const states = [
AppState,
@@ -90,6 +91,11 @@ export function _feedServiceEndpointProviderFactory(conf: ConfigService) {
ModalModule.forRoot(),
SsoModule.forRoot(environment.production),
OfflineOverlayModule,
NgCircleProgressModule.forRoot({
animation: true,
animationDuration: 300,
clockwise: true,
}),
],
providers: [
{

View File

@@ -84,7 +84,7 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy {
selectBreadcrumb(breadcrumb: Breadcrumb) {
this.store.dispatch(new ChangeCurrentRoute(breadcrumb.path, false));
this.router.navigate([breadcrumb.path]);
this.router.navigate([breadcrumb.path], { queryParams: breadcrumb.queryParams ? breadcrumb.queryParams : {} });
}
goBack(breadcrumb: Breadcrumb) {

View File

@@ -1,16 +1,10 @@
import { FeedCard } from '../models/feed-card.model';
// import { FeedBook } from '../models/feed-book.model';
import { FeedBook } from '../models/feed-book.model';
import { FeedEvent } from '../models/feed-event.model';
import { FeedNews } from '../models/feed-news.model';
import { FeedRecommandation } from '../models/feed-recommandation.model';
import { Injectable } from '@angular/core';
import { FeedDTO } from 'swagger/lib/isa/models/feed-dto';
export interface FeedBook {
id: number;
imgUrl: string;
name: string;
}
import { FeedKpi } from '../models/feed-kpi.model';
@Injectable({ providedIn: 'root' })
export class FeedMapping {
@@ -21,12 +15,12 @@ export class FeedMapping {
const event: FeedEvent[] = [];
const news: FeedNews[] = [];
const recommandation: FeedRecommandation = null;
let kpi: FeedKpi = null;
if (feed.type === 'products') {
feed.items.forEach(item => {
books.push({
id: item.id,
imgUrl: item.product.ean,
ean: item.product.ean,
name: item.product.name,
});
});
@@ -47,25 +41,23 @@ export class FeedMapping {
content: i.text,
})
);
} else if (feed.type === 'kpi') {
kpi = {
actual: feed.items[0].actual,
target: feed.items[0].target,
percantage: Math.round((feed.items[0].actual / feed.items[0].target) * 100),
};
}
// else if (feed.type === 'kpi') {
// feed.items.forEach(i =>
// news.push(<FeedNews>{
// id: i.id,
// title: i.heading,
// content: i.text,
// })
// );
// }
return <FeedCard>{
id: feed.id,
cardTitle: feed.label,
type: feed.type,
headline: feed.headline ? feed.headline : '',
text: feed.desc ? feed.desc : '',
books: books,
event: event,
news: news,
kpi: kpi,
recommandation: recommandation,
};
}

View File

@@ -1,4 +1,5 @@
export interface Breadcrumb {
name: string;
path: string;
name: string;
path: string;
queryParams?: any;
}

View File

@@ -1,21 +1,4 @@
export interface FeedBook {
id: string;
firstBookEan: string;
firstBookAuthor: string;
firstBookTitle: string;
firstBookType: string;
firstBookTypeIcon: string;
firstBookLanguage: string;
firstBookPrice: string;
firstBookCurrency: string;
firstBookIcon: string;
secondBookEan?: string;
secondBookAuthor?: string;
secondBookTitle?: string;
secondBookType?: string;
secondBookTypeIcon?: string;
secondBookLanguage?: string;
secondBookPrice?: string;
secondBookIcon?: string;
secondBookCurrency?: string;
ean: string;
name: string;
}

View File

@@ -2,13 +2,16 @@ import { FeedBook } from './feed-book.model';
import { FeedEvent } from './feed-event.model';
import { FeedNews } from './feed-news.model';
import { FeedRecommandation } from './feed-recommandation.model';
import { FeedKpi } from './feed-kpi.model';
export interface FeedCard {
id: string;
cardTitle: string;
type: string;
books: any[];
headline: string;
text: string;
books: FeedBook[];
event: FeedEvent[];
news: FeedNews[];
kpi: FeedKpi;
recommandation: FeedRecommandation;
}

View File

@@ -0,0 +1,5 @@
export interface FeedKpi {
actual: number;
target: number;
percantage: number;
}

View File

@@ -1,6 +1,5 @@
import { State, Selector, Action, StateContext } from '@ngxs/store';
import { LoadFeed } from '../actions/feed.actions';
import { feedMock } from 'mock';
import { FeedCard } from '../../models/feed-card.model';
import { tap } from 'rxjs/operators';
import { Observable } from 'rxjs';
@@ -8,13 +7,11 @@ import { DashboardFeedService } from '../../services/dashboard-feed.service';
export class FeedStateModel {
feed: FeedCard[];
loading: boolean;
}
@State<FeedStateModel>({
name: 'feed',
defaults: {
loading: false,
feed: [],
},
})
@@ -23,25 +20,14 @@ export class FeedState {
@Selector()
static getFeed(state: FeedStateModel) {
// TODO: removed dashboard mock, remeber to delete commented line
// return [...state.feed, feedMock[3]];
return [...state.feed];
}
@Selector()
static loading(state: FeedStateModel) {
return state.loading;
}
@Action(LoadFeed)
load(ctx: StateContext<FeedStateModel>): Observable<FeedCard[]> {
ctx.patchState({
loading: true,
});
return this.feedService.info().pipe(
tap((feeds: FeedCard[]) => {
ctx.patchState({
loading: false,
feed: [...feeds],
});
})

View File

@@ -77,9 +77,15 @@ export function isArray(data: any) {
* function to check if array lenght has at least so many items as passed down with the minLenght
*/
export function isArrayMinLength(object: any, property: string, minLenght: number) {
const data = getProperty(object, property);
if (data && isArray(data)) {
return data.length > minLenght;
if (property) {
const data = getProperty(object, property);
if (data && isArray(data)) {
return data.length > minLenght;
}
} else {
if (isArray(object)) {
return object.length > minLenght;
}
}
return false;

View File

@@ -1,15 +1,14 @@
<div class="feed-container">
<div class="info">
<div class="title">Bestseller</div>
<div class="heading">Die aktuellen Bestseller</div>
<div class="text">Die meistverkauften Bücher, Hörbücher und eBooks auf einen Blick.</div>
<div class="title">{{ card.cardTitle }}</div>
<div class="heading">{{ card.headline }}</div>
<div class="text">{{ card.text }}</div>
</div>
<div class="books">
<ng-container *ngFor="let book of card.books; let last = last">
<img [src]="book.imgUrl | bookImageUrl | async" class="book-image" alt="book image" />
<img [src]="book.ean | bookImageUrl | async" class="book-image" alt="book image" (click)="details(book.ean, book.name)" />
<div class="last" *ngIf="last"></div>
</ng-container>
<!-- recommendation_tag -->
<lib-icon class="recommendation-icon" name="recommendation_tag" alt="recommendation icon" type="png" height="48px"></lib-icon>
<div class="book-layer-fade"></div>
</div>

View File

@@ -4,7 +4,7 @@
margin-top: 10px;
background-color: white;
border-radius: 4px;
height: 155px;
min-height: 155px;
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 55% 45%;
@@ -15,12 +15,13 @@
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding: 20px 0 0 25px;
padding: 20px 20px 20px 25px;
.title {
font-size: 15px;
font-weight: bold;
color: #172062;
text-transform: uppercase;
}
.heading {
@@ -45,20 +46,24 @@
height: 100%;
overflow-y: hidden;
overflow-x: scroll;
align-items: center;
justify-content: flex-start;
padding: 8px 0 0px 0px;
.book-image {
cursor: pointer;
margin: 7.5px 10px 0 0;
margin: 18px 10px 0 0;
margin: 0 10px 0 0;
display: block;
height: 140px;
width: 85px;
min-width: 85px;
border-radius: 8px;
}
.book-layer-fade {
position: absolute;
height: 155px;
width: 55px;
min-height: 155px;
min-width: 55px;
z-index: 20;
right: 15px;
opacity: 0.7;

View File

@@ -32,6 +32,7 @@ export class BookCardComponent implements OnInit {
const newBread: Breadcrumb = {
name: name.substring(0, 12) + (name.length > 12 ? '...' : ''),
path: '/product/details/' + ean + '?type=ean',
queryParams: { type: 'ean' },
};
this.store.dispatch(new AddBreadcrumb(newBread));

View File

@@ -0,0 +1,28 @@
<div class="kpi-container">
<div class="info">
<div class="title">{{ card.cardTitle }}</div>
<div class="heading">{{ card.kpi.actual }}</div>
<div class="text">von {{ card.kpi.target }} noch offen</div>
</div>
<div class="chart">
<circle-progress
[percent]="card.kpi.percantage"
[maxPercent]="100"
[radius]="45"
[space]="-12"
[outerStrokeWidth]="12"
[innerStrokeWidth]="12"
[outerStrokeColor]="'#34c6ab'"
[innerStrokeColor]="'#e9f0f8'"
[animation]="true"
[animationDuration]="400"
[imageHeight]="140"
[imageWidth]="140"
[showTitle]="false"
[showSubtitle]="false"
[showUnits]="false"
[responsive]="false"
[showZeroOuterStroke]="false"
></circle-progress>
</div>
</div>

View File

@@ -0,0 +1,53 @@
@import '../../../../../assets/scss/variables';
.kpi-container {
margin-top: 10px;
background-color: white;
border-radius: 4px;
height: 155px;
width: 50%;
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 50% 50%;
box-shadow: 0px 0px 10px 0px #dce2e9;
.info {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding: 20px 0 0 25px;
.title {
font-size: 15px;
font-weight: bold;
color: #172062;
text-transform: uppercase;
}
.heading {
font-weight: bold;
font-size: 54px;
color: #000000;
text-align: left;
margin-top: 15px;
line-height: 54px;
}
.text {
margin-top: 5px;
font-size: 14px;
color: #a7b9cb;
text-align: left;
line-height: 20px;
}
}
.chart {
height: 100%;
display: flex;
width: 100%;
justify-content: center;
align-items: center;
}
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { KpiCardComponent } from './kpi-card.component';
describe('KpiCardComponent', () => {
let component: KpiCardComponent;
let fixture: ComponentFixture<KpiCardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ KpiCardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(KpiCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { Component, OnInit, Input } from '@angular/core';
import { FeedCard } from '../../../../core/models/feed-card.model';
@Component({
selector: 'app-kpi-card',
templateUrl: './kpi-card.component.html',
styleUrls: ['./kpi-card.component.scss'],
})
export class KpiCardComponent implements OnInit {
@Input() card: FeedCard;
constructor() {}
ngOnInit() {}
}

View File

@@ -8,10 +8,19 @@ import { NewsCardComponent } from './components/news-card/news-card.component';
import { RecommandationCardComponent } from './components/recommandation-card/recommandation-card.component';
import { SharedModule } from '../../shared/shared.module';
import { LoadingModule, IconModule } from '@libs/ui';
import { KpiCardComponent } from './components/kpi-card/kpi-card.component';
import { NgCircleProgressModule } from 'ng-circle-progress';
@NgModule({
imports: [CommonModule, SharedModule, LoadingModule, IconModule],
imports: [CommonModule, SharedModule, LoadingModule, IconModule, NgCircleProgressModule],
exports: [DashboardComponent],
declarations: [DashboardComponent, BookCardComponent, EventCardComponent, NewsCardComponent, RecommandationCardComponent],
declarations: [
DashboardComponent,
BookCardComponent,
EventCardComponent,
NewsCardComponent,
RecommandationCardComponent,
KpiCardComponent,
],
providers: [],
})
export class DashboardModule {}

View File

@@ -1,15 +1,12 @@
<div [@stagger]="(feed$ | async).length">
<div class="card" *ngFor="let card of feed$ | async" class="card">
<div [@stagger]="feed.length">
<div class="card" *ngFor="let card of feed" class="card">
<ng-container *ngIf="card.type === 'products'">
<app-book-card *ngIf="card.type === 'products'" [card]="card"></app-book-card>
<app-book-card [card]="card"></app-book-card>
</ng-container>
<!-- <div *ngFor="let card of feed$ | async" class="card">
<app-book-card *ngIf="card.type === 'products'" [card]="card"></app-book-card>
<app-event-card *ngIf="card.type === 'events'" [card]="card"></app-event-card>
<app-news-card *ngIf="card.type === 'info'" [card]="card"></app-news-card>
<app-recommandation-card *ngIf="card.type === 'REC'" [card]="card"></app-recommandation-card>
</div> -->
<ng-container *ngIf="card.type === 'kpi'">
<app-kpi-card [card]="card"></app-kpi-card>
</ng-container>
</div>
</div>
<app-loading [loading]="loading$ | async" text="Inhalte werden geladen" class="loading"></app-loading>
<app-loading [loading]="loading" text="Inhalte werden geladen" class="loading"></app-loading>

View File

@@ -5,6 +5,8 @@ import { FeedCard } from '../../../core/models/feed-card.model';
import { LoadFeed } from '../../../core/store/actions/feed.actions';
import { FeedState } from '../../../core/store/state/feed.state';
import { staggerAnimation } from '../stagger.animation';
import { filter, take } from 'rxjs/operators';
import { isArrayMinLength } from '../../../core/utils/app.utils';
@Component({
selector: 'app-dashboard',
@@ -13,16 +15,23 @@ import { staggerAnimation } from '../stagger.animation';
animations: [staggerAnimation],
})
export class DashboardComponent implements OnInit {
@Select(FeedState.loading) loading$: Observable<boolean>;
@Select(FeedState.getFeed) feed$: Observable<FeedCard[]>;
feed: FeedCard[] = [];
loading = true;
constructor(private store: Store) {}
ngOnInit() {
this.store.dispatch(new LoadFeed());
// this.feed$.subscribe((feeds: FeedCard[]) => {
// console.log('Dash data', feeds);
// });
this.feed$
.pipe(
filter(data => isArrayMinLength(data, '', 1)),
take(1)
)
.subscribe((feeds: FeedCard[]) => {
this.loading = false;
this.feed = feeds;
});
}
}

View File

@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { ApiConfiguration } from './api-configuration';
import { PromotionService } from './services/promotion.service';
import { SearchService } from './services/search.service';
import { StockService } from './services/stock.service';
@@ -19,6 +20,7 @@ import { StockService } from './services/stock.service';
declarations: [],
providers: [
ApiConfiguration,
PromotionService,
SearchService,
StockService
],

View File

@@ -1,3 +1,7 @@
export { ResponseArgsOfIDictionaryOfInt64AndNullableOfInt32 } from './models/response-args-of-idictionary-of-int-64and-nullable-of-int-32';
export { ResponseArgs } from './models/response-args';
export { IPublicUserInfo } from './models/ipublic-user-info';
export { LesepunkteRequest } from './models/lesepunkte-request';
export { ListResponseArgsOfItemDTO } from './models/list-response-args-of-item-dto';
export { ResponseArgsOfIEnumerableOfItemDTO } from './models/response-args-of-ienumerable-of-item-dto';
export { ItemDTO } from './models/item-dto';
@@ -10,8 +14,6 @@ export { ShelfInfoDTO } from './models/shelf-info-dto';
export { ReviewDTO } from './models/review-dto';
export { EntityDTO } from './models/entity-dto';
export { EntityStatus } from './models/entity-status';
export { ResponseArgs } from './models/response-args';
export { IPublicUserInfo } from './models/ipublic-user-info';
export { QueryTokenDTO } from './models/query-token-dto';
export { OrderByDTO } from './models/order-by-dto';
export { ListResponseArgsOfAutocompleteDTO } from './models/list-response-args-of-autocomplete-dto';

View File

@@ -0,0 +1,5 @@
/* tslint:disable */
export interface LesepunkteRequest {
id: number;
quantity: number;
}

View File

@@ -0,0 +1,5 @@
/* tslint:disable */
import { ResponseArgs } from './response-args';
export interface ResponseArgsOfIDictionaryOfInt64AndNullableOfInt32 extends ResponseArgs {
result?: {[key: string]: number};
}

View File

@@ -1,2 +1,3 @@
export { PromotionService } from './services/promotion.service';
export { SearchService } from './services/search.service';
export { StockService } from './services/stock.service';

View File

@@ -0,0 +1,63 @@
/* tslint:disable */
import { Injectable } from '@angular/core';
import { HttpClient, HttpRequest, HttpResponse, HttpHeaders } from '@angular/common/http';
import { BaseService as __BaseService } from '../base-service';
import { ApiConfiguration as __Configuration } from '../api-configuration';
import { StrictHttpResponse as __StrictHttpResponse } from '../strict-http-response';
import { Observable as __Observable } from 'rxjs';
import { map as __map, filter as __filter } from 'rxjs/operators';
import { ResponseArgsOfIDictionaryOfInt64AndNullableOfInt32 } from '../models/response-args-of-idictionary-of-int-64and-nullable-of-int-32';
import { LesepunkteRequest } from '../models/lesepunkte-request';
@Injectable({
providedIn: 'root',
})
class PromotionService extends __BaseService {
static readonly PromotionLesepunktePath = '/promotion/lesepunkte';
constructor(
config: __Configuration,
http: HttpClient
) {
super(config, http);
}
/**
* @param items undefined
*/
PromotionLesepunkteResponse(items: Array<LesepunkteRequest>): __Observable<__StrictHttpResponse<ResponseArgsOfIDictionaryOfInt64AndNullableOfInt32>> {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
__body = items;
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/promotion/lesepunkte`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfIDictionaryOfInt64AndNullableOfInt32>;
})
);
}
/**
* @param items undefined
*/
PromotionLesepunkte(items: Array<LesepunkteRequest>): __Observable<ResponseArgsOfIDictionaryOfInt64AndNullableOfInt32> {
return this.PromotionLesepunkteResponse(items).pipe(
__map(_r => _r.body as ResponseArgsOfIDictionaryOfInt64AndNullableOfInt32)
);
}
}
module PromotionService {
}
export { PromotionService }

View File

@@ -36,6 +36,7 @@
"core-js": "^2.6.5",
"faker": "^4.1.0",
"hammerjs": "^2.0.8",
"ng-circle-progress": "^1.4.0",
"ng-connection-service": "^1.0.4",
"ngx-infinite-scroll": "^7.0.1",
"rxjs": "~6.4.0",