Merge branch 'develop' into feature/ISA-9-Filiale-Taetigkeitskalender

This commit is contained in:
Lorenz Hilpert
2020-06-23 16:29:40 +02:00
159 changed files with 3667 additions and 1284 deletions

18
.npmrc
View File

@@ -1,13 +1,13 @@
registry=https://pkgs.dev.azure.com/hugendubel/_packaging/hugendubel/npm/registry/
registry=https://pkgs.dev.azure.com/hugendubel/_packaging/hugendubel%40Local/npm/registry/
; Treat this auth token like a password. Do not share it with anyone, including Microsoft support. This token expires on or before 24.05.2020.
always-auth=true
; Treat this auth token like a password. Do not share it with anyone, including Microsoft support. This token expires on or before 25.08.2020.
; begin auth token
//pkgs.dev.azure.com/hugendubel/_packaging/hugendubel/npm/registry/:username=hugendubel
//pkgs.dev.azure.com/hugendubel/_packaging/hugendubel/npm/registry/:_password=dG5yYTJ2bnBtbnZyeDZmeHJqc2N4bGV4aDRxd2xvZHplbzducGo3YzNyeDRkeGppZmRtYQ==
//pkgs.dev.azure.com/hugendubel/_packaging/hugendubel/npm/registry/:email=npm requires email to be set but doesn't use the value
//pkgs.dev.azure.com/hugendubel/_packaging/hugendubel/npm/:username=hugendubel
//pkgs.dev.azure.com/hugendubel/_packaging/hugendubel/npm/:_password=dG5yYTJ2bnBtbnZyeDZmeHJqc2N4bGV4aDRxd2xvZHplbzducGo3YzNyeDRkeGppZmRtYQ==
//pkgs.dev.azure.com/hugendubel/_packaging/hugendubel/npm/:email=npm requires email to be set but doesn't use the value
//pkgs.dev.azure.com/hugendubel/_packaging/hugendubel%40Local/npm/registry/:username=hugendubel
//pkgs.dev.azure.com/hugendubel/_packaging/hugendubel%40Local/npm/registry/:_password=M2JkaDdwNjIzbWVoZGlmeDU3N2Ficjc3M252NXBkaWg1M2VtaW94dXp5amwyejNkaW5yYQ==
//pkgs.dev.azure.com/hugendubel/_packaging/hugendubel%40Local/npm/registry/:email=npm requires email to be set but doesn't use the value
//pkgs.dev.azure.com/hugendubel/_packaging/hugendubel%40Local/npm/:username=hugendubel
//pkgs.dev.azure.com/hugendubel/_packaging/hugendubel%40Local/npm/:_password=M2JkaDdwNjIzbWVoZGlmeDU3N2Ficjc3M252NXBkaWg1M2VtaW94dXp5amwyejNkaW5yYQ==
//pkgs.dev.azure.com/hugendubel/_packaging/hugendubel%40Local/npm/:email=npm requires email to be set but doesn't use the value
; end auth token

View File

@@ -27,10 +27,7 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"libs/ui/tsconfig.lib.json",
"libs/ui/tsconfig.spec.json"
],
"tsConfig": ["libs/ui/tsconfig.lib.json", "libs/ui/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
@@ -145,20 +142,13 @@
"karmaConfig": "apps/sales/karma.conf.js",
"styles": ["apps/sales/src/styles.scss"],
"scripts": [],
"assets": [
"apps/sales/src/favicon.ico",
"apps/sales/src/assets",
"apps/sales/src/manifest.webmanifest"
]
"assets": ["apps/sales/src/favicon.ico", "apps/sales/src/assets", "apps/sales/src/manifest.webmanifest"]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/sales/tsconfig.app.json",
"apps/sales/tsconfig.spec.json"
],
"tsConfig": ["apps/sales/tsconfig.app.json", "apps/sales/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
@@ -217,10 +207,7 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"libs/sso/tsconfig.lib.json",
"libs/sso/tsconfig.spec.json"
],
"tsConfig": ["libs/sso/tsconfig.lib.json", "libs/sso/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
@@ -250,10 +237,7 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/swagger/availability/tsconfig.lib.json",
"apps/swagger/availability/tsconfig.spec.json"
],
"tsConfig": ["apps/swagger/availability/tsconfig.lib.json", "apps/swagger/availability/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
@@ -283,10 +267,7 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/swagger/checkout/tsconfig.lib.json",
"apps/swagger/checkout/tsconfig.spec.json"
],
"tsConfig": ["apps/swagger/checkout/tsconfig.lib.json", "apps/swagger/checkout/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
@@ -316,10 +297,7 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/swagger/crm/tsconfig.lib.json",
"apps/swagger/crm/tsconfig.spec.json"
],
"tsConfig": ["apps/swagger/crm/tsconfig.lib.json", "apps/swagger/crm/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
@@ -349,10 +327,7 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/swagger/isa/tsconfig.lib.json",
"apps/swagger/isa/tsconfig.spec.json"
],
"tsConfig": ["apps/swagger/isa/tsconfig.lib.json", "apps/swagger/isa/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
@@ -382,10 +357,7 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/swagger/oms/tsconfig.lib.json",
"apps/swagger/oms/tsconfig.spec.json"
],
"tsConfig": ["apps/swagger/oms/tsconfig.lib.json", "apps/swagger/oms/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
@@ -415,10 +387,7 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/swagger/print/tsconfig.lib.json",
"apps/swagger/print/tsconfig.spec.json"
],
"tsConfig": ["apps/swagger/print/tsconfig.lib.json", "apps/swagger/print/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
@@ -448,10 +417,7 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/swagger/cat/tsconfig.lib.json",
"apps/swagger/cat/tsconfig.spec.json"
],
"tsConfig": ["apps/swagger/cat/tsconfig.lib.json", "apps/swagger/cat/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
@@ -481,10 +447,37 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/swagger/eis/tsconfig.lib.json",
"apps/swagger/eis/tsconfig.spec.json"
],
"tsConfig": ["apps/swagger/eis/tsconfig.lib.json", "apps/swagger/eis/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
}
},
"native-container": {
"projectType": "library",
"root": "apps/native-container",
"sourceRoot": "apps/native-container/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "apps/native-container/tsconfig.lib.json",
"project": "apps/native-container/ng-package.json"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/native-container/src/test.ts",
"tsConfig": "apps/native-container/tsconfig.spec.json",
"karmaConfig": "apps/native-container/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["apps/native-container/tsconfig.lib.json", "apps/native-container/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}

View File

