Compare commits

...

57 Commits

Author SHA1 Message Date
Lorenz Hilpert
9668aa8829 Format 2022-10-12 18:34:48 +02:00
Lorenz Hilpert
c49c428688 Newsletter text entfernt 2022-10-12 18:34:00 +02:00
Lorenz Hilpert
46b963c2d1 Ausblenden des Newsletter-Textes bei Gastkunden 2022-10-12 18:31:11 +02:00
Lorenz Hilpert
68a2eab425 Merged PR 1396: #3502 Guard bei navigation auf P4M Anlage angepasst
#3502 Guard bei navigation auf P4M Anlage angepasst
2022-10-12 15:02:17 +00:00
Andreas Schickinger
886f063d1b Fix für Anzeigefehler im SectionToggle 2022-10-12 16:40:29 +02:00
Andreas Schickinger
ed6ee36509 Merged PR 1395: #3481 Remission Filter zwischenspeichern bei Wechsel zwischen Pflicht- und Ab...
#3481 Remission Filter zwischenspeichern bei Wechsel zwischen Pflicht- und Abteilungsremission

Related work items: #3481
2022-10-12 13:57:28 +00:00
Andreas Schickinger
ab345dae0d Merged PR 1393: #3514 TK Fehlermeldung fix
#3514 TK Fehlermeldung fix

Related work items: #3514
2022-10-12 13:39:29 +00:00
Lorenz Hilpert
fbb1e6c4a2 Merged PR 1394: #3511 Addressvalidierung für Onlinekonto aktiviert
#3511 Addressvalidierung für Onlinekonto aktiviert
2022-10-12 10:38:35 +00:00
Lorenz Hilpert
4ede9226b4 Merged PR 1392: #3484 Keep Userstates on Tab Changes
#3484 Keep Userstates on Tab Changes
2022-10-11 15:05:04 +00:00
Andreas Schickinger
fbfecbd8ae Merged PR 1391: TK Suche Timespan Fallback angepasst
TK Suche Timespan von 6 Monate Vergangenheit/Zukunft auf 6 Wochen Vergangenheit, 2 Wochen Zukunft geändert. Wird verwendet, wenn kein Timespan Filter gesetzt ist

Related work items: #3422
2022-10-11 14:32:30 +00:00
Lorenz Hilpert
3c4612d15c Merged PR 1389: #3307 Filter Wird Nicht Gerendert
#3307 Filter Wird Nicht Gerendert
2022-10-11 13:33:10 +00:00
Andreas Schickinger
7fa2e7862d Merged PR 1388: #3508 Bestellbestätigung "zur Warenausgabe": Prüfung auf enabled bei CustomerFeature B2B entfernt
#3508 Prüfung auf enabled bei CustomerFeature B2B entfernt

Related work items: #3508
2022-10-11 12:16:07 +00:00
Andreas Schickinger
114267362c Merged PR 1384: #3503 TK Sortierung umgedreht, #3504 Scroll Fix
#3503 TK Sortierung umgedreht, #3504 Scroll Fix

Related work items: #3503, #3504
2022-10-11 11:06:01 +00:00
Lorenz Hilpert
4050e9605d FIX Routing Kundenalage 2022-10-11 10:41:25 +02:00
Andreas Schickinger
fdd5373aaf Merged PR 1385: #3493 TK Modal Header Abstand erhöht
#3493 TK Modal Header Abstand erhöht

Related work items: #3493
2022-10-10 15:59:04 +00:00
Andreas Schickinger
2dfe7ec05b Merged PR 1386: #3492 TK Drucken in der Trefferliste fix
#3492 TK Drucken in der Trefferliste fix

Related work items: #3492
2022-10-10 15:58:42 +00:00
Lorenz Hilpert
82513b5dde Merged PR 1387: Kubi Kundenanlage und Kundenkarte
Related work items: #3230, #3233
2022-10-10 15:57:50 +00:00
Lorenz Hilpert
14d1bb6ac8 Merged PR 1383: HFI Geschnakkarte
Related work items: #3496
2022-10-07 14:20:05 +00:00
Lorenz Hilpert
d589c94681 Fix Kundendatenerfassen - added modifier add-loyality-card 2022-10-06 12:42:45 +02:00
Lorenz Hilpert
eca19bb507 Updateing Customer for P4M 2022-10-06 10:47:10 +02:00
Lorenz Hilpert
282ff30b3e #3494 Upgrade Ava API auf v6 2022-10-05 13:58:02 +02:00
Lorenz Hilpert
5d0b810674 Upgrade catsearch API auf V6 2022-10-05 13:54:12 +02:00
Lorenz Hilpert
e32482c634 Type Kundendaten erfassen speichern aufruf 2022-10-05 13:25:43 +02:00
Lorenz Hilpert
650026b0c0 Es existiert bereits ein Onlinekonto 2022-10-04 17:48:15 +02:00
Lorenz Hilpert
cd25d6da38 #3489 fonts 2022-10-04 12:10:17 +02:00
Andreas Schickinger
9dd0954967 Merged PR 1381: #3326 Tätigkeitskalender Suchfunktion und neues Design
Tätigkeitskalender Suchfunktion und neues Design

Zusätzliche Änderungen im PR:
- Anpassungen für GitHub package Zugriff
- UiModalRef um ein afterChanged$ erweitert, um nach dem schließen zu erkennen ob ein reload notwendig ist
- ui-loader funktionierte nicht bei verwendung von ui-scroll-container mit useLoadAnimation false
- ui-skeleton-loader um Template für TK Listenitem erweitert

Related work items: #3419, #3420, #3421, #3422, #3423
2022-10-04 09:42:49 +00:00
Lorenz Hilpert
fdaceb9bf8 Merged PR 1382: Kubi
Related work items: #3228, #3230, #3289, #3467, #3471, #3478
2022-09-30 13:48:19 +00:00
Lorenz Hilpert
4ab3a3b3cf #3307 Fixed With Filter 2022-09-30 11:19:17 +02:00
Lorenz Hilpert
eb8b54dc63 Fix Unit Test 2022-09-29 18:05:43 +02:00
Lorenz Hilpert
4703aee60c Interveptor Unit Test Fix 2022-09-29 16:26:43 +02:00
Lorenz Hilpert
93b0d43bd7 #3307 Anzeige des backdrops bei Filtern 2022-09-29 14:20:41 +02:00
Lorenz Hilpert
1029310e0d #3483 Neuanmeldung bei 401 Antworten 2022-09-29 14:11:59 +02:00
Michael Auer
c083684db2 Merge tag '3452-Autocomplete-Abbrechen-Bei-Suche' into develop 2022-09-28 22:19:03 +02:00
Lorenz Hilpert
3eff10bbb4 Merged PR 1380: Upgrade der API auf V6
Upgrade der API auf V6

Related work items: #3466
2022-09-20 16:14:50 +00:00
Lorenz Hilpert
e4cbab8365 Merged PR 1379: #3428 Aktivierung der Buttons ohne Raio button aktivieren wenn nur ein artikel
#3428 Aktivierung der Buttons ohne Raio button aktivieren wenn nur ein artikel
2022-09-15 16:05:12 +00:00
Lorenz Hilpert
18212e7a4c Merged PR 1378: Update CRM API V6
Update CRM API V6
2022-09-14 15:13:14 +00:00
Lorenz Hilpert
0cd0b1abfd Merged PR 1377: Update CRM API
Related work items: #3464
2022-09-14 13:48:25 +00:00
Lorenz Hilpert
a66137873c Merged PR 1376: #3448 Anzeige Sonderinfo
#3448 Anzeige Sonderinfo
2022-09-14 08:26:29 +00:00
Andreas Schickinger
469110eabf Merged PR 1375: #3455 AHF Frist immer für gesamten Warenkorb festlegen
#3455 AHF Frist immer für gesamten Warenkorb festlegen

Related work items: #3455
2022-09-13 13:08:59 +00:00
Lorenz Hilpert
55474fa4e3 Merged PR 1374: #3451 nach löschen des Browser-Verlauf kommt Fehler
#3451 nach löschen des Browser-Verlauf kommt Fehler
2022-09-13 12:56:17 +00:00
Lorenz Hilpert
246c5a61dd Merged PR 1373: #3428 bei Teilabholung ohne ausgewählten Radio-Button wirf Fehler
#3428 bei Teilabholung ohne ausgewählten Radio-Button wirf Fehler
2022-09-08 13:20:45 +00:00
Andreas Schickinger
0c8bfba515 Merged PR 1372: #3340 Quantity Dropdown um Suffix erweitert
#3340 Quantity Dropdown um Suffix erweitert

Related work items: #3340
2022-09-01 08:23:20 +00:00
Michael Auer
3c8d9bb1e5 Merge tag '2.0' into develop 2022-08-24 10:33:19 +02:00
Lorenz Hilpert
0334b2dd33 Merge branch 'release/2.0' into develop 2022-08-23 14:24:21 +02:00
Lorenz Hilpert
96356042af Remission - Anpassung HTML fuer E2E Tests 2022-08-23 14:02:50 +02:00
Lorenz Hilpert
21adff8d0c #3140 Button "Nicht-Clickbar"-Icon nur wenn disabled 2022-08-22 16:11:51 +02:00
Lorenz Hilpert
35def2a7c7 #3405 Kalender -Speichern-Button verschwindet 2022-08-19 17:07:30 +02:00
Lorenz Hilpert
e3d82794a3 Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2022-08-19 15:55:10 +02:00
Lorenz Hilpert
20cbac8f17 #3140 Ausgrauen des Auswaehlen Buttons fur
Zuruecklegen bis
2022-08-19 15:55:01 +02:00
Michael Auer
8b1baf9ebd Merge tag '2.0.531' into develop 2022-08-18 13:47:13 +02:00
Lorenz Hilpert
199c4f30e7 #3139 Loder angepasst 2022-08-11 17:23:20 +02:00
Lorenz Hilpert
732c0d4e35 #3139 Load Spinner für Abholfrist 2022-08-10 16:50:29 +02:00
Lorenz Hilpert
029997d624 #3338 - Uebergabe Filter angepasst 2022-08-08 11:56:46 +02:00
Andreas Schickinger
d2546409cb Merged PR 1370: #3139 Abholfachfrist: Für alle festlegen Button in Bestellbestätigung
#3139 Abholfachfrist: Für alle festlegen Button in Bestellbestätigung

Related work items: #3139
2022-08-08 08:46:00 +00:00
Andreas Schickinger
cb2bc8d65b Merged PR 1369: #3332 AHFFrist DisplayOrderItemSubsetDTO verwendet
#3332 AHFFrist DisplayOrderItemSubsetDTO verwendet

Related work items: #3332
2022-08-04 13:38:39 +00:00
Andreas Schickinger
cc1e210799 Merged PR 1367: #3139, #3140, #3328 AHFFrist auf Bestellbestätigung- und Bestellpostenseite
Related work items: #3139, #3140, #3328
2022-08-03 13:51:25 +00:00
Lorenz Hilpert
4bee08d483 Merged PR 1366: Merge release => develop
Related work items: #3180, #3203, #3245, #3293, #3299, #3312, #3320, #3322
2022-08-02 12:30:08 +00:00
738 changed files with 12894 additions and 8334 deletions

4
.npmrc
View File

@@ -1,3 +1 @@
@isa:registry=https://pkgs.dev.azure.com/hugendubel/_packaging/hugendubel%40Local/npm/registry/
@cmf:registry=https://pkgs.dev.azure.com/hugendubel/_packaging/hugendubel%40Local/npm/registry/
always-auth=true
@paragondata:registry=https://npm.pkg.github.com

View File

@@ -3629,6 +3629,37 @@
}
}
}
},
"@ui/form-field": {
"projectType": "library",
"root": "apps/ui/form-field",
"sourceRoot": "apps/ui/form-field/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/ui/form-field/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/ui/form-field/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/ui/form-field/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/ui/form-field/src/test.ts",
"tsConfig": "apps/ui/form-field/tsconfig.spec.json",
"karmaConfig": "apps/ui/form-field/karma.conf.js"
}
}
}
}
},
"defaultProject": "isa-app"

View File

@@ -31,7 +31,11 @@ export class AuthService {
this._oAuthService.tokenValidationHandler = new JwksValidationHandler();
this._oAuthService.setupAutomaticSilentRefresh();
await this._oAuthService.loadDiscoveryDocumentAndTryLogin();
try {
await this._oAuthService.loadDiscoveryDocumentAndTryLogin();
} catch (error) {
this.login();
}
this._initialized.next(true);
}

View File

