[HIMA-110] moved application files to apps/sales/ & changed import paths to match new paths

This commit is contained in:
Eraldo Hasanaj
2019-03-04 13:46:41 +01:00
parent c94fc11147
commit 583906a711
414 changed files with 3027 additions and 1886 deletions

View File

@@ -0,0 +1,28 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.e2e.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

View File

@@ -0,0 +1,14 @@
import { AppPage } from './app.po';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('Welcome to sales!');
});
});

View File

@@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get('/');
}
getTitleText() {
return element(by.css('app-root h1')).getText();
}
}

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

11
apps/sales/browserslist Normal file
View File

@@ -0,0 +1,11 @@
# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11

31
apps/sales/karma.conf.js Normal file
View File

@@ -0,0 +1,31 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

View File

@@ -0,0 +1,29 @@
{
"index": "/index.html",
"assetGroups": [
{
"name": "sales",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/*.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
]
}
}
]
}

View File

@@ -0,0 +1,39 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './modules/dashboard/dashboard.component';
import {
CustomerSearchComponent,
SearchCustomerResultComponent,
EditCustomerCardComponent,
EditBillingAddressComponent
} from './modules/customer-search';
import { ProductDetailsComponent } from './components/product-details/product-details.component';
import { BarcodeSearchComponent } from './modules/barcode-search/barcode-search.component';
import { CartReviewComponent } from './modules/cart/components/cart-review/cart-review.component';
import { ArticleSearchComponent } from './modules/article-search/article-search.component';
import { SearchResultsComponent } from './components/search-results/search-results.component';
import { CartConfirmationComponent } from './modules/cart';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'article-search', component: ArticleSearchComponent },
{ path: 'customer-search', component: CustomerSearchComponent },
{ path: 'customer-search-result', component: SearchCustomerResultComponent },
{ path: 'customer-edit/:id', component: EditCustomerCardComponent },
{ path: 'customer-edit/:id/billing', component: EditBillingAddressComponent },
{ path: 'search-results#start', component: SearchResultsComponent },
{ path: 'product-details/:id', component: ProductDetailsComponent },
{ path: 'article-scan', component: BarcodeSearchComponent },
{ path: 'debug', loadChildren: './modules/debug/debug.module#DebugModule' },
{ path: 'cart', component: CartReviewComponent },
{ path: 'cart-confirmation', component: CartConfirmationComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(routes, { scrollPositionRestoration: 'enabled' })
],
exports: [RouterModule]
})
export class AppRoutingModule {}

View File

@@ -0,0 +1,4 @@
<sales-header></sales-header>
<sales-content [isConnected]="isConnected"></sales-content>
<sales-menu></sales-menu>

View File

@@ -0,0 +1,5 @@
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}

View File

@@ -0,0 +1,35 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'Hugendubel InstoreApp'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('Hugendubel InstoreApp');
});
it('should render title in a h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to Hugendubel InstoreApp!');
});
});

View File

@@ -0,0 +1,24 @@
import { Component, ViewChild } from '@angular/core';
import { ConnectionService } from 'ng-connection-service';
@Component({
selector: 'sales-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'Hugendubel InstoreApp';
status = 'ONLINE';
isConnected = true;
constructor(private connectionService: ConnectionService) {
this.connectionService.monitor().subscribe(isConnected => {
this.isConnected = isConnected;
if (this.isConnected) {
this.status = 'ONLINE';
} else {
this.status = 'OFFLINE';
}
});
}
}

View File

@@ -0,0 +1,129 @@
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ComponentsModule } from './modules/components.module';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import {
BasicAuthorizationInterceptor,
BasicAuthorizationOptions
} from './core/interceptors';
import {
CatServiceModule,
CAT_SERVICE_ENDPOINT,
CatSearchService,
CAT_AV_SERVICE_ENDPOINT
} from 'cat-service';
import { ConfigService } from './core/services/config.service';
import {
FeedServiceModule,
FEED_SERVICE_ENDPOINT,
FeedService,
FeedMockService
} from 'feed-service';
import { NgxsModule } from '@ngxs/store';
import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin';
import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
import { FeedState } from './core/store/state/feed.state';
import { ProcessState } from './core/store/state/process.state';
import { BreadcrumbsState } from './core/store/state/breadcrumbs.state';
import { FilterState } from './core/store/state/filter.state';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { SharedModule } from './shared/shared.module';
import { AutocompleteState } from './core/store/state/autocomplete.state';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { NotifierState } from './core/store/state/notifier.state';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
const states = [
FeedState,
ProcessState,
BreadcrumbsState,
FilterState,
AutocompleteState,
NotifierState
];
export function _configInitializer(conf: ConfigService) {
// load config from /assets/config.json
return () => conf.load();
}
export function _basicAuthorizationInterceptorFactory(conf: ConfigService) {
return new BasicAuthorizationInterceptor(
conf.select<BasicAuthorizationOptions>('basicAuthorization')
);
}
export function _catServiceEndpointProviderFactory(conf: ConfigService) {
return conf.select<string>('catService', 'endpoint', 'catService');
}
export function _catAvServiceEndpointProviderFactory(conf: ConfigService) {
return conf.select<string>('catService', 'endpoint', 'avService');
}
export function _feedServiceEndpointProviderFactory(conf: ConfigService) {
return conf.select<string>('feedService', 'endpoint');
}
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
ComponentsModule,
SharedModule,
HttpClientModule,
NgxsModule.forRoot(states, { developmentMode: !environment.production }),
NgxsReduxDevtoolsPluginModule.forRoot(),
NgxsLoggerPluginModule,
CatServiceModule,
FeedServiceModule,
FormsModule,
ReactiveFormsModule,
SharedModule,
ScrollingModule,
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production
})
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: _configInitializer,
multi: true,
deps: [ConfigService]
},
{
provide: HTTP_INTERCEPTORS,
useFactory: _basicAuthorizationInterceptorFactory,
deps: [ConfigService],
multi: true
},
{
provide: CAT_SERVICE_ENDPOINT,
useFactory: _catServiceEndpointProviderFactory,
deps: [ConfigService]
},
{
provide: CAT_AV_SERVICE_ENDPOINT,
useFactory: _catAvServiceEndpointProviderFactory,
deps: [ConfigService]
},
{
provide: FEED_SERVICE_ENDPOINT,
useFactory: _feedServiceEndpointProviderFactory,
deps: [ConfigService]
}
// { provide: CatSearchService, useClass: CatSearchMockService }, // Uncomment if u want to use the CatSearchMockService
// { provide: FeedService, useClass: FeedMockService } // Uncomment if u want to use the FeedMockService
],
bootstrap: [AppComponent]
})
export class AppModule {}

View File

@@ -0,0 +1,33 @@
<div class="breadacrumb-grid" [ngClass]="{'grid-with-arrow': !backArrow, 'breadcumb-mb-5': lowerMargin}">
<!-- <sales-back-arrow (back)="addOne()"></sales-back-arrow> TESTING ANIMATION PURPOSES-->
<sales-back-arrow
*ngIf="backArrow"
(back)="goBack(breadcrumbs[breadcrumbs.length - 2])"
class="align-right back-arrow"
></sales-back-arrow>
<div
class="align-center breadcrumb-container"
[ngClass]="{ 'with-arrow': backArrow }"
>
<span
*ngFor="let breadcrumb of breadcrumbs; let i = index"
class="breadcrumb hide"
(click)="selectBreadcrumb(breadcrumb)"
[ngClass]="{ selected: selectedBreadCrumbIndex === i, show: 1 === 1 }"
>
<span
class="breadcrumb more"
*ngIf="firstVisibleItem > 0 && i === firstVisibleItem"
(click)="selectBreadcrumb(breadcrumbs)"
>...
</span>
<img
class="next"
src="../../../assets/images/Arrow_Next.svg"
alt="next"
*ngIf="this.breadcrumbs.indexOf(breadcrumb) > 0 && i != 0 && i !== this.breadcrumbs.indexOf(breadcrumb) - 4"
/>
<span>{{ breadcrumb.name }}</span>
</span>
</div>
</div>

View File

@@ -0,0 +1,81 @@
@import '../../../assets/scss/variables';
.breadacrumb-grid {
display: grid;
grid-template-columns: min-content auto;
margin-bottom: 16px;
}
.breadcumb-mb-5 {
margin-bottom: 5px;
}
.grid-with-arrow {
grid-template-columns: auto;
}
.breadcrumb-container {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
}
.with-arrow {
width: 86.5%;
}
.breadcrumb {
outline: none;
font-size: 16px;
color: #5a728a;
line-height: 21px;
padding: 10px 0;
}
.hide {
display: none;
opacity: 0;
&:nth-last-child(4) {
/*declarations*/
display: block;
overflow: hidden;
opacity: 0;
flex: 0.0001;
animation: fadeSlide 400ms;
}
&:nth-last-child(-n + 3) {
/*declarations*/
opacity: 1;
display: block;
}
}
@keyframes fadeSlide {
0% {
opacity: 1;
transform: translateX(80px);
flex: 0.5;
overflow: hidden;
}
50% {
opacity: 0;
}
100% {
transform: translateX(0);
flex: 0.0001;
}
}
.back-arrow {
padding: 10px 0;
}
.next {
padding: 0 7px;
}
.selected {
font-weight: bold;
}

View File

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

View File

@@ -0,0 +1,97 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Breadcrumb } from 'apps/sales/src/app/core/models/breadcrumb.model';
import { Process } from 'apps/sales/src/app/core/models/process.model';
import { Select, Store } from '@ngxs/store';
import { ProcessState } from 'apps/sales/src/app/core/store/state/process.state';
import {
PopBreadcrumbsAfterCurrent,
ChangeCurrentRoute
} from 'apps/sales/src/app/core/store/actions/process.actions';
import { Router } from '@angular/router';
@Component({
selector: 'sales-breadcrumbs',
templateUrl: './breadcrumbs.component.html',
styleUrls: ['./breadcrumbs.component.scss']
})
export class BreadcrumbsComponent implements OnInit {
@Select(ProcessState.getProcesses) processes$: Observable<Process[]>;
breadcrumbs: Breadcrumb[] = [];
// breadcrumbs: Breadcrumb[] = ['one'].map(i => ({ name: i, path: './i' }));
currentRoute = '';
start: number;
end: number;
get firstVisibleItem() {
if (this.start) {
return this.start;
}
return this.breadcrumbs.length - 3;
}
get lastVisibleItem() {
if (this.end) {
return this.end;
}
return this.breadcrumbs.length - 1;
}
get backArrow() {
return this.router.url.substring(0, 16) === '/product-details';
}
get selectedBreadCrumbIndex() {
return this.breadcrumbs.findIndex(
t => t.path.indexOf(this.currentRoute) >= 0
);
}
get lowerMargin() {
if (this.router.url === '/customer-search-result') {
return true;
}
if (this.router.url.substring(0, 14) === '/customer-edit') {
return true;
}
if (this.router.url.substring(0, 16) === '/product-details') {
return true;
}
return false;
}
constructor(private store: Store, private router: Router) {
this.processes$.subscribe((data: Process[]) =>
this.getBreadcrumbsFromCurentProcess(data)
);
}
getBreadcrumbsFromCurentProcess(processes: Process[]) {
const currentProcess = processes.find(p => p.selected === true);
if (currentProcess) {
this.currentRoute = `${currentProcess.currentRoute}`;
this.breadcrumbs = currentProcess.breadcrumbs;
}
}
selectBreadcrumb(breadcrumb: Breadcrumb) {
// this.store.dispatch(new PopBreadcrumbsAfterCurrent(breadcrumb));
this.store.dispatch(new ChangeCurrentRoute(breadcrumb.path, false));
this.router.navigate([breadcrumb.path]);
}
goBack(breadcrumb: Breadcrumb) {
// this.store.dispatch(new PopBreadcrumbsAfterCurrent(breadcrumb));
this.store.dispatch(new ChangeCurrentRoute(breadcrumb.path, false));
this.router.navigate(['/search-results#start']);
}
ngOnInit() {}
addOne() {
this.breadcrumbs.push({
name: 'test' + this.breadcrumbs.length,
path: './i'
});
}
}

View File

@@ -0,0 +1,98 @@
<sales-modal id="checkout-modal">
<div class="modal-step-1" *ngIf="stepOne">
<div class="header">
<h1>Wie möchten Sie den Artikel erhalten?</h1>
<img (click)="closeModal()" class="close-icon" src="../../../assets/images/close.svg" alt="close">
</div>
<div class="body">
<div class="option">
<img class="img" src="../../../assets/images/Take_now.svg" alt="take now">
<h2 class="title-take-now">Jetzt mitnehmen</h2>
<span class="description description-take-now">Möchten Sie den Artikel jetzt gleich mit nach Hause nehmen?</span>
<span class="price price-take-now">{{ currentPrice }} {{ currency }}</span>
<!-- <a class="btn btn-active" (click)="selectedAction('mitnehmen')">Auswählen</a> -->
<a class="btn">Auswählen</a>
</div>
<div class="option">
<img class="img" src="../../../assets/images/Package_Icon.svg" alt="package">
<h2>Abholung</h2>
<span class="description description-take-away">Möchten Sie den Artikel in einer unserer Fillialen abholen?</span>
<div class="dropdown-select-text" (click)="openDropdown(dropdown)" #selectedText [class.dropdown-select-text-active]="displayDropdown">
<span class="location location-take-away">{{ currentLocation.name }}</span>
<img class="dropdown-icon" src="../../../assets/images/Arrow_Down_2.svg" alt="arrow" *ngIf="!displayDropdown">
<img class="dropdown-icon" src="../../../assets/images/Arrow_Up.svg" alt="arrow" *ngIf="displayDropdown">
</div>
<span class="location location-date-take-away">Lieferdatum {{ currentPickUpDate }}</span>
<div class="location location-dropdown" #dropdown>
<ul>
<li *ngFor="let item of locations; let i = index" (click)="selectLocation(i, dropdown)">{{ item.name }}</li>
</ul>
</div>
<span class="price price-take-away">{{ currentPrice }} {{ currency }}</span>
<!-- <a class="btn" (click)="selectedAction('abholung')">Auswählen</a> -->
<a class="btn">Auswählen</a>
</div>
<div class="option">
<img class="img" src="../../../assets/images/truck_Icon.svg" alt="truck">
<h2>Versand</h2>
<span class="description description-delivery">Möchten Sie den Artikel nach Hause geliefert bekommen?</span>
<div class="delivery" (click)="openDropdown(dropdown)"><img class="check" src="../../../assets/images/Check-green.svg" alt="arrow">Versandkostenfrei</div>
<span class="delivery-date">Lieferdatum {{ currentPickUpDate }}</span>
<span class="price price-order">{{ currentPrice }} {{ currency }}</span>
<a class="btn btn-active" (click)="selectedAction('versand')">Auswählen</a>
</div>
</div>
</div>
<div class="modal-step-2" *ngIf="!stepOne">
<div class="header">
<h1>Artikel wurde dem Warenkorb hinzugefügt</h1>
<img (click)="closeModal()" class="close-icon" src="../../../assets/images/close.svg" alt="close">
</div>
<div class="modal-body">
<div class="body-heading">
<img (click)="closeModal()" class="close-icon" src="../../../assets/images/{{ stepTwoImgType }}" alt="truck">
<h1>Versand</h1>
</div>
<div class="line"></div>
<div class="body-content">
<img src="{{ imgUrl }}" alt="book">
<span class="book-title">{{ book.pr.name }}</span>
<div class="order-details">
<span><img class="order-book-icon" src="../../../assets/images/Book_Icon.svg" alt="book-icon"> {{ book.pr.manufacturer }} I {{ book.pr.contributors }}</span>
<span class="order-details-delivery-info">DHL I Lieferung 18.01.</span>
</div>
<span class="price">{{ currentPrice }} {{ currency }}</span>
<div class="dropdown_container">
<div (click)="openDropdown(dropdown)" class="dropdown-selected-text" [class.dropdown-selected-text-active]="displayDropdown">
<span class="">{{ currentNumberOfItems }}</span>
<img class="dropdown-icon" src="../../../assets/images/Arrow_Down_2.svg" alt="arrow" *ngIf="!displayDropdown">
<img class="dropdown-icon" src="../../../assets/images/Arrow_Up.svg" alt="arrow" *ngIf="displayDropdown">
</div>
<div class="dropdown-options" #dropdown>
<ul>
<li *ngFor="let item of possibleItems" (click)="setNumberOfItems(item, dropdown)">{{ item }}</li>
</ul>
</div>
</div>
<a class="btn" (click)="switchSteps(true)">Ändern</a>
</div>
<div class="line bottom-line"></div>
<div class="overview">
<span class="items">{{ displayItemsNumber }} Artikel I {{ currentPoints }} Lesepunkte</span>
<div class="overview-price-container">
<span class="overview-price">Zwischensumme {{ currentPrice }} {{ currency }}</span>
<span class="overview-tax">ohne Versandkosten</span>
</div>
</div>
</div>
<div class="modal-footer">
<a class="btn secondary" (click)="updateCart()">Weiter einkaufen</a>
<a class="btn active" (click)="itemsConfirmed()">Bezahlen</a>
</div>
</div>
</sales-modal>

View File

