mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
[HIMA-110] moved application files to apps/sales/ & changed import paths to match new paths
This commit is contained in:
28
apps/sales-e2e/protractor.conf.js
Normal file
28
apps/sales-e2e/protractor.conf.js
Normal 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 } }));
|
||||
}
|
||||
};
|
||||
14
apps/sales-e2e/src/app.e2e-spec.ts
Normal file
14
apps/sales-e2e/src/app.e2e-spec.ts
Normal 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!');
|
||||
});
|
||||
});
|
||||
11
apps/sales-e2e/src/app.po.ts
Normal file
11
apps/sales-e2e/src/app.po.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
13
apps/sales-e2e/tsconfig.e2e.json
Normal file
13
apps/sales-e2e/tsconfig.e2e.json
Normal 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
11
apps/sales/browserslist
Normal 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
31
apps/sales/karma.conf.js
Normal 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
|
||||
});
|
||||
};
|
||||
29
apps/sales/ngsw-config.json
Normal file
29
apps/sales/ngsw-config.json
Normal 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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
39
apps/sales/src/app/app-routing.module.ts
Normal file
39
apps/sales/src/app/app-routing.module.ts
Normal 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 {}
|
||||
4
apps/sales/src/app/app.component.html
Normal file
4
apps/sales/src/app/app.component.html
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
<sales-header></sales-header>
|
||||
<sales-content [isConnected]="isConnected"></sales-content>
|
||||
<sales-menu></sales-menu>
|
||||
5
apps/sales/src/app/app.component.scss
Normal file
5
apps/sales/src/app/app.component.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
h1 {
|
||||
color: #369;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 250%;
|
||||
}
|
||||
35
apps/sales/src/app/app.component.spec.ts
Normal file
35
apps/sales/src/app/app.component.spec.ts
Normal 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!');
|
||||
});
|
||||
});
|
||||
24
apps/sales/src/app/app.component.ts
Normal file
24
apps/sales/src/app/app.component.ts
Normal 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';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
129
apps/sales/src/app/app.module.ts
Normal file
129
apps/sales/src/app/app.module.ts
Normal 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 {}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
516
apps/sales/src/app/components/checkout/checkout.component.scss
Normal file
516
apps/sales/src/app/components/checkout/checkout.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
177
apps/sales/src/app/components/checkout/checkout.component.ts
Normal file
177
apps/sales/src/app/components/checkout/checkout.component.ts
Normal 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('.', ',');
|
||||
}
|
||||
}
|
||||
36
apps/sales/src/app/components/header/header.component.html
Normal file
36
apps/sales/src/app/components/header/header.component.html
Normal 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>
|
||||
71
apps/sales/src/app/components/header/header.component.scss
Normal file
71
apps/sales/src/app/components/header/header.component.scss
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
16
apps/sales/src/app/components/header/header.component.ts
Normal file
16
apps/sales/src/app/components/header/header.component.ts
Normal 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() {
|
||||
}
|
||||
}
|
||||
54
apps/sales/src/app/components/menu/menu.component.html
Normal file
54
apps/sales/src/app/components/menu/menu.component.html
Normal 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>
|
||||
39
apps/sales/src/app/components/menu/menu.component.scss
Normal file
39
apps/sales/src/app/components/menu/menu.component.scss
Normal 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;
|
||||
}
|
||||
25
apps/sales/src/app/components/menu/menu.component.spec.ts
Normal file
25
apps/sales/src/app/components/menu/menu.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
78
apps/sales/src/app/components/menu/menu.component.ts
Normal file
78
apps/sales/src/app/components/menu/menu.component.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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%)' }))
|
||||
])
|
||||
]);
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
])
|
||||
]);
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
1
apps/sales/src/app/core/interceptors/index.ts
Normal file
1
apps/sales/src/app/core/interceptors/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './basic-authorization.interceptor';
|
||||
75
apps/sales/src/app/core/mappings/feed.mapping.ts
Normal file
75
apps/sales/src/app/core/mappings/feed.mapping.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
26
apps/sales/src/app/core/mappings/filter-item.mapping.ts
Normal file
26
apps/sales/src/app/core/mappings/filter-item.mapping.ts
Normal 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
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
107
apps/sales/src/app/core/mappings/filter.mapping.ts
Normal file
107
apps/sales/src/app/core/mappings/filter.mapping.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
75
apps/sales/src/app/core/mappings/product.mapping.ts
Normal file
75
apps/sales/src/app/core/mappings/product.mapping.ts
Normal 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
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
9
apps/sales/src/app/core/models/book-data.model.ts
Normal file
9
apps/sales/src/app/core/models/book-data.model.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ItemDTO } from 'cat-service';
|
||||
|
||||
export interface BookData {
|
||||
book: ItemDTO;
|
||||
quantity: number;
|
||||
price: string;
|
||||
currency: string;
|
||||
imgUrl: string;
|
||||
}
|
||||
4
apps/sales/src/app/core/models/breadcrumb.model.ts
Normal file
4
apps/sales/src/app/core/models/breadcrumb.model.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Breadcrumb {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
6
apps/sales/src/app/core/models/cart.model.ts
Normal file
6
apps/sales/src/app/core/models/cart.model.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ItemDTO } from 'cat-service';
|
||||
|
||||
export interface Cart {
|
||||
book: ItemDTO;
|
||||
quantity: number;
|
||||
}
|
||||
21
apps/sales/src/app/core/models/feed-book.model.ts
Normal file
21
apps/sales/src/app/core/models/feed-book.model.ts
Normal 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;
|
||||
}
|
||||
14
apps/sales/src/app/core/models/feed-card.model.ts
Normal file
14
apps/sales/src/app/core/models/feed-card.model.ts
Normal 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;
|
||||
}
|
||||
6
apps/sales/src/app/core/models/feed-event.model.ts
Normal file
6
apps/sales/src/app/core/models/feed-event.model.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface FeedEvent {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
6
apps/sales/src/app/core/models/feed-news.model.ts
Normal file
6
apps/sales/src/app/core/models/feed-news.model.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface FeedNews {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
icon: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface FeedRecommandation {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
5
apps/sales/src/app/core/models/filter-item.model.ts
Normal file
5
apps/sales/src/app/core/models/filter-item.model.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface FilterItem {
|
||||
id: string;
|
||||
name: string;
|
||||
selected: boolean;
|
||||
}
|
||||
14
apps/sales/src/app/core/models/filter.model.ts
Normal file
14
apps/sales/src/app/core/models/filter.model.ts
Normal 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[];
|
||||
}
|
||||
27
apps/sales/src/app/core/models/process.model.ts
Normal file
27
apps/sales/src/app/core/models/process.model.ts
Normal 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[];
|
||||
}
|
||||
24
apps/sales/src/app/core/models/product.model.ts
Normal file
24
apps/sales/src/app/core/models/product.model.ts
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface RecentArticleSearch {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
9
apps/sales/src/app/core/models/search.model.ts
Normal file
9
apps/sales/src/app/core/models/search.model.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Filter } from './filter.model';
|
||||
|
||||
export interface Search {
|
||||
query: string;
|
||||
skip: number;
|
||||
take: number;
|
||||
fitlers: Filter[];
|
||||
firstLoad: boolean;
|
||||
}
|
||||
23
apps/sales/src/app/core/models/user.model.ts
Normal file
23
apps/sales/src/app/core/models/user.model.ts
Normal 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,
|
||||
) {}
|
||||
}
|
||||
41
apps/sales/src/app/core/services/config.service.ts
Normal file
41
apps/sales/src/app/core/services/config.service.ts
Normal 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()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
12
apps/sales/src/app/core/services/filter.service.spec.ts
Normal file
12
apps/sales/src/app/core/services/filter.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
96
apps/sales/src/app/core/services/filter.service.ts
Normal file
96
apps/sales/src/app/core/services/filter.service.ts
Normal 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)
|
||||
));
|
||||
}
|
||||
}
|
||||
28
apps/sales/src/app/core/services/modal.service.ts
Normal file
28
apps/sales/src/app/core/services/modal.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
12
apps/sales/src/app/core/services/product.service.spec.ts
Normal file
12
apps/sales/src/app/core/services/product.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
134
apps/sales/src/app/core/services/product.service.ts
Normal file
134
apps/sales/src/app/core/services/product.service.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
12
apps/sales/src/app/core/services/user.service.spec.ts
Normal file
12
apps/sales/src/app/core/services/user.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
46
apps/sales/src/app/core/services/user.service.ts
Normal file
46
apps/sales/src/app/core/services/user.service.ts
Normal 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])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
5
apps/sales/src/app/core/store/actions/feed.actions.ts
Normal file
5
apps/sales/src/app/core/store/actions/feed.actions.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const LOAD_FEED = '[FEED] Load';
|
||||
|
||||
export class LoadFeed {
|
||||
static readonly type = LOAD_FEED;
|
||||
}
|
||||
40
apps/sales/src/app/core/store/actions/filter.actions.ts
Normal file
40
apps/sales/src/app/core/store/actions/filter.actions.ts
Normal 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) {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const NOTIFY = 'NOTIFY';
|
||||
|
||||
export class Notify {
|
||||
static readonly type = NOTIFY;
|
||||
|
||||
constructor(public payload: number) {}
|
||||
}
|
||||
143
apps/sales/src/app/core/store/actions/process.actions.ts
Normal file
143
apps/sales/src/app/core/store/actions/process.actions.ts
Normal 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) {}
|
||||
}
|
||||
39
apps/sales/src/app/core/store/actions/product.actions.ts
Normal file
39
apps/sales/src/app/core/store/actions/product.actions.ts
Normal 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) {}
|
||||
}
|
||||
39
apps/sales/src/app/core/store/state/autocomplete.state.ts
Normal file
39
apps/sales/src/app/core/store/state/autocomplete.state.ts
Normal 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
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
29
apps/sales/src/app/core/store/state/breadcrumbs.state.ts
Normal file
29
apps/sales/src/app/core/store/state/breadcrumbs.state.ts
Normal 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]
|
||||
});
|
||||
}
|
||||
}
|
||||
58
apps/sales/src/app/core/store/state/feed.state.ts
Normal file
58
apps/sales/src/app/core/store/state/feed.state.ts
Normal 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]
|
||||
// });
|
||||
}
|
||||
}
|
||||
180
apps/sales/src/app/core/store/state/filter.state.ts
Normal file
180
apps/sales/src/app/core/store/state/filter.state.ts
Normal 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]
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
28
apps/sales/src/app/core/store/state/notifier.state.ts
Normal file
28
apps/sales/src/app/core/store/state/notifier.state.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
828
apps/sales/src/app/core/store/state/process.state.ts
Normal file
828
apps/sales/src/app/core/store/state/process.state.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
9
apps/sales/src/app/core/utils/process.util.ts
Normal file
9
apps/sales/src/app/core/utils/process.util.ts
Normal 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);
|
||||
}
|
||||
17
apps/sales/src/app/core/utils/product.util.ts
Normal file
17
apps/sales/src/app/core/utils/product.util.ts
Normal 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
Reference in New Issue
Block a user