@@ -1,10 +1,10 @@
import { Injectable, Injector } from '@angular/core';
import { Injectable, Injector, Optional, SkipSelf } from '@angular/core';
import { ActionHandler } from './action-handler.interface';
import { FEATURE_ACTION_HANDLERS, ROOT_ACTION_HANDLERS } from './tokens';
@Injectable()
export class CommandService {
constructor(private injector: Injector) {}
constructor(private injector: Injector, @Optional() @SkipSelf() private _parent: CommandService) {}
async handleCommand<T>(command: string, data?: T): Promise<T> {
const actions = this.getActions(command);
@@ -15,7 +15,7 @@ export class CommandService {
console.error('CommandService.handleCommand', 'Action Handler does not exist', { action });
throw new Error('Action Handler does not exist');
}
console.log('handle command', handler, data);
data = await handler.handler(data);
}
return data;
@@ -25,10 +25,16 @@ export class CommandService {
return command?.split('|') || [];
}
getActionHandler(action: string): ActionHandler {
getActionHandler(action: string): ActionHandler | undefined {
const featureActionHandlers: ActionHandler[] = this.injector.get(FEATURE_ACTION_HANDLERS, []);
const rootActionHandlers: ActionHandler[] = this.injector.get(ROOT_ACTION_HANDLERS, []);
return [...featureActionHandlers, ...rootActionHandlers].find((handler) => handler.action === action);
let handler = [...featureActionHandlers, ...rootActionHandlers].find((handler) => handler.action === action);
if (this._parent && !handler) {
handler = this._parent.getActionHandler(action);
}
return handler;
}
}

View File

@@ -1,34 +1,42 @@
import { Injectable } from '@angular/core';
import { ItemDTO } from '@swagger/cat';
import { AvailabilityDTO, BranchDTO, OLAAvailabilityDTO, StoreCheckoutService, SupplierDTO } from '@swagger/checkout';
import {
AvailabilityDTO,
BranchDTO,
OLAAvailabilityDTO,
StoreCheckoutBranchService,
StoreCheckoutSupplierService,
SupplierDTO,
} from '@swagger/checkout';
import { combineLatest, Observable, of } from 'rxjs';
import {
AvailabilityRequestDTO,
AvailabilityService as SwaggerAvailabilityService,
AvailabilityService,
AvailabilityDTO as SwaggerAvailabilityDTO,
AvailabilityType,
} from '@swagger/availability';
import { AvailabilityDTO as CatAvailabilityDTO } from '@swagger/cat';
import { map, shareReplay, switchMap, withLatestFrom, mergeMap, timeout } from 'rxjs/operators';
import { isArray, memorize } from '@utils/common';
import { OrderService } from '@swagger/oms';
import { LogisticianDTO, LogisticianService } from '@swagger/oms';
import { ResponseArgsOfIEnumerableOfStockInfoDTO, StockDTO, StockInfoDTO, StockService } from '@swagger/remi';
import { ItemData } from './defs/item-data.model';
import { PriceDTO } from '@swagger/availability';
import { AvailabilityByBranchDTO } from './defs/availability-by-branch-dto.model';
import { AvailabilityByBranchDTO, ItemData } from './defs';
import { Availability } from './defs/availability';
@Injectable()
export class DomainAvailabilityService {
constructor(
private swaggerAvailabilityService: SwaggerAvailabilityService,
private storeCheckoutService: StoreCheckoutService,
private orderService: OrderService,
private _stock: StockService
private _availabilityService: AvailabilityService,
private _logisticanService: LogisticianService,
private _stockService: StockService,
private _supplierService: StoreCheckoutSupplierService,
private _branchService: StoreCheckoutBranchService
) {}
@memorize()
getSuppliers(): Observable<SupplierDTO[]> {
return this.storeCheckoutService.StoreCheckoutGetSuppliers({}).pipe(
return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
map((response) => response.result),
shareReplay()
);
@@ -36,7 +44,7 @@ export class DomainAvailabilityService {
@memorize()
getTakeAwaySupplier(): Observable<SupplierDTO> {
return this.storeCheckoutService.StoreCheckoutGetSuppliers({}).pipe(
return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
map(({ result }) => result?.find((supplier) => supplier?.supplierNumber === 'F')),
shareReplay()
);
@@ -44,7 +52,7 @@ export class DomainAvailabilityService {
@memorize()
getBranches(): Observable<BranchDTO[]> {
return this.storeCheckoutService.StoreCheckoutGetBranches({}).pipe(
return this._branchService.StoreCheckoutBranchGetBranches({}).pipe(
map((response) => response.result),
shareReplay()
);
@@ -52,7 +60,7 @@ export class DomainAvailabilityService {
@memorize()
getCurrentStock(): Observable<StockDTO> {
return this._stock.StockCurrentStock().pipe(
return this._stockService.StockCurrentStock().pipe(
map((response) => response.result),
shareReplay()
);
@@ -60,7 +68,7 @@ export class DomainAvailabilityService {
@memorize()
getCurrentBranch(): Observable<BranchDTO> {
return this._stock.StockCurrentBranch().pipe(
return this._stockService.StockCurrentBranch().pipe(
map((response) => ({
id: response.result.id,
name: response.result.name,
@@ -83,8 +91,8 @@ export class DomainAvailabilityService {
}
@memorize({})
getLogisticians() {
return this.orderService.OrderGetLogisticians({}).pipe(
getLogisticians(): Observable<LogisticianDTO> {
return this._logisticanService.LogisticianGetLogisticians({}).pipe(
map((response) => response.result?.find((l) => l.logisticianNumber === '2470')),
shareReplay()
);
@@ -101,7 +109,7 @@ export class DomainAvailabilityService {
price: PriceDTO;
quantity: number;
}): Observable<AvailabilityByBranchDTO[]> {
return this._stock.StockStockRequest({ stockRequest: { branchIds, itemId } }).pipe(
return this._stockService.StockStockRequest({ stockRequest: { branchIds, itemId } }).pipe(
map((response) => response.result),
withLatestFrom(this.getTakeAwaySupplier()),
map(([result, supplier]) => {
@@ -127,7 +135,7 @@ export class DomainAvailabilityService {
return this.getCurrentStock().pipe(
switchMap((s) =>
combineLatest([
this._stock.StockInStock({ articleIds: [item.itemId], stockId: s.id }),
this._stockService.StockInStock({ articleIds: [item.itemId], stockId: s.id }),
this.getTakeAwaySupplier(),
this.getCurrentBranch(),
])
@@ -153,7 +161,7 @@ export class DomainAvailabilityService {
quantity: number;
}): Observable<AvailabilityDTO> {
return combineLatest([
this._stock.StockStockRequest({ stockRequest: { branchIds: [branch.id], itemId } }),
this._stockService.StockStockRequest({ stockRequest: { branchIds: [branch.id], itemId } }),
this.getTakeAwaySupplier(),
]).pipe(
map(([response, supplier]) => {
@@ -173,7 +181,7 @@ export class DomainAvailabilityService {
quantity: number;
}): Observable<AvailabilityDTO> {
return this.getCurrentStock().pipe(
switchMap((s) => this._stock.StockInStockByEAN({ eans, stockId: s.id })),
switchMap((s) => this._stockService.StockInStockByEAN({ eans, stockId: s.id })),
withLatestFrom(this.getTakeAwaySupplier(), this.getCurrentBranch()),
map(([response, supplier, branch]) => {
return this._mapToTakeAwayAvailability({ response, supplier, branch, quantity, price });
@@ -185,7 +193,7 @@ export class DomainAvailabilityService {
getTakeAwayAvailabilitiesByEans({ eans }: { eans: string[] }): Observable<StockInfoDTO[]> {
const eansFiltered = Array.from(new Set(eans));
return this.getCurrentStock().pipe(
switchMap((s) => this._stock.StockInStockByEAN({ eans: eansFiltered, stockId: s.id })),
switchMap((s) => this._stockService.StockInStockByEAN({ eans: eansFiltered, stockId: s.id })),
withLatestFrom(this.getTakeAwaySupplier(), this.getCurrentBranch()),
map((response) => response[0].result),
shareReplay()
@@ -193,8 +201,16 @@ export class DomainAvailabilityService {
}
@memorize({ ttl: 10000 })
getPickUpAvailability({ item, branch, quantity }: { item: ItemData; quantity: number; branch: BranchDTO }): Observable<AvailabilityDTO> {
return this.swaggerAvailabilityService
getPickUpAvailability({
item,
branch,
quantity,
}: {
item: ItemData;
quantity: number;
branch: BranchDTO;
}): Observable<Availability<AvailabilityDTO, SwaggerAvailabilityDTO>> {
return this._availabilityService
.AvailabilityStoreAvailability([
{
qty: quantity,
@@ -212,7 +228,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
return this.swaggerAvailabilityService
return this._availabilityService
.AvailabilityShippingAvailability([
{
ean: item?.ean,
@@ -230,7 +246,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDigDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
return this.swaggerAvailabilityService
return this._availabilityService
.AvailabilityShippingAvailability([
{
qty: quantity,
@@ -275,7 +291,9 @@ export class DomainAvailabilityService {
timeout(5000),
mergeMap((branch) =>
this.getPickUpAvailability({ item, quantity, branch }).pipe(
mergeMap((availability) => logistician$.pipe(map((logistician) => ({ ...availability, logistician: { id: logistician.id } })))),
mergeMap((availability) =>
logistician$.pipe(map((logistician) => ({ ...availability[0], logistician: { id: logistician.id } })))
),
shareReplay()
)
)
@@ -284,7 +302,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDownloadAvailability({ item }: { item: ItemData }): Observable<AvailabilityDTO> {
return this.swaggerAvailabilityService
return this._availabilityService
.AvailabilityShippingAvailability([
{
ean: item?.ean,
@@ -319,11 +337,11 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getTakeAwayAvailabilities(items: { id: number; price: PriceDTO }[], branchId: number) {
return this._stock.StockGetStocksByBranch({ branchId }).pipe(
return this._stockService.StockGetStocksByBranch({ branchId }).pipe(
map((req) => req.result?.find((_) => true)?.id),
switchMap((stockId) =>
stockId
? this._stock.StockInStock({ articleIds: items.map((i) => i.id), stockId })
? this._stockService.StockInStock({ articleIds: items.map((i) => i.id), stockId })
: of({ result: [] } as ResponseArgsOfIEnumerableOfStockInfoDTO)
),
timeout(20000),
@@ -344,7 +362,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getPickUpAvailabilities(payload: AvailabilityRequestDTO[], preferred?: boolean) {
return this.swaggerAvailabilityService.AvailabilityStoreAvailability(payload).pipe(
return this._availabilityService.AvailabilityStoreAvailability(payload).pipe(
timeout(20000),
map((response) => (preferred ? this._mapToPickUpAvailability(response.result) : response.result))
);
@@ -352,7 +370,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
return this.swaggerAvailabilityService.AvailabilityShippingAvailability(payload).pipe(
return this._availabilityService.AvailabilityShippingAvailability(payload).pipe(
timeout(20000),
map((response) => this._mapToShippingAvailability(response.result))
);
@@ -360,7 +378,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
return this.swaggerAvailabilityService.AvailabilityShippingAvailability(payload).pipe(
return this._availabilityService.AvailabilityShippingAvailability(payload).pipe(
timeout(20000),
map((response) => this._mapToShippingAvailability(response.result))
);
@@ -386,7 +404,7 @@ export class DomainAvailabilityService {
): PriceDTO {
switch (purchasingOption) {
case 'take-away':
return availability?.price || availability?.retailPrice;
return availability?.price || catalogAvailability?.price;
case 'delivery':
case 'dig-delivery':
if (catalogAvailability?.price?.value?.value < availability?.price?.value?.value) {
@@ -447,9 +465,11 @@ export class DomainAvailabilityService {
inStock: inStock,
supplierSSC: quantity <= inStock ? '999' : '',
supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '',
price,
price: price ?? stockInfo?.retailPrice,
supplier: { id: supplier?.id },
retailPrice: (stockInfo as any)?.retailPrice, // TODO: Change after API Update
// TODO: Change after API Update
// LH: 2021-03-09 preis Property hat nun ein Fallback auf retailPrice
// retailPrice: (stockInfo as any)?.retailPrice,
};
return availability;
}
@@ -479,26 +499,29 @@ export class DomainAvailabilityService {
return availability;
}
private _mapToPickUpAvailability(availabilities: SwaggerAvailabilityDTO[]) {
private _mapToPickUpAvailability(availabilities: SwaggerAvailabilityDTO[]): Availability<AvailabilityDTO, SwaggerAvailabilityDTO>[] {
if (isArray(availabilities)) {
const preferred = availabilities.filter((f) => f.preferred === 1);
const totalAvailable = availabilities.reduce((sum, av) => sum + (av?.qty || 0), 0);
return preferred.map((p) => {
return {
availabilityType: p?.status,
ssc: p?.ssc,
sscText: p?.sscText,
supplier: { id: p?.supplierId },
isPrebooked: p?.isPrebooked,
estimatedShippingDate: p?.requestStatusCode === '32' ? p?.altAt : p?.at,
price: p?.price,
inStock: totalAvailable,
supplierProductNumber: p?.supplierProductNumber,
supplierInfo: p?.requestStatusCode,
lastRequest: p?.requested,
itemId: p.itemId,
};
return [
{
availabilityType: p?.status,
ssc: p?.ssc,
sscText: p?.sscText,
supplier: { id: p?.supplierId },
isPrebooked: p?.isPrebooked,
estimatedShippingDate: p?.requestStatusCode === '32' ? p?.altAt : p?.at,
price: p?.price,
inStock: totalAvailable,
supplierProductNumber: p?.supplierProductNumber,
supplierInfo: p?.requestStatusCode,
lastRequest: p?.requested,
itemId: p.itemId,
},
p,
];
});
}
}

View File

@@ -0,0 +1 @@
export type Availability<T, S> = [T, S];

View File

@@ -1,3 +1,3 @@
// start:ng42.barrel
export * from './item-data.model';
// end:ng42.barrel
export * from './availability-by-branch-dto';
export * from './availability';
export * from './item-data';

View File

@@ -21,8 +21,13 @@ import {
UpdateShoppingCartItemDTO,
InputDTO,
ItemPayload,
StoreCheckoutShoppingCartService,
StoreCheckoutPaymentService,
StoreCheckoutBuyerService,
StoreCheckoutPayerService,
StoreCheckoutBranchService,
} from '@swagger/checkout';
import { DisplayOrderDTO, OrderCheckoutService, ReorderValues } from '@swagger/oms';
import { DisplayOrderDTO, DisplayOrderItemDTO, OrderCheckoutService, ReorderValues } from '@swagger/oms';
import { isNullOrUndefined, memorize } from '@utils/common';
import { combineLatest, Observable, of, concat, isObservable, throwError } from 'rxjs';
import { bufferCount, catchError, filter, first, map, mergeMap, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
@@ -32,6 +37,7 @@ import * as DomainCheckoutActions from './store/domain-checkout.actions';
import { DomainAvailabilityService } from '@domain/availability';
import { HttpErrorResponse } from '@angular/common/http';
import { ApplicationService } from '@core/application';
import { CustomerDTO, EntityDTOContainerOfAttributeDTO } from '@swagger/crm';
@Injectable()
export class DomainCheckoutService {
@@ -40,7 +46,12 @@ export class DomainCheckoutService {
private applicationService: ApplicationService,
private storeCheckoutService: StoreCheckoutService,
private orderCheckoutService: OrderCheckoutService,
private availabilityService: DomainAvailabilityService
private availabilityService: DomainAvailabilityService,
private _shoppingCartService: StoreCheckoutShoppingCartService,
private _paymentService: StoreCheckoutPaymentService,
private _buyerService: StoreCheckoutBuyerService,
private _payerService: StoreCheckoutPayerService,
private _branchService: StoreCheckoutBranchService
) {}
//#region shoppingcart
@@ -53,8 +64,8 @@ export class DomainCheckoutService {
return false;
} else if (cart && _latest) {
_latest = false;
this.storeCheckoutService
.StoreCheckoutGetShoppingCart({
this._shoppingCartService
.StoreCheckoutShoppingCartGetShoppingCart({
shoppingCartId: cart.id,
})
.pipe(
@@ -77,7 +88,7 @@ export class DomainCheckoutService {
}
createShoppingCart({ processId }: { processId: number }): Observable<ShoppingCartDTO> {
return this.storeCheckoutService.StoreCheckoutCreateShoppingCart().pipe(
return this._shoppingCartService.StoreCheckoutShoppingCartCreateShoppingCart().pipe(
map((response) => response.result),
tap((shoppingCart) =>
this.store.dispatch(
@@ -94,8 +105,8 @@ export class DomainCheckoutService {
return this.getShoppingCart({ processId }).pipe(
first(),
mergeMap((cart) =>
this.storeCheckoutService
.StoreCheckoutAddItemToShoppingCart({
this._shoppingCartService
.StoreCheckoutShoppingCartAddItemToShoppingCart({
items,
shoppingCartId: cart.id,
})
@@ -125,8 +136,8 @@ export class DomainCheckoutService {
return this.getShoppingCart({ processId }).pipe(
first(),
mergeMap((shoppingCart) =>
this.storeCheckoutService
.StoreCheckoutSetLogisticianOnDestinationsByBuyer({
this._shoppingCartService
.StoreCheckoutShoppingCartSetLogisticianOnDestinationsByBuyer({
shoppingCartId: shoppingCart?.id,
payload: { customerFeatures },
})
@@ -139,7 +150,7 @@ export class DomainCheckoutService {
return this.getShoppingCart({ processId }).pipe(
first(),
mergeMap((cart) =>
this.storeCheckoutService.StoreCheckoutCanAddDestination({
this._shoppingCartService.StoreCheckoutShoppingCartCanAddDestination({
shoppingCartId: cart.id,
payload: destinationDTO,
})
@@ -166,8 +177,8 @@ export class DomainCheckoutService {
first(),
withLatestFrom(this.store.select(DomainCheckoutSelectors.selectCustomerFeaturesByProcessId, { processId })),
mergeMap(([shoppingCart, customerFeatures]) =>
this.storeCheckoutService
.StoreCheckoutCanAddItem({
this._shoppingCartService
.StoreCheckoutShoppingCartCanAddItem({
shoppingCartId: shoppingCart?.id,
payload: {
customerFeatures,
@@ -199,8 +210,8 @@ export class DomainCheckoutService {
orderType,
};
});
return this.storeCheckoutService
.StoreCheckoutCanAddItems({
return this._shoppingCartService
.StoreCheckoutShoppingCartCanAddItems({
shoppingCartId: shoppingCart.id,
payload,
})
@@ -222,7 +233,7 @@ export class DomainCheckoutService {
shoppingCartItemId: number;
availability: AvailabilityDTO;
}) {
return this.storeCheckoutService.StoreCheckoutUpdateShoppingCartItemAvailability({
return this._shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability({
shoppingCartId,
shoppingCartItemId,
availability,
@@ -241,8 +252,8 @@ export class DomainCheckoutService {
return this.getShoppingCart({ processId }).pipe(
first(),
mergeMap((shoppingCart) =>
this.storeCheckoutService
.StoreCheckoutUpdateShoppingCartItem({
this._shoppingCartService
.StoreCheckoutShoppingCartUpdateShoppingCartItem({
shoppingCartId: shoppingCart.id,
shoppingCartItemId,
values: update,
@@ -318,8 +329,8 @@ export class DomainCheckoutService {
return this.getCheckout({ processId }).pipe(
first(),
mergeMap((checkout) =>
this.storeCheckoutService
.StoreCheckoutGetCheckoutPayment({
this._paymentService
.StoreCheckoutPaymentGetCheckoutPayment({
checkoutId: checkout.id,
})
.pipe(map((response) => response.result))
@@ -335,8 +346,8 @@ export class DomainCheckoutService {
return this.getCheckout({ processId }).pipe(
first(),
mergeMap((checkout) =>
this.storeCheckoutService
.StoreCheckoutSetPaymentType({
this._paymentService
.StoreCheckoutPaymentSetPaymentType({
checkoutId: checkout?.id,
paymentType,
})
@@ -352,8 +363,8 @@ export class DomainCheckoutService {
return this.getCheckout({ processId }).pipe(
first(),
mergeMap((checkout) =>
this.storeCheckoutService
.StoreCheckoutSetBuyer({
this._buyerService
.StoreCheckoutBuyerSetBuyerPOST({
checkoutId: checkout?.id,
buyerDTO: buyer,
})
@@ -369,8 +380,8 @@ export class DomainCheckoutService {
return this.getCheckout({ processId }).pipe(
first(),
mergeMap((checkout) =>
this.storeCheckoutService
.StoreCheckoutSetPayer({
this._payerService
.StoreCheckoutPayerSetPayerPOST({
checkoutId: checkout?.id,
payerDTO: payer,
})
@@ -389,7 +400,7 @@ export class DomainCheckoutService {
mergeMap((cart) =>
concat(
...cart.items.map((item) =>
this.storeCheckoutService.StoreCheckoutUpdateShoppingCartItem({
this._shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItem({
shoppingCartId: cart.id,
shoppingCartItemId: item.id,
values: { specialComment },
@@ -650,7 +661,7 @@ export class DomainCheckoutService {
first(),
mergeMap((checkout) =>
this.orderCheckoutService
.OrderCheckoutCreateOrder({
.OrderCheckoutCreateOrderPOST({
checkoutId: checkout.id,
})
.pipe(
@@ -734,8 +745,8 @@ export class DomainCheckoutService {
return this.getShoppingCart({ processId }).pipe(
first(),
mergeMap((shoppingCart) =>
this.storeCheckoutService
.StoreCheckoutCanAddBuyer({
this._shoppingCartService
.StoreCheckoutShoppingCartCanAddBuyer({
shoppingCartId: shoppingCart.id,
payload: { customerFeatures },
})
@@ -808,8 +819,8 @@ export class DomainCheckoutService {
@memorize()
getBranches(): Observable<BranchDTO[]> {
return this.storeCheckoutService
.StoreCheckoutGetBranches({
return this._branchService
.StoreCheckoutBranchGetBranches({
take: 999,
})
.pipe(
@@ -828,10 +839,6 @@ export class DomainCheckoutService {
.pipe(map((response) => response.result));
}
setCustomerFeatures({ processId, customerFeatures }: { processId: number; customerFeatures: { [key: string]: string } }) {
this.store.dispatch(DomainCheckoutActions.setCustomerFeatures({ processId, customerFeatures }));
}
setOlaErrors({ processId, errorIds }: { processId: number; errorIds: number[] }) {
this.store.dispatch(
DomainCheckoutActions.setOlaError({
@@ -857,6 +864,14 @@ export class DomainCheckoutService {
this.store.dispatch(DomainCheckoutActions.removeProcess({ processId }));
}
setCustomer({ processId, customerDto }: { processId: number; customerDto: CustomerDTO }) {
this.store.dispatch(DomainCheckoutActions.setCustomer({ processId, customer: customerDto }));
}
getCustomer({ processId }: { processId: number }): Observable<CustomerDTO> {
return this.store.select(DomainCheckoutSelectors.selectCustomerByProcessId, { processId });
}
setPayer({ processId, payer }: { processId: number; payer: PayerDTO }) {
this.store.dispatch(DomainCheckoutActions.setPayer({ processId, payer }));
}
@@ -877,6 +892,10 @@ export class DomainCheckoutService {
return this.store.select(DomainCheckoutSelectors.selectOrders);
}
updateOrderItem(item: DisplayOrderItemDTO) {
this.store.dispatch(DomainCheckoutActions.updateOrderItem({ item }));
}
removeAllOrders() {
this.store.dispatch(DomainCheckoutActions.removeAllOrders());
}

View File

@@ -1,11 +1,12 @@
import { BuyerDTO, CheckoutDTO, NotificationChannel, PayerDTO, ShippingAddressDTO, ShoppingCartDTO } from '@swagger/checkout';
import { CustomerDTO } from '@swagger/crm';
import { DisplayOrderDTO } from '@swagger/oms';
export interface CheckoutEntity {
processId: number;
checkout: CheckoutDTO;
shoppingCart: ShoppingCartDTO;
customerFeatures: { [key: string]: string };
customer: CustomerDTO;
payer: PayerDTO;
buyer: BuyerDTO;
shippingAddress: ShippingAddressDTO;

View File

@@ -8,7 +8,8 @@ import {
BuyerDTO,
PayerDTO,
} from '@swagger/checkout';
import { DisplayOrderDTO } from '@swagger/oms';
import { CustomerDTO } from '@swagger/crm';
import { DisplayOrderDTO, DisplayOrderItemDTO } from '@swagger/oms';
const prefix = '[DOMAIN-CHECKOUT]';
@@ -38,11 +39,6 @@ export const setCheckoutDestination = createAction(
props<{ processId: number; destination: DestinationDTO }>()
);
export const setCustomerFeatures = createAction(
`${prefix} Set Customer Features`,
props<{ processId: number; customerFeatures: { [key: string]: string } }>()
);
export const setShippingAddress = createAction(
`${prefix} Set Shipping Address`,
props<{ processId: number; shippingAddress: ShippingAddressDTO }>()
@@ -52,6 +48,8 @@ export const removeProcess = createAction(`${prefix} Remove Process`, props<{ pr
export const setOrders = createAction(`${prefix} Add Orders`, props<{ orders: DisplayOrderDTO[] }>());
export const updateOrderItem = createAction(`${prefix} Update Orders`, props<{ item: DisplayOrderItemDTO }>());
export const removeAllOrders = createAction(`${prefix} Remove All Orders`);
export const setBuyer = createAction(`${prefix} Set Buyer`, props<{ processId: number; buyer: BuyerDTO }>());
@@ -61,3 +59,5 @@ export const setPayer = createAction(`${prefix} Set Payer`, props<{ processId: n
export const setSpecialComment = createAction(`${prefix} Set Agent Comment`, props<{ processId: number; agentComment: string }>());
export const setOlaError = createAction(`${prefix} Set Ola Error`, props<{ processId: number; olaErrorIds: number[] }>());
export const setCustomer = createAction(`${prefix} Set Customer`, props<{ processId: number; customer: CustomerDTO }>());

View File

@@ -46,11 +46,6 @@ const _domainCheckoutReducer = createReducer(
};
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setCustomerFeatures, (s, { processId, customerFeatures }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.customerFeatures = customerFeatures;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setShippingAddress, (s, { processId, shippingAddress }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.shippingAddress = shippingAddress;
@@ -73,6 +68,25 @@ const _domainCheckoutReducer = createReducer(
}),
on(DomainCheckoutActions.removeProcess, (s, { processId }) => storeCheckoutAdapter.removeOne(processId, s)),
on(DomainCheckoutActions.setOrders, (s, { orders }) => ({ ...s, orders: [...s.orders, ...orders] })),
on(DomainCheckoutActions.updateOrderItem, (s, { item }) => {
const orders = [...s.orders];
const orderToUpdate = orders?.find((order) => order.items?.find((i) => i.id === item?.id));
const orderToUpdateIndex = orders?.indexOf(orderToUpdate);
const orderItemToUpdate = orderToUpdate?.items?.find((i) => i.id === item?.id);
const orderItemToUpdateIndex = orderToUpdate?.items?.indexOf(orderItemToUpdate);
const items = [...orderToUpdate?.items];
items[orderItemToUpdateIndex] = item;
orders[orderToUpdateIndex] = {
...orderToUpdate,
items: [...items],
};
return { ...s, orders: [...orders] };
}),
on(DomainCheckoutActions.removeAllOrders, (s) => ({
...s,
orders: [],
@@ -81,6 +95,11 @@ const _domainCheckoutReducer = createReducer(
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.olaErrorIds = olaErrorIds;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setCustomer, (s, { processId, customer }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.customer = customer;
return storeCheckoutAdapter.setOne(entity, s);
})
);
@@ -96,7 +115,6 @@ function getOrCreateCheckoutEntity({ entities, processId }: { entities: Dictiona
processId,
checkout: undefined,
shoppingCart: undefined,
customerFeatures: undefined,
shippingAddress: undefined,
orders: [],
payer: undefined,
@@ -104,6 +122,7 @@ function getOrCreateCheckoutEntity({ entities, processId }: { entities: Dictiona
specialComment: '',
notificationChannels: 0,
olaErrorIds: [],
customer: undefined,
};
}

View File

@@ -1,5 +1,6 @@
import { Dictionary } from '@ngrx/entity';
import { createSelector } from '@ngrx/store';
import { CustomerDTO } from '@swagger/crm';
import { CheckoutEntity } from './defs/checkout.entity';
import { storeCheckoutAdapter, storeFeatureSelector } from './domain-checkout.state';
@@ -22,7 +23,7 @@ export const selectCheckoutByProcessId = createSelector(
export const selectCustomerFeaturesByProcessId = createSelector(
selectEntities,
(entities: Dictionary<CheckoutEntity>, { processId }: { processId: number }) => entities[processId]?.customerFeatures
(entities: Dictionary<CheckoutEntity>, { processId }: { processId: number }) => getCusomterFeatures(entities[processId]?.customer)
);
export const selectShippingAddressByProcessId = createSelector(
@@ -61,3 +62,19 @@ export const selectOlaErrorsByProcessId = createSelector(
selectEntities,
(entities: Dictionary<CheckoutEntity>, { processId }: { processId: number }) => entities[processId]?.olaErrorIds
);
export const selectCustomerByProcessId = createSelector(
selectEntities,
(entities: Dictionary<CheckoutEntity>, { processId }: { processId: number }) => entities[processId]?.customer
);
function getCusomterFeatures(custoemr: CustomerDTO): { [key: string]: string } {
const customerFeatures = custoemr?.features ?? [];
const features: { [key: string]: string } = {};
for (const feature of customerFeatures) {
features[feature.key] = feature.key;
}
return features;
}

View File

@@ -1,14 +1,17 @@
import { Injectable } from '@angular/core';
import {
AddressDTO,
AddressService,
AssignedPayerDTO,
AutocompleteDTO,
CommunicationDetailsDTO,
CountryDTO,
CountryService,
CustomerDTO,
CustomerInfoDTO,
CustomerService,
InputDTO,
KeyValueDTOOfStringAndString,
ListResponseArgsOfCustomerInfoDTO,
NotificationChannel,
PayerDTO,
@@ -16,15 +19,23 @@ import {
ResponseArgsOfHistoryDTO,
ResponseArgsOfIEnumerableOfBonusCardInfoDTO,
ShippingAddressDTO,
ShippingAddressService,
} from '@swagger/crm';
import { isArray } from '@utils/common';
import { PagedResult, Result } from 'apps/domain/defs/src/public-api';
import { isNil } from 'lodash';
import { Observable, of, ReplaySubject } from 'rxjs';
import { catchError, map, mergeMap, retry } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CrmCustomerService {
constructor(private customerService: CustomerService, private payerService: PayerService) {}
constructor(
private customerService: CustomerService,
private payerService: PayerService,
private addressService: AddressService,
private countryService: CountryService,
private shippingAddressService: ShippingAddressService
) {}
complete(queryString: string, filter?: { [key: string]: string }): Observable<Result<AutocompleteDTO[]>> {
return this.customerService.CustomerCustomerAutocomplete({
@@ -96,12 +107,23 @@ export class CrmCustomerService {
return this.customerService.CustomerPatchCustomer({ customerId, customer: { ...customer, notificationChannels } });
}
createB2BCustomer(customer: CustomerDTO) {
createB2BCustomer(customer: CustomerDTO): Promise<Result<CustomerDTO>> {
const notificationChannels = this.getNotificationChannelForCommunicationDetails({
communicationDetails: customer?.communicationDetails,
});
return this.customerService.CustomerCreateCustomer({ ...customer, customerType: 16, notificationChannels });
const payload: CustomerDTO = { ...customer, customerType: 16, notificationChannels };
payload.shippingAddresses = payload.shippingAddresses ?? [];
payload.payers = payload.payers ?? [];
return this.customerService
.CustomerCreateCustomer({
customer: payload,
modifiers: [{ key: 'b2b', group: 'customertype' }],
})
.toPromise();
}
createOnlineCustomer(customer: CustomerDTO): Observable<Result<CustomerDTO>> {
@@ -148,63 +170,253 @@ export class CrmCustomerService {
];
}
return this.customerService.CustomerCreateOnlineCustomer({ ...payload, notificationChannels });
const p4mUser = customer.features.find((f) => f.key === 'p4mUser')?.value;
const modifiers: KeyValueDTOOfStringAndString[] = [{ key: 'webshop', group: 'customertype' }];
if (p4mUser) {
modifiers.push({ key: 'add-loyalty-card', value: p4mUser });
}
return this.customerService.CustomerCreateCustomer({
customer: { ...payload, notificationChannels },
modifiers,
});
}
createGuestCustomer(customer: CustomerDTO): Observable<Result<CustomerDTO>> {
mapCustomerToPayer(customer: CustomerDTO): PayerDTO {
return {
address: customer.address,
communicationDetails: customer.communicationDetails,
firstName: customer.firstName,
lastName: customer.lastName,
organisation: customer.organisation,
title: customer.title,
payerType: 1,
gender: customer.gender,
};
}
mapCustomerToShippingAddress(customer: CustomerDTO): ShippingAddressDTO {
return {
address: customer.address,
communicationDetails: customer.communicationDetails,
firstName: customer.firstName,
gender: customer.gender,
lastName: customer.lastName,
organisation: customer.organisation,
title: customer.title,
type: 1,
};
}
async updateToOnlineCustomer(customer: CustomerDTO): Promise<Result<CustomerDTO>> {
const payload: CustomerDTO = { shippingAddresses: [], payers: [], ...customer, customerType: 8, hasOnlineAccount: true };
const notificationChannels = this.getNotificationChannelForCommunicationDetails({
communicationDetails: payload?.communicationDetails,
});
const shippingAddressesToAdd = payload.shippingAddresses?.filter((sa) => !sa.id)?.map((m) => m.data) ?? [];
payload.shippingAddresses = payload.shippingAddresses?.filter((sa) => !!sa.id) ?? [];
if (payload.shippingAddresses.length === 0) {
shippingAddressesToAdd.unshift(this.mapCustomerToShippingAddress(payload));
}
const payersToAdd = payload.payers?.filter((p) => !p.assignedToCustomer)?.map((p) => p.payer?.data) ?? [];
payload.payers = payload.payers?.filter((p) => !!p.assignedToCustomer) ?? [];
if (payload.payers.length === 0) {
payersToAdd.unshift({
...this.mapCustomerToPayer(payload),
payerType: payload.customerType,
});
}
const modifiers: KeyValueDTOOfStringAndString[] = [{ key: 'webshop', group: 'customertype' }];
const res = await this.customerService
.CustomerUpdateCustomer({
customerId: customer.id,
payload: {
modifiers,
customer: { ...payload, notificationChannels },
},
})
.toPromise();
for (let shippingAddress of shippingAddressesToAdd) {
await this.createShippingAddress(res.result.id, shippingAddress, true);
}
for (let payer of payersToAdd) {
await this.createPayer(res.result.id, payer, true);
}
return res;
}
async updateToP4MOnlineCustomer(customer: CustomerDTO): Promise<Result<CustomerDTO>> {
const shippingAddressesToAdd = customer.shippingAddresses?.filter((sa) => !sa.id)?.map((m) => m.data) ?? [];
customer.shippingAddresses = customer.shippingAddresses?.filter((sa) => !!sa.id) ?? [];
if (customer.shippingAddresses.length === 0) {
shippingAddressesToAdd.unshift(this.mapCustomerToShippingAddress(customer));
}
const payersToAdd = customer.payers?.filter((p) => !p.assignedToCustomer)?.map((p) => p.payer?.data) ?? [];
customer.payers = customer.payers?.filter((p) => !!p.assignedToCustomer) ?? [];
if (customer.payers.length === 0) {
payersToAdd.unshift({
...this.mapCustomerToPayer(customer),
payerType: customer.customerType,
});
}
const p4mUser = customer.features.find((f) => f.key === 'p4mUser')?.value;
const modifiers: KeyValueDTOOfStringAndString[] = [{ key: 'webshop', group: 'customertype' }];
if (p4mUser) {
modifiers.push({ key: 'add-loyalty-card', value: p4mUser });
}
const res = await this.customerService
.CustomerUpdateCustomer({
customerId: customer.id,
payload: {
customer,
modifiers,
},
})
.toPromise();
for (let shippingAddress of shippingAddressesToAdd) {
await this.createShippingAddress(res.result.id, shippingAddress, true);
}
for (let payer of payersToAdd) {
await this.createPayer(res.result.id, payer, true);
}
return res;
}
async updateStoreP4MToWebshopP4M(customer: CustomerDTO): Promise<Result<CustomerDTO>> {
const shippingAddressesToAdd = customer.shippingAddresses?.filter((sa) => !sa.id)?.map((m) => m.data) ?? [];
customer.shippingAddresses = customer.shippingAddresses?.filter((sa) => !!sa.id) ?? [];
if (customer.shippingAddresses.length === 0) {
shippingAddressesToAdd.unshift(this.mapCustomerToShippingAddress(customer));
}
const payersToAdd = customer.payers?.filter((p) => !p.assignedToCustomer)?.map((p) => p.payer?.data) ?? [];
customer.payers = customer.payers?.filter((p) => !!p.assignedToCustomer) ?? [];
if (customer.payers.length === 0) {
payersToAdd.unshift({
...this.mapCustomerToPayer(customer),
payerType: customer.customerType,
});
}
const modifiers: KeyValueDTOOfStringAndString[] = [{ key: 'webshop', group: 'customertype' }];
const res = await this.customerService
.CustomerUpdateCustomer({
customerId: customer.id,
payload: {
customer,
modifiers,
},
})
.toPromise();
for (let shippingAddress of shippingAddressesToAdd) {
await this.createShippingAddress(res.result.id, shippingAddress, true);
}
for (let payer of payersToAdd) {
await this.createPayer(res.result.id, payer, true);
}
return res;
}
async createGuestCustomer(customer: CustomerDTO): Promise<Result<CustomerDTO>> {
const notificationChannels = this.getNotificationChannelForCommunicationDetails({
communicationDetails: customer?.communicationDetails,
});
const payload: CustomerDTO = { ...customer, customerType: 8, isGuestAccount: true, notificationChannels };
if (!(isArray(payload.shippingAddresses) && payload.shippingAddresses.length > 0)) {
payload.shippingAddresses = [
{
data: {
address: payload.address,
communicationDetails: payload.communicationDetails,
firstName: payload.firstName,
gender: payload.gender,
lastName: payload.lastName,
organisation: payload.organisation,
title: payload.title,
type: 1,
},
},
];
}
payload.shippingAddresses = customer.shippingAddresses ?? [];
if (!(isArray(payload.payers) && payload.payers.length > 0)) {
payload.payers = [
{
payer: {
data: {
address: payload.address,
communicationDetails: payload.communicationDetails,
firstName: payload.firstName,
gender: payload.gender,
lastName: payload.lastName,
organisation: payload.organisation,
title: payload.title,
payerType: payload.customerType,
},
},
},
];
}
payload.shippingAddresses.push({
data: this.mapCustomerToShippingAddress(customer),
});
return this.customerService.CustomerCreateOnlineCustomer(payload);
payload.payers = customer.payers ?? [];
payload.payers.push({
payer: {
data: {
...this.mapCustomerToPayer(customer),
payerType: payload.customerType,
},
},
});
const res = await this.customerService
.CustomerCreateCustomer({
customer: payload,
modifiers: [{ key: 'webshop', group: 'customertype' }],
})
.toPromise();
return res;
}
createBranchCustomer(customer: CustomerDTO): Observable<Result<CustomerDTO>> {
createStoreCustomer(customer: CustomerDTO): Observable<Result<CustomerDTO>> {
const notificationChannels = this.getNotificationChannelForCommunicationDetails({
communicationDetails: customer?.communicationDetails,
});
return this.customerService.CustomerCreateCustomer({ ...customer, customerType: 8, notificationChannels });
const p4mUser = customer.features.find((f) => f.key === 'p4mUser')?.value;
const modifiers: KeyValueDTOOfStringAndString[] = [{ key: 'store', group: 'customertype' }];
if (p4mUser) {
modifiers.push({ key: 'add-loyalty-card', value: p4mUser });
}
return this.customerService.CustomerCreateCustomer({
customer: { ...customer, customerType: 8, notificationChannels },
modifiers,
});
}
validateAddress(address: AddressDTO): Observable<Result<AddressDTO[]>> {
return this.customerService.CustomerValidateAddress(address);
return this.addressService.AddressValidateAddress(address);
}
getOnlineCustomerByEmail(email: string): Observable<CustomerInfoDTO | null> {
return this.getCustomers(email, {
take: 1,
filter: {
customertype: 'webshop',
},
}).pipe(
map((r) => {
if (r.hits === 1) {
return r.result[0];
} else {
return null;
}
}),
catchError((err) => [null])
);
}
private cachedCountriesFailed = false;
@@ -213,8 +425,8 @@ export class CrmCustomerService {
if (!this.cachedCountries || this.cachedCountriesFailed) {
this.cachedCountriesFailed = false;
this.cachedCountries = new ReplaySubject();
this.customerService
.CustomerGetCountries({})
this.countryService
.CountryGetCountries({})
.pipe(
retry(3),
catchError((err) => {
@@ -263,11 +475,7 @@ export class CrmCustomerService {
return this.customerService.CustomerModifyPayerReference({ payerId, customerId, isDefault });
}
createShippingAddress(
customerId: number,
shippingAddress: ShippingAddressDTO,
isDefault?: boolean
): Observable<Result<ShippingAddressDTO>> {
createShippingAddress(customerId: number, shippingAddress: ShippingAddressDTO, isDefault?: boolean): Promise<Result<ShippingAddressDTO>> {
const data: ShippingAddressDTO = { ...shippingAddress };
if (isDefault) {
data.isDefault = new Date().toJSON();
@@ -275,7 +483,7 @@ export class CrmCustomerService {
delete data.isDefault;
}
return this.customerService.CustomerCreateShippingAddress({ customerId, shippingAddress: data });
return this.shippingAddressService.ShippingAddressCreateShippingAddress({ customerId, shippingAddress: data }).toPromise();
}
updateShippingAddress(
@@ -283,7 +491,7 @@ export class CrmCustomerService {
shippingAddressId: number,
shippingAddress: ShippingAddressDTO,
isDefault?: boolean
): Observable<Result<ShippingAddressDTO>> {
): Promise<Result<ShippingAddressDTO>> {
const data: ShippingAddressDTO = { ...shippingAddress };
if (isDefault) {
@@ -292,15 +500,17 @@ export class CrmCustomerService {
delete data.isDefault;
}
return this.customerService.CustomerUpdateShippingAddress({ shippingAddressId, shippingAddress: data, customerId });
return this.shippingAddressService
.ShippingAddressUpdateShippingAddress({ shippingAddressId, shippingAddress: data, customerId })
.toPromise();
}
getShippingAddress(shippingAddressId: number): Observable<Result<ShippingAddressDTO>> {
return this.customerService.CustomerGetShippingaddress(shippingAddressId);
return this.shippingAddressService.ShippingAddressGetShippingaddress(shippingAddressId);
}
getShippingAddresses(params: CustomerService.CustomerGetShippingAddressesParams): Observable<Result<ShippingAddressDTO[]>> {
return this.customerService.CustomerGetShippingAddresses(params);
getShippingAddresses(params: ShippingAddressService.ShippingAddressGetShippingAddressesParams): Observable<Result<ShippingAddressDTO[]>> {
return this.shippingAddressService.ShippingAddressGetShippingAddresses(params);
}
getPayer(payerId: number): Observable<Result<PayerDTO>> {

View File

@@ -1,3 +1,5 @@
import { DialogOfString } from '@swagger/crm';
export interface Result<T> {
/** Ergebnis */
result?: T;
@@ -13,4 +15,6 @@ export interface Result<T> {
/** Fehlerhafte Daten */
invalidProperties?: { [key: string]: string };
dialog?: DialogOfString;
}

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core';
import {
BranchService,
ChangeStockStatusCodeValues,
HistoryDTO,
NotificationChannel,
@@ -11,8 +12,10 @@ import {
OrderService,
ReceiptService,
StatusValues,
StockStatusCodeService,
ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO,
ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO,
VATService,
} from '@swagger/oms';
import { memorize } from '@utils/common';
import { Observable } from 'rxjs';
@@ -23,6 +26,9 @@ export class DomainOmsService {
constructor(
private orderService: OrderService,
private receiptService: ReceiptService,
private branchService: BranchService,
private vatService: VATService,
private stockStatusCodeService: StockStatusCodeService,
private _orderCheckoutService: OrderCheckoutService
) {}
@@ -37,7 +43,7 @@ export class DomainOmsService {
}
getBranches() {
return this.orderService.OrderGetBranches({});
return this.branchService.BranchGetBranches({});
}
getHistory(orderItemSubsetId: number): Observable<HistoryDTO> {
@@ -62,13 +68,13 @@ export class DomainOmsService {
@memorize()
getVATs() {
return this.orderService.OrderGetVATs({}).pipe(map((response) => response.result));
return this.vatService.VATGetVATs({}).pipe(map((response) => response.result));
}
// ttl 4 Stunden
@memorize({ ttl: 14400000 })
getStockStatusCodes({ supplierId, eagerLoading = 0 }: { supplierId: number; eagerLoading?: number }) {
return this.orderService.OrderGetStockStatusCodes({ supplierId, eagerLoading }).pipe(
return this.stockStatusCodeService.StockStatusCodeGetStockStatusCodes({ supplierId, eagerLoading }).pipe(
map((response) => response.result),
shareReplay()
);
@@ -152,6 +158,10 @@ export class DomainOmsService {
.pipe(map((response) => response.result));
}
setPreferredPickUpDate({ data }: { data: { [key: string]: string } }) {
return this.orderService.OrderSetPreferredPickUpDate({ data });
}
changeOrderItemStatus(data: OrderService.OrderChangeStatusParams) {
return this.orderService.OrderChangeStatus(data);
}

View File

@@ -11,6 +11,7 @@ import {
PrintRequestOfIEnumerableOfPriceQRCodeDTO,
PrintService,
ResponseArgs,
LoyaltyCardPrintService,
} from '@swagger/print';
import { Observable, of } from 'rxjs';
import { catchError, filter, map, switchMap, timeout } from 'rxjs/operators';
@@ -25,7 +26,8 @@ export class DomainPrinterService {
private oMSPrintService: OMSPrintService,
private catalogPrintService: CatalogPrintService,
private checkoutPrintService: CheckoutPrintService,
private eisPublicDocumentService: EISPublicDocumentService
private eisPublicDocumentService: EISPublicDocumentService,
private _loyaltyCardPrintService: LoyaltyCardPrintService
) {}
getAvailablePrinters(): Observable<Printer[] | { error: string }> {
@@ -143,6 +145,13 @@ export class DomainPrinterService {
});
}
printKubiAgb({ p4mCode, printer }: { p4mCode: string; printer: string }) {
return this._loyaltyCardPrintService.LoyaltyCardPrintPrintLoyaltyCardAGB({
printer,
data: p4mCode,
});
}
printProduct({ item, printer }: { item: ItemDTO; printer: string }): Observable<ResponseArgs> {
const params = <PrintRequestOfIEnumerableOfItemDTO>{
printer: printer,

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { EISPublicService, FileDTO, ProcessingStatus, QueryTokenDTO } from '@swagger/eis';
import { DisplayInfoDTO, EISPublicService, FileDTO, ProcessingStatus, QueryTokenDTO } from '@swagger/eis';
import { DateAdapter } from '@ui/common';
import { memorize } from '@utils/common';
import { map, shareReplay, switchMap } from 'rxjs/operators';
@@ -279,4 +279,80 @@ export class DomainTaskCalendarService {
)
);
}
moveRemovedToEnd(a: DisplayInfoDTO, b: DisplayInfoDTO) {
const statusA = this.getProcessingStatusList(a)?.includes('Removed');
const statusB = this.getProcessingStatusList(b)?.includes('Removed');
if (statusA && statusB) {
return 0;
} else if (statusA && !statusB) {
return 1;
} else if (!statusA && statusB) {
return -1;
}
return 0;
}
/**
* Returns an Array of DisplayInfoDTO, sorted by ProcessingStatus
* Ignores Overdue if Task is already Completed
* Compared DisploayInfoDTO is of Type Task and Info Or PreInfo then sort by Type
* @param items DisplayInfoDTO Array to sort
* @param order Processing Status Order
* @returns DisplayInfoDTO Array ordered by Processing Status anf Type
*/
sort(items: DisplayInfoDTO[], order: ProcessingStatusList) {
let result = [...items];
const reversedOrder = [...order].reverse();
for (const status of reversedOrder) {
result = result?.sort((a, b) => {
const statusA = this.getProcessingStatusList(a);
const statusB = this.getProcessingStatusList(b);
// Ignore Overdue when it is already Completed
if (status === 'Overdue' && statusA.includes('Completed')) {
return 0;
}
const aHasStatus = statusA.includes(status);
const bHasStatus = statusB.includes(status);
if (aHasStatus && bHasStatus) {
// If it has the same ProcessingStatus then Sort by Type
const aType = this.getInfoType(a);
const bType = this.getInfoType(b);
if (aType !== bType) {
if (aType === 'Info' || aType === 'PreInfo') {
return -1;
} else {
return 1;
}
}
if (statusB.includes('Completed')) {
return -1;
}
return 0;
} else if (aHasStatus && !bHasStatus) {
return -1;
} else if (!aHasStatus && bHasStatus) {
return 1;
}
return 0;
});
}
return result;
}
getDateGroupKey(d: string) {
// Get Date as string key to ignore time for grouping
const date = new Date(d);
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
}
}

View File

@@ -6,6 +6,7 @@ import { PlatformModule } from '@angular/cdk/platform';
import { Config, ConfigModule, JsonConfigLoader } from '@core/config';
import { AuthModule, AuthService } from '@core/auth';
import { CoreCommandModule } from '@core/command';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@@ -30,6 +31,7 @@ import { IsaLogProvider } from './providers';
import { IsaErrorHandler } from './providers/isa.error-handler';
import { ScanAdapterModule } from '@adapter/scan';
import { RootStateService } from './store/root-state.service';
import * as Commands from './commands';
registerLocaleData(localeDe, localeDeExtra);
registerLocaleData(localeDe, 'de', localeDeExtra);
@@ -70,6 +72,7 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
useConfigLoader: JsonConfigLoader,
jsonConfigLoaderUrl: '/config/config.json',
}),
CoreCommandModule.forRoot(Object.values(Commands)),
CoreLoggerModule.forRoot(),
AppStoreModule,
AuthModule.forRoot(),

View File

@@ -0,0 +1,14 @@
import { Injectable } from '@angular/core';
import { ActionHandler } from '@core/command';
/** Dummy Command um Fehlermeldungen aus dem Diloag zu verhinden */
@Injectable()
export class CloseCommand extends ActionHandler<any> {
constructor() {
super('CLOSE');
}
handler(ctx: any): any {
return ctx;
}
}

View File

@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { ActionHandler } from '@core/command';
import { Result } from '@domain/defs';
import { CustomerInfoDTO } from '@swagger/crm';
@Injectable()
export class CreateCustomerCommand extends ActionHandler<Result<CustomerInfoDTO[]>> {
constructor(private _router: Router, private _application: ApplicationService) {
super('CREATE_CUSTOMER');
}
async handler(data: Result<CustomerInfoDTO[]>): Promise<Result<CustomerInfoDTO[]>> {
let customerType: string;
if (data.result.length > 0) {
const customerInfo = data.result[0];
if (customerInfo.features) {
if (customerInfo.features.some((f) => f.key === 'store')) {
customerType = 'store';
}
if (customerInfo.features.some((f) => f.key === 'webshop')) {
customerType = 'webshop';
}
}
}
if (!customerType) {
customerType = 'store';
}
await this._router.navigate(['/kunde', this._application.activatedProcessId, 'customer', 'create', customerType]);
return data;
}
}

View File

@@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { ActionHandler } from '@core/command';
import { Result } from '@domain/defs';
import { CustomerInfoDTO } from '@swagger/crm';
import { encodeFormData, mapCustomerInfoDtoToCustomerCreateFormData } from 'apps/page/customer/src/lib/create-customer';
@Injectable()
export class CreateKubiCustomerCommand extends ActionHandler<Result<CustomerInfoDTO[]>> {
constructor(private _router: Router, private _application: ApplicationService) {
super('CREATE_KUBI_CUSTOMER');
}
async handler(data: Result<CustomerInfoDTO[]>): Promise<Result<CustomerInfoDTO[]>> {
let customerType: string;
let formData: string;
if (data.result.length > 0) {
const customerInfo = data.result[0];
const fd = mapCustomerInfoDtoToCustomerCreateFormData(customerInfo);
formData = encodeFormData({
...fd,
agb: false,
});
if (customerInfo.features) {
if (customerInfo.features.some((f) => f.key === 'store')) {
customerType = 'store';
}
if (customerInfo.features.some((f) => f.key === 'webshop')) {
customerType = 'webshop';
}
}
}
if (!customerType) {
customerType = 'store';
}
await this._router.navigate(['/kunde', this._application.activatedProcessId, 'customer', 'create', `${customerType}-p4m`], {
queryParams: { formData },
});
return data;
}
}

View File

@@ -0,0 +1,4 @@
export * from './close.command';
export * from './create-customer.command';
export * from './create-kubi-customer.command';
export * from './print-kubi-agb.command';

View File

@@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { ActionHandler } from '@core/command';
import { Result } from '@domain/defs';
import { DomainPrinterService } from '@domain/printer';
import { PrintModalComponent, PrintModalData } from '@modal/printer';
import { CustomerInfoDTO } from '@swagger/crm';
import { UiModalService } from '@ui/modal';
@Injectable()
export class PrintKubiCustomerCommand extends ActionHandler<Result<CustomerInfoDTO[]>> {
constructor(private _uiModal: UiModalService, private _printerService: DomainPrinterService) {
super('PRINT_KUBI_AGB');
}
async handler(data: Result<CustomerInfoDTO[]>): Promise<Result<CustomerInfoDTO[]>> {
const customerInfo = data.result ? data.result[0] : undefined;
let p4mCode: string;
if (customerInfo) {
p4mCode = customerInfo.features.find((f) => f.key === 'p4mUser').value;
}
await this._uiModal
.open({
content: PrintModalComponent,
config: { showScrollbarY: false },
data: {
printerType: 'Label',
print: (printer) => this._printerService.printKubiAgb({ printer, p4mCode }).toPromise(),
} as PrintModalData,
})
.afterClosed$.toPromise();
return data;
}
}

View File

@@ -1,8 +1,9 @@
import { HttpErrorInterceptor } from './http-error.interceptor';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
import { UiErrorModalComponent, UiMessageModalComponent, UiModalService } from '@ui/modal';
import { throwError } from 'rxjs';
import { UiMessageModalComponent, UiModalResult, UiModalService } from '@ui/modal';
import { of, Subject, throwError } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { AuthService } from '@core/auth';
describe('HttpErrorInterceptor', () => {
let spectator: SpectatorService<HttpErrorInterceptor>;
@@ -11,13 +12,17 @@ describe('HttpErrorInterceptor', () => {
const createService = createServiceFactory({
service: HttpErrorInterceptor,
mocks: [UiModalService],
mocks: [UiModalService, AuthService],
});
beforeEach(() => {
spectator = createService();
httpErrorInterceptor = spectator.service;
modalMock = spectator.inject(UiModalService);
modalMock.open.and.returnValue({
afterClosed$: of({} as UiModalResult<any>),
} as any);
});
it('should be created', () => {
@@ -31,6 +36,7 @@ describe('HttpErrorInterceptor', () => {
statusText: '',
url: '',
});
const handleErrorSpy = spyOn(httpErrorInterceptor, 'handleError').and.callThrough();
httpErrorInterceptor.intercept(null, { handle: () => throwError(error) }).subscribe({
error: () => {

View File

@@ -2,11 +2,12 @@ import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
import { catchError } from 'rxjs/operators';
import { catchError, mergeMap, tap } from 'rxjs/operators';
import { AuthService } from '@core/auth';
@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
constructor(private _modal: UiModalService) {}
constructor(private _modal: UiModalService, private _auth: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(catchError((error: HttpErrorResponse, caught: any) => this.handleError(error)));
@@ -14,11 +15,26 @@ export class HttpErrorInterceptor implements HttpInterceptor {
handleError(error: HttpErrorResponse): Observable<any> {
if (error.status === 0) {
this._modal.open({
content: UiMessageModalComponent,
title: 'Sie sind offline, keine Verbindung zum Netzwerk',
data: { message: 'Bereits geladene Inhalte werden angezeigt. Interaktionen sind aktuell nicht möglich.' },
});
return this._modal
.open({
content: UiMessageModalComponent,
title: 'Sie sind offline, keine Verbindung zum Netzwerk',
data: { message: 'Bereits geladene Inhalte werden angezeigt. Interaktionen sind aktuell nicht möglich.' },
})
.afterClosed$.pipe(mergeMap(() => throwError(error)));
} else if (error.status === 401) {
return this._modal
.open({
content: UiMessageModalComponent,
title: 'Sie sind nicht mehr angemeldet',
data: { message: 'Sie werden neu angemeldet' },
})
.afterClosed$.pipe(
tap(() => {
this._auth.login();
}),
mergeMap(() => throwError(error))
);
}
return throwError(error);

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -0,0 +1,219 @@
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: normal;
src: url('./Open_Sans-400-cyrillic-ext1.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: normal;
src: url('./Open_Sans-400-cyrillic2.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: normal;
src: url('./Open_Sans-400-greek-ext3.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: normal;
src: url('./Open_Sans-400-greek4.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: normal;
src: url('./Open_Sans-400-hebrew5.woff2') format('woff2');
unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: normal;
src: url('./Open_Sans-400-vietnamese6.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: normal;
src: url('./Open_Sans-400-latin-ext7.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: normal;
src: url('./Open_Sans-400-latin8.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: normal;
src: url('./Open_Sans-600-cyrillic-ext9.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: normal;
src: url('./Open_Sans-600-cyrillic10.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: normal;
src: url('./Open_Sans-600-greek-ext11.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: normal;
src: url('./Open_Sans-600-greek12.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: normal;
src: url('./Open_Sans-600-hebrew13.woff2') format('woff2');
unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: normal;
src: url('./Open_Sans-600-vietnamese14.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: normal;
src: url('./Open_Sans-600-latin-ext15.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: normal;
src: url('./Open_Sans-600-latin16.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: normal;
src: url('./Open_Sans-700-cyrillic-ext17.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: normal;
src: url('./Open_Sans-700-cyrillic18.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: normal;
src: url('./Open_Sans-700-greek-ext19.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: normal;
src: url('./Open_Sans-700-greek20.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: normal;
src: url('./Open_Sans-700-hebrew21.woff2') format('woff2');
unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: normal;
src: url('./Open_Sans-700-vietnamese22.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: normal;
src: url('./Open_Sans-700-latin-ext23.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: normal;
src: url('./Open_Sans-700-latin24.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

@@ -212,4 +212,7 @@
<g id="shiiping_document" transform="matrix(1.11057,0,0,1.11057,2.2921,-1.02614)">
<path d="M20.713,0.924L2.151,0.924C1.824,0.924 1.517,1.048 1.288,1.283C1.06,1.512 0.936,1.819 0.936,2.139L0.936,25.634C0.936,26.307 1.478,26.849 2.151,26.849C2.151,26.849 2.758,26.849 2.758,26.849C2.758,26.849 2.758,28.523 2.758,28.523C2.758,29.196 3.3,29.738 3.973,29.738L22.535,29.738C23.208,29.738 23.75,29.196 23.75,28.523L23.75,5.028C23.75,4.361 23.202,3.819 22.535,3.819C22.535,3.819 21.928,3.819 21.928,3.819C21.928,3.819 21.928,2.139 21.928,2.139C21.928,1.466 21.386,0.924 20.713,0.924ZM22.471,5.098L22.471,28.459L4.037,28.459L4.037,26.849C4.037,26.849 20.713,26.849 20.713,26.849C21.341,26.849 21.855,26.371 21.921,25.766L21.928,25.64C21.928,25.64 21.828,25.734 21.828,25.734L21.928,25.634L21.928,5.098L22.471,5.098ZM2.215,2.197L2.215,25.57L20.649,25.57L20.649,2.197L2.215,2.197ZM9.403,22.1L3.397,22.1C3.042,22.1 2.758,22.384 2.758,22.739C2.758,23.094 3.042,23.379 3.397,23.379C3.397,23.379 9.403,23.379 9.403,23.379C9.758,23.379 10.043,23.094 10.043,22.739C10.043,22.384 9.758,22.1 9.403,22.1ZM14.522,19.469L3.277,19.469C2.996,19.469 2.758,19.747 2.758,20.108C2.758,20.469 2.996,20.747 3.277,20.747L14.522,20.747C14.803,20.747 15.041,20.469 15.041,20.108C15.041,19.747 14.803,19.469 14.522,19.469ZM12.352,16.843L3.397,16.843C3.042,16.843 2.758,17.127 2.758,17.483C2.758,17.838 3.042,18.122 3.397,18.122C3.397,18.122 12.352,18.122 12.352,18.122C12.707,18.122 12.992,17.838 12.992,17.483C12.992,17.127 12.707,16.843 12.352,16.843ZM14.478,14.212L3.322,14.212C3.013,14.212 2.758,14.492 2.758,14.851C2.758,15.211 3.013,15.491 3.322,15.491C3.322,15.491 14.478,15.491 14.478,15.491C14.786,15.491 15.041,15.211 15.041,14.851C15.041,14.492 14.786,14.212 14.478,14.212ZM9.593,11.587L3.298,11.587C3.004,11.587 2.758,11.866 2.758,12.226C2.758,12.587 3.004,12.866 3.298,12.866C3.298,12.866 9.593,12.866 9.593,12.866C9.887,12.866 10.133,12.587 10.133,12.226C10.133,11.866 9.887,11.587 9.593,11.587ZM15.163,3.657C12.86,3.657 10.987,5.53 10.987,7.833C10.987,10.136 12.86,12.009 15.163,12.009C17.466,12.009 19.345,10.136 19.345,7.833C19.345,5.53 17.472,3.657 15.163,3.657ZM15.163,4.936C16.762,4.936 18.066,6.234 18.066,7.833C18.066,9.432 16.768,10.73 15.163,10.73C13.564,10.73 12.266,9.432 12.266,7.833C12.266,6.234 13.564,4.936 15.163,4.936ZM9.525,8.956L3.366,8.956C3.03,8.956 2.758,9.238 2.758,9.595C2.758,9.952 3.03,10.234 3.366,10.234L9.525,10.234C9.861,10.234 10.133,9.952 10.133,9.595C10.133,9.238 9.861,8.956 9.525,8.956ZM14.821,7.954L14.61,7.634C14.459,7.403 14.127,7.319 13.867,7.453C13.596,7.593 13.507,7.902 13.664,8.142C13.664,8.142 14.228,9.011 14.228,9.011C14.313,9.141 14.457,9.233 14.626,9.251C14.651,9.256 14.676,9.256 14.701,9.256C14.846,9.256 14.984,9.207 15.087,9.116C15.087,9.116 17.248,7.196 17.248,7.196C17.469,6.999 17.469,6.68 17.248,6.483C17.037,6.291 16.693,6.291 16.481,6.479L14.821,7.954ZM2.164,2.197C2.16,2.197 2.157,2.197 2.154,2.197C2.152,2.197 2.151,2.197 2.151,2.197L2.164,2.197ZM2.151,2.097L2.151,2.097L2.151,2.197L2.151,2.097Z" style="fill:rgb(0,4,0);"/>
</g>
<g id="filter_new" transform="matrix(1.77778,0,0,2,0.000431998,3.99944)">
<path d="M7,12L11,12L11,10L7,10L7,12ZM0,0L0,2L18,2L18,0L0,0ZM3,7L15,7L15,5L3,5L3,7Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -17,19 +17,19 @@
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
},
"@swagger/cat": {
"rootUrl": "https://isa-test.paragon-data.net/catsearch/v5"
"rootUrl": "https://isa-test.paragon-data.net/catsearch/v6"
},
"@swagger/av": {
"rootUrl": "https://isa-test.paragon-data.net/ava/v4"
"rootUrl": "https://isa-test.paragon-data.net/ava/v6"
},
"@swagger/checkout": {
"rootUrl": "https://isa-test.paragon-data.net/checkout/v3"
"rootUrl": "https://isa-test.paragon-data.net/checkout/v6"
},
"@swagger/crm": {
"rootUrl": "https://isa-test.paragon-data.net/crm/v3"
"rootUrl": "https://isa-test.paragon-data.net/crm/v6"
},
"@swagger/oms": {
"rootUrl": "https://isa-test.paragon-data.net/oms/v4"
"rootUrl": "https://isa-test.paragon-data.net/oms/v6"
},
"@swagger/print": {
"rootUrl": "https://isa-test.paragon-data.net/print/v1"

View File

@@ -17,19 +17,19 @@
"rootUrl": "https://isa-integration.paragon-data.net/isa/v1"
},
"@swagger/cat": {
"rootUrl": "https://isa-integration.paragon-data.net/catsearch/v5"
"rootUrl": "https://isa-integration.paragon-data.net/catsearch/v6"
},
"@swagger/av": {
"rootUrl": "https://isa-integration.paragon-data.net/ava/v4"
"rootUrl": "https://isa-integration.paragon-data.net/ava/v6"
},
"@swagger/checkout": {
"rootUrl": "https://isa-integration.paragon-data.net/checkout/v3"
"rootUrl": "https://isa-integration.paragon-data.net/checkout/v6"
},
"@swagger/crm": {
"rootUrl": "https://isa-integration.paragon-data.net/crm/v3"
"rootUrl": "https://isa-integration.paragon-data.net/crm/v6"
},
"@swagger/oms": {
"rootUrl": "https://isa-integration.paragon-data.net/oms/v4"
"rootUrl": "https://isa-integration.paragon-data.net/oms/v6"
},
"@swagger/print": {
"rootUrl": "https://isa-integration.paragon-data.net/print/v1"

View File

@@ -17,19 +17,19 @@
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
},
"@swagger/cat": {
"rootUrl": "https://isa-test.paragon-data.net/catsearch/v5"
"rootUrl": "https://isa-test.paragon-data.net/catsearch/v6"
},
"@swagger/av": {
"rootUrl": "https://isa-test.paragon-data.net/ava/v4"
"rootUrl": "https://isa-test.paragon-data.net/ava/v6"
},
"@swagger/checkout": {
"rootUrl": "https://isa-test.paragon-data.net/checkout/v3"
"rootUrl": "https://isa-test.paragon-data.net/checkout/v6"
},
"@swagger/crm": {
"rootUrl": "https://isa-test.paragon-data.net/crm/v3"
"rootUrl": "https://isa-test.paragon-data.net/crm/v6"
},
"@swagger/oms": {
"rootUrl": "https://isa-test.paragon-data.net/oms/v4"
"rootUrl": "https://isa-test.paragon-data.net/oms/v6"
},
"@swagger/print": {
"rootUrl": "https://isa-test.paragon-data.net/print/v1"
@@ -60,4 +60,4 @@
}
},
"checkForUpdates": 3600000
}
}

View File

@@ -17,19 +17,19 @@
"rootUrl": "https://isa.paragon-systems.de/isa/v1"
},
"@swagger/cat": {
"rootUrl": "https://isa.paragon-systems.de/catsearch/v5"
"rootUrl": "https://isa.paragon-systems.de/catsearch/v6"
},
"@swagger/av": {
"rootUrl": "https://isa.paragon-systems.de/ava/v4"
"rootUrl": "https://isa.paragon-systems.de/ava/v6"
},
"@swagger/checkout": {
"rootUrl": "https://isa.paragon-systems.de/checkout/v3"
"rootUrl": "https://isa.paragon-systems.de/checkout/v6"
},
"@swagger/crm": {
"rootUrl": "https://isa.paragon-systems.de/crm/v3"
"rootUrl": "https://isa.paragon-systems.de/crm/v6"
},
"@swagger/oms": {
"rootUrl": "https://isa.paragon-systems.de/oms/v4"
"rootUrl": "https://isa.paragon-systems.de/oms/v6"
},
"@swagger/print": {
"rootUrl": "https://isa.paragon-systems.de/print/v1"

View File

@@ -17,19 +17,19 @@
"rootUrl": "https://isa-staging.paragon-systems.de/isa/v1"
},
"@swagger/cat": {
"rootUrl": "https://isa-staging.paragon-systems.de/catsearch/v5"
"rootUrl": "https://isa-staging.paragon-systems.de/catsearch/v6"
},
"@swagger/av": {
"rootUrl": "https://isa-staging.paragon-systems.de/ava/v4"
"rootUrl": "https://isa-staging.paragon-systems.de/ava/v6"
},
"@swagger/checkout": {
"rootUrl": "https://isa-staging.paragon-systems.de/checkout/v3"
"rootUrl": "https://isa-staging.paragon-systems.de/checkout/v6"
},
"@swagger/crm": {
"rootUrl": "https://isa-staging.paragon-systems.de/crm/v3"
"rootUrl": "https://isa-staging.paragon-systems.de/crm/v6"
},
"@swagger/oms": {
"rootUrl": "https://isa-staging.paragon-systems.de/oms/v4"
"rootUrl": "https://isa-staging.paragon-systems.de/oms/v6"
},
"@swagger/print": {
"rootUrl": "https://isa-staging.paragon-systems.de/print/v1"

View File

@@ -17,19 +17,19 @@
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
},
"@swagger/cat": {
"rootUrl": "https://isa-test.paragon-data.net/catsearch/v5"
"rootUrl": "https://isa-test.paragon-data.net/catsearch/v6"
},
"@swagger/av": {
"rootUrl": "https://isa-test.paragon-data.net/ava/v4"
"rootUrl": "https://isa-test.paragon-data.net/ava/v6"
},
"@swagger/checkout": {
"rootUrl": "https://isa-test.paragon-data.net/checkout/v3"
"rootUrl": "https://isa-test.paragon-data.net/checkout/v6"
},
"@swagger/crm": {
"rootUrl": "https://isa-test.paragon-data.net/crm/v3"
"rootUrl": "https://isa-test.paragon-data.net/crm/v6"
},
"@swagger/oms": {
"rootUrl": "https://isa-test.paragon-data.net/oms/v4"
"rootUrl": "https://isa-test.paragon-data.net/oms/v6"
},
"@swagger/print": {
"rootUrl": "https://isa-test.paragon-data.net/print/v1"

View File

@@ -6,10 +6,11 @@
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700" rel="stylesheet" />
<link href="/assets/fonts/fonts.css" rel="stylesheet" />
<link rel="manifest" href="manifest.webmanifest" />
<meta name="theme-color" content="#1976d2" />
</head>
<body>
<app-root>
<div class="grid place-items-center h-screen">

View File

@@ -1,5 +1,8 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import * as moment from 'moment';
moment.locale('de');
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

View File

@@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core';
import { AvailabilityByBranchDTO } from 'apps/domain/availability/src/lib/defs/availability-by-branch-dto.model';
import { AvailabilityByBranchDTO } from '@domain/availability';
@Pipe({
name: 'inStock',

View File

@@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core';
import { AvailabilityByBranchDTO } from 'apps/domain/availability/src/lib/defs/availability-by-branch-dto.model';
import { AvailabilityByBranchDTO } from '@domain/availability';
@Pipe({
name: 'stockInfo',

View File

@@ -86,7 +86,7 @@
<div class="availability-icons">
<div class="fetching xsmall" *ngIf="store.fetchingTakeAwayAvailability$ | async; else showAvailabilityTakeAwayIcon"></div>
<ng-template #showAvailabilityTakeAwayIcon>
<ui-icon *ngIf="store.isTakeAwayAvailabilityAvailable$ | async" icon="shopping_bag" size="18px"></ui-icon>
<ui-icon *ngIf="store.isTakeAwayAvailabilityAvailable$ | async" icon="shopping_bag" size="18px"> </ui-icon>
</ng-template>
<div class="fetching xsmall" *ngIf="store.fetchingPickUpAvailability$ | async; else showAvailabilityPickUpIcon"></div>
@@ -104,7 +104,7 @@
*ngIf="store.fetchingDeliveryB2BAvailability$ | async; else showAvailabilityDeliveryB2BIcon"
></div>
<ng-template #showAvailabilityDeliveryB2BIcon>
<ui-icon *ngIf="showDeliveryB2BTruck$ | async" class="truck_b2b" icon="truck_b2b" size="40px"></ui-icon>
<ui-icon *ngIf="showDeliveryB2BTruck$ | async" class="truck_b2b" icon="truck_b2b" size="40px"> </ui-icon>
</ng-template>
<span *ngIf="store.isDownload$ | async" class="download-icon">

View File

@@ -2,6 +2,7 @@ import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ElementRef } fro
import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainPrinterService } from '@domain/printer';
import { ItemDTO as PrinterItemDTO } from '@swagger/print';
import { PrintModalComponent, PrintModalData } from '@modal/printer';
import { AvailabilityDTO, BranchDTO } from '@swagger/checkout';
import { UiModalService } from '@ui/modal';
@@ -148,7 +149,8 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
content: PrintModalComponent,
data: {
printerType: 'Label',
print: (printer) => this.domainPrinterService.printProduct({ item, printer }).toPromise(),
// TODO: remove item: item as PrinterItemDTO when the backend is fixed
print: (printer) => this.domainPrinterService.printProduct({ item: item as PrinterItemDTO, printer }).toPromise(),
} as PrintModalData,
config: {
panelClass: [],

View File

@@ -6,6 +6,7 @@ import { DomainAvailabilityService, ItemData } from '@domain/availability';
import { DomainCatalogService } from '@domain/catalog';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ItemDTO, ResponseArgsOfItemDTO } from '@swagger/cat';
import { AvailabilityDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { combineLatest, Observable, of } from 'rxjs';
import { catchError, filter, first, map, shareReplay, switchMap, tap } from 'rxjs/operators';
@@ -113,11 +114,22 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
//#region Abholung
readonly fetchingPickUpAvailability$ = this.select((s) => s.fetchingPickUpAvailability);
readonly pickUpAvailability$ = combineLatest([this.itemData$, this.branch$, this.isDownload$]).pipe(
readonly pickUpAvailability$: Observable<AvailabilityDTO> = combineLatest([this.itemData$, this.branch$, this.isDownload$]).pipe(
tap(() => this.patchState({ fetchingPickUpAvailability: true, fetchingPickUpAvailabilityError: undefined })),
switchMap(([item, branch, isDownload]) =>
!!item && !!branch && !isDownload
? this.domainAvailabilityService.getPickUpAvailability({ item, branch, quantity: 1 }).pipe(
map((av) => {
if (av[1].availableFor) {
if ((av[1].availableFor & 2) === 2) {
return av[0];
} else {
return undefined;
}
} else {
return av[0];
}
}),
catchError((err) => {
console.error('getPickUpAvailability failed.', err);
this.patchState({ fetchingPickUpAvailabilityError: 'Fehler beim laden der Abholung.' });
@@ -235,11 +247,16 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
]).pipe(
map(([item, isDownload, pickupAvailability, deliveryDigAvailability, downloadAvailability]) => {
const availability = isDownload ? downloadAvailability : pickupAvailability || deliveryDigAvailability;
return item?.catalogAvailability?.supplier === 'S' && !isDownload
? `${item?.catalogAvailability?.ssc} - ${item?.catalogAvailability?.sscText}`
: availability?.ssc || availability?.sscText
? `${availability?.ssc} - ${availability?.sscText}`
: 'Keine Lieferanten vorhanden';
if (item?.catalogAvailability?.supplier === 'S' && !isDownload) {
return [item?.catalogAvailability?.ssc, item?.catalogAvailability?.sscText].filter((f) => !!f).join(' - ');
}
if (availability?.ssc || availability?.sscText) {
return [availability?.ssc, availability?.sscText].filter((f) => !!f).join(' - ');
}
return 'Keine Lieferanten vorhanden';
})
);

View File

@@ -251,6 +251,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
catalogProductNumber: String(item?.id),
...item?.product,
},
itemType: item.type,
promotion: { points: item?.promoPoints },
};

View File

@@ -54,9 +54,9 @@
[min]="minDate"
[disabledDaysOfWeek]="[0]"
[selected]="estimatedShippingDate$ | async"
saveLabel="Übernehmen"
(save)="changeEstimatedShippingDate($event); uiDatepicker.close()"
>
<ng-container #content>Übernehmen</ng-container>
</ui-datepicker>
</ui-form-control>
</div>

View File

@@ -305,6 +305,7 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
destination: {
data: { target: 1, targetBranch: { id: branch.id } },
},
itemType: this.item.type,
};
if (update) {

View File

@@ -526,9 +526,17 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
availabilities['download'] = downloadAvailability;
}
if (pickupAvailability && this.availabilityService.isAvailable({ availability: pickupAvailability })) {
availableOptions.push('pick-up');
availabilities['pick-up'] = pickupAvailability;
if (pickupAvailability && this.availabilityService.isAvailable({ availability: pickupAvailability[0] })) {
if (pickupAvailability[1].availableFor) {
if ((pickupAvailability[1].availableFor & 2) === 2) {
availableOptions.push('pick-up');
availabilities['pick-up'] = pickupAvailability[0];
}
} else {
availableOptions.push('pick-up');
availabilities['pick-up'] = pickupAvailability[0];
}
if (!customerFeatures?.webshop && this.availabilityService.isAvailable({ availability: b2bAvailability })) {
availableOptions.push('b2b-delivery');
availabilities['b2b-delivery'] = b2bAvailability;
@@ -648,6 +656,7 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
},
quantity,
})
.pipe(map((av) => av[0]))
.toPromise();
break;
case 'Versand':

View File

@@ -45,7 +45,8 @@
<div class="item-date" *ngIf="orderType === 'Abholung'">Abholung ab {{ item?.availability?.estimatedShippingDate | date }}</div>
<div class="item-date" *ngIf="orderType === 'Versand' || orderType === 'B2B-Versand' || orderType === 'DIG-Versand'">
<ng-container *ngIf="item?.availability?.estimatedDelivery; else estimatedShippingDate">
Zustellung zwischen {{ (item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
Zustellung zwischen {{ (item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }}
und
{{ (item?.availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
</ng-container>
<ng-template #estimatedShippingDate> Versand {{ item?.availability?.estimatedShippingDate | date }} </ng-template>
@@ -90,7 +91,7 @@
<button
[disabled]="(loadingOnQuantityChangeById$ | async) === item?.id || (loadingOnItemChangeById$ | async) === item?.id"
(click)="onChangeItem()"
*ngIf="(isDummy$ | async) || (hasOrderType$ | async)"
*ngIf="canEdit$ | async"
>
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id">
Ändern

View File

@@ -3,7 +3,7 @@ import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import { ComponentStore } from '@ngrx/component-store';
import { ShoppingCartItemDTO } from '@swagger/checkout';
import { ItemType, ShoppingCartItemDTO } from '@swagger/checkout';
import { combineLatest } from 'rxjs';
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
@@ -83,6 +83,15 @@ export class ShoppingCartItemComponent extends ComponentStore<ShoppingCartItemCo
shareReplay()
);
canEdit$ = combineLatest([this.isDummy$, this.hasOrderType$, this.item$]).pipe(
map(([isDummy, hasOrderType, item]) => {
if (item.itemType === (66560 as ItemType)) {
return false;
}
return isDummy || hasOrderType;
})
);
quantityRange$ = combineLatest([this.orderType$, this.item$]).pipe(
map(([orderType, item]) => (orderType === 'Rücklage' ? item.availability?.inStock : 999))
);

View File

@@ -83,7 +83,20 @@
<ng-container *ngSwitchCase="'Abholung'">
<span class="supplier">{{ (order?.subsetItems)[0].supplierLabel }}</span>
<span class="separator">|</span>
<span class="order-type">Abholung ab {{ (order?.subsetItems)[0]?.estimatedShippingDate | date }}</span>
<span class="order-type"
>Abholung ab {{ (order?.subsetItems)[0]?.estimatedShippingDate | date }}
<ng-container [ngTemplateOutlet]="abholfrist" [ngTemplateOutletContext]="{ order: order }"> </ng-container>
</span>
</ng-container>
<ng-container *ngSwitchCase="'Rücklage'">
<ng-container *ngIf="(order?.subsetItems)[0].supplierLabel; let supplierLabel">
<span class="supplier">{{ supplierLabel }}</span>
<span class="separator">|</span>
</ng-container>
<span class="order-type"
>{{ order?.features?.orderType }}
<ng-container [ngTemplateOutlet]="abholfrist" [ngTemplateOutletContext]="{ order: order }"> </ng-container>
</span>
</ng-container>
<ng-container *ngSwitchCase="['Versand', 'B2B-Versand', 'DIG-Versand'].indexOf(order?.features?.orderType) > -1">
<span class="supplier">{{ (order?.subsetItems)[0].supplierLabel }}</span>
@@ -135,3 +148,34 @@
</div>
</div>
</div>
<ng-template #abholfrist let-order="order">
<div *ngIf="!(updatingPreferredPickUpDate$ | async)[(order?.subsetItems)[0].id]" class="inline-flex">
<button [uiOverlayTrigger]="deadlineDatepicker" #deadlineDatepickerTrigger="uiOverlayTrigger" class="cta-pickup-deadline">
<span class="mx-2">bis</span>
<strong>
{{ ((order?.subsetItems)[0]?.preferredPickUpDate | date: 'dd.MM.yy') || 'Auswählen' }}
</strong>
<ui-icon class="ml-2" [rotate]="deadlineDatepickerTrigger.opened ? '270deg' : '90deg'" icon="arrow_head"> </ui-icon>
</button>
<ui-datepicker
#deadlineDatepicker
yPosition="below"
xPosition="after"
[xOffset]="8"
[min]="minDateDatepicker"
[disabledDaysOfWeek]="[0]"
[(selected)]="selectedDate"
>
<div #content class="grid grid-flow-row gap-2">
<button
class="rounded-full font-bold text-white bg-brand py-px-15 px-px-25"
(click)="updatePreferredPickUpDate(undefined, selectedDate); deadlineDatepickerTrigger.close()"
>
Für den Warenkorb festlegen
</button>
</div>
</ui-datepicker>
</div>
<div class="fetching" *ngIf="!!(updatingPreferredPickUpDate$ | async)[(order?.subsetItems)[0].id]"></div>
</ng-template>

View File

@@ -2,6 +2,23 @@
@apply block box-border;
}
@keyframes load {
0% {
opacity: 0;
}
30% {
opacity: 0.5;
}
100% {
opacity: 0;
}
}
.fetching {
@apply w-28 h-px-20 bg-wild-blue-yonder ml-4;
animation: load 1s linear infinite;
}
.card {
@apply bg-white rounded-card shadow-card overflow-scroll;
height: calc(100vh - 410px);
@@ -72,6 +89,30 @@
}
}
.cta-pickup-deadline {
@apply flex flex-row items-center;
}
.abholfrist-wrapper {
@apply flex flex-row my-1;
.label {
width: 145px;
}
.value {
@apply flex flex-row font-bold;
ui-icon {
@apply flex items-center;
}
}
.loader {
width: 130px;
}
}
.top-line {
@apply mt-8;
}
@@ -96,7 +137,7 @@
.product-details {
@apply flex flex-col whitespace-nowrap overflow-ellipsis overflow-hidden;
width: 320px;
width: 390px;
.info-row {
@apply flex items-center whitespace-nowrap overflow-ellipsis overflow-hidden;
@@ -119,7 +160,7 @@
.delivery-row {
.order-type {
@apply w-px-100 font-bold;
@apply font-bold inline-flex flex-wrap;
}
.supplier {

View File

@@ -1,8 +1,8 @@
import { Component, ChangeDetectionStrategy, OnDestroy } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core';
import { DomainCheckoutService } from '@domain/checkout';
import { UiModalService } from '@ui/modal';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { PrintModalComponent, PrintModalData } from '@modal/printer';
import { debounceTime, first, map, shareReplay, switchMap } from 'rxjs/operators';
import { first, map, shareReplay, switchMap } from 'rxjs/operators';
import { CrmCustomerService } from '@domain/crm';
import { ActivatedRoute, Router } from '@angular/router';
import { DomainOmsService } from '@domain/oms';
@@ -11,7 +11,8 @@ import { DisplayOrderDTO, DisplayOrderItemDTO } from '@swagger/oms';
import { BreadcrumbService } from '@core/breadcrumb';
import { ApplicationService } from '@core/application';
import { DomainPrinterService } from '@domain/printer';
import { combineLatest, NEVER } from 'rxjs';
import { BehaviorSubject, combineLatest, NEVER, of, Subject } from 'rxjs';
import { DateAdapter } from '@ui/common';
@Component({
selector: 'page-checkout-summary',
@@ -20,7 +21,12 @@ import { combineLatest, NEVER } from 'rxjs';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutSummaryComponent implements OnDestroy {
private _onDestroy$ = new Subject();
processId = Date.now();
selectedDate = of(this.dateAdapter.today());
minDateDatepicker = this.dateAdapter.addCalendarDays(this.dateAdapter.today(), -1);
updatingPreferredPickUpDate$ = new BehaviorSubject<Record<string, string>>({});
displayOrders$ = combineLatest([this.domainCheckoutService.getOrders(), this._route.params]).pipe(
map(([orders, params]) => {
@@ -97,7 +103,7 @@ export class CheckoutSummaryComponent implements OnDestroy {
switchMap((o) =>
this.customerService
.getCustomers(o[0].buyerNumber, { take: 5 })
.pipe(map((customers) => customers.result[0].features?.find((f) => f.enabled && f.key === 'b2b') != null))
.pipe(map((customers) => customers.result[0].features?.find((f) => f.key === 'b2b') != null))
)
);
@@ -115,7 +121,8 @@ export class CheckoutSummaryComponent implements OnDestroy {
private uiModal: UiModalService,
private breadcrumb: BreadcrumbService,
public applicationService: ApplicationService,
private domainPrinterService: DomainPrinterService
private domainPrinterService: DomainPrinterService,
private dateAdapter: DateAdapter
) {
this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['checkout'])
@@ -144,6 +151,9 @@ export class CheckoutSummaryComponent implements OnDestroy {
if (!checkoutProcess) {
this.domainCheckoutService.removeAllOrders();
}
this._onDestroy$.next();
this._onDestroy$.complete();
}
openPrintModal(id: number) {
@@ -160,6 +170,54 @@ export class CheckoutSummaryComponent implements OnDestroy {
});
}
async updatePreferredPickUpDate(item: DisplayOrderItemDTO, date: Date) {
const data: Record<string, string> = {};
try {
const items = item ? [item] : await this.getAllOrderItems();
const subsetItems = items
.filter((item) => ['Rücklage', 'Abholung'].includes(item.features.orderType))
.flatMap((item) => item.subsetItems);
subsetItems.forEach((item) => (data[`${item.id}`] = date?.toISOString()));
try {
this.updatingPreferredPickUpDate$.next(data);
await this.omsService.setPreferredPickUpDate({ data }).toPromise();
} catch (error) {
this.uiModal.open({
content: UiErrorModalComponent,
title: 'Fehler beim setzen des Wunschdatums',
data: error,
});
} finally {
this.updatingPreferredPickUpDate$.next({});
}
items.forEach((item) => {
this.updateDisplayOrderItem({
...item,
subsetItems: subsetItems.map((subsetItem) => {
return {
...subsetItem,
preferredPickUpDate: date?.toISOString(),
};
}),
});
});
} catch (error) {
console.error(error);
}
}
async getAllOrderItems() {
const orders = await this.displayOrders$.pipe(first()).toPromise();
return orders.flatMap((order) => order.items);
}
async updateDisplayOrderItem(item: DisplayOrderItemDTO) {
this.domainCheckoutService.updateOrderItem(item);
}
async navigateToGoodsOut() {
let takeNowOrders = await this.takeNowOrders$.pipe(first()).toPromise();
if (takeNowOrders.length != 1) return;

View File

@@ -7,9 +7,22 @@ import { PageCheckoutPipeModule } from '../pipes/page-checkout-pipe.module';
import { ProductImageModule } from 'apps/cdn/product-image/src/public-api';
import { ModalPrinterModule } from '@modal/printer';
import { RouterModule } from '@angular/router';
import { UiCommonModule } from '@ui/common';
import { UiSpinnerModule } from '@ui/spinner';
import { UiDatepickerModule } from '@ui/datepicker';
@NgModule({
imports: [CommonModule, RouterModule, UiIconModule, PageCheckoutPipeModule, ProductImageModule, ModalPrinterModule],
imports: [
CommonModule,
RouterModule,
UiIconModule,
PageCheckoutPipeModule,
ProductImageModule,
ModalPrinterModule,
UiCommonModule,
UiSpinnerModule,
UiDatepickerModule,
],
exports: [CheckoutSummaryComponent],
declarations: [CheckoutSummaryComponent],
})

View File

@@ -1,7 +1,7 @@
<form *ngIf="control" [formGroup]="control">
<ui-form-control label="MwSt" variant="default">
<ui-form-control label="MwSt" variant="default" *ngIf="!hideVat">
<ui-select formControlName="vat">
<ui-select-option *ngFor="let vat of vats$ | async" [label]="vat.name + '%'" [value]="vat.vatType"></ui-select-option>
<ui-select-option *ngFor="let vat of vats$ | async" [label]="vat.name + '%'" [value]="vat.vatType"> </ui-select-option>
</ui-select>
</ui-form-control>

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DomainOmsService } from '@domain/oms';
import { VATDTO } from '@swagger/oms';
@@ -25,6 +25,9 @@ export class PurchasingOptionsModalPriceInputComponent implements OnInit {
private _subscriptions = new Subscription();
@Input()
hideVat = false;
constructor(private _omsService: DomainOmsService, private _fb: FormBuilder) {}
ngOnInit() {
@@ -35,7 +38,7 @@ export class PurchasingOptionsModalPriceInputComponent implements OnInit {
const fb = this._fb;
this.control = fb.group({
price: fb.control(undefined, [Validators.required, Validators.pattern(/^\d+([\,]\d{1,2})?$/), Validators.max(99999)]),
vat: fb.control(undefined, [Validators.required]),
vat: fb.control(0, [Validators.required]),
});
this._subscriptions.add(

View File

@@ -83,9 +83,11 @@
</div>
<div class="custom-price" *ngIf="showCustomPrice$ | async">
<page-purchasing-options-modal-price-input
[hideVat]="item.type === 66560"
(priceChanged)="changeCustomPrice($event)"
(vatChanged)="changeCustomVat($event)"
></page-purchasing-options-modal-price-input>
>
</page-purchasing-options-modal-price-input>
</div>
<hr />
<div class="summary-row" *ngIf="quantity$ | async; let quantity">
@@ -112,7 +114,7 @@
Weiter einkaufen
</button>
<button *ngIf="canUpgrade$ | async" class="cta-upgrade-customer" (click)="continue('add-customer-data')">
Kundendaten hinzufügen
Kundendaten erfassen
</button>
<button
*ngIf="showTakeAwayButton$ | async"

View File

@@ -10,6 +10,10 @@ import { PurchasingOptionsModalData } from './purchasing-options-modal.data';
import { PurchasingOptionsModalStore } from './purchasing-options-modal.store';
import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb';
import {
encodeFormData,
mapCustomerDtoToCustomerCreateFormData,
} from 'apps/page/customer/src/lib/create-customer/customer-create-form-data';
@Component({
selector: 'page-purchasing-options-modal',
@@ -47,14 +51,27 @@ export class PurchasingOptionsModalComponent {
readonly showCustomPrice$ = this.purchasingOptionsModalStore.selectAvailabilities.pipe(
withLatestFrom(this.option$),
map(([availabilities, option]) => !(availabilities[option]?.price?.value?.value || availabilities[option]?.retailPrice?.value?.value))
map(([availabilities, option]) => !availabilities[option]?.price?.value?.value)
);
readonly customPriceInvalid$ = combineLatest([
this.item$,
this.showCustomPrice$,
this.purchasingOptionsModalStore.selectCustomPrice,
this.purchasingOptionsModalStore.selectCustomVat,
]).pipe(map(([showCustomPrice, customPrice, customVat]) => showCustomPrice && (!customPrice || !customVat)));
]).pipe(
map(([item, showCustomPrice, customPrice, customVat]) => {
if (!showCustomPrice) {
return false;
}
if ((item.type as any) === 66560) {
return !customPrice;
}
return !customPrice || !customVat;
})
);
readonly showTakeAwayButton$ = combineLatest([
this.option$,
@@ -111,10 +128,7 @@ export class PurchasingOptionsModalComponent {
switchMap((processId) => this.checkoutService.getCustomerFeatures({ processId }))
);
readonly customerId$ = this.application.activatedProcessId$.pipe(
switchMap((processId) => this.checkoutService.getBuyer({ processId })),
map((buyer) => buyer.source)
);
readonly customer$ = this.application.activatedProcessId$.pipe(switchMap((processId) => this.checkoutService.getCustomer({ processId })));
price$ = combineLatest([
this.purchasingOptionsModalStore.selectAvailabilities,
@@ -126,7 +140,7 @@ export class PurchasingOptionsModalComponent {
if (availabilities[option]?.price?.value?.value) {
return availabilities[option]?.price?.value?.value;
}
return availabilities[option]?.retailPrice?.value?.value ?? customPrice;
return availabilities[option]?.price?.value?.value ?? customPrice;
} else {
const key = Object.keys(availabilities).find((key) => !!availabilities[key]?.price?.value?.value);
return availabilities[key]?.price?.value?.value ?? customPrice;
@@ -144,7 +158,7 @@ export class PurchasingOptionsModalComponent {
if (availabilities[option]?.price?.vat?.vatType) {
return availabilities[option]?.price?.vat?.vatType;
}
return availabilities[option]?.retailPrice?.vat?.vatType ?? customVat;
return availabilities[option]?.price?.vat?.vatType ?? customVat;
} else {
const key = Object.keys(availabilities).find((key) => !!availabilities[key]?.price?.vat?.vatType);
return availabilities[key]?.price?.vat?.vatType ?? customVat;
@@ -228,7 +242,7 @@ export class PurchasingOptionsModalComponent {
this.activeSpinner = navigate ? 'continue-shopping' : 'continue';
try {
const processId = await this.purchasingOptionsModalStore.selectProcessId.pipe(first()).toPromise();
const customer = await this.checkoutService.getBuyer({ processId }).pipe(first()).toPromise();
const buyer = await this.checkoutService.getBuyer({ processId }).pipe(first()).toPromise();
const item = await this.item$.pipe(first()).toPromise();
const quantity = await this.quantity$.pipe(first()).toPromise();
const availability = await this.availability$.pipe(first()).toPromise();
@@ -237,7 +251,8 @@ export class PurchasingOptionsModalComponent {
const shoppingCartItem = await this.purchasingOptionsModalStore.selectShoppingCartItem.pipe(first()).toPromise();
const canAdd = await this.canAdd$.pipe(first()).toPromise();
const customPrice = await this.purchasingOptionsModalStore.selectCustomPrice.pipe(first()).toPromise();
const customVat = await this.purchasingOptionsModalStore.selectCustomVat.pipe(first()).toPromise();
const customVat = (await this.purchasingOptionsModalStore.selectCustomVat.pipe(first()).toPromise()) ?? 0;
const customer = await this.checkoutService.getCustomer({ processId }).pipe(first()).toPromise();
if (canAdd || navigate === 'add-customer-data') {
const newItem: AddToShoppingCartDTO = {
@@ -248,6 +263,7 @@ export class PurchasingOptionsModalComponent {
...item.product,
},
promotion: { points: item.promoPoints },
itemType: item.type,
};
newItem.product.catalogProductNumber = String(item.id);
@@ -340,7 +356,7 @@ export class PurchasingOptionsModalComponent {
} else if (navigate === 'continue') {
// Set filter for navigation to customer search if customer is not set
let filter: { [key: string]: string };
if (!customer) {
if (!buyer) {
filter = await this.customerFeatures$
.pipe(
first(),
@@ -357,10 +373,15 @@ export class PurchasingOptionsModalComponent {
this.router.navigate(['/kunde', this.application.activatedProcessId, 'cart', 'review']);
}
} else if (navigate === 'add-customer-data') {
const upgradeCustomerId = await this.customerId$.pipe(first()).toPromise();
this.router.navigate(['/kunde', this.application.activatedProcessId, 'customer', 'create', 'webshop'], {
queryParams: { upgradeCustomerId },
});
if (customer?.attributes.some((attr) => attr.data.key === 'p4mUser')) {
this.router.navigate(['/kunde', this.application.activatedProcessId, 'customer', 'create', 'webshop-p4m'], {
queryParams: { formData: encodeFormData(mapCustomerDtoToCustomerCreateFormData(customer)) },
});
} else {
this.router.navigate(['/kunde', this.application.activatedProcessId, 'customer', 'create', 'webshop'], {
queryParams: { formData: encodeFormData(mapCustomerDtoToCustomerCreateFormData(customer)) },
});
}
}
} catch (error) {
console.log('PurchasingOptionsModalComponent.continue', error);

View File

@@ -376,11 +376,25 @@ export class PurchasingOptionsModalStore extends ComponentStore<PurchasingOption
break;
case 'pick-up':
if (!isNullOrUndefined(branch)) {
availability$ = this.availabilityService.getPickUpAvailability({
item: { itemId: item.id, ean: item.product.ean, price: item.catalogAvailability.price },
quantity,
branch,
});
availability$ = this.availabilityService
.getPickUpAvailability({
item: { itemId: item.id, ean: item.product.ean, price: item.catalogAvailability.price },
quantity,
branch,
})
.pipe(
map((av) => {
if (av[1].availableFor) {
if ((av[1].availableFor & 2) === 2) {
return av[0];
} else {
undefined;
}
} else {
return av[0];
}
})
);
}
break;
case 'delivery':

View File

@@ -0,0 +1,386 @@
import { HttpErrorResponse } from '@angular/common/http';
import { ChangeDetectorRef, Directive, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { AbstractControl, AsyncValidatorFn, FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { CrmCustomerService } from '@domain/crm';
import { AddressDTO, CustomerDTO, PayerDTO, ShippingAddressDTO } from '@swagger/crm';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { UiValidators } from '@ui/validators';
import { isNull } from 'lodash';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import {
first,
map,
distinctUntilChanged,
shareReplay,
delay,
mergeMap,
catchError,
tap,
bufferCount,
startWith,
takeUntil,
} from 'rxjs/operators';
import { AddressFormBlockComponent, DeviatingAddressFormBlockComponent, DeviatingAddressFormBlockData } from '../form-blocks';
import { FormBlock } from '../form-blocks/form-block';
import { AddressSelectionModalService } from '../modals/address-selection-modal/address-selection-modal.service';
import { CustomerCreateFormData, decodeFormData, encodeFormData } from './customer-create-form-data';
@Directive()
export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
protected onDestroy$ = new Subject<void>();
abstract validateAddress?: boolean;
abstract validateShippingAddress?: boolean;
private _formData = new BehaviorSubject<CustomerCreateFormData>({});
formData$ = this._formData.asObservable();
get formData() {
return this._formData.getValue();
}
form = new FormGroup({});
latestProcessId: number;
processId$: Observable<number>;
busy$ = new BehaviorSubject(false);
customerExists$ = new Subject<boolean>();
@ViewChild(DeviatingAddressFormBlockComponent)
deviatingDeliveryAddressFormBlock: DeviatingAddressFormBlockComponent;
@ViewChild(AddressFormBlockComponent)
addressFormBlock: AddressFormBlockComponent;
abstract customerType: string;
constructor(
protected activatedRoute: ActivatedRoute,
protected router: Router,
protected customerService: CrmCustomerService,
protected addressVlidationModal: AddressSelectionModalService,
protected modal: UiModalService,
protected breadcrumb: BreadcrumbService,
protected cdr: ChangeDetectorRef
) {
this._initProcessId$();
}
ngOnInit() {}
private _initProcessId$(): void {
this.processId$ = this.activatedRoute.parent.parent.data.pipe(
map((data) => +data.processId),
tap((processId) => (this.latestProcessId = processId)),
distinctUntilChanged(),
shareReplay(1)
);
this.processId$
.pipe(startWith(undefined), bufferCount(2, 1), takeUntil(this.onDestroy$), delay(100))
.subscribe(async ([previous, current]) => {
if (previous === undefined) {
await this._initFormData();
await this.updateBreadcrumb(current, this.formData);
} else if (previous !== current) {
await this.updateBreadcrumb(previous, this.formData);
await this._initFormData();
await this.updateBreadcrumb(current, this.formData);
}
});
}
ngOnDestroy(): void {
this.updateBreadcrumb(this.latestProcessId, this.formData);
this.onDestroy$.next();
this.onDestroy$.complete();
this.busy$.complete();
this._formData.complete();
}
private async _initFormData() {
const formData = await this.activatedRoute.queryParams
.pipe(
map((params) => params['formData']),
first()
)
.toPromise();
if (formData) {
const parsedFormData = decodeFormData(formData);
this._formData.next(parsedFormData);
}
}
async updateBreadcrumb(processId: number, formData: CustomerCreateFormData) {
await this.cleanupBreadcrumb(processId);
await this.addOrUpdateBreadcrumb(processId, formData);
}
async cleanupBreadcrumb(processId: number) {
const crumbs = await this.breadcrumb.getBreadcrumbsByKeyAndTags$(processId, ['customer', 'main']).pipe(first()).toPromise();
for (const crumb of crumbs) {
await this.breadcrumb.removeBreadcrumbsAfter(crumb.id);
}
}
async addOrUpdateBreadcrumb(processId: number, formData: CustomerCreateFormData) {
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: processId,
name: 'Kundendaten erfassen',
path: this.getPath(processId),
params: this.getQueryParams(formData),
tags: ['customer', 'create'],
section: 'customer',
});
}
getPath(processId: number) {
return ['/kunde', processId, 'customer', 'create', this.customerType].join('/');
}
getQueryParams(formData: CustomerCreateFormData): Record<string, string> {
const param = formData ? encodeFormData(formData) : undefined;
return { ...this.activatedRoute.snapshot.queryParams, formData: param };
}
async customerTypeChanged(customerType: string) {
const processId = await this.processId$.pipe(first()).toPromise();
this.router.navigate(['/kunde', processId, 'customer', 'create', customerType], {
queryParams: this.getQueryParams(this.formData),
});
}
addFormBlock(key: keyof CustomerCreateFormData, block: FormBlock<any, AbstractControl>) {
this.form.addControl(key, block.control);
this.cdr.markForCheck();
}
patchFormData(key: keyof CustomerCreateFormData, value: any) {
this._formData.next({ ...this.formData, [key]: value });
this.cdr.markForCheck();
}
emailExistsValidator: AsyncValidatorFn = (control) => {
return of(control.value).pipe(
tap((_) => this.customerExists$.next(false)),
delay(500),
mergeMap((value) => {
return this.customerService.emailExists(value).pipe(
map((response) => {
if (response?.result) {
return { exists: response?.message };
}
return null;
}),
catchError((error) => {
if (error instanceof HttpErrorResponse) {
if (error?.error?.invalidProperties?.email) {
return of({ invalid: error.error.invalidProperties.email });
} else {
return of({ invalid: 'E-Mail ist ungültig' });
}
}
})
);
}),
tap((error) => {
if (error) {
this.customerExists$.next(true);
}
control.markAsTouched();
this.cdr.markForCheck();
})
);
};
async navigateToCustomerDetails(customer: CustomerDTO) {
const processId = await this.processId$.pipe(first()).toPromise();
return this.router.navigate(['/kunde', processId, 'customer', customer.id]);
}
async validateAddressData(address: AddressDTO): Promise<AddressDTO> {
const addressValidationResult = await this.addressVlidationModal.validateAddress(address);
if (addressValidationResult !== undefined && addressValidationResult !== 'continue') {
address = addressValidationResult;
}
return address;
}
async getCustomerFromFormData(): Promise<CustomerDTO> {
const data: CustomerCreateFormData = this.form.value;
const customer: CustomerDTO = {
communicationDetails: {},
attributes: [],
features: [],
};
if (data.name) {
customer.gender = data.name.gender;
customer.title = data.name.title;
customer.firstName = data.name.firstName;
customer.lastName = data.name.lastName;
}
if (data.organisation) {
customer.organisation = data.organisation;
}
if (data.email) {
customer.communicationDetails.email = data.email;
}
if (data.phoneNumbers) {
customer.communicationDetails.mobile = data.phoneNumbers.mobile;
customer.communicationDetails.phone = data.phoneNumbers.phone;
}
if (data.address) {
customer.address = data.address;
if (this.validateAddress) {
try {
const address = await this.validateAddressData(customer.address);
this.addressFormBlock.data = address;
customer.address = address;
} catch (error) {
this.form.enable();
setTimeout(() => {
this.addressFormBlock.setAddressValidationError(error.error.invalidProperties);
}, 10);
return;
}
}
}
if (data.birthDate && isNull(UiValidators.date(new FormControl(data.birthDate)))) {
customer.dateOfBirth = data.birthDate;
}
if (data.billingAddress?.deviatingAddress) {
const billingAddress = this.mapToBillingAddress(data.billingAddress);
if (this.validateShippingAddress) {
try {
billingAddress.address = await this.validateAddressData(billingAddress.address);
} catch (error) {
this.form.enable();
setTimeout(() => {
this.addressFormBlock.setAddressValidationError(error.error.invalidProperties);
}, 10);
return;
}
}
customer.payers = [
{
payer: { data: billingAddress },
isDefault: new Date().toISOString(),
},
];
}
if (data.deviatingDeliveryAddress?.deviatingAddress) {
const shippingAddress = this.mapToShippingAddress(data.deviatingDeliveryAddress);
if (this.validateShippingAddress) {
try {
shippingAddress.address = await this.validateAddressData(shippingAddress.address);
} catch (error) {
this.form.enable();
setTimeout(() => {
this.addressFormBlock.setAddressValidationError(error.error.invalidProperties);
}, 10);
return;
}
}
customer.shippingAddresses = [
{
data: { ...shippingAddress, isDefault: new Date().toISOString() },
},
];
}
return customer;
}
mapToShippingAddress({ name, address, email, organisation, phoneNumbers }: DeviatingAddressFormBlockData): ShippingAddressDTO {
return {
gender: name?.gender,
title: name?.title,
firstName: name?.firstName,
lastName: name?.lastName,
address,
communicationDetails: {
email: email ? email : null,
mobile: phoneNumbers?.mobile ? phoneNumbers.mobile : null,
phone: phoneNumbers?.phone ? phoneNumbers.phone : null,
},
organisation,
isDefault: new Date().toJSON(),
};
}
mapToBillingAddress({ name, address, email, organisation, phoneNumbers }: DeviatingAddressFormBlockData): PayerDTO {
return {
gender: name?.gender,
title: name?.title,
firstName: name?.firstName,
lastName: name?.lastName,
address,
communicationDetails: {
email: email ? email : null,
mobile: phoneNumbers?.mobile ? phoneNumbers.mobile : null,
phone: phoneNumbers?.phone ? phoneNumbers.phone : null,
},
organisation,
};
}
async save() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
try {
this.busy$.next(true);
this.form.disable();
const customer: CustomerDTO = await this.getCustomerFromFormData();
if (!customer) {
this.form.enable();
return;
}
const response = await this.saveCustomer(customer);
if (!!response) {
this.navigateToCustomerDetails(response);
}
} catch (error) {
this.form.enable();
this.modal.open({
content: UiErrorModalComponent,
data: error,
});
} finally {
this.busy$.next(false);
}
}
abstract saveCustomer(customer: CustomerDTO): Promise<CustomerDTO>;
}

View File

@@ -0,0 +1,91 @@
<form *ngIf="formData$ | async; let data" (keydown.enter)="$event.preventDefault()">
<h1 class="title">Kundendaten erfassen</h1>
<p class="description">
Um Ihnen den ausgewählten Service <br />
zu ermöglichen, legen wir Ihnen <br />
gerne ein Kundenkonto an. <br />
</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="false"
customerType="b2b"
(valueChanges)="customerTypeChanged($event)"
>
</app-customer-type-selector>
<app-organisation-form-block
#orga
[tabIndexStart]="1"
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
(onInit)="addFormBlock('organisation', $event)"
[requiredMarks]="organisationFormBlockRequiredMarks"
[validatorFns]="organisationFormBlockValidators"
>
</app-organisation-form-block>
<app-name-form-block
#name
[tabIndexStart]="orga.tabIndexEnd + 1"
[data]="data.name"
(dataChanges)="patchFormData('name', $event)"
(onInit)="addFormBlock('name', $event)"
>
</app-name-form-block>
<app-address-form-block
#address
[tabIndexStart]="name.tabIndexEnd + 1"
[data]="data.address"
(dataChanges)="patchFormData('address', $event)"
(onInit)="addFormBlock('address', $event)"
[requiredMarks]="addressRequiredMarks"
[validatorFns]="addressValidators"
[defaults]="{ country: 'DEU' }"
>
</app-address-form-block>
<app-email-form-block
#email
[tabIndexStart]="address.tabIndexEnd + 1"
[data]="data.email"
(dataChanges)="patchFormData('email', $event)"
[validatorFns]="emailFormBlockValidators"
(onInit)="addFormBlock('email', $event)"
>
</app-email-form-block>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="email.tabIndexEnd + 1"
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
(onInit)="addFormBlock('phoneNumbers', $event)"
>
</app-phone-numbers-form-block>
<app-deviating-address-form-block
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[nameRequiredMarks]="deviatingNameRequiredMarks"
[nameValidatorFns]="deviatingNameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidators"
[organisationRequiredMarks]="organisationFormBlockRequiredMarks"
[organisationValidatorFns]="organisationFormBlockValidators"
[defaults]="{ address: { country: 'DEU' } }"
[organisation]="true"
[email]="true"
[phoneNumbers]="true"
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<div class="spacer"></div>
<button class="cta-submit" type="button" (click)="save()" [disabled]="form.invalid || form.pending">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</form>

View File

@@ -0,0 +1,52 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { ValidatorFn, Validators } from '@angular/forms';
import { CustomerDTO } from '@swagger/crm';
import { map } from 'rxjs/operators';
import { validateEmail } from '../../validators/email-validator';
import { AbstractCreateCustomer } from '../abstract-create-customer';
@Component({
selector: 'app-create-b2b-customer',
templateUrl: 'create-b2b-customer.component.html',
styleUrls: ['../create-customer.scss', 'create-b2b-customer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateB2BCustomerComponent extends AbstractCreateCustomer {
customerType = 'b2b';
validateAddress = true;
validateShippingAddress = true;
organisationFormBlockRequiredMarks = ['name'];
organisationFormBlockValidators: Record<string, ValidatorFn[]> = {
name: [Validators.required],
};
addressRequiredMarks = ['street', 'streetNumber', 'zipCode', 'city', 'country'];
addressValidators: Record<string, ValidatorFn[]> = {
street: [Validators.required],
streetNumber: [Validators.required],
zipCode: [Validators.required],
city: [Validators.required],
country: [Validators.required],
};
emailFormBlockValidators = [Validators.email, validateEmail];
deviatingNameRequiredMarks = ['gender', 'firstName', 'lastName'];
deviatingNameValidationFns: Record<string, ValidatorFn[]> = {
gender: [Validators.required],
firstName: [Validators.required],
lastName: [Validators.required],
};
async saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
const res = await this.customerService.createB2BCustomer(customer);
return res.result;
}
}

View File

@@ -0,0 +1,28 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CreateB2BCustomerComponent } from './create-b2b-customer.component';
import { OrganisationFormBlockModule } from '../../form-blocks/organisation';
import { NameFormBlockModule } from '../../form-blocks/name';
import { AddressFormBlockModule } from '../../form-blocks/address';
import { CustomerTypeSelectorModule } from '../../shared/customer-type-selector';
import { DeviatingAddressFormBlockComponentModule, EmailFormBlockModule } from '../../form-blocks';
import { PhoneNumbersFormBlockModule } from '../../form-blocks/phone-numbers/phone-numbers-form-block.module';
import { UiSpinnerModule } from '@ui/spinner';
@NgModule({
imports: [
CommonModule,
OrganisationFormBlockModule,
NameFormBlockModule,
AddressFormBlockModule,
DeviatingAddressFormBlockComponentModule,
CustomerTypeSelectorModule,
EmailFormBlockModule,
PhoneNumbersFormBlockModule,
UiSpinnerModule,
],
exports: [CreateB2BCustomerComponent],
declarations: [CreateB2BCustomerComponent],
})
export class CreateB2BCustomerModule {}

View File

@@ -0,0 +1,2 @@
export * from './create-b2b-customer.component';
export * from './create-b2b-customer.module';

View File

@@ -0,0 +1,27 @@
import { NgModule } from '@angular/core';
import { CreateB2BCustomerModule } from './create-b2b-customer/create-b2b-customer.module';
import { CreateGuestCustomerModule } from './create-guest-customer';
import { CreateP4MCustomerModule } from './create-p4m-customer';
import { CreateStoreCustomerModule } from './create-store-customer/create-store-customer.module';
import { CreateWebshopCustomerModule } from './create-webshop-customer/create-webshop-customer.module';
import { UpdateP4MWebshopCustomerModule } from './update-p4m-webshop-customer';
@NgModule({
imports: [
CreateB2BCustomerModule,
CreateGuestCustomerModule,
CreateStoreCustomerModule,
CreateWebshopCustomerModule,
CreateP4MCustomerModule,
UpdateP4MWebshopCustomerModule,
],
exports: [
CreateB2BCustomerModule,
CreateGuestCustomerModule,
CreateStoreCustomerModule,
CreateWebshopCustomerModule,
CreateP4MCustomerModule,
UpdateP4MWebshopCustomerModule,
],
})
export class CreateCustomerModule {}

View File

@@ -0,0 +1,42 @@
:host {
@apply block bg-white rounded-card px-20 py-10;
}
h1.title {
@apply text-2xl font-bold text-center mb-6;
}
p.description {
@apply text-xl text-center;
}
p.info {
@apply mt-8 font-semibold;
}
form {
@apply relative pb-4;
}
button.cta-submit {
@apply sticky left-1/2 bottom-8 text-center bg-brand text-cta-l text-white font-bold px-7 py-3 rounded-full transform -translate-x-1/2 transition-all duration-200 ease-in-out;
&:disabled {
@apply bg-active-branch cursor-not-allowed;
}
}
app-newsletter-form-block,
app-accept-agb-form-block,
app-interests-form-block,
app-deviating-address-form-block {
@apply mt-8;
}
.spacer {
@apply w-full h-8;
}
app-customer-type-selector {
@apply mt-8;
}

View File

@@ -0,0 +1,98 @@
<form *ngIf="formData$ | async; let data" (keydown.enter)="$event.preventDefault()">
<h1 class="title">Kundendaten erfassen</h1>
<p class="description">
Um Ihnen den ausgewählten Service <br />
zu ermöglichen, legen wir Ihnen <br />
gerne ein Kundenkonto an. <br />
</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="false"
customerType="guest"
(valueChanges)="customerTypeChanged($event)"
>
</app-customer-type-selector>
<app-name-form-block
#name
[tabIndexStart]="1"
[data]="data.name"
(dataChanges)="patchFormData('name', $event)"
(onInit)="addFormBlock('name', $event)"
[requiredMarks]="nameRequiredMarks"
[validatorFns]="nameValidationFns"
>
</app-name-form-block>
<app-email-form-block
#email
[tabIndexStart]="name.tabIndexEnd + 1"
[data]="data.email"
[requiredMark]="true"
(dataChanges)="patchFormData('email', $event)"
[validatorFns]="emailFormBlockValidators"
(onInit)="addFormBlock('email', $event)"
>
</app-email-form-block>
<app-organisation-form-block
#orga
[tabIndexStart]="email.tabIndexStart + 1"
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
(onInit)="addFormBlock('organisation', $event)"
appearence="compact"
>
</app-organisation-form-block>
<app-address-form-block
#address
[tabIndexStart]="orga.tabIndexEnd + 1"
[data]="data.address"
(dataChanges)="patchFormData('address', $event)"
(onInit)="addFormBlock('address', $event)"
[requiredMarks]="addressRequiredMarks"
[validatorFns]="addressValidators"
[defaults]="{ country: 'DEU' }"
>
</app-address-form-block>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="address.tabIndexEnd + 1"
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
(onInit)="addFormBlock('phoneNumbers', $event)"
>
</app-phone-numbers-form-block>
<app-birth-date-form-block
#birthDate
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
(onInit)="addFormBlock('birthDate', $event)"
[data]="data.birthDate"
(dataChanges)="patchFormData('birthDate', $event)"
>
</app-birth-date-form-block>
<app-deviating-address-form-block
[tabIndexStart]="birthDate.tabIndexEnd + 1"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[nameRequiredMarks]="deviatingNameRequiredMarks"
[nameValidatorFns]="deviatingNameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidators"
[defaults]="{ address: { country: 'DEU' } }"
[organisation]="true"
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<div class="spacer"></div>
<button class="cta-submit" type="button" (click)="save()" [disabled]="form.invalid || form.pending">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</form>

View File

@@ -0,0 +1,64 @@
import { Component, ChangeDetectionStrategy, ViewChild } from '@angular/core';
import { ValidatorFn, Validators } from '@angular/forms';
import { CustomerDTO } from '@swagger/crm';
import { map } from 'rxjs/operators';
import { DeviatingAddressFormBlockComponent } from '../../form-blocks';
import { AddressFormBlockComponent } from '../../form-blocks/address';
import { NameFormBlockData } from '../../form-blocks/name/name-form-block-data';
import { validateEmail } from '../../validators/email-validator';
import { AbstractCreateCustomer } from '../abstract-create-customer';
@Component({
selector: 'app-create-guest-customer',
templateUrl: 'create-guest-customer.component.html',
styleUrls: ['../create-customer.scss', 'create-guest-customer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateGuestCustomerComponent extends AbstractCreateCustomer {
customerType = 'guest';
validateAddress = true;
validateShippingAddress = true;
nameRequiredMarks = ['gender', 'firstName', 'lastName'];
nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
firstName: [Validators.required],
lastName: [Validators.required],
gender: [Validators.required],
title: [],
};
addressRequiredMarks = ['street', 'streetNumber', 'zipCode', 'city', 'country'];
addressValidators: Record<string, ValidatorFn[]> = {
street: [Validators.required],
streetNumber: [Validators.required],
zipCode: [Validators.required],
city: [Validators.required],
country: [Validators.required],
};
emailFormBlockValidators = [Validators.email, validateEmail, Validators.required];
deviatingNameRequiredMarks = ['gender', 'firstName', 'lastName'];
deviatingNameValidationFns: Record<string, ValidatorFn[]> = {
gender: [Validators.required],
firstName: [Validators.required],
lastName: [Validators.required],
};
@ViewChild(AddressFormBlockComponent, { static: false })
addressFormBlock: AddressFormBlockComponent;
@ViewChild(DeviatingAddressFormBlockComponent, { static: false })
deviatingDeliveryAddressFormBlock: DeviatingAddressFormBlockComponent;
async saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
const res = await this.customerService.createGuestCustomer(customer);
return res.result;
}
}

View File

@@ -0,0 +1,29 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CreateGuestCustomerComponent } from './create-guest-customer.component';
import { OrganisationFormBlockModule } from '../../form-blocks/organisation';
import { NameFormBlockModule } from '../../form-blocks/name';
import { AddressFormBlockModule } from '../../form-blocks/address';
import { CustomerTypeSelectorModule } from '../../shared/customer-type-selector';
import { BirthDateFormBlockModule, DeviatingAddressFormBlockComponentModule, EmailFormBlockModule } from '../../form-blocks';
import { PhoneNumbersFormBlockModule } from '../../form-blocks/phone-numbers/phone-numbers-form-block.module';
import { UiSpinnerModule } from '@ui/spinner';
@NgModule({
imports: [
CommonModule,
OrganisationFormBlockModule,
NameFormBlockModule,
AddressFormBlockModule,
DeviatingAddressFormBlockComponentModule,
CustomerTypeSelectorModule,
EmailFormBlockModule,
PhoneNumbersFormBlockModule,
BirthDateFormBlockModule,
UiSpinnerModule,
],
exports: [CreateGuestCustomerComponent],
declarations: [CreateGuestCustomerComponent],
})
export class CreateGuestCustomerModule {}

View File

@@ -0,0 +1,2 @@
export * from './create-guest-customer.component';
export * from './create-guest-customer.module';

View File

@@ -0,0 +1,142 @@
<form *ngIf="formData$ | async; let data" (keydown.enter)="$event.preventDefault()">
<h1 class="title flex flex-row items-center justify-center">
Kundendaten erfassen
<!-- <span
class="rounded-full ml-4 h-8 w-8 text-xl text-center border-2 border-solid border-brand text-brand">i</span> -->
</h1>
<p class="description">Haben Sie eine Kundenkarte?</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="true"
[customerType]="_customerType"
(valueChanges)="customerTypeChanged($event)"
[p4mReadonly]="data?._meta?.p4mRequired"
>
</app-customer-type-selector>
<app-p4m-number-form-block
#p4mBlock
[tabIndexStart]="1"
(onInit)="addFormBlock('p4m', $event)"
[data]="data.p4m"
(dataChanges)="patchFormData('p4m', $event)"
[readonly]="data?._meta?.p4mRequired"
[focusAfterInit]="!data?._meta?.p4mRequired"
>
</app-p4m-number-form-block>
<app-newsletter-form-block
#newsletterBlock
[tabIndexStart]="p4mBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('newsletter', $event)"
[data]="data.newsletter"
(dataChanges)="patchFormData('newsletter', $event)"
[focusAfterInit]="data?._meta?.p4mRequired"
>
</app-newsletter-form-block>
<app-name-form-block
#nameBlock
[tabIndexStart]="newsletterBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('name', $event)"
[data]="data.name"
[requiredMarks]="nameRequiredMarks"
[validatorFns]="nameValidationFns"
(dataChanges)="patchFormData('name', $event)"
>
</app-name-form-block>
<app-email-form-block
class="flex-grow"
#email
[tabIndexStart]="nameBlock.tabIndexEnd + 1"
[requiredMark]="emailRequiredMark"
(onInit)="addFormBlock('email', $event)"
[data]="data.email"
(dataChanges)="patchFormData('email', $event)"
[validatorFns]="emailValidatorFn"
[asyncValidatorFns]="asyncEmailVlaidtorFn"
>
</app-email-form-block>
<app-organisation-form-block
#orgBlock
[tabIndexStart]="email.tabIndexEnd + 1"
appearence="compact"
(onInit)="addFormBlock('organisation', $event)"
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
>
</app-organisation-form-block>
<app-address-form-block
[defaults]="{ country: 'DEU' }"
#addressBlock
[tabIndexStart]="orgBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('address', $event)"
[data]="data.address"
(dataChanges)="patchFormData('address', $event)"
[requiredMarks]="addressRequiredMarks"
[validatorFns]="addressValidatorFns"
>
</app-address-form-block>
<app-deviating-address-form-block
#ddaBlock
[defaults]="{ address: { country: 'DEU' } }"
[tabIndexStart]="addressBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="shippingAddressRequiredMarks"
[addressValidatorFns]="shippingAddressValidators"
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="ddaBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('phoneNumbers', $event)"
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
></app-phone-numbers-form-block>
<app-birth-date-form-block
#bdBlock
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
(onInit)="addFormBlock('birthDate', $event)"
[data]="data.birthDate"
(dataChanges)="patchFormData('birthDate', $event)"
[requiredMark]="true"
[validatorFns]="birthDateValidatorFns"
>
</app-birth-date-form-block>
<app-interests-form-block
#inBlock
[tabIndexStart]="bdBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('interests', $event)"
[data]="data.interests"
(dataChanges)="patchFormData('interests', $event)"
></app-interests-form-block>
<app-accept-agb-form-block
[tabIndexStart]="inBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('agb', $event)"
[data]="data.agb"
(dataChanges)="patchFormData('agb', $event)"
[requiredMark]="true"
[validatorFns]="agbValidatorFns"
>
</app-accept-agb-form-block>
<div class="spacer"></div>
<button class="cta-submit" type="button" [disabled]="form.invalid || form.pending" (click)="save()">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</form>

View File

@@ -0,0 +1,267 @@
import { Component, ChangeDetectionStrategy, ViewChild, OnInit } from '@angular/core';
import { AsyncValidatorFn, ValidatorFn, Validators } from '@angular/forms';
import { Result } from '@domain/defs';
import { CustomerDTO, CustomerInfoDTO, KeyValueDTOOfStringAndString } from '@swagger/crm';
import { UiErrorModalComponent, UiModalResult } from '@ui/modal';
import { NEVER, Observable, of } from 'rxjs';
import { catchError, distinctUntilChanged, first, map, switchMap, takeUntil } from 'rxjs/operators';
import { AddressFormBlockComponent, DeviatingAddressFormBlockComponent } from '../../form-blocks';
import { NameFormBlockData } from '../../form-blocks/name/name-form-block-data';
import { WebshopCustomnerAlreadyExistsModalComponent } from '../../modals';
import { validateEmail } from '../../validators/email-validator';
import { AbstractCreateCustomer } from '../abstract-create-customer';
import { encodeFormData, mapCustomerDtoToCustomerCreateFormData } from '../customer-create-form-data';
@Component({
selector: 'app-create-p4m-customer',
templateUrl: 'create-p4m-customer.component.html',
styleUrls: ['../create-customer.scss', 'create-p4m-customer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateP4MCustomerComponent extends AbstractCreateCustomer implements OnInit {
validateAddress = true;
validateShippingAddress = true;
get _customerType() {
return this.activatedRoute.snapshot.data.customerType;
}
get customerType() {
return `${this._customerType}-p4m`;
}
nameRequiredMarks = ['gender', 'firstName', 'lastName'];
nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
firstName: [Validators.required],
lastName: [Validators.required],
gender: [Validators.required],
title: [],
};
emailRequiredMark: boolean;
emailValidatorFn: ValidatorFn[];
asyncEmailVlaidtorFn: AsyncValidatorFn[];
shippingAddressRequiredMarks = ['street', 'streetNumber', 'zipCode', 'city', 'country'];
shippingAddressValidators: Record<string, ValidatorFn[]> = {
street: [Validators.required],
streetNumber: [Validators.required],
zipCode: [Validators.required],
city: [Validators.required],
country: [Validators.required],
};
addressRequiredMarks: string[];
addressValidatorFns: Record<string, ValidatorFn[]>;
@ViewChild(AddressFormBlockComponent, { static: false })
addressFormBlock: AddressFormBlockComponent;
@ViewChild(DeviatingAddressFormBlockComponent, { static: false })
deviatingDeliveryAddressFormBlock: DeviatingAddressFormBlockComponent;
agbValidatorFns = [Validators.requiredTrue];
birthDateValidatorFns = [Validators.required];
existingCustomer$: Observable<CustomerInfoDTO | null>;
ngOnInit(): void {
super.ngOnInit();
this.initMarksAndValidators();
this.existingCustomer$ = this.customerExists$.pipe(
distinctUntilChanged(),
switchMap((exists) => {
if (exists) {
return this.fetchCustomerInfo();
}
return of(null);
})
);
this.existingCustomer$
.pipe(
takeUntil(this.onDestroy$),
switchMap((info) => {
if (info) {
return this.customerService.getCustomer(info.id, 2).pipe(
map((res) => res.result),
catchError((err) => NEVER)
);
}
return NEVER;
})
)
.subscribe((customer) => {
if (customer) {
this.modal
.open({
content: WebshopCustomnerAlreadyExistsModalComponent,
data: customer,
title: 'Es existiert bereits ein Onlinekonto mit dieser E-Mail-Adresse',
})
.afterClosed$.subscribe(async (result: UiModalResult<boolean>) => {
if (result.data) {
this.navigateToUpdatePage(customer);
}
});
}
});
}
async navigateToUpdatePage(customer: CustomerDTO) {
const processId = await this.processId$.pipe(first()).toPromise();
this.router.navigate(['/kunde', processId, 'customer', 'create', 'webshop-p4m', 'update'], {
queryParams: {
formData: encodeFormData({
...mapCustomerDtoToCustomerCreateFormData(customer),
p4m: this.formData.p4m,
}),
},
});
}
initMarksAndValidators() {
if (this._customerType === 'webshop') {
this.emailRequiredMark = true;
this.emailValidatorFn = [Validators.required, Validators.email, validateEmail];
this.asyncEmailVlaidtorFn = [this.emailExistsValidator];
this.addressRequiredMarks = this.shippingAddressRequiredMarks;
this.addressValidatorFns = this.shippingAddressValidators;
} else {
this.emailRequiredMark = false;
this.emailValidatorFn = [Validators.email, validateEmail];
}
}
fetchCustomerInfo(): Observable<CustomerDTO | null> {
const email = this.formData.email;
return this.customerService.getOnlineCustomerByEmail(email).pipe(
map((result) => {
if (result) {
return result;
}
return null;
}),
catchError((err) => {
this.modal.open({
content: UiErrorModalComponent,
data: err,
});
return [null];
})
);
}
getInterests(): KeyValueDTOOfStringAndString[] {
const interests: KeyValueDTOOfStringAndString[] = [];
for (const key in this.formData.interests) {
if (this.formData.interests[key]) {
interests.push({ key, group: 'KUBI_INTERESSEN' });
}
}
return interests;
}
getNewsletter(): KeyValueDTOOfStringAndString | undefined {
if (this.formData.newsletter) {
return { key: 'kubi_newsletter', group: 'KUBI_NEWSLETTER' };
}
}
static MapCustomerInfoDtoToCustomerDto(customerInfoDto: CustomerInfoDTO): CustomerDTO {
return {
address: customerInfoDto.address,
agentComment: customerInfoDto.agentComment,
bonusCard: customerInfoDto.bonusCard,
campaignCode: customerInfoDto.campaignCode,
communicationDetails: customerInfoDto.communicationDetails,
createdInBranch: customerInfoDto.createdInBranch,
customerGroup: customerInfoDto.customerGroup,
customerNumber: customerInfoDto.customerNumber,
customerStatus: customerInfoDto.customerStatus,
customerType: customerInfoDto.customerType,
dateOfBirth: customerInfoDto.dateOfBirth,
features: customerInfoDto.features,
firstName: customerInfoDto.firstName,
lastName: customerInfoDto.lastName,
gender: customerInfoDto.gender,
hasOnlineAccount: customerInfoDto.hasOnlineAccount,
isGuestAccount: customerInfoDto.isGuestAccount,
label: customerInfoDto.label,
notificationChannels: customerInfoDto.notificationChannels,
organisation: customerInfoDto.organisation,
title: customerInfoDto.title,
id: customerInfoDto.id,
pId: customerInfoDto.pId,
};
}
async saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
const isWebshop = this._customerType === 'webshop';
let res: Result<CustomerDTO>;
const { customerDto, customerInfoDto } = this.formData?._meta ?? {};
if (customerDto) {
customer = { ...customerDto, ...customer };
} else if (customerInfoDto) {
customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
}
const p4mFeature = customer.features?.find((attr) => attr.key === 'p4mUser');
if (p4mFeature) {
p4mFeature.value = this.formData.p4m;
} else {
customer.features.push({
key: 'p4mUser',
value: this.formData.p4m,
});
}
const interests = this.getInterests();
if (interests.length > 0) {
customer.features?.push(...interests);
// TODO: Klärung wie Interessen zukünftig gespeichert werden
// await this._loyaltyCardService
// .LoyaltyCardSaveInteressen({
// customerId: res.result.id,
// interessen: this.getInterests(),
// })
// .toPromise();
}
const newsletter = this.getNewsletter();
if (newsletter) {
customer.features.push(newsletter);
} else {
customer.features = customer.features.filter((feature) => feature.key !== 'kubi_newsletter' && feature.group !== 'KUBI_NEWSLETTER');
}
if (isWebshop) {
if (customer.id > 0) {
if (this.formData?._meta?.hasLocalityCard) {
res = await this.customerService.updateStoreP4MToWebshopP4M(customer);
} else {
res = await this.customerService.updateToP4MOnlineCustomer(customer);
}
} else {
res = await this.customerService.createOnlineCustomer(customer).toPromise();
}
} else {
res = await this.customerService.createStoreCustomer(customer).toPromise();
}
return res.result;
}
}

View File

@@ -0,0 +1,45 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CreateP4MCustomerComponent } from './create-p4m-customer.component';
import {
AddressFormBlockModule,
BirthDateFormBlockModule,
InterestsFormBlockModule,
NameFormBlockModule,
OrganisationFormBlockModule,
P4mNumberFormBlockModule,
NewsletterFormBlockModule,
DeviatingAddressFormBlockComponentModule,
AcceptAGBFormBlockModule,
EmailFormBlockModule,
PhoneNumbersFormBlockModule,
} from '../../form-blocks';
import { CustomerTypeSelectorModule } from '../../shared/customer-type-selector';
import { UiSpinnerModule } from '@ui/spinner';
import { UiIconModule } from '@ui/icon';
import { RouterModule } from '@angular/router';
@NgModule({
imports: [
CommonModule,
CustomerTypeSelectorModule,
AddressFormBlockModule,
BirthDateFormBlockModule,
InterestsFormBlockModule,
NameFormBlockModule,
OrganisationFormBlockModule,
P4mNumberFormBlockModule,
NewsletterFormBlockModule,
DeviatingAddressFormBlockComponentModule,
AcceptAGBFormBlockModule,
EmailFormBlockModule,
PhoneNumbersFormBlockModule,
UiSpinnerModule,
UiIconModule,
RouterModule,
],
exports: [CreateP4MCustomerComponent],
declarations: [CreateP4MCustomerComponent],
})
export class CreateP4MCustomerModule {}

View File

@@ -0,0 +1,2 @@
export * from './create-p4m-customer.component';
export * from './create-p4m-customer.module';

View File

@@ -0,0 +1,94 @@
<form *ngIf="formData$ | async; let data" (keydown.enter)="$event.preventDefault()">
<h1 class="title">Kundendaten erfassen</h1>
<p class="description">
Um Ihnen den ausgewählten Service <br />
zu ermöglichen, legen wir Ihnen <br />
gerne ein Kundenkonto an.
</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="false"
customerType="store"
(valueChanges)="customerTypeChanged($event)"
>
</app-customer-type-selector>
<app-name-form-block
#name
[tabIndexStart]="1"
(onInit)="addFormBlock('name', $event)"
[requiredMarks]="nameRequiredMarks"
[validatorFns]="nameValidationFns"
[data]="data.name"
(dataChanges)="patchFormData('name', $event)"
>
</app-name-form-block>
<app-email-form-block
#email
[tabIndexStart]="name.tabIndexEnd + 1"
[validatorFns]="emailFormBlockValidators"
(onInit)="addFormBlock('email', $event)"
[data]="data.email"
(dataChanges)="patchFormData('email', $event)"
>
</app-email-form-block>
<app-organisation-form-block
#orga
[tabIndexStart]="email.tabIndexEnd + 1"
appearence="name"
(onInit)="addFormBlock('organisation', $event)"
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
>
</app-organisation-form-block>
<app-address-form-block
#address
[tabIndexStart]="orga.tabIndexEnd + 1"
(onInit)="addFormBlock('address', $event)"
[data]="data.address"
(dataChanges)="patchFormData('address', $event)"
>
</app-address-form-block>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="address.tabIndexEnd + 1"
(onInit)="addFormBlock('phoneNumbers', $event)"
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
></app-phone-numbers-form-block>
<app-birth-date-form-block
#birthDate
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
(onInit)="addFormBlock('birthDate', $event)"
[data]="data.birthDate"
(dataChanges)="patchFormData('birthDate', $event)"
></app-birth-date-form-block>
<app-deviating-address-form-block
[tabIndexStart]="birthDate.tabIndexEnd + 1"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[organisation]="true"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidators"
[defaults]="{ address: { country: 'DEU' } }"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<div class="spacer"></div>
<button class="cta-submit" type="button" [disabled]="form.invalid || form.pending" (click)="save()">
<ui-spinner [show]="busy$ | async">
Speichern
</ui-spinner>
</button>
</form>

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