@@ -0,0 +1,516 @@
// COMMON STYLES
.modal-step-1,
.modal-step-2 {
font-family: 'Open Sans';
line-height: 21px;
margin: 16px 0;
display: flex;
align-items: center;
flex-direction: column;
h1 {
font-size: 20px;
font-weight: bold;
}
.header {
.close-icon {
position: absolute;
top: 25px;
right: 25px;
height: 21px;
}
.close-icon:hover {
cursor: pointer;
}
}
}
// FIRST STEP DESIGN
.modal-step-1 {
height: 479px;
width: 728px;
justify-content: center;
.header {
padding-top: 20px;
}
.body {
width: 100%;
height: 100%;
display: flex;
justify-content: space-around;
align-items: center;
text-align: center;
.option {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100%;
.description {
font-size: 16px;
max-width: 193px;
margin-bottom: 25px;
&-take-away {
position: relative;
top: 12px;
}
&-take-now {
position: relative;
top: 30px;
}
&-delivery {
position: relative;
top: 9px;
}
}
h2 {
font-size: 26px;
font-weight: bold;
position: absolute;
top: 156px;
}
.title-take-now {
width: 153px;
line-height: 32px;
}
.img {
display: block;
position: absolute;
top: 112px;
}
.dropdown-icon {
height: 9px;
width: 17px;
}
.check {
height: 12px;
width: 16px;
padding-right: 5px;
}
.price {
font-size: 20px;
font-weight: bold;
// margin-bottom: 20px;
&-take-away {
position: relative;
top: 28px;
}
&-take-now {
position: relative;
top: 57px;
}
&-order {
position: relative;
top: 33px;
}
}
.btn {
font-family: 'Open Sans';
font-size: 18px;
font-weight: bold;
color: #f70400;
width: 149px;
padding: 12px;
cursor: pointer;
position: absolute;
bottom: 25px;
&-active {
background-color: #f70400;
border: none;
border-radius: 25px;
color: #ffffff;
}
}
.delivery {
display: flex;
justify-content: space-between;
align-items: center;
width: 165px;
font-size: 16px;
cursor: pointer;
text-align: left;
position: relative;
top: 7px;
&-date {
font-size: 14px;
font-weight: 300;
width: 165px;
position: relative;
top: 10px;
}
}
.location {
display: flex;
justify-content: space-between;
align-items: center;
width: 135px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
text-align: left;
&-take-away {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
&-date-take-away {
font-size: 14px;
font-weight: 300;
cursor: default;
width: 165px;
text-align: left;
padding-top: 3px;
position: relative;
top: 5px;
left: 7px;
}
&-dropdown {
display: flex;
flex-direction: column;
position: fixed;
top: 344px;
left: 224px;
z-index: 10;
display: none;
cursor: pointer;
width: 225px;
background-color: #ffffff;
border-radius: 5px;
box-shadow: 0px -2px 24px 0px #dce2e9;
overflow: hidden;
ul {
list-style: none;
width: 100%;
padding: 15px 10px;
margin: 0;
li {
// padding: 5px 0 5px 20px;
padding: 7px 10px;
font-weight: 300;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
&:hover {
background-color: #E9EDF9;
}
}
}
span {
padding: 5px 0 5px 20px;
font-weight: 300;
width: 100%;
&:hover {
background-color: #E9EDF9;
}
}
}
}
.dropdown-select-text {
display: flex;
justify-content: center;
align-items: center;
position: relative;
top: 9px;
padding: 5px;
&-active {
background-color: #E9EDF9;
}
&:hover {
cursor: pointer;
}
}
}
}
}
// SECOND STEP DESIGN
.modal-step-2 {
height: 394px;
width: 728px;
justify-content: flex-start;
.header {
h1 {
margin-top: 30px;
}
}
.modal-body {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
flex-direction: column;
margin-top: 15px;
.body-heading {
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
margin-bottom: 8px;
img {
height: 16px;
width: 26px;
margin-right: 13px;
margin-top: 3px;
margin-left: 25px;
}
}
}
.line {
height: 3px;
width: 100%;
background-image: url('../../../assets/images/Line.svg');
background-repeat: repeat-x;
}
.body-content {
margin-top: 15px;
display: flex;
justify-content: space-around;
align-items: center;
width: 97%;
text-align: left;
img {
height: 39px;
width: 24px;
}
.book-title {
font-size: 16px;
font-weight: 600;
text-overflow: ellipsis;
overflow: hidden;
width: 112px;
white-space: nowrap;
}
.order-details {
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
height: 50px;
width: 215px;
position: relative;
top: 13px;
.order-book-icon {
height: 18px;
width: 13px;
position: relative;
top: 2px;
padding-right: 8px;
}
span {
font-size: 16px;
text-align: left;
line-height: 25px;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&-delivery-info {
font-weight: 600;
}
}
.price {
font-weight: 600;
font-size: 16px;
line-height: 25px;
}
.dropdown_container {
display: flex;
justify-content: space-between;
align-items: center;
width: 35px;
&:hover {
cursor: pointer;
}
img {
height: 9px;
width: 17px;
padding-left: 5px;
}
.dropdown-selected-text {
padding: 5px;
font-weight: 600;
&-active {
background-color: #E9EDF9;
}
}
.dropdown-options {
display: flex;
flex-direction: column;
position: fixed;
top: 220px;
z-index: 10;
display: none;
cursor: pointer;
text-align: left;
right: 115px;
ul {
list-style: none;
padding: 15px 10px;
margin: 0;
background-color: #ffffff;
border-radius: 5px;
box-shadow: 0px -2px 24px 0px #dce2e9;
padding: 15px 0;
width: 60px;
li {
padding: 7px 10px;
font-weight: 300;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
&:hover {
background-color: #E9EDF9;
}
}
}
}
}
.btn {
font-size: 18px;
font-weight: bold;
color: #f70400;
cursor: pointer;
}
}
.bottom-line {
margin-top: 30px;
}
.overview {
display: flex;
justify-content: space-between;
justify-items: center;
width: 93%;
margin-top: 30px;
.items {
font-size: 16px;
font-weight: 600;
color: rgba(167, 185, 203, 1);
text-align: left;
}
.overview-price-container {
display: flex;
justify-content: flex-start;
justify-items: center;
flex-direction: column;
.overview-price {
font-size: 18px;
font-weight: bold;
text-align: left;
}
.overview-tax {
font-size: 14px;
text-align: right;
}
}
}
.modal-footer {
display: flex;
justify-content: flex-end;
align-items: center;
width: 93%;
margin-top: 30px;
.btn {
font-family: 'Open Sans';
font-size: 18px;
font-weight: bold;
color: #f70400;
cursor: pointer;
padding: 14px;
text-align: center;
}
.secondary {
width: 160px;
&:hover {
background-color: #f70400;
border: none;
border-radius: 25px;
color: #ffffff;
}
}
.active {
background-color: #f70400;
border: none;
border-radius: 25px;
color: #ffffff;
margin-left: 30px;
width: 121px;
}
}
}

View File

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

View File

@@ -0,0 +1,177 @@
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
import { ModalService } from '../../core/services/modal.service';
import { Router } from '@angular/router';
import { Store } from '@ngxs/store';
import {
ChangeCurrentRoute,
SetCartData
} from 'apps/sales/src/app/core/store/actions/process.actions';
import { ItemDTO, CatImageService } from 'cat-service';
const points = 60;
@Component({
selector: 'sales-checkout',
templateUrl: './checkout.component.html',
styleUrls: ['./checkout.component.scss']
})
export class CheckoutComponent implements OnInit {
id = 'checkout-modal';
stepOne = true;
stepTwoImgType = '';
// Step one mock data
locations = [
{ name: 'München Karlsplatz', date: '01.05.2019' },
{ name: 'München Marienplatz', date: '01.05.2019' },
{ name: 'München Ollenhauerstraße', date: '01.05.2019' },
{ name: 'München Pasing Bahnhofsplatz', date: '01.05.2019' },
{ name: 'München Theatinerstraße', date: '01.05.2019' }
];
currentLocation = this.locations[0];
currentPickUpDate = this.locations[0].date;
imgUrl = '';
// Step two mock data
currentNumberOfItems = 1;
possibleItems = [1, 2, 3, 4];
displayItemsNumber = 1;
currentPrice = '';
currentPoints = points;
currency = '';
// Toggle for dropdown
displayDropdown = false;
// Trigger other functionality if needed
@Output() closed: EventEmitter<boolean> = new EventEmitter();
private _book: ItemDTO;
@Input() set book(val: ItemDTO) {
if (val) {
this._book = val;
this.catImageService.getImageUrl(val.pr.ean).subscribe((url: string) => {
this.imgUrl = url;
});
}
}
get book() {
return this._book;
}
constructor(
private modalService: ModalService,
private store: Store,
private router: Router,
private catImageService: CatImageService
) {}
ngOnInit() {
this.currentPrice = this.bookPriceString();
this.currency = this.book.av[0].price.value.currency;
}
// STEP ONE
selectLocation(locationIndx: number, dropdown: any) {
this.currentLocation = this.locations[locationIndx];
this.currentPickUpDate = this.currentLocation.date;
this.toggleDropdown(dropdown);
}
selectedAction(action: string) {
if (action === 'mitnehmen') {
this.stepTwoImgType = 'Take_now.svg';
} else if (action === 'abholung') {
this.stepTwoImgType = 'Package_Icon.svg';
} else {
this.stepTwoImgType = 'truck_Icon.svg';
}
this.switchSteps();
}
// STEP TWO
setNumberOfItems(numberOfItems: number, element: any) {
this.currentNumberOfItems = numberOfItems;
this.displayItemsNumber = numberOfItems;
this.currentPoints = numberOfItems * points;
this.currentPrice = (
Math.round(this.bookPrice() * numberOfItems * 100) / 100
)
.toLocaleString()
.replace('.', ',');
this.toggleDropdown(element);
}
updateCart() {
this.store.dispatch(
new SetCartData(this.currentNumberOfItems, this._book, {
name: 'Artikelsuche',
path: '/article-search'
})
);
this.store.dispatch(new ChangeCurrentRoute('article-search'));
this.router.navigate(['article-search']);
this.closeModal();
}
itemsConfirmed() {
this.store.dispatch(
new SetCartData(this.currentNumberOfItems, this._book, {
name: 'Kundensuche',
path: '/customer-search'
})
);
this.store.dispatch(new ChangeCurrentRoute('customer-search'));
this.router.navigate(['customer-search']);
this.closeModal();
}
// COMMON
openDropdown(element: any) {
this.toggleDropdown(element);
}
openDialog() {
this.modalService.open(this.id);
}
closeModal(dialogSubmited: boolean = false) {
this.closed.emit(dialogSubmited);
this.modalService.close(this.id);
this.defaultValues();
this.stepOne = true;
}
switchSteps(reset: boolean = false) {
if (reset) {
this.defaultValues();
}
this.displayDropdown = false;
this.stepOne = !this.stepOne;
}
private toggleDropdown(element: any) {
element.style.display = this.displayDropdown ? 'none' : 'flex';
this.displayDropdown = !this.displayDropdown;
}
private defaultValues() {
this.currentNumberOfItems = 1;
this.displayItemsNumber = 1;
this.currentPoints = points;
this.currentPrice = this.bookPriceString();
}
private bookPrice(): number {
return this._book.av[0].price.value.value;
}
private bookPriceString(): string {
const formatedPrice = (
+Math.round(this._book.av[0].price.value.value * 100) / 100
).toFixed(2);
return formatedPrice.toLocaleString().replace('.', ',');
}
}

View File

@@ -0,0 +1,36 @@
<div class="app-header px-16">
<div class="three-col-grid-container">
<div class="align-left">
<a routerLink="/dashboard">
<img class="logo-icon" src="/assets/images/Hugendubel_Logo-3x.png" />
</a>
</div>
<div class="align-center">
<div class="two-col-grid-container pt-5">
<a class="align-right" routerLink="/dashboard">
<img class="header-icon" src="/assets/images/Infoboard.svg" />
</a>
<div class="align-left">
<img class="header-icon" src="/assets/images/Notifictation.svg" />
</div>
</div>
</div>
<div class="align-right">
<div class="three-col-grid-container-fixed pt-5">
<div class="align-right">
<img
class="header-icon profile-icon"
src="/assets/images/Icon_Kundensuche.svg"
/>
</div>
<div class="align-right pt-3">
<span class="profile-name">{{ customer }}</span>
</div>
<div class="align-right pt-3 pr-4">
<img class="header-icon" src="/assets/images/Dots.svg" />
</div>
</div>
</div>
</div>
<sales-process-header></sales-process-header>
</div>

View File

@@ -0,0 +1,71 @@
@import '../../../assets/scss/variables';
.app-header {
position: fixed;
top: 0;
width: 98%;
background-color: white;
height: 105px;
padding-top: 40px;
z-index: 100;
box-shadow: 0px 2px 6px 0px #dde5ec;
}
.logo-icon {
width: 155px;
}
@media screen and (max-width: 1275px) {
.app-header {
width: 98%;
}
}
@media screen and (max-width: 1105px) {
.app-header {
width: 97%;
}
}
@media screen and (max-width: 827px) {
.app-header {
width: 96%;
}
}
@media screen and (max-width: 461px) {
.app-header {
width: 95%;
}
}
.three-col-grid-container {
display: grid;
grid-template-columns: auto auto auto;
}
.three-col-grid-container-fixed {
display: grid;
grid-template-columns: auto max-content 40px;
grid-column-gap: 1vh;
}
.profile-name {
font-weight: bold;
color: $color-active;
font-size: 16px;
}
.two-col-grid-container {
display: grid;
grid-template-columns: auto auto;
grid-column-gap: 2vh;
}
.grid-item {
background-color: rgba(255, 255, 255, 0.8);
}
.header-icon {
width: 25px;
}

View File

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

View File

@@ -0,0 +1,16 @@
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'sales-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
constructor() { }
customer = 'Kunden';
ngOnInit() {
}
}

View File

@@ -0,0 +1,54 @@
<div class="menu">
<div class="menu-grid">
<div class="menu-item-grid align-center" (click)="routeToMenu('/article-search', 'articlesearch')">
<div>
<img class="menu-icon" *ngIf="router.url === '/article-search' ||
router.url.startsWith('/search-results') ||
router.url.startsWith('/product-details');
else articleSearchImageElse" src="/assets/images/Icon_Artikelsuche.svg">
<ng-template #articleSearchImageElse>
<img class="menu-icon" src="/assets/images/Icon_Artikelsuche_inactive.svg">
</ng-template>
</div>
<span *ngIf="router.url === '/article-search' ||
router.url.startsWith('/search-results') ||
router.url.startsWith('/product-details');
else articleSearchLabelElse" class="menu-item selected">Artikelsuche</span>
<ng-template #articleSearchLabelElse>
<span class="menu-item">Artikelsuche</span>
</ng-template>
</div>
<div class="menu-item-grid align-center" (click)="routeToMenu('/customer-search', 'customersearch')">
<div>
<img class="menu-icon" *ngIf="router.url === '/customer-search'; else customerSearchImageElse"
src="/assets/images/Icon_Kundensuche.svg">
<ng-template #customerSearchImageElse>
<img class="menu-icon" src="/assets/images/Icon_Kundensuche_inactive.svg">
</ng-template>
</div>
<span *ngIf="router.url === '/customer-search'; else customerSearchLabelElse"
class="menu-item selected">Kundensuche</span>
<ng-template #customerSearchLabelElse>
<span class="menu-item">Kundensuche</span>
</ng-template>
</div>
<div class="menu-item-grid align-center">
<div>
<img class="menu-icon" src="/assets/images/Icon_Abholfach_inactive.svg">
</div>
<span class="menu-item">Abholfach</span>
</div>
<div class="menu-item-grid align-center">
<div>
<img class="menu-icon" src="/assets/images/Icon_Retoure_inactive.svg">
</div>
<span class="menu-item">Retoure</span>
</div>
<div class="menu-item-grid align-center">
<div>
<img class="menu-icon" src="/assets/images/Icon_DragDrop_inactive.svg">
</div>
<span class="menu-item">Drag & Drop</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,39 @@
@import '../../../assets/scss/variables';
.menu {
position: fixed;
bottom: 0;
width: 100%;
height: 80px;
background-color: white;
z-index: 100;
box-shadow: 0px -2px 6px 0px #dde5ec;
}
.menu-grid {
display: grid;
grid-template-columns: 20% 20% 20% 20% 20%;
padding-top: 20px;
}
.menu-item-grid {
display: grid;
grid-template-columns: auto;
outline: none;
}
.menu-item {
font-size: 15px;
line-height: 21px;
font-weight: 600;
color: $color-inactive;
}
.selected {
color: $color-active;
}
.menu-icon {
width: 34px;
height: 24px;
}

View File

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

View File

@@ -0,0 +1,78 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { Process } from 'apps/sales/src/app/core/models/process.model';
import { Store, Select } from '@ngxs/store';
import { ProcessState } from 'apps/sales/src/app/core/store/state/process.state';
import {
AddProcess,
ChangeCurrentRoute,
ResetBreadcrumbsTo
} from 'apps/sales/src/app/core/store/actions/process.actions';
import { getRandomPic } from 'apps/sales/src/app/core/utils/process.util';
import { Breadcrumb } from 'apps/sales/src/app/core/models/breadcrumb.model';
@Component({
selector: 'sales-menu',
templateUrl: './menu.component.html',
styleUrls: ['./menu.component.scss']
})
export class MenuComponent implements OnInit {
@Select(ProcessState.getProcesses) processes$: Observable<Process[]>;
processes: Process[];
constructor(public router: Router, private store: Store) {}
activeMenu = '';
routeToMenu(menuPath: string, menuTag: string): void {
if (this.processes.length < 1) {
this.createProcess(menuPath);
}
this.activeMenu = menuTag;
this.store.dispatch(
new ResetBreadcrumbsTo({
name: this.nameFromPath(menuPath),
path: this.routeFromPath(menuPath)
})
);
this.store.dispatch(new ChangeCurrentRoute(this.routeFromPath(menuPath)));
this.router.navigate([menuPath]);
}
createProcess(menuPath: string) {
const newProcess = <Process>{
id: 1,
name: '# 1',
selected: true,
icon: getRandomPic(),
breadcrumbs: <Breadcrumb[]>[
{
name: this.nameFromPath(menuPath),
path: menuPath
}
],
currentRoute: this.routeFromPath(menuPath)
};
this.store.dispatch(new AddProcess(newProcess));
}
routeFromPath(path: string) {
return path.substring(1, path.length);
}
nameFromPath(path: string) {
switch (path) {
case '/article-search':
return 'Artikelsuche';
case '/customer-search':
return 'Kundensuche';
default:
return 'Artikelsuche';
}
}
ngOnInit() {
this.processes$.subscribe((data: Process[]) => (this.processes = data));
}
}

View File

@@ -0,0 +1,8 @@
import { trigger, transition, animate, style } from '@angular/animations';
export const addAnimation = trigger('add', [
transition('void => true', [
style({ opacity: 0, transform: 'translateX(200%)' }),
animate('0.3s ease-out', style({ opacity: 1, transform: 'translateX(0%)' }))
])
]);

View File

@@ -0,0 +1,29 @@
<div class="grid-container pt-19">
<div class="align-left pt-3">
<div class="process-grid-container ml-5">
<sales-process-tab
style="display: inline-block;"
*ngFor="let process of processes"
[process]="process"
[@add]="process.new"
></sales-process-tab>
</div>
</div>
<div class="align-right">
<div class="grid-container-fix-width-last-col">
<div class="align-right add-process-label">
<span
*ngIf="processes.length == 0"
class="process-span"
(click)="addProcess()"
>{{ startProcessLabel }}</span
>
</div>
<div class="add-process">
<a (click)="addProcess()"
><img class="process-icon" src="/assets/images/add-red.svg"
/></a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,39 @@
@import "../../../assets/scss/variables";
.grid-container {
display: grid;
grid-template-columns: 93% auto;
grid-column-gap: 1vh;
}
.grid-container-fix-width-last-col {
display: grid;
grid-template-columns: max-content 50px;
}
.process-span {
color: $hima-color-red;
font-weight: bold;
}
.process-icon {
width: 34px;
}
.process-grid-container {
overflow: auto;
white-space: nowrap;
height: 41px;
}
.add-process {
position: absolute;
top: 102px;
right: 15px;
}
.add-process-label {
position: absolute;
top: 110px;
right: 63px;
}

View File

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

View File

@@ -0,0 +1,57 @@
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { Process } from 'apps/sales/src/app/core/models/process.model';
import { Observable } from 'rxjs';
import { Breadcrumb } from 'apps/sales/src/app/core/models/breadcrumb.model';
import { getRandomPic } from 'apps/sales/src/app/core/utils/process.util';
import { Store, Select } from '@ngxs/store';
import { AddProcess } from 'apps/sales/src/app/core/store/actions/process.actions';
import { ProcessState } from 'apps/sales/src/app/core/store/state/process.state';
import { addAnimation } from './add.animation';
import { Router } from '@angular/router';
@Component({
selector: 'sales-process-header',
templateUrl: './process-header.component.html',
styleUrls: ['./process-header.component.scss'],
animations: [addAnimation]
})
export class ProcessHeaderComponent implements OnInit {
startProcessLabel = 'VORGANG STARTEN';
@Select(ProcessState.getProcesses) process$: Observable<Process[]>;
processes: Process[];
constructor(
private store: Store,
private router: Router
) { }
addProcess() {
const itemNo =
this.processes.length === 0
? 1
: this.processes[this.processes.length - 1].id + 1;
const newProcess = <Process>{
id: itemNo,
new: true,
name: '# ' + itemNo,
selected: true,
icon: getRandomPic(),
breadcrumbs: <Breadcrumb[]>[
{
name: 'Artikelsuche',
path: '/article-search'
}
],
currentRoute: 'article-search'
};
this.store.dispatch(new AddProcess(newProcess));
this.router.navigate(['/article-search']);
}
ngOnInit() {
this.process$.subscribe(
(data: any) => (this.processes = data),
err => console.log(err)
);
}
}

View File

@@ -0,0 +1,33 @@
<div class="grid-item" id="{{ process.id }}">
<div
class="grid-container"
[ngClass]="{ 'selected-process': process.selected }"
>
<div (click)="selectProcess(process)">
<img
class="process-leading-icon"
src="/assets/images/{{ process.icon }}.png"
/>
</div>
<div class="pt-3" (click)="selectProcess(process)">
<span class="process-name">{{ process.name }}</span>
</div>
<ng-container *ngIf="process.cart">
<div (click)="openCart()">
<img
class="process-cart-icon"
src="../../../assets/images/Shopping_Cart.svg"
/>
</div>
<div class="pt-3">
<span class="process-cart-number">{{ cartCount }}</span>
</div>
</ng-container>
<div>
<a (click)="openDeleteConfirmationDialog()">
<img class="process-delete-icon" src="/assets/images/close.svg" />
</a>
</div>
</div>
<sales-process-delete-dialog #deleteporcessdialog (deleted)="deleteProcess($event)" [process]='process'></sales-process-delete-dialog>
</div>

View File

@@ -0,0 +1,58 @@
@import '../../../assets/scss/variables';
.process-tab {
border-bottom: 2px solid black;
}
.grid-container {
display: flex;
flex-direction: row;
height: 36px;
padding-top: 2px;
& > * {
margin-left: 3px;
margin-right: 3px;
}
}
.process-delete-icon {
width: 14px;
margin-left: 5px;
margin-top: 6px;
}
.process-cart-icon {
margin-top: 6px;
width: 19px;
}
.process-leading-icon {
width: 25px;
}
.grid-item {
display: inline-block;
padding-right: 20px;
}
.selected-process {
border-bottom: 3px solid $hima-color-red;
}
.not-selected-process {
border-bottom: 3px solid white;
}
.process-name {
font-size: 15px;
font-weight: bold;
color: $color-active;
margin-top: 5px;
}
.process-cart-number {
font-size: 15px;
font-weight: bold;
color: $color-active;
margin-top: 5px;
}

View File

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

View File

@@ -0,0 +1,90 @@
import {
Component,
OnInit,
Input,
ViewChild,
Output,
EventEmitter
} from '@angular/core';
import { Process } from 'apps/sales/src/app/core/models/process.model';
import { Router } from '@angular/router';
import { Store, Select } from '@ngxs/store';
import {
DeleteProcess,
SelectProcess,
PreventProductLoad,
ChangeCurrentRoute
} from '../../core/store/actions/process.actions';
import { Cart } from '../../core/models/cart.model';
import { AddBreadcrumb } from '../../core/store/actions/process.actions';
import { Breadcrumb } from '../../core/models/breadcrumb.model';
import { ProcessDeleteDialogComponent } from 'apps/sales/src/app/modules/process/process-delete-dialog/process-delete-dialog.component';
import { ProcessState } from 'apps/sales/src/app/core/store/state/process.state';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { SearchResultsComponent } from '../search-results/search-results.component';
import { Notify } from 'apps/sales/src/app/core/store/actions/notifier.actions';
@Component({
selector: 'sales-process-tab',
templateUrl: './process-tab.component.html',
styleUrls: ['./process-tab.component.scss']
})
export class ProcessTabComponent implements OnInit {
@Input() process: Process;
@Input() processes: Array<Process>;
@Select(ProcessState.getProcesses) procecesses$: Observable<Process[]>;
@Select(ProcessState.getProcessCount) processCount: Observable<number>;
@ViewChild('deleteporcessdialog')
processDeleteDialogComponent: ProcessDeleteDialogComponent;
cartCount = 0;
constructor(private store: Store, private router: Router) {}
deleteProcess(process: Process) {
this.store.dispatch(new DeleteProcess(process));
this.processCount.subscribe((count: number) => {
if (count < 1) {
this.router.navigate(['/dashboard']);
} else {
this.procecesses$.subscribe((data: Process[]) => {
const newSelectedProcess = data[count - 1];
if (newSelectedProcess) {
this.router.navigate([newSelectedProcess.currentRoute]);
}
});
}
});
this.store.dispatch(new Notify(1));
}
openDeleteConfirmationDialog() {
this.processDeleteDialogComponent.openDialog();
}
selectProcess(process: Process): void {
this.store.dispatch(new SelectProcess(process));
this.store.dispatch(new PreventProductLoad());
this.store.dispatch(new Notify(process.id));
this.router.navigate([process.currentRoute]);
}
ngOnInit() {
if (this.process.cart) {
this.process.cart.forEach((items: Cart) => {
this.cartCount += items.quantity;
});
}
}
openCart() {
const newBread: Breadcrumb = {
name: 'Warenkorb',
path: 'cart'
};
this.store.dispatch(new AddBreadcrumb(newBread));
this.store.dispatch(new ChangeCurrentRoute('cart'));
this.router.navigate(['cart']);
}
}

View File

@@ -0,0 +1,28 @@
<div class="card-container">
<div class="icon-container">
<div class="product-icon"></div>
</div>
<div class="content-container">
<div class="author align-left">
<span>author</span>
</div>
<div class="title-price">
<div class="title align-left">
<div>
<span>title </span>
</div>
</div>
<div class="price align-right">
<span>--</span>
<span class="currency"></span>
</div>
</div>
<div class="type-stock">
<div class="type align-left">
<div class="type-text align-left">
<span>type</span>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,160 @@
@import '../../../assets/scss/variables';
.card-container {
display: grid;
grid-template-columns: min-content auto;
grid-gap: 7vh;
background-color: white;
border-radius: 5px;
padding: 24px;
min-height: 134px;
}
.recommanded {
background-color: $important-notification;
color: white;
}
.product-icon {
width: 69px;
height: 100%;
background-color: #e9edf9;
color: #e9edf9;
}
.content-container {
display: grid;
grid-template-columns: auto;
opacity: 1;
animation: load 1.5s linear infinite;
}
@keyframes load {
50% {
opacity: 0.3;
}
100% {
opacity: 1;
}
}
.author span {
font-size: 16px;
background-color: #e9edf9;
color: #e9edf9;
width: 400px;
}
.title-price {
display: grid;
grid-template-columns: auto min-content;
// grid-gap: 8vh;
margin-top: 10px;
}
.title span {
font-size: 22px;
font-weight: bold;
background-color: #e9edf9;
color: #e9edf9;
width: 500px;
display: inline-block;
}
.price span {
font-size: 20px;
font-weight: bold;
display: inline-block;
background-color: #e9edf9;
color: #e9edf9;
width: 50px;
}
.title-grid {
display: grid;
grid-template-columns: min-content auto;
grid-gap: 2vh;
}
.rec-icon-container {
padding-top: 4px;
}
.currency {
margin-left: 5px;
display: inline-block;
background-color: #e9edf9;
color: #e9edf9;
width: 20px;
}
.type-stock {
display: grid;
grid-template-columns: auto max-content;
margin-top: 10px;
}
.type {
display: grid;
grid-template-columns: min-content auto;
grid-gap: 2vh;
background-color: #e9edf9;
color: #e9edf9;
width: 350px;
}
.type-text {
font-size: 18px;
font-weight: bold;
}
.available-stock {
display: grid;
grid-template-columns: auto auto auto auto;
grid-gap: 2vh;
}
.available-icon-container {
padding-top: 2px;
}
.stock-icon {
width: 15px;
}
.available-text span {
font-size: 18px;
font-weight: bold;
}
.stock span {
font-size: 18px;
font-weight: bold;
}
.publisher-order {
display: grid;
grid-template-columns: auto max-content;
margin-top: 10px;
}
.publisher-serial {
display: grid;
grid-template-columns: max-content min-content auto;
grid-gap: 2vh;
}
.publisher-serial span {
font-size: 16px;
}
.order span {
font-size: 16px;
color: #a7b9cb;
}
.type-icon-container {
padding-top: 3px;
}
.publisher {
max-width: 300px;
}

View File

@@ -0,0 +1,10 @@
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'sales-product-card-loading',
templateUrl: './product-card-loading.component.html',
styleUrls: ['./product-card-loading.component.scss']
})
export class ProductCardLoadingComponent implements OnInit {
ngOnInit() {}
}