@@ -0,0 +1,24 @@
# NativeContainer
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.14.
## Code scaffolding
Run `ng generate component component-name --project native-container` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project native-container`.
> Note: Don't forget to add `--project native-container` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build native-container` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build native-container`, go to the dist folder `cd dist/native-container` and run `npm publish`.
## Running unit tests
Run `ng test native-container` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View File

@@ -0,0 +1,32 @@
// 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/native-container'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/native-container",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "native-container",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^8.2.14",
"@angular/core": "^8.2.14"
}
}

View File

@@ -0,0 +1,3 @@
export * from './native-container.service';
export * from './scan-request.type';
export * from './window-ref.service';

View File

@@ -0,0 +1,89 @@
import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject, fromEvent, Subject } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { WindowRef } from './window-ref.service';
import { ScanRequestType } from './scan-request.type';
@Injectable({
providedIn: 'root',
})
export class NativeContainerService {
private wm: Observable<any>;
public windowMessages = new Subject<any>();
private webViewDetected = false;
private webViewEventRecieved = false;
private browserDetected = false;
constructor(private windowRef: WindowRef) {
this.defineWindowCallback();
this.wm = fromEvent(this.windowRef.nativeWindow, 'message').pipe(
map((e: MessageEvent) => {
return e.data;
})
);
this.wm.subscribe((data) => {
if (data.status === 'INIT') {
this.webViewEventRecieved = true;
}
this.windowMessages.next(data);
});
}
public openScanner(scanRequestType: ScanRequestType) {
const scanRequest = {
[scanRequestType]: true,
};
this.sendMessage(scanRequest);
return this.windowMessages.asObservable();
}
public sendMessage(message: any) {
this.windowRef.nativeWindow.postMessage({ status: 'IN_PROGRESS', data: 'Scan Started' }, '*');
if (this.isUiWebview()) {
(this.windowRef.nativeWindow as any).webkit.messageHandlers.scanRequest.postMessage(message);
} else {
this.windowRef.nativeWindow.postMessage({ status: 'ERROR', data: 'Not a WebView' }, '*');
this.windowRef.nativeWindow.postMessage(message, '*');
}
}
public isUiWebview() {
const navigator = this.windowRef.nativeWindow.navigator as Navigator;
const standalone = (navigator as any).standalone,
userAgent = navigator.userAgent.toLowerCase(),
safari = /safari/.test(userAgent),
ios = /iphone|ipod|ipad/.test(userAgent),
chrome = /chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor),
crios = /crios/.test(userAgent);
this.webViewDetected = ios && !standalone && !safari;
this.browserDetected = !standalone && (safari || chrome) && !crios;
return {
isSafari: this.browserDetected,
isNative: this.webViewDetected || this.webViewEventRecieved,
};
}
private defineWindowCallback() {
if (this.windowRef.nativeWindow['scanResults'] === undefined) {
this.windowRef.nativeWindow['scanResults'] = (result) => window.postMessage(result, '*');
}
if (this.windowRef.nativeWindow['isRunningNative'] === undefined) {
this.windowRef.nativeWindow['isRunningNative'] = (_) => window.postMessage({ status: 'INIT', data: 'Is a WebView' }, '*');
}
// Try sending ping request, to invoke the containers isRunningNative event
try {
(this.windowRef.nativeWindow as any).webkit.messageHandlers.scanRequest.postMessage('PING');
} catch (error) {
this.windowRef.nativeWindow.postMessage({ status: 'ERROR', data: 'Not a WebView' }, '*');
this.windowRef.nativeWindow.postMessage('PING', '*');
}
}
}

View File

@@ -0,0 +1,8 @@
export type ScanRequestType =
| 'scanLogin'
| 'scanBook'
| 'scanShelf'
| 'scanCustomer'
| 'remissionContainer'
| 'remissionProduct'
| 'shippingDocument';

View File

@@ -0,0 +1,10 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class WindowRef {
get nativeWindow(): Window {
return window;
}
}

View File

@@ -0,0 +1,5 @@
/*
* Public API Surface of native-container
*/
export * from './lib';

View File

@@ -0,0 +1,21 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@@ -0,0 +1,26 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@@ -0,0 +1,17 @@
{
"extends": "../../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"lib",
"camelCase"
],
"component-selector": [
true,
"element",
"lib",
"kebab-case"
]
}
}

View File

@@ -3,15 +3,15 @@
<app-header [ngClass]="{ loading: loading }" *ngIf="authenticated"></app-header>
<app-content [ngClass]="{ loading: loading }" *ngIf="authenticated"></app-content>
<app-menu [ngClass]="{ loading: loading }" *ngIf="authenticated"></app-menu>
<img *ngIf="loading" src="/assets/images/Icon_Loading.svg" class="app-loader"
/>
<img *ngIf="loading" src="/assets/images/Icon_Loading.svg" class="app-loader" />
</lib-offline-overlay>
<router-outlet></router-outlet>
<script *ngIf="includeGoogleAnalytics">
(function(i, s, o, g, r, a, m) {
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
(i[r] =
i[r] ||
function() {
function () {
(i[r].q = i[r].q || []).push(arguments);
}),
(i[r].l = 1 * new Date());
@@ -22,4 +22,4 @@
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
ga('create', 'UA-76423009-4', 'auto');
</script>
</script>

View File

@@ -7,9 +7,8 @@ import {
import { Observable } from 'rxjs';
import { RemissionOverlayService } from '../../modules/remission/services/remission-overlay.service';
import { RemissionSelectors } from '../../core/store/selectors/remission.selectors';
import { RemissionSelectedFilters } from '@isa/remission';
import { Select } from '@ngxs/store';
import { map } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
@Component({
selector: 'app-content-header',
@@ -18,7 +17,7 @@ import { map } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ContentHeaderComponent implements OnInit {
@Select(RemissionSelectors.getRemissionSelectedFilters)
@Select(RemissionSelectors.getRemissionActiveFilters)
remissionFilters$: Observable<string[]>;
@Input() showFilter$: Observable<boolean>;
@@ -26,11 +25,11 @@ export class ContentHeaderComponent implements OnInit {
isFilterActive$: Observable<boolean>;
constructor(private remissionOverlayService: RemissionOverlayService) {}
constructor(private remissionOverlayService: RemissionOverlayService) { }
ngOnInit() {
this.isFilterActive$ = this.remissionFilters$.pipe(
map((selectedFilters) => selectedFilters && !!selectedFilters.length)
map((selectedFilters) => selectedFilters && !!Object.entries(selectedFilters).length),
);
}

View File

@@ -5,7 +5,7 @@ import { SsoService } from 'sso';
@Component({
selector: 'app-log-in',
templateUrl: './log-in.component.html',
styleUrls: ['./log-in.component.scss']
styleUrls: ['./log-in.component.scss'],
})
export class LogInComponent implements OnInit {
constructor(private route: ActivatedRoute, private ssoService: SsoService) {}
@@ -13,7 +13,7 @@ export class LogInComponent implements OnInit {
ngOnInit() {
const token = this.route.snapshot.paramMap.get('token');
if (token === null) {
this.ssoService.logoff();
// this.ssoService.logoff();
}
this.logIn(token);

View File

@@ -6,7 +6,7 @@ import { NativeContainerService } from 'shared/lib/barcode-scanner';
@Component({
selector: 'app-log-out',
templateUrl: './log-out.component.html',
styleUrls: ['./log-out.component.scss']
styleUrls: ['./log-out.component.scss'],
})
export class LogOutComponent implements OnInit {
id = 'logout-modal';
@@ -16,7 +16,6 @@ export class LogOutComponent implements OnInit {
logoff() {
this.ssoService.logoff();
this.containerNotificationMessage();
}
openDialog() {
@@ -26,12 +25,4 @@ export class LogOutComponent implements OnInit {
closeModal() {
this.modalService.close(this.id);
}
private containerNotificationMessage() {
// Notify the container app if the user has logged out
// For the purpposes of removing the container Header element
if (this.nativeContainer.isUiWebview()) {
this.nativeContainer.sendMessage({ scanLogin: true });
}
}
}

View File

@@ -14,12 +14,14 @@ export class ProductMapping {
let price = 0;
let priceDto: PriceDTO;
let ssctext = '';
let ssc = '';
let sscText = '';
let storeStatusCode = 0;
if (!!item.catalogAvailability) {
priceDto = item.catalogAvailability.price;
ssctext = item.catalogAvailability.sscText;
ssc = item.catalogAvailability.ssc;
sscText = item.catalogAvailability.sscText;
storeStatusCode = item.catalogAvailability.status;
}
@@ -63,7 +65,8 @@ export class ProductMapping {
imageId: item.imageId,
edition: item.product.edition,
volume: item.product.volume,
sscText: ssctext && ssctext.length > 28 ? ssctext.substr(0, 28) + '...' : ssctext,
ssc,
sscText,
storeStatusCode: storeStatusCode,
};
}

View File

@@ -22,6 +22,7 @@ export interface Product {
location: string;
ean: string;
imageId: string;
ssc: string,
sscText: string;
storeStatusCode: number;
}

View File

@@ -1,11 +1,13 @@
import {
RemissionProcess,
RemissionProduct,
FilterOption,
ShippingDocument
ShippingDocument,
} from '@isa/remission';
import { RemissionResourceType } from '../../modules/remission/models/remission-resource-type.model';
import { RemissionFinishingProcessStatus } from '../../modules/remission/models/remission-finishing-process-status.enum';
import {
RemissionActiveView,
RemissionFinishingProcessStatus,
RemissionResourceType,
} from '../../modules/remission/models';
export interface Remission {
remissionProcessCreated?: boolean;
@@ -21,4 +23,5 @@ export interface Remission {
blockReminder?: boolean;
remissionFinishingProcessStatus?: RemissionFinishingProcessStatus;
containerId?: string;
activeView?: RemissionActiveView;
}

View File

@@ -1,6 +1,6 @@
import { PositionStrategy, OverlayRef } from '@angular/cdk/overlay';
import { ElementRef } from '@angular/core';
import { fromEvent, Subscription, merge } from 'rxjs';
import { isNumber } from 'util';
export class ContainerPositionStrategy implements PositionStrategy {
windowScroll$ = fromEvent(window, 'scroll');
@@ -17,7 +17,7 @@ export class ContainerPositionStrategy implements PositionStrategy {
return this.overlayRef.overlayElement;
}
constructor(public containerElement: HTMLElement) {}
constructor(public containerElement: HTMLElement, private position?: { top?: number; bottom?: number; left?: number; right?: number }) {}
attach(overlayRef: OverlayRef): void {
this.overlayRef = overlayRef;
@@ -40,15 +40,24 @@ export class ContainerPositionStrategy implements PositionStrategy {
return;
}
this.overlayElement.style.position = 'relative';
this.overlayElement.style.overflow = 'auto';
const domRect = this.containerElement.getClientRects().item(0);
this.overlayElement.style.top = `${domRect.top}px`;
this.overlayElement.style.left = `${domRect.left}px`;
this.overlayElement.style.width = `${domRect.width}px`;
this.overlayElement.style.height = `${domRect.height}px`;
this.overlayElement.style.zIndex = '999';
const position = this.position || {};
this.overlayElement.style.top = `${isNumber(position.top) ? position.top : domRect.top}px`;
this.overlayElement.style.left = `${isNumber(position.left) ? position.left : domRect.left}px`;
if (isNumber(position.right)) {
this.overlayElement.style.right = `${position.right}px`;
} else {
this.overlayElement.style.width = `${domRect.width}px`;
}
if (isNumber(position.bottom)) {
this.overlayElement.style.bottom = `${position.bottom}px`;
} else {
this.overlayElement.style.height = `${domRect.height}px`;
}
}
}

View File

@@ -8,6 +8,7 @@ import {
} from '@isa/remission';
import { RemissionResourceType } from '../../../modules/remission/models/remission-resource-type.model';
import { RemissionFinishingProcessStatus } from '../../../modules/remission/models/remission-finishing-process-status.enum';
import { RemissionActiveView } from '../../../modules/remission/models';
export const SET_REMISSION_CREATED = '[REMISSION] Set created';
export const SET_REMISSION_STARTED = '[REMISSION] Set started';
@@ -36,6 +37,8 @@ export const SET_ALL_OPEN_REMISSIONS = '[REMISSION] Set all open remissions';
export const SET_ALL_REMISSIONS = '[REMISSION] Set all remissions';
export const SET_REMISSION_FILTER = '[REMISSION] Set remission filter';
export const SET_REMISSION_ISLOADING = '[REMISSION] Set remission isLoading';
export const SET_REMISSION_ACTIVE_VIEW =
'[REMISSION] Set remission active view';
export const REQUEST_UPDATE_SHIPPING_DOCUMENT =
'[REMISSION] Update shipping document';
@@ -204,3 +207,9 @@ export class SetRemissionIsLoading {
constructor(public isLoading: boolean) {}
}
export class SetRemissionActiveView {
static readonly type = SET_REMISSION_ACTIVE_VIEW;
constructor(public activeView: RemissionActiveView) {}
}

View File

@@ -216,4 +216,22 @@ export class RemissionSelectors {
return remission.target === 'Blank';
}
@Selector([RemissionState])
static getActiveView(remissionState: RemissionStateModel) {
const remission = remissionState.remission;
return remission.activeView;
}
@Selector([RemissionState])
static getRemissionSupplierId(remissionState: RemissionStateModel) {
const remission = remissionState.remission;
const supplier =
remission.remissionProcess &&
remission.remissionProcess.filter &&
remission.remissionProcess.filter.target;
return supplier && supplier.id;
}
}

View File

@@ -5,12 +5,15 @@ import {
AppDeleteProcess,
AppUserDataSync,
AppSwitchModule,
ReloadSavedState
ReloadSavedState,
} from '../actions/app.actions';
import { LoadBranches, LoadUserBranch } from '../actions/branch.actions';
import { BranchSelectors } from '../selectors/branch.selector';
import { isNullOrUndefined } from 'util';
import { RemoveProcessNewState, ReloadProcessData } from '../actions/process.actions';
import {
RemoveProcessNewState,
ReloadProcessData,
} from '../actions/process.actions';
import { UserStateService } from '../../services/user-state.service';
import { UserStateSyncData } from '../../models/user-state-sync.model';
import { ReloadCustomersData } from '../actions/customer.actions';
@@ -31,7 +34,7 @@ import { ReloadRemission } from '../actions/remission.actions';
import { ReloadFormState } from '../actions/forms.actions';
import { FILIALE_LANDING_PAGE } from '../../utils/app.constants';
export const SYNC_DATA_VERSION = 204;
export const SYNC_DATA_VERSION = 205;
export class AppStateModel {
currentProcesssId: number;
@@ -48,8 +51,8 @@ export class AppStateModel {
processIds: [],
activeModule: ModuleSwitcher.Customer,
synced: false,
syncedUserData: ''
}
syncedUserData: '',
},
})
export class AppState {
userStateService$ = new Subject<string>();
@@ -93,7 +96,7 @@ export class AppState {
.pipe(
debounceTime(500),
distinctUntilChanged((prev, next) => prev && next && prev === next),
switchMap(state => {
switchMap((state) => {
return this.userStateService.saveUserState(state);
})
)
@@ -101,12 +104,16 @@ export class AppState {
}
@Action(AppSetCurrentProcess)
appSetCurrentProcess(ctx: StateContext<AppStateModel>, { payload }: AppSetCurrentProcess) {
appSetCurrentProcess(
ctx: StateContext<AppStateModel>,
{ payload }: AppSetCurrentProcess
) {
const state = ctx.getState();
const processExists = state.processIds.findIndex(t => t === payload) !== -1;
const processExists =
state.processIds.findIndex((t) => t === payload) !== -1;
if (processExists) {
ctx.patchState({
currentProcesssId: payload
currentProcesssId: payload,
});
this.store.dispatch(new RemoveProcessNewState());
}
@@ -115,18 +122,23 @@ export class AppState {
@Action(AppAddProcess)
appAddProcess(ctx: StateContext<AppStateModel>, { payload }: AppAddProcess) {
const state = ctx.getState();
const processExists = state.processIds.findIndex(t => t === payload) !== -1;
const processExists =
state.processIds.findIndex((t) => t === payload) !== -1;
if (!processExists) {
const processIds = [...state.processIds, payload];
const currentProcesssId = payload;
ctx.patchState({ currentProcesssId, processIds });
const branchesLoaded = this.store.selectSnapshot(BranchSelectors.getBranches);
const branchesLoaded = this.store.selectSnapshot(
BranchSelectors.getBranches
);
if (!branchesLoaded || Object.keys(branchesLoaded).length === 0) {
this.store.dispatch(new LoadBranches());
}
const userBranch = this.store.selectSnapshot(BranchSelectors.getUserBranch);
const userBranch = this.store.selectSnapshot(
BranchSelectors.getUserBranch
);
if (isNullOrUndefined(userBranch)) {
this.store.dispatch(new LoadUserBranch());
}
@@ -136,18 +148,24 @@ export class AppState {
}
@Action(AppDeleteProcess)
appDeleteProcess(ctx: StateContext<AppStateModel>, { payload }: AppDeleteProcess) {
appDeleteProcess(
ctx: StateContext<AppStateModel>,
{ payload }: AppDeleteProcess
) {
const state = ctx.getState();
const currentIds = state.processIds;
if (currentIds) {
const processIds = currentIds.filter(t => t && t !== payload);
const processIds = currentIds.filter((t) => t && t !== payload);
ctx.patchState({ processIds });
this.syncApiState(processIds, state.activeModule);
}
}
@Action(AppSwitchModule)
appSwitchModule(ctx: StateContext<AppStateModel>, { payload }: AppSwitchModule) {
appSwitchModule(
ctx: StateContext<AppStateModel>,
{ payload }: AppSwitchModule
) {
const state = ctx.getState();
ctx.patchState({ activeModule: payload });
this.syncApiState(state.processIds, payload);
@@ -157,7 +175,10 @@ export class AppState {
* Save store data on backend
*/
@Action(AppUserDataSync)
appUserDataSynced(ctx: StateContext<AppStateModel>, { data, sync }: AppUserDataSync) {
appUserDataSynced(
ctx: StateContext<AppStateModel>,
{ data, sync }: AppUserDataSync
) {
const state = ctx.getState();
let currentUserData: UserStateSyncData = {};
@@ -175,7 +196,7 @@ export class AppState {
ctx.patchState({
synced: true,
syncedUserData: updatedUserDataString
syncedUserData: updatedUserDataString,
});
this.userStateService$.next(updatedUserDataString);
}
@@ -184,7 +205,10 @@ export class AppState {
* Initial store loading from API, triggered once on page load
*/
@Action(ReloadSavedState)
reloadSavedState(ctx: StateContext<AppStateModel>, { data, sync }: ReloadSavedState) {
reloadSavedState(
ctx: StateContext<AppStateModel>,
{ data, sync }: ReloadSavedState
) {
const state = ctx.getState();
let currentUserData: UserStateSyncData = {};
@@ -208,18 +232,25 @@ export class AppState {
if (sync && syncedData.version === SYNC_DATA_VERSION) {
this.reloadDataFromAPI(syncedData);
if (syncedData.currentProcesssId || syncedData.activeModule === ModuleSwitcher.Branch) {
if (
syncedData.currentProcesssId ||
syncedData.activeModule === ModuleSwitcher.Branch
) {
ctx.patchState({
...state,
synced: true,
syncedUserData: updatedUserDataString,
currentProcesssId: serverData.currentProcesssId,
processIds: serverData.processIds,
activeModule: serverData.activeModule
activeModule: serverData.activeModule,
});
}
} else {
ctx.patchState({ ...state, synced: true, syncedUserData: updatedUserDataString });
ctx.patchState({
...state,
synced: true,
syncedUserData: updatedUserDataString,
});
}
this.userStateService$.next(updatedUserDataString);
@@ -231,11 +262,19 @@ export class AppState {
return;
}
if (data.customers) {
this.store.dispatch(new ReloadCustomersData(data.customers, data.lastCreatedCustomerId, data.cachedCustomerSearch));
this.store.dispatch(
new ReloadCustomersData(
data.customers,
data.lastCreatedCustomerId,
data.cachedCustomerSearch
)
);
}
if (data.products) {
this.store.dispatch(new ReloadProductsData(data.products, data.cachedProductResults));
this.store.dispatch(
new ReloadProductsData(data.products, data.cachedProductResults)
);
}
if (data.carts) {
@@ -255,7 +294,9 @@ export class AppState {
}
if (data.processes) {
this.store.dispatch(new ReloadProcessData(data.processes, data.recentArticles));
this.store.dispatch(
new ReloadProcessData(data.processes, data.recentArticles)
);
}
if (data.formsState) {
@@ -263,11 +304,20 @@ export class AppState {
}
if (data.activeModule === ModuleSwitcher.Customer) {
if (data.processes && data.currentProcesssId && data.processes[data.currentProcesssId]) {
if (
data.processes &&
data.currentProcesssId &&
data.processes[data.currentProcesssId]
) {
const currentProcesssId = data.currentProcesssId;
const currentRoute = data.processes[currentProcesssId].currentRoute;
if (currentRoute && currentRoute.length > 0) {
this.routingAvailableAction(data, currentProcesssId, currentRoute, data.activeModule);
this.routingAvailableAction(
data,
currentProcesssId,
currentRoute,
data.activeModule
);
} else {
this.router.navigate(['/dashboard']);
}
@@ -279,7 +329,12 @@ export class AppState {
const currentProcesssId = -1;
const currentRoute = data.branchProcess.currentRoute;
if (currentRoute && currentRoute.length > 0) {
this.routingAvailableAction(data, currentProcesssId, currentRoute, data.activeModule);
this.routingAvailableAction(
data,
currentProcesssId,
currentRoute,
data.activeModule
);
} else {
this.router.navigate([FILIALE_LANDING_PAGE]);
}
@@ -293,11 +348,24 @@ export class AppState {
}
if (data.activeModule === ModuleSwitcher.Branch) {
this.moduleSwitcherService.switch(ModuleSwitcher.Branch, data.branchProcess);
this.moduleSwitcherService.switch(
ModuleSwitcher.Branch,
data.branchProcess
);
}
if (data.processesBreadcrumbs && data.activeCrumbs && (data.currentProcesssId || data.activeModule === ModuleSwitcher.Branch)) {
this.store.dispatch(new ReloadBreadcrumbsData(data.processesBreadcrumbs, data.activeCrumbs, data.previusMenuPath));
if (
data.processesBreadcrumbs &&
data.activeCrumbs &&
(data.currentProcesssId || data.activeModule === ModuleSwitcher.Branch)
) {
this.store.dispatch(
new ReloadBreadcrumbsData(
data.processesBreadcrumbs,
data.activeCrumbs,
data.previusMenuPath
)
);
}
if (data.filters && data.processesSelectedFilters && data.dropdownFilters) {
@@ -323,16 +391,28 @@ export class AppState {
);
}
private routingAvailableAction(data: UserStateSyncData, currentProcesssId: number, currentRoute: string, module: ModuleSwitcher) {
const hasActiveCrumbsAvailableForProcess = data.activeCrumbs && data.activeCrumbs[currentProcesssId];
private routingAvailableAction(
data: UserStateSyncData,
currentProcesssId: number,
currentRoute: string,
module: ModuleSwitcher
) {
const hasActiveCrumbsAvailableForProcess =
data.activeCrumbs && data.activeCrumbs[currentProcesssId];
const activeCrumbs = data.activeCrumbs[currentProcesssId];
const hasProcessBreadcrumbsAvailableForProcess = data.processesBreadcrumbs && data.processesBreadcrumbs[activeCrumbs];
const hasProcessBreadcrumbsAvailableForProcess =
data.processesBreadcrumbs && data.processesBreadcrumbs[activeCrumbs];
const breadcrumb = hasProcessBreadcrumbsAvailableForProcess
? data.processesBreadcrumbs[activeCrumbs].find(t => t.processId === currentProcesssId)
? data.processesBreadcrumbs[activeCrumbs].find(
(t) => t.processId === currentProcesssId
)
: null;
const breadcrumbPath = breadcrumb && breadcrumb.breadcrumbs ? breadcrumb.breadcrumbs.find(t => t && t.path === currentRoute) : null;
const breadcrumbPath =
breadcrumb && breadcrumb.breadcrumbs
? breadcrumb.breadcrumbs.find((t) => t && t.path === currentRoute)
: null;
if (
hasActiveCrumbsAvailableForProcess &&
@@ -341,7 +421,9 @@ export class AppState {
breadcrumb.breadcrumbs &&
breadcrumbPath
) {
this.router.navigate([breadcrumbPath.path], { queryParams: breadcrumbPath.queryParams });
this.router.navigate([breadcrumbPath.path], {
queryParams: breadcrumbPath.queryParams,
});
} else {
this.navigateToCourrentRoute(
currentRoute,
@@ -364,7 +446,7 @@ export class AppState {
const userSyncData: UserStateSyncData = {
version: SYNC_DATA_VERSION,
processIds: processIds,
activeModule: activeModule
activeModule: activeModule,
};
const userData = JSON.stringify(userSyncData);
this.store.dispatch(new AppUserDataSync(userData));

View File

@@ -5,10 +5,14 @@ import * as actions from '../actions/remission.actions';
import { UserStateSyncData } from '../../models/user-state-sync.model';
import { AppUserDataSync } from '../actions/app.actions';
import { isNullOrUndefined } from 'util';
import { RemissionResourceType, RemissionTargetType } from '../../../modules/remission/models/remission-resource-type.model';
import {
RemissionResourceType,
RemissionTargetType,
} from '../../../modules/remission/models/remission-resource-type.model';
import { RemissionService, RemissionProcess } from '@isa/remission';
import { RemissionFinishingProcessStatus } from '../../../modules/remission/models/remission-finishing-process-status.enum';
import get from 'lodash/get';
import { RemissionActiveView } from '../../../modules/remission/models';
export class RemissionStateModel {
remission: Remission;
@@ -25,22 +29,29 @@ export class RemissionStateModel {
},
})
export class RemissionState {
constructor(private store: Store, private remissionService: RemissionService) {}
constructor(private store: Store) {}
@Action(actions.SetRemissionCreated)
setRemissionCreated(ctx: StateContext<RemissionStateModel>, { status }: actions.SetRemissionCreated) {
setRemissionCreated(
ctx: StateContext<RemissionStateModel>,
{ status }: actions.SetRemissionCreated
) {
const state = ctx.getState();
const currentRemission = state.remission;
const remission: Remission = {
...currentRemission,
remissionProcessCreated: status,
activeView: RemissionActiveView.PRODUCTS,
};
ctx.patchState({ remission });
this.syncApiState(remission);
}
@Action(actions.SetRemissionStarted)
setRemissionStarted(ctx: StateContext<RemissionStateModel>, { status }: actions.SetRemissionStarted) {
setRemissionStarted(
ctx: StateContext<RemissionStateModel>,
{ status }: actions.SetRemissionStarted
) {
const state = ctx.getState();
const currentRemission = state.remission;
const remission: Remission = {
@@ -52,7 +63,10 @@ export class RemissionState {
}
@Action(actions.SetRemissionCompleted)
setRemissionCompleted(ctx: StateContext<RemissionStateModel>, { status }: actions.SetRemissionCompleted) {
setRemissionCompleted(
ctx: StateContext<RemissionStateModel>,
{ status }: actions.SetRemissionCompleted
) {
const state = ctx.getState();
const currentRemission = state.remission;
const remission: Remission = {
@@ -64,14 +78,19 @@ export class RemissionState {
}
@Action(actions.SetRemissionProcess)
setRemissionProcess(ctx: StateContext<RemissionStateModel>, { remissionProcess, initialValue }: actions.SetRemissionProcess) {
setRemissionProcess(
ctx: StateContext<RemissionStateModel>,
{ remissionProcess, initialValue }: actions.SetRemissionProcess
) {
const state = ctx.getState();
const currentRemission = state.remission;
const filter = get(remissionProcess, 'filter.filter', null);
const remissionWithEmptyFilters =
filter === null ||
Object.keys(filter).every((sourceKey) =>
Object.keys(filter[sourceKey]).every((targetKey) => Object.keys(filter[sourceKey][targetKey]).length === 0)
Object.keys(filter[sourceKey]).every(
(targetKey) => Object.keys(filter[sourceKey][targetKey]).length === 0
)
);
const remission: Remission = {
...currentRemission,
@@ -92,7 +111,6 @@ export class RemissionState {
const remissionState: RemissionStateModel = {
...state,
remission,
isLoading: false,
};
ctx.patchState(remissionState);
@@ -100,7 +118,10 @@ export class RemissionState {
}
@Action(actions.SetRemissionFinishedProcessStatus)
setRemissionFinishedProcessStatus(ctx: StateContext<RemissionStateModel>, { status }: actions.SetRemissionFinishedProcessStatus) {
setRemissionFinishedProcessStatus(
ctx: StateContext<RemissionStateModel>,
{ status }: actions.SetRemissionFinishedProcessStatus
) {
const state = ctx.getState();
const currentRemission = state.remission;
const remission: Remission = {
@@ -112,7 +133,10 @@ export class RemissionState {
}
@Action(actions.SetRemissionScannedContainerId)
setRemissionScannedContainerId(ctx: StateContext<RemissionStateModel>, { containerId }: actions.SetRemissionScannedContainerId) {
setRemissionScannedContainerId(
ctx: StateContext<RemissionStateModel>,
{ containerId }: actions.SetRemissionScannedContainerId
) {
const state = ctx.getState();
const currentRemission = state.remission;
const remission: Remission = {
@@ -124,7 +148,10 @@ export class RemissionState {
}
@Action(actions.SetRemissionTarget)
setRemissionTarget(ctx: StateContext<RemissionStateModel>, { target, source }: actions.SetRemissionTarget) {
setRemissionTarget(
ctx: StateContext<RemissionStateModel>,
{ target, source }: actions.SetRemissionTarget
) {
const state = ctx.getState();
const currentRemission = state.remission;
let remission: Remission;
@@ -137,7 +164,10 @@ export class RemissionState {
}
@Action(actions.SetRemissionSource)
setRemissionSource(ctx: StateContext<RemissionStateModel>, { source }: actions.SetRemissionSource) {
setRemissionSource(
ctx: StateContext<RemissionStateModel>,
{ source }: actions.SetRemissionSource
) {
const state = ctx.getState();
const currentRemission = state.remission;
const remission: Remission = {
@@ -156,7 +186,10 @@ export class RemissionState {
}
@Action(actions.SetRemissionProducts)
setRemissionProducts(ctx: StateContext<RemissionStateModel>, { products }: actions.SetRemissionProducts) {
setRemissionProducts(
ctx: StateContext<RemissionStateModel>,
{ products }: actions.SetRemissionProducts
) {
const state = ctx.getState();
const currentRemission = state.remission;
const remission: Remission = {
@@ -168,7 +201,10 @@ export class RemissionState {
}
@Action(actions.SetRemissionSearchedProduct)
setRemissionSearchedProduct(ctx: StateContext<RemissionStateModel>, { product }: actions.SetRemissionSearchedProduct) {
setRemissionSearchedProduct(
ctx: StateContext<RemissionStateModel>,
{ product }: actions.SetRemissionSearchedProduct
) {
const state = ctx.getState();
const currentRemission = state.remission;
const remission: Remission = {
@@ -206,7 +242,10 @@ export class RemissionState {
}
@Action(actions.CompleteRemissionShippingDocument)
completeRemissionShippingDocument(ctx: StateContext<RemissionStateModel>, { shippingDocument }: actions.SetRemissionShippingDocument) {
completeRemissionShippingDocument(
ctx: StateContext<RemissionStateModel>,
{ shippingDocument }: actions.SetRemissionShippingDocument
) {
const state = ctx.getState();
const currentRemission = state.remission;
const currentRemissionProcess = currentRemission.remissionProcess;
@@ -216,7 +255,9 @@ export class RemissionState {
if (currentRemissionProcess.shippingDocuments) {
const shippingDocumentAlreadyExists =
currentRemissionProcess.shippingDocuments.findIndex((sd) => sd.id === shippingDocument.id) !== -1;
currentRemissionProcess.shippingDocuments.findIndex(
(sd) => sd.id === shippingDocument.id
) !== -1;
if (shippingDocumentAlreadyExists) {
updatedRemissionProcess = {
...updatedRemissionProcess,
@@ -236,7 +277,8 @@ export class RemissionState {
}
const remission: Remission = {
...currentRemission,
remissionFinishingProcessStatus: RemissionFinishingProcessStatus.containerScanned,
remissionFinishingProcessStatus:
RemissionFinishingProcessStatus.containerScanned,
remissionProcess: updatedRemissionProcess,
};
ctx.patchState({ remission });
@@ -244,7 +286,10 @@ export class RemissionState {
}
@Action(actions.DeleteRemissionShippingDocument)
deleteShippingDocument(ctx: StateContext<RemissionStateModel>, { remissionProcessId }: actions.DeleteRemissionShippingDocument) {
deleteShippingDocument(
ctx: StateContext<RemissionStateModel>,
{ remissionProcessId }: actions.DeleteRemissionShippingDocument
) {
const state = ctx.getState();
const currentRemission = state.remission;
@@ -260,7 +305,10 @@ export class RemissionState {
}
@Action(actions.SetRemissionShippingDocument)
setRemissionShippingDocument(ctx: StateContext<RemissionStateModel>, { shippingDocument }: actions.SetRemissionShippingDocument) {
setRemissionShippingDocument(
ctx: StateContext<RemissionStateModel>,
{ shippingDocument }: actions.SetRemissionShippingDocument
) {
const state = ctx.getState();
const currentRemission = state.remission;
const currentRemissionProcess = currentRemission.remissionProcess;
@@ -268,7 +316,9 @@ export class RemissionState {
if (currentRemissionProcess.shippingDocuments) {
const shippingDocumentAlreadyExists =
currentRemissionProcess.shippingDocuments.findIndex((sd) => sd.id === shippingDocument.id) !== -1;
currentRemissionProcess.shippingDocuments.findIndex(
(sd) => sd.id === shippingDocument.id
) !== -1;
if (shippingDocumentAlreadyExists) {
updatedRemissionProcess = {
...currentRemission.remissionProcess,
@@ -284,13 +334,19 @@ export class RemissionState {
} else {
updatedRemissionProcess = {
...currentRemission.remissionProcess,
shippingDocuments: [...currentRemission.remissionProcess.shippingDocuments, shippingDocument],
shippingDocuments: [
...currentRemission.remissionProcess.shippingDocuments,
shippingDocument,
],
};
}
} else {
updatedRemissionProcess = {
...currentRemission.remissionProcess,
shippingDocuments: [...currentRemission.remissionProcess.shippingDocuments, shippingDocument],
shippingDocuments: [
...currentRemission.remissionProcess.shippingDocuments,
shippingDocument,
],
};
}
const remission: Remission = {
@@ -363,7 +419,10 @@ export class RemissionState {
}
@Action(actions.ReloadRemission)
reload(ctx: StateContext<RemissionStateModel>, { remission }: actions.ReloadRemission) {
reload(
ctx: StateContext<RemissionStateModel>,
{ remission }: actions.ReloadRemission
) {
ctx.patchState({
remission: {
...remission,
@@ -375,7 +434,10 @@ export class RemissionState {
}
@Action(actions.SetAllOpenExistingRemissions)
setAllOpenExistingRemissions(ctx: StateContext<RemissionStateModel>, { remissionProcesses }: actions.SetAllOpenExistingRemissions) {
setAllOpenExistingRemissions(
ctx: StateContext<RemissionStateModel>,
{ remissionProcesses }: actions.SetAllOpenExistingRemissions
) {
const state = ctx.getState();
const currentExistingRemissions = state.existingRemissions;
const existingRemissions: RemissionExistingOverview = {
@@ -387,7 +449,10 @@ export class RemissionState {
}
@Action(actions.SetAllExistingRemissions)
setAllExistingRemissions(ctx: StateContext<RemissionStateModel>, { remissionProcesses }: actions.SetAllExistingRemissions) {
setAllExistingRemissions(
ctx: StateContext<RemissionStateModel>,
{ remissionProcesses }: actions.SetAllExistingRemissions
) {
const state = ctx.getState();
const currentExistingRemissions = state.existingRemissions;
const existingRemissions: RemissionExistingOverview = {
@@ -407,7 +472,10 @@ export class RemissionState {
}
@Action(actions.SetRemissionFilter)
setRemissionFilter(ctx: StateContext<RemissionStateModel>, { filter }: actions.SetRemissionFilter) {
setRemissionFilter(
ctx: StateContext<RemissionStateModel>,
{ filter }: actions.SetRemissionFilter
) {
const state = ctx.getState();
const remissionState = state.remission;
@@ -423,13 +491,15 @@ export class RemissionState {
ctx.patchState({
remission,
isLoading: false,
});
this.syncApiState(remission);
}
@Action(actions.RequestUpdateRemissionFilter)
setIsLoadingFilters(ctx: StateContext<RemissionStateModel>, { filter }: actions.RequestUpdateRemissionFilter) {
setIsLoadingFilters(
ctx: StateContext<RemissionStateModel>,
{ filter }: actions.RequestUpdateRemissionFilter
) {
const state = ctx.getState();
const remissionState = state.remission;
@@ -441,8 +511,10 @@ export class RemissionState {
...remissionState.remissionProcess,
filter: {
...remissionState.remissionProcess.filter,
source: filter.source || remissionState.remissionProcess.filter.source,
target: filter.target || remissionState.remissionProcess.filter.target,
source:
filter.source || remissionState.remissionProcess.filter.source,
target:
filter.target || remissionState.remissionProcess.filter.target,
},
},
};
@@ -452,11 +524,13 @@ export class RemissionState {
remission,
isLoading: true,
});
this.syncApiState(remission);
}
@Action(actions.SetRemissionIsLoading)
setIsLoading(ctx: StateContext<RemissionStateModel>, { isLoading }: actions.SetRemissionIsLoading) {
setIsLoading(
ctx: StateContext<RemissionStateModel>,
{ isLoading }: actions.SetRemissionIsLoading
) {
const state = ctx.getState();
ctx.patchState({
@@ -464,4 +538,21 @@ export class RemissionState {
isLoading,
});
}
@Action(actions.SetRemissionActiveView)
setActiveView(
ctx: StateContext<RemissionStateModel>,
{ activeView }: actions.SetRemissionActiveView
) {
const state = ctx.getState();
const remissionState = state.remission;
ctx.patchState({
...state,
remission: {
...remissionState,
activeView,
},
});
}
}

View File

@@ -1,4 +1,11 @@
import { Component, EventEmitter, OnInit, ViewChild, OnDestroy, Output } from '@angular/core';
import {
Component,
EventEmitter,
OnInit,
ViewChild,
OnDestroy,
Output,
} from '@angular/core';
import { BarcodeFormat, Result } from '@zxing/library';
import { ZXingScannerComponent } from '@zxing/ngx-scanner';
import { Subject } from 'rxjs';
@@ -7,13 +14,18 @@ import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-barcode-scanner',
templateUrl: 'barcode-scanner.component.html',
styleUrls: ['barcode-scanner.component.scss']
styleUrls: ['barcode-scanner.component.scss'],
})
export class BarcodeScannerComponent implements OnInit, OnDestroy {
@Output() scan: EventEmitter<string> = new EventEmitter();
@ViewChild('scanner', { static: false }) scanner: ZXingScannerComponent;
@ViewChild('scanner', { static: true }) scanner: ZXingScannerComponent;
allowedFormats = [BarcodeFormat.QR_CODE, BarcodeFormat.EAN_13, BarcodeFormat.CODE_128, BarcodeFormat.UPC_A];
allowedFormats = [
BarcodeFormat.QR_CODE,
BarcodeFormat.EAN_13,
BarcodeFormat.CODE_128,
BarcodeFormat.UPC_A,
];
enabled = false;
debugMode = true;
code = '';
@@ -24,11 +36,17 @@ export class BarcodeScannerComponent implements OnInit, OnDestroy {
destroy$ = new Subject();
ngOnInit() {
this.scanner.camerasNotFound.pipe(takeUntil(this.destroy$)).subscribe(() => (this.hasDevices = false));
this.scanner.scanComplete.pipe(takeUntil(this.destroy$)).subscribe(result => this.scanSuccess(result));
this.scanner.permissionResponse.pipe(takeUntil(this.destroy$)).subscribe((perm: boolean) => {
this.hasPermission = perm;
});
this.scanner.camerasNotFound
.pipe(takeUntil(this.destroy$))
.subscribe(() => (this.hasDevices = false));
this.scanner.scanComplete
.pipe(takeUntil(this.destroy$))
.subscribe((result) => this.scanSuccess(result));
this.scanner.permissionResponse
.pipe(takeUntil(this.destroy$))
.subscribe((perm: boolean) => {
this.hasPermission = perm;
});
}
scanSuccess(result: Result) {

View File

@@ -74,7 +74,7 @@ export class CustomerCardBarcodeSearchComponent implements OnInit, OnDestroy, Af
.pipe(takeUntil(this.destroy$))
.subscribe(
(customers: CustomerInfoDTO[]) => {
if (customers.length > 0) {
if (Array.isArray(customers) && customers.length > 0) {
this.store.dispatch(
new AddBreadcrumb(
{

View File

@@ -1,11 +1,21 @@
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, OnDestroy, Output, EventEmitter, Input, ViewRef } from '@angular/core';
import {
Component,
OnInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
OnDestroy,
Output,
EventEmitter,
Input,
ViewRef,
} from '@angular/core';
import { Cart } from '../../../../core/models/cart.model';
import { DeliveryOption } from '../../../../core/models/delivery-option.model';
import {
UpdateCustomerFormState,
ChangeCurrentRoute,
SetDetailsCustomer,
RemoveSelectedItem
RemoveSelectedItem,
} from '../../../../core/store/actions/process.actions';
import { CustomerFormState } from '../../../../core/models/process.model';
import { Store, Select } from '@ngxs/store';
@@ -22,12 +32,11 @@ import { isArrayMinLength } from '../../../../core/utils/app.utils';
import { DeleteProductFromCart } from 'apps/sales/src/app/core/store/actions/cart.actions';
import { cartItem } from 'apps/sales/src/app/shared/animations/cart-item.animation';
@Component({
selector: 'app-delivery-cart',
templateUrl: './delivery-cart.component.html',
styleUrls: ['./delivery-cart.component.scss'],
animations: [cartItem]
animations: [cartItem],
})
export class DeliveryCartComponent implements OnInit, OnDestroy {
@Select(SharedSelectors.getCart) cartData$: Observable<ProcessCart>;
@@ -40,23 +49,26 @@ export class DeliveryCartComponent implements OnInit, OnDestroy {
lastDeletedItemIndex = -1;
destroy$ = new Subject();
constructor(private store: Store, private router: Router, private cdr: ChangeDetectorRef) { }
constructor(private store: Store, private router: Router, private cdr: ChangeDetectorRef) {}
ngOnInit() {
this.delivery$ = this.cartData$.pipe(
filter(c => isArrayMinLength(c, 'cart', 0)),
map(review => review.cart),
filter((c) => isArrayMinLength(c, 'cart', 0)),
map((review) => review.cart),
map(deliveryFilter(DeliveryOption.DELIVERY)),
map(cart => cartToCartReviewArray(cart))
map((cart) => cartToCartReviewArray(cart))
);
this.cartData$
.pipe(
filter(cart => !!cart),
filter((cart) => !!cart),
takeUntil(this.destroy$)
)
.subscribe(cart => {
this.validateDeliveryAddress(cart);
.subscribe((cart) => {
if (!this.validateDeliveryAddress(cart)) {
this.destroy$.next();
return;
}
this.parseDeliveryAddress(cart);
});
}
@@ -69,7 +81,10 @@ export class DeliveryCartComponent implements OnInit, OnDestroy {
if (!cart.customer) {
return null;
}
this.validateDeliveryAddress(cart);
if (!this.validateDeliveryAddress(cart)) {
this.destroy$.next();
return;
}
const customer = cart.customer;
if (customer.delivery_addres) {
const { first_name, last_name, street, streetNo, zip, city } = customer.delivery_addres;
@@ -90,7 +105,7 @@ export class DeliveryCartComponent implements OnInit, OnDestroy {
const newBread: Breadcrumb = {
name: 'Lieferadresse',
path: currentRoute,
queryParams: { cart: true, notEditable: true }
queryParams: { cart: true, notEditable: true },
};
this.store.dispatch(new SetDetailsCustomer(customerId));
@@ -106,7 +121,7 @@ export class DeliveryCartComponent implements OnInit, OnDestroy {
new AddBreadcrumb(
{
name: item.product.name ? item.product.name.substring(0, 12) + (item.product.name.length > 12 ? '...' : '') : '',
path: '/product/details/' + item.id
path: '/product/details/' + item.id,
},
'product'
)
@@ -126,8 +141,10 @@ export class DeliveryCartComponent implements OnInit, OnDestroy {
this.store.dispatch(new UpdateCustomerFormState(CustomerFormState.MISSING_DELIVERY));
const currentRoute = `customer/search/missing-data/${processCart.customer.id}`;
this.store.dispatch(new ChangeCurrentRoute(currentRoute));
this.router.navigate([currentRoute]);
this.router.navigate([currentRoute], { queryParams: { card: 'create' } });
return false;
}
return true;
}
isAddressSet(deliveryAddress: Address) {

View File

@@ -1,7 +1,13 @@
<app-modal id="address-sugestions" *ngIf="addresses && addresses.length > 0">
<div class="modal-wrapper">
<app-modal id="address-sugestions">
<div class="modal-wrapper" *ngIf="addresses && addresses.length > 0">
<div class="header">
<lib-icon (click)="closeDialog()" height="21px" class="close-icon" name="close" alt="close"></lib-icon>
<lib-icon
(click)="closeDialog()"
height="21px"
class="close-icon"
name="close"
alt="close"
></lib-icon>
</div>
<div class="title">
<span>Bitte überprüfen Sie die eingegebenen Adressdaten</span>
@@ -10,7 +16,10 @@
<span>Vorschläge:</span>
</div>
<div class="body">
<div class="item-row" *ngFor="let address of addresses; let first = first">
<div
class="item-row"
*ngFor="let address of addresses; let first = first"
>
<hr *ngIf="first" />
<div class="row-container" *ngIf="address">
<div class="data">
@@ -24,7 +33,9 @@
</div>
</div>
<div class="modal-action">
<app-button (click)="ignoreSuggestions()" [primary]="true">Eingegebene Adresse übernehmen</app-button>
<app-button (click)="ignoreSuggestions()" [primary]="true"
>Eingegebene Adresse übernehmen</app-button
>
</div>
</div>
</app-modal>

View File

@@ -1,5 +1,18 @@
import { Component, OnInit, OnDestroy, HostListener, Renderer, ChangeDetectorRef, ViewChild, AfterViewInit, ViewRef } from '@angular/core';
import { FormGroup, FormBuilder, Validators, AbstractControl } from '@angular/forms';
import {
Component,
OnInit,
OnDestroy,
HostListener,
ChangeDetectorRef,
ViewChild,
ViewRef,
} from '@angular/core';
import {
FormGroup,
FormBuilder,
Validators,
AbstractControl,
} from '@angular/forms';
import { Store, Select } from '@ngxs/store';
import {
ChangeCurrentRoute,
@@ -8,14 +21,35 @@ import {
SetOnlineCustomerCreationStatus,
ClearPartOfCustomerCreationStatus,
} from '../../../../core/store/actions/process.actions';
import { User, Address, Features, Organisation } from '../../../../core/models/user.model';
import {
User,
Address,
Features,
Organisation,
} from '../../../../core/models/user.model';
import { Router } from '@angular/router';
import { Process, CustomerFormState } from '../../../../core/models/process.model';
import {
Process,
CustomerFormState,
} from '../../../../core/models/process.model';
import { Breadcrumb } from '../../../../core/models/breadcrumb.model';
import { Observable, Subject, of } from 'rxjs';
import { AddBreadcrumb } from '../../../../core/store/actions/breadcrumb.actions';
import { AddUser, SetUserDetails } from '../../../../core/store/actions/customer.actions';
import { takeUntil, distinctUntilChanged, switchMap, map, filter, tap, share, debounceTime, take, catchError } from 'rxjs/operators';
import {
AddUser,
SetUserDetails,
} from '../../../../core/store/actions/customer.actions';
import {
takeUntil,
distinctUntilChanged,
switchMap,
map,
filter,
tap,
debounceTime,
take,
catchError,
} from 'rxjs/operators';
import { CustomValidators } from '../../../../shared/validation/custom-validation';
import { GENDERS, TIITLES } from '../../dropdown-values';
import { ProcessSelectors } from '../../../../core/store/selectors/process.selectors';
@@ -23,7 +57,11 @@ import { SharedSelectors } from '../../../../core/store/selectors/shared.selecto
import { EditCustomerData } from '../../../../core/models/edit-customer.model';
import { CountrySelector } from '../../../../core/store/selectors/countries.selector';
import { Country } from '../../../../core/models/country.model';
import { isoDateFromString, scrollToFirstInvalidElement, isEmptyString } from '../../../../core/utils/app.utils';
import {
isoDateFromString,
scrollToFirstInvalidElement,
isEmptyString,
} from '../../../../core/utils/app.utils';
import { isNullOrUndefined } from 'util';
import { DeliveryOption } from 'apps/sales/src/app/core/models/delivery-option.model';
import { CustomerService } from 'apps/sales/src/app/core/services/customer.service';
@@ -34,9 +72,17 @@ import { AddressSugestionsComponent } from '../address-sugestions/address-sugest
import { ErrorService } from 'apps/sales/src/app/core/error/component/error.service';
import { FormsSelectors } from 'apps/sales/src/app/core/store/selectors/forms.selectors';
import { SaveFormState, DeleteFormState } from 'apps/sales/src/app/core/store/actions/forms.actions';
import { USER_FORM_STATE_KEY, USER_EXTRAS_FORM_STATE_KEY, USER_ERRORS_FORM_STATE_KEY } from 'apps/sales/src/app/core/utils/app.constants';
import {
SaveFormState,
DeleteFormState,
} from 'apps/sales/src/app/core/store/actions/forms.actions';
import {
USER_FORM_STATE_KEY,
USER_EXTRAS_FORM_STATE_KEY,
USER_ERRORS_FORM_STATE_KEY,
} from 'apps/sales/src/app/core/utils/app.constants';
import { AppSelectors } from 'apps/sales/src/app/core/store/selectors/app.selectors';
import { CustomerHelperService } from '../../services/customer-helper.service';
@Component({
selector: 'app-create-customer-card',
@@ -45,20 +91,30 @@ import { AppSelectors } from 'apps/sales/src/app/core/store/selectors/app.select
})
export class CreateCustomerCardComponent implements OnInit, OnDestroy {
@Select(ProcessSelectors.getProcesses) processes$: Observable<Process[]>;
@Select(ProcessSelectors.getCurrentProcess) currentProcess$: Observable<Process>;
@Select(SharedSelectors.getCustomerEditData) customerEditData$: Observable<EditCustomerData>;
@Select(CountrySelector.getCountriesIterable) countries$: Observable<Country[]>;
@Select(CountrySelector.getDeutchlandIterable) deutchland$: Observable<Country[]>;
@Select(ProcessSelectors.getCurrentProcess) currentProcess$: Observable<
Process
>;
@Select(SharedSelectors.getCustomerEditData) customerEditData$: Observable<
EditCustomerData
>;
@Select(CountrySelector.getCountriesIterable) countries$: Observable<
Country[]
>;
@Select(CountrySelector.getDeutchlandIterable) deutchland$: Observable<
Country[]
>;
@ViewChild('postBtn', { static: false }) postBtn: ButtonComponent;
@ViewChild('landDD', { static: false }) landDD: SelectComponent;
@ViewChild('suggestions', { static: false }) suggestions: AddressSugestionsComponent;
@ViewChild('suggestions', { static: false })
suggestions: AddressSugestionsComponent;
destroy$ = new Subject();
destroyForm$ = new Subject();
userForm: FormGroup;
submitted = false;
processes: Process[] = [];
id: number;
headerText = 'Wenn Sie möchten legen wir Ihnen gerne ein Onlinekonto an. Dort können Sie Ihre Bestellungen einsehen.';
headerText =
'Wenn Sie möchten legen wir Ihnen gerne ein Onlinekonto an. Dort können Sie Ihre Bestellungen einsehen.';
countryList: Country[];
@@ -96,6 +152,9 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
lastEmailValueChecked: string;
lastEmailCheckResult: any = null;
flagsSetByConditions = false;
customerFormState: CustomerFormState = CustomerFormState.CREATE;
firstLoad = true;
formListenerSet = false;
addressSuggestions: Address[];
addressFields = {
@@ -128,12 +187,12 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
private fb: FormBuilder,
private store: Store,
private router: Router,
private renderer: Renderer,
private customerService: CustomerService,
private cdrf: ChangeDetectorRef,
private appService: AppService,
private errorService: ErrorService
) { }
private errorService: ErrorService,
private customerHelper: CustomerHelperService
) {}
ngOnInit() {
this.isIPad = this.appService.isIPadEnv();
@@ -144,13 +203,13 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
this.store
.select(SharedSelectors.cartHasItemsFor)
.pipe(
takeUntil(this.destroy$),
distinctUntilChanged(),
tap(this.processTypes),
switchMap(() => this.customerEditData$),
filter((data) => !isNullOrUndefined(data)),
switchMap(this.customerToProcessSwitcher),
filter((data) => !isNullOrUndefined(data))
filter((data) => !isNullOrUndefined(data)),
takeUntil(this.destroy$)
)
.subscribe(this.initializationSubscriptionHandler);
@@ -167,6 +226,21 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
this.loadExtrasState();
this.loadErrorsState();
});
this.customerHelper.customerUpdateRedirect$
.pipe(debounceTime(2000), take(1))
.subscribe(() => {
if (
this.customerFormState === CustomerFormState.MISSING_DELIVERY ||
(this.customerFormState === CustomerFormState.MISSING_ONLINE &&
this.userForm.value.first_name !== undefined)
) {
this.valueChangesListenerImplementation(this.userForm.value);
setTimeout(() => {
this.updateFormLabelsState(null);
}, 50);
}
});
}
isAddressField = (field: string): boolean => {
@@ -187,15 +261,24 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
this.cartHasItemsForTakeNow = cartHasItemsFor(DeliveryOption.TAKE_NOW);
this.cartHasItemsForDownload = cartHasItemsFor(DeliveryOption.DOWNLOAD);
this.cartHasItemsForDownload.ifTrue(() => {
this.onlineChecked = true;
this.guestChecked = false;
this.flagsSetByConditions = true;
this.headerText =
'Um Ihnen das ebook bereitstellen zu können, brauchen Sie ein Onlinekonto bei Hugendubel. Wir richten es Ihnen gerne sofort ein.';
});
(this.cartHasItemsForDownload || this.cartHasItemsForDelivery).ifTrue(
() => {
this.onlineChecked = true;
this.guestChecked = false;
this.flagsSetByConditions = true;
(this.cartHasItemsForDelivery && !this.cartHasItemsForDownload && !this.onlineChecked).ifTrue(this.guestCheckboxInitialization);
if (this.cartHasItemsForDownload) {
this.headerText =
'Um Ihnen das ebook bereitstellen zu können, brauchen Sie ein Onlinekonto bei Hugendubel. Wir richten es Ihnen gerne sofort ein.';
}
}
);
(
this.cartHasItemsForDelivery &&
!this.cartHasItemsForDownload &&
!this.onlineChecked
).ifTrue(this.guestCheckboxInitialization);
};
guestCheckboxInitialization = () => {
@@ -209,24 +292,34 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
if (process) {
this.backendValidation = false;
clearTimeout(this.errorTimer);
if (process.onlineCustomerCreationError && process.onlineCustomerCreationError.error === true) {
if (
process.onlineCustomerCreationError &&
process.onlineCustomerCreationError.error === true
) {
this.buildCreateForm(this.fb);
this.errorTimer = setTimeout(() => {
this.onlineCustomerCreationErrorHandler(process.onlineCustomerCreationError.invalidProperties);
this.onlineCustomerCreationErrorHandler(
process.onlineCustomerCreationError.invalidProperties
);
}, 1000);
this.loaded = true;
return;
}
this.formState = process.customerFormState;
this.canBeSetToActiveUser = !process.closeDirectlyTab;
this.customerFormState = process.customerFormState;
if (process.customerFormState === CustomerFormState.MISSING_DELIVERY) {
this.userForm = this.buildSetDeliveryAddressForm(this.fb, this.customer.customer);
this.buildSetDeliveryAddressForm(this.fb, this.customer.customer);
this.customerHelper.customerUpdateRedirect();
this.id = this.customer.customer.id;
this.submitted = true;
this.btnText = 'Weiter';
this.create = false;
} else if (process.customerFormState === CustomerFormState.MISSING_ONLINE) {
this.userForm = this.buildSetDeliveryAddressForm(this.fb, this.customer.customer);
} else if (
process.customerFormState === CustomerFormState.MISSING_ONLINE
) {
this.buildSetDeliveryAddressForm(this.fb, this.customer.customer);
this.customerHelper.customerUpdateRedirect();
this.id = this.customer.customer.id;
this.submitted = true;
this.btnText = 'Weiter';
@@ -246,20 +339,36 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
};
private listenFormChanges() {
// if (!this.formListenerSet) {
this.destroyForm$.next();
this.userForm.valueChanges.pipe(takeUntil(this.destroyForm$)).subscribe((v) => {
const formStateString = JSON.stringify(v);
const errors = {};
Object.keys(v).forEach((field) => {
errors[field] = this.userForm.get(field).errors;
this.userForm.valueChanges
.pipe(takeUntil(this.destroyForm$))
.subscribe((v) => {
this.valueChangesListenerImplementation(v);
});
this.store.dispatch(new SaveFormState(USER_FORM_STATE_KEY, formStateString));
this.store.dispatch(new SaveFormState(USER_ERRORS_FORM_STATE_KEY, JSON.stringify(errors)));
// this.formListenerSet = true;
// }
}
private valueChangesListenerImplementation(v: any) {
const formStateString = JSON.stringify(v);
const errors = {};
Object.keys(v).forEach((field) => {
errors[field] = this.userForm.get(field).errors;
});
this.store.dispatch(
new SaveFormState(USER_FORM_STATE_KEY, formStateString)
);
this.store.dispatch(
new SaveFormState(USER_ERRORS_FORM_STATE_KEY, JSON.stringify(errors))
);
}
onlineCustomerCreationErrorHandler(invalidProperties: any) {
if (isNullOrUndefined(invalidProperties) || isNullOrUndefined(this.userForm)) {
if (
isNullOrUndefined(invalidProperties) ||
isNullOrUndefined(this.userForm)
) {
return;
}
if (invalidProperties.Email) {
@@ -296,8 +405,15 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
if (key !== 'Email' && key !== 'FirstName' && key !== 'LastName') {
showErrorPopUp = true;
}
if (keysChecked === Object.keys(invalidProperties).length && showErrorPopUp) {
this.errorService.addErrors(400, null, JSON.stringify(invalidProperties));
if (
keysChecked === Object.keys(invalidProperties).length &&
showErrorPopUp
) {
this.errorService.addErrors(
400,
null,
JSON.stringify(invalidProperties)
);
}
});
scrollToFirstInvalidElement();
@@ -306,9 +422,12 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
}
setValidations() {
this.deliveryAndGuestOrOnline = this.cartHasItemsForDelivery && (this.guestChecked || this.onlineChecked);
this.deliveryAndGuestOrOnline =
this.cartHasItemsForDelivery && (this.guestChecked || this.onlineChecked);
this.pickUpOrTakeNowAndOnline =
(this.cartHasItemsForPickUp || this.cartHasItemsForTakeNow) && this.onlineChecked && !this.deliveryAndGuestOrOnline;
(this.cartHasItemsForPickUp || this.cartHasItemsForTakeNow) &&
this.onlineChecked &&
!this.deliveryAndGuestOrOnline;
this.downloadAndOnline = this.cartHasItemsForDownload && this.onlineChecked;
(
this.deliveryAndGuestOrOnline ||
@@ -316,14 +435,20 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
this.downloadAndOnline ||
this.onlineChecked ||
this.guestChecked
).ifTrue(this.deliveryAndGuestValidationsOrpickUpOrTakeNowAndOnlineValidations);
).ifTrue(
this.deliveryAndGuestValidationsOrpickUpOrTakeNowAndOnlineValidations
);
this.onlineChecked.ifTrue(this.onlineExtraValidations);
}
deliveryAndGuestValidationsOrpickUpOrTakeNowAndOnlineValidations = () => {
this.userForm.get('gender').setValidators([Validators.required, CustomValidators.validateGender]);
this.userForm
.get('gender')
.setValidators([Validators.required, CustomValidators.validateGender]);
this.userForm.get('gender').updateValueAndValidity();
this.userForm.get('email').setValidators([Validators.required, CustomValidators.validateEmail]);
this.userForm
.get('email')
.setValidators([Validators.required, CustomValidators.validateEmail]);
this.userForm.get('email').updateValueAndValidity();
this.userForm.get('address').setValidators([Validators.required]);
this.userForm.get('address').updateValueAndValidity();
@@ -374,7 +499,9 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
};
onlineExtraValidations = () => {
this.userForm.get('email').setAsyncValidators(this.validateIfEmailExists.bind(this));
this.userForm
.get('email')
.setAsyncValidators(this.validateIfEmailExists.bind(this));
this.userForm.get('email').updateValueAndValidity();
};
@@ -394,8 +521,11 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
this.userForm.get('city').updateValueAndValidity();
this.userForm.get('country').clearValidators();
this.userForm.get('country').updateValueAndValidity();
this.deliveryAndGuestOrOnline = this.cartHasItemsForDelivery && (this.guestChecked || this.onlineChecked);
this.pickUpOrTakeNowAndOnline = (this.cartHasItemsForPickUp || this.cartHasItemsForTakeNow) && this.onlineChecked;
this.deliveryAndGuestOrOnline =
this.cartHasItemsForDelivery && (this.guestChecked || this.onlineChecked);
this.pickUpOrTakeNowAndOnline =
(this.cartHasItemsForPickUp || this.cartHasItemsForTakeNow) &&
this.onlineChecked;
};
validateIfEmailExists(control: AbstractControl) {
@@ -431,7 +561,12 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
errors[field] = this.userForm.get(field).errors;
}
});
this.store.dispatch(new SaveFormState(USER_ERRORS_FORM_STATE_KEY, JSON.stringify(errors)));
this.store.dispatch(
new SaveFormState(
USER_ERRORS_FORM_STATE_KEY,
JSON.stringify(errors)
)
);
result$.next(result);
});
}, 2000);
@@ -511,11 +646,21 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
const addressesLength = address.length;
address.forEach((add) => {
processedItems++;
const street = add.street ? add.street : (this.userForm.get('address').value as string);
const streetNo = add.streetNumber ? add.streetNumber : (this.userForm.get('streetNo').value as string);
const zipCode = add.zipCode ? add.zipCode : (this.userForm.get('zipCode').value as string);
const city = add.city ? add.city : (this.userForm.get('city').value as string);
const countryValue = add.country ? add.country : (this.userForm.get('country').value as string);
const street = add.street
? add.street
: (this.userForm.get('address').value as string);
const streetNo = add.streetNumber
? add.streetNumber
: (this.userForm.get('streetNo').value as string);
const zipCode = add.zipCode
? add.zipCode
: (this.userForm.get('zipCode').value as string);
const city = add.city
? add.city
: (this.userForm.get('city').value as string);
const countryValue = add.country
? add.country
: (this.userForm.get('country').value as string);
if (city && street && streetNo && zipCode && countryValue) {
this.addressSuggestions.push({
@@ -529,7 +674,10 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
zip: zipCode,
});
}
if (processedItems === addressesLength && this.addressSuggestions.length === addressesLength) {
if (
processedItems === addressesLength &&
this.addressSuggestions.length === addressesLength
) {
this.openSuggestions();
}
});
@@ -613,10 +761,10 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
this.store
.dispatch(new AddUser(data, this.canBeSetToActiveUser))
.pipe(
take(1),
switchMap(() => {
return this.store.select(ProcessSelectors.getDetailsUserId);
}),
take(1),
catchError((error) => {
this.postBtn.stopLoading();
scrollToFirstInvalidElement();
@@ -656,7 +804,9 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
.toPromise()
.then(() => {
const currentRoute = 'cart/review';
this.store.dispatch(new UpdateCustomerFormState(CustomerFormState.CREATE));
this.store.dispatch(
new UpdateCustomerFormState(CustomerFormState.CREATE)
);
this.store.dispatch(new ChangeCurrentRoute(currentRoute));
this.postBtn.stopLoading();
this.router.navigate([currentRoute]);
@@ -664,9 +814,11 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
}
loadProcesses() {
this.processes$.pipe(takeUntil(this.destroy$)).subscribe((data: Process[]) => {
this.processes = data;
});
this.processes$
.pipe(takeUntil(this.destroy$))
.subscribe((data: Process[]) => {
this.processes = data;
});
}
createProcess() {
@@ -709,7 +861,9 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
}
setAddressValidators = () => {
this.userForm.valueChanges.pipe(distinctUntilChanged(), take(1)).subscribe(this.addressValidatorForSimpleCustomer);
this.userForm.valueChanges
.pipe(distinctUntilChanged(), take(1))
.subscribe(this.addressValidatorForSimpleCustomer);
};
get f() {
@@ -795,15 +949,29 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
null,
this.userForm.get('firstName').value,
this.userForm.get('lastName').value,
this.userForm.get('address').value ? this.userForm.get('address').value : '',
this.userForm.get('streetNo').value ? this.userForm.get('streetNo').value : '',
+this.userForm.get('zipCode').value ? this.userForm.get('zipCode').value : '',
this.userForm.get('address').value
? this.userForm.get('address').value
: '',
this.userForm.get('streetNo').value
? this.userForm.get('streetNo').value
: '',
+this.userForm.get('zipCode').value
? this.userForm.get('zipCode').value
: '',
this.userForm.get('city').value ? this.userForm.get('city').value : '',
this.userForm.get('country').value ? this.getCountryCode(this.userForm.get('country').value) : '',
this.userForm.get('company').value ? this.userForm.get('company').value : '',
this.userForm.get('department').value ? this.userForm.get('department').value : '',
this.userForm.get('country').value
? this.getCountryCode(this.userForm.get('country').value)
: '',
this.userForm.get('company').value
? this.userForm.get('company').value
: '',
this.userForm.get('department').value
? this.userForm.get('department').value
: '',
this.userForm.get('note').value ? this.userForm.get('note').value : '',
this.userForm.get('tax_number').value ? this.userForm.get('tax_number').value : '',
this.userForm.get('tax_number').value
? this.userForm.get('tax_number').value
: '',
this.userForm.get('title').value ? this.userForm.get('title').value : '',
this.userForm.get('gender').value ? this.userForm.get('gender').value : ''
);
@@ -834,10 +1002,18 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
let organisation: Organisation;
if (this.showCompanyFields) {
organisation = {
name: this.userForm.get('company').value ? this.userForm.get('company').value : undefined,
department: this.userForm.get('department').value ? this.userForm.get('department').value : undefined,
extraAddress: this.userForm.get('note').value ? this.userForm.get('note').value : undefined,
vatId: this.userForm.get('tax_number').value ? this.userForm.get('tax_number').value : undefined,
name: this.userForm.get('company').value
? this.userForm.get('company').value
: undefined,
department: this.userForm.get('department').value
? this.userForm.get('department').value
: undefined,
extraAddress: this.userForm.get('note').value
? this.userForm.get('note').value
: undefined,
vatId: this.userForm.get('tax_number').value
? this.userForm.get('tax_number').value
: undefined,
};
}
@@ -845,16 +1021,26 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
id: this.id,
first_name: this.userForm.get('firstName').value,
last_name: this.userForm.get('lastName').value,
title: this.userForm.get('title').value ? this.userForm.get('title').value : '',
gender: this.userForm.get('gender').value ? this.userForm.get('gender').value : '',
mobile_number: this.userForm.get('mobile').value ? this.userForm.get('mobile').value.replace(' ', '') : '',
phone_number: this.userForm.get('telephone').value ? this.userForm.get('telephone').value.replace(' ', '') : '',
title: this.userForm.get('title').value
? this.userForm.get('title').value
: '',
gender: this.userForm.get('gender').value
? this.userForm.get('gender').value
: '',
mobile_number: this.userForm.get('mobile').value
? this.userForm.get('mobile').value.replace(' ', '')
: '',
phone_number: this.userForm.get('telephone').value
? this.userForm.get('telephone').value.replace(' ', '')
: '',
date_of_birth: isoDateFromString(this.userForm.get('dateOfBirth').value),
delivery_addres: address,
poossible_invoice_addresses: [address],
invoice_address: address,
poossible_delivery_addresses: [address],
email: this.userForm.get('email').value ? this.userForm.get('email').value.trim() : '',
email: this.userForm.get('email').value
? this.userForm.get('email').value.trim()
: '',
shop: Math.floor(Math.random() * 6) + 1 > 3 ? true : false,
newUser: true,
features: features,
@@ -864,7 +1050,11 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
return data;
}
private updateDataForSelectedOption(field: string, value: string, defaultValue: string) {
private updateDataForSelectedOption(
field: string,
value: string,
defaultValue: string
) {
if (value !== defaultValue) {
this.userForm.get(field).patchValue(value);
} else {
@@ -873,33 +1063,140 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
}
private buildSetDeliveryAddressForm(fb: FormBuilder, customer: User) {
let country = this.getProperty(customer, 'delivery_addres.country');
const countriesList = this.store.selectSnapshot(CountrySelector.getCountriesIterable);
if (countriesList) {
const countryObj = countriesList.find((c) => c.key === country);
if (countryObj !== undefined) {
country = countryObj.value;
}
}
return fb.group({
firstName: [this.getProperty(customer, 'first_name'), Validators.required],
lastName: [this.getProperty(customer, 'last_name'), Validators.required],
email: [this.getProperty(customer, 'email'), [CustomValidators.validateEmail, Validators.required]],
address: [this.getProperty(customer, 'delivery_addres.street'), Validators.required],
streetNo: [this.getProperty(customer, 'delivery_addres.streetNo'), Validators.required],
zipCode: [this.getProperty(customer, 'delivery_addres.zip'), Validators.required],
city: [this.getProperty(customer, 'delivery_addres.city'), Validators.required],
dateOfBirth: [this.getProperty(customer, 'date_of_birth'), CustomValidators.validateDateOfBirth],
mobile: [this.getProperty(customer, 'mobile_number'), CustomValidators.validateTelephone],
telephone: [this.getProperty(customer, 'phone_number'), CustomValidators.validateTelephone],
country: [country, Validators.required],
gender: [this.getProperty(customer, 'gender')],
title: [this.getProperty(customer, 'title')],
company: [this.getProperty(customer, 'delivery_addres.company_name')],
department: [this.getProperty(customer, 'delivery_addres.company_department')],
tax_number: [this.getProperty(customer, 'delivery_addres.company_tax_number')],
note: [this.getProperty(customer, 'delivery_addres.note')],
});
this.store
.selectOnce(FormsSelectors.getFormState)
.pipe(map((filterFn) => filterFn(USER_FORM_STATE_KEY)))
.subscribe((value) => {
if (
this.customerFormState === CustomerFormState.MISSING_DELIVERY ||
this.customerFormState === CustomerFormState.MISSING_ONLINE
) {
let data;
if (this.firstLoad) {
data = customer;
} else {
data = value ? JSON.parse(value) : undefined;
}
let country = this.getProperty(
data,
this.firstLoad ? 'delivery_addres.country' : 'country'
);
const countriesList = this.store.selectSnapshot(
CountrySelector.getCountriesIterable
);
if (countriesList) {
const countryObj = countriesList.find((c) => c.key === country);
if (countryObj !== undefined) {
country = countryObj.value;
}
}
this.userForm = fb.group({
firstName: [
this.getProperty(
data,
this.firstLoad ? 'first_name' : 'firstName'
),
Validators.required,
],
lastName: [
this.getProperty(data, this.firstLoad ? 'last_name' : 'lastName'),
Validators.required,
],
email: [
this.getProperty(data, 'email'),
[CustomValidators.validateEmail, Validators.required],
],
address: [
this.getProperty(
data,
this.firstLoad ? 'delivery_addres.street' : 'address'
),
Validators.required,
],
streetNo: [
this.getProperty(
data,
this.firstLoad ? 'delivery_addres.streetNo' : 'streetNo'
),
Validators.required,
],
zipCode: [
this.getProperty(
data,
this.firstLoad ? 'delivery_addres.zip' : 'zipCode'
),
Validators.required,
],
city: [
this.getProperty(
data,
this.firstLoad ? 'delivery_addres.city' : 'city'
),
Validators.required,
],
dateOfBirth: [
this.getProperty(
data,
this.firstLoad ? 'date_of_birth' : 'dateOfBirth'
),
CustomValidators.validateDateOfBirth,
],
mobile: [
this.getProperty(
data,
this.firstLoad ? 'mobile_number' : 'mobile'
),
CustomValidators.validateTelephone,
],
telephone: [
this.getProperty(
data,
this.firstLoad ? 'phone_number' : 'telephone'
),
CustomValidators.validateTelephone,
],
country: [country, Validators.required],
gender: [this.getProperty(data, 'gender')],
title: [this.getProperty(data, 'title')],
company: [
this.getProperty(
data,
this.firstLoad ? 'delivery_addres.company_name' : 'company'
),
],
department: [
this.getProperty(
data,
this.firstLoad
? 'delivery_addres.company_department'
: 'department'
),
],
tax_number: [
this.getProperty(
data,
this.firstLoad
? 'delivery_addres.company_tax_number'
: 'tax_number'
),
],
note: [
this.getProperty(
data,
this.firstLoad ? 'delivery_addres.note' : 'note'
),
],
});
if (this.firstLoad) {
this.valueChangesListenerImplementation(this.userForm.value);
this.firstLoad = false;
}
setTimeout(() => {
this.updateFormLabelsState(null);
}, 50);
}
});
}
private buildCreateForm(fb: FormBuilder) {
@@ -907,29 +1204,49 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
.selectOnce(FormsSelectors.getFormState)
.pipe(map((filterFn) => filterFn(USER_FORM_STATE_KEY)))
.subscribe((value) => {
const data = value ? JSON.parse(value) : undefined;
this.userForm = fb.group({
firstName: [this.getProperty(data, 'firstName') || '', Validators.required],
lastName: [this.getProperty(data, 'lastName') || '', Validators.required],
email: [this.getProperty(data, 'email') || '', CustomValidators.validateEmail],
address: [this.getProperty(data, 'address') || ''],
streetNo: [this.getProperty(data, 'streetNo') || ''],
zipCode: [this.getProperty(data, 'zipCode') || ''],
city: [this.getProperty(data, 'city') || ''],
dateOfBirth: [this.getProperty(data, 'dateOfBirth') || '', CustomValidators.validateDateOfBirth],
mobile: [this.getProperty(data, 'mobile') || '', CustomValidators.validateTelephone],
telephone: [this.getProperty(data, 'telephone') || '', CustomValidators.validateTelephone],
country: [this.getProperty(data, 'country') || ''],
gender: [this.getProperty(data, 'gender') || ''],
title: [this.getProperty(data, 'title') || ''],
company: [this.getProperty(data, 'company') || ''],
department: [this.getProperty(data, 'department') || ''],
tax_number: [this.getProperty(data, 'tax_number') || ''],
note: [this.getProperty(data, 'note') || ''],
});
setTimeout(() => {
this.updateFormLabelsState(data);
}, 50);
if (this.customerFormState === CustomerFormState.CREATE) {
const data = value ? JSON.parse(value) : undefined;
this.userForm = fb.group({
firstName: [
this.getProperty(data, 'firstName') || '',
Validators.required,
],
lastName: [
this.getProperty(data, 'lastName') || '',
Validators.required,
],
email: [
this.getProperty(data, 'email') || '',
CustomValidators.validateEmail,
],
address: [this.getProperty(data, 'address') || ''],
streetNo: [this.getProperty(data, 'streetNo') || ''],
zipCode: [this.getProperty(data, 'zipCode') || ''],
city: [this.getProperty(data, 'city') || ''],
dateOfBirth: [
this.getProperty(data, 'dateOfBirth') || '',
CustomValidators.validateDateOfBirth,
],
mobile: [
this.getProperty(data, 'mobile') || '',
CustomValidators.validateTelephone,
],
telephone: [
this.getProperty(data, 'telephone') || '',
CustomValidators.validateTelephone,
],
country: [this.getProperty(data, 'country') || ''],
gender: [this.getProperty(data, 'gender') || ''],
title: [this.getProperty(data, 'title') || ''],
company: [this.getProperty(data, 'company') || ''],
department: [this.getProperty(data, 'department') || ''],
tax_number: [this.getProperty(data, 'tax_number') || ''],
note: [this.getProperty(data, 'note') || ''],
});
setTimeout(() => {
this.updateFormLabelsState(data);
}, 50);
}
});
}
@@ -955,7 +1272,11 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
private updateFieldState(field: any) {
if (field) {
if (this.userForm.get(field) && this.userForm.get(field).value.length > 0) {
if (
this.userForm.get(field) &&
this.userForm.get(field).value &&
this.userForm.get(field).value.length > 0
) {
this.userForm.get(field).markAsDirty();
} else {
this.userForm.get(field).markAsUntouched();
@@ -964,12 +1285,22 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
}
private loadExtrasState() {
const value = this.store.selectSnapshot(FormsSelectors.getFormState)(USER_EXTRAS_FORM_STATE_KEY);
const value = this.store.selectSnapshot(FormsSelectors.getFormState)(
USER_EXTRAS_FORM_STATE_KEY
);
const data = value ? JSON.parse(value) : undefined;
const guest = data ? (this.getProperty(data, 'guest') as boolean) : undefined;
const online = data ? (this.getProperty(data, 'online') as boolean) : undefined;
const submitted = data ? (this.getProperty(data, 'submitted') as boolean) : undefined;
const guestDisabled = data ? (this.getProperty(data, 'guestDisabled') as boolean) : undefined;
const guest = data
? (this.getProperty(data, 'guest') as boolean)
: undefined;
const online = data
? (this.getProperty(data, 'online') as boolean)
: undefined;
const submitted = data
? (this.getProperty(data, 'submitted') as boolean)
: undefined;
const guestDisabled = data
? (this.getProperty(data, 'guestDisabled') as boolean)
: undefined;
setTimeout(() => {
if (guest !== this.guestChecked && !isNullOrUndefined(guest)) {
this.toggleGuest(false);
@@ -982,14 +1313,19 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
} else {
this.submitted = false;
}
if (guestDisabled !== this.guestDisabled && !isNullOrUndefined(guestDisabled)) {
if (
guestDisabled !== this.guestDisabled &&
!isNullOrUndefined(guestDisabled)
) {
this.guestDisabled = guestDisabled;
}
}, 400);
}
private loadErrorsState() {
const value = this.store.selectSnapshot(FormsSelectors.getFormState)(USER_ERRORS_FORM_STATE_KEY);
const value = this.store.selectSnapshot(FormsSelectors.getFormState)(
USER_ERRORS_FORM_STATE_KEY
);
if (value) {
const data = JSON.parse(value);
setTimeout(() => {
@@ -1036,7 +1372,11 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
if (this.guestDisabled) {
return;
}
if (this.cartHasItemsForDelivery && this.guestChecked && !this.onlineChecked) {
if (
this.cartHasItemsForDelivery &&
this.guestChecked &&
!this.onlineChecked
) {
return;
}
this.guestChecked = !this.guestChecked;
@@ -1056,7 +1396,11 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
}
toggleOnline(saveState = true) {
if (this.cartHasItemsForDelivery && !this.guestChecked && this.onlineChecked) {
if (
this.cartHasItemsForDelivery &&
!this.guestChecked &&
this.onlineChecked
) {
return;
}
if (this.onlineChecked && this.cartHasItemsForDownload) {
@@ -1112,13 +1456,17 @@ export class CreateCustomerCardComponent implements OnInit, OnDestroy {
@HostListener('window:scroll', ['$event'])
scrollHandler(event) {
if (event.target.id === 'customer-form') {
this.renderer.invokeElementMethod(document.activeElement, 'blur');
document.activeElement['blur'].apply(document.activeElement);
}
}
detectChanges() {
setTimeout(() => {
if (this.cdrf !== null && this.cdrf !== undefined && !(this.cdrf as ViewRef).destroyed) {
if (
this.cdrf !== null &&
this.cdrf !== undefined &&
!(this.cdrf as ViewRef).destroyed
) {
this.cdrf.detectChanges();
}
}, 0);

View File

@@ -85,7 +85,7 @@ export class CustomerEditCardComponent implements OnInit, OnDestroy {
private fb: FormBuilder,
private router: Router,
private route: ActivatedRoute
) {}
) { }
ngOnInit() {
this.userForm = this.buildForm(this.fb);
@@ -227,10 +227,13 @@ export class CustomerEditCardComponent implements OnInit, OnDestroy {
this.deliveryAddressEditDetails = true;
this.phoneNotEditable = true;
this.mobileNotEditable = true;
this.billingAddressEditable = false;
this.deliveryAddressEditable = false;
this.companyNameNotEditable = true;
this.companyAddressNotEditable = true;
this.companyDepartmentNotEditable = true;
this.companyVatIdNotEditable = true;
this.disableAssignment = false;
};
guestState = () => {
@@ -260,11 +263,13 @@ export class CustomerEditCardComponent implements OnInit, OnDestroy {
this.phoneNotEditable = true;
this.mobileNotEditable = true;
this.billingAddressEditable = true;
this.deliveryAddressEditable = false;
this.deliveryAddressEditDetails = true;
this.companyNameNotEditable = true;
this.companyAddressNotEditable = true;
this.companyDepartmentNotEditable = true;
this.companyVatIdNotEditable = true;
this.disableAssignment = false;
};
cardState = () => {

View File

@@ -1,4 +1,12 @@
import { Component, OnInit, AfterViewInit, ViewChild, ChangeDetectorRef, ChangeDetectionStrategy, ViewRef } from '@angular/core';
import {
Component,
OnInit,
AfterViewInit,
ViewChild,
ChangeDetectorRef,
ChangeDetectionStrategy,
ViewRef,
} from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { User, Address } from '../../../../core/models/user.model';
@@ -6,7 +14,7 @@ import {
ChangeCurrentRoute,
UpdateCustomerFormState,
CustomerOrdersVisited,
SetDetailsCustomer
SetDetailsCustomer,
} from '../../../../core/store/actions/process.actions';
import { Router, ActivatedRoute } from '@angular/router';
import { Breadcrumb } from '../../../../core/models/breadcrumb.model';
@@ -39,11 +47,14 @@ export interface CustomerFilters {
selector: 'app-search-customer-result',
templateUrl: './customer-search-result.component.html',
styleUrls: ['./customer-search-result.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerSearchResultComponent implements OnInit {
@ViewChild(CdkVirtualScrollViewport, { static: false }) viewport: CdkVirtualScrollViewport;
@Select(SharedSelectors.getSearchedCustomers) customers$: Observable<CustomerSearchResult>;
@ViewChild(CdkVirtualScrollViewport, { static: false })
viewport: CdkVirtualScrollViewport;
@Select(SharedSelectors.getSearchedCustomers) customers$: Observable<
CustomerSearchResult
>;
@Select(SharedSelectors.getCart) cartData$: Observable<ProcessCart>;
customerSearch: CustomerSearch;
customers: User[] = [];
@@ -52,12 +63,13 @@ export class CustomerSearchResultComponent implements OnInit {
ds: CustomerSearchDataSource;
isScan = false;
cartHasItemsForDownload = false;
cartHasItemsForDelivery = false;
selectedFilterMode: Side = Side.RIGHT;
filters: CustomerFilters = {
guestaccount: undefined,
onlineshop: undefined,
bonuscard: undefined
bonuscard: undefined,
};
constructor(
@@ -66,16 +78,18 @@ export class CustomerSearchResultComponent implements OnInit {
private customerService: CustomerService,
private route: ActivatedRoute,
private cdrf: ChangeDetectorRef
) { }
) {}
ngOnInit() {
this.cartData$
.pipe(
filter(c => !isNullOrUndefined(c)),
filter((c) => !isNullOrUndefined(c)),
take(1),
tap((processCart: ProcessCart) => {
if (processCart && processCart.cart && processCart.cart.length > 0) {
const count = processCart.cart.map(t => t.quantity).reduce((t1, t2) => t1 + t2);
const count = processCart.cart
.map((t) => t.quantity)
.reduce((t1, t2) => t1 + t2);
this.cartExist = count > 0 ? true : false;
} else {
this.cartExist = false;
@@ -85,20 +99,23 @@ export class CustomerSearchResultComponent implements OnInit {
return this.route.queryParams;
})
)
.subscribe(params => {
.subscribe((params) => {
const isScan = params && params['scan'] && params['scan'] === 'true';
this.isScan = isScan;
this.filters = {
guestaccount: this.selectedFilterMode === Side.RIGHT ? false : undefined,
guestaccount:
this.selectedFilterMode === Side.RIGHT ? false : undefined,
bonuscard: undefined,
onlineshop: undefined
onlineshop: undefined,
};
this.loadDataSource(isScan, true);
});
}
loadDataSource(isScan: boolean, firstLoad = false) {
this.customerSearch = this.store.selectSnapshot(ProcessSelectors.getCustomerSearch);
this.customerSearch = this.store.selectSnapshot(
ProcessSelectors.getCustomerSearch
);
if (!!this.customerSearch) {
this.ds = new CustomerSearchDataSource(
this.customerSearch.query,
@@ -115,11 +132,21 @@ export class CustomerSearchResultComponent implements OnInit {
}
details(user: User, index: number): void {
const cartHasItemFor = this.store.selectSnapshot(SharedSelectors.cartHasItemsFor);
this.cartHasItemsForDownload = cartHasItemFor && cartHasItemFor(DeliveryOption.DOWNLOAD);
const hasOnlineAccount = this.hasOnlineAccount(user);
const cartHasItemFor = this.store.selectSnapshot(
SharedSelectors.cartHasItemsFor
);
this.cartHasItemsForDownload =
cartHasItemFor && cartHasItemFor(DeliveryOption.DOWNLOAD);
this.cartHasItemsForDelivery =
cartHasItemFor && cartHasItemFor(DeliveryOption.DELIVERY);
if (this.cartHasItemsForDownload && !hasOnlineAccount) {
const hasOnlineAccount = this.hasOnlineAccount(user);
const hasAllRequiredData = this.hasAllRequiredData(user);
if (
(this.cartHasItemsForDownload || this.cartHasItemsForDelivery) &&
(!hasOnlineAccount || !hasAllRequiredData)
) {
this.redirectToCustomerUpdate(user);
return;
}
@@ -133,7 +160,7 @@ export class CustomerSearchResultComponent implements OnInit {
new AddBreadcrumb(
{
name: 'Kundendetails',
path: currentRoute
path: currentRoute,
},
'customer'
)
@@ -142,8 +169,31 @@ export class CustomerSearchResultComponent implements OnInit {
this.router.navigate([currentRoute]);
}
hasAllRequiredData = (user: User): boolean => {
if (
!user.gender ||
!user.first_name ||
!user.last_name ||
!user.email ||
!!(
isNullOrUndefined(user.delivery_addres) &&
isNullOrUndefined(user.invoice_address)
) ||
!(
(user.delivery_addres && user.delivery_addres.country) ||
(user.invoice_address && user.invoice_address.country)
)
) {
return false;
}
return true;
};
hasOnlineAccount = (user: User) => {
const found = user.features.filter(feature => feature.key === 'onlineshop');
const found = user.features.filter(
(feature) => feature.key === 'onlineshop'
);
if (!found || found.length < 1) {
return false;
@@ -160,31 +210,39 @@ export class CustomerSearchResultComponent implements OnInit {
const newBread: Breadcrumb = {
name: 'Kundendaten erfassen',
path: '/customer/search/create',
queryParams: { card: 'create' }
queryParams: { card: 'create' },
};
this.store.dispatch(new UpdateCustomerFormState(CustomerFormState.CREATE));
this.store.dispatch(new AddBreadcrumb(newBread, 'customer'));
this.store.dispatch(new ChangeCurrentRoute('/customer/search/create'));
this.router.navigate(['/customer/search/create'], { queryParams: { card: 'create' } });
this.router.navigate(['/customer/search/create'], {
queryParams: { card: 'create' },
});
}
redirectToCustomerUpdate(user: User) {
const newBread: Breadcrumb = {
name: 'Kundendaten erfassen',
path: '/customer/search/create'
path: '/customer/search/create',
};
this.store.dispatch(new SetDetailsCustomer(user.id));
this.store.dispatch(new UpdateCustomerFormState(CustomerFormState.MISSING_ONLINE));
this.store.dispatch(
new UpdateCustomerFormState(CustomerFormState.MISSING_ONLINE)
);
this.store.dispatch(new AddBreadcrumb(newBread, 'customer'));
const currentRoute = `customer/search/missing-data/${user.id}`;
this.store.dispatch(new ChangeCurrentRoute(currentRoute));
this.router.navigate([currentRoute]);
this.router.navigate([currentRoute], { queryParams: { card: 'create' } });
}
updateFilter(val: boolean, type: string) {
this.filters[type] = val ? (this.selectedFilterMode === Side.LEFT ? true : false) : undefined;
this.filters[type] = val
? this.selectedFilterMode === Side.LEFT
? true
: false
: undefined;
this.ds = new CustomerSearchDataSource(
this.customerSearch.query,
this.store,
@@ -200,7 +258,9 @@ export class CustomerSearchResultComponent implements OnInit {
if (!isNullOrUndefined(index)) {
setTimeout(() => {
const ids = index.split(':');
const processId = this.store.selectSnapshot(AppState.getCurrentProcessId);
const processId = this.store.selectSnapshot(
AppState.getCurrentProcessId
);
if (processId === +ids[1]) {
const idx = +ids[0];
if (idx) {
@@ -219,8 +279,16 @@ export class CustomerSearchResultComponent implements OnInit {
return '';
}
const address = user.delivery_addres;
const plz = address ? (isNullOrUndefined(address.zip) ? '' : address.zip) : '';
const city = address ? (isNullOrUndefined(address.city) ? '' : address.city) : '';
const plz = address
? isNullOrUndefined(address.zip)
? ''
: address.zip
: '';
const city = address
? isNullOrUndefined(address.city)
? ''
: address.city
: '';
return plz + ' ' + city;
}
@@ -236,7 +304,7 @@ export class CustomerSearchResultComponent implements OnInit {
this.filters = {
guestaccount: side === Side.RIGHT ? false : undefined,
bonuscard: undefined,
onlineshop: undefined
onlineshop: undefined,
};
if (!noFilters || side === Side.RIGHT) {
this.ds = new CustomerSearchDataSource(
@@ -253,7 +321,11 @@ export class CustomerSearchResultComponent implements OnInit {
detectChanges() {
setTimeout(() => {
if (this.cdrf !== null && this.cdrf !== undefined && !(this.cdrf as ViewRef).destroyed) {
if (
this.cdrf !== null &&
this.cdrf !== undefined &&
!(this.cdrf as ViewRef).destroyed
) {
this.cdrf.detectChanges();
}
}, 0);

View File

@@ -6,7 +6,9 @@ import { Subject } from 'rxjs';
})
export class CustomerHelperService {
resetCustomerSearchView$ = new Subject();
constructor() {}
customerUpdateRedirect$ = new Subject();
constructor() { }
public resetCustomerSearchView = () => this.resetCustomerSearchView$.next();
public customerUpdateRedirect = () => this.customerUpdateRedirect$.next();
}

View File

@@ -32,7 +32,7 @@ import { WindowRef } from 'apps/sales/src/app/core/services/window-ref.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BookCardComponent implements OnInit {
@ViewChild('books', { read: ElementRef, static: false }) public books: ElementRef<any>;
@ViewChild('books', { read: ElementRef, static: true }) public books: ElementRef<any>;
@Select(ProcessSelectors.getProcesses) processes$: Observable<Process[]>;
@Input() card: FeedCard;
isIPad: boolean;

View File

@@ -1,15 +1,29 @@
<div class="filter-wrapper">
<div class="filter-nav">
<button *ngFor="let filter of value; trackBy: trackByKeyOrName; let first = first" type="button"
(click)="selected = filter" class="isa-btn isa-btn-block isa-btn-default isa-btn-l isa-font-weight-bold"
[class.isa-mt-10]="!first">
<button
*ngFor="let filter of value; trackBy: trackByKeyOrName; let first = first"
type="button"
(click)="selected = filter"
class="isa-btn isa-btn-block isa-btn-default isa-btn-l"
[class.isa-mt-10]="!first"
[class.is-active]="filter === selected"
[class.isa-font-weight-emphasis]="filter !== selected"
[class.isa-font-weight-bold]="filter === selected"
>
<span>{{ filter.name }}</span>
<lib-icon name="Arrow_Next" height="17px"></lib-icon>
<lib-icon
[name]="filter === selected ? 'Arrow_Next_Dark' : 'Arrow_Next_Grey'"
height="17px"
></lib-icon>
</button>
</div>
<div class="filter-content isa-ml-10" [ngSwitch]="selected?.type">
<app-select-filter *ngSwitchCase="'select'" [options]="selected.options" [max]="selected.max"
(optionsChanged)="emitOnChange($event)">
<app-select-filter
*ngSwitchCase="'select'"
[options]="selected.options"
[max]="selected.max"
(optionsChanged)="emitOnChange($event)"
>
</app-select-filter>
</div>
</div>
</div>

View File

@@ -16,6 +16,10 @@
justify-content: space-between;
align-items: center;
}
.is-active {
background: #89949e;
}
}
.filter-content {

View File

@@ -49,7 +49,7 @@ export class FilterGroupComponent implements OnInit, ControlValueAccessor, OnCha
trackByKeyOrName = (filter: Filter) => filter.key || filter.name;
constructor(private cdr: ChangeDetectorRef) {}
constructor(private cdr: ChangeDetectorRef) { }
ngOnChanges({ value, selected }: SimpleChanges): void {
if (value) {
@@ -63,7 +63,7 @@ export class FilterGroupComponent implements OnInit, ControlValueAccessor, OnCha
}
}
ngOnInit() {}
ngOnInit() { }
writeValue(obj: Filter[]): void {
if (obj !== this.value) {

View File

@@ -1,5 +1,5 @@
<div class="select-filter-option-wrapper">
<div class="isa-form-group isa-pt-10 isa-pb-10">
<div class="isa-form-group isa-pt-15 isa-pb-15">
<input
type="checkbox"
[name]="option?.name"
@@ -15,9 +15,9 @@
type="button"
class="isa-btn expand-button"
[class.expanded]="option?.expanded"
(click)="option.expanded = !option?.expanded"
(click)="toggleExpanded()"
>
<lib-icon name="Arrow_Down_2"></lib-icon>
<lib-icon name="Arrow_Down_2_grey"></lib-icon>
</button>
</div>

View File

@@ -10,8 +10,11 @@
.expand-button {
transition: all 150ms ease-in-out;
padding: 0px;
&.expanded {
padding-bottom: 18px;
padding-top: 20px;
transform: rotate(180deg);
}
}

View File

@@ -24,7 +24,7 @@ export class SelectFilterOptionComponent implements OnChanges {
@Output()
optionChanged = new EventEmitter<SelectFilterOption>();
constructor(private cdr: ChangeDetectorRef) {}
constructor(private cdr: ChangeDetectorRef) { }
ngOnChanges({ option }: SimpleChanges): void {
if (option) {
@@ -62,6 +62,12 @@ export class SelectFilterOptionComponent implements OnChanges {
this.optionChanged.emit(this.option);
}
toggleExpanded() {
this.option.expanded = !this.option.expanded;
this.onChange();
this.cdr.markForCheck();
}
childChanged(option: SelectFilterOption, index: number) {
this.option.options[index] = option;

View File

@@ -5,7 +5,7 @@
>
<button
type="button"
class="isa-btn isa-p-0"
class="isa-btn isa-p-0 isa-font-lightgrey isa-font-weight-emphasis"
*ngIf="!max"
(click)="selectAll()"
>
@@ -15,8 +15,9 @@
<div class="select-filter-options isa-mt-10">
<div
class="select-filter-options-content"
(scroll)="onOptionsContentScroll()"
(scroll)="updateScrollButton()"
#optionsContainer
(cdkObserveContent)="updateScrollButton()"
>
<ng-container *ngFor="let option of options; let index = index">
<app-select-filter-option
@@ -36,5 +37,10 @@
[class.down]="scrollPositionPersantage < 20"
(click)="scroll()"
>
<lib-icon name="Arrow_back_branch"></lib-icon>
<lib-icon
width="18px"
height="20px"
mt="3px"
name="Arrow_back_branch"
></lib-icon>
</button>

View File

@@ -1,6 +1,5 @@
:host {
display: block;
position: relative;
}
.isa-card {
overflow: hidden;
@@ -21,9 +20,10 @@
}
.scroll-btn {
position: absolute;
bottom: -3rem;
right: 1rem;
box-shadow: 0px 0px 3px 3px rgba(0, 0, 0, 0.2);
bottom: 4px;
right: 6px;
box-shadow: 0px 0px 20px 0px rgba(89, 100, 112, 0.5);
transition: all 100ms linear;
&.up {
transform: rotate(90deg);

View File

@@ -10,13 +10,11 @@ import {
SimpleChanges,
ChangeDetectorRef,
AfterViewInit,
OnDestroy,
} from '@angular/core';
import { SelectFilterOption } from '../../../models';
import { BehaviorSubject } from 'rxjs';
import {
cloneSelectFilterOption,
flattenSelectFilterOption,
} from '../../../utils';
import { BehaviorSubject, interval, Subscription } from 'rxjs';
import { cloneSelectFilterOption, flattenSelectFilterOption } from '../../../utils';
@Component({
selector: 'app-select-filter',
@@ -24,7 +22,7 @@ import {
styleUrls: ['select-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SelectFilterComponent implements OnChanges, AfterViewInit {
export class SelectFilterComponent implements OnChanges, AfterViewInit, OnDestroy {
@Input()
options: SelectFilterOption[];
@@ -41,20 +39,38 @@ export class SelectFilterComponent implements OnChanges, AfterViewInit {
scrollPositionPersantage = 0;
allSelected$ = new BehaviorSubject<boolean>(false);
constructor(private cdr: ChangeDetectorRef) { }
updateScrollPositionPersantageIntervalSub: Subscription;
constructor(private cdr: ChangeDetectorRef) {}
ngOnDestroy(): void {
if (this.updateScrollPositionPersantageIntervalSub) {
this.updateScrollPositionPersantageIntervalSub.unsubscribe();
}
}
ngAfterViewInit(): void {
this.onOptionsContentScroll();
this.updateScrollButton();
this.updateScrollPositionPersantageIntervalSub = interval(500).subscribe(() => this.updateScrollButton());
}
updateScrollButton() {
this.updateScrollPositionPersantage();
this.setCanScroll();
this.cdr.markForCheck();
}
setCanScroll() {
const container = this.optionsContainer.nativeElement;
const scrollHeight = container.scrollHeight;
const height = container.clientHeight;
if (this.optionsContainer) {
const container = this.optionsContainer.nativeElement;
const scrollHeight = container.scrollHeight;
const height = container.clientHeight;
this.canScroll = scrollHeight > height;
this.cdr.markForCheck();
if (this.canScroll !== scrollHeight > height) {
this.canScroll = scrollHeight > height;
this.cdr.markForCheck();
}
}
}
ngOnChanges({ options }: SimpleChanges): void {
@@ -62,7 +78,6 @@ export class SelectFilterComponent implements OnChanges, AfterViewInit {
this.options = this.options.map(cloneSelectFilterOption);
this.setAllSelected(options.currentValue);
this.cdr.markForCheck();
setTimeout(() => this.setCanScroll(), 0);
}
}
@@ -75,9 +90,7 @@ export class SelectFilterComponent implements OnChanges, AfterViewInit {
return {
...option,
selected: !this.allSelected$.value,
options: Array.isArray(option.options)
? option.options.map(selectFn)
: option.options,
options: Array.isArray(option.options) ? option.options.map(selectFn) : option.options,
};
};
@@ -91,9 +104,7 @@ export class SelectFilterComponent implements OnChanges, AfterViewInit {
optionChanged(option: SelectFilterOption, index: number) {
let options = [...this.options];
options[index] = option;
const selectedOptions = flattenSelectFilterOption(options).filter(
(o) => o.selected
);
const selectedOptions = flattenSelectFilterOption(options).filter((o) => o.selected);
if (this.max > 0 && selectedOptions.length > this.max) {
const unselectOptions = selectedOptions
@@ -101,9 +112,7 @@ export class SelectFilterComponent implements OnChanges, AfterViewInit {
.slice(0, selectedOptions.length - this.max);
const updateOption = (o: SelectFilterOption): SelectFilterOption =>
unselectOptions.some((s) => s.key === o.key && s.name === o.name)
? { ...o, selected: false }
: o;
unselectOptions.some((s) => s.key === o.key && s.name === o.name) ? { ...o, selected: false } : o;
options = options.map(updateOption);
}
@@ -131,15 +140,19 @@ export class SelectFilterComponent implements OnChanges, AfterViewInit {
});
}
onOptionsContentScroll() {
const container: HTMLElement = this.optionsContainer.nativeElement;
const scrollHeight = container.scrollHeight;
const scrollTop = container.scrollTop;
const height = container.clientHeight;
updateScrollPositionPersantage() {
if (this.optionsContainer) {
const container: HTMLElement = this.optionsContainer.nativeElement;
const scrollHeight = container.scrollHeight;
const scrollTop = container.scrollTop;
const height = container.clientHeight;
this.scrollPositionPersantage = Math.round(
(100 / (scrollHeight - height)) * scrollTop
);
const scrollPositionPersantage = Math.round((100 / (scrollHeight - height)) * scrollTop);
if (this.scrollPositionPersantage !== scrollPositionPersantage) {
this.scrollPositionPersantage = scrollPositionPersantage;
this.cdr.markForCheck();
}
}
}
scroll() {

View File

@@ -1,9 +1,10 @@
import { NgModule } from '@angular/core';
import { FilterGroupModule } from './filter-group/filter-group.module';
import { SelectedFilterOptionsModule } from './selected-filter-options';
import { ObserversModule } from '@angular/cdk/observers';
@NgModule({
imports: [FilterGroupModule, SelectedFilterOptionsModule],
imports: [FilterGroupModule, SelectedFilterOptionsModule, ObserversModule],
exports: [],
declarations: [],
providers: [],

View File

@@ -1,29 +1,23 @@
<div class="container" *ngIf="value">
<div class="clear-all">
<span class="filter-name">Alle Filter Entfernen</span>
<lib-icon (click)="clearAllFilters()" name="close-branch"></lib-icon>
<button class="isa-btn isa-btn-primary-link remove-all isa-pt-0 isa-pb-0 isa-pl-15 isa-pr-15"
(click)="clearAllFilters()">
Alle Filter Entfernen
</button>
</div>
<ng-container *ngFor="let filter of value">
<ng-container
*ngTemplateOutlet="optionTmpl; context: { $implicit: filter }"
></ng-container>
<ng-container *ngTemplateOutlet="optionTmpl; context: { $implicit: filter }"></ng-container>
</ng-container>
<ng-container
[ngSwitch]="isCollapsed"
*ngIf="items && items.length > collapsedStateNumberOfSelectedFiltersShown"
>
<div class="expand" *ngSwitchCase="true" (click)="updateCollapsed(false)">
<ng-container [ngSwitch]="isCollapsed" *ngIf="items && items.length > collapsedStateNumberOfSelectedFiltersShown">
<div class="expand c-pointer" *ngSwitchCase="true" (click)="updateCollapsed(false)">
<span>Mehr</span>
<lib-icon name="Arrow_More_Grey"></lib-icon>
<lib-icon name="Arrow_More_Grey" class="isa-mt-0"></lib-icon>
</div>
<div class="collapse" *ngSwitchDefault (click)="updateCollapsed(true)">
<lib-icon
[rotateBackwards]="true"
style="margin-left: 12px;"
name="Arrow_More_Grey"
></lib-icon>
<div class=" collapse c-pointer" *ngSwitchDefault (click)="updateCollapsed(true)">
<lib-icon [rotateBackwards]="true" class="isa-mt-0 isa-ml-12" name="Arrow_More_Grey">
</lib-icon>
<span>Weniger</span>
</div>
</ng-container>
@@ -32,17 +26,10 @@
<ng-template #optionTmpl let-filter>
<div *ngIf="filter.selected" class="selected-filter" #filteredItem>
<span class="filter-name">{{ filter.name }}</span>
<lib-icon
(click)="clearFilter(filter); (false)"
name="close-branch"
></lib-icon>
<lib-icon (click)="clearFilter(filter); (false)" name="close-branch"></lib-icon>
</div>
<ng-container
*ngFor="let option of filter.options | checkAllChildOptionsSelected"
>
<ng-container
*ngTemplateOutlet="optionTmpl; context: { $implicit: option }"
></ng-container>
<ng-container *ngFor="let option of filter.options | checkAllChildOptionsSelected">
<ng-container *ngTemplateOutlet="optionTmpl; context: { $implicit: option }"></ng-container>
</ng-container>
</ng-template>
</ng-template>

View File

@@ -1,6 +1,7 @@
$color-grey: #596470;
$filter-padding-right: 13px;
$icon-margin: 6px;
$font-size: 18px;
.container {
display: flex;
@@ -35,11 +36,20 @@ $icon-margin: 6px;
.filter-name {
font-weight: bold;
font-size: $font-size;
line-height: 2;
display: flex;
padding-left: 15px;
}
.remove-all {
font-weight: bold;
line-height: 2;
font-size: $font-size;
cursor: pointer;
color: #f70400;
}
.selected-filter {
margin-right: $filter-padding-right;
.filter-name {
@@ -56,6 +66,7 @@ $icon-margin: 6px;
.collapse {
margin-left: $icon-margin;
font-weight: bold;
font-size: $font-size;
color: $color-grey;
display: flex;
align-items: center;

View File

@@ -11,10 +11,8 @@ import {
Renderer2,
ElementRef,
} from '@angular/core';
import { Filter, SelectFilterOption, SelectFilter } from '../../models';
import { Filter, SelectFilterOption } from '../../models';
import { cloneFilter } from '../../utils';
import { isNullOrUndefined } from 'util';
import { DeleteSelectedFiltersForProcess } from 'apps/sales/src/app/core/store/actions/filter.actions';
@Component({
selector: 'app-selected-filter-options',
@@ -33,7 +31,7 @@ export class SelectedFilterOptionsComponent implements AfterViewInit {
collapsedStateNumberOfSelectedFiltersShown = 3;
isCollapsed = true;
constructor(private cdr: ChangeDetectorRef, private renderer: Renderer2) { }
constructor(private cdr: ChangeDetectorRef, private renderer: Renderer2) {}
ngAfterViewInit() {
this.items.changes.subscribe((_) => {

View File

@@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'getAuthorsPipe',
})
export class GetAuthorsPipe implements PipeTransform {
transform(author: string): string[] {
if (author) {
const authors = author.split(';');
return authors.map((a) => a.trim());
}
return [];
}
}

View File

@@ -9,13 +9,24 @@
</div>
<div class="content-container">
<div class="author align-left">
<ng-container *ngFor="let author of authors; let i = index; let last = last">
<span [id]="'author' + i" (click)="filterByAuthor(author)">{{ author }}</span>
<ng-container
*ngFor="
let author of product.author | getAuthorsPipe;
let i = index;
let last = last
"
>
<span [id]="'author' + i" (click)="filterByAuthor(author)">{{
author
}}</span>
<span *ngIf="!last">{{ '; ' }}</span>
</ng-container>
</div>
<div class="title-price">
<div class="title align-left" [ngClass]="{ 'title-grid': product.recommandation }">
<div
class="title align-left"
[ngClass]="{ 'title-grid': product.recommandation }"
>
<div class="rec-icon-container" *ngIf="product.recommandation">
<lib-icon name="Empfehlungen_Icon"></lib-icon>
</div>
@@ -32,30 +43,45 @@
<div class="type-stock">
<div class="type align-left">
<div class="type-icon-container align-left">
<lib-icon *ngIf="!!product.typeIcon && product.typeIcon != '--'" name="Icon_{{ product.typeIcon }}"></lib-icon>
<lib-icon
*ngIf="!!product.typeIcon && product.typeIcon != '--'"
name="Icon_{{ product.typeIcon }}"
></lib-icon>
</div>
<div class="type-text align-left">
<span>{{ product.type }}</span>
</div>
</div>
<div class="stock-container align-right">
<div class="available-stock" [ngClass]="{ 'no-quantity-container': product.itemsInStock === 0 }">
<div class="stock-icon-wraper" *ngIf="product.itemsInStock && product.itemsInStock > 0">
<div
class="available-stock"
[ngClass]="{ 'no-quantity-container': product.itemsInStock === 0 }"
>
<div
class="stock-icon-wraper"
*ngIf="product.itemsInStock && product.itemsInStock > 0"
>
<lib-icon
class="stock-icon"
name="{{ product.recommandation ? 'Icon_House_recommended' : 'Icon_House' }}"
name="{{
product.recommandation ? 'Icon_House_recommended' : 'Icon_House'
}}"
height="19px"
width="16px"
></lib-icon>
</div>
<div class="stock" *ngIf="product.itemsInStock && product.itemsInStock > 0">
<span *ngIf="product.itemsInStock">{{ product.itemsInStock }}x</span>
<div
class="stock"
*ngIf="product.itemsInStock && product.itemsInStock > 0"
>
<span *ngIf="product.itemsInStock"
>{{ product.itemsInStock }}x</span
>
<span *ngIf="!product.itemsInStock">0x</span>
</div>
</div>
<div class="store-availability">
<div class="availability-image">
<!-- <lib-icon name="Package_Icon_blue" height="18px" *ngIf="product.typeIcon !== 'EB' && product.storeStatusCode > 1"></lib-icon> -->
<lib-icon
name="download_product"
mr="3px"
@@ -80,16 +106,31 @@
</div>
<div class="volume-edition">
<span
>{{ product.edition }}<span *ngIf="product.edition && product.volume">|</span>{{ product.volume ? product.volume : '' }}
<span *ngIf="(product.edition || product.volume) && product.publicationDate">|</span> {{ publicationDate }}</span>
>{{ product.edition
}}<span *ngIf="product.edition && product.volume">|</span
>{{ product.volume ? product.volume : '' }}
<span
*ngIf="
(product.edition || product.volume) && product.publicationDate
"
>|</span
>
{{ publicationDate }}</span
>
<span class="order">{{ product.category }}</span>
</div>
</div>
<div class="avalability-text">
<span *ngIf="product.typeIcon !== 'EB' && product.typeIcon !== 'DL'">{{
product.storeStatusCode === 999 ? 'Lieferbar' : product.sscText
}}</span>
<span *ngIf="product.typeIcon === 'EB' || product.typeIcon === 'DL'">Download</span>
<div *ngIf="product.typeIcon !== 'EB' && product.typeIcon !== 'DL'">
<span *ngIf="product.ssc">{{ product.ssc }}</span
><span *ngIf="product.ssc && product.sscText"> - </span>
<span *ngIf="product.sscText">{{
product.storeStatusCode === 999 ? 'Lieferbar' : product.sscText
}}</span>
</div>
<span *ngIf="product.typeIcon === 'EB' || product.typeIcon === 'DL'"
>Download</span
>
</div>
</div>
</div>

View File

@@ -9,7 +9,7 @@
margin-top: 10px;
padding: 15px 20px;
// Virtual Scroll Item Height is set to 220px based on maring/paddings => 220 - (10 + 2 * 15) = 180
min-height: 180px;
min-height: 140px;
cursor: pointer;
}
@@ -32,7 +32,7 @@
}
.content-container {
display: grid;
// display: grid;
grid-template-columns: auto;
}
@@ -59,11 +59,12 @@
font-size: 26px;
font-weight: bold;
display: inline-block;
margin-bottom: 2px;
&.multi-line {
font-size: 22px;
transition: font-size .05s ease-out;
margin-bottom: 14px;
transition: font-size 0.05s ease-out;
margin-bottom: 6px;
}
}
@@ -90,18 +91,17 @@
display: grid;
grid-template-columns: auto max-content;
// margin-top: 10px;
align-items: end;
align-items: baseline;
}
.avalability-text {
font-family: 'Open Sans';
font-size: 18px;
font-size: 16px;
font-weight: bold;
line-height: 21px;
color: #000000;
word-break: break-word;
max-width: 220px;
text-align: end;
margin-top: -1px;
}
.type {
@@ -160,14 +160,14 @@
.publisher-order {
display: grid;
grid-template-columns: auto max-content;
margin-top: 10px;
// margin-top: 5px;
}
.publisher-serial {
display: grid;
grid-template-columns: max-content min-content auto;
grid-gap: 1vh;
// grid-gap: 1vh;
position: relative;
}
@@ -187,6 +187,11 @@
.publisher {
max-width: 210px;
margin-right: 10px;
}
.serial {
margin-left: 10px;
}
.no-quantity-container {

View File

@@ -9,7 +9,7 @@ import {
ElementRef,
Renderer2,
AfterViewChecked,
ViewChild
ViewChild,
} from '@angular/core';
import { Router } from '@angular/router';
import { Store } from '@ngxs/store';
@@ -22,9 +22,10 @@ import { AddBreadcrumb } from 'apps/sales/src/app/core/store/actions/breadcrumb.
@Component({
selector: 'app-product-card',
templateUrl: './product-card.component.html',
styleUrls: ['./product-card.component.scss']
styleUrls: ['./product-card.component.scss'],
})
export class ProductCardComponent implements OnInit, OnDestroy, AfterViewChecked {
export class ProductCardComponent
implements OnInit, OnDestroy, AfterViewChecked {
private _product: Product;
@Input() index: number;
@@ -41,7 +42,6 @@ export class ProductCardComponent implements OnInit, OnDestroy, AfterViewChecked
} else {
this.publicationDate = undefined;
}
this.splitAuthors(val.author);
}
}
@@ -64,9 +64,9 @@ export class ProductCardComponent implements OnInit, OnDestroy, AfterViewChecked
private router: Router,
private store: Store,
private renderer: Renderer2
) { }
) {}
ngOnInit() { }
ngOnInit() {}
ngOnDestroy() {
this.destroy$.next();
@@ -97,11 +97,15 @@ export class ProductCardComponent implements OnInit, OnDestroy, AfterViewChecked
}
this.saveProductPosition.emit(this.index);
// TODO: this is temporary solution for the incostency of product detail API
const products = this.store.selectSnapshot(SharedSelectors.getSearchedProducts);
const products = this.store.selectSnapshot(
SharedSelectors.getSearchedProducts
);
if (products && products.itemsDTO) {
const productDetailsData = products.itemsDTO[product.id];
if (productDetailsData) {
this.store.dispatch(new AddSelectedProduct(productDetailsData, this.index));
this.store.dispatch(
new AddSelectedProduct(productDetailsData, this.index)
);
}
}
@@ -109,8 +113,11 @@ export class ProductCardComponent implements OnInit, OnDestroy, AfterViewChecked
this.store.dispatch(
new AddBreadcrumb(
{
name: product.title ? product.title.substring(0, 12) + (product.title.length > 12 ? '...' : '') : '',
path: '/product/details/' + product.id
name: product.title
? product.title.substring(0, 12) +
(product.title.length > 12 ? '...' : '')
: '',
path: '/product/details/' + product.id,
},
'product'
)
@@ -142,11 +149,4 @@ export class ProductCardComponent implements OnInit, OnDestroy, AfterViewChecked
return '';
}
}
splitAuthors(author: string) {
if (author) {
const _authors = author.split(';');
this.authors = _authors.map(a => a.trim());
}
}
}

View File

@@ -1,21 +1,18 @@
<div
id="product-detail-container"
class="product-detail-container"
#productDetailContainer
*ngIf="product"
[@shrinkMainCard]="expanded"
(scroll)="scrollHandler($event)"
>
<div id="product-detail-container" class="product-detail-container" #productDetailContainer *ngIf="product"
[@shrinkMainCard]="expanded" (scroll)="scrollHandler($event)">
<div class="feature-icons" *ngIf="showFeatures">
<lib-icon name="subscription_tag" alt="subscription icon" type="png" height="48px" *ngIf="isSubscription"></lib-icon>
<lib-icon name="subscription_tag" alt="subscription icon" type="png" height="48px" *ngIf="isSubscription">
</lib-icon>
<lib-icon name="gift_tag" alt="gift icon" type="png" height="48px" *ngIf="isGiftable"></lib-icon>
<lib-icon name="recommendation_tag" alt="recommendation icon" type="png" height="48px" *ngIf="isRecommendation"></lib-icon>
<lib-icon name="recommendation_tag" alt="recommendation icon" type="png" height="48px" *ngIf="isRecommendation">
</lib-icon>
</div>
<div [@shrinkTitle]="!expanded" (click)="expand()">{{ product.title }}</div>
<div class="general-details" *ngIf="expanded">
<div class="product-image-container">
<img [src]="product.imageId | bookImageUrl: 469:575 | async" class="product-image" alt="book image" (click)="openGallery()" />
<img [src]="product.imageId | bookImageUrl: 469:575 | async" class="product-image" alt="book image"
(click)="openGallery()" />
<div class="img-magnifying-glass" (click)="openGallery()">
<lib-icon name="magnifying-glass" width="27px"></lib-icon>
</div>
@@ -45,7 +42,9 @@
</div>
</div>
<div class="volume-edition">
<span>{{ product.volume }} <span *ngIf="product.volume && product.edition">|</span> {{ product.edition }}</span>
<span>{{ product.volume }}
<span *ngIf="product.volume && product.edition">|</span>
{{ product.edition }}</span>
</div>
<div class="publication">
<span>{{ product.publicationDate }}</span>
@@ -65,11 +64,8 @@
<span>{{ product.publisher }}</span>
</div>
<div class="quantity-category">
<div
class="quantity-wrapper align-right"
[ngClass]="{ 'stock-loading': !stockInfoLoaded }"
*ngIf="product.formatIcon !== 'EB' && product.formatIcon !== 'DL'"
>
<div class="quantity-wrapper align-right" [ngClass]="{ 'stock-loading': !stockInfoLoaded }"
*ngIf="product.formatIcon !== 'EB' && product.formatIcon !== 'DL'">
<div class="home-icon" *ngIf="stockInfoLoaded">
<lib-icon name="House_Icon_grey" height="22px"></lib-icon>
</div>
@@ -97,10 +93,13 @@
<div class="lang-ean-age-availability-status">
<div class="lang-ean-age">
<div class="languages standart-text">
<span
>{{ product.genre.length > 20 ? product.genre.substring(0, 20) + '...' : product.genre }}
<span *ngIf="product.genre && product.locale">|</span> {{ product.locale }}</span
>
<span>{{
product.genre.length > 20
? product.genre.substring(0, 20) + '...'
: product.genre
}}
<span *ngIf="product.genre && product.locale">|</span>
{{ product.locale }}</span>
</div>
<div class="ean standart-text">
<span>{{ product.eanTag }}</span>
@@ -111,39 +110,43 @@
</div>
<div class="avilability-status" *ngIf="!(showAvLoading | async)">
<div class="status-icons" *ngIf="!(isDownload | async)">
<div class="align-right ship-icon align-right" *ngIf="(shippingAvailable | async) && !(deliveryError$ |
async)">
<div class="align-right ship-icon align-right"
*ngIf="(shippingAvailable | async) && !(deliveryError$ | async)">
<lib-icon name="Truck_Icon_grey" height="17px"></lib-icon>
</div>
<div class="align-right ship-icon align-right" *ngIf="(deliveryError$ | async)">
<lib-icon name="Icon_Truck_Questionmark" mt="-4px" height="22px"
tooltip="{{(deliveryError$ | async)}}" placement="top" delay="200"></lib-icon>
<div class="align-right ship-icon align-right" *ngIf="deliveryError$ | async">
<lib-icon name="Icon_Truck_Questionmark" mt="-4px" height="22px" tooltip="{{ deliveryError$ | async }}"
placement="top" delay="200"></lib-icon>
</div>
<div class="align-right send-icon" *ngIf="(storeAvailable | async) && !(storeError$ | async)">
<lib-icon name="Package_Icon_grey" height="18px"></lib-icon>
</div>
<div class="align-right send-icon" *ngIf="(storeError$ | async)">
<lib-icon name="Icon_Pickup_Questionmark" mt="-4px" height="22px"
tooltip="{{(storeError$ | async)}}" placement="top" delay="200"></lib-icon>
<div class="align-right send-icon" *ngIf="storeError$ | async">
<lib-icon name="Icon_Pickup_Questionmark" mt="-4px" height="22px" tooltip="{{ storeError$ | async }}"
placement="top" delay="200"></lib-icon>
</div>
</div>
<div class="availability-status-text align-right">
<span>{{ availabilityStatusText }}</span>
<span *ngIf="availabilityStatus">{{ availabilityStatus }}</span><span
*ngIf="availabilityStatus && availabilityStatus"> - </span>
<span *ngIf="availabilityStatusText">{{
availabilityStatusText
}}</span>
</div>
</div>
<div *ngIf="showAvLoading | async">
<app-availability-loading></app-availability-loading>
</div>
<ng-container>
<div *ngIf="showAvLoading | async">
<app-availability-loading></app-availability-loading>
</div>
</ng-container>
<div></div>
<div class="category align-right" *ngIf="!(isDownload | async)">
<span>{{ product.assortment }}</span>
</div>
</div>
<div
class="product-staus-info"
[ngClass]="{ 'partly-loaded': !fullyLoaded }"
*ngIf="(productReview$ | async)?.employeeReviews || (fullStars | async)"
>
<div class="product-staus-info" [ngClass]="{ 'partly-loaded': !fullyLoaded }" *ngIf="
(productReview$ | async)?.employeeReviews || (fullStars | async)
">
<div class="stars-wrapper">
<ng-container *ngFor="let star of fullStars | async">
<lib-icon name="Star_full" width="12px" type="png"></lib-icon>
@@ -165,19 +168,24 @@
</div>
<div class="separator" *ngIf="expanded"></div>
<div class="product-details" *ngIf="expanded">
<ng-container *ngIf="product.fullDescription && product.fullDescription.length > 0 && showNoDescription === false; else noDescription">
<ng-container *ngIf="
product.fullDescription &&
product.fullDescription.length > 0 &&
showNoDescription === false;
else noDescription
">
<ng-container *ngIf="product.fullDescription.length > 100">
<div class="details" id="details-container" *ngIf="!moreBtn">
<span id="details-text">{{ product.fullDescription | descriptionText }}</span>
<span class="more-btn" (click)="toggleMore('more')"
>Mehr
<span id="details-text">{{
product.fullDescription | descriptionText
}}</span>
<span class="more-btn" (click)="toggleMore('more')">Mehr
<lib-icon name="Arrow_Next-with-body" pl="8px" alt="more"></lib-icon>
</span>
</div>
<div class="details-full" id="details-container" *ngIf="moreBtn">
<span id="details-text" [innerHTML]="product.fullDescription"></span>
<span class="more-btn opened" (click)="toggleMore('less')"
>Weniger
<span class="more-btn opened" (click)="toggleMore('less')">Weniger
<lib-icon name="Arrow_back" pl="8px" alt="less"></lib-icon>
</span>
</div>
@@ -195,33 +203,16 @@
</div>
</ng-template>
<div class="actions align-right" [ngClass]="{ 'action-partly-loaded': !fullyLoaded }">
<app-button (action)="openBranchesAvailabilityModal()" *ngIf="product.formatIcon !== 'EB' && product.formatIcon !== 'DL'"
>weitere Verfügbarkeiten</app-button
>
<app-button
[primary]="true"
[load]="true"
[stayOnPage]="true"
[disabled]="inTheCartButtonDisabled"
(loaded)="contentLoaded()"
(action)="openModal()"
#addtocart
>In den Warenkorb</app-button
>
<app-button (action)="openBranchesAvailabilityModal()"
*ngIf="product.formatIcon !== 'EB' && product.formatIcon !== 'DL'">weitere Verfügbarkeiten</app-button>
<app-button [primary]="true" [load]="true" [stayOnPage]="true" [disabled]="inTheCartButtonDisabled"
(loaded)="contentLoaded()" (action)="openModal()" #addtocart>In den Warenkorb</app-button>
</div>
</div>
<app-product-other-formats
[productFormats]="product.otherFormats"
[detailsExpanded]="detailsExpanded"
#otherformats
></app-product-other-formats>
<lib-photo-gallery
*ngIf="item"
#photoGallery
[productId]="item.id"
[images]="images$ | async"
[title]="product.title"
></lib-photo-gallery>
<app-product-other-formats [productFormats]="product.otherFormats" [detailsExpanded]="detailsExpanded" #otherformats>
</app-product-other-formats>
<lib-photo-gallery *ngIf="item" #photoGallery [productId]="item.id" [images]="images$ | async"
[title]="product.title"></lib-photo-gallery>
</div>
<div loading="true" *ngIf="!product && !loadingError">
<app-product-details-loading></app-product-details-loading>
@@ -237,43 +228,35 @@
</div>
</ng-container>
<ng-container
*ngIf="
<ng-container *ngIf="
item &&
product &&
(((storeAvLoaded === true && shippingAvLoaded === true) || downloadLoaded === true) &&
((takeNowAvailable | async) || (storeAvailable | async) || (shippingAvailable | async) || (downloadAvailable | async)))
"
>
<app-checkout
#checkout
(closed)="cartActionCompleted($event)"
[currentPickUpDate]="currentPickUpDate"
[currentDeliveryDate]="currentDeliveryDate"
[book]="item"
[availability]="availability"
[avpickupprice]="pickUpPrice"
[avdeliveryprice]="deliveryPrice"
[avdownloadprice]="downloadPrice"
[isDownload]="product.formatIcon === 'EB' || product.formatIcon === 'DL'"
></app-checkout>
((storeAvLoaded === true && shippingAvLoaded === true) ||
downloadLoaded === true) &&
((takeNowAvailable | async) ||
(storeAvailable | async) ||
(shippingAvailable | async) ||
(downloadAvailable | async))
">
<app-checkout #checkout (closed)="cartActionCompleted($event)" [currentPickUpDate]="currentPickUpDate"
[currentDeliveryDate]="currentDeliveryDate" [book]="item" [availability]="availability"
[avpickupprice]="pickUpPrice" [avdeliveryprice]="deliveryPrice" [avdownloadprice]="downloadPrice"
[isDownload]="product.formatIcon === 'EB' || product.formatIcon === 'DL'"></app-checkout>
</ng-container>
<ng-container *ngIf="item && loadBranchesInfoComponent">
<app-branches-avalability-overview
(destroymodal)="destroyAvailabilityModal($event)"
[item]="item"
#branchesAvailabilityInfo
></app-branches-avalability-overview>
<app-branches-avalability-overview (destroymodal)="destroyAvailabilityModal($event)" [item]="item"
#branchesAvailabilityInfo></app-branches-avalability-overview>
</ng-container>
<app-printer-selection #printModal (print)="print($event)"></app-printer-selection>
<div class="recommendations" [@shrinkSecondary]="!expanded" *ngIf="item && !loadingError">
<div class="header" *ngIf="expanded" (click)="expand()">
<lib-icon name="recommendation_tag" alt="recommendation icon" type="png" height="48px" class="recommendation-card-icon"></lib-icon>
<lib-icon name="recommendation_tag" alt="recommendation icon" type="png" height="48px"
class="recommendation-card-icon"></lib-icon>
<span>Empfehlungen</span>
</div>
<app-recommendations [book]="item" #recommendations></app-recommendations>
</div>
</div>

View File

@@ -338,7 +338,7 @@
.lang-ean-age-availability-status {
display: grid;
grid-template-columns: auto 173px;
grid-template-columns: auto 250px;
grid-gap: 5px;
.avilability-status {
@@ -352,9 +352,11 @@
}
.availability-status-text {
width: 173px;
font-size: 18px;
font-weight: bold;
line-height: 21px;
text-align: end;
margin-left: auto;
}
}
}
@@ -375,7 +377,7 @@
margin-right: 5px;
}
/*
/*
##Device = Big Desktops
*/
@media (min-width: 1281px) {
@@ -393,7 +395,7 @@
}
}
/*
/*
##Device = Laptops, Desktops, Ipad pro
*/
@media (min-width: 1025px) and (max-width: 1280px) {
@@ -411,7 +413,7 @@
}
}
/*
/*
##Device = Tablets, Ipads
*/
@media (min-width: 768px) and (max-width: 1024px) {
@@ -420,13 +422,13 @@
}
}
/*
/*
##Device = Low Resolution Tablets, Mobiles (Landscape)
*/
@media (min-width: 481px) and (max-width: 767px) {
}
/*
/*
##Device = Most of the Smartphones Mobiles (Portrait)
*/
@media (min-width: 320px) and (max-width: 480px) {

View File

@@ -68,6 +68,7 @@ export class ProductDetailsComponent implements OnInit, OnDestroy {
_halfStars = 0;
_emptyStars = 0;
loadBranchesInfoComponent = false;
availabilityStatus: string;
availabilityStatusText: string;
currentPickUpDate = '';
currentDeliveryDate = '';
@@ -632,7 +633,7 @@ export class ProductDetailsComponent implements OnInit, OnDestroy {
.getShippingAvailabilityWithCheck(item, ean, branch.id)
.pipe(takeUntil(this.destroy$))
.subscribe((response) => {
if ((response as { error: boolean; message: string; type: CheckoutType }).error) {
if ((response as { error: boolean; message: string; type: CheckoutType; }).error) {
this.downloadError = true;
this.downloadLoaded = true;
if (this.addToCartBtn) {
@@ -647,6 +648,7 @@ export class ProductDetailsComponent implements OnInit, OnDestroy {
if (successfulResponse) {
const preferredAvailability = successfulResponse.av.find((t) => t.preferred === 1 && t.supplier === 'DIG');
if (preferredAvailability) {
this.availabilityStatus = preferredAvailability.ssc;
this.availabilityStatusText = preferredAvailability.sscText;
if (preferredAvailability.price && preferredAvailability.price.value && preferredAvailability.price.value.value) {
this.downloadPrice = preferredAvailability.price.value.value;
@@ -684,7 +686,7 @@ export class ProductDetailsComponent implements OnInit, OnDestroy {
async loadAvailability(it: ItemDTO, ean: string): Promise<boolean> {
const availabilityObservables: Observable<
{ branchId: number; type: CheckoutType; av: AvailabilityDTO[] } | { error: boolean; message: string; type: CheckoutType }
{ branchId: number; type: CheckoutType; av: AvailabilityDTO[]; } | { error: boolean; message: string; type: CheckoutType; }
>[] = [];
const userBranchNumber = this.store.selectSnapshot(BranchSelectors.getUserBranch);
const branch = this.store.selectSnapshot(BranchSelectors.getBranchesIterable).find((t) => t.branchNumber === userBranchNumber);
@@ -713,7 +715,7 @@ export class ProductDetailsComponent implements OnInit, OnDestroy {
type: CheckoutType;
av: AvailabilityDTO[];
}
| { error: boolean; message: string; type: CheckoutType }
| { error: boolean; message: string; type: CheckoutType; }
) => {
if (
(response as {
@@ -751,6 +753,7 @@ export class ProductDetailsComponent implements OnInit, OnDestroy {
return;
}
if (item.type === CheckoutType.store) {
this.availabilityStatus = preferredAvailability.ssc;
this.availabilityStatusText = preferredAvailability.sscText;
this.currentPickUpDate = preferredAvailability.at
? this.datePipe.transform(new Date(preferredAvailability.at), 'dd.MM.yy')
@@ -790,6 +793,8 @@ export class ProductDetailsComponent implements OnInit, OnDestroy {
if (this.addToCartBtn) {
this.addToCartBtn.stopLoading();
}
this.cdrf.detectChanges();
});
return true;
}

View File

@@ -53,7 +53,7 @@ export class ProductResultsComponent implements OnInit, OnDestroy, AfterViewInit
products: Product[];
skip = 0;
processCount = 0;
@ViewChild('scroller', { static: true }) scroller: CdkVirtualScrollViewport;
@ViewChild('scroller', { static: false }) scroller: CdkVirtualScrollViewport;
@ViewChildren('productCard') productCardList: QueryList<ProductCardComponent>;
loading = true;
filters: Filter[];

View File

@@ -7,9 +7,21 @@ import { ProductCardComponent } from './components/product-card/product-card.com
import { ScrollingModule } from '@angular/cdk/scrolling';
import { ScrollingModule as ExperimentalScrollingModule } from '@angular/cdk-experimental/scrolling';
import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
import { LoadingModule, PhotoGalleryModule, CardModule, SearchInputModule, DeleteDropdownModule } from '@libs/ui';
import {
LoadingModule,
PhotoGalleryModule,
CardModule,
SearchInputModule,
DeleteDropdownModule,
} from '@libs/ui';
import { SharedModule } from './../../shared/shared.module';
import { ModalModule, ButtonModule, InputModule, DropdownModule, IconModule } from '@libs/ui';
import {
ModalModule,
ButtonModule,
InputModule,
DropdownModule,
IconModule,
} from '@libs/ui';
import { ProductDetailsComponent } from './pages/product-details/product-details.component';
import { RecommendationsdModule } from './../recommendations/recommendations.module';
import { ProductReviewComponent } from './components/product-review/product-review.component';
@@ -37,6 +49,7 @@ import { UiSwitchModule } from 'ngx-toggle-switch';
import { InteractiveFiltersComponent } from './components/interactive-filters/interactive-filters.component';
import { StockInfoLoadingComponent } from './components/stock-info-loading/stock-info-loading.component';
import { SmallDoubleChoiceSwitchModule } from '@libs/ui/lib/small-double-choice-switch';
import { GetAuthorsPipe } from './components/product-card/get-authors.pipe';
@NgModule({
imports: [
@@ -64,7 +77,7 @@ import { SmallDoubleChoiceSwitchModule } from '@libs/ui/lib/small-double-choice-
SearchDropdownModule,
PrinterSelectionModule,
UiSwitchModule,
SmallDoubleChoiceSwitchModule
SmallDoubleChoiceSwitchModule,
],
declarations: [
ProductResultsComponent,
@@ -86,9 +99,10 @@ import { SmallDoubleChoiceSwitchModule } from '@libs/ui/lib/small-double-choice-
ProductDetailsLoadingComponent,
ProductOtherFormatsComponent,
InteractiveFiltersComponent,
StockInfoLoadingComponent
StockInfoLoadingComponent,
GetAuthorsPipe,
],
exports: [],
providers: [BookImagePipe, BookThumbnailPipe, DescriptionTextPipe]
providers: [BookImagePipe, BookThumbnailPipe, DescriptionTextPipe],
})
export class ProductModule {}

View File

@@ -1,18 +1,27 @@
import {
trigger,
transition,
stagger,
animate,
style,
query,
state
state,
} from '@angular/animations';
export const fadeInAnimation = trigger('fadeIn', [
state('true', style({ opacity: 1, transform: 'translateY(0)' })),
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-50px)' }),
animate('200ms ease-in')
animate('200ms ease-in'),
]),
transition(':leave', animate(200, style({ opacity: 0 })))
transition(':leave', animate(200, style({ opacity: 0 }))),
]);
export function fadeInWithDelay(delayInSec: number) {
return trigger('fadeInWithDelay', [
state('true', style({ opacity: 1, transform: 'translateY(0)' })),
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-50px)' }),
animate(`200ms ${delayInSec}s ease-in`),
]),
transition(':leave', animate(200, style({ opacity: 0 }))),
]);
}

View File

@@ -94,11 +94,20 @@
name="Delete-white"
></lib-icon>
</div>
<div class="feature-tooltip-arrow-down" [style.marginLeft.px]="tooltipArrowMl" *ngIf="toolTipOpened"></div>
<div class="features item" *ngIf="isArray(product.features); else emptyFeatures">
<span *ngFor="let feature of product.features; let index = index" (click)="openTooltip(feature.name, index)">{{
feature.key
}}</span>
<div
class="feature-tooltip-arrow-down"
[style.marginLeft.px]="tooltipArrowMl"
*ngIf="toolTipOpened"
></div>
<div
class="features item"
*ngIf="isArray(product.features); else emptyFeatures"
>
<span
*ngFor="let feature of product.features; let index = index"
(click)="openTooltip(feature.name, index)"
>{{ feature.key }}</span
>
</div>
<ng-template #emptyFeatures>
<div class="empty-feature"></div>
@@ -130,7 +139,11 @@
</div>
<hr class="spacer branch" />
<div class="actions">
<app-button primary="true" (action)="addProdcutToList()"
<app-button
[load]="true"
primary="true"
(action)="addProdcutToList()"
#addProductButton
>Hinzufügen</app-button
>
</div>

View File

@@ -1,17 +1,30 @@
import { Component, OnInit, Input, EventEmitter, Output, OnDestroy } from '@angular/core';
import {
Component,
OnInit,
Input,
EventEmitter,
Output,
OnDestroy,
ViewChild,
} from '@angular/core';
import { RemissionProduct, RemissionService } from '@isa/remission';
import { ModalService } from '@libs/ui';
import { filter, take } from 'rxjs/operators';
import { ModalService, ButtonComponent } from '@libs/ui';
import { filter, take, catchError, debounceTime, delay } from 'rxjs/operators';
import { isNullOrUndefined } from 'util';
import { NO_RESMISSION_REASON_SELECTED } from '../../constants/remission.constants';
import { RemissionHelperService } from '../../services/remission-helper.service';
import { Store } from '@ngxs/store';
import { SetRemissionIsLoading } from 'apps/sales/src/app/core/store/actions/remission.actions';
@Component({
selector: 'app-add-product-to-remission-dialog',
templateUrl: './add-product-to-remission-dialog.component.html',
styleUrls: ['./add-product-to-remission-dialog.component.scss']
styleUrls: ['./add-product-to-remission-dialog.component.scss'],
})
export class AddProductToRemissionDialogComponent implements OnInit, OnDestroy {
@ViewChild('addProductButton', { static: false })
addProductButton: ButtonComponent;
id = 'remission-add-product-to-remission-modal';
@Input() product: RemissionProduct;
@Output() add = new EventEmitter<RemissionProduct>();
@@ -34,7 +47,8 @@ export class AddProductToRemissionDialogComponent implements OnInit, OnDestroy {
constructor(
private modalService: ModalService,
private remissionService: RemissionService,
private remissionHelper: RemissionHelperService
private remissionHelper: RemissionHelperService,
private store: Store
) {}
ngOnInit() {
@@ -47,10 +61,10 @@ export class AddProductToRemissionDialogComponent implements OnInit, OnDestroy {
this.remissionService
.getRemissionReasons()
.pipe(
filter(data => !isNullOrUndefined(data)),
filter((data) => !isNullOrUndefined(data)),
take(1)
)
.subscribe(reasons => {
.subscribe((reasons) => {
this.reasons = reasons;
this.reason = NO_RESMISSION_REASON_SELECTED;
});
@@ -108,14 +122,31 @@ export class AddProductToRemissionDialogComponent implements OnInit, OnDestroy {
this.reasonError = true;
return;
}
this.store.dispatch(new SetRemissionIsLoading(true));
this.addProductButton.startLoading();
this.remissionService
.addProductToRemit({ product: this.product, remissionReason: this.reason, remissionQuantity: this.quantity })
.pipe(take(1))
.subscribe(response => {
.addProductToRemit({
product: this.product,
remissionReason: this.reason,
remissionQuantity: this.quantity,
})
.pipe(
catchError((err) => {
this.store.dispatch(new SetRemissionIsLoading(false));
this.addProductButton.stopLoading();
return [err];
}),
debounceTime(250),
take(1),
delay(2000)
)
.subscribe((product: RemissionProduct) => {
this.reasonError = false;
this.remissionHelper.addRemissionListItem(response);
this.remissionHelper.addRemissionListItem(product);
this.modalService.close(this.id);
this.add.emit(this.product);
this.add.emit(product);
});
}
}

View File

@@ -6,31 +6,48 @@ import { isNullOrUndefined } from 'util';
@Component({
selector: 'app-remission-add-product-to-shipping-document-partially-dialog',
templateUrl: './remission-add-product-to-shipping-document-partially-dialog.component.html',
styleUrls: ['./remission-add-product-to-shipping-document-partially-dialog.component.scss']
templateUrl:
'./remission-add-product-to-shipping-document-partially-dialog.component.html',
styleUrls: [
'./remission-add-product-to-shipping-document-partially-dialog.component.scss',
],
})
export class RemissionAddProductToShippingDocumentPartiallyDialogComponent implements OnInit {
export class RemissionAddProductToShippingDocumentPartiallyDialogComponent
implements OnInit {
id = 'remission-add-product-to-shipping-document-partially-modal';
@Input() quantity: number;
@Input() placementType: RemissionPlacementType;
@Output() add = new EventEmitter();
@Output() closed = new EventEmitter<{ quantity: number; placementType: string }>();
@Output() closed = new EventEmitter<{
quantity: number;
placementType: string;
}>();
placementTypes: RemissionPlacementType[];
loading = false;
predefinedRemissionQuantity: number;
constructor(private modalService: ModalService, private remissionService: RemissionService) {}
constructor(
private modalService: ModalService,
private remissionService: RemissionService
) {}
ngOnInit() {
this.remissionService
.getPlacementTypes()
.pipe(
filter(data => !isNullOrUndefined(data) && data.length > 0),
filter((data) => !isNullOrUndefined(data) && data.length > 0),
take(1)
)
.subscribe(placementTypes => {
.subscribe((placementTypes) => {
this.placementTypes = placementTypes;
});
this.setPredefinedRemissionQuantity(this.quantity);
}
setPredefinedRemissionQuantity(totalRemissionQuantity: number) {
this.predefinedRemissionQuantity = totalRemissionQuantity;
}
public openDialog() {
@@ -48,7 +65,11 @@ export class RemissionAddProductToShippingDocumentPartiallyDialogComponent imple
if (isNullOrUndefined(this.quantity) || this.quantity < 0) {
return;
}
this.add.emit({ quantity: this.quantity, placementType: this.placementType });
this.add.emit({
quantity: this.quantity,
placementType: this.placementType,
predefinedRemissionQuantity: this.predefinedRemissionQuantity,
});
}
placementUpdated(placementType: RemissionPlacementType) {

View File

@@ -11,7 +11,7 @@
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
color: #557596;
color: #596470;
padding: 0 10%;
}
}

View File

@@ -13,23 +13,10 @@
<app-remission-open-shipping-documents-widget></app-remission-open-shipping-documents-widget>
</div>
</div>
<div class="start-remission">
<app-button [primary]="true" (action)="startRemission.emit()"
>Remission starten</app-button
>
</div>
</div>
<div class="actions" *ngIf="status === 'started'">
<div class="remit-all">
<span>Wanne befüllen</span>
</div>
<div class="complete-remission">
<app-button
[primary]="true"
[disabled]="!enableCompleteShippingDocument"
(action)="completeShippingDocument.emit()"
>Wanne abschließen</app-button
>
</div>
</div>

View File

@@ -3,16 +3,11 @@
flex-direction: column;
justify-content: center;
.start-remission,
.add-article {
display: flex;
justify-content: center;
}
.start-remission {
padding-top: 22px;
}
.secondary-actions {
display: flex;
justify-content: center;

View File

@@ -0,0 +1,4 @@
// start:ng42.barrel
export * from './remission-list-start-action.component';
// end:ng42.barrel

View File

@@ -0,0 +1,8 @@
<div class="start-remission">
<button
class="isa-btn isa-btn-l isa-btn-primary isa-btn-shadow isa-btn-pill"
(click)="startRemission.emit()"
>
Remission starten
</button>
</div>

View File

@@ -0,0 +1,5 @@
.start-remission {
display: flex;
justify-content: center;
padding-top: 22px;
}

View File

@@ -0,0 +1,18 @@
import {
Component,
ChangeDetectionStrategy,
EventEmitter,
Output,
} from '@angular/core';
@Component({
selector: 'app-remission-list-start-action',
templateUrl: 'remission-list-start-action.component.html',
styleUrls: ['./remission-list-start-action.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RemissionListStartActionComponent {
@Output() startRemission = new EventEmitter<void>();
constructor() {}
}

View File

@@ -60,9 +60,9 @@ export class RemissionListCardStartedComponent implements OnInit {
private remissionHelper: RemissionHelperService,
private cdr: ChangeDetectorRef,
private errorService: ErrorService
) { }
) {}
ngOnInit() { }
ngOnInit() {}
openTooltip(index: number) {
this.toolTipOpened = this.toolTipOpened.map(
@@ -136,6 +136,7 @@ export class RemissionListCardStartedComponent implements OnInit {
remitPartially(partialRemission: {
quantity: number;
placementType: string;
predefinedRemissionQuantity: number;
}) {
if (this.product.remissionQuantity < 1) {
setTimeout(() => {
@@ -146,6 +147,7 @@ export class RemissionListCardStartedComponent implements OnInit {
const currentShippingDocument = this.store.selectSnapshot(
RemissionSelectors.getRemissionShippingDocument
);
this.remissionService
.addProductToShippingDocument({
remissionProcessId: this.remissionProcess.id,
@@ -154,6 +156,8 @@ export class RemissionListCardStartedComponent implements OnInit {
quantity: partialRemission.quantity,
placementType: partialRemission.placementType,
inStock: this.product.inStock,
predefinedRemissionQuantity:
partialRemission.predefinedRemissionQuantity,
})
.toPromise()
.then((err) => {

View File

@@ -1,37 +1,26 @@
<div
class="search-results"
[style.height]="containerHeight === 0 ? '100%' : containerHeight + 'px'"
infinite-scroll
[infiniteScrollDistance]="scrollDistance"
[infiniteScrollUpDistance]="scrollUpDistance"
[fromRoot]="true"
[infiniteScrollContainer]="'.remission-list-container'"
(scrolled)="onScrollDown()"
(scrolledUp)="onUp()"
#remissionListContainer
>
<ng-container *ngIf="!(isLoading$ | async); else spinner">
<ng-container *ngFor="let product of products; let last = last">
<ng-container *ngIf="product != null">
<app-remission-list-card
*ngIf="!started"
[last]="last"
[product]="product"
(deleteProduct)="deleteProduct(product)"
></app-remission-list-card>
<app-remission-list-card-started
[remissionProcess]="remissionProcess"
(updateShippingDocument)="updateShippingDocument.emit()"
*ngIf="started"
[product]="product"
></app-remission-list-card-started>
</ng-container>
<ng-template #loadingComponent>
<app-remission-list-card-loading></app-remission-list-card-loading>
</ng-template>
<div class="search-results" [style.height]="containerHeight === 0 ? '100%' : containerHeight + 'px'" infinite-scroll
[infiniteScrollDistance]="scrollDistance" [infiniteScrollUpDistance]="scrollUpDistance" [fromRoot]="true"
[infiniteScrollContainer]="'.remission-list-container'" (scrolled)="onScrollDown()" (scrolledUp)="onUp()"
#remissionListContainer>
<ng-container *ngIf="!(isLoading$ | async); else spinner"></ng-container>
<ng-container *ngFor="let product of products; let last = last">
<ng-container *ngIf="product != null">
<app-remission-list-card *ngIf="!started" [last]="last" [product]="product" [class.loading]="isLoading$ | async"
(deleteProduct)="deleteProduct(product)"></app-remission-list-card>
<app-remission-list-card-started [remissionProcess]="remissionProcess"
(updateShippingDocument)="updateShippingDocument.emit()" *ngIf="started" [product]="product"
[class.loading]="isLoading$ | async">
</app-remission-list-card-started>
</ng-container>
<ng-template #loadingComponent>
<app-remission-list-card-loading></app-remission-list-card-loading>
</ng-template>
</ng-container>
<ng-template #spinner>
<div class="spinner"></div>
</ng-template>
</div>
<ng-template #spinner>
<div class="spinner"></div>
</ng-template>

View File

@@ -44,3 +44,7 @@
cdk-virtual-scroll-viewport {
-webkit-overflow-scrolling: auto !important;
}
.loading {
display: none;
}

View File

@@ -1,9 +1,35 @@
import { Component, OnInit, Input, OnDestroy, ViewChild, Output, EventEmitter, ElementRef, ChangeDetectorRef } from '@angular/core';
import {
Component,
OnInit,
Input,
OnDestroy,
ViewChild,
Output,
EventEmitter,
ElementRef,
ChangeDetectorRef,
} from '@angular/core';
import { RemissionListDataSource } from './remission-list.datasource';
import { RemissionService, RemissionProcess, RemissionProduct, RemissionFilter } from '@isa/remission';
import {
RemissionService,
RemissionProcess,
RemissionProduct,
RemissionFilter,
} from '@isa/remission';
import { RemissionHelperService } from '../../services/remission-helper.service';
import { Subject, of, Subscription, Observable } from 'rxjs';
import { takeUntil, filter, take, catchError, share, debounceTime, first, map, switchMap, tap } from 'rxjs/operators';
import {
takeUntil,
filter,
take,
catchError,
share,
debounceTime,
first,
map,
switchMap,
tap,
} from 'rxjs/operators';
import { isNullOrUndefined } from 'util';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { Select, Store } from '@ngxs/store';
@@ -17,7 +43,8 @@ import { SetRemissionIsLoading } from 'apps/sales/src/app/core/store/actions/rem
})
export class RemissionListComponent implements OnInit, OnDestroy {
@ViewChild('scroller', { static: false }) scroller: CdkVirtualScrollViewport;
@ViewChild('remissionListContainer', { static: false }) remissionListContainer: ElementRef;
@ViewChild('remissionListContainer', { static: false })
remissionListContainer: ElementRef;
@Input() started = false;
@Input() hits: number;
@Input() remissionProcess: RemissionProcess;
@@ -28,9 +55,12 @@ export class RemissionListComponent implements OnInit, OnDestroy {
@Output() fullListLoaded = new EventEmitter();
@Output() scroll = new EventEmitter<void>();
@Output() updateShippingDocument = new EventEmitter<void>();
@Select(RemissionSelectors.getRemissionProcess) remissionProcess$: Observable<RemissionProcess>;
@Select(RemissionSelectors.getRemissionProcess) remissionProcess$: Observable<
RemissionProcess
>;
@Select(RemissionSelectors.getRemissionActiveFilters)
selectedFilters$: Observable<{ [key: string]: [] }>;
ds: RemissionListDataSource;
destroy$ = new Subject();
subscribedToProducts = false;
topScrolledOffset = 0;
@@ -60,7 +90,7 @@ export class RemissionListComponent implements OnInit, OnDestroy {
private remissionHelper: RemissionHelperService,
private store: Store,
private cdr: ChangeDetectorRef
) { }
) {}
ngOnInit() {
this.fetchProductPaged(this.page)
@@ -70,19 +100,44 @@ export class RemissionListComponent implements OnInit, OnDestroy {
});
this.subscribeToRefetchProducts();
this.subscribeToFetchLastProducts();
this.subscribeToResetCurrentPage();
this.subscribeToRefetchAllProducts();
}
ngOnDestroy() {
this.destroy$.next();
}
subscribeToResetCurrentPage() {
this.selectedFilters$
.pipe(takeUntil(this.destroy$))
.subscribe(() => this.resetPagesFetched());
}
private resetPagesFetched() {
this.page = 0;
this.isFullListLoaded = false;
this.isFullLoad = false;
}
subscribeToRefetchAllProducts() {
this.remissionHelper.refetchProducts$
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.refetchLoadedProducts(10);
});
}
subscribeToFetchLastProducts() {
this.fetchLastProducts$.pipe(takeUntil(this.destroy$)).subscribe((_) => {
// Increase number of takes to show all residual products (currently: 10)
const takeParam = this.pageSize;
const skipParam = Math.max(0, this.totalHits - takeParam);
if (this.products.length > this.pageSize + (this.totalHits % this.pageSize)) {
if (
this.products.length >
this.pageSize + (this.totalHits % this.pageSize)
) {
this.products = this.cleanResidualProducts(this.products);
}
@@ -95,15 +150,21 @@ export class RemissionListComponent implements OnInit, OnDestroy {
}
private filterUniqueProducts(products: RemissionProduct[]) {
return products.filter((product, i, allProducts) => allProducts.findIndex((t) => t.id === product.id) === i);
return products.filter(
(product, i, allProducts) =>
allProducts.findIndex((t) => t.id === product.id) === i
);
}
subscribeToRefetchProducts() {
this.refetchProducts$.pipe(debounceTime(10), takeUntil(this.destroy$)).subscribe((_) => this.fetchNewProducts());
this.refetchProducts$
.pipe(debounceTime(10), takeUntil(this.destroy$))
.subscribe((_) => this.fetchNewProducts());
}
onScrollDown() {
this.scroll.emit();
if (this.isFullListLoaded || this.isFullLoad) {
return;
}
@@ -112,6 +173,7 @@ export class RemissionListComponent implements OnInit, OnDestroy {
private fetchNewProducts() {
this.page += 1;
this.fetchProductPaged(this.page).subscribe();
}
@@ -182,46 +244,87 @@ export class RemissionListComponent implements OnInit, OnDestroy {
map((process) => process.id),
tap((_) => this.store.dispatch(new SetRemissionIsLoading(true))),
switchMap((remissionProcessId) =>
this.remissionService.getRemissionProducts({ remissionProcessId }).pipe(
filter((data) => !isNullOrUndefined(data)),
debounceTime(250)
)
this.remissionService
.getRemissionProducts({ remissionProcessId })
.pipe(
filter((data) => !isNullOrUndefined(data)),
debounceTime(250),
catchError((_) => {
this.store.dispatch(new SetRemissionIsLoading(false));
return [
{
hits: 0,
completed: true,
},
];
})
)
),
takeUntil(this.destroy$),
tap((_) => this.store.dispatch(new SetRemissionIsLoading(false)))
takeUntil(this.destroy$)
)
.subscribe((result: { skip?: number; take?: number; hits?: number; items: RemissionProduct[]; completed: boolean }) => {
console.error('Subscribe [RemissionProducts]', result);
.subscribe(
(result: {
skip?: number;
take?: number;
hits?: number;
items: RemissionProduct[];
completed: boolean;
}) => {
this.totalHits = result.hits;
this.totalHits = result.hits;
this.updateFullListLoaded({
totalHits: this.totalHits,
numberOfProductsFetched: this.products.length,
});
this.updateFullListLoaded({
totalHits: this.totalHits,
numberOfProductsFetched: this.products.length,
});
this.isLoading = false;
this.isLoading = false;
if ((this.page === 0 || result.skip === 0) && !this.isFullLoad) {
this.products = [];
}
this.remissionHelper.setRemissionListHits(result.hits || -1);
if (result.items && result.items.length > 0) {
if (!this.isItRefetch && !this.isFullLoad) {
this.products = this.filterUniqueProducts([...this.products, ...result.items]);
} else if (this.isItRefetch && !this.isFullLoad) {
this.products.splice(result.skip, result.items.length, ...result.items);
this.isItRefetch = false;
} else if (this.isFullLoad) {
this.products = this.filterUniqueProducts([...this.products, ...result.items]);
this.isFullLoad = false;
this.isFullListLoaded = true;
this.cdr.detectChanges();
this.fullListLoaded.emit();
if (
(this.page === 0 || result.skip === 0) &&
result.completed &&
result.hits > 0 &&
result.items
) {
if (!result.items.length) {
this.products = [...this.products];
} else {
this.products = [...result.items];
}
} else {
if ((this.page === 0 || result.skip === 0) && !this.isFullLoad) {
this.products = [];
}
this.remissionHelper.setRemissionListHits(result.hits || -1);
if (result.items && result.items.length > 0) {
if (!this.isItRefetch && !this.isFullLoad) {
this.products = this.filterUniqueProducts([
...this.products,
...result.items,
]);
} else if (this.isItRefetch && !this.isFullLoad) {
this.products.splice(
result.skip,
result.items.length,
...result.items
);
this.isItRefetch = false;
} else if (this.isFullLoad) {
this.products = this.filterUniqueProducts([
...this.products,
...result.items,
]);
this.isFullLoad = false;
this.isFullListLoaded = true;
this.cdr.detectChanges();
this.fullListLoaded.emit();
}
}
}
}
this.cdr.detectChanges();
});
this.store.dispatch(new SetRemissionIsLoading(false));
this.cdr.detectChanges();
}
);
}
public loadFullList() {
@@ -257,9 +360,19 @@ export class RemissionListComponent implements OnInit, OnDestroy {
this.isFullListLoaded = false;
}
private updateFullListLoaded(params: { totalHits: number; numberOfProductsFetched: number }) {
private updateFullListLoaded(params: {
totalHits: number;
numberOfProductsFetched: number;
}) {
if (this.page === 0) {
return;
}
this.page = Math.floor(params.numberOfProductsFetched / this.pageSize);
const allProductsFetched = params.numberOfProductsFetched >= this.totalHits;
allProductsFetched ? (this.isFullListLoaded = true) : (this.isFullListLoaded = false);
allProductsFetched
? (this.isFullListLoaded = true)
: (this.isFullListLoaded = false);
}
deleteProduct(product: RemissionProduct) {
@@ -276,7 +389,9 @@ export class RemissionListComponent implements OnInit, OnDestroy {
)
.subscribe((result) => {
if (result.result) {
this.products = this.products.filter((existingProduct) => existingProduct.id !== product.id);
this.products = this.products.filter(
(existingProduct) => existingProduct.id !== product.id
);
this.hits = this.products.length;
}
});

View File

@@ -1,13 +1,15 @@
<div
class="container"
*ngIf="
numberOfOpenShippingDocuments$ | async as numberOfOpenShippingDocuments
"
>
<div class="container">
<lib-icon class="icon" name="Icon_Warenbegleitschein_Red"></lib-icon>
<app-button alignLeft="true" (action)="openOverviewPage()">
<span
>Warenbegleitscheine ({{ numberOfOpenShippingDocuments$ | async }})</span
>Warenbegleitscheine
<span
*ngIf="
numberOfOpenShippingDocuments$
| async as numberOfOpenShippingDocuments
"
>({{ numberOfOpenShippingDocuments }})</span
></span
>
</app-button>
</div>

View File

@@ -0,0 +1,5 @@
// start:ng42.barrel
export * from './remission-required-capacities-widget.component';
export * from './remission-required-capacities-widget.module';
// end:ng42.barrel

View File

@@ -0,0 +1,21 @@
<div class="progress-wrapper" *ngIf="capacities">
<ng-container *ngFor="let item of capacities">
<div>
<lib-progress-bar
[progress]="item.utilized / item.available"
></lib-progress-bar>
</div>
<div>
<span
>{{ item.name }}: {{ item.utilized }} von {{ item.available }}
{{ item.label }}</span
>
</div>
</ng-container>
<div class="note" *ngIf="showOverflowMessage">
<span class="align-center"
>Wählen Sie die Abteilung aus, die Sie remittieren möchten.</span
>
</div>
</div>

View File

@@ -0,0 +1,15 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { CapacityType } from '@isa/remission';
@Component({
selector: 'app-remission-required-capacities-widget',
templateUrl: './remission-required-capacities-widget.component.html',
styleUrls: ['./remission-required-capacities-widget.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RemissionRequiredCapacitiesWidgetComponent {
@Input() capacities: CapacityType[];
@Input() showOverflowMessage: boolean;
constructor() {}
}

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { RemissionRequiredCapacitiesWidgetComponent } from './remission-required-capacities-widget.component';
import { CommonModule } from '@angular/common';
import { ProgressBarModule } from '@libs/ui';
@NgModule({
imports: [CommonModule, ProgressBarModule],
exports: [RemissionRequiredCapacitiesWidgetComponent],
declarations: [RemissionRequiredCapacitiesWidgetComponent],
providers: [],
})
export class RemissionRequiredCapacitiesWidgetModule {}

View File

@@ -0,0 +1,4 @@
// start:ng42.barrel
export * from './remission-scroll-button.component';
// end:ng42.barrel

View File

@@ -0,0 +1,8 @@
<div class="button" [class.reverse]="iconPosition === 'right'">
<div class="icon">
<ng-content select="lib-icon"></ng-content>
</div>
<div class="text">
<span>{{ text }}</span>
</div>
</div>

View File

@@ -0,0 +1,34 @@
.button {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background-color: white;
-moz-box-shadow: 0 0 30px #596470;
-webkit-box-shadow: 0 0 30px #596470;
box-shadow: 0 0 30px rgba(89, 100, 112, 0.5);
border-radius: 50px;
padding: 15px 20px;
cursor: pointer;
&.reverse {
display: flex;
flex-direction: row-reverse;
.icon {
margin-left: 8px;
}
}
.icon {
margin-top: 3px;
margin-right: 12px;
}
span {
font-family: 'Open Sans';
font-size: 18px;
font-weight: bold;
color: #000000;
}
}

View File

@@ -0,0 +1,14 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'app-remission-scroll-button',
templateUrl: './remission-scroll-button.component.html',
styleUrls: ['./remission-scroll-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RemissionScrollButtonComponent {
@Input() text: string;
@Input() iconPosition: 'left' | 'right' = 'left';
constructor() {}
}

View File

@@ -44,6 +44,7 @@ export class RemissionShippingDocumentCardComponent {
.then(() => {
this.remissionHelper.addRemissionListItem(this.product);
this.updateShippingDocument.emit();
this.remissionHelper.requestRefetchProducts();
});
}

View File

@@ -41,6 +41,8 @@
></app-remission-shipping-document-card>
<hr class="branch" *ngIf="!last" />
</ng-container>
<ng-content class="bottom"></ng-content>
</div>
<div class="empty-list" *ngIf="shippingDocument.products.length === 0">
<span>Neue Artikel aus der Remi-Liste hinzufügen</span>

View File

@@ -1,20 +1,14 @@
<div @fadeIn class="top" *ngIf="showTop$ | async">
<div class="button" (click)="scrollBottom()">
<div class="icon">
<lib-icon name="Hover_Arrow_Down"></lib-icon>
</div>
<div class="text">
<span>Zum Warenbegleitschein</span>
</div>
</div>
<app-remission-scroll-button
text="Zum Warenbegleitschein"
iconPosition="right"
(click)="scrolltoWarenbegleitschein()"
>
<lib-icon name="Hover_Arrow_Right"></lib-icon
></app-remission-scroll-button>
</div>
<div @fadeIn class="bottom" *ngIf="showBottom$ | async">
<div class="button" (click)="scrollTop()">
<div class="icon">
<lib-icon name="Hover_Arrow_Up"></lib-icon>
</div>
<div class="text">
<span>Nach oben</span>
</div>
</div>
<app-remission-scroll-button text="Nach oben" (click)="scrollTop()">
<lib-icon name="Hover_Arrow_Up"></lib-icon
></app-remission-scroll-button>
</div>

View File

@@ -17,28 +17,3 @@
top: 200px;
left: 0;
}
.button {
display: flex;
flex-direction: row;
justify-content: center;
background-color: white;
-moz-box-shadow: 0 0 30px #596470;
-webkit-box-shadow: 0 0 30px #596470;
box-shadow: 0 0 30px rgba(89, 100, 112, 0.5);
border-radius: 50px;
padding: 15px 20px;
cursor: pointer;
.icon {
margin-top: 3px;
margin-right: 12px;
}
span {
font-family: 'Open Sans';
font-size: 18px;
font-weight: bold;
color: #000000;
}
}

View File

@@ -10,7 +10,7 @@ import { fadeInAnimation } from '../../animations/fadeIn.animation';
})
export class RemissionToTopToBottomActionsComponent {
@Output() navigateToTop = new EventEmitter();
@Output() navigateToBottom = new EventEmitter();
@Output() navigateToWarenbegleitschein = new EventEmitter();
@Input() showTop$: BehaviorSubject<boolean>;
@Input() showBottom$: BehaviorSubject<boolean>;
@@ -21,7 +21,7 @@ export class RemissionToTopToBottomActionsComponent {
this.navigateToTop.emit();
}
scrollBottom() {
this.navigateToBottom.emit();
scrolltoWarenbegleitschein() {
this.navigateToWarenbegleitschein.emit();
}
}

View File

@@ -0,0 +1,9 @@
// start:ng42.barrel
export * from './capacity-type-client-wrapper.model';
export * from './remission-active-view.model';
export * from './remission-finishing-process-status.enum';
export * from './remission-resource-type.model';
export * from './resource-type-specific-model.model';
export * from './update-filter.model';
// end:ng42.barrel

View File

@@ -0,0 +1,4 @@
export enum RemissionActiveView {
PRODUCTS = 'Products',
WBS = 'Warenbegleitschein',
}

View File

@@ -5,4 +5,4 @@ import { RemissionListFilterModule } from './remission-list-filter/remission-lis
imports: [RemissionListFilterModule],
exports: [RemissionListFilterModule],
})
export class RemissionOverlaysModule {}
export class RemissionOverlaysModule { }

View File

@@ -1,12 +1,94 @@
import { trigger, transition, style, animate } from '@angular/animations';
import {
trigger,
transition,
style,
animate,
keyframes,
} from '@angular/animations';
export const topRightInOut = trigger('topRightInOut', [
transition(':enter', [
style({ width: '0', height: '0', marginLeft: 'auto', overflow: 'hidden', opacity: '0', borderRadius: '100% 0 100% 100%' }),
animate(300, style({ width: '100%', height: '100%', opacity: '1', borderRadius: '0%' })),
style({
width: '0',
height: '0',
marginLeft: 'auto',
overflow: 'hidden',
opacity: '0',
}),
animate(
300,
style({ width: '100%', height: '100%', opacity: '1', borderRadius: '0%' })
),
]),
transition('* => out', [
style({ width: '100%', height: '100%', marginLeft: 'auto', opacity: '1', borderRadius: '0%', overflow: 'hidden' }),
animate(300, style({ width: '0', height: '0', overflow: 'hidden', opacity: '0', borderRadius: '100% 0 100% 100%' })),
style({
width: '100%',
height: '100%',
marginLeft: 'auto',
opacity: '1',
borderRadius: '0%',
overflow: 'hidden',
}),
animate(
300,
style({
width: '0',
height: '0',
overflow: 'hidden',
opacity: '0',
})
),
]),
]);
export const slideIn = trigger('slideIn', [
transition(':enter', [
style({
transform: 'translateX(100%)',
marginLeft: 'auto',
opacity: '0',
height: '100%',
}),
animate(
430,
keyframes([
style({
opacity: '1',
transform: 'translateX(65%)',
offset: 0.35,
}),
style({
transform: 'translateX(0)',
height: '100%',
opacity: '1',
offset: 1,
}),
])
),
]),
transition('* => out', [
style({
transform: 'translateX(0)',
height: '100%',
opacity: '1',
marginLeft: 'auto',
}),
animate(
430,
keyframes([
style({
opacity: '1',
transform: 'translateX(65%)',
offset: 0.65,
}),
style({
width: '0',
height: '100%',
transform: 'translateX(100%)',
opacity: '0',
offset: 1,
}),
])
),
]),
]);

View File

@@ -1,46 +1,53 @@
<div
class="bg-branch"
[@topRightInOut]="animate"
(@topRightInOut.done)="closeAnimationDone()"
(headerClicked)="close()"
[@slideIn]="animate"
(@slideIn.done)="closeAnimationDone()"
>
<div class="filter-container">
<button type="button" class="btn-close" (click)="close()">
<lib-icon
height="21px"
class="close-icon"
name="close"
name="close-branch"
alt="close"
></lib-icon>
</button>
<h2 class="filter-title">Filter</h2>
<app-remission-uberlauf-capacities
*ngIf="(remissionSource$ | async) === 'ueberlauf'"
>
</app-remission-uberlauf-capacities>
<div class="selection-container" *ngIf="filtersByGroup$ | async as filters">
<app-selected-filter-options
[value]="filters"
(filterChanged)="updateFilter($event)"
<div>
<app-remission-required-capacities-widget
*ngIf="(remissionSource$ | async) === 'ueberlauf'"
[showOverflowMessage]="showOverflowInitialMessage$ | async"
[capacities]="capacities$ | async"
>
</app-selected-filter-options>
<app-filter-group
*ngIf="filters"
[value]="filters"
[selected]="selectedFilter$ | async"
(valueChanged)="onFiltersChange($event)"
(valueGroupChanged)="updateLastGroupChanged($event)"
></app-filter-group>
</app-remission-required-capacities-widget>
</div>
<ng-container *ngIf="filtersByGroup$ | async as filters">
<div>
<app-selected-filter-options
[value]="filters"
(filterChanged)="onFiltersChange($event)"
>
</app-selected-filter-options>
</div>
<div class="selection-container" #selectionContainer>
<app-filter-group
*ngIf="filters"
[value]="filters"
[selected]="selectedFilter$ | async"
(valueChanged)="onFiltersChange($event)"
(valueGroupChanged)="updateLastGroupChanged($event)"
>
</app-filter-group>
</div>
</ng-container>
<div class="apply-filter-wrapper">
<button
class="isa-btn isa-btn-primary isa-btn-pill btn-apply-filters text-nowrap"
(click)="applyFilters()"
>
Filter anwenden
</button>
</div>
</div>
</div>
<button
@fadeIn
*ngIf="pendingFilters$ | async"
class="isa-btn isa-btn-primary isa-btn-medium isa-btn-pill btn-apply-filters"
(click)="applyFilters()"
>
Filter anwenden
</button>

View File

@@ -1,6 +1,16 @@
@import 'variables';
@import 'mixins/media';
:host {
display: block;
height: 100%;
width: 100%;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
.bg-branch {
height: 100%;
@@ -13,12 +23,21 @@
}
}
.selection-container {
overflow: scroll;
}
.filter-container {
height: 95%;
max-height: 95%;
position: relative;
margin-left: auto;
margin-right: auto;
max-width: $max-content-width;
overflow: hidden;
box-sizing: border-box;
display: grid;
grid-template-rows: auto auto 1fr auto;
grid-template-rows: auto auto auto 1fr;
}
.btn-close {
@@ -28,6 +47,7 @@
background: none;
right: 20px;
top: 20px;
cursor: pointer;
}
.filter-title {
@@ -35,14 +55,30 @@
margin-top: 25px;
}
.apply-filter-wrapper {
text-align: center;
padding-bottom: 5px;
}
.btn-apply-filters {
position: sticky;
left: 50%;
bottom: 30px;
transform: translateX(-50%);
padding: 18px 27px 20px 27px;
border-radius: 57px;
font-size: 18px;
}
.text-nowrap {
white-space: nowrap;
}
:host ::ng-deep .select-filter-options-content {
max-height: 400px;
overflow: scroll;
@include mq-tablet-landscape() {
max-height: 200px;
}
@include mq-desktop() {
max-height: 300px;
}
}

View File

@@ -1,51 +1,129 @@
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import {
Component,
ChangeDetectionStrategy,
OnInit,
ViewChild,
ElementRef,
OnDestroy,
Renderer2,
ChangeDetectorRef,
} from '@angular/core';
import { IsaOverlayRef } from 'apps/sales/src/app/core/overlay/isa-overlay-ref';
import { Select } from '@ngxs/store';
import { RemissionSelectors } from 'apps/sales/src/app/core/store/selectors/remission.selectors';
import { Observable, BehaviorSubject } from 'rxjs';
import { Observable, BehaviorSubject, interval } from 'rxjs';
import {
RemissionProcess,
RemissionSelectedFilters,
RemissionService,
CapacityType,
} from '@isa/remission';
import { RemissionFilterService } from '../../services/remission-filter.service';
import { fadeInAnimation } from '../../animations/fadeIn.animation';
import { Filter, SelectFilter } from '../../../filter';
import { Filter, SelectFilter, SelectFilterOption } from '../../../filter';
import { RemissionResourceType } from '../../models/remission-resource-type.model';
import { trigger, transition, style, animate } from '@angular/animations';
import { topRightInOut } from './remission-list-filter.animations';
import { tap } from 'rxjs/operators';
import { slideIn } from './remission-list-filter.animations';
import {
withLatestFrom,
filter,
switchMap,
map,
throttleTime,
startWith,
} from 'rxjs/operators';
import { isNullOrUndefined } from 'util';
@Component({
selector: 'app-remission-list-filter',
templateUrl: 'remission-list-filter.component.html',
styleUrls: ['remission-list-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fadeInAnimation, topRightInOut],
animations: [slideIn],
})
export class RemissionListFilterComponent implements OnInit {
export class RemissionListFilterComponent implements OnInit, OnDestroy {
animate = 'in';
@ViewChild('selectionContainer', { static: false })
selectionContainer: ElementRef;
checkHeightTimerSub = interval(100)
.pipe(startWith(0), throttleTime(250))
.subscribe(() => this.updateHeight());
@Select(RemissionSelectors.getRemissionProcess)
remissionProcess$: Observable<RemissionProcess>;
@Select(RemissionSelectors.getRemissionSource)
remissionSource$: Observable<RemissionResourceType>;
@Select(RemissionSelectors.getRemissionFilters)
remissionFilter$: Observable<RemissionSelectedFilters>;
@Select(RemissionSelectors.getRemissionSupplierId)
remissionSupplierId$: Observable<number>;
@Select(RemissionSelectors.getRemissionActiveFilters)
activeFilters$: Observable<{
[filterId: string]: string[];
}>;
filter$: Observable<Filter[]>;
filtersByGroup$: Observable<Filter[]>;
pendingFilters$ = new BehaviorSubject<Filter[]>(null);
selectedFilter$: Observable<Filter>;
capacities$: Observable<CapacityType[]>;
showOverflowInitialMessage$: Observable<boolean>;
constructor(
public overlayRef: IsaOverlayRef,
private filterService: RemissionFilterService
private filterService: RemissionFilterService,
private remissionService: RemissionService,
private renderer: Renderer2,
private cdr: ChangeDetectorRef
) {}
ngOnInit() {
this.getAvailableFilters();
this.getSelectedFilter();
this.getPendingFilters();
this.getRequiredCapacities();
this.initShowOverflowMessage();
}
ngOnDestroy(): void {
this.checkHeightTimerSub.unsubscribe();
}
updateHeight() {
if (this.selectionContainer) {
const element: HTMLElement = this.selectionContainer.nativeElement;
const selectFilterElement: HTMLElement = element.querySelector(
'app-select-filter'
);
if (isNullOrUndefined(selectFilterElement)) {
return;
}
const selectFilterOptionsContent: HTMLElement = selectFilterElement.querySelector(
'.select-filter-options-content'
);
if (isNullOrUndefined(selectFilterOptionsContent)) {
return;
}
const height =
element.clientHeight -
(selectFilterElement.clientHeight -
selectFilterOptionsContent.clientHeight);
if (selectFilterOptionsContent.offsetHeight !== height) {
this.renderer.setStyle(
selectFilterOptionsContent,
'max-height',
`${height}px`
);
this.cdr.markForCheck();
}
}
}
getAvailableFilters() {
@@ -57,6 +135,60 @@ export class RemissionListFilterComponent implements OnInit {
this.selectedFilter$ = this.filterService.lastFilterGroupChanged$;
}
getPendingFilters() {
this.pendingFilters$ = this.filterService.pendingFilters$;
}
getRequiredCapacities() {
this.capacities$ = this.pendingFilters$.pipe(
withLatestFrom(this.remissionSource$),
filter(([_, source]) => source === 'ueberlauf'),
map(([pendingFilters]) =>
this.filterService.mapToRemissionFilter(pendingFilters || [])
),
withLatestFrom(this.remissionSupplierId$),
switchMap(([selectedFilters, supplierId]) =>
this.remissionService.getCapacities({ selectedFilters, supplierId })
)
);
}
initShowOverflowMessage() {
this.showOverflowInitialMessage$ = this.pendingFilters$.pipe(
map((pendingFilters) => [
pendingFilters,
this.filterService.flattenPendingFilters(pendingFilters),
]),
map(
([filters, flattenedFilters]: [
SelectFilterOption[],
SelectFilterOption[]
]) => {
if (
isNullOrUndefined(flattenedFilters) ||
isNullOrUndefined(filters)
) {
return true;
}
const departmentFilters = filters.find((f) => f.id === 'abteilungen');
if (!departmentFilters || !departmentFilters.options) {
return true;
}
const selectedFilters = flattenedFilters.filter((f) => !!f.selected);
const result = !selectedFilters.some(
(f) => !!departmentFilters.options.find((opt) => opt.id === f.id)
);
return result;
}
)
);
}
updateFilter(updatedFilters: SelectFilter[]) {
this.filterService.updateFilters(updatedFilters);
}
@@ -66,7 +198,9 @@ export class RemissionListFilterComponent implements OnInit {
}
applyFilters() {
this.filterService.updateFilters(this.pendingFilters$.value);
if (this.pendingFilters$.value) {
this.filterService.updateFilters(this.pendingFilters$.value);
}
this.close();
}

View File

@@ -7,15 +7,19 @@ import { SelectedFilterOptionsModule } from '../../../filter';
import { FilterGroupModule } from '../../../filter/filter-group/filter-group.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RemissionUeberlaufCapacitiesModule } from '../../pages/remission-list-create/ueberlauf-capacities';
import { SharedModule } from 'apps/sales/src/app/shared/shared.module';
import { RemissionRequiredCapacitiesWidgetModule } from '../../components/remission-required-capacities-widget';
@NgModule({
imports: [
CommonModule,
SharedModule,
IconModule,
SelectedFilterOptionsModule,
FilterGroupModule,
BrowserAnimationsModule,
RemissionUeberlaufCapacitiesModule
RemissionUeberlaufCapacitiesModule,
RemissionRequiredCapacitiesWidgetModule,
],
exports: [RemissionListFilterComponent],
declarations: [RemissionListFilterComponent],

View File

@@ -1,35 +1,41 @@
<div
class="list"
*ngIf="{
productsData: remissionProductsData$ | async,
remissionProcess: remissionProcess$ | async,
status: remissionProcessStatuses$ | async,
isLoading: isLoading$ | async
} as data"
class="wrapper"
[class.slide-left]="_slideOutOfView"
[class.remove-from-flow]="_removeFromDocumentFlow"
>
<ng-container *ngIf="data.productsData && data.productsData.hits">
<div #remissionListHitsRef class="hits" *ngIf="!data.isLoading">
<span>{{ data.productsData.hits }} Titel</span>
</div>
<div
class="list"
*ngIf="{
productsData: remissionProductsData$ | async,
remissionProcess: remissionProcess$ | async,
status: remissionProcessStatuses$ | async,
isLoading: isLoading$ | async
} as data"
>
<ng-container *ngIf="data.productsData && data.productsData.hits">
<div #remissionListHitsRef class="hits" *ngIf="!data.isLoading">
<span>{{ data.productsData.hits }} Titel</span>
</div>
<div class="ng-container" *ngIf="data.remissionProcess && data.status">
<app-remission-list
[started]="data.status.started"
[remissionProcess]="data.remissionProcess"
[hits]="data.productsData.hits"
[isLoading$]="isLoading$"
[refetchProducts$]="refetchRemissionProductsOnScrollUp$"
[fetchLastProducts$]="fetchLastProducts$"
(updateShippingDocument)="updateShippingDocument()"
#remissionList
></app-remission-list>
<app-remission-to-top-to-bottom-actions
(navigateToTop)="navigateToTop()"
(navigateToBottom)="navigateToBottom()"
[showTop$]="showJumpToTop$"
[showBottom$]="showJumpToBottom$"
#toTopToBottomActions
></app-remission-to-top-to-bottom-actions>
</div>
</ng-container>
<div class="ng-container" *ngIf="data.remissionProcess && data.status">
<app-remission-list
[started]="data.status.started"
[remissionProcess]="data.remissionProcess"
[hits]="data.productsData.hits"
[isLoading$]="isLoading$"
[refetchProducts$]="refetchRemissionProductsOnScrollUp$"
[fetchLastProducts$]="fetchLastProducts$"
(updateShippingDocument)="updateShippingDocument()"
#remissionList
></app-remission-list>
<app-remission-to-top-to-bottom-actions
(navigateToTop)="navigateToTop()"
(navigateToWarenbegleitschein)="navigateToWarenbegleitschein()"
[showTop$]="showJumpToTop$"
[showBottom$]="showJumpToBottom$"
#toTopToBottomActions
></app-remission-to-top-to-bottom-actions>
</div>
</ng-container>
</div>
</div>

View File

@@ -9,5 +9,28 @@
padding-right: 15px;
}
.ng-container {
margin-bottom: 125px;
}
margin-top: 8px;
margin-bottom: 16px;
transition: all 1s ease-in-out;
}
.wrapper {
margin-bottom: 16px;
transition: all 1s ease-in-out;
&.slide-left {
position: relative;
transform: translateX(-100vw);
transition: all 1.5s ease-in-out;
&.remove-from-flow {
position: absolute;
opacity: 0;
transition: all 0.6s ease-in-out;
}
}
}

View File

@@ -9,18 +9,46 @@ import {
QueryList,
ElementRef,
AfterViewInit,
ChangeDetectorRef,
} from '@angular/core';
import { Observable, of, Subject, BehaviorSubject, combineLatest, merge } from 'rxjs';
import { RemissionProduct, RemissionService, RemissionProcess } from '@isa/remission';
import {
Observable,
of,
Subject,
BehaviorSubject,
combineLatest,
merge,
} from 'rxjs';
import {
RemissionProduct,
RemissionService,
RemissionProcess,
} from '@isa/remission';
import { Store, Select } from '@ngxs/store';
import { RemissionSelectors } from 'apps/sales/src/app/core/store/selectors/remission.selectors';
import { map, switchMap, filter, catchError, takeUntil, take, distinctUntilChanged, debounceTime, tap, skip } from 'rxjs/operators';
import {
map,
switchMap,
filter,
catchError,
takeUntil,
take,
distinctUntilChanged,
debounceTime,
tap,
skip,
} from 'rxjs/operators';
import { RemissionListComponent } from '../../../components/remission-list';
import { RemissionResourceType } from '../../../models/remission-resource-type.model';
import { isNullOrUndefined } from 'util';
import { RemissionProcessStatuses } from 'apps/sales/src/app/core/models/remission-process-statuses.model';
import { RemissionToTopToBottomActionsComponent } from '../../../components/remission-to-top-to-bottom-actions';
import { UpdateShippingDocuent } from 'apps/sales/src/app/core/store/actions/remission.actions';
import {
UpdateShippingDocuent,
SetRemissionActiveView,
} from 'apps/sales/src/app/core/store/actions/remission.actions';
import { RemissionActiveView } from '../../../models';
import { RemissionHelperService } from '../../../services';
@Component({
selector: 'app-remission-product-list',
@@ -28,26 +56,50 @@ import { UpdateShippingDocuent } from 'apps/sales/src/app/core/store/actions/rem
styleUrls: ['./product-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RemissionProductListComponent implements OnInit, OnDestroy, AfterViewInit {
export class RemissionProductListComponent
implements OnInit, OnDestroy, AfterViewInit {
@ViewChild('toTopToBottomActions', { static: false })
toTopToBottomActions: RemissionToTopToBottomActionsComponent;
@ViewChild('remissionList', { static: false }) remissionList: RemissionListComponent;
@ViewChild('remissionList', { static: false })
remissionList: RemissionListComponent;
@ViewChildren('remissionListHitsRef') remissionHitsRef: QueryList<ElementRef>;
@Input() remissionListContainer: HTMLElement;
@Input() shippingDocumentContainer: HTMLElement;
@Input() containerScrolled$: Observable<Event> = new Subject<Event>().asObservable();
@Input() containerScrolled$: Observable<Event> = new Subject<
Event
>().asObservable();
@Input() shippingDocumentRef: QueryList<ElementRef>;
@Input() set slideOutOfView(shouldBeOutOfView: boolean) {
if (!shouldBeOutOfView) {
this._removeFromDocumentFlow = false;
} else {
setTimeout(() => {
this._removeFromDocumentFlow = true;
this.cdr.detectChanges();
}, 400);
}
this._slideOutOfView = shouldBeOutOfView;
}
_slideOutOfView: boolean;
_removeFromDocumentFlow = false;
@Select(RemissionSelectors.getIsLoading) isLoading$: Observable<boolean>;
@Select(RemissionSelectors.getRemissionProcess) remissionProcess$: Observable<RemissionProcess>;
@Select(RemissionSelectors.getRemissionSource) source$: Observable<RemissionResourceType>;
@Select(RemissionSelectors.getRemissionProcess) remissionProcess$: Observable<
RemissionProcess
>;
@Select(RemissionSelectors.getRemissionSource) source$: Observable<
RemissionResourceType
>;
@Select(RemissionSelectors.getRemissionProcessStatuses)
remissionProcessStatuses$: Observable<RemissionProcessStatuses>;
@Select(RemissionSelectors.getRemissiontarget)
remissionTarget$: Observable<string>;
@Select(RemissionSelectors.getRemissionSource)
remissionSource$: Observable<RemissionResourceType>;
@Select(RemissionSelectors.getActiveView)
activeView$: Observable<RemissionActiveView>;
refetchRemissionProductsOnScrollUp$ = new Subject<void>();
destroy$ = new Subject<void>();
@@ -86,11 +138,17 @@ export class RemissionProductListComponent implements OnInit, OnDestroy, AfterVi
// Feature Flag: Indicates whether to load entire remission list on navigate to bottom
loadAllProductsOnNavigateToBottom = false;
constructor(private store: Store, private remissionService: RemissionService) { }
constructor(
private store: Store,
private remissionService: RemissionService,
private remissionHelperService: RemissionHelperService,
private cdr: ChangeDetectorRef
) {}
ngOnInit() {
this.initProducts();
this.initScrollHandler();
this.initScrollRestoration();
}
ngAfterViewInit() {
@@ -104,9 +162,20 @@ export class RemissionProductListComponent implements OnInit, OnDestroy, AfterVi
this.destroy$.complete();
}
initScrollRestoration() {
this.activeView$.pipe(takeUntil(this.destroy$)).subscribe((activeView) => {
if (activeView === 'Warenbegleitschein') {
this.resetRemissionListScrollPosition(true);
}
});
}
initProducts() {
this.remissionProductsData$ = this.remissionProcess$.pipe(
filter((process) => !isNullOrUndefined(process) && !isNullOrUndefined(process.id)),
filter(
(process) =>
!isNullOrUndefined(process) && !isNullOrUndefined(process.id)
),
take(1),
map((process) => process.id),
switchMap((remissionProcessId) =>
@@ -114,7 +183,10 @@ export class RemissionProductListComponent implements OnInit, OnDestroy, AfterVi
filter((data) => !isNullOrUndefined(data)),
map((data) => {
if (!data.hits) {
data.hits = (this.latestRemissionProductsData$.value && this.latestRemissionProductsData$.value.hits) || 0;
data.hits =
(this.latestRemissionProductsData$.value &&
this.latestRemissionProductsData$.value.hits) ||
0;
}
return data;
@@ -154,7 +226,10 @@ export class RemissionProductListComponent implements OnInit, OnDestroy, AfterVi
map((process) => process.id),
take(1)
)
.subscribe((remissionProcessId) => this.store.dispatch(new UpdateShippingDocuent(remissionProcessId)));
.subscribe((remissionProcessId) => {
this.remissionHelperService.requestRefetchProducts();
this.store.dispatch(new UpdateShippingDocuent(remissionProcessId));
});
}
navigateToTop() {
@@ -164,38 +239,16 @@ export class RemissionProductListComponent implements OnInit, OnDestroy, AfterVi
});
}
navigateToBottom() {
this.remissionList.isFullLoad = true;
if (this.loadAllProductsOnNavigateToBottom) {
return this.remissionList.loadFullList().subscribe();
}
this.navigateToWarenbegleitschein();
}
private navigateToWarenbegleitschein() {
this.fetchLastProducts$.next();
setTimeout(
() =>
this.shippingDocumentContainer.scrollIntoView({
block: 'end',
behavior: 'smooth',
}),
1000
);
setTimeout(() => {
this.viewShippingDocumentFromJumpToBottom = true;
this.remissionList.isFullLoad = false;
}, 4000);
navigateToWarenbegleitschein() {
this.store.dispatch(new SetRemissionActiveView(RemissionActiveView.WBS));
}
scrollHandler(target: HTMLElement) {
const { clientHeight, scrollHeight, scrollTop } = target;
if (this.refetchProductsOnScroll()) {
const shippingContainerHeight = this.shippingDocumentContainer.clientHeight;
const shippingContainerHeight = this.shippingDocumentContainer
.clientHeight;
if (
this.shouldRefreshProducts({
clientHeight,
@@ -233,7 +286,8 @@ export class RemissionProductListComponent implements OnInit, OnDestroy, AfterVi
const pixelsScrolledUpToTriggerRefresh = 150;
return (
scrollTop + clientHeight - scrollHeight < pixelsScrolledUpToTriggerRefresh - shippingContainerHeight &&
scrollTop + clientHeight - scrollHeight <
pixelsScrolledUpToTriggerRefresh - shippingContainerHeight &&
!!this.viewShippingDocumentFromJumpToBottom
);
}
@@ -244,7 +298,10 @@ export class RemissionProductListComponent implements OnInit, OnDestroy, AfterVi
this.refetchRemissionProductsOnScrollUp$.next();
}
private setScrollToTopBotomVisibility(topValue: boolean, bottomValue: boolean) {
private setScrollToTopBotomVisibility(
topValue: boolean,
bottomValue: boolean
) {
this.showJumpToTop$.next(topValue);
this.showJumpToBottom$.next(bottomValue);
}
@@ -255,21 +312,25 @@ export class RemissionProductListComponent implements OnInit, OnDestroy, AfterVi
}
private setUpIntersectionObservers() {
this.remissionHitsRef.changes.pipe(this.returnFirstElement).subscribe((element) => {
this.subscribeToIntersectionSub({
element,
sub: this.isNumberOfHitsVisible$,
type: 'top',
this.remissionHitsRef.changes
.pipe(this.returnFirstElement)
.subscribe((element) => {
this.subscribeToIntersectionSub({
element,
sub: this.isNumberOfHitsVisible$,
type: 'top',
});
});
});
this.shippingDocumentRef.changes.pipe(this.returnFirstElement).subscribe((element) => {
this.subscribeToIntersectionSub({
element,
sub: this.isShippingDocumentVisible$,
type: 'bottom',
this.shippingDocumentRef.changes
.pipe(this.returnFirstElement)
.subscribe((element) => {
this.subscribeToIntersectionSub({
element,
sub: this.isShippingDocumentVisible$,
type: 'bottom',
});
});
});
}
private returnFirstElement(el: Observable<any>): Observable<any> {
@@ -279,7 +340,11 @@ export class RemissionProductListComponent implements OnInit, OnDestroy, AfterVi
);
}
private subscribeToIntersectionSub(params: { element: Element; sub: BehaviorSubject<boolean>; type: 'top' | 'bottom' }) {
private subscribeToIntersectionSub(params: {
element: Element;
sub: BehaviorSubject<boolean>;
type: 'top' | 'bottom';
}) {
const { element, sub, type } = params;
const unsubscribe = this.intersectionObserverSubs.get(type);
@@ -287,7 +352,10 @@ export class RemissionProductListComponent implements OnInit, OnDestroy, AfterVi
unsubscribe();
}
this.intersectionObserverSubs.set(type, this.initIntersectionObserver(element, type));
this.intersectionObserverSubs.set(
type,
this.initIntersectionObserver(element, type)
);
this.intersectionObserver$
.pipe(
@@ -306,11 +374,13 @@ export class RemissionProductListComponent implements OnInit, OnDestroy, AfterVi
}
private initIntersectionObserver(element: Element, type: 'top' | 'bottom') {
const intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
this.intersectionObserver$.next({ entry, observer, type });
});
});
const intersectionObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
this.intersectionObserver$.next({ entry, observer, type });
});
}
);
intersectionObserver.observe(element);
@@ -345,22 +415,47 @@ export class RemissionProductListComponent implements OnInit, OnDestroy, AfterVi
this.isLoading$
)
.pipe(
map(([isNumberOfHitsVisible, isShippingDocumentVisible, hits, isLoading]) => {
if (!hits || hits === -1 || !!isLoading) {
map(
([
isNumberOfHitsVisible,
isShippingDocumentVisible,
hits,
isLoading,
]) => {
if (!hits || hits === -1 || !!isLoading) {
return {
showTop: false,
showBottom: false,
};
}
return {
showTop: false,
showBottom: false,
showTop: !isShippingDocumentVisible,
showBottom: !isNumberOfHitsVisible,
};
}
return {
showTop: !isShippingDocumentVisible,
showBottom: !isNumberOfHitsVisible,
};
}),
),
takeUntil(this.destroy$),
debounceTime(350)
)
.subscribe(({ showTop, showBottom }) => this.setScrollToTopBotomVisibility(showTop, showBottom));
.subscribe(({ showTop, showBottom }) =>
this.setScrollToTopBotomVisibility(showTop, showBottom)
);
}
private resetRemissionListScrollPosition(
waitForAnimationToComplete: boolean = false
) {
if (!this.remissionHitsRef || !this.remissionHitsRef.first) {
return;
}
const container = this.remissionHitsRef.first.nativeElement;
if (waitForAnimationToComplete) {
setTimeout(() => {
container.scrollTop = 0;
}, 300);
}
}
}

View File

@@ -27,21 +27,26 @@
<app-remission-suppliers-filter></app-remission-suppliers-filter>
</div>
<div *ngIf="data.selectedSource === 'ueberlauf'; else callToActions">
<div class="pb-16" *ngIf="data.selectedSource === 'ueberlauf'">
<app-remission-uberlauf-capacities></app-remission-uberlauf-capacities>
</div>
<ng-template #callToActions>
<ng-container *ngIf="showCtAs$ | async">
<app-remission-list-actions
(startRemission)="openStartRemissionDialog()"
(addProduct)="openAddProductToRemission()"
(scanProduct)="searchProductScannedResult($event)"
[isNative]="isNative"
[isSafari]="isSafari"
status="created"
*ngIf="data.remissionListHits > 0"
></app-remission-list-actions>
</ng-template>
<app-remission-list-start-action
@fadeInWithDelay
*ngIf="!data.isLoading"
class="bottom"
(startRemission)="openStartRemissionDialog()"
></app-remission-list-start-action>
</ng-container>
</ng-container>
<ng-template #loadFilters>

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