View File

@@ -0,0 +1,93 @@
<div
class="card-container"
[ngClass]="{ recommanded: product.recommandation }"
(click)="productDetails(product, cardContainer)"
#cardContainer
>
<div class="icon-container">
<img class="product-icon" [src]="imageUrl$ | async" />
</div>
<div class="content-container">
<div class="author align-left">
<span>{{ product.author }}</span>
</div>
<div class="title-price">
<div
class="title align-left"
[ngClass]="{ 'title-grid': product.recommandation }"
>
<div class="rec-icon-container" *ngIf="product.recommandation">
<img src="../../../assets/images/Empfehlungen_Icon.svg" />
</div>
<div>
<span>{{ product.title }}</span>
</div>
</div>
<div class="price align-right">
<span>{{ price }}</span>
<span class="currency">{{ product.currency }}</span>
</div>
</div>
<div class="type-stock">
<div class="type align-left">
<div class="type-icon-container align-left">
<img
class="type-icon"
src="../../../assets/images/Icon_{{ product.typeIcon }}.svg"
*ngIf="!!product.typeIcon && product.typeIcon != '--'"
/>
</div>
<div class="type-text align-left">
<span>{{ product.type }}</span>
</div>
</div>
<div class="stock-container align-right">
<div *ngIf="product.itemsInStock > 0" class="available-stock">
<div class="available-icon-container">
<img
class="available-icon"
src="../../../assets/images/Check.svg"
/>
</div>
<div class="available-text">
<span>Lieferbar</span>
</div>
<div class="stock-icon-wraper">
<img
*ngIf="!product.recommandation"
class="stock-icon"
src="../../../assets/images/Icon_House.svg"
/>
<img
*ngIf="product.recommandation"
class="stock-icon"
src="../../../assets/images/Icon_House_recommended.svg"
/>
</div>
<div class="stock">
<span>{{ product.itemsInStock }}x</span>
</div>
</div>
<div *ngIf="product.itemsInStock === 0" class="not-available-stock">
<span>{{ product.notAvailableReason }}</span>
</div>
</div>
</div>
<div class="publisher-order">
<div class="publisher-serial">
<div class="publisher align-left wrap-text-more">
<span>{{ product.publisher }}</span>
</div>
<div class="align-left">
<span>|</span>
</div>
<div class="serial align-left">
<span>{{ product.ean }}</span>
</div>
</div>
<div class="order">
<span>{{ product.location }}</span>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,168 @@
@import '../../../assets/scss/variables';
.card-container {
display: grid;
grid-template-columns: min-content auto;
grid-gap: 7vh;
background-color: white;
border-radius: 5px;
padding: 24px;
min-height: 134px;
}
.recommanded {
background-color: $important-notification;
color: white;
}
.product-icon {
width: 69px;
}
.content-container {
display: grid;
grid-template-columns: auto;
}
.author span {
font-size: 16px;
}
.title-price {
display: grid;
grid-template-columns: auto max-content;
grid-gap: 8vh;
margin-top: 10px;
}
.title span {
font-size: 22px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
.price span {
font-size: 20px;
font-weight: bold;
}
.title-grid {
display: grid;
grid-template-columns: min-content auto;
grid-gap: 2vh;
}
.rec-icon-container {
padding-top: 4px;
}
.currency {
margin-left: 5px;
}
.type-stock {
display: grid;
grid-template-columns: auto max-content;
margin-top: 10px;
}
.type {
display: grid;
grid-template-columns: min-content auto;
grid-gap: 2vh;
}
.type-text {
font-size: 18px;
font-weight: bold;
height: 20px;
overflow: hidden;
}
.available-stock {
display: grid;
grid-template-columns: auto auto auto auto;
grid-gap: 2vh;
}
.available-icon-container {
padding-top: 2px;
}
.stock-icon {
width: 15px;
}
.available-text span {
font-size: 18px;
font-weight: bold;
}
.stock span {
font-size: 18px;
font-weight: bold;
}
.publisher-order {
display: grid;
grid-template-columns: auto max-content;
margin-top: 10px;
}
.publisher-serial {
display: grid;
grid-template-columns: max-content min-content auto;
grid-gap: 2vh;
}
.publisher-serial span {
font-size: 16px;
}
.order span {
font-size: 16px;
color: #a7b9cb;
}
.type-icon-container {
padding-top: 3px;
}
.publisher {
max-width: 300px;
}
/* Portrait */
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: portrait) and (-webkit-min-device-pixel-ratio: 2) {
.title span {
max-width: 370px;
}
}
/* Landscape */
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) and (-webkit-min-device-pixel-ratio: 2) {
.title span {
max-width: 810px;
}
}
/* Portrait */
/* Declare the same value for min- and max-width to avoid colliding with desktops */
/* Source: https://medium.com/connect-the-dots/css-media-queries-for-ipad-pro-8cad10e17106*/
@media only screen and (min-device-width: 1024px) and (max-device-width: 1024px) and (orientation: portrait) and (-webkit-min-device-pixel-ratio: 2) {
.title span {
max-width: 565px;
}
}
/* Landscape */
/* Declare the same value for min- and max-width to avoid colliding with desktops */
/* Source: https://medium.com/connect-the-dots/css-media-queries-for-ipad-pro-8cad10e17106*/
@media only screen and (min-device-width: 1366px) and (max-device-width: 1366px) and (orientation: landscape) and (-webkit-min-device-pixel-ratio: 2) {
.title span {
max-width: 900px;
}
}

View File

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

View File

@@ -0,0 +1,90 @@
import { Component, OnInit, Input } from '@angular/core';
import { Product } from 'apps/sales/src/app/core/models/product.model';
import { Router } from '@angular/router';
import { CatImageService } from 'cat-service';
import { ReplaySubject, Observable, of } from 'rxjs';
import { flatMap, catchError, filter, map } from 'rxjs/operators';
import { getProductTypeIcon } from 'apps/sales/src/app/core/utils/product.util';
import { ItemDTO } from 'projects/cat-service/src/lib';
import { Select, Store } from '@ngxs/store';
import { AddSelectedProduct } from 'apps/sales/src/app/core/store/actions/product.actions';
import { ChangeCurrentRoute } from 'apps/sales/src/app/core/store/actions/process.actions';
import { ProcessState } from 'apps/sales/src/app/core/store/state/process.state';
@Component({
selector: 'sales-product-card',
templateUrl: './product-card.component.html',
styleUrls: ['./product-card.component.scss']
})
export class ProductCardComponent implements OnInit {
private _product: Product;
@Select(ProcessState.getProducts) items$: Observable<ItemDTO[]>;
@Input() index: number;
@Input()
get product() {
return this._product;
}
set product(val) {
if (val !== this.product) {
this._product = val;
this.eanChangedSub.next(!!val ? val.ean : '');
}
}
get price() {
if (this._product.price.toString().indexOf('.') === -1) {
return this._product.price + ',00';
}
const afterDecimal = this._product.price.toString().split('.')[1];
if (afterDecimal.length !== 2) {
return this._product.price.toString().replace('.', ',') + '0';
}
return this._product.price.toString().replace('.', ',');
}
eanChangedSub = new ReplaySubject<string>();
imageUrl$: Observable<string>;
productTypeIcon: string;
constructor(
private router: Router,
private catImageService: CatImageService,
private store: Store
) {
this.imageUrl$ = this.eanChangedSub.pipe(
flatMap(ean => {
// TODO: remove mock data
if (ean === '3') {
return of('../../../assets/images/ResultBook4.png');
}
return this.catImageService.getImageUrl(ean);
}),
catchError(() => of(''))
);
}
productDetails(product: Product, element: HTMLElement) {
// TODO: this is temporary solution for the incostency of product detail API
this.items$
.pipe(
map(item => {
if (item) {
return item.find(i => i && i.id === product.id);
}
})
)
.subscribe((data: ItemDTO) =>
this.store.dispatch(new AddSelectedProduct(data, this.index))
);
const currentRoute = 'product-details/' + product.id;
this.store.dispatch(new ChangeCurrentRoute(currentRoute));
this.router.navigate([currentRoute]);
}
ngOnInit() {}
}

View File

@@ -0,0 +1,130 @@
<div class="product-detail-container" *ngIf="product">
<div class="general-details">
<div class="product-image" [ngStyle]="{'background-image':'url(' + (product.productIcon$ | async) + ')'}">
<!-- <img [src]="product.productIcon$ | async"> -->
</div>
<div class="product-info-container">
<div class="product-info">
<div class="autor standart-text">
<span>{{product.author}}</span>
</div>
<div class="title-price">
<div class="title align-left">
<span>{{product.title}}</span>
</div>
<div class="price align-right">
<span>{{product.price}}</span>
</div>
</div>
<div class="type-stock-info">
<div class="type align-left">
<div>
<img class="icon" src="../../../assets/images/Icon_{{product.formatIcon}}.svg">
</div>
<div>
<span>{{product.format}}</span>
</div>
</div>
<div class="stock-info align-right" *ngIf="product.quantity > 0">
<div class="ship-icon">
<img class="icon" src="../../../assets/images/Truck_Icon_2.svg">
</div>
<div class="send-icon">
<img class="icon" src="../../../assets/images/Package_Icon_2.svg">
</div>
<div class="stock-label" *ngIf="product.quantity > 0">
<span>Lieferbar</span>
</div>
<div class="home-icon" *ngIf="product.quantity > 0">
<img class="icon" src="../../../assets/images/Icon_House.svg">
</div>
<div class="stock-quantity" *ngIf="product.quantity > 0">
<span>{{product.quantity}}x</span>
</div>
</div>
</div>
<div class="category align-right" *ngIf="product.assortment">
<span>{{product.assortment}}</span>
</div>
<div class="languages standart-text">
<span>{{product.locale}}</span>
</div>
<div class="ean standart-text">
<span>{{product.eanTag}}</span>
</div>
<div class="publisher standart-text">
<span>{{product.publisher}}</span>
</div>
<div class="publication-format-pages standart-text">
<div class="publication">
<span>{{product.publicationDate}}</span>
</div>
<div>
<span class="divider">I</span>
</div>
<div class="format">
<span>{{product.format}}</span>
</div>
<!---<div>
<span>|</span>
</div>
<div class="pages">
<span>Seiten</span>
</div>
-->
</div>
<div>
</div>
</div>
</div>
<div class="product-staus-info"></div>
</div>
<div class="separator"></div>
<div class="product-details">
<ng-container *ngIf="product.fullDescription.length > 0; else noDescription">
<div class="details" id="details-container" *ngIf="!moreBtn">
<span id="details-text">{{descriptionText(product.fullDescription)}}</span>
<span class="more-btn" (click)="toggleMore()">Mehr<img src="../../../assets/images/Arrow_Next-with-body.svg" alt="more"></span>
</div>
<div class="details-full" id="details-container" *ngIf="moreBtn">
<span id="details-text">{{product.fullDescription}}</span>
<span class="more-btn opened" (click)="toggleMore()">Weniger<img src="../../../assets/images/Arrow_back.svg" alt="less"></span>
</div>
</ng-container>
<ng-template #noDescription>
<div class="details" id="details-container">
<span id="details-text">Für diesen Artikel ist keine Beschreibung verfügbar</span>
</div>
</ng-template>
<div class="actions align-right">
<button class="btn align-right reserve">Reservieren</button>
<button class="btn btn-active align-right" (click)="openModal()">Kaufoptionen</button>
</div>
</div>
<div class="other-formats">
<div class="other-format-label">
<span>Weitere verfügbare Formate</span>
</div>
<div class="other-format">
<div>
<img src="../../../assets/images/E-Book_Icon_grey.svg">
</div>
<div>Hörbuch 12,95€</div>
</div>
<div class="other-format align-left">
<div>
<img src="../../../assets/images/Headphone_Icon_grey.svg">
</div>
<div>E-Book 13,95€</div>
</div>
</div>
<div class="recommandations">
<sales-recommendations></sales-recommendations>
</div>
</div>
<sales-loading loading="true" *ngIf="!product"></sales-loading>
<ng-container *ngIf="item">
<sales-checkout #checkout (closed)="cartActionCompleted($event)" [book]="item"></sales-checkout>
</ng-container>

View File

@@ -0,0 +1,369 @@
@import "../../../assets/scss/variables";
.product-detail-container {
display: grid;
grid-template-columns: auto;
background-color: white;
padding: 20px 0 0 0;
border-radius: 5px;
box-shadow: 0px 0px 10px 0px #dce2e9;
-moz-box-shadow: 0px 0px 10px 0px #dce2e9;;
-webkit-box-shadow: 0px 0px 10px 0px #dce2e9;
box-shadow: 0px 0px 10px 0px #dce2e9;
& > div {
padding: 0 20px;
}
}
.general-details {
display: grid;
grid-template-columns: min-content auto;
grid-gap: 37px;
}
.product-details {
display: grid;
grid-template-columns: auto;
margin-top: 20px;
}
.recommandations {
display: grid;
grid-template-columns: auto;
}
.details {
height: 68px;
}
.details-full {
height: auto;
}
.details span {
font-size: 16px;
line-height: 21px;
}
.product-image {
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
.product-info {
display: grid;
grid-template-columns: auto;
}
.title {
width: 90%;
display: flex;
justify-content: flex-start;
align-items: center;
span {
font-size: 26px;
font-weight: bold;
}
}
.standart-text {
font-size: 16px;
}
.publication-format-pages {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 20px;
.publication {
display: flex;
justify-content: flex-start;
align-items: center;
}
.divider {
padding: 0 8px;
}
}
.type-stock-info {
display: grid;
grid-template-columns: auto auto;
margin-top: 15px;
}
.stock-info {
display: flex;
justify-content: flex-end;
align-items: center;
img {
padding-right: 8px;
}
}
.title-price {
display: grid;
grid-template-columns: auto auto;
grid-gap: 2px;
}
.type {
display: grid;
grid-template-columns: min-content auto;
grid-gap: 13px;
}
.type span {
font-size: 18px;
font-weight: bold;
}
.icon {
height: 17px;
margin-top: 4px;
}
.home-icon {
padding-left: 15px;
}
.stock-label span {
font-size: 18px;
font-weight: bold;
padding-left: 8px;
position: relative;
bottom: 1px;
}
.stock-quantity span {
font-size: 18px;
font-weight: bold;
}
.category {
margin-top: 10px;
font-size: 16px;
color: #a7b9cb;
}
.languages {
margin-top: 17px;
}
.price {
display: flex;
justify-content: flex-end;
align-items: flex-start;
padding-top: 7px;
span {
font-size: 20px;
font-weight: bold;
}
}
.publisher {
margin-top: 15px;
}
.separator {
background-color: $hima-content-color;
height: 3px;
}
.actions {
display: inline;
padding-top: 20px;
}
.btn {
font-family: 'Open Sans';
font-size: 18px;
font-weight: bold;
color: #f70400;
width: 149px;
padding: 12px;
cursor: pointer;
&-active {
background-color: #f70400;
border: none;
border-radius: 25px;
color: #ffffff;
width: 174px;
text-align: center;
}
}
.reserve {
padding-right: 150px;
background-color: transparent;
outline: none;
border: none;
}
.other-formats {
display: grid;
grid-template-columns: max-content max-content auto;
grid-gap: 20px;
padding-top: 35px;
margin: 30px 0;
}
.other-format-label {
font-size: 16px;
color: #a7b9cb;
line-height: 21px;
}
.other-format {
display: grid;
grid-template-columns: min-content auto;
grid-gap: 5px;
font-size: 16px;
font-weight: bold;
color: #5a728a;
line-height: 21px;
}
.more-btn {
font-size: 16px;
font-weight: bold;
color: #5a728a;
display: flex;
justify-content: flex-end;
align-items: center;
position: relative;
bottom: 22px;
// right: 10px;
margin-top: 1px;
align-self: right;
background-color: white;
width: 10%;
position: relative;
right: -91%;
img {
padding-left: 8px;
}
}
.opened {
bottom: 0px;
margin-bottom: 10px;
}
.recommandations {
background-color: #ffffff;
border-radius: 5px;
box-shadow: 0px -2px 18px 0px #dce2e9;
padding: 25px 15px !important;
}
app-recommendations{
padding: 0;
}
/* Portrait */
@media only screen
and (min-device-width: 768px)
and (max-device-width: 1024px)
and (orientation: portrait)
and (-webkit-min-device-pixel-ratio: 2) {
.product-image {
width: 200px;
}
.price {
padding-top: 5px;
}
.details {
height: 85px;
}
}
/* Landscape */
@media only screen
and (min-device-width: 768px)
and (max-device-width: 1024px)
and (orientation: landscape)
and (-webkit-min-device-pixel-ratio: 2) {
.product-image {
width: 200px;
}
.price {
padding-top: 5px;
}
.details {
min-height: 85px;
}
}
/* Portrait */
/* Declare the same value for min- and max-width to avoid colliding with desktops */
/* Source: https://medium.com/connect-the-dots/css-media-queries-for-ipad-pro-8cad10e17106*/
@media only screen
and (min-device-width: 1024px)
and (max-device-width: 1024px)
and (orientation: portrait)
and (-webkit-min-device-pixel-ratio: 2) {
.product-image {
width: 205px;
}
.price {
padding-top: 6px;
}
.more-btn {
bottom: 21px;
}
.opened {
bottom: 5px;
}
.details {
min-height: 68px;
}
}
/* Landscape */
/* Declare the same value for min- and max-width to avoid colliding with desktops */
/* Source: https://medium.com/connect-the-dots/css-media-queries-for-ipad-pro-8cad10e17106*/
@media only screen
and (min-device-width: 1366px)
and (max-device-width: 1366px)
and (orientation: landscape)
and (-webkit-min-device-pixel-ratio: 2) {
.product-image {
width: 205px;
}
.price {
padding-top: 8px;
}
.more-btn {
bottom: 21px;
}
.opened {
bottom: 5px;
}
.details {
min-height: 68px;
}
}

View File

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

View File

@@ -0,0 +1,233 @@
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit, ViewChild } from '@angular/core';
import { CheckoutComponent } from '../checkout/checkout.component';
import { ProductService } from 'apps/sales/src/app/core/services/product.service';
import { ItemDTO, CatImageService } from 'cat-service';
import { Observable } from 'rxjs';
import { ProcessState } from 'apps/sales/src/app/core/store/state/process.state';
import { Select, Store } from '@ngxs/store';
import { UpdateBreadcrump } from 'apps/sales/src/app/core/store/actions/process.actions';
import { NotifierState } from 'apps/sales/src/app/core/store/state/notifier.state';
import { Notify } from 'apps/sales/src/app/core/store/actions/notifier.actions';
import { Process } from 'apps/sales/src/app/core/models/process.model';
@Component({
selector: 'sales-product-details',
templateUrl: './product-details.component.html',
styleUrls: ['./product-details.component.scss']
})
export class ProductDetailsComponent implements OnInit {
@ViewChild('checkout') checkoutDialog: CheckoutComponent;
id: number;
item: ItemDTO;
selectedItem: ItemDTO;
@Select(ProcessState.getSelectedProduct) selectedItem$: Observable<ItemDTO>;
moreBtn = false;
shortenText = null;
currentProcess: Process;
readonly FULL_DESCRIPTION_LABEL = 'Klappentext';
readonly AUTOR = 'Autor';
readonly TITLE = 'Titel';
product: {
author: string;
title: string;
ean: string;
eanTag: string;
locale: string;
publicationDate: string;
format: string;
formatIcon: string;
quantity: string;
category: string;
price: string;
publisher: string;
productIcon$: Observable<string>;
fullDescription: string;
};
constructor(
private route: ActivatedRoute,
private productService: ProductService,
private catImageService: CatImageService,
private store: Store
) {}
ngOnInit() {
this.store
.select(ProcessState.getCurrentProcess)
.subscribe((process: Process) => (this.currentProcess = process));
this.detailInitialize();
}
detailInitialize() {
this.route.params.subscribe(params => {
this.selectedItem$.subscribe((data: ItemDTO) => {
this.selectedItem = data;
});
this.productService
.getItemById(params['id'])
.subscribe((item: ItemDTO) => {
this.item = item;
this.product = this.productDetailMapper(item);
this.store.dispatch(
new UpdateBreadcrump({
name:
this.product.title.substring(0, 12) +
(this.product.title.length > 12 ? '...' : ''),
path: '/product-details/' + item.id
})
);
return this.product;
});
});
}
productDetailMapper(item: ItemDTO) {
let fullDescription: string = null;
let ean: string = null;
let eanTag: string = null;
let productIcon$: Observable<string> = null;
let author: string;
let title: string;
let locale: string;
let publicationDate: string;
let format: string;
let formatIcon: string;
let quantity: string;
let category: string;
let price: string;
let publisher: string;
let assortment: string;
// product object mapping
if (item.pr) {
ean = item.pr.ean;
eanTag = ean;
productIcon$ = this.catImageService.getImageUrl(ean, {
width: 469,
height: 575
});
locale = item.pr.locale;
// publicationDate = getFormatedPublicationDate(item.pr.publicationDate);
publicationDate = this.formatDate(item.pr.publicationDate);
format = this.selectedItem ? this.selectedItem.pr.formatDetail : null;
formatIcon = this.selectedItem ? this.selectedItem.pr.format : null;
category = item.pr.productGroup;
publisher = item.pr.manufacturer;
}
// text object mapping
if (item.te) {
const teItem = item.te.find(t => t.label === this.FULL_DESCRIPTION_LABEL);
fullDescription = !!teItem ? teItem.value : '';
}
// specs object mapping
if (item.sp) {
author = item.sp.find(s => s.key === this.AUTOR)
? item.sp.find(s => s.key === this.AUTOR).value
: 'missing';
title = item.sp.find(s => s.key === this.TITLE).value;
}
if (item.av.length > 0) {
quantity = (item.av[0].qty ? item.av[0].qty : 0) + 'x';
price =
item.av[0].price.value.value + ' ' + item.av[0].price.value.currency;
if (item.av[0].price.value.value.toString().indexOf('.') === -1) {
price =
item.av[0].price.value.value +
',00 ' +
item.av[0].price.value.currency;
} else {
const afterDecimal = item.av[0].price.value.value
.toString()
.split('.')[1];
if (afterDecimal.length === 1) {
price =
item.av[0].price.value.value.toString().replace('.', ',') +
'0' +
' ' +
item.av[0].price.value.currency;
} else {
price =
item.av[0].price.value.value.toString().replace('.', ',') +
' ' +
item.av[0].price.value.currency;
}
}
}
if (item.sh) {
assortment = item.sh[0].assortment;
}
return {
author: author,
title: title,
ean: ean,
eanTag: eanTag,
locale: locale,
publicationDate: publicationDate,
format: format,
formatIcon: formatIcon,
quantity: quantity,
category: category,
price: price,
publisher: publisher,
productIcon$: productIcon$,
fullDescription: fullDescription,
assortment: assortment
};
}
getShortDescription(description: string): string {
return description.slice(0, 267) + '...';
}
openModal() {
this.checkoutDialog.openDialog();
}
cartActionCompleted(open: boolean) {
// Logic if needed
}
formatDate(date: Date): string {
if (!!date) {
const dateToFormat = new Date(date);
return `${dateToFormat.getMonth()}/${dateToFormat.getFullYear()}`;
} else {
return '';
}
}
descriptionText(text: string) {
const container = document.getElementById('details-container');
const el = document.getElementById('details-text');
el.innerHTML = text;
const wordArray = el.innerHTML.split(' ');
if (el.offsetHeight > container.offsetHeight && !this.shortenText) {
while (el.offsetHeight > container.offsetHeight) {
wordArray.pop();
el.innerHTML = wordArray.join(' ') + '...';
}
// Make room for the more button
wordArray.pop();
wordArray.pop();
this.shortenText = wordArray.join(' ') + '...';
el.innerHTML = wordArray.join(' ') + '...';
} else {
el.innerHTML = this.shortenText;
}
}
toggleMore() {
this.moreBtn = !this.moreBtn;
}
}

View File

@@ -0,0 +1,160 @@
import { DataSource, CollectionViewer } from '@angular/cdk/collections';
import { procuctsMock } from 'mocks/products.mock';
import { Product } from '../../core/models/product.model';
import { BehaviorSubject, Subscription, Observable, of } from 'rxjs';
import { ProductMapping } from '../../core/mappings/product.mapping';
import { ProductService } from '../../core/services/product.service';
import { Store } from '@ngxs/store';
import { debounceTime, take } from 'rxjs/operators';
import { SetProducts, CurrentPageLoaded } from '../../core/store/actions/product.actions';
import { ItemDTO } from 'dist/cat-service/lib/dtos';
export class SearchDataSource extends DataSource<Product | undefined> {
private pageSize = 10;
private cachedData = Array.from<Product>({ length: 0 });
private cachedItemsDTO = Array.from<ItemDTO>({ length: 0 });
private fetchedPages = new Set<number>();
private dataStream = new BehaviorSubject<(Product | undefined)[]>(
this.cachedData
);
public dataStreamDTO = new BehaviorSubject<(ItemDTO | undefined)[]>(
this.cachedItemsDTO
);
private subscription = new Subscription();
private dssub = new Subscription();
public loading = true;
public results = false;
private productMapping = new ProductMapping();
constructor(
public id: number,
private searchService: ProductService,
private search: string,
private store: Store,
private filters: any[],
private cachcedProducts: ItemDTO[],
private currentCachedPage: number
) {
super();
}
connect(
collectionViewer: CollectionViewer
): Observable<(Product | undefined)[]> {
this.subscription.add(
collectionViewer.viewChange.subscribe(range => {
const startPage = this.getPageForIndex(range.start);
const endPage = this.getPageForIndex(range.end - 1);
for (let i = startPage; i <= endPage; i++) {
this.fetchPage(i);
}
})
);
this.dssub.add(
this.dataStreamDTO
.pipe(debounceTime(1000))
.subscribe(i => {
this.store.dispatch(new SetProducts([...i], this.search));
}
)
);
this.fetchPage(0);
return this.dataStream;
}
disconnect(): void {
this.subscription.unsubscribe();
}
public getPageForIndex(index: number): number {
return Math.floor(index / this.pageSize);
}
private fetchPage(page: number) {
this.store.dispatch(new CurrentPageLoaded(page));
if (page === 0) {
this.results = false;
}
if (this.fetchedPages.has(page)) {
return;
}
this.fetchedPages.add(page);
this.loading = true;
// TODO: check if search is already in store? Then take the store data and do not fetch new
if (this.cachcedProducts.length > 0 && this.currentCachedPage <= page) {
this.loading = false;
this.results = true;
const cachedItems = this.cachcedProducts.filter((item: ItemDTO) => item);
if (page === 0) {
this.cachedData = Array.from<Product>({ length: this.cachcedProducts.length });
this.cachedItemsDTO = Array.from<ItemDTO>({ length: this.cachcedProducts.length });
}
this.cachedItemsDTO.splice(
page * this.pageSize,
this.pageSize,
... cachedItems
);
this.cachedData.splice(
page * this.pageSize,
this.pageSize,
...cachedItems.map((item, i) => {
if (i === 3) {
return procuctsMock[3];
}
return this.productMapping.fromItemDTO(item);
})
);
this.dataStream.next(this.cachedData);
this.dataStreamDTO.next(this.cachedItemsDTO);
if (page === 0) {
// dispatch immediately on first page load
this.store.dispatch(
new SetProducts([...this.cachedItemsDTO], this.search)
);
}
} else {
this.searchService
.searchItemsWithPagination(
this.search,
page * this.pageSize,
this.pageSize,
this.filters
)
.pipe(take(1))
.subscribe(data => {
this.loading = false;
this.results = true;
if (page === 0) {
this.cachedData = Array.from<Product>({ length: data.hits });
this.cachedItemsDTO = Array.from<ItemDTO>({ length: data.hits });
}
this.cachedItemsDTO.splice(
page * this.pageSize,
this.pageSize,
...data.result
);
this.cachedData.splice(
page * this.pageSize,
this.pageSize,
...data.result.map((item, i) => {
if (i === 3) {
return procuctsMock[3];
}
return this.productMapping.fromItemDTO(item);
})
);
this.dataStream.next(this.cachedData);
this.dataStreamDTO.next(this.cachedItemsDTO);
if (page === 0) {
// dispatch immediately on first page load
this.store.dispatch(
new SetProducts([...this.cachedItemsDTO], this.search)
);
}
});
}
}
}

View File

@@ -0,0 +1,30 @@
<div class="result-container">
<sales-filter (filtersChanged)="updateSearch()"></sales-filter>
<div *ngIf="!ds || (ds.loading && !ds.results)">
<div [@stagger]="'yes'">
<div *ngFor="let dummy of dummies" [style.marginTop.px]="10">
<sales-product-card-loading> </sales-product-card-loading>
</div>
</div>
</div>
<cdk-virtual-scroll-viewport itemSize="190" class="viewport" #scroller>
<div *cdkVirtualFor="let product of ds; let i = index" class="product-item">
<sales-product-card
[product]="product"
[index]="i"
*ngIf="product != null; else loadingComponent"
>
</sales-product-card>
<ng-template #loadingComponent>
<sales-product-card-loading></sales-product-card-loading>
</ng-template>
</div>
</cdk-virtual-scroll-viewport>
<sales-loading
*ngIf="!ds || ds.loading"
[style.marginTop.px]="60"
[style.marginBottom.px]="60"
loading="true"
text="Inhalte werden geladen"
></sales-loading>
</div>

View File

@@ -0,0 +1,24 @@
app-product-card-loading {
display: block;
position: relative;
width: 100%;
}
app-filter {
display: block;
margin-bottom: 10px;
}
.result-container {
height: calc(100% - 80px);
}
.viewport {
padding-bottom: 10px;
height: calc(100% - 10px);
width: 100%;
}
.product-item {
height: 180px;
overflow: hidden;
padding-bottom: 10px;
}

View File

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

View File

@@ -0,0 +1,145 @@
import { ProductService } from './../../core/services/product.service';
import { Component, OnInit, AfterViewInit, ViewChild, ChangeDetectorRef } from '@angular/core';
import { Search } from '../../core/models/search.model';
import { Process } from '../../core/models/process.model';
import { Product } from '../../core/models/product.model';
import { Router, ActivatedRoute } from '@angular/router';
import { Select, Store } from '@ngxs/store';
import { ItemDTO } from 'dist/cat-service/lib/dtos';
import { Observable, of } from 'rxjs';
import { ProcessState } from '../../core/store/state/process.state';
import { staggerAnimation } from './stagger.animation';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { SearchDataSource } from './search-data.datasource';
import { NotifierState } from 'apps/sales/src/app/core/store/state/notifier.state';
@Component({
selector: 'sales-search-results',
templateUrl: './search-results.component.html',
styleUrls: ['./search-results.component.scss'],
animations: [staggerAnimation]
})
export class SearchResultsComponent implements OnInit, AfterViewInit {
@Select(ProcessState.getProcesses) processes$: Observable<Process[]>;
currentProcess: Process;
@Select(ProcessState.getScrollPositionForProduct) scrollTo$: Observable<number>;
@Select(ProcessState.getScrollPositionForProduct) getCurrentPageCached$: Observable<number>;
index: number;
id: number;
firstload = 'false';
currentPageCached: number;
currentSearch: Search;
products: Product[];
@Select(ProcessState.getProducts) products$: Observable<ItemDTO[]>;
cachedProducts: ItemDTO[] = [];
skip = 0;
processCount = 0;
test = 'test';
@ViewChild('scroller') scroller: CdkVirtualScrollViewport;
dummies = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
ds: SearchDataSource;
constructor(
private store: Store,
private router: Router,
private productService: ProductService,
private route: ActivatedRoute,
private cd: ChangeDetectorRef
) {
this.route.queryParams.subscribe(
params => this.firstload = params['firstload']
);
}
ngOnInit() {
this.loadCurrentSearch();
if (!this.currentSearch) {
this.router.navigate(['dashboard']);
return;
}
if (this.firstload === 'true') {
this.index = 0;
} else {
this.scrollTo$.subscribe((index: number) => {
if (index) {
this.index = index;
}
});
}
this.getCurrentPageCached$.subscribe((page: number) => {
if (page) {
this.currentPageCached = page;
}
});
this.products$.subscribe((productsInCache: ItemDTO[]) => {
if (productsInCache) {
this.cachedProducts = productsInCache;
}
});
this.store.select(NotifierState.getNotifier)
.subscribe(
(processId: number) => {
if (this.ds) {
this.loadDataSource();
} else {
this.loadDataSource();
}
}
);
}
loadDataSource() {
if (!!this.currentSearch) {
this.ds = new SearchDataSource(
this.id,
this.productService,
this.currentSearch.query,
this.store,
[],
this.cachedProducts,
this.currentPageCached
);
}
}
ngAfterViewInit() {
setTimeout(() => {
if (this.index) {
this.scroller.scrollToIndex(this.index);
}
}, 400);
}
updateSearch() {
this.store.selectOnce(ProcessState.getProcessFilters).subscribe(fil => {
if (!!fil) {
this.ds = new SearchDataSource(
this.id,
this.productService,
this.currentSearch.query,
this.store,
fil,
this.cachedProducts,
this.currentPageCached
);
}
});
}
loadCurrentSearch() {
this.store
.select(state => state.processes)
.subscribe((data: any) => {
const process = data.processes.find(t => t.selected === true);
if (process) {
this.currentProcess = process;
this.currentSearch = process.search;
this.id = process.id;
}
});
}
}

View File

@@ -0,0 +1,21 @@
import {
trigger,
transition,
stagger,
animate,
style,
query
} from '@angular/animations';
export const staggerAnimation = trigger('stagger', [
transition('* => *', [
query(
':enter',
[
style({ opacity: 0 }),
stagger(50, [animate(300, style({ opacity: 1 }))])
],
{ optional: true }
)
])
]);

View File

@@ -0,0 +1,52 @@
import { BasicAuthorizationInterceptor } from './basic-authorization.interceptor';
import { TestBed, getTestBed } from '@angular/core/testing';
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';
import { HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http';
describe('BasicAuthorizationInterceptor', () => {
let injector: TestBed;
let httpMock: HttpTestingController;
let httpClient: HttpClient;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useFactory: () => new BasicAuthorizationInterceptor({
client: 'testclient',
password: 'testpassword',
endpoints: ['https://test1.com']
}),
multi: true
}
]
});
injector = getTestBed();
httpMock = injector.get(HttpTestingController);
httpClient = injector.get(HttpClient);
});
it('should add a basic authorization header', () => {
httpClient.get('https://test1.com/abcdef').subscribe();
const req = httpMock.expectOne('https://test1.com/abcdef');
expect(req.request.headers.has('Authorization'))
.toBeTruthy();
expect(req.request.headers.get('Authorization'))
.toBe('Basic dGVzdGNsaWVudDp0ZXN0cGFzc3dvcmQ=');
});
it('should not add a basic authorization header', () => {
httpClient.get('https://test2.com/abcdef').subscribe();
const req = httpMock.expectOne('https://test2.com/abcdef');
expect(req.request.headers.has('Authorization'))
.toBeFalsy();
});
});

View File

@@ -0,0 +1,58 @@
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable, AsyncSubject, ReplaySubject } from 'rxjs';
import { map, filter, shareReplay, first, flatMap, withLatestFrom, tap } from 'rxjs/operators';
export interface BasicAuthorizationOptions {
client: string;
password: string;
endpoints: string[];
}
export const SKIP_BASIC_AUTHORIZATION_INTERCEPTOR = '';
@Injectable()
export class BasicAuthorizationInterceptor implements HttpInterceptor {
private optionsSub = new ReplaySubject<{ token: string; endpointMatchers: RegExp[] }>();
constructor(
options$: Observable<BasicAuthorizationOptions>
) {
options$.pipe(
map((options) => {
const token = btoa(`${options.client}:${options.password}`);
const endpointMatchers: RegExp[] = [];
for (const endpoint of options.endpoints) {
endpointMatchers.push(new RegExp(`^${endpoint}`, 'i'));
}
return { token, endpointMatchers };
})
).subscribe(this.optionsSub);
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// let request = req;
if (req.headers.has('SKIP_BASIC_AUTHORIZATION_INTERCEPTOR')) {
return next.handle(req);
}
return this.optionsSub.pipe(
first(),
map(options => {
let request = req;
if (options.endpointMatchers.find(matcher => matcher.test(req.url))) {
const headers = req.headers
.set('Authorization', `Basic ${options.token}`);
request = req.clone({ headers });
}
return request;
}),
flatMap(request => next.handle(request))
);
}
}

View File

@@ -0,0 +1 @@
export * from './basic-authorization.interceptor';

View File

@@ -0,0 +1,75 @@
import { FeedCard } from '../models/feed-card.model';
import { FeedDTO } from 'feed-service';
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';
@Injectable({ providedIn: 'root' })
export class FeedMapping {
constructor () {}
fromFeedDTO (feed: FeedDTO): FeedCard {
let books: FeedBook = null;
const event: FeedEvent[] = [];
const news: FeedNews[] = [];
const recommandation: FeedRecommandation = null;
if (feed.type === 'products' && feed.items[0]) {
books = {
id: feed.id,
firstBookTypeIcon: feed.items[0].pr.format,
firstBookEan: feed.items[0].pr.ean,
firstBookAuthor: feed.items[0].pr.contributors,
firstBookTitle: feed.items[0].pr.name,
firstBookType: feed.items[0].pr.formatDetail,
firstBookIcon: feed.items[0].pr.format,
firstBookLanguage: feed.items[0].pr.locale,
firstBookPrice: feed.items[0].av[0].price.value.value,
firstBookCurrency: feed.items[0].av[0].price.value.currency
};
if (feed.items[1]) {
books = {
...books,
secondBookEan: feed.items[1].pr.ean,
secondBookAuthor: feed.items[1].pr.contributors,
secondBookTitle: feed.items[1].pr.name,
secondBookType: feed.items[1].pr.formatDetail,
secondBookTypeIcon: feed.items[1].pr.format,
secondBookIcon: feed.items[1].pr.format,
secondBookLanguage: feed.items[1].pr.locale,
secondBookPrice: feed.items[1].av[0].price.value.value,
secondBookCurrency: feed.items[1].av[0].price.value.currency
};
}
} else if (feed.type === 'events') {
feed.items.forEach (
i => event.push(<FeedEvent> {
id: i.id,
title: i.name,
content: i.desc,
imageUrl: i.image
})
);
} else if (feed.type === 'info') {
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,
books: books,
event: event,
news: news,
recommandation: recommandation
};
}
}

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { InputDTO, OrderByDTO, OptionDTO } from 'cat-service';
import { FilterItem } from '../models/filter-item.model';
@Injectable({ providedIn: 'root' })
export class FilterItemMapping {
constructor() { }
fromOptionDto(option: OptionDTO): FilterItem {
return {
id: option.value,
name: option.label,
selected: false
};
}
fromOrderByDto(orderBy: OrderByDTO): FilterItem {
return {
id: orderBy.by,
name: orderBy.label,
selected: false
};
}
}

View File

@@ -0,0 +1,107 @@
import { Injectable } from '@angular/core';
import { InputDTO, UISettingsDTO, InputType, QueryTokenDTO, SortValueDTO } from 'cat-service';
import { Filter } from '../models/filter.model';
import { FilterItemMapping } from './filter-item.mapping';
import { FilterItem } from '../models/filter-item.model';
@Injectable({ providedIn: 'root' })
export class FilterMapping {
constructor(private filterItemMapping: FilterItemMapping) { }
fromInputDto(input: InputDTO): Filter {
let items: FilterItem[] = [];
let max: number;
if (input.options != null && input.options.values) {
max = input.options.max,
items = input.options.values
.map(item => this.filterItemMapping.fromOptionDto(item));
}
if (input.value != null) {
if (max == null) {
const selectedValues = input.value.split(';');
for (const selected of selectedValues) {
const idx = items.findIndex(f => f.id === selected);
if (idx >= 0) {
items[idx].selected = true;
}
}
} else {
const idx = items.findIndex(f => f.id === input.value);
if (idx >= 0) {
items[idx].selected = true;
}
}
}
return {
expanded: false,
id: input.key,
name: input.label,
max,
items
};
}
fromUiSettingsDto(settings: UISettingsDTO): Filter[] {
const filters: Filter[] = [];
const filteredSettings = settings.filter
.filter(f => f.type === InputType.Bool && f.options)
.filter(f => f.options && Array.isArray(f.options.values));
for (const filter of filteredSettings) {
filters.push(this.fromInputDto(filter));
}
if (Array.isArray(settings.orderBy)) {
filters.push({
expanded: false,
id: 'orderBy',
max: 1,
name: 'Sortierung',
items: settings.orderBy.map(o => this.filterItemMapping.fromOrderByDto(o))
});
}
return filters;
}
toQueryTokenDto(target: QueryTokenDTO, source: Filter[]) {
const orderBy = source.find(f => f.id === 'orderBy');
if (orderBy != null) {
target.sort = orderBy.items
.filter(i => i.selected)
.map(m => ({
by: m.id,
asc: m.selected
}) as SortValueDTO);
}
const filter = source
.filter(f => f.id !== 'orderBy')
.filter(f => f.items.some(s => s.selected));
if (Array.isArray(filter)) {
const kvps = filter.map((fil) => {
const key = fil.id;
const value = fil.items.filter(f => f.selected)
.map(f => f.id)
.join(';');
return [key, value];
});
if (kvps.length > 0) {
target.filter = {};
for (const kvp of kvps) {
target.filter[kvp[0]] = kvp[1];
}
}
}
return target;
}
}

View File

@@ -0,0 +1,75 @@
import { Product } from '../models/product.model';
import { isNullOrUndefined } from 'util';
import { AvailabilityDTO, PriceDTO, ItemDTO, AvailabilityType } from 'cat-service';
import { Injectable } from '@angular/core';
import { ShelfInfoDTO } from 'cat-service';
@Injectable({ providedIn: 'root' })
export class ProductMapping {
fromItemDTO(item: ItemDTO): Product {
if (isNullOrUndefined(item)) {
throw new Error('argument item:ItemDTO is null or undefined.');
}
let availability: AvailabilityDTO;
let currency = '';
let price = 0;
let priceDto: PriceDTO;
if (Array.isArray(item.av) && item.av.length > 0) {
availability = item.av.find(av => av.status === AvailabilityType.Available);
if (!!availability) {
priceDto = availability.price;
} else {
priceDto = item.av[0].price;
}
}
if (!!priceDto && priceDto.value) {
price = priceDto.value.value || price;
currency = priceDto.value.currency || currency;
}
let itemsInStock = 0;
if (Array.isArray(item.st)) {
itemsInStock = item.st.reduce((aggr, si) => aggr + si.inStock, 0);
}
let assortment: string;
if (item.sh) {
assortment = item.sh[0].assortment;
}
return {
author: item.pr.contributors,
availability: !!availability,
currency,
price,
id: item.id,
itemsInStock,
err: '',
category: item.pr.productGroup,
icon: '',
notAvailableReason: itemsInStock === 0 ? '' : '',
publisher: item.pr.manufacturer,
recommandation: false,
serial: item.pr.serial,
slogan: item.pr.additionalName,
title: item.pr.name,
type: item.pr.formatDetail,
typeIcon: item.pr.format,
location: assortment,
publicationDate: item.pr.publicationDate,
ean: item.pr.ean,
edition: item.pr.edition,
volume: item.pr.volume
};
}
}

View File

@@ -0,0 +1,9 @@
import { ItemDTO } from 'cat-service';
export interface BookData {
book: ItemDTO;
quantity: number;
price: string;
currency: string;
imgUrl: string;
}

View File

@@ -0,0 +1,4 @@
export interface Breadcrumb {
name: string;
path: string;
}

View File

@@ -0,0 +1,6 @@
import { ItemDTO } from 'cat-service';
export interface Cart {
book: ItemDTO;
quantity: number;
}

View File

@@ -0,0 +1,21 @@
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;
}

View File

@@ -0,0 +1,14 @@
import { FeedBook } from './feed-book.model';
import { FeedEvent } from './feed-event.model';
import { FeedNews } from './feed-news.model';
import { FeedRecommandation } from './feed-recommandation.model';
export interface FeedCard {
id: string;
cardTitle: string;
type: string;
books: FeedBook;
event: FeedEvent[];
news: FeedNews[];
recommandation: FeedRecommandation;
}

View File

@@ -0,0 +1,6 @@
export interface FeedEvent {
id: number;
title: string;
content: string;
imageUrl: string;
}

View File

@@ -0,0 +1,6 @@
export interface FeedNews {
id: number;
title: string;
content: string;
icon: string;
}

View File

@@ -0,0 +1,5 @@
export interface FeedRecommandation {
id: number;
title: string;
content: string;
}

View File

@@ -0,0 +1,5 @@
export interface FilterItem {
id: string;
name: string;
selected: boolean;
}

View File

@@ -0,0 +1,14 @@
import { FilterItem } from './filter-item.model';
export class Filter {
id: string;
name: string;
expanded: boolean;
/**
* undefined => all items can be selected => multiselect
* if max is set to 3, 3 items ca be selected => multiselect
* if max is set to 1 only one item can be selected => select
*/
max?: number;
items: FilterItem[];
}

View File

@@ -0,0 +1,27 @@
import { Breadcrumb } from './breadcrumb.model';
import { Search } from './search.model';
import { User } from './user.model';
import { Cart } from './cart.model';
import { ItemDTO } from 'cat-service';
import { Filter } from './filter.model';
export interface Process {
id: number;
name: string;
new: boolean;
loading: boolean;
selected: boolean;
icon: string;
currentRoute: string;
breadcrumbs: Breadcrumb[];
search: Search;
users: User[];
cart: Cart[];
itemsDTO: ItemDTO[];
selectedItem: ItemDTO;
preventLoading: boolean;
activeUser: User;
productScrollTo: number;
currentPageCached: number;
selectedFilters: Filter[];
}

View File

@@ -0,0 +1,24 @@
export interface Product {
id: number;
author: string;
title: string;
type: string;
typeIcon: string;
category: string;
serial: string;
volume: string;
edition: string;
price: number;
currency: string;
availability: boolean;
publicationDate: Date;
itemsInStock: number;
notAvailableReason: string;
publisher: string;
slogan: string;
icon: string;
recommandation: boolean;
err: string;
location: string;
ean: string;
}

View File

@@ -0,0 +1,4 @@
export interface RecentArticleSearch {
id: number;
name: string;
}

View File

@@ -0,0 +1,9 @@
import { Filter } from './filter.model';
export interface Search {
query: string;
skip: number;
take: number;
fitlers: Filter[];
firstLoad: boolean;
}

View File

@@ -0,0 +1,23 @@
export interface User {
id: number;
name: string;
date_of_birth: string;
email?: string;
phone_number?: string;
delivery_addres?: Address;
invoice_address?: Address;
shop?: boolean;
payement_method?: string;
poossible_addresses?: Address[];
newUser?: boolean;
}
export class Address {
constructor (
public name: string,
public street: string,
public zip: number,
public city: string,
public company_name?: string,
) {}
}

View File

@@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { tap, map, distinctUntilChanged } from 'rxjs/operators';
import { isNullOrUndefined } from 'util';
import { ReplaySubject } from 'rxjs';
import { SKIP_BASIC_AUTHORIZATION_INTERCEPTOR } from '../interceptors';
import { environment } from '../../../environments/environment';
@Injectable({ providedIn: 'root' })
export class ConfigService {
private configSub = new ReplaySubject<Object>();
constructor(private httpClient: HttpClient) { }
load(): Promise<Object> {
return this.httpClient.get(environment.config, { headers: { SKIP_BASIC_AUTHORIZATION_INTERCEPTOR } })
.pipe(tap(result => this.configSub.next(result)))
.toPromise();
}
select<T = any>(...path: string[]) {
return this.configSub.asObservable().pipe(
map((config) => {
let value = config;
for (const _path of path) {
if (isNullOrUndefined(value[_path])) {
console.warn(`No configuration available for ${path.join('.')}. Add configuration to /assets/config.json.`);
return;
}
value = value[_path];
}
return value as T;
}),
distinctUntilChanged()
);
}
}

View File

@@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { FilterService } from './filter.service';
describe('FilterService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: FilterService = TestBed.get(FilterService);
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,96 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Filter } from '../models/filter.model';
import { filterMock } from 'mocks/filters.mock';
import { CatSearchService, ApiResponse, UISettingsDTO } from 'cat-service';
import { FilterItem } from '../models/filter-item.model';
import { FilterMapping } from '../mappings/filter.mapping';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class FilterService {
constructor(
private service: CatSearchService,
private filterMapping: FilterMapping
) { }
selectFilterById(filters: Filter[], id: string): Observable<Filter[]> {
const newFilterState = filters.map((filter: Filter) => {
if (filter.id === id) {
return { ...filter, expanded: true };
}
return { ...filter, expanded: false };
});
return of(newFilterState);
}
unselectFilterById(filters: Filter[], id: string): Observable<Filter[]> {
const newFilterState = filters.map((filter: Filter) => {
if (filter.id === id) {
return { ...filter, expanded: false };
}
return { ...filter };
});
return of(newFilterState);
}
toggleFilterItemsById(filters: Filter[], id: string): Observable<Filter[]> {
const newFilterState = filters.map((filter: Filter) => {
if (filter.expanded === true) {
const newItemsState = this.toggleItem(filter.items, id);
return { ...filter, items: newItemsState };
}
return { ...filter };
});
return of(newFilterState);
}
toggleFilterItemsByName(filters: Filter[], name: string): Observable<Filter[]> {
const newFilterState = filters.map((filter: Filter) => {
if (filter.expanded === true) {
const newItemsState = this.toggleItemByName(filter.items, name);
return { ...filter, items: newItemsState };
}
return { ...filter };
});
return of(newFilterState);
}
private toggleItem(items: FilterItem[], id: string): FilterItem[] {
return items.map((item: FilterItem) => {
if (item.id === id) {
return { ...item, selected: !item.selected };
}
return { ...item };
});
}
private toggleItemByName(items: FilterItem[], name: string): FilterItem[] {
return items.map((item: FilterItem) => {
if (item.name === name) {
return { ...item, selected: !item.selected };
}
return { ...item };
});
}
// service method to get the first 3 filters
getFilters(): Observable<Filter[]> {
return this.service.settings().pipe(
map(
(data: ApiResponse<UISettingsDTO>) =>
this.filterMapping.fromUiSettingsDto(data.result).slice(0, 3)
));
}
// service method to get filters metadata
getFullFilter(): Observable<Filter[]> {
return this.service.settings().pipe(
map(
(data: ApiResponse<UISettingsDTO>) =>
this.filterMapping.fromUiSettingsDto(data.result)
));
}
}

View File

@@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ModalService {
private modals: any[] = [];
add(modal: any) {
// add modal to array of active modals
this.modals.push(modal);
}
remove(id: string) {
// remove modal from array of active modals
this.modals = this.modals.filter(x => x.id !== id);
}
open(id: string) {
// open modal specified by id
const modal: any = this.modals.filter(x => x.id === id)[0];
modal.open();
}
close(id: string) {
// close modal specified by id
const modal: any = this.modals.filter(x => x.id === id)[0];
modal.close();
}
}

View File

@@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { ProductService } from './product.service';
describe('ProductService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: ProductService = TestBed.get(ProductService);
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,134 @@
import { Injectable } from '@angular/core';
import { procuctsMock } from 'mocks/products.mock';
import { of, Observable } from 'rxjs';
import { Product } from '../models/product.model';
import { RecentArticleSearch } from '../models/recent-article-search.model';
import {
CatSearchService,
CatImageService,
QueryTokenDTO,
ItemDTO,
PagedApiResponse
} from 'cat-service';
import { map } from 'rxjs/operators';
import { Search } from '../models/search.model';
import { FilterMapping } from '../mappings/filter.mapping';
@Injectable({
providedIn: 'root'
})
export class ProductService {
searchResponse$: Observable<PagedApiResponse<ItemDTO>>;
constructor(
private searchService: CatSearchService,
private filterMapping: FilterMapping
) {}
persistLastSearchToLocalStorage(param: string) {
// get recent searches from local storage
const recentSearches: RecentArticleSearch[] = JSON.parse(
localStorage.getItem('recent_searches')
);
/*
* check if there are search items in local storage, if there are not add search to local storage
* else check if current search already exists in loacl storage, if exist delete it (deletion is made becouse
* we want every search to display in LIFO order).
* finaly push the new search at the end of the local storage array
*/
if (recentSearches) {
const searches = [
...recentSearches.filter(data => {
return data.name !== param;
}),
<RecentArticleSearch>{
id: recentSearches[recentSearches.length - 1].id + 1,
name: param
}
];
localStorage.setItem('recent_searches', JSON.stringify(searches));
} else {
const searches = [
<RecentArticleSearch>{
id: 1,
name: param
}
];
localStorage.setItem('recent_searches', JSON.stringify(searches));
}
}
getRecentSearches(): Observable<RecentArticleSearch[]> {
const recentSearches: RecentArticleSearch[] = JSON.parse(
localStorage.getItem('recent_searches')
);
return of(recentSearches);
}
// placeholder service method for calling product search API
searchProducts(params: string): Observable<Product[]> {
this.persistLastSearchToLocalStorage(params);
return of(procuctsMock);
}
// service method for calling product search API
searchItems(search: Search): Observable<ItemDTO[]> {
this.persistLastSearchToLocalStorage(search.query);
const queryToken = <QueryTokenDTO>{
input: { qs: search.query },
skip: search.skip,
take: search.take
};
const queryWithFilters = this.filterMapping.toQueryTokenDto(
queryToken,
search.fitlers
);
return this.searchService.search(queryWithFilters).pipe(
map(response => {
if (response.error) {
throw new Error(response.message);
}
return response;
}),
map(response => response.result)
);
}
searchItemsWithPagination(
query,
skip,
size,
filters
): Observable<PagedApiResponse<ItemDTO>> {
const queryToken = <QueryTokenDTO>{
input: { qs: query },
skip: skip,
take: size
};
const queryWithFilters = this.filterMapping.toQueryTokenDto(
queryToken,
filters
);
return this.searchService.search(queryWithFilters).pipe(
map(response => {
if (response.error) {
throw new Error(response.message);
}
this.persistLastSearchToLocalStorage(query);
return response;
})
);
}
getItemById(id: number): Observable<ItemDTO> {
return this.searchService.getById(id).pipe(
map(response => {
if (response.error) {
throw new Error(response.message);
}
return response;
}),
map(response => response.result)
);
}
}

View File

@@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
describe('UserService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: UserService = TestBed.get(UserService);
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import { User, Address } from '../../core/models/user.model';
import { usersMock } from 'mocks/users.mock';
import { Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor() {
localStorage.setItem('users', JSON.stringify(usersMock));
}
searchUser(params: string): Observable<User[]> {
const mockedUsers = JSON.parse(localStorage.getItem('users'));
let foundUsers: User[] = [];
const splitedParams = params.split(' ');
mockedUsers.forEach((user: User) => {
splitedParams.forEach((word: string) => {
if (word) {
if (user.name.toLowerCase().indexOf(word.toLowerCase()) >= 0 ||
user.delivery_addres.city.toString().toLowerCase().indexOf(word.toLowerCase()) >= 0 ||
user.delivery_addres.zip.toString().toLowerCase().indexOf(params.toLowerCase()) >= 0
) {
const userExists = foundUsers.filter((userAdded: User) => userAdded.id === user.id);
if (userExists.length === 0) {
foundUsers.push(user);
}
}
}
});
});
return of(foundUsers);
}
addUser(user: User) {
const mockedUsers = JSON.parse(localStorage.getItem('users'));
localStorage.setItem('users',
JSON.stringify([...mockedUsers, user])
);
}
}

View File

@@ -0,0 +1,7 @@
export const LOAD_PRODUCT_SEARCH_AUTOCOMPLETE = '[AUTOCOMPLETE] Load product';
export class LoadAutocomplete {
static readonly type = LOAD_PRODUCT_SEARCH_AUTOCOMPLETE;
constructor(public payload: string) {}
}

View File

@@ -0,0 +1,9 @@
import { Breadcrumb } from '../../models/breadcrumb.model';
export const ADD_BREADCRUMB = '[BREADCRUMB] Add';
export class AddBreadcrumb {
static readonly type = ADD_BREADCRUMB;
constructor(public payload: Breadcrumb) {}
}

View File

@@ -0,0 +1,5 @@
export const LOAD_FEED = '[FEED] Load';
export class LoadFeed {
static readonly type = LOAD_FEED;
}

View File

@@ -0,0 +1,40 @@
import { Filter } from '../../models/filter.model';
export const LOAD_FILTERS = '[FILTERS] Load';
export const LOAD_FULL_FILTERS = '[FILTERS] Load full';
export const SELECT_FILTER_BY_ID = '[FILTERS] Select by id';
export const UNSELECT_FILTER_BY_ID = '[FILTERS] Unselect by id';
export const TOGGLE_FILTER_ITEM_BY_ID = '[FILTERS] Toggle item by id';
export const TOGGLE_FILTER_ITEM_BY_NAME = '[FILTERS] Toggle item by name';
export class LoadFilters {
static readonly type = LOAD_FILTERS;
}
export class LoadFullFilters {
static readonly type = LOAD_FULL_FILTERS;
}
export class SelectFilterById {
static readonly type = SELECT_FILTER_BY_ID;
constructor(public id: string) {}
}
export class UnselectFilterById {
static readonly type = UNSELECT_FILTER_BY_ID;
constructor(public id: string) {}
}
export class ToggleFilterItemById {
static readonly type = TOGGLE_FILTER_ITEM_BY_ID;
constructor(public id: string, public payload: Filter[]) {}
}
export class ToggleFilterItemByName {
static readonly type = TOGGLE_FILTER_ITEM_BY_NAME;
constructor(public name: string) {}
}

View File

@@ -0,0 +1,7 @@
export const NOTIFY = 'NOTIFY';
export class Notify {
static readonly type = NOTIFY;
constructor(public payload: number) {}
}

View File

@@ -0,0 +1,143 @@
import { Process } from '../../models/process.model';
import { Search } from '../../models/search.model';
import { ItemDTO } from 'cat-service';
import { Breadcrumb } from '../../models/breadcrumb.model';
import { User } from '../../models/user.model';
import { Filter } from '../../models/filter.model';
import { FilterItem } from '../../models/filter-item.model';
export const ADD_PROCESS = '[PROCESS] Add';
export const DELETE_PROCESS = '[PROCESS] Delete';
export const SELECT_PROCESS = '[PROCESS] Select';
export const CHANGE_CURRENT_ROUTE = '[PROCESS] Change current route';
export const ADD_SEARCH = '[PROCESS] Add search';
export const SEARCH_USER = '[PROCESS] Search for user';
export const SET_ACTIVE_USER = '[PROCESS] Set active user in tab';
export const SET_CART = '[PROCESS] Set cart data for user';
export const PREVENT_PRODUCT_LOAD = '[POCESS] Prevent product load';
export const ALLOW_PRODUCT_LOAD = '[POCESS] Allow product load';
export const ADD_USER = '[PROCESS] Add new user to store';
export const SET_EDIT_USER = '[PROCESS] User which data will be updated';
export const ADD_BREADCRUMB = '[PROCESS] Add breadcrumb';
export const UPDATE_BREADCRUMB = '[PROCESS] Update breadcrumb';
export const UPDATE_CURRENT_BREADCRUMB_NAME =
'[PROCESS] Update breadcrumb name';
export const POP_BREADCRUMB = '[PROCESS] Pop breadcrumb';
export const ADD_SELECTED_FILTER = '[PROCESS] Add selected process';
export const REMOVE_SELECTED_FILTER = '[PROCESS] Remove selected process';
export const RESET_BREADCRUMB = '[PROCESS] Reset breadcumbs';
export class AddProcess {
static readonly type = ADD_PROCESS;
constructor(public payload: Process) {}
}
export class DeleteProcess {
static readonly type = DELETE_PROCESS;
constructor(public payload: Process) {}
}
export class SelectProcess {
static readonly type = SELECT_PROCESS;
constructor(public payload: Process) {}
}
export class ChangeCurrentRoute {
static readonly type = CHANGE_CURRENT_ROUTE;
constructor(
public payload: string,
public removeLastBreadcrumb: boolean = false
) {}
}
export class AddSearch {
static readonly type = ADD_SEARCH;
constructor(public payload: Search) {}
}
export class SearchUser {
static readonly type = SEARCH_USER;
constructor(public payload: string) {}
}
export class SetActiveUser {
static readonly type = SET_ACTIVE_USER;
constructor(public payload: User) {}
}
export class SetCartData {
static readonly type = SET_CART;
constructor(
public quantity: number,
public payload: ItemDTO,
public breadcrumb: Breadcrumb
) {}
}
export class PreventProductLoad {
static readonly type = PREVENT_PRODUCT_LOAD;
}
export class AllowProductLoad {
static readonly type = ALLOW_PRODUCT_LOAD;
}
export class AddUser {
static readonly type = ADD_USER;
constructor(public payload: User) {}
}
export class SetUserDetails {
static readonly type = SET_EDIT_USER;
constructor(public payload: User) {}
}
export class AddBreadcrumb {
static readonly type = ADD_BREADCRUMB;
constructor(public payload: Breadcrumb) {}
}
export class UpdateBreadcrump {
static readonly type = UPDATE_BREADCRUMB;
constructor(public payload: Breadcrumb) {}
}
export class UpdateCurrentBreadcrumbName {
static readonly type = UPDATE_CURRENT_BREADCRUMB_NAME;
constructor(public payload: string) {}
}
export class PopBreadcrumbsAfterCurrent {
static readonly type = POP_BREADCRUMB;
constructor(public payload: Breadcrumb) {}
}
export class AddSelectedFilter {
static readonly type = ADD_SELECTED_FILTER;
constructor(public payload: Filter) {}
}
export class RemoveSelectedFilter {
static readonly type = REMOVE_SELECTED_FILTER;
constructor(public payload: FilterItem) {}
}
export class ResetBreadcrumbsTo {
static readonly type = RESET_BREADCRUMB;
constructor(public payload: Breadcrumb) {}
}

View File

@@ -0,0 +1,39 @@
import { ItemDTO } from 'projects/cat-service/src/lib';
import { Search } from '../../models/search.model';
import { Product } from '../../models/product.model';
export const LOAD_RECENT_PRODUCTS = '[PRODUCTS] Load recent';
export const GET_PRODUCTS = '[PRODUCTS] Get';
export const SET_PRODUCTS = '[PRODUCTS] Set';
export const ADD_SELECTED_PRODUCT = '[PRODUCTS] Add selected';
export const CURRENT_PAGE_LOADED = '[PRODUCTS] Current page cached';
export class LoadRecentProducts {
static readonly type = LOAD_RECENT_PRODUCTS;
constructor() {}
}
export class GetProducts {
static readonly type = GET_PRODUCTS;
constructor(public payload: Search) {}
}
export class SetProducts {
static readonly type = SET_PRODUCTS;
constructor(public payload: ItemDTO[], public search: string) {}
}
export class AddSelectedProduct {
static readonly type = ADD_SELECTED_PRODUCT;
constructor(public payload: ItemDTO, public index: number) {}
}
export class CurrentPageLoaded {
static readonly type = CURRENT_PAGE_LOADED;
constructor(public page: number) {}
}

View File

@@ -0,0 +1,39 @@
import { State, Selector, Action, StateContext } from '@ngxs/store';
import { CatSearchService, PagedApiResponse, AutocompleteTokenDTO } from 'cat-service';
import { LoadAutocomplete } from '../actions/autocomplete.actions';
export class AutocompleteStateModel {
result: string[];
}
@State<AutocompleteStateModel>({
name: 'autocomplete',
defaults: {
result: []
}
})
export class AutocompleteState {
constructor(private service: CatSearchService) {}
@Selector()
static getAutocompleteResults(state: AutocompleteStateModel) {
return state.result;
}
@Action(LoadAutocomplete)
load(ctx: StateContext<AutocompleteStateModel>, { payload }: LoadAutocomplete) {
const state = ctx.getState();
this.service.complete(<AutocompleteTokenDTO>{
input: payload,
take: 1
}).subscribe(
(result: PagedApiResponse<string>) => {
const response = result.result;
ctx.patchState({
...state,
result: response
});
}
);
}
}

View File

@@ -0,0 +1,29 @@
import { Breadcrumb } from '../../models/breadcrumb.model';
import { State, Selector, Action, StateContext } from '@ngxs/store';
import { AddBreadcrumb } from '../actions/breadcrumb.actions';
export class BreadcrumbsStateModel {
breadcrumbs: Breadcrumb[];
}
@State<BreadcrumbsStateModel>({
name: 'breadcrumbs',
defaults: {
breadcrumbs: []
}
})
export class BreadcrumbsState {
@Selector()
static getBreadcrumbs(state: BreadcrumbsStateModel) {
return state.breadcrumbs;
}
@Action(AddBreadcrumb)
add(ctx: StateContext<BreadcrumbsStateModel>, { payload }: AddBreadcrumb) {
const state = ctx.getState();
ctx.patchState({
...state,
breadcrumbs: [...state.breadcrumbs, payload]
});
}
}

View File

@@ -0,0 +1,58 @@
import { State, Selector, Action, StateContext } from '@ngxs/store';
import { LoadFeed } from '../actions/feed.actions';
import { feedMock } from 'mocks/feed.mock';
import { FeedCard } from '../../models/feed-card.model';
import { FeedService, FeedDTO } from 'feed-service';
import { map } from 'rxjs/operators';
import { FeedMapping } from 'apps/sales/src/app/core/mappings/feed.mapping';
import { PagedApiResponse } from 'projects/feed-service/src/lib';
export class FeedStateModel {
feed: FeedCard[];
loading: boolean;
}
@State<FeedStateModel>({
name: 'feed',
defaults: {
loading: false,
feed: []
}
})
export class FeedState {
constructor(
private feedService: FeedService,
private feedMapping: FeedMapping
) {}
@Selector()
static getFeed(state: FeedStateModel) {
return [...state.feed, feedMock[3]];
}
@Selector()
static loading(state: FeedStateModel) {
return state.loading;
}
@Action(LoadFeed)
load(ctx: StateContext<FeedStateModel>) {
const state = ctx.getState();
ctx.patchState({
loading: true
});
this.feedService
.info()
.subscribe((feed: PagedApiResponse<FeedDTO<any>>) => {
const feeds = feed.result.map(t => this.feedMapping.fromFeedDTO(t));
ctx.patchState({
loading: false,
feed: [...feeds]
});
});
// ctx.patchState({
// ...state,
// feed: [...feedMock]
// });
}
}

View File

@@ -0,0 +1,180 @@
import { Filter } from '../../models/filter.model';
import {
State,
Selector,
Action,
StateContext,
createSelector
} from '@ngxs/store';
import {
LoadFilters,
LoadFullFilters,
SelectFilterById,
UnselectFilterById,
ToggleFilterItemById,
ToggleFilterItemByName
} from '../actions/filter.actions';
import { load } from '@angular/core/src/render3';
import { FilterService } from '../../services/filter.service';
export class FilterStateModel {
filters: Filter[];
}
@State<FilterStateModel>({
name: 'filters',
defaults: {
filters: []
}
})
export class FilterState {
constructor(private filterService: FilterService) {}
@Selector()
static getFilters(state: FilterStateModel) {
return state.filters;
}
@Selector()
static getFilterCount(state: FilterStateModel) {
return state.filters.length;
}
static getFilterIndex(index: number) {
const val = index;
return createSelector(
[FilterState],
(state: FilterStateModel) => {
return state.filters[val];
}
);
}
@Selector()
static getFiltersJSON(state: FilterStateModel) {
return JSON.stringify(
state.filters.reduce((prev, curr) => [...curr.items, ...prev], [])
);
}
@Selector()
static getSelectedFilters(state: FilterStateModel) {
return state.filters.filter(f => f.items.find(i => i.selected === true));
}
@Action(LoadFilters)
load(ctx: StateContext<FilterStateModel>) {
const state = ctx.getState();
this.filterService.getFilters().subscribe((filters: Filter[]) => {
const mock = ['Warengruppe'];
const missingfilters = mock.map((f, i) => ({
expanded: false,
id: 'mock' + i,
items: [],
max: 1,
name: f
}));
ctx.patchState({
...state,
filters: [...filters, ...missingfilters]
});
});
}
@Action(LoadFullFilters)
loadFullFilters(ctx: StateContext<FilterStateModel>) {
const state = ctx.getState();
this.filterService.getFullFilter().subscribe((filters: Filter[]) => {
const mock = [
'Warengruppe',
'Lesealter',
'Sprache',
'Bestand',
'Archiv'
];
const missingfilters = mock.map((f, i) => ({
expanded: false,
id: 'mock' + i,
items: [],
max: 1,
name: f
}));
ctx.patchState({
...state,
filters: [...filters, ...missingfilters]
});
});
}
@Action(SelectFilterById)
selectFilterById(
ctx: StateContext<FilterStateModel>,
{ id }: SelectFilterById
) {
const state = ctx.getState();
const filters = state.filters;
this.filterService
.selectFilterById(filters, id)
.subscribe((filter: Filter[]) => {
ctx.patchState({
...state,
filters: filter
});
});
}
@Action(UnselectFilterById)
unselectFilterById(
ctx: StateContext<FilterStateModel>,
{ id }: UnselectFilterById
) {
const state = ctx.getState();
const filters = state.filters;
this.filterService
.unselectFilterById(filters, id)
.subscribe((filter: Filter[]) => {
ctx.patchState({
...state,
filters: [...filter]
});
});
}
@Action(ToggleFilterItemById)
toggleItemById(
ctx: StateContext<FilterStateModel>,
{ id }: ToggleFilterItemById
) {
const state = ctx.getState();
const filters = state.filters;
this.filterService
.toggleFilterItemsById(filters, id)
.subscribe((filter: Filter[]) => {
ctx.patchState({
...state,
filters: [...filter]
});
});
}
@Action(ToggleFilterItemByName)
toggleItemByName(
ctx: StateContext<FilterStateModel>,
{ name }: ToggleFilterItemByName
) {
const state = ctx.getState();
const filters = state.filters;
this.filterService
.toggleFilterItemsByName(filters, name)
.subscribe((filter: Filter[]) => {
ctx.patchState({
...state,
filters: [...filter]
});
});
}
}

View File

@@ -0,0 +1,28 @@
import { State, Selector, Action, StateContext } from '@ngxs/store';
import { Notify } from '../actions/notifier.actions';
export class NotifierStateModel {
processId: number;
}
@State<NotifierStateModel> ({
name: 'notifier',
defaults: {
processId: null
}
})
export class NotifierState {
@Selector()
static getNotifier(state: NotifierStateModel) {
return state.processId;
}
@Action(Notify)
notify(ctx: StateContext<NotifierStateModel>, { payload }: Notify) {
const state = ctx.getState();
ctx.patchState({
...state,
processId: payload
});
}
}

View File

@@ -0,0 +1,828 @@
import { Process } from '../../models/process.model';
import { State, Selector, Action, StateContext } from '@ngxs/store';
import * as actions from '../actions/process.actions';
import { UserService } from '../../services/user.service';
import { User } from '../../models/user.model';
import { Breadcrumb } from '../../models/breadcrumb.model';
import { Cart } from '../../models/cart.model';
import { ProductService } from '../../services/product.service';
import { RecentArticleSearch } from '../../models/recent-article-search.model';
import {
GetProducts,
LoadRecentProducts,
AddSelectedProduct,
SetProducts,
CurrentPageLoaded
} from '../actions/product.actions';
import { ItemDTO } from 'dist/cat-service/lib/dtos';
import { getCurrentProcess } from '../../utils/process.util';
import { map } from 'rxjs/operators';
import { FilterItem } from '../../models/filter-item.model';
export class ProcessStateModel {
processes: Process[];
recentArticles: RecentArticleSearch[];
}
@State<ProcessStateModel>({
name: 'processes',
defaults: {
processes: [],
recentArticles: []
}
})
export class ProcessState {
constructor(
private usersService: UserService,
protected productService: ProductService
) { }
@Selector()
static getState(state: ProcessStateModel) {
return state;
}
@Selector()
static getProcesses(state: ProcessStateModel) {
return state.processes;
}
@Selector()
static getRecentProducts(state: ProcessStateModel) {
return state.recentArticles;
}
@Selector()
static getProcessCount(state: ProcessStateModel) {
return state.processes.length;
}
@Selector()
static getProducts(state: ProcessStateModel) {
return state.processes.find(t => t.selected === true).itemsDTO;
}
@Selector()
static getSelectedProduct(state: ProcessStateModel) {
return state.processes.find(t => t.selected === true).selectedItem;
}
@Selector()
static getSelectedFilterItems(state: ProcessStateModel): FilterItem[] {
const selectedProcess = state.processes.find(t => t.selected === true);
const filterItems: FilterItem[] = [];
selectedProcess.selectedFilters.map(f => f.items.map(i => {
if (i.selected === true) {
filterItems.push(i);
}
}));
return filterItems;
}
@Selector()
static getCurrentProcess(state: ProcessStateModel) {
return state.processes.find(t => t.selected === true);
}
@Selector()
static getCurrentProcessItems(state: ProcessStateModel) {
if (!state.processes.find(t => t.selected === true)) {
return;
}
return state.processes.find(t => t.selected === true).itemsDTO;
}
@Selector()
static getUsers(state: ProcessStateModel) {
return state.processes.find(t => t.selected === true).users;
}
@Selector()
static getBreadcrumbs(state: ProcessStateModel) {
return state.processes.find(t => t.selected === true).breadcrumbs;
}
@Selector()
static getActiveUser(state: ProcessStateModel) {
return state.processes.find(t => t.selected === true).activeUser;
}
@Selector()
static getCart(state: ProcessStateModel) {
return state.processes.find(t => t.selected === true).cart;
}
@Selector()
static getScrollPositionForProduct(state: ProcessStateModel) {
return state.processes.find(t => t.selected === true).productScrollTo;
}
@Selector()
static getCurrentPageCached(state: ProcessStateModel) {
return state.processes.find(t => t.selected === true).currentPageCached;
}
@Selector()
static getProcessFilters(state: ProcessStateModel) {
return state.processes.find(t => t.selected === true).selectedFilters;
}
@Action(actions.AddProcess)
add(ctx: StateContext<ProcessStateModel>, { payload }: actions.AddProcess) {
const state = ctx.getState();
const processes = state.processes.map((process: Process) => {
if (process.selected === true) {
return { ...process, selected: false, new: false };
}
return { ...process, new: false };
});
ctx.patchState({
...state,
processes: [...processes, payload]
});
}
@Action(actions.DeleteProcess)
delete(
{ patchState, dispatch, getState },
{ payload }: actions.DeleteProcess
) {
const state = getState();
const indexOfProcessToDelete = payload.selected
? state.processes.indexOf(payload)
: -1;
let selectedProcess = null;
const newProcessState = state.processes
.filter(p => p.id !== payload.id)
.map((process, index) => {
if (index === indexOfProcessToDelete - 1) {
selectedProcess = process;
}
return process;
});
patchState({
...state,
processes: newProcessState
});
if (selectedProcess != null) {
dispatch(new actions.SelectProcess(selectedProcess));
}
}
@Action(actions.SelectProcess)
select(
ctx: StateContext<ProcessStateModel>,
{ payload }: actions.SelectProcess
) {
const state = ctx.getState();
const newProcessState = state.processes.map((process: Process) => {
if (process.selected === true && process.id !== payload.id) {
return { ...process, selected: false, new: false };
} else if (process.selected === false && process.id === payload.id) {
return { ...process, selected: true, new: false };
} else {
return process;
}
});
ctx.patchState({
...state,
processes: [...newProcessState]
});
}
@Action(actions.AddSearch)
addSearch(
ctx: StateContext<ProcessStateModel>,
{ payload }: actions.AddSearch
) {
const state = ctx.getState();
const newProcessState = state.processes.map((process: Process) => {
if (process.selected === true) {
return { ...process, search: payload };
} else {
return process;
}
});
ctx.patchState({
...state,
processes: [...newProcessState]
});
}
@Action(actions.ChangeCurrentRoute)
changeCurrentRoute(
ctx: StateContext<ProcessStateModel>,
{ payload, removeLastBreadcrumb }: actions.ChangeCurrentRoute
) {
const state = ctx.getState();
const newProcessState = state.processes.map((process: Process) => {
if (process.selected === true) {
if (removeLastBreadcrumb) {
const updateBreadcrumbs: Breadcrumb[] = [];
process.breadcrumbs.forEach(
(breadcrumb: Breadcrumb, index: number) => {
if (process.breadcrumbs.length - 1 !== index) {
updateBreadcrumbs.push(breadcrumb);
}
}
);
return {
...process,
currentRoute: payload,
breadcrumbs: updateBreadcrumbs,
new: false
};
} else {
return { ...process, currentRoute: payload, new: false };
}
} else {
return process;
}
});
ctx.patchState({
...state,
processes: [...newProcessState]
});
}
@Action(actions.SearchUser)
searchUser(
ctx: StateContext<ProcessStateModel>,
{ payload }: actions.SearchUser
) {
return this.usersService.searchUser(payload).pipe(
map((users: User[]) => {
const state = ctx.getState();
const newProcessState = state.processes.map((process: Process) => {
if (process.selected === true) {
const breadcrumbExist = process.breadcrumbs.filter(
(breadcrumb: Breadcrumb) => breadcrumb.name === payload
);
if (breadcrumbExist.length > 0) {
// Breadcrumb already exists
return { ...process, users: users };
} else {
// If users found, add new breadcrumb for searched term
const currentBreadcrumbs = [...process.breadcrumbs];
const missingBreadcrumb = currentBreadcrumbs.filter(
(breadcrumb: Breadcrumb) =>
breadcrumb.path === '/customer-search'
);
if (missingBreadcrumb.length === 0) {
currentBreadcrumbs.push(
{
name: 'Kundensuche',
path: '/customer-search'
},
{
name: payload,
path: '/customer-search-result'
}
);
} else {
currentBreadcrumbs.push({
name: payload,
path: '/customer-search-result'
});
}
if (users.length > 0 && currentBreadcrumbs.length > 1) {
// Update breadcrumbs if last search was a success
return {
...process,
users: users,
breadcrumbs: [...currentBreadcrumbs]
};
} else if (users.length > 0) {
// Users found, add new breadcrumb for search
return {
...process,
users: users,
breadcrumbs: [
...currentBreadcrumbs,
{
name: payload,
path: '/customer-search-result'
}
]
};
} else {
// Remove last breadcrumb customer search
const removedLastCustomersearchBreadcrumbs = process.breadcrumbs.filter(
(breadcrumb: Breadcrumb) =>
breadcrumb.path !== '/customer-search-result'
);
return {
...process,
users: [],
breadcrumbs: [...removedLastCustomersearchBreadcrumbs]
};
}
}
} else {
return process;
}
});
ctx.patchState({
...state,
processes: [...newProcessState]
});
})
);
}
@Action(actions.SetActiveUser)
setActiveUser(
ctx: StateContext<ProcessStateModel>,
{ payload }: actions.SetActiveUser
) {
const state = ctx.getState();
const newProcessState = state.processes.map((process: Process) => {
if (process.selected === true) {
return { ...process, name: payload.name, activeUser: payload };
} else {
return process;
}
});
ctx.patchState({
...state,
processes: [...newProcessState]
});
}
@Action(actions.SetCartData)
setCartData(
ctx: StateContext<ProcessStateModel>,
{ quantity, payload, breadcrumb }: actions.SetCartData
) {
const state = ctx.getState();
const newProcessState = state.processes.map((process: Process) => {
if (process.selected === true) {
let currentCart = process.cart ? process.cart : [];
const itemExists = currentCart.filter(
(item: Cart) => item.book.id === payload.id
);
if (itemExists.length > 0) {
// Update item in cart
currentCart = currentCart.map((item: Cart) => {
if (item.book.id === payload.id) {
return {
quantity: item.quantity + quantity,
book: payload
};
}
return item;
});
return {
...process,
breadcrumbs: [...process.breadcrumbs, breadcrumb],
cart: [...currentCart]
};
} else {
// Add new item to cart
return {
...process,
breadcrumbs: [...process.breadcrumbs, breadcrumb],
cart: [
...currentCart,
{
quantity: quantity,
book: payload
}
]
};
}
} else {
return process;
}
});
ctx.patchState({
...state,
processes: [...newProcessState]
});
}
@Action(GetProducts)
getProducts(ctx: StateContext<ProcessStateModel>, { payload }: GetProducts) {
const state = ctx.getState();
if (!state.processes) {
return;
}
const breadcrumb = <Breadcrumb>{
name: payload.query,
path: '/search-results#start'
};
const currentProcess = this.updateBreadcrumbForCurrentProcess(
getCurrentProcess(state.processes),
breadcrumb
);
if (
currentProcess.search === payload &&
currentProcess.itemsDTO &&
currentProcess.itemsDTO.length > 0 &&
currentProcess.preventLoading
) {
ctx.patchState({
...state
});
} else {
ctx.patchState({
...state,
processes: state.processes.map(p =>
p.id !== currentProcess.id ? p : { ...currentProcess, loading: true }
)
});
this.productService.searchItems(payload).subscribe((items: ItemDTO[]) => {
if (items) {
ctx.patchState({
...state,
processes:
payload.skip === 0
? this.changeProducResultsForCurrentProcess(
state.processes,
items,
''
)
: this.extendProducResultsForCurrentProcess(
state.processes,
items
)
});
}
});
}
}
@Action(SetProducts, { cancelUncompleted: true })
setProducts(
ctx: StateContext<ProcessStateModel>,
{ payload, search }: SetProducts
) {
const state = ctx.getState();
const currentProcess = getCurrentProcess(state.processes);
ctx.patchState({
...state,
processes: this.changeProducResultsForCurrentProcess(
state.processes,
payload,
search
)
});
}
@Action(LoadRecentProducts)
loadRecentProducts(ctx: StateContext<ProcessStateModel>) {
const state = ctx.getState();
this.productService
.getRecentSearches()
.subscribe((products: RecentArticleSearch[]) => {
if (products) {
ctx.patchState({
...state,
recentArticles: products.reverse().slice(0, 5)
});
}
});
}
@Action(AddSelectedProduct)
AddSelectedProduct(
ctx: StateContext<ProcessStateModel>,
{ payload, index }: AddSelectedProduct
) {
const state = ctx.getState();
ctx.patchState({
...state,
processes: this.changeSelectedItemForCurrentProcess(
state.processes,
payload,
index
)
});
}
@Action(actions.PreventProductLoad)
preventProductLoad(ctx: StateContext<ProcessStateModel>) {
const state = ctx.getState();
ctx.patchState({
...state,
processes: state.processes.map(process => {
if (process.selected === true) {
return { ...process, preventLoading: true };
}
return { ...process };
})
});
}
@Action(actions.AllowProductLoad)
allowProductLoad(ctx: StateContext<ProcessStateModel>) {
const state = ctx.getState();
ctx.patchState({
...state,
processes: state.processes.map(process => {
if (process.selected === true) {
return { ...process, preventLoading: false };
}
return { ...process };
})
});
}
changeSelectedItemForCurrentProcess(
processes: Process[],
item: ItemDTO,
index: number
): Process[] {
const newProcessState = processes.map(process => {
if (process.selected === true) {
return { ...process, selectedItem: item, productScrollTo: index };
}
return { ...process };
});
return newProcessState;
}
changeProducResultsForCurrentProcess(
processes: Process[],
items: ItemDTO[],
search: string
): Process[] {
const newProcessState = processes.map(process => {
const breadcrumb = <Breadcrumb>{
name: search + ` (${items.length} Ergebnisse)`,
path: '/search-results#start'
};
if (process.selected === true) {
return {
...this.updateBreadcrumbForCurrentProcess(process, breadcrumb),
itemsDTO: items,
loading: false
};
}
return { ...this.updateBreadcrumbForCurrentProcess(process, breadcrumb) };
});
return newProcessState;
}
extendProducResultsForCurrentProcess(
processes: Process[],
items: ItemDTO[]
): Process[] {
const newProcessState = processes.map(process => {
if (process.selected === true) {
return {
...process,
itemsDTO: [...process.itemsDTO, ...items],
loading: false
};
}
return { ...process };
});
return newProcessState;
}
@Action(actions.AddUser)
addUser(ctx: StateContext<ProcessStateModel>, { payload }: actions.AddUser) {
this.usersService.addUser(payload);
}
@Action(actions.SetUserDetails)
editUser(
ctx: StateContext<ProcessStateModel>,
{ payload }: actions.SetUserDetails
) {
const state = ctx.getState();
ctx.patchState({
...state,
processes: state.processes.map(process => {
if (process.selected === true) {
return {
...process,
activeUser: payload,
breadcrumbs: [
...process.breadcrumbs,
{
name: 'Kundendetails',
path: '/customer-edit/' + payload.id
}
]
};
}
return { ...process };
})
});
}
@Action(actions.AddBreadcrumb)
addBreadcrumb(
ctx: StateContext<ProcessStateModel>,
{ payload }: actions.AddBreadcrumb
) {
const state = ctx.getState();
ctx.patchState({
...state,
processes: state.processes.map(process => {
if (process.selected === true) {
return {
...this.updateBreadcrumbForCurrentProcess(process, payload)
};
}
return { ...process };
})
});
}
@Action(actions.UpdateBreadcrump)
updateBreadcrumb(
ctx: StateContext<ProcessStateModel>,
{ payload }: actions.UpdateBreadcrump
) {
const state = ctx.getState();
ctx.patchState({
...state,
processes: state.processes.map(process => {
if (process.selected === true) {
return {
...this.updateBreadcrumbForCurrentProcess(process, payload)
};
}
return { ...process };
})
});
}
@Action(actions.ResetBreadcrumbsTo)
resetBreadCrumbs(ctx: StateContext<ProcessStateModel>, { payload }: actions.ResetBreadcrumbsTo) {
const state = ctx.getState();
ctx.patchState({
...state,
processes: state.processes.map(p => {
if (p.selected === true) {
return {...p, breadcrumbs: [payload]};
}
return {...p};
})
});
}
@Action(actions.PopBreadcrumbsAfterCurrent)
popBreadcrumbs(
ctx: StateContext<ProcessStateModel>,
{ payload }: actions.PopBreadcrumbsAfterCurrent
) {
const state = ctx.getState();
const process = state.processes.find(t => t.selected === true);
const breadcrumbs = [...process.breadcrumbs];
const indexOfCurrentBreadcrumb = breadcrumbs.findIndex(
b => b.name === payload.name
);
for (let x = breadcrumbs.length - 1; x >= 0; x--) {
if (indexOfCurrentBreadcrumb < x) {
ctx.dispatch(new actions.ChangeCurrentRoute(breadcrumbs[x - 1].path));
breadcrumbs.pop();
}
}
ctx.patchState({
...state,
processes: state.processes.map(p => {
if (p.selected === true) {
return { ...p, breadcrumbs: breadcrumbs };
}
return { ...p };
})
});
}
@Action(actions.UpdateCurrentBreadcrumbName)
updateCurrentBreadcrumbName(
ctx: StateContext<ProcessStateModel>,
{ payload }: actions.UpdateCurrentBreadcrumbName
) {
const state = ctx.getState();
const process = state.processes.find(t => t.selected === true);
const breadcrumbs = [...process.breadcrumbs].map((b, i) => {
if (i === process.breadcrumbs.length - 1) {
return { name: payload, path: b.path };
}
return b;
});
ctx.patchState({
...state,
processes: state.processes.map(p => {
if (p.selected === true) {
return { ...p, breadcrumbs: breadcrumbs };
}
return p;
})
});
}
updateBreadcrumbForCurrentProcess(
process: Process,
payload: Breadcrumb
): Process {
if (process.selected === false) {
return process;
}
const breadcrumbExist = process.breadcrumbs.filter(
(breadcrumb: Breadcrumb) => breadcrumb.name === payload.name
);
if (breadcrumbExist.length > 0) {
return process;
}
const updatedBreadcrumbs = process.breadcrumbs.map(
(breadcrumb: Breadcrumb) => {
if (
breadcrumb.path === payload.path ||
breadcrumb.path.substring(0, 16) === payload.path.substring(0, 16)
) {
return { name: payload.name, path: payload.path };
}
return breadcrumb;
}
);
if (!updatedBreadcrumbs.find(b => b.name === payload.name)) {
return <Process>{
...process,
breadcrumbs: [
...process.breadcrumbs,
{
name: payload.name,
path: payload.path
}
]
};
}
return <Process>{
...process,
breadcrumbs: [...updatedBreadcrumbs]
};
}
@Action(CurrentPageLoaded)
setCurrentPage(ctx: StateContext<ProcessStateModel>, { page }: CurrentPageLoaded) {
const state = ctx.getState();
ctx.patchState({
...state,
processes: state.processes.map(p => {
if (p.selected === true) {
return { ...p, currentPageCached: page };
}
return p;
})
});
}
@Action(actions.AddSelectedFilter)
addSelectedFilterToCurrentProcess(ctx: StateContext<ProcessStateModel>, { payload }: actions.AddSelectedFilter) {
const state = ctx.getState();
ctx.patchState({
...state,
processes: state.processes.map(process => {
if (process.selected === true) {
let filters = [];
if (process.selectedFilters) {
filters = [...process.selectedFilters.filter(f => f.id !== payload.id), payload];
} else {
filters.push(payload);
}
return { ...process, selectedFilters: filters };
}
return process;
})
});
}
@Action(actions.RemoveSelectedFilter)
removeSelectedFilterFromCurrentProcess(ctx: StateContext<ProcessStateModel>, { payload }: actions.RemoveSelectedFilter) {
const state = ctx.getState();
const updatedProcesses = state.processes.map(process => {
if (process.selected === true) {
const updatedFilters = process.selectedFilters.map(
f => {
return {...f, items: f.items.map(i => {
if (i.id === payload.id && i.selected === true) {
return <FilterItem>{ ...i, selected: false };
}
return i;
})};
}
);
return { ...process, selectedFilters: updatedFilters };
}
return process;
});
ctx.patchState({
...state,
processes: updatedProcesses
});
}
}

View File

@@ -0,0 +1,9 @@
import { Process } from '../models/process.model';
export function getRandomPic() {
return 'Pic_' + (Math.floor(Math.random() * 6) + 1) + '-3x';
}
export function getCurrentProcess(processes: Process[]): Process {
return processes.find(t => t.selected === true);
}

View File

@@ -0,0 +1,17 @@
export function getProductTypeIcon(type: string): string {
switch (type) {
case 'KT':
return 'TypeBook';
case 'GEB':
return 'TypeBook';
}
}
export const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
export function getFormatedPublicationDate(publicationDate: Date): string {
return new Date(publicationDate).getDay().toString() + '. ' + monthNames[new Date(publicationDate).getMonth()]
+ ' ' + new Date(publicationDate).getFullYear();
}

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