From b443c7a5deaf063dec5b8cf18565347b067cf394 Mon Sep 17 00:00:00 2001 From: Nino Righi Date: Tue, 8 Mar 2022 12:25:32 +0000 Subject: [PATCH] Merged PR 1088: Merge Listenbestellung into Develop Merge Listenbestellung into Develop Related work items: #2560, #2655, #2656, #2699, #2745, #2746, #2747, #2749, #2752, #2760, #2789 --- angular.json | 40 ++ .../src/lib/availability.service.ts | 221 +++++-- .../checkout/src/lib/checkout.service.ts | 34 ++ .../reorder/src/lib/reorder.component.spec.ts | 8 +- .../reorder/src/lib/reorder.component.ts | 14 +- .../article-search/article-search.store.ts | 22 + .../added-to-cart-modal.component.html | 8 + .../added-to-cart-modal.component.scss | 19 + .../added-to-cart-modal.component.ts | 22 + .../search-result-item.component.html | 4 + .../search-result-item.component.scss | 6 + .../search-result-item.component.ts | 59 +- .../search-results.component.html | 17 +- .../search-results.component.scss | 18 + .../search-results.component.ts | 113 +++- .../search-results/search-results.module.ts | 27 +- .../selected/search-result-selected.pipe.ts | 11 + .../checkout-review.component.html | 189 +++--- .../checkout-review.component.scss | 57 +- .../checkout-review.component.ts | 225 +++++-- .../checkout-review/checkout-review.module.ts | 4 + .../shopping-cart-item.component.html | 74 +++ .../shopping-cart-item.component.scss | 139 +++++ .../shopping-cart-item.component.ts | 94 +++ .../special-comment.component.html | 12 +- .../checkout-summary.component.html | 10 +- .../delivery-b2b-option-list.component.html | 13 + .../delivery-b2b-option-list.component.ts | 23 + .../delivery-option-b2b/index.ts | 3 + .../delivery-option-list.component.html | 13 + .../delivery-option-list.component.ts | 23 + .../delivery-option/index.ts | 3 + .../purchasing-options-list-modal/index.ts | 7 + .../list-options.scss | 35 ++ .../pick-up-option/index.ts | 3 + .../pick-up-option-list.component.html | 18 + .../pick-up-option-list.component.ts | 34 ++ ...urchasing-options-list-item.component.html | 109 ++++ ...urchasing-options-list-item.component.scss | 166 ++++++ .../purchasing-options-list-item.component.ts | 147 +++++ ...rchasing-options-list-modal.component.html | 47 ++ ...rchasing-options-list-modal.component.scss | 49 ++ ...purchasing-options-list-modal.component.ts | 211 +++++++ .../purchasing-options-list-modal.data.ts | 12 + .../purchasing-options-list-modal.module.ts | 41 ++ .../purchasing-options-list-modal.store.ts | 560 ++++++++++++++++++ .../take-away-option/index.ts | 3 + .../take-away-option-list.component.html | 18 + .../take-away-option-list.component.ts | 34 ++ .../b2b-delivery-option.component.html | 2 +- .../b2b-delivery-option.component.ts | 14 +- .../delivery-option.component.html | 15 +- .../delivery-option.component.ts | 24 +- .../dig-delivery-option.component.html | 15 +- .../dig-delivery-option.component.ts | 24 +- .../pick-up-dropdown.component.ts | 44 -- .../pick-up-option.component.html | 8 +- .../pick-up-option.component.ts | 22 +- .../purchasing-options-modal.component.html | 20 +- .../purchasing-options-modal.component.scss | 4 + .../purchasing-options-modal.component.ts | 33 +- .../purchasing-options-modal.data.ts | 2 +- .../purchasing-options-modal.module.ts | 4 +- .../purchasing-options-modal.store.ts | 51 -- .../take-away-option.component.html | 9 +- .../take-away-option.component.ts | 14 +- .../src/lib/page-checkout-modals.module.ts | 5 +- .../process-tab/process-tab.component.html | 22 +- .../process-tab/process-tab.component.scss | 5 + ...s-in-out-order-details-item.component.html | 28 +- ...s-in-out-order-details-item.component.scss | 3 +- ...ods-in-out-order-details-item.component.ts | 5 +- .../availability/src/lib/av-configuration.ts | 4 + .../swagger/availability/src/lib/av.module.ts | 18 +- apps/swagger/availability/src/lib/models.ts | 6 + .../src/lib/models/availability-dto.ts | 33 +- .../lib/models/availability-request-dto.ts | 18 +- .../src/lib/models/availability-type.ts | 2 +- .../src/lib/models/dialog-content-type.ts | 2 + .../src/lib/models/dialog-of-string.ts | 16 + .../src/lib/models/dialog-settings.ts | 2 + .../src/lib/models/ipublic-user-info.ts | 2 +- .../key-value-dtoof-string-and-string.ts | 11 + .../availability/src/lib/models/price-dto.ts | 3 +- .../src/lib/models/price-value-dto.ts | 5 +- .../src/lib/models/problem-details.ts | 9 +- .../availability/src/lib/models/range-dto.ts | 5 + ...args-of-ienumerable-of-availability-dto.ts | 2 +- .../src/lib/models/response-args.ts | 4 +- .../src/lib/models/touched-base.ts | 3 + .../src/lib/models/vatvalue-dto.ts | 7 +- .../src/lib/services/availability.service.ts | 8 + apps/swagger/checkout/src/lib/models.ts | 8 +- .../checkout/src/lib/models/address-dto.ts | 3 +- .../src/lib/models/availability-dto.ts | 2 + .../lib/models/communication-details-dto.ts | 3 +- .../checkout/src/lib/models/date-range-dto.ts | 6 + .../src/lib/models/dialog-content-type.ts | 2 + .../src/lib/models/dialog-of-string.ts | 16 + .../src/lib/models/dialog-settings.ts | 2 + .../src/lib/models/organisation-dto.ts | 3 +- .../checkout/src/lib/models/price-dto.ts | 3 +- .../src/lib/models/price-value-dto.ts | 3 +- .../src/lib/models/problem-details.ts | 2 +- .../checkout/src/lib/models/product-dto.ts | 3 +- .../checkout/src/lib/models/promotion-dto.ts | 3 +- .../checkout/src/lib/models/response-args.ts | 2 + .../checkout/src/lib/models/vatvalue-dto.ts | 3 +- apps/ui/branch-dropdown/README.md | 25 + apps/ui/branch-dropdown/karma.conf.js | 32 + apps/ui/branch-dropdown/ng-package.json | 7 + apps/ui/branch-dropdown/package.json | 11 + .../lib/branch-dropdown-item.component.html | 3 + .../lib/branch-dropdown-item.component.scss | 11 + .../src/lib/branch-dropdown-item.component.ts | 37 ++ .../src/lib/branch-dropdown.component.html} | 28 +- .../src/lib/branch-dropdown.component.scss} | 17 +- .../src/lib/branch-dropdown.component.ts | 116 ++++ .../src/lib/branch-dropdown.module.ts | 14 + .../src/lib/branch-dropdown.service.spec.ts | 16 + .../src/lib/branch-dropdown.service.ts | 8 + apps/ui/branch-dropdown/src/public-api.ts | 7 + apps/ui/branch-dropdown/src/test.ts | 24 + apps/ui/branch-dropdown/tsconfig.lib.json | 25 + .../ui/branch-dropdown/tsconfig.lib.prod.json | 10 + apps/ui/branch-dropdown/tsconfig.spec.json | 17 + apps/ui/branch-dropdown/tslint.json | 17 + .../quantity-dropdown-content.component.html | 23 + .../quantity-dropdown-content.component.scss | 93 +++ .../quantity-dropdown-content.component.ts | 75 +++ .../src/lib/quantity-dropdown.component.html | 32 +- .../src/lib/quantity-dropdown.component.scss | 58 +- .../src/lib/quantity-dropdown.component.ts | 16 +- .../src/lib/quantity-dropdown.module.ts | 9 +- apps/ui/quantity-dropdown/src/public-api.ts | 1 + .../src/lib/select-bullet.component.ts | 1 + tsconfig.json | 3 + 137 files changed, 3724 insertions(+), 628 deletions(-) create mode 100644 apps/page/catalog/src/lib/article-search/search-results/added-to-cart-modal/added-to-cart-modal.component.html create mode 100644 apps/page/catalog/src/lib/article-search/search-results/added-to-cart-modal/added-to-cart-modal.component.scss create mode 100644 apps/page/catalog/src/lib/article-search/search-results/added-to-cart-modal/added-to-cart-modal.component.ts create mode 100644 apps/page/catalog/src/lib/article-search/search-results/selected/search-result-selected.pipe.ts create mode 100644 apps/page/checkout/src/lib/checkout-review/shopping-cart-item/shopping-cart-item.component.html create mode 100644 apps/page/checkout/src/lib/checkout-review/shopping-cart-item/shopping-cart-item.component.scss create mode 100644 apps/page/checkout/src/lib/checkout-review/shopping-cart-item/shopping-cart-item.component.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option-b2b/delivery-b2b-option-list.component.html create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option-b2b/delivery-b2b-option-list.component.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option-b2b/index.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option/delivery-option-list.component.html create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option/delivery-option-list.component.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option/index.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/index.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/list-options.scss create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/pick-up-option/index.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/pick-up-option/pick-up-option-list.component.html create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/pick-up-option/pick-up-option-list.component.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-item/purchasing-options-list-item.component.html create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-item/purchasing-options-list-item.component.scss create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-item/purchasing-options-list-item.component.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.component.html create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.component.scss create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.component.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.data.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.module.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.store.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/take-away-option/index.ts create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/take-away-option/take-away-option-list.component.html create mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-list-modal/take-away-option/take-away-option-list.component.ts delete mode 100644 apps/page/checkout/src/lib/modals/purchasing-options-modal/pick-up-option/pick-up-dropdown/pick-up-dropdown.component.ts create mode 100644 apps/swagger/availability/src/lib/models/dialog-content-type.ts create mode 100644 apps/swagger/availability/src/lib/models/dialog-of-string.ts create mode 100644 apps/swagger/availability/src/lib/models/dialog-settings.ts create mode 100644 apps/swagger/availability/src/lib/models/key-value-dtoof-string-and-string.ts create mode 100644 apps/swagger/availability/src/lib/models/range-dto.ts create mode 100644 apps/swagger/availability/src/lib/models/touched-base.ts create mode 100644 apps/swagger/checkout/src/lib/models/date-range-dto.ts create mode 100644 apps/swagger/checkout/src/lib/models/dialog-content-type.ts create mode 100644 apps/swagger/checkout/src/lib/models/dialog-of-string.ts create mode 100644 apps/swagger/checkout/src/lib/models/dialog-settings.ts create mode 100644 apps/ui/branch-dropdown/README.md create mode 100644 apps/ui/branch-dropdown/karma.conf.js create mode 100644 apps/ui/branch-dropdown/ng-package.json create mode 100644 apps/ui/branch-dropdown/package.json create mode 100644 apps/ui/branch-dropdown/src/lib/branch-dropdown-item.component.html create mode 100644 apps/ui/branch-dropdown/src/lib/branch-dropdown-item.component.scss create mode 100644 apps/ui/branch-dropdown/src/lib/branch-dropdown-item.component.ts rename apps/{page/checkout/src/lib/modals/purchasing-options-modal/pick-up-option/pick-up-dropdown/pick-up-dropdown.component.html => ui/branch-dropdown/src/lib/branch-dropdown.component.html} (63%) rename apps/{page/checkout/src/lib/modals/purchasing-options-modal/pick-up-option/pick-up-dropdown/pick-up-dropdown.component.scss => ui/branch-dropdown/src/lib/branch-dropdown.component.scss} (73%) create mode 100644 apps/ui/branch-dropdown/src/lib/branch-dropdown.component.ts create mode 100644 apps/ui/branch-dropdown/src/lib/branch-dropdown.module.ts create mode 100644 apps/ui/branch-dropdown/src/lib/branch-dropdown.service.spec.ts create mode 100644 apps/ui/branch-dropdown/src/lib/branch-dropdown.service.ts create mode 100644 apps/ui/branch-dropdown/src/public-api.ts create mode 100644 apps/ui/branch-dropdown/src/test.ts create mode 100644 apps/ui/branch-dropdown/tsconfig.lib.json create mode 100644 apps/ui/branch-dropdown/tsconfig.lib.prod.json create mode 100644 apps/ui/branch-dropdown/tsconfig.spec.json create mode 100644 apps/ui/branch-dropdown/tslint.json create mode 100644 apps/ui/quantity-dropdown/src/lib/quantity-dropdown-content/quantity-dropdown-content.component.html create mode 100644 apps/ui/quantity-dropdown/src/lib/quantity-dropdown-content/quantity-dropdown-content.component.scss create mode 100644 apps/ui/quantity-dropdown/src/lib/quantity-dropdown-content/quantity-dropdown-content.component.ts diff --git a/angular.json b/angular.json index da53f33f8..ad4ab9718 100644 --- a/angular.json +++ b/angular.json @@ -3296,6 +3296,46 @@ } } } + }, + "@ui/branch-dropdown": { + "projectType": "library", + "root": "apps/ui/branch-dropdown", + "sourceRoot": "apps/ui/branch-dropdown/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "tsConfig": "apps/ui/branch-dropdown/tsconfig.lib.json", + "project": "apps/ui/branch-dropdown/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "apps/ui/branch-dropdown/tsconfig.lib.prod.json" + } + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "apps/ui/branch-dropdown/src/test.ts", + "tsConfig": "apps/ui/branch-dropdown/tsconfig.spec.json", + "karmaConfig": "apps/ui/branch-dropdown/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "apps/ui/branch-dropdown/tsconfig.lib.json", + "apps/ui/branch-dropdown/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } } }, "defaultProject": "sales" diff --git a/apps/domain/availability/src/lib/availability.service.ts b/apps/domain/availability/src/lib/availability.service.ts index 2d20d315d..2f46e52e3 100644 --- a/apps/domain/availability/src/lib/availability.service.ts +++ b/apps/domain/availability/src/lib/availability.service.ts @@ -1,8 +1,14 @@ import { Injectable } from '@angular/core'; import { ItemDTO } from '@swagger/cat'; import { AvailabilityDTO, BranchDTO, OLAAvailabilityDTO, StoreCheckoutService, SupplierDTO } from '@swagger/checkout'; -import { Observable } from 'rxjs'; -import { AvailabilityService as SwaggerAvailabilityService } from '@swagger/availability'; +import { Observable, of } from 'rxjs'; +import { + AvailabilityRequestDTO, + AvailabilityService as SwaggerAvailabilityService, + 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'; @@ -168,29 +174,7 @@ export class DomainAvailabilityService { }, ]) .pipe( - map((r) => { - const availabilities = r.result; - - if (isArray(availabilities)) { - const preferred = availabilities.find((f) => f.preferred === 1); - const totalAvailable = availabilities.reduce((sum, av) => sum + (av?.qty || 0), 0); - - const availability: AvailabilityDTO = { - availabilityType: preferred?.status, - ssc: preferred?.ssc, - sscText: preferred?.sscText, - supplier: { id: preferred?.supplierId }, - isPrebooked: preferred?.isPrebooked, - estimatedShippingDate: preferred?.at, - price: preferred?.price, - inStock: totalAvailable, - supplierProductNumber: preferred?.supplierProductNumber, - supplierInfo: preferred?.requestStatusCode, - lastRequest: preferred?.requested, - }; - return availability; - } - }), + map((r) => this._mapToPickUpAvailability(r.result)?.find((_) => true)), shareReplay() ); } @@ -208,23 +192,7 @@ export class DomainAvailabilityService { ]) .pipe( timeout(5000), - map((r) => { - const availabilities = r.result; - const preferred = availabilities.find((f) => f.preferred === 1); - - const availability: AvailabilityDTO = { - availabilityType: preferred?.status, - ssc: preferred?.ssc, - sscText: preferred?.sscText, - isPrebooked: preferred?.isPrebooked, - estimatedShippingDate: preferred?.at, - price: preferred?.price, - supplierProductNumber: preferred?.supplierProductNumber, - supplierInfo: preferred?.requestStatusCode, - lastRequest: preferred?.requested, - }; - return availability; - }), + map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)), shareReplay() ); } @@ -244,7 +212,7 @@ export class DomainAvailabilityService { timeout(5000), map((r) => { const availabilities = r.result; - const preferred = availabilities.find((f) => f.preferred === 1); + const preferred = availabilities?.find((f) => f.preferred === 1); const availability: AvailabilityDTO = { availabilityType: preferred?.status, @@ -253,6 +221,7 @@ export class DomainAvailabilityService { supplier: { id: preferred?.supplierId }, isPrebooked: preferred?.isPrebooked, estimatedShippingDate: preferred?.at, + estimatedDelivery: preferred?.estimatedDelivery, price: preferred?.price, logistician: { id: preferred?.logisticianId }, supplierProductNumber: preferred?.supplierProductNumber, @@ -269,7 +238,7 @@ export class DomainAvailabilityService { getB2bDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable { const logistician$ = this.orderService .OrderGetLogisticians({}) - .pipe(map((response) => response.result.find((l) => l.logisticianNumber === '2470'))); + .pipe(map((response) => response.result?.find((l) => l.logisticianNumber === '2470'))); const currentBranch$ = this.getCurrentBranch(); @@ -298,7 +267,7 @@ export class DomainAvailabilityService { .pipe( map((r) => { const availabilities = r.result; - const preferred = availabilities.find((f) => f.preferred === 1); + const preferred = availabilities?.find((f) => f.preferred === 1); const availability: AvailabilityDTO = { availabilityType: preferred?.status, @@ -320,18 +289,85 @@ export class DomainAvailabilityService { } @memorize({ ttl: 10000 }) - getStoreAvailabilities({ item, branch, quantity }: { item: ItemData; quantity: number; branch: BranchDTO }) { - return this.swaggerAvailabilityService - .AvailabilityStoreAvailability([ - { - qty: quantity, - ean: item?.ean, - itemId: item?.itemId ? String(item?.itemId) : null, - shopId: branch?.id, - price: item?.price, - }, - ]) - .pipe(map((response) => response.result)); + getTakeAwayAvailabilities(items: { id: number; price: PriceDTO }[], branchId: number) { + return this._stock.StockGetStocksByBranch({ branchId }).pipe( + map((req) => req.result?.find((_) => true)?.id), + switchMap((stockId) => + stockId + ? this._stock.StockInStock({ articleIds: items.map((i) => i.id), stockId }) + : of({ result: [] } as ResponseArgsOfIEnumerableOfStockInfoDTO) + ), + timeout(20000), + withLatestFrom(this.getTakeAwaySupplier()), + map(([response, supplier]) => { + return response.result?.map((stockInfo) => + this._mapToTakeAwayAvailabilities({ + stockInfo, + supplier, + quantity: 1, + price: items?.find((i) => i.id === stockInfo.itemId)?.price, + }) + ); + }), + shareReplay() + ); + } + + @memorize({ ttl: 10000 }) + getPickUpAvailabilities(payload: AvailabilityRequestDTO[], preferred?: boolean) { + return this.swaggerAvailabilityService.AvailabilityStoreAvailability(payload).pipe( + timeout(20000), + map((response) => (preferred ? this._mapToPickUpAvailability(response.result) : response.result)) + ); + } + + @memorize({ ttl: 10000 }) + getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) { + return this.swaggerAvailabilityService.AvailabilityShippingAvailability(payload).pipe( + timeout(20000), + map((response) => this._mapToShippingAvailability(response.result)) + ); + } + + @memorize({ ttl: 10000 }) + getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) { + return this.swaggerAvailabilityService.AvailabilityShippingAvailability(payload).pipe( + timeout(20000), + map((response) => this._mapToShippingAvailability(response.result)) + ); + } + + @memorize({ ttl: 10000 }) + getB2bDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) { + const logistician$ = this.orderService + .OrderGetLogisticians({}) + .pipe(map((response) => response.result?.find((l) => l.logisticianNumber === '2470'))); + + return this.getPickUpAvailabilities(payload, true).pipe( + timeout(20000), + switchMap((availability) => + logistician$.pipe(map((logistician) => ({ availability: [...availability], logistician: { id: logistician.id } }))) + ), + shareReplay() + ); + } + + getPriceForAvailability( + purchasingOption: string, + catalogAvailability: CatAvailabilityDTO | AvailabilityDTO, + availability: AvailabilityDTO + ): PriceDTO { + switch (purchasingOption) { + case 'take-away': + return availability?.price || availability?.retailPrice; + case 'delivery': + case 'dig-delivery': + if (catalogAvailability?.price?.value?.value < availability?.price?.value?.value) { + return catalogAvailability.price; + } + return availability?.price; + } + return availability?.price; } isAvailable({ availability }: { availability: AvailabilityDTO }) { @@ -377,7 +413,7 @@ export class DomainAvailabilityService { quantity: number; price: PriceDTO; }): AvailabilityDTO { - const stockInfo = response.result.find((si) => si.branchId === branch.id); + const stockInfo = response.result?.find((si) => si.branchId === branch.id); const inStock = stockInfo?.inStock ?? 0; const availability: AvailabilityDTO = { availabilityType: quantity <= inStock ? 1024 : 1, // 1024 (=Available) @@ -390,4 +426,73 @@ export class DomainAvailabilityService { }; return availability; } + + private _mapToTakeAwayAvailabilities({ + stockInfo, + quantity, + price, + supplier, + }: { + stockInfo: StockInfoDTO; + quantity: number; + price: PriceDTO; + supplier: SupplierDTO; + }) { + const inStock = stockInfo?.inStock ?? 0; + + const availability = { + itemId: stockInfo.itemId, + availabilityType: quantity <= inStock ? (1024 as AvailabilityType) : (1 as AvailabilityType), // 1024 (=Available) + inStock: inStock, + supplierSSC: quantity <= inStock ? '999' : '', + supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '', + price, + supplier: { id: supplier?.id }, + }; + return availability; + } + + private _mapToPickUpAvailability(availabilities: 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?.at, + price: p?.price, + inStock: totalAvailable, + supplierProductNumber: p?.supplierProductNumber, + supplierInfo: p?.requestStatusCode, + lastRequest: p?.requested, + itemId: p.itemId, + }; + }); + } + } + + private _mapToShippingAvailability(availabilities: SwaggerAvailabilityDTO[]) { + const preferred = availabilities.filter((f) => f.preferred === 1); + + return preferred.map((p) => { + return { + availabilityType: p?.status, + ssc: p?.ssc, + sscText: p?.sscText, + isPrebooked: p?.isPrebooked, + estimatedShippingDate: p?.at, + estimatedDelivery: p?.estimatedDelivery, + price: p?.price, + supplierProductNumber: p?.supplierProductNumber, + supplierInfo: p?.requestStatusCode, + lastRequest: p?.requested, + itemId: p.itemId, + }; + }); + } } diff --git a/apps/domain/checkout/src/lib/checkout.service.ts b/apps/domain/checkout/src/lib/checkout.service.ts index d94f1ef06..d49d113a2 100644 --- a/apps/domain/checkout/src/lib/checkout.service.ts +++ b/apps/domain/checkout/src/lib/checkout.service.ts @@ -183,6 +183,40 @@ export class DomainCheckoutService { ); } + canAddItems({ + processId, + availabilities, + orderType, + }: { + processId: number; + availabilities: OLAAvailabilityDTO[]; + orderType: string; + }): Observable { + return this.getShoppingCart({ processId }).pipe( + first(), + withLatestFrom(this.store.select(DomainCheckoutSelectors.selectCustomerFeaturesByProcessId, { processId })), + mergeMap(([shoppingCart, customerFeatures]) => + this.storeCheckoutService + .StoreCheckoutCanAddItem({ + shoppingCartId: shoppingCart?.id, + payload: { + customerFeatures, + availabilities, + orderType, + }, + }) + .pipe( + map((response) => { + if (response.result.ok) { + return true; + } + return response.message; + }) + ) + ) + ); + } + updateItemInShoppingCart({ processId, shoppingCartItemId, diff --git a/apps/modal/reorder/src/lib/reorder.component.spec.ts b/apps/modal/reorder/src/lib/reorder.component.spec.ts index ebc796f78..37ad54694 100644 --- a/apps/modal/reorder/src/lib/reorder.component.spec.ts +++ b/apps/modal/reorder/src/lib/reorder.component.spec.ts @@ -50,7 +50,7 @@ describe('ReorderModalComponent', () => { mockProvider(DomainCheckoutService), mockProvider(DomainAvailabilityService, { getCurrentBranch: jasmine.createSpy().and.returnValue(of({})), - getStoreAvailabilities: jasmine.createSpy().and.returnValue(of(storeAvailabilitiesMock)), + getPickUpAvailabilities: jasmine.createSpy().and.returnValue(of(storeAvailabilitiesMock)), getTakeAwayAvailability: jasmine.createSpy().and.returnValue(of(takeAwayAvailabiltyMock)), }), mockProvider(DomainOmsService), @@ -76,11 +76,7 @@ describe('ReorderModalComponent', () => { spectator.component.patchState({ orderItem }); await spectator.detectComponentChanges(); - expect(domainAvailabilityServiceMock.getStoreAvailabilities).toHaveBeenCalledWith({ - item: { ean: orderItem.product.ean, itemId: +orderItem.product.catalogProductNumber, price: orderItem.retailPrice }, - quantity: 1, - branch: {}, - }); + expect(domainAvailabilityServiceMock.getPickUpAvailabilities).toHaveBeenCalled(); }); it('should set checkedAvailability to the preferred availability', async () => { diff --git a/apps/modal/reorder/src/lib/reorder.component.ts b/apps/modal/reorder/src/lib/reorder.component.ts index a687c1e6c..749b7388d 100644 --- a/apps/modal/reorder/src/lib/reorder.component.ts +++ b/apps/modal/reorder/src/lib/reorder.component.ts @@ -69,11 +69,15 @@ export class ReorderModalComponent extends ComponentStore this.domainAvailabilityService - .getStoreAvailabilities({ - item: { ean: item.product.ean, itemId: +item.product?.catalogProductNumber, price: item.retailPrice }, - branch, - quantity: item.quantity, - }) + .getPickUpAvailabilities([ + { + qty: item.quantity, + ean: item.product.ean, + itemId: item.product?.catalogProductNumber, + shopId: branch.id, + price: item.retailPrice, + }, + ]) .pipe( catchError(() => { this.patchState({ storeAvailabilityError: true }); diff --git a/apps/page/catalog/src/lib/article-search/article-search.store.ts b/apps/page/catalog/src/lib/article-search/article-search.store.ts index 0c1c88471..9434a14f1 100644 --- a/apps/page/catalog/src/lib/article-search/article-search.store.ts +++ b/apps/page/catalog/src/lib/article-search/article-search.store.ts @@ -13,6 +13,7 @@ export interface ArticleSearchState { searchState: '' | 'fetching' | 'empty' | 'error'; items: ItemDTO[]; hits: number; + selectedItemIds: number[]; } @Injectable() @@ -48,6 +49,12 @@ export class ArticleSearchService extends ComponentStore { searchboxHint$ = this.select((s) => (s.searchState === 'empty' ? 'Keine Suchergebnisse' : undefined)); + selectedItemIds$ = this.select((s) => s.selectedItemIds); + + get selectedItemIds() { + return this.get((s) => s.selectedItemIds); + } + hits$ = this.select((s) => s.hits); get hits() { @@ -61,6 +68,7 @@ export class ArticleSearchService extends ComponentStore { items: [], processId: 0, searchState: '', + selectedItemIds: [], }); this.setDefaultFilter(); } @@ -94,6 +102,20 @@ export class ArticleSearchService extends ComponentStore { this.patchState({ filter }); } + setSelected({ selected, itemId }: { selected: boolean; itemId: number }) { + const included = this.selectedItemIds.includes(itemId); + + if (!included && selected) { + this.patchState({ + selectedItemIds: [...this.selectedItemIds, itemId], + }); + } else if (included && !selected) { + this.patchState({ + selectedItemIds: this.selectedItemIds.filter((id) => id !== itemId), + }); + } + } + search = this.effect((options$: Observable<{ clear?: boolean }>) => options$.pipe( tap((options) => { diff --git a/apps/page/catalog/src/lib/article-search/search-results/added-to-cart-modal/added-to-cart-modal.component.html b/apps/page/catalog/src/lib/article-search/search-results/added-to-cart-modal/added-to-cart-modal.component.html new file mode 100644 index 000000000..18dc82327 --- /dev/null +++ b/apps/page/catalog/src/lib/article-search/search-results/added-to-cart-modal/added-to-cart-modal.component.html @@ -0,0 +1,8 @@ +
+ + +
diff --git a/apps/page/catalog/src/lib/article-search/search-results/added-to-cart-modal/added-to-cart-modal.component.scss b/apps/page/catalog/src/lib/article-search/search-results/added-to-cart-modal/added-to-cart-modal.component.scss new file mode 100644 index 000000000..5c493deca --- /dev/null +++ b/apps/page/catalog/src/lib/article-search/search-results/added-to-cart-modal/added-to-cart-modal.component.scss @@ -0,0 +1,19 @@ +.actions { + @apply flex flex-row justify-center items-center pb-4; + + .cta { + @apply border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap no-underline; + + &:disabled { + @apply bg-inactive-branch border-inactive-branch text-white; + } + } + + .cta-action-primary { + @apply bg-brand text-white ml-2; + } + + .cta-action-secondary { + @apply bg-white text-brand mr-2; + } +} diff --git a/apps/page/catalog/src/lib/article-search/search-results/added-to-cart-modal/added-to-cart-modal.component.ts b/apps/page/catalog/src/lib/article-search/search-results/added-to-cart-modal/added-to-cart-modal.component.ts new file mode 100644 index 000000000..8cea2d06a --- /dev/null +++ b/apps/page/catalog/src/lib/article-search/search-results/added-to-cart-modal/added-to-cart-modal.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { UiModalRef } from '@ui/modal'; + +@Component({ + selector: 'added-to-cart-modal', + templateUrl: 'added-to-cart-modal.component.html', + styleUrls: ['added-to-cart-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AddedToCartModalComponent { + constructor(public ref: UiModalRef, private readonly _router: Router) {} + continue() { + this._router.navigate(['/product/search']); + this.ref.close(); + } + + toCart() { + this._router.navigate(['/cart/review']); + this.ref.close(); + } +} diff --git a/apps/page/catalog/src/lib/article-search/search-results/search-result-item.component.html b/apps/page/catalog/src/lib/article-search/search-results/search-result-item.component.html index 1d31f7d2a..58640dc0c 100644 --- a/apps/page/catalog/src/lib/article-search/search-results/search-result-item.component.html +++ b/apps/page/catalog/src/lib/article-search/search-results/search-result-item.component.html @@ -29,6 +29,10 @@ {{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }} +
+ +
+
{{ item?.stockInfos | stockInfos }} x
diff --git a/apps/page/catalog/src/lib/article-search/search-results/search-result-item.component.scss b/apps/page/catalog/src/lib/article-search/search-results/search-result-item.component.scss index 9ccbfb174..79a8d838c 100644 --- a/apps/page/catalog/src/lib/article-search/search-results/search-result-item.component.scss +++ b/apps/page/catalog/src/lib/article-search/search-results/search-result-item.component.scss @@ -5,6 +5,7 @@ grid-template-areas: 'item-thumbnail item-contributors item-contributors' 'item-thumbnail item-title item-price' + 'item-thumbnail item-title item-data-selector' 'item-thumbnail item-format item-stock' 'item-thumbnail item-misc item-ssc'; } @@ -96,6 +97,11 @@ @apply font-bold text-xs; } +.item-data-selector { + @apply w-full flex justify-end; + grid-area: item-data-selector; +} + @media (min-width: 1025px) { .item-contributors { max-width: 780px; diff --git a/apps/page/catalog/src/lib/article-search/search-results/search-result-item.component.ts b/apps/page/catalog/src/lib/article-search/search-results/search-result-item.component.ts index 206f1fb83..863515631 100644 --- a/apps/page/catalog/src/lib/article-search/search-results/search-result-item.component.ts +++ b/apps/page/catalog/src/lib/article-search/search-results/search-result-item.component.ts @@ -1,7 +1,16 @@ import { DatePipe } from '@angular/common'; -import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; +import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output } from '@angular/core'; +import { ComponentStore } from '@ngrx/component-store'; import { ItemDTO } from '@swagger/cat'; import { DateAdapter } from '@ui/common'; +import { isEqual } from 'lodash'; +import { ArticleSearchService } from '../article-search.store'; + +export interface SearchResultItemComponentState { + item?: ItemDTO; + selected: boolean; + selectable: boolean; +} @Component({ selector: 'search-result-item', @@ -9,9 +18,42 @@ import { DateAdapter } from '@ui/common'; styleUrls: ['search-result-item.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SearchResultItemComponent { +export class SearchResultItemComponent extends ComponentStore { @Input() - item: ItemDTO; + get item() { + return this.get((s) => s.item); + } + set item(item: ItemDTO) { + if (!isEqual(this.item, item)) { + this.patchState({ item }); + } + } + + readonly item$ = this.select((s) => s.item); + + @Input() + get selected() { + return this.get((s) => s.selected); + } + set selected(selected: boolean) { + if (this.selected !== selected) { + this.patchState({ selected }); + } + } + readonly selected$ = this.select((s) => s.selected); + + @Input() + get selectable() { + return this.get((s) => s.selectable); + } + set selectable(selectable: boolean) { + if (this.selectable !== selectable) { + this.patchState({ selectable }); + } + } + + @Output() + selectedChange = new EventEmitter(); get contributors() { return this.item?.product?.contributors?.split(';').map((val) => val.trim()); @@ -30,5 +72,14 @@ export class SearchResultItemComponent { return ''; } - constructor(private _dateAdapter: DateAdapter, private _datePipe: DatePipe) {} + constructor(private _dateAdapter: DateAdapter, private _datePipe: DatePipe, private _articleSearchService: ArticleSearchService) { + super({ + selected: false, + selectable: false, + }); + } + + setSelected(selected: boolean) { + this._articleSearchService.setSelected({ selected, itemId: this.item?.id }); + } } diff --git a/apps/page/catalog/src/lib/article-search/search-results/search-results.component.html b/apps/page/catalog/src/lib/article-search/search-results/search-results.component.html index 6d7b81cf5..4d65b7514 100644 --- a/apps/page/catalog/src/lib/article-search/search-results/search-results.component.html +++ b/apps/page/catalog/src/lib/article-search/search-results/search-results.component.html @@ -12,7 +12,22 @@ (scrolledIndexChange)="scrolledIndexChange($event)" >
- +
+ +
+ +
diff --git a/apps/page/catalog/src/lib/article-search/search-results/search-results.component.scss b/apps/page/catalog/src/lib/article-search/search-results/search-results.component.scss index 446f08181..445dfd6b6 100644 --- a/apps/page/catalog/src/lib/article-search/search-results/search-results.component.scss +++ b/apps/page/catalog/src/lib/article-search/search-results/search-results.component.scss @@ -26,3 +26,21 @@ @apply mx-auto; } } + +.actions { + @apply fixed bottom-28 inline-grid grid-flow-col gap-7; + left: 50%; + transform: translateX(-50%); + + .cta-cart { + @apply border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap no-underline; + + &:disabled { + @apply bg-inactive-branch border-inactive-branch text-white; + } + } + + .cta-action-primary { + @apply bg-brand text-white; + } +} diff --git a/apps/page/catalog/src/lib/article-search/search-results/search-results.component.ts b/apps/page/catalog/src/lib/article-search/search-results/search-results.component.ts index 0c2c7ff9e..146f7dcb7 100644 --- a/apps/page/catalog/src/lib/article-search/search-results/search-results.component.ts +++ b/apps/page/catalog/src/lib/article-search/search-results/search-results.component.ts @@ -1,15 +1,20 @@ import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; -import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, ViewChildren, QueryList } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { ApplicationService } from '@core/application'; import { BreadcrumbService } from '@core/breadcrumb'; +import { DomainCheckoutService } from '@domain/checkout'; import { ItemDTO } from '@swagger/cat'; +import { AddToShoppingCartDTO } from '@swagger/checkout'; import { UiFilter } from '@ui/filter'; +import { UiErrorModalComponent, UiModalRef, UiModalService } from '@ui/modal'; import { CacheService } from 'apps/core/cache/src/public-api'; import { isEqual } from 'lodash'; -import { combineLatest, Subscription } from 'rxjs'; -import { debounceTime, first } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; +import { debounceTime, first, map, tap } from 'rxjs/operators'; import { ArticleSearchService } from '../article-search.store'; +import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component'; +import { SearchResultItemComponent } from './search-result-item.component'; @Component({ selector: 'page-search-results', @@ -18,6 +23,7 @@ import { ArticleSearchService } from '../article-search.store'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ArticleSearchResultsComponent implements OnInit, OnDestroy { + @ViewChildren(SearchResultItemComponent) listItems: QueryList; @ViewChild('scrollContainer', { static: true }) scrollContainer: CdkVirtualScrollViewport; @@ -27,16 +33,28 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy { filter$ = this.searchService.filter$; - trackByItemId = (item: ItemDTO) => item.id; + selectedItemIds$ = this.searchService.selectedItemIds$; + + selectedItems$ = combineLatest([this.results$, this.selectedItemIds$]).pipe( + map(([items, selectedItemIds]) => { + return items?.filter((item) => selectedItemIds?.find((selectedItemId) => item.id === selectedItemId)); + }) + ); + + loading$ = new BehaviorSubject(false); private subscriptions = new Subscription(); + trackByItemId = (item: ItemDTO) => item.id; + constructor( - private searchService: ArticleSearchService, + public searchService: ArticleSearchService, private route: ActivatedRoute, private application: ApplicationService, private breadcrumb: BreadcrumbService, - private cache: CacheService + private cache: CacheService, + private _uiModal: UiModalService, + private _checkoutService: DomainCheckoutService ) {} ngOnInit() { @@ -68,6 +86,12 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy { this.search(); } else { this.scrollTop(Number(queryParams.scroll_position ?? 0)); + const selectedItemIds: Array = queryParams?.selected_item_ids?.split(',') ?? []; + for (const id of selectedItemIds) { + if (id) { + this.searchService.setSelected({ selected: true, itemId: Number(id) }); + } + } } } @@ -83,6 +107,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy { this.cacheCurrentData(this.searchService.processId, this.searchService.filter.getQueryParams()); this.updateBreadcrumbs(this.searchService.processId, this.searchService.filter.getQueryParams()); + this.unselectAll(); } search() { @@ -119,6 +144,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy { queryParams: Record = this.searchService.filter?.getQueryParams() ) { const scroll_position = this.scrollContainer.measureScrollOffset('top'); + const selected_item_ids = this.searchService?.selectedItemIds?.toString(); if (queryParams) { const crumbs = await this.breadcrumb @@ -127,7 +153,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy { .toPromise(); const name = queryParams.main_qs ? queryParams.main_qs : 'Alle Artikel'; - const params = { ...queryParams, scroll_position }; + const params = { ...queryParams, scroll_position, selected_item_ids }; for (const crumb of crumbs) { this.breadcrumb.patchBreadcrumb(crumb.id, { @@ -181,6 +207,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy { cleanupQueryParams(params: Record = {}) { const clean = { ...params }; delete clean['scroll_position']; + delete clean['selected_item_ids']; for (const key in clean) { if (Object.prototype.hasOwnProperty.call(clean, key)) { @@ -192,4 +219,76 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy { return clean; } + + isSelectable(item: ItemDTO) { + // Zeige Select Radio Button nicht an wenn Item Archivartikel oder Fortsetzungsartikel ist + const isArchiv = item?.catalogAvailability?.status === 1; + const isFortsetzung = item?.features?.find((i) => i?.key === 'PFO'); + return !(isArchiv || isFortsetzung); + } + + unselectAll() { + this.listItems.forEach((listItem) => this.searchService.setSelected({ selected: false, itemId: listItem.item.id })); + this.searchService.patchState({ selectedItemIds: [] }); + } + + async addSelectedItemsToCart() { + this.loading$.next(true); + const selectedItems = await this.selectedItems$.pipe(first()).toPromise(); + const items: AddToShoppingCartDTO[] = []; + + for (const item of selectedItems) { + const isDownload = item?.product?.format === 'EB' || item?.product?.format === 'DL'; + const shoppingCartItem: AddToShoppingCartDTO = { + quantity: 1, + availability: { + availabilityType: item?.catalogAvailability?.status, + price: item?.catalogAvailability?.price, + }, + product: { + catalogProductNumber: String(item?.id), + ...item?.product, + }, + promotion: { points: item?.promoPoints }, + }; + + if (isDownload) { + shoppingCartItem.destination = { data: { target: 16 } }; + } + + items.push(shoppingCartItem); + } + + try { + await this._checkoutService + .addItemToShoppingCart({ + processId: this.application.activatedProcessId, + items, + }) + .toPromise(); + this.openModal({ itemLength: selectedItems?.length }); + } catch (error) { + this.openModal({ itemLength: selectedItems?.length, error }); + console.error(error); + } + } + + openModal({ itemLength, error }: { itemLength: number; error?: Error }) { + const modal = this._uiModal.open({ + title: !error + ? `${itemLength} Artikel ${itemLength > 1 ? 'wurden' : 'wurde'} in den Warenkorb gelegt` + : `Artikel ${itemLength > 1 ? 'konnten' : 'konnte'} nicht in den Warenkorb gelegt werden`, + content: !error ? AddedToCartModalComponent : UiErrorModalComponent, + data: error ? error : {}, + config: { showScrollbarY: false }, + }); + this.subscriptions.add( + modal.afterClosed$.subscribe(() => { + if (!error) { + this.unselectAll(); + } + this.loading$.next(false); + }) + ); + } } diff --git a/apps/page/catalog/src/lib/article-search/search-results/search-results.module.ts b/apps/page/catalog/src/lib/article-search/search-results/search-results.module.ts index e47c30165..88785bef3 100644 --- a/apps/page/catalog/src/lib/article-search/search-results/search-results.module.ts +++ b/apps/page/catalog/src/lib/article-search/search-results/search-results.module.ts @@ -1,20 +1,43 @@ import { ScrollingModule } from '@angular/cdk/scrolling'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { DomainCatalogModule } from '@domain/catalog'; import { UiCommonModule } from '@ui/common'; import { UiIconModule } from '@ui/icon'; +import { UiSelectBulletModule } from '@ui/select-bullet'; import { UiOrderByFilterModule } from 'apps/ui/filter/src/lib/next/order-by-filter/order-by-filter.module'; +import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module'; +import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component'; import { StockInfosPipe } from './order-by-filter/stick-infos.pipe'; import { SearchResultItemLoadingComponent } from './search-result-item-loading.component'; import { SearchResultItemComponent } from './search-result-item.component'; import { ArticleSearchResultsComponent } from './search-results.component'; +import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe'; @NgModule({ - imports: [CommonModule, RouterModule, DomainCatalogModule, UiCommonModule, UiIconModule, UiOrderByFilterModule, ScrollingModule], + imports: [ + CommonModule, + FormsModule, + RouterModule, + DomainCatalogModule, + UiCommonModule, + UiIconModule, + UiSelectBulletModule, + UiSpinnerModule, + UiOrderByFilterModule, + ScrollingModule, + ], exports: [ArticleSearchResultsComponent, SearchResultItemComponent], - declarations: [ArticleSearchResultsComponent, SearchResultItemComponent, StockInfosPipe, SearchResultItemLoadingComponent], + declarations: [ + ArticleSearchResultsComponent, + SearchResultItemComponent, + StockInfosPipe, + SearchResultItemLoadingComponent, + SearchResultSelectedPipe, + AddedToCartModalComponent, + ], providers: [], }) export class SearchResultsModule {} diff --git a/apps/page/catalog/src/lib/article-search/search-results/selected/search-result-selected.pipe.ts b/apps/page/catalog/src/lib/article-search/search-results/selected/search-result-selected.pipe.ts new file mode 100644 index 000000000..9ae930f2e --- /dev/null +++ b/apps/page/catalog/src/lib/article-search/search-results/selected/search-result-selected.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { ItemDTO } from '@swagger/cat'; + +@Pipe({ + name: 'searchResultSelected', +}) +export class SearchResultSelectedPipe implements PipeTransform { + transform(item: ItemDTO, selectedItemIds: number[]): any { + return selectedItemIds?.includes(item?.id); + } +} diff --git a/apps/page/checkout/src/lib/checkout-review/checkout-review.component.html b/apps/page/checkout/src/lib/checkout-review/checkout-review.component.html index d56a06b4d..d10d6d312 100644 --- a/apps/page/checkout/src/lib/checkout-review/checkout-review.component.html +++ b/apps/page/checkout/src/lib/checkout-review/checkout-review.component.html @@ -30,21 +30,33 @@

Warenkorb

Überprüfen Sie die Details.
-
-
-
- Rechnungsadresse + + +
+
+ +
+ Rechnungsadresse +
+
+ {{ payer$ | async | payerAddress | trim: 55 }} +
+
+ +
+ Name, Vorname +
+
{{ payer.lastName }}, {{ payer.firstName }}
+
+ +
+
+ +
-
- {{ payer$ | async | payerAddress | trim: 55 }} -
-
-
- -
-
+
@@ -57,32 +69,41 @@ (ngModelChange)="setAgentComment($event)" (isDirtyChange)="specialCommentIsDirty = $event" > -
-
- -
- {{ group.orderType !== 'Dummy' ? group.orderType : 'Manuelle Anlage / Dummy Bestellung' }} - + +
+
+ +
+ {{ group.orderType !== 'Dummy' ? group.orderType : 'Manuelle Anlage / Dummy Bestellung' }} + +
+ +
+
+ +
-
+
@@ -102,7 +123,9 @@
- +
@@ -114,78 +137,19 @@ -
-
- product-image -
- -
-
- book-icon - {{ item?.product?.manufacturer }} + - - {{ ' | ' + item?.product?.contributors | trim: 30 }} - -
-
- {{ group.orderType === 'DIG-Versand' ? 'Versand' : group.orderType }} {{ group.orderType === 'Abholung' ? 'ab' : '' }} - {{ item?.availability?.estimatedShippingDate | date }} -
-
- Abholung {{ group.orderType === 'Dummy' ? 'ab' : '' }} - {{ item?.availability?.estimatedShippingDate ? (item?.availability?.estimatedShippingDate | date) : '-' }} -
-
-
- {{ item?.unitPrice?.value?.value | currency: item?.unitPrice?.value?.currency:'code' }} -
-
- - - - {{ item?.quantity }} - -
-
- - -
-

-
-
diff --git a/apps/page/checkout/src/lib/checkout-review/checkout-review.component.scss b/apps/page/checkout/src/lib/checkout-review/checkout-review.component.scss index 17b120557..1caface2d 100644 --- a/apps/page/checkout/src/lib/checkout-review/checkout-review.component.scss +++ b/apps/page/checkout/src/lib/checkout-review/checkout-review.component.scss @@ -64,16 +64,16 @@ .cta-primary { @apply bg-brand text-white font-bold text-lg outline-none border-brand border-solid border-2 rounded-full px-6 py-3; + + &:disabled { + @apply bg-inactive-customer border-none; + } } .cta-secondary { @apply bg-white text-brand border-none font-bold text-lg outline-none px-6 py-3 mt-4; } -.cta-order.special-comment-dirty { - @apply bg-active-branch text-white; -} - .cta-edit { @apply text-lg; } @@ -129,50 +129,11 @@ hr { @apply text-regular overflow-hidden overflow-ellipsis ml-4; } -.product-name { - @apply overflow-ellipsis whitespace-nowrap overflow-hidden text-active-customer text-base font-bold; - width: 130px; - - a { - @apply text-active-customer no-underline; - } -} - .book-icon { @apply mr-2 w-px-15; height: 18px; } -.product-misc-container { - @apply flex flex-col; - width: 210px; -} - -.product-misc { - @apply overflow-ellipsis whitespace-nowrap overflow-hidden; -} - -.product-delivery { - @apply font-bold; -} - -.product-price { - @apply font-bold; -} - -.product-image { - @apply w-px-50 h-px-50 text-center overflow-hidden; -} - -.product-container { - @apply py-0 px-4 flex flex-row justify-between; - height: 80px; -} - -.product-actions { - min-width: 80px; -} - .footer { @apply absolute bottom-0 left-0 right-0 p-7; box-shadow: 0px -2px 24px 0px #dce2e9; @@ -197,13 +158,3 @@ hr { .total-item-reading-points { @apply text-base font-bold text-ucla-blue; } - -@media (min-width: 1025px) { - .product-misc-container { - width: 300px; - } - - .product-name { - width: 225px; - } -} diff --git a/apps/page/checkout/src/lib/checkout-review/checkout-review.component.ts b/apps/page/checkout/src/lib/checkout-review/checkout-review.component.ts index 5dbcdbcc9..f83bd6bbd 100644 --- a/apps/page/checkout/src/lib/checkout-review/checkout-review.component.ts +++ b/apps/page/checkout/src/lib/checkout-review/checkout-review.component.ts @@ -3,19 +3,27 @@ import { Router } from '@angular/router'; import { ApplicationService } from '@core/application'; import { DomainAvailabilityService } from '@domain/availability'; import { DomainCheckoutService } from '@domain/checkout'; -import { AvailabilityDTO, DestinationDTO, ShoppingCartItemDTO } from '@swagger/checkout'; +import { AvailabilityDTO, DestinationDTO, ShoppingCartDTO, ShoppingCartItemDTO } from '@swagger/checkout'; import { UiMessageModalComponent, UiModalService } from '@ui/modal'; import { PrintModalData, PrintModalComponent } from '@modal/printer'; -import { first, map, shareReplay, switchMap, takeUntil } from 'rxjs/operators'; +import { first, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators'; import { SsoService } from 'sso'; import { PurchasingOptionsModalComponent, PurchasingOptionsModalData } from '../modals/purchasing-options-modal'; import { PurchasingOptions } from '../modals/purchasing-options-modal/purchasing-options-modal.store'; -import { Subject, NEVER } from 'rxjs'; +import { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs'; import { DomainCatalogService } from '@domain/catalog'; import { BreadcrumbService } from '@core/breadcrumb'; import { DomainPrinterService } from '@domain/printer'; import { CheckoutDummyComponent } from '../checkout-dummy/checkout-dummy.component'; import { ResponseArgsOfItemDTO } from '@swagger/cat'; +import { PurchasingOptionsListModalComponent } from '../modals/purchasing-options-list-modal'; +import { PurchasingOptionsListModalData } from '../modals/purchasing-options-list-modal/purchasing-options-list-modal.data'; +import { ComponentStore, tapResponse } from '@ngrx/component-store'; + +export interface CheckoutReviewComponentState { + shoppingCart: ShoppingCartDTO; + shoppingCartItems: ShoppingCartItemDTO[]; +} @Component({ selector: 'page-checkout-review', @@ -23,18 +31,29 @@ import { ResponseArgsOfItemDTO } from '@swagger/cat'; styleUrls: ['checkout-review.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CheckoutReviewComponent implements OnInit { +export class CheckoutReviewComponent extends ComponentStore implements OnInit { private _orderCompleted = new Subject(); - shoppingCart$ = this.applicationService.activatedProcessId$.pipe( - takeUntil(this._orderCompleted), - switchMap((processId) => this.domainCheckoutService.getShoppingCart({ processId, latest: true })), - shareReplay() - ); + get shoppingCart() { + return this.get((s) => s.shoppingCart); + } + set shoppingCart(shoppingCart: ShoppingCartDTO) { + this.patchState({ shoppingCart }); + } + readonly shoppingCart$ = this.select((s) => s.shoppingCart); + + get shoppingCartItems() { + return this.get((s) => s.shoppingCartItems); + } + set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) { + this.patchState({ shoppingCartItems }); + } + readonly shoppingCartItems$ = this.select((s) => s.shoppingCartItems); payer$ = this.applicationService.activatedProcessId$.pipe( takeUntil(this._orderCompleted), - switchMap((processId) => this.domainCheckoutService.getPayer({ processId })) + switchMap((processId) => this.domainCheckoutService.getPayer({ processId })), + shareReplay() ); shippingAddress$ = this.applicationService.activatedProcessId$.pipe( @@ -42,12 +61,12 @@ export class CheckoutReviewComponent implements OnInit { switchMap((processId) => this.domainCheckoutService.getShippingAddress({ processId })) ); - items$ = this.shoppingCart$.pipe( + shoppingCartItemsWithoutOrderType$ = this.shoppingCartItems$.pipe( takeUntil(this._orderCompleted), - map((shoppingCart) => shoppingCart?.items?.map((item) => item.data) || []) + map((items) => items?.filter((item) => item?.features?.orderType === undefined)) ); - groupedItems$ = this.items$.pipe( + groupedItems$ = this.shoppingCartItems$.pipe( takeUntil(this._orderCompleted), map((items) => items.reduce((grouped, item) => { @@ -65,10 +84,10 @@ export class CheckoutReviewComponent implements OnInit { orderType: item?.availability?.supplyChannel === 'MANUALLY' ? 'Dummy' - : item.features.orderType === 'DIG-Versand' + : item?.features?.orderType === 'DIG-Versand' ? 'Versand' - : item.features.orderType, - destination: item.destination?.data, + : item?.features?.orderType, + destination: item?.destination?.data, items: [], }; } @@ -81,26 +100,21 @@ export class CheckoutReviewComponent implements OnInit { grouped.push(group); } - return [...grouped]; + return [...grouped].sort((a, b) => (a?.orderType === undefined ? -1 : b?.orderType === undefined ? 1 : 0)); }, [] as { orderType: string; destination: DestinationDTO; items: ShoppingCartItemDTO[] }[]) ) ); - hasItemsForDelivery$ = this.items$.pipe( - takeUntil(this._orderCompleted), - map((items) => items.some((item) => item.features?.orderType === 'Versand' || item.features?.orderType === 'B2B-Versand')) - ); - specialComment$ = this.applicationService.activatedProcessId$.pipe( switchMap((processId) => this.domainCheckoutService.getSpecialComment({ processId })) ); - totalItemCount$ = this.items$.pipe( + totalItemCount$ = this.shoppingCartItems$.pipe( takeUntil(this._orderCompleted), map((items) => items.reduce((total, item) => total + item.quantity, 0)) ); - totalReadingPoints$ = this.items$.pipe( + totalReadingPoints$ = this.shoppingCartItems$.pipe( switchMap((displayOrders) => { if (displayOrders.length === 0) { return NEVER; @@ -133,14 +147,56 @@ export class CheckoutReviewComponent implements OnInit { switchMap((processId) => this.domainCheckoutService.getCustomerFeatures({ processId })) ); - showNotificationChannels$ = this.items$.pipe( + showBillingAddress$ = this.shoppingCartItems$.pipe( takeUntil(this._orderCompleted), - map((items) => items.some((item) => item.features?.orderType === 'Rücklage' || item.features?.orderType === 'Abholung')) + withLatestFrom(this.customerFeatures$), + map( + ([items, customerFeatures]) => + items.some( + (item) => + item.features?.orderType === 'Versand' || + item.features?.orderType === 'B2B-Versand' || + item.features?.orderType === 'DIG-Versand' + ) || !!customerFeatures?.b2b + ) ); - showQuantityControlSpinnerItemId: number; + showNotificationChannels$ = combineLatest([this.shoppingCartItems$, this.payer$]).pipe( + takeUntil(this._orderCompleted), + map( + ([items, payer]) => + !!payer && items.some((item) => item.features?.orderType === 'Rücklage' || item.features?.orderType === 'Abholung') + ) + ); + + quantityError$ = new BehaviorSubject<{ [key: string]: string }>({}); + + primaryCtaLabel$ = combineLatest([this.payer$, this.shoppingCartItemsWithoutOrderType$]).pipe( + map(([payer, shoppingCartItemsWithoutOrderType]) => { + if (shoppingCartItemsWithoutOrderType?.length > 0) { + return 'Kaufoptionen'; + } + if (!payer) { + return 'Weiter'; + } + return 'Bestellen'; + }) + ); + + primaryCtaDisabled$ = this.quantityError$.pipe( + map((quantityError) => { + for (const key in quantityError) { + if (Object.prototype.hasOwnProperty.call(quantityError, key)) { + return true; + } + } + return this.showOrderButtonSpinner; + }) + ); + + loadingOnItemChangeById$ = new Subject(); + loadingOnQuantityChangeById$ = new Subject(); showOrderButtonSpinner: boolean; - showChangeButtonSpinnerItemId: number; constructor( private domainCheckoutService: DomainCheckoutService, @@ -153,13 +209,54 @@ export class CheckoutReviewComponent implements OnInit { private domainCatalogService: DomainCatalogService, private breadcrumb: BreadcrumbService, private domainPrinterService: DomainPrinterService - ) {} + ) { + super({ + shoppingCart: undefined, + shoppingCartItems: [], + }); + } async ngOnInit() { + this.applicationService.activatedProcessId$.pipe(takeUntil(this._orderCompleted)).subscribe((_) => { + this.loadShoppingCart(); + }); + await this.removeBreadcrumbs(); await this.updateBreadcrumb(); } + loadShoppingCart = this.effect(($) => + $.pipe( + withLatestFrom(this.applicationService.activatedProcessId$), + switchMap(([_, processId]) => { + return this.domainCheckoutService.getShoppingCart({ processId, latest: true }).pipe( + tapResponse( + (shoppingCart) => { + const shoppingCartItems = shoppingCart?.items?.map((item) => item.data) || []; + this.patchState({ + shoppingCart, + shoppingCartItems, + }); + this.checkQuantityErrors(shoppingCartItems); + }, + (err) => {}, + () => {} + ) + ); + }) + ) + ); + + checkQuantityErrors(shoppingCartItems: ShoppingCartItemDTO[]) { + shoppingCartItems.forEach((item) => { + if (item.features?.orderType === 'Rücklage') { + this.setQuantityError(item, item.availability, item.quantity > item.availability?.inStock); + } else { + this.setQuantityError(item, item.availability, false); + } + }); + } + async updateBreadcrumb() { await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({ key: this.applicationService.activatedProcessId, @@ -187,7 +284,7 @@ export class CheckoutReviewComponent implements OnInit { }); } - changeDummyItem(shoppingCartItem: ShoppingCartItemDTO) { + changeDummyItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) { const data = { price: shoppingCartItem?.availability?.price?.value?.value, vat: shoppingCartItem?.availability?.price?.vat?.vatType, @@ -202,8 +299,8 @@ export class CheckoutReviewComponent implements OnInit { this.openDummyModal(data); } - async changeItem(shoppingCartItem: ShoppingCartItemDTO) { - this.showChangeButtonSpinnerItemId = shoppingCartItem.id; + async changeItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) { + this.loadingOnItemChangeById$.next(shoppingCartItem.id); const quantity = shoppingCartItem.quantity; @@ -329,11 +426,11 @@ export class CheckoutReviewComponent implements OnInit { } } - this.showChangeButtonSpinnerItemId = undefined; + this.loadingOnItemChangeById$.next(undefined); this.cdr.markForCheck(); const itemId = Number(shoppingCartItem.product.catalogProductNumber); - this.uiModal.open({ + const modal = this.uiModal.open({ content: PurchasingOptionsModalComponent, data: { availableOptions, @@ -353,6 +450,10 @@ export class CheckoutReviewComponent implements OnInit { availabilities, } as PurchasingOptionsModalData, }); + + modal.afterClosed$.pipe(takeUntil(this._orderCompleted)).subscribe(() => { + this.setQuantityError(shoppingCartItem, undefined, false); + }); } setAgentComment(agentComment: string) { @@ -374,8 +475,12 @@ export class CheckoutReviewComponent implements OnInit { }); } - async updateItemQuantity(shoppingCartItem: ShoppingCartItemDTO, quantity: number) { - this.showQuantityControlSpinnerItemId = shoppingCartItem.id; + async updateItemQuantity({ shoppingCartItem, quantity }: { shoppingCartItem: ShoppingCartItemDTO; quantity: number }) { + if (quantity === shoppingCartItem.quantity) { + return; + } + + this.loadingOnQuantityChangeById$.next(shoppingCartItem.id); const shoppingCartItemPrice = shoppingCartItem?.availability?.price?.value?.value; const orderType = shoppingCartItem?.features?.orderType; @@ -397,6 +502,8 @@ export class CheckoutReviewComponent implements OnInit { quantity, }) .toPromise(); + + this.setQuantityError(shoppingCartItem, availability, availability?.inStock < quantity); break; case 'Abholung': availability = await this.availabilityService @@ -473,6 +580,7 @@ export class CheckoutReviewComponent implements OnInit { }, }) .toPromise(); + this.setQuantityError(shoppingCartItem, availability, false); } else if (availability) { await this.domainCheckoutService .updateItemInShoppingCart({ @@ -485,9 +593,29 @@ export class CheckoutReviewComponent implements OnInit { }) .toPromise(); } else { - // TODO: Set Prev Quantity + await this.domainCheckoutService + .updateItemInShoppingCart({ + processId: this.applicationService.activatedProcessId, + shoppingCartItemId: shoppingCartItem.id, + update: { + quantity, + }, + }) + .toPromise(); + } + + this.loadingOnQuantityChangeById$.next(undefined); + } + + setQuantityError(item: ShoppingCartItemDTO, availability: AvailabilityDTO, error: boolean) { + const quantityErrors: { [key: string]: string } = this.quantityError$.value; + if (error) { + quantityErrors[item.product.catalogProductNumber] = `${availability.inStock} Exemplar(e) sofort lieferbar`; + this.quantityError$.next({ ...quantityErrors }); + } else { + delete quantityErrors[item.product.catalogProductNumber]; + this.quantityError$.next({ ...quantityErrors }); } - this.showQuantityControlSpinnerItemId = undefined; } // Bei unbekannten Kunden und DIG Bestellung findet ein Vergleich der Preise statt @@ -534,7 +662,28 @@ export class CheckoutReviewComponent implements OnInit { } } + async showPurchasingListModal(shoppingCartItems: ShoppingCartItemDTO[]) { + const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise(); + this.uiModal.open({ + content: PurchasingOptionsListModalComponent, + title: 'Wie möchten Sie die Artikel erhalten?', + config: { showScrollbarY: false }, + data: { + processId: this.applicationService.activatedProcessId, + shoppingCartItems: shoppingCartItems, + customerFeatures, + } as PurchasingOptionsListModalData, + }); + } + async order() { + const shoppingCartItemsWithoutOrderType = await this.shoppingCartItemsWithoutOrderType$.pipe(first()).toPromise(); + + if (shoppingCartItemsWithoutOrderType?.length > 0) { + this.showPurchasingListModal(shoppingCartItemsWithoutOrderType); + return; + } + const processId = this.applicationService.activatedProcessId; const customer = await this.domainCheckoutService.getBuyer({ processId }).pipe(first()).toPromise(); if (!customer) { diff --git a/apps/page/checkout/src/lib/checkout-review/checkout-review.module.ts b/apps/page/checkout/src/lib/checkout-review/checkout-review.module.ts index 45d8eac4b..0ed131307 100644 --- a/apps/page/checkout/src/lib/checkout-review/checkout-review.module.ts +++ b/apps/page/checkout/src/lib/checkout-review/checkout-review.module.ts @@ -17,10 +17,13 @@ import { NotificationEditComponent } from './notification-channels/notification- import { UiCheckboxModule } from '@ui/checkbox'; import { SpecialCommentComponent } from './special-comment/special-comment.component'; import { UiQuantityDropdownModule } from '@ui/quantity-dropdown'; +import { UiCommonModule } from '@ui/common'; +import { ShoppingCartItemComponent } from './shopping-cart-item/shopping-cart-item.component'; @NgModule({ imports: [ CommonModule, + UiCommonModule, RouterModule, PageCheckoutPipeModule, UiIconModule, @@ -41,6 +44,7 @@ import { UiQuantityDropdownModule } from '@ui/quantity-dropdown'; NotificationEditComponent, NotificationCheckboxComponent, SpecialCommentComponent, + ShoppingCartItemComponent, ], }) export class CheckoutReviewModule {} diff --git a/apps/page/checkout/src/lib/checkout-review/shopping-cart-item/shopping-cart-item.component.html b/apps/page/checkout/src/lib/checkout-review/shopping-cart-item/shopping-cart-item.component.html new file mode 100644 index 000000000..47a7f68bd --- /dev/null +++ b/apps/page/checkout/src/lib/checkout-review/shopping-cart-item/shopping-cart-item.component.html @@ -0,0 +1,74 @@ +
+ +
+ +
+ {{ item.product.contributors }} +
+ +
+ {{ item?.product?.name }} +
+ +
+ + {{ item?.product?.formatDetail }} +
+ +
+ {{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }}
+ {{ item?.product?.volume }} | + {{ item?.product?.publicationDate | date }} +
+ +
+
{{ item?.availability?.price?.value?.value | currency: 'EUR':'code' }}
+
+ + + + {{ item?.quantity }} + +
+
+ {{ quantityError }} +
+
+ +
+ + +
diff --git a/apps/page/checkout/src/lib/checkout-review/shopping-cart-item/shopping-cart-item.component.scss b/apps/page/checkout/src/lib/checkout-review/shopping-cart-item/shopping-cart-item.component.scss new file mode 100644 index 000000000..5302c1b46 --- /dev/null +++ b/apps/page/checkout/src/lib/checkout-review/shopping-cart-item/shopping-cart-item.component.scss @@ -0,0 +1,139 @@ +:host { + @apply text-black no-underline grid p-4; + grid-template-columns: 102px 50% auto; + grid-template-rows: auto; + grid-template-areas: + 'item-thumbnail item-contributors item-contributors' + 'item-thumbnail item-title item-price-stock' + 'item-thumbnail item-format item-price-stock' + 'item-thumbnail item-info actions' + 'item-thumbnail item-date actions' + 'item-thumbnail item-ssc actions' + 'item-thumbnail item-availability actions'; +} + +.item-thumbnail { + grid-area: item-thumbnail; + width: 70px; + @apply mr-8; + img { + max-width: 100%; + max-height: 150px; + @apply rounded-card shadow-cta; + } +} + +.item-contributors { + @apply text-active-customer font-bold no-underline; + grid-area: item-contributors; + height: 22px; + text-overflow: ellipsis; + overflow: hidden; + max-width: 600px; + white-space: nowrap; +} + +.item-title { + grid-area: item-title; + @apply font-bold text-lg mb-4; + max-height: 64px; +} + +.item-title.xl { + @apply font-bold text-xl; +} + +.item-title.lg { + @apply font-bold text-lg; +} + +.item-title.md { + @apply font-bold text-base; +} + +.item-title.sm { + @apply font-bold text-sm; +} + +.item-title.xs { + @apply font-bold text-xs; +} + +.item-format { + grid-area: item-format; + @apply flex flex-row items-center font-bold text-lg whitespace-nowrap; + + img { + @apply mr-2; + } +} + +.item-price-stock { + grid-area: item-price-stock; + @apply font-bold text-xl text-right; + + .quantity { + @apply flex flex-row justify-end items-center; + } + + .quantity-error { + @apply text-dark-goldenrod font-bold text-sm whitespace-nowrap; + } + + ui-quantity-dropdown { + @apply flex justify-end mt-2; + } +} + +.item-stock { + grid-area: item-stock; + @apply flex flex-row justify-end items-baseline font-bold text-lg; + + ui-icon { + @apply text-active-customer mr-2; + } +} + +.item-info { + grid-area: item-info; +} + +.item-availability { + @apply flex flex-row items-center mt-4; + grid-area: item-availability; + + .fetching { + @apply w-52 h-px-20; + background-color: #e6eff9; + animation: load 0.75s linear infinite; + } + + span { + @apply mr-4; + } + + ui-icon { + @apply text-dark-cerulean mx-1; + } + + div { + @apply ml-2 flex items-center; + } + + .truck { + @apply -mb-px-5 -mt-px-5; + } +} + +.actions { + @apply flex items-center justify-end; + grid-area: actions; + + button { + @apply bg-transparent text-brand font-bold text-xl outline-none border-none; + + &:disabled { + @apply text-disabled-customer; + } + } +} diff --git a/apps/page/checkout/src/lib/checkout-review/shopping-cart-item/shopping-cart-item.component.ts b/apps/page/checkout/src/lib/checkout-review/shopping-cart-item/shopping-cart-item.component.ts new file mode 100644 index 000000000..2fc0fd216 --- /dev/null +++ b/apps/page/checkout/src/lib/checkout-review/shopping-cart-item/shopping-cart-item.component.ts @@ -0,0 +1,94 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { ComponentStore } from '@ngrx/component-store'; +import { ShoppingCartItemDTO } from '@swagger/checkout'; +import { first, map, shareReplay } from 'rxjs/operators'; + +export interface ShoppingCartItemComponentState { + item: ShoppingCartItemDTO; + orderType: string; + loadingOnItemChangeById?: number; + loadingOnQuantityChangeById?: number; +} + +@Component({ + selector: 'page-shopping-cart-item', + templateUrl: 'shopping-cart-item.component.html', + styleUrls: ['shopping-cart-item.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ShoppingCartItemComponent extends ComponentStore implements OnInit { + @Output() changeItem = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO }>(); + @Output() changeDummyItem = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO }>(); + @Output() changeQuantity = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO; quantity: number }>(); + + @Input() + get item() { + return this.get((s) => s.item); + } + set item(item: ShoppingCartItemDTO) { + if (this.item !== item) { + this.patchState({ item }); + } + } + readonly item$ = this.select((s) => s.item); + + @Input() + get orderType() { + return this.get((s) => s.orderType); + } + set orderType(orderType: string) { + if (this.orderType !== orderType) { + this.patchState({ orderType }); + } + } + readonly orderType$ = this.select((s) => s.orderType); + + @Input() + get loadingOnItemChangeById() { + return this.get((s) => s.loadingOnItemChangeById); + } + set loadingOnItemChangeById(loadingOnItemChangeById: number) { + if (this.loadingOnItemChangeById !== loadingOnItemChangeById) { + this.patchState({ loadingOnItemChangeById }); + } + } + readonly loadingOnItemChangeById$ = this.select((s) => s.loadingOnItemChangeById).pipe(shareReplay()); + + @Input() + get loadingOnQuantityChangeById() { + return this.get((s) => s.loadingOnQuantityChangeById); + } + set loadingOnQuantityChangeById(loadingOnQuantityChangeById: number) { + if (this.loadingOnQuantityChangeById !== loadingOnQuantityChangeById) { + this.patchState({ loadingOnQuantityChangeById }); + } + } + readonly loadingOnQuantityChangeById$ = this.select((s) => s.loadingOnQuantityChangeById).pipe(shareReplay()); + + @Input() + quantityError: string; + + isDummy$ = this.item$.pipe( + map((item) => item?.availability?.supplyChannel === 'MANUALLY'), + shareReplay() + ); + hasOrderType$ = this.orderType$.pipe( + map((orderType) => orderType !== undefined), + shareReplay() + ); + + constructor() { + super({ item: undefined, orderType: '' }); + } + + ngOnInit() {} + + async onChangeItem() { + const isDummy = await this.isDummy$.pipe(first()).toPromise(); + isDummy ? this.changeDummyItem.emit({ shoppingCartItem: this.item }) : this.changeItem.emit({ shoppingCartItem: this.item }); + } + + onChangeQuantity(quantity: number) { + this.changeQuantity.emit({ shoppingCartItem: this.item, quantity }); + } +} diff --git a/apps/page/checkout/src/lib/checkout-review/special-comment/special-comment.component.html b/apps/page/checkout/src/lib/checkout-review/special-comment/special-comment.component.html index 0fe4b9709..cee0c5d49 100644 --- a/apps/page/checkout/src/lib/checkout-review/special-comment/special-comment.component.html +++ b/apps/page/checkout/src/lib/checkout-review/special-comment/special-comment.component.html @@ -1,9 +1,17 @@ - +
-
diff --git a/apps/page/checkout/src/lib/checkout-summary/checkout-summary.component.html b/apps/page/checkout/src/lib/checkout-summary/checkout-summary.component.html index 88074c2f9..ba1148085 100644 --- a/apps/page/checkout/src/lib/checkout-summary/checkout-summary.component.html +++ b/apps/page/checkout/src/lib/checkout-summary/checkout-summary.component.html @@ -82,7 +82,15 @@ {{ (order?.subsetItems)[0].supplierLabel }} | - Versanddatum {{ (order?.subsetItems)[0]?.estimatedShippingDate | date }} + + Versanddatum {{ (order?.subsetItems)[0]?.estimatedShippingDate | date }} + + + Zustellung zwischen {{ ((order?.subsetItems)[0]?.estimatedDelivery?.start | date: 'EEE, dd.MM')?.replace('.', '') }} und + {{ ((order?.subsetItems)[0]?.estimatedDelivery?.stop | date: 'EEE, dd.MM')?.replace('.', '') }} + diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option-b2b/delivery-b2b-option-list.component.html b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option-b2b/delivery-b2b-option-list.component.html new file mode 100644 index 000000000..a97f83972 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option-b2b/delivery-b2b-option-list.component.html @@ -0,0 +1,13 @@ +
+ +
+ +

Möchten Sie die Artikel
geliefert bekommen?

+

Versandkostenfrei

diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option-b2b/delivery-b2b-option-list.component.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option-b2b/delivery-b2b-option-list.component.ts new file mode 100644 index 000000000..7ecbdf73b --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option-b2b/delivery-b2b-option-list.component.ts @@ -0,0 +1,23 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store'; + +@Component({ + selector: 'page-delivery-b2b-option-list', + templateUrl: 'delivery-b2b-option-list.component.html', + styleUrls: ['../list-options.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DeliveryB2bOptionListComponent { + selectedOption$ = this._store.selectedFilterOption$; + optionChipDisabled$ = this._store.fetchingAvailabilities$; + + constructor(private _store: PurchasingOptionsListModalStore) {} + + optionChange(option: string) { + if (this._store.selectedFilterOption === option) { + this._store.selectedFilterOption = undefined; + } else { + this._store.selectedFilterOption = option; + } + } +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option-b2b/index.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option-b2b/index.ts new file mode 100644 index 000000000..1b25ac6f5 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option-b2b/index.ts @@ -0,0 +1,3 @@ +// start:ng42.barrel +export * from './delivery-b2b-option-list.component'; +// end:ng42.barrel diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option/delivery-option-list.component.html b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option/delivery-option-list.component.html new file mode 100644 index 000000000..06ab683cc --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option/delivery-option-list.component.html @@ -0,0 +1,13 @@ +
+ +
+ +

Möchten Sie die Artikel
geliefert bekommen?

+

Versandkostenfrei

diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option/delivery-option-list.component.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option/delivery-option-list.component.ts new file mode 100644 index 000000000..871659b82 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option/delivery-option-list.component.ts @@ -0,0 +1,23 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store'; + +@Component({ + selector: 'page-delivery-option-list', + templateUrl: 'delivery-option-list.component.html', + styleUrls: ['../list-options.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DeliveryOptionListComponent { + selectedOption$ = this._store.selectedFilterOption$; + optionChipDisabled$ = this._store.fetchingAvailabilities$; + + constructor(private _store: PurchasingOptionsListModalStore) {} + + optionChange(option: string) { + if (this._store.selectedFilterOption === option) { + this._store.selectedFilterOption = undefined; + } else { + this._store.selectedFilterOption = option; + } + } +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option/index.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option/index.ts new file mode 100644 index 000000000..6e41cd327 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/delivery-option/index.ts @@ -0,0 +1,3 @@ +// start:ng42.barrel +export * from './delivery-option-list.component'; +// end:ng42.barrel diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/index.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/index.ts new file mode 100644 index 000000000..306f73a2a --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/index.ts @@ -0,0 +1,7 @@ +// start:ng42.barrel +export * from './delivery-option'; +export * from './pick-up-option'; +export * from './take-away-option'; +export * from './purchasing-options-list-modal.component'; +export * from './purchasing-options-list-modal.module'; +// end:ng42.barrel diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/list-options.scss b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/list-options.scss new file mode 100644 index 000000000..22aebfe6c --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/list-options.scss @@ -0,0 +1,35 @@ +:host { + @apply block w-72; +} + +.option-icon { + @apply text-ucla-blue mx-auto; + width: 40px; + + .truck-b2b { + margin-top: -21px; + margin-bottom: -12px; + width: 70px; + } +} + +.option-chip { + @apply rounded-full text-base px-4 py-3 bg-glitter text-inactive-customer border-none font-bold; + + &.selected { + @apply bg-active-customer text-white; + } +} + +.option-description { + @apply my-2; +} + +.option-select { + @apply mt-4 mb-4 border-2 border-solid border-brand text-brand text-cta-l font-bold bg-white rounded-full py-3 px-6; +} + +::ng-deep page-purchasing-options-list-modal ui-branch-dropdown .wrapper { + @apply mx-auto; + width: 80%; +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/pick-up-option/index.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/pick-up-option/index.ts new file mode 100644 index 000000000..10f4eeaa1 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/pick-up-option/index.ts @@ -0,0 +1,3 @@ +// start:ng42.barrel +export * from './pick-up-option-list.component'; +// end:ng42.barrel diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/pick-up-option/pick-up-option-list.component.html b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/pick-up-option/pick-up-option-list.component.html new file mode 100644 index 000000000..67d6633f5 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/pick-up-option/pick-up-option-list.component.html @@ -0,0 +1,18 @@ +
+ +
+ +

Möchten Sie die Artikel
in einer unserer Filialen
abholen?

+ + diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/pick-up-option/pick-up-option-list.component.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/pick-up-option/pick-up-option-list.component.ts new file mode 100644 index 000000000..d8e235e0d --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/pick-up-option/pick-up-option-list.component.ts @@ -0,0 +1,34 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { BranchDTO } from '@swagger/checkout'; +import { first } from 'rxjs/operators'; +import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store'; + +@Component({ + selector: 'page-pick-up-option-list', + templateUrl: 'pick-up-option-list.component.html', + styleUrls: ['../list-options.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PickUpOptionListComponent { + branches$ = this._store.branches$; + selectedBranch$ = this._store.selectedPickUpBranch$; + selectedOption$ = this._store.selectedFilterOption$; + optionChipDisabled$ = this._store.fetchingAvailabilities$; + + constructor(private _store: PurchasingOptionsListModalStore) {} + + optionChange(option: string) { + if (this._store.selectedFilterOption === option) { + this._store.selectedFilterOption = undefined; + } else { + this._store.selectedFilterOption = option; + } + } + + async selectBranch(branch: BranchDTO) { + this._store.selectedPickUpBranch = branch; + + const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise(); + shoppingCartItems.forEach((item) => this._store.loadPickUpAvailability({ item })); + } +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-item/purchasing-options-list-item.component.html b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-item/purchasing-options-list-item.component.html new file mode 100644 index 000000000..e27f88b92 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-item/purchasing-options-list-item.component.html @@ -0,0 +1,109 @@ +
+ +
+ +
+ {{ item.product.contributors }} +
+ +
+ {{ item?.product?.name }} +
+ +
+ + {{ item?.product?.formatDetail }} +
+ +
+ {{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }}
+ {{ item?.product?.volume }} | + {{ item?.product?.publicationDate | date: 'dd. MMMM yyyy' }} +
+ +
+
+ + + + Günstigerer Preis aus Hugendubel Katalog wird übernommen + + + +
{{ price?.value?.value | currency: price?.value?.currency:'code' }}
+
+ +
+ + +
+
+ +
+ +
+ +
+
+ + + Derzeit nicht bestellbar + + + + Verfügbar als +
+ + + + {{ deliveryDigAvailabilities.estimatedShippingDate | date: 'dd. MMMM yyyy' }} + + + {{ (deliveryDigAvailabilities?.estimatedDelivery?.start | date: 'EEE, dd.MM')?.replace('.', '') }} - + {{ (deliveryDigAvailabilities?.estimatedDelivery?.stop | date: 'EEE, dd.MM')?.replace('.', '') }} + + +
+ + +
+ + {{ (deliveryB2bAvailabilities$ | async)?.estimatedShippingDate | date: 'dd. MMMM yyyy' }} +
+
+ +
+ + {{ (pickUpAvailabilities$ | async)?.estimatedShippingDate | date: 'dd. MMMM yyyy' }} +
+ +
+ + ab sofort +
+
+
+
+ + +
+ {{ canAdd }} +
+
diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-item/purchasing-options-list-item.component.scss b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-item/purchasing-options-list-item.component.scss new file mode 100644 index 000000000..7b5adcf1e --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-item/purchasing-options-list-item.component.scss @@ -0,0 +1,166 @@ +:host { + @apply text-black no-underline grid py-4; + grid-template-columns: 102px 70% auto; + grid-template-rows: auto; + grid-template-areas: + 'item-thumbnail item-contributors item-contributors' + 'item-thumbnail item-title item-price-stock' + 'item-thumbnail item-format item-price-stock' + 'item-thumbnail item-info item-select' + 'item-thumbnail item-date item-select' + 'item-thumbnail item-ssc item-select' + 'item-thumbnail item-availability item-select' + 'item-thumbnail item-can-add item-can-add'; +} + +.item-thumbnail { + grid-area: item-thumbnail; + width: 70px; + @apply mr-8; + img { + max-width: 100%; + max-height: 150px; + @apply rounded-card shadow-cta; + } +} + +.item-contributors { + @apply text-active-customer font-bold no-underline; + grid-area: item-contributors; + height: 22px; + text-overflow: ellipsis; + overflow: hidden; + max-width: 600px; + white-space: nowrap; +} + +.item-title { + grid-area: item-title; + @apply font-bold text-lg mb-4; + max-height: 64px; +} + +.item-title.xl { + @apply font-bold text-xl; +} + +.item-title.lg { + @apply font-bold text-lg; +} + +.item-title.md { + @apply font-bold text-base; +} + +.item-title.sm { + @apply font-bold text-sm; +} + +.item-title.xs { + @apply font-bold text-xs; +} + +.item-format { + grid-area: item-format; + @apply flex flex-row items-center font-bold text-lg whitespace-nowrap; + + img { + @apply mr-2; + } +} + +.item-price-stock { + grid-area: item-price-stock; + @apply font-bold text-xl text-right; + + .price { + @apply flex flex-row justify-end items-center; + } + + .info-tooltip-button { + @apply border-font-customer bg-white rounded-full text-base font-bold mr-3; + border-style: outset; + width: 31px; + height: 31px; + margin-left: 10px; + } + + .quantity-btn { + @apply flex flex-row items-center p-0 w-full text-right outline-none border-none bg-transparent text-lg; + } + + .quantity-btn-icon { + @apply inline-flex ml-2; + transition: transform 200ms ease-in-out; + } + + ui-quantity-dropdown { + @apply flex justify-end mt-2; + + &.disabled { + @apply cursor-not-allowed bg-inactive-branch; + } + } +} + +.item-stock { + grid-area: item-stock; + @apply flex flex-row justify-end items-baseline font-bold text-lg; + + ui-icon { + @apply text-active-customer mr-2; + } +} + +.item-info { + grid-area: item-info; +} + +.item-availability { + @apply flex flex-row items-center mt-4; + grid-area: item-availability; + + .fetching { + @apply w-52 h-px-20; + background-color: #e6eff9; + animation: load 0.75s linear infinite; + } + + span { + @apply mr-4; + } + + ui-icon { + @apply text-dark-cerulean mx-2; + } + + div { + @apply mr-4 flex items-center; + } + + .truck { + @apply -mb-px-5 -mt-px-5; + } + + .truck-b2b { + @apply -mb-px-10 -mt-px-10; + } +} + +.item-can-add { + @apply text-xl text-dark-goldenrod mt-2; + grid-area: item-can-add; +} + +.item-select { + @apply flex items-center justify-end; + grid-area: item-select; + + ui-select-bullet { + @apply cursor-pointer; + + &.disabled { + @apply cursor-not-allowed; + } + } +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-item/purchasing-options-list-item.component.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-item/purchasing-options-list-item.component.ts new file mode 100644 index 000000000..0a7cb35f1 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-item/purchasing-options-list-item.component.ts @@ -0,0 +1,147 @@ +import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; +import { DomainAvailabilityService } from '@domain/availability'; +import { AvailabilityDTO, ShoppingCartItemDTO } from '@swagger/checkout'; +import { combineLatest } from 'rxjs'; +import { filter, map, shareReplay, withLatestFrom } from 'rxjs/operators'; +import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store'; + +@Component({ + selector: 'page-purchasing-options-list-item', + templateUrl: 'purchasing-options-list-item.component.html', + styleUrls: ['purchasing-options-list-item.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PurchasingOptionsListItemComponent { + @Input() + item: ShoppingCartItemDTO; + + isSelected$ = this._store.selectedShoppingCartItems$.pipe( + map((selectedShoppingCartItems) => !!selectedShoppingCartItems?.find((item) => item.id === this.item.id)) + ); + + fetchingAvailabilities$ = combineLatest([ + this._store.takeAwayAvailabilities$, + this._store.pickUpAvailabilities$, + this._store.deliveryAvailabilities$, + ]).pipe( + map( + ([takeAway, pickUp, delivery]) => + !takeAway || + takeAway[this.item.product.catalogProductNumber] === true || + !pickUp || + pickUp[this.item.product.catalogProductNumber] === true || + !delivery || + delivery[this.item.product.catalogProductNumber] === true + ) + ); + + takeAwayAvailabilities$ = this._store.takeAwayAvailabilities$.pipe( + map((takeAwayAvailabilities) => (!!takeAwayAvailabilities ? takeAwayAvailabilities[this.item.product?.catalogProductNumber] : [])), + shareReplay() + ); + + pickUpAvailabilities$ = this._store.pickUpAvailabilities$.pipe( + map((pickUpAvailabilities) => (!!pickUpAvailabilities ? pickUpAvailabilities[this.item.product?.catalogProductNumber] : [])), + shareReplay() + ); + + deliveryAvailabilities$ = this._store.deliveryAvailabilities$.pipe( + map((shippingAvailabilities) => (!!shippingAvailabilities ? shippingAvailabilities[this.item.product?.catalogProductNumber] : [])), + shareReplay() + ); + + deliveryDigAvailabilities$ = this._store.deliveryDigAvailabilities$.pipe( + map((shippingAvailabilities) => (!!shippingAvailabilities ? shippingAvailabilities[this.item.product?.catalogProductNumber] : [])), + shareReplay() + ); + + deliveryB2bAvailabilities$ = this._store.deliveryB2bAvailabilities$.pipe( + map((shippingAvailabilities) => (!!shippingAvailabilities ? shippingAvailabilities[this.item.product?.catalogProductNumber] : [])), + shareReplay() + ); + + notAvailable$ = combineLatest([ + this.fetchingAvailabilities$, + this.takeAwayAvailabilities$, + this.pickUpAvailabilities$, + this.deliveryAvailabilities$, + this.deliveryDigAvailabilities$, + this.deliveryB2bAvailabilities$, + ]).pipe( + map( + ([fetching, takeAway, store, delivery, deliveryDig, deliveryB2b]) => + !fetching && !takeAway && !store && !delivery && !deliveryDig && !deliveryB2b + ) + ); + + showTooltip$ = this._store.selectedFilterOption$.pipe( + withLatestFrom(this.deliveryAvailabilities$, this.deliveryDigAvailabilities$), + map(([option, delivery, deliveryDig]) => { + if (option === 'delivery') { + const deliveryAvailability = (deliveryDig as AvailabilityDTO) || (delivery as AvailabilityDTO); + + const shippingPrice = deliveryAvailability?.price?.value?.value; + const catalogPrice = this.item?.availability?.price?.value?.value; + return catalogPrice < shippingPrice; + } + return false; + }) + ); + + price$ = this._store.selectedFilterOption$.pipe( + withLatestFrom( + this.takeAwayAvailabilities$, + this.pickUpAvailabilities$, + this.deliveryAvailabilities$, + this.deliveryDigAvailabilities$, + this.deliveryB2bAvailabilities$ + ), + map(([option, takeAway, pickUp, delivery, deliveryDig, deliveryB2b]) => { + let availability; + switch (option) { + case 'take-away': + availability = takeAway; + break; + case 'pick-up': + availability = pickUp; + break; + case 'delivery': + availability = deliveryDig || delivery; + break; + case 'b2b-delivery': + availability = deliveryB2b; + break; + default: + return this.item.availability?.price; + } + + return this._availabilityService.getPriceForAvailability(option, this.item.availability, availability); + }) + ); + + selectDisabled$ = this._store.selectedFilterOption$.pipe(map((selectedFilterOption) => !selectedFilterOption)); + + canAdd$ = this._store.canAdd$.pipe( + filter((canAdd) => !!this.item && !!canAdd), + map((canAdd) => canAdd[this.item.product.catalogProductNumber]) + ); + + constructor(private _store: PurchasingOptionsListModalStore, private _availabilityService: DomainAvailabilityService) {} + + selected(value: boolean) { + this._store.selectShoppingCartItem([this.item], value); + + if (value) { + this._store.checkCanAddItem(this.item); + } + } + + changeQuantity(quantity: number) { + if (quantity === 0) { + this._store.removeShoppingCartItem(this.item); + } else { + this._store.updateItemQuantity({ itemId: this.item.id, quantity }); + this._store.loadAvailabilities({ items: [{ ...this.item, quantity }] }); + } + } +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.component.html b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.component.html new file mode 100644 index 000000000..53748fe07 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.component.html @@ -0,0 +1,47 @@ +
+ + + + + + + +
+ +
+
+ + + + + + +
+ {{ (selectedShoppingCartItems$ | async)?.length || 0 }} von {{ shoppingCartItems?.length || 0 }} Artikeln +
+ +
+
+ + +
+
+
+ + +
Keine Artikel für die ausgewählte Kaufoption verfügbar
+
+
+ +
+ +
diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.component.scss b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.component.scss new file mode 100644 index 000000000..b9f63a222 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.component.scss @@ -0,0 +1,49 @@ +:host { + @apply block box-border; +} + +.options { + @apply flex flex-row box-border text-center justify-center mt-4; +} + +.items { + min-height: 480px; + + .item-actions { + @apply text-right; + + .cta-select-all { + @apply text-brand bg-transparent text-base font-bold outline-none border-none px-4 py-4 -mr-4; + + &:disabled { + @apply text-inactive-branch; + } + } + } + + .item-list { + @apply overflow-y-scroll -ml-4; + max-height: calc(100vh - 560px); + width: calc(100% + 2rem); + + page-purchasing-options-list-item { + @apply px-4; + } + } + + .empty-message { + @apply text-inactive-branch my-8 text-center font-bold; + } +} + +.actions { + @apply flex justify-center mt-8; + + .cta-apply { + @apply text-white border-2 border-solid border-brand bg-brand font-bold text-lg px-4 py-2 rounded-full; + + &:disabled { + @apply bg-inactive-branch border-inactive-branch; + } + } +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.component.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.component.ts new file mode 100644 index 000000000..bba759763 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.component.ts @@ -0,0 +1,211 @@ +import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; +import { DomainAvailabilityService } from '@domain/availability'; +import { DomainCheckoutService } from '@domain/checkout'; +import { ShoppingCartItemDTO, UpdateShoppingCartItemDTO } from '@swagger/checkout'; +import { UiModalRef } from '@ui/modal'; +import { combineLatest, Subject } from 'rxjs'; +import { first, map, shareReplay, takeUntil, withLatestFrom } from 'rxjs/operators'; +import { PurchasingOptionsListModalData } from './purchasing-options-list-modal.data'; +import { PurchasingOptionsListModalStore } from './purchasing-options-list-modal.store'; + +@Component({ + selector: 'page-purchasing-options-list-modal', + templateUrl: 'purchasing-options-list-modal.component.html', + styleUrls: ['purchasing-options-list-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [PurchasingOptionsListModalStore], +}) +export class PurchasingOptionsListModalComponent implements OnInit { + private _onDestroy$ = new Subject(); + + shoppingCartItems$ = combineLatest([ + this._store.fetchingAvailabilities$, + this._store.selectedFilterOption$, + this._store.shoppingCartItems$, + ]).pipe( + withLatestFrom( + this._store.takeAwayAvailabilities$, + this._store.pickUpAvailabilities$, + this._store.deliveryAvailabilities$, + this._store.deliveryDigAvailabilities$, + this._store.deliveryB2bAvailabilities$ + ), + map( + ([ + [_, selectedFilterOption, shoppingCartItems], + takeAwayAvailability, + pickUpAvailability, + deliveryAvailability, + deliveryDigAvailability, + deliveryB2bAvailability, + ]) => { + if (!!takeAwayAvailability && !!pickUpAvailability && !!deliveryAvailability) { + switch (selectedFilterOption) { + case 'take-away': + return shoppingCartItems.filter((item) => !!takeAwayAvailability[item.product?.catalogProductNumber]); + case 'pick-up': + return shoppingCartItems.filter((item) => !!pickUpAvailability[item.product?.catalogProductNumber]); + case 'delivery': + return shoppingCartItems.filter( + (item) => + !!deliveryAvailability[item.product?.catalogProductNumber] || + !!deliveryDigAvailability[item.product?.catalogProductNumber] || + !!deliveryB2bAvailability[item.product?.catalogProductNumber] + ); + } + } + return shoppingCartItems; + } + ), + shareReplay() + ); + + b2bCustomer$ = this._store.customerFeatures$.pipe(map((customerFeatures) => customerFeatures && customerFeatures['b2b'])); + + selectedShoppingCartItems$ = this._store.selectedShoppingCartItems$; + + allShoppingCartItemsSelected$ = combineLatest([this.shoppingCartItems$, this.selectedShoppingCartItems$]).pipe( + map(([shoppingCartItems, selectedShoppingCartItems]) => + shoppingCartItems.every((item) => selectedShoppingCartItems.find((i) => item.id === i.id)) + ) + ); + + selectAllCtaDisabled$ = this._store.selectedFilterOption$.pipe( + withLatestFrom(this.shoppingCartItems$), + map(([selectedFilterOption, items]) => !selectedFilterOption || items?.length === 0) + ); + + applyCtaDisabled$ = combineLatest([this._store.selectedFilterOption$, this._store.selectedShoppingCartItems$, this._store.canAdd$]).pipe( + withLatestFrom(this.shoppingCartItems$), + map( + ([[selectedFilterOption, selectedShoppingCartItems, canAdd], shoppingCartItems]) => + !selectedFilterOption || + shoppingCartItems?.length === 0 || + selectedShoppingCartItems?.length === 0 || + !!selectedShoppingCartItems?.find((i) => !!canAdd && canAdd[i.product.catalogProductNumber] !== true) + ) + ); + + constructor( + private _modalRef: UiModalRef, + private _store: PurchasingOptionsListModalStore, + private _availability: DomainAvailabilityService, + private _checkout: DomainCheckoutService + ) { + this._store.shoppingCartItems = _modalRef.data.shoppingCartItems; + this._store.customerFeatures = _modalRef.data.customerFeatures; + this._store.processId = _modalRef.data.processId; + } + + ngOnInit() { + this._store.loadBranches(); + + // Beim Wechsel der ausgewählten Filteroption die Auswahl leeren + this._store.selectedFilterOption$.pipe(takeUntil(this._onDestroy$)).subscribe((_) => { + this._store.clearSelectedShoppingCartItems(); + }); + } + + async selectAll(items: ShoppingCartItemDTO[], value: boolean) { + this._store.selectShoppingCartItem(items, value); + + if (value) { + const selectedItems = await this.selectedShoppingCartItems$.pipe(first()).toPromise(); + selectedItems.forEach((item) => this._store.checkCanAddItem(item)); + } + } + + async apply() { + const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise(); + const items = await this._store.selectedShoppingCartItems$.pipe(first()).toPromise(); + const takeAwayAvailabilities = await this._store.takeAwayAvailabilities$.pipe(first()).toPromise(); + const pickupAvailabilities = await this._store.pickUpAvailabilities$.pipe(first()).toPromise(); + const deliveryAvailabilities = await this._store.deliveryAvailabilities$.pipe(first()).toPromise(); + const deliveryB2bAvailabilities = await this._store.deliveryB2bAvailabilities$.pipe(first()).toPromise(); + const deliveryDigAvailabilities = await this._store.deliveryDigAvailabilities$.pipe(first()).toPromise(); + const selectedTakeAwayBranch = await this._store.selectedTakeAwayBranch$.pipe(first()).toPromise(); + const selectedPickUpBranch = await this._store.selectedPickUpBranch$.pipe(first()).toPromise(); + + for (const item of items) { + let availability; + switch (this._store.selectedFilterOption) { + case 'take-away': + availability = takeAwayAvailabilities[item.product.catalogProductNumber]; + break; + case 'pick-up': + availability = pickupAvailabilities[item.product.catalogProductNumber]; + break; + case 'delivery': + if ( + deliveryDigAvailabilities[item.product.catalogProductNumber] && + deliveryB2bAvailabilities[item.product.catalogProductNumber] && + deliveryAvailabilities[item.product.catalogProductNumber] + ) { + availability = deliveryAvailabilities[item.product.catalogProductNumber]; + } else if (deliveryDigAvailabilities[item.product.catalogProductNumber]) { + availability = deliveryDigAvailabilities[item.product.catalogProductNumber]; + } else if (deliveryB2bAvailabilities[item.product.catalogProductNumber]) { + availability = deliveryB2bAvailabilities[item.product.catalogProductNumber]; + } + break; + case 'b2b-delivery': + availability = deliveryB2bAvailabilities[item.product.catalogProductNumber]; + break; + } + + const price = this._availability.getPriceForAvailability(this._store.selectedFilterOption, item.availability, availability); + + // Negative Preise und nicht vorhandene Availability ignorieren + if (price?.value?.value < 0 || !availability) { + continue; + } + + const updateItem: UpdateShoppingCartItemDTO = { + quantity: item.quantity, + availability: { + ...availability, + price, + }, + promotion: { points: item.promotion.points }, + }; + + switch (this._store.selectedFilterOption) { + case 'take-away': + updateItem.destination = { + data: { target: 1, targetBranch: { id: selectedTakeAwayBranch.id } }, + }; + break; + case 'pick-up': + updateItem.destination = { + data: { target: 1, targetBranch: { id: selectedPickUpBranch.id } }, + }; + break; + case 'delivery': + case 'dig-delivery': + case 'b2b-delivery': + updateItem.destination = { + data: { target: 2, logistician: availability?.logistician }, + }; + break; + } + + await this._checkout + .updateItemInShoppingCart({ + processId: this._modalRef.data.processId, + shoppingCartItemId: item.id, + update: { + ...updateItem, + }, + }) + .toPromise(); + } + const remainingItems = shoppingCartItems.filter((i) => !items.find((j) => i.id === j.id)); + this._store.shoppingCartItems = [...remainingItems]; + + this._store.clearSelectedShoppingCartItems(); + + if (remainingItems?.length === 0) { + this._modalRef.close(); + } + } +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.data.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.data.ts new file mode 100644 index 000000000..5fbbb9c24 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.data.ts @@ -0,0 +1,12 @@ +import { ShoppingCartItemDTO } from '@swagger/checkout'; + +export interface PurchasingOptionsListModalData { + processId: number; + shoppingCartItems?: ShoppingCartItemDTO[]; + customerFeatures: { [key: string]: string }; + // item: ItemDTO; + // availableOptions: PurchasingOptions[]; + // option?: PurchasingOptions; + // shoppingCartItem?: ShoppingCartItemDTO; + // availabilities?: { [key: string]: AvailabilityDTO }; +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.module.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.module.ts new file mode 100644 index 000000000..13fae4ab2 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.module.ts @@ -0,0 +1,41 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { PurchasingOptionsListModalComponent } from './purchasing-options-list-modal.component'; +import { UiIconModule } from '@ui/icon'; +import { ProductImageModule } from '@cdn/product-image'; +import { UiCommonModule } from '@ui/common'; +import { UiSelectBulletModule } from '@ui/select-bullet'; +import { UiQuantityDropdownModule } from '@ui/quantity-dropdown'; +import { PickUpOptionListComponent } from './pick-up-option/pick-up-option-list.component'; +import { TakeAwayOptionListComponent } from './take-away-option/take-away-option-list.component'; +import { DeliveryOptionListComponent } from './delivery-option/delivery-option-list.component'; +import { PurchasingOptionsListItemComponent } from './purchasing-options-list-item/purchasing-options-list-item.component'; +import { FormsModule } from '@angular/forms'; +import { UiBranchDropdownModule } from '@ui/branch-dropdown'; +import { DeliveryB2bOptionListComponent } from './delivery-option-b2b'; +import { UiTooltipModule } from '@ui/tooltip'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + UiCommonModule, + UiIconModule, + UiSelectBulletModule, + UiQuantityDropdownModule, + ProductImageModule, + UiBranchDropdownModule, + UiTooltipModule, + ], + exports: [PurchasingOptionsListModalComponent], + declarations: [ + PurchasingOptionsListModalComponent, + PurchasingOptionsListItemComponent, + PickUpOptionListComponent, + TakeAwayOptionListComponent, + DeliveryOptionListComponent, + DeliveryB2bOptionListComponent, + ], +}) +export class PurchasingOptionsListModalModule {} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.store.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.store.ts new file mode 100644 index 000000000..053c86f2e --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/purchasing-options-list-modal.store.ts @@ -0,0 +1,560 @@ +import { Injectable } from '@angular/core'; +import { ComponentStore, tapResponse } from '@ngrx/component-store'; +import { AvailabilityDTO, BranchDTO, ShoppingCartItemDTO } from '@swagger/checkout'; +import { map, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators'; +import { DomainAvailabilityService } from '@domain/availability'; +import { combineLatest, Observable } from 'rxjs'; +import { DomainCheckoutService } from '@domain/checkout'; + +interface PurchasingOptionsListModalState { + processId: number; + shoppingCartItems: ShoppingCartItemDTO[]; + selectedFilterOption: string; + takeAwayAvailabilities: { [key: string]: AvailabilityDTO | true }; + pickUpAvailabilities: { [key: string]: AvailabilityDTO | true }; + deliveryAvailabilities: { [key: string]: AvailabilityDTO | true }; + deliveryB2bAvailabilities: { [key: string]: AvailabilityDTO | true }; + deliveryDigAvailabilities: { [key: string]: AvailabilityDTO | true }; + customerFeatures: { [key: string]: string }; + canAdd: { [key: string]: true | string }; + selectedShoppingCartItems: ShoppingCartItemDTO[]; + branches: BranchDTO[]; + currentBranch: BranchDTO; + selectedTakeAwayBranch: BranchDTO; + selectedPickUpBranch: BranchDTO; +} + +@Injectable() +export class PurchasingOptionsListModalStore extends ComponentStore { + branches$ = this.select((s) => s.branches); + currentBranch$ = this.select((s) => s.currentBranch); + takeAwayAvailabilities$ = this.select((s) => s.takeAwayAvailabilities); + pickUpAvailabilities$ = this.select((s) => s.pickUpAvailabilities); + deliveryAvailabilities$ = this.select((s) => s.deliveryAvailabilities); + deliveryB2bAvailabilities$ = this.select((s) => s.deliveryB2bAvailabilities); + canAdd$ = this.select((s) => s.canAdd); + deliveryDigAvailabilities$ = this.select((s) => s.deliveryDigAvailabilities); + + shoppingCartItems$ = this.select((s) => s.shoppingCartItems); + + set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) { + shoppingCartItems = shoppingCartItems.sort((a, b) => a.product?.name.localeCompare(b.product.name)); + this.patchState({ shoppingCartItems }); + } + + processId$ = this.select((s) => s.processId); + + set processId(processId: number) { + this.patchState({ processId }); + } + + customerFeatures$ = this.select((s) => s.customerFeatures); + + set customerFeatures(customerFeatures: { [key: string]: string }) { + this.patchState({ customerFeatures }); + } + + selectedFilterOption$ = this.select((s) => s.selectedFilterOption); + + set selectedFilterOption(selectedFilterOption: string) { + this.patchState({ selectedFilterOption, canAdd: {} }); + } + + get selectedFilterOption() { + return this.get((s) => s.selectedFilterOption); + } + + selectedShoppingCartItems$ = this.select((s) => s.selectedShoppingCartItems); + + get selectedShoppingCartItems() { + return this.get((s) => s.selectedShoppingCartItems); + } + + selectedTakeAwayBranch$ = this.select((s) => s.selectedTakeAwayBranch); + + set selectedTakeAwayBranch(selectedTakeAwayBranch: BranchDTO) { + this.patchState({ selectedTakeAwayBranch }); + } + + selectedPickUpBranch$ = this.select((s) => s.selectedPickUpBranch); + + set selectedPickUpBranch(selectedPickUpBranch: BranchDTO) { + this.patchState({ selectedPickUpBranch }); + } + + fetchingAvailabilities$ = combineLatest([this.takeAwayAvailabilities$, this.pickUpAvailabilities$, this.deliveryAvailabilities$]).pipe( + map(([takeAway, pickUp, delivery]) => { + const fetchingCheck = (obj) => { + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const element = obj[key]; + if (typeof element === 'boolean') { + return true; + } + } + } + return false; + }; + return !takeAway || fetchingCheck(takeAway) || !pickUp || fetchingCheck(pickUp) || !delivery || fetchingCheck(delivery); + }) + ); + + constructor(private _availabilityService: DomainAvailabilityService, private _checkoutService: DomainCheckoutService) { + super({ + processId: undefined, + shoppingCartItems: [], + selectedFilterOption: undefined, + pickUpAvailabilities: undefined, + deliveryAvailabilities: undefined, + takeAwayAvailabilities: undefined, + deliveryB2bAvailabilities: undefined, + deliveryDigAvailabilities: undefined, + selectedShoppingCartItems: [], + branches: [], + currentBranch: undefined, + selectedTakeAwayBranch: undefined, + selectedPickUpBranch: undefined, + customerFeatures: undefined, + canAdd: undefined, + }); + } + + loadAvailabilities(options: { items?: ShoppingCartItemDTO[] }) { + const shoppingCartItems = options.items ?? this.get((s) => s.shoppingCartItems); + + for (const item of shoppingCartItems) { + this.loadTakeAwayAvailability({ item }); + this.loadPickUpAvailability({ item }); + this.loadDeliveryAvailability({ item }); + this.loadDeliveryB2bAvailability({ item }); + this.loadDeliveryDigAvailability({ item }); + } + } + + readonly setAvailabilityFetching = this.updater((state, { name, id, fetching }: { name: string; id: string; fetching?: boolean }) => { + const availability = { ...state[name] }; + + if (fetching) { + availability[id] = fetching; + } else { + delete availability[id]; + } + + return { + ...state, + [name]: { + ...availability, + }, + }; + }); + + readonly setAvailability = this.updater((state, { name, availability }: { name: string; availability: any }) => { + const av = { ...state[name] }; + + if (this._availabilityService.isAvailable({ availability })) { + av[availability.itemId] = availability; + } + + return { + ...state, + [name]: av, + }; + }); + + loadPickUpAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) => + options$.pipe( + withLatestFrom(this.selectedPickUpBranch$), + mergeMap(([options, branch]) => { + this.setAvailabilityFetching({ + name: 'pickUpAvailabilities', + id: options.item.product.catalogProductNumber, + fetching: true, + }); + + return this._availabilityService + .getPickUpAvailability({ + item: { + itemId: +options.item.product.catalogProductNumber, + ean: options.item.product.ean, + price: options.item.availability.price, + }, + branch, + quantity: options.item.quantity, + }) + .pipe( + tapResponse( + (availability) => { + this.setAvailabilityFetching({ + name: 'pickUpAvailabilities', + id: options.item.product.catalogProductNumber, + }); + this.setAvailability({ + name: 'pickUpAvailabilities', + availability: { ...availability, itemId: options.item.product.catalogProductNumber }, + }); + }, + () => { + this.setAvailabilityFetching({ + name: 'pickUpAvailabilities', + id: options.item.product.catalogProductNumber, + fetching: false, + }); + this.setAvailability({ name: 'pickUpAvailabilities', availability: {} }); + } + ) + ); + }) + ) + ); + + loadDeliveryAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) => + options$.pipe( + mergeMap((options) => { + this.setAvailabilityFetching({ + name: 'deliveryAvailabilities', + id: options.item.product.catalogProductNumber, + fetching: true, + }); + + return this._availabilityService + .getDeliveryAvailability({ + item: { + itemId: +options.item.product.catalogProductNumber, + ean: options.item.product.ean, + price: options.item.availability.price, + }, + quantity: options.item.quantity, + }) + .pipe( + tapResponse( + (availability) => { + this.setAvailabilityFetching({ + name: 'deliveryAvailabilities', + id: options.item.product.catalogProductNumber, + }); + this.setAvailability({ + name: 'deliveryAvailabilities', + availability: { ...availability, itemId: options.item.product.catalogProductNumber }, + }); + }, + () => { + this.setAvailabilityFetching({ + name: 'deliveryAvailabilities', + id: options.item.product.catalogProductNumber, + fetching: false, + }); + this.setAvailability({ name: 'deliveryAvailabilities', availability: {} }); + } + ) + ); + }) + ) + ); + + loadDeliveryB2bAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) => + options$.pipe( + mergeMap((options) => { + this.setAvailabilityFetching({ + name: 'deliveryB2bAvailabilities', + id: options.item.product.catalogProductNumber, + fetching: true, + }); + + return this._availabilityService + .getB2bDeliveryAvailability({ + item: { + itemId: +options.item.product.catalogProductNumber, + ean: options.item.product.ean, + price: options.item.availability.price, + }, + quantity: options.item.quantity, + }) + .pipe( + tapResponse( + (availability) => { + this.setAvailabilityFetching({ + name: 'deliveryB2bAvailabilities', + id: options.item.product.catalogProductNumber, + }); + + this.setAvailability({ + name: 'deliveryB2bAvailabilities', + availability: { ...availability, itemId: options.item.product.catalogProductNumber }, + }); + }, + () => { + this.setAvailabilityFetching({ + name: 'deliveryB2bAvailabilities', + id: options.item.product.catalogProductNumber, + fetching: false, + }); + this.setAvailability({ name: 'deliveryB2bAvailabilities', availability: {} }); + } + ) + ); + }) + ) + ); + + loadDeliveryDigAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) => + options$.pipe( + mergeMap((options) => { + this.setAvailabilityFetching({ + name: 'deliveryDigAvailabilities', + id: options.item.product.catalogProductNumber, + fetching: true, + }); + + return this._availabilityService + .getDigDeliveryAvailability({ + item: { + itemId: +options.item.product.catalogProductNumber, + ean: options.item.product.ean, + price: options.item.availability.price, + }, + quantity: options.item.quantity, + }) + .pipe( + tapResponse( + (availability) => { + this.setAvailabilityFetching({ + name: 'deliveryDigAvailabilities', + id: options.item.product.catalogProductNumber, + }); + this.setAvailability({ + name: 'deliveryDigAvailabilities', + availability: { ...availability, itemId: options.item.product.catalogProductNumber }, + }); + }, + () => { + this.setAvailabilityFetching({ + name: 'deliveryDigAvailabilities', + id: options.item.product.catalogProductNumber, + fetching: false, + }); + this.setAvailability({ name: 'deliveryDigAvailabilities', availability: {} }); + } + ) + ); + }) + ) + ); + + loadTakeAwayAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) => + options$.pipe( + withLatestFrom(this.selectedTakeAwayBranch$), + mergeMap(([options, branch]) => { + this.setAvailabilityFetching({ + name: 'takeAwayAvailabilities', + id: options.item.product.catalogProductNumber, + fetching: true, + }); + + return this._availabilityService + .getTakeAwayAvailabilityByBranch({ + itemId: +options.item.product.catalogProductNumber, + price: options.item.availability.price, + quantity: options.item.quantity, + branch, + }) + .pipe( + tapResponse( + (availability) => { + this.setAvailabilityFetching({ + name: 'takeAwayAvailabilities', + id: options.item.product.catalogProductNumber, + }); + this.setAvailability({ + name: 'takeAwayAvailabilities', + availability: { ...availability, itemId: options.item.product.catalogProductNumber }, + }); + }, + () => { + this.setAvailabilityFetching({ + name: 'takeAwayAvailabilities', + id: options.item.product.catalogProductNumber, + fetching: false, + }); + this.setAvailability({ name: 'takeAwayAvailabilities', availability: {} }); + } + ) + ); + }) + ) + ); + + loadBranches = this.effect(($) => + $.pipe( + switchMap(() => + this._availabilityService.getBranches().pipe( + map((branches) => + branches.filter( + (branch) => branch.status === 1 && branch.branchType === 1 && branch.isOnline === true && branch.isShippingEnabled === true + ) + ), + withLatestFrom(this._availabilityService.getCurrentBranch()), + tapResponse( + ([branches, currentBranch]) => { + this.patchState({ + branches, + selectedTakeAwayBranch: currentBranch, + selectedPickUpBranch: currentBranch, + currentBranch, + }); + this.loadAvailabilities({}); + }, + () => + this.patchState({ + branches: [], + selectedTakeAwayBranch: undefined, + selectedPickUpBranch: undefined, + currentBranch: undefined, + }) + ) + ) + ) + ) + ); + + checkCanAddItem = this.effect((item$: Observable) => + item$.pipe( + withLatestFrom( + this.processId$, + this.selectedFilterOption$, + this.takeAwayAvailabilities$, + this.pickUpAvailabilities$, + this.deliveryAvailabilities$, + this.deliveryB2bAvailabilities$, + this.deliveryDigAvailabilities$ + ), + mergeMap(([item, processId, selectedOption, takeAway, pickUp, delivery, deliveryB2b, deliveryDig]) => { + let availability; + let orderType; + + switch (selectedOption) { + case 'take-away': + availability = this.getOlaAvailability(takeAway[item.product.catalogProductNumber], item); + orderType = 'Rücklage'; + break; + case 'pick-up': + availability = this.getOlaAvailability(pickUp[item.product.catalogProductNumber], item); + orderType = 'Abholung'; + break; + case 'delivery': + orderType = 'Versand'; + if ( + deliveryDig[item.product.catalogProductNumber] && + deliveryB2b[item.product.catalogProductNumber] && + delivery[item.product.catalogProductNumber] + ) { + availability = this.getOlaAvailability(delivery[item.product.catalogProductNumber], item); + } else if (deliveryDig[item.product.catalogProductNumber]) { + availability = this.getOlaAvailability(deliveryDig[item.product.catalogProductNumber], item); + } else if (deliveryB2b[item.product.catalogProductNumber]) { + availability = this.getOlaAvailability(deliveryB2b[item.product.catalogProductNumber], item); + } + break; + case 'delivery-b2b': + orderType = 'B2B-Versand'; + availability = this.getOlaAvailability(deliveryB2b[item.product.catalogProductNumber], item); + break; + } + + return this._checkoutService + .canAddItem({ + processId, + availability, + orderType, + }) + .pipe( + tapResponse( + (canAddItem) => this.setCanAdd({ itemId: item.product.catalogProductNumber, canAddItem }), + (error: Error) => this.setCanAdd({ itemId: item.product.catalogProductNumber, canAddItem: error?.message }) + ) + ); + }) + ) + ); + + getOlaAvailability(availability: AvailabilityDTO, item: ShoppingCartItemDTO) { + return { + qty: item.quantity, + ean: item.product.ean, + itemId: item.product.catalogProductNumber, + format: item.product.format, + at: availability.estimatedShippingDate, + isPrebooked: availability.isPrebooked, + status: availability.availabilityType, + logisticianId: availability.logistician?.id, + price: availability.price, + ssc: availability.ssc, + sscText: availability.sscText, + supplierId: availability.supplier?.id, + }; + } + + readonly setCanAdd = this.updater((state, data: { itemId: number; canAddItem: true | string }) => { + const canAdd = { ...state.canAdd }; + canAdd[data.itemId] = data.canAddItem; + + return { ...state, canAdd }; + }); + + readonly updateItemQuantity = this.updater((state, value: { itemId: number; quantity: number }) => { + const itemToUpdate = state.shoppingCartItems.find((item) => item.id === value.itemId); + const otherItems = state.shoppingCartItems.filter((item) => item.id !== value.itemId); + const updatedItem = { ...itemToUpdate, quantity: value.quantity }; + const shoppingCartItems = [...otherItems, updatedItem].sort((a, b) => a.product?.name.localeCompare(b.product.name)); + + // Ausgewählte Items auch aktualisieren + let selectedShoppingCartItems = state.selectedShoppingCartItems; + if (state.selectedShoppingCartItems.find((item) => item.id === value.itemId)) { + const selectedItems = state.selectedShoppingCartItems.filter((item) => item.id !== value.itemId); + selectedShoppingCartItems = [...selectedItems, updatedItem].sort((a, b) => a.product?.name.localeCompare(b.product.name)); + } + + return { + ...state, + shoppingCartItems, + selectedShoppingCartItems, + }; + }); + + async removeShoppingCartItem(item: ShoppingCartItemDTO) { + const items = this.get((s) => s.shoppingCartItems); + const processId = this.get((s) => s.processId); + + await this._checkoutService + .updateItemInShoppingCart({ + processId, + shoppingCartItemId: item.id, + update: { + quantity: 0, + availability: null, + }, + }) + .toPromise(); + this.selectShoppingCartItem([item], false); + const shoppingCartItems = items.filter((i) => i.id !== item.id); + this.patchState({ shoppingCartItems }); + } + + selectShoppingCartItem(shoppingCartItems: ShoppingCartItemDTO[], selected: boolean) { + if (selected) { + this.patchState({ + selectedShoppingCartItems: [ + ...this.selectedShoppingCartItems.filter((item) => !shoppingCartItems.find((i) => item.id === i.id)), + ...shoppingCartItems, + ], + }); + } else { + this.patchState({ + selectedShoppingCartItems: this.selectedShoppingCartItems.filter((item) => !shoppingCartItems.find((i) => item.id === i.id)), + }); + } + } + + clearSelectedShoppingCartItems() { + this.patchState({ selectedShoppingCartItems: [] }); + } +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/take-away-option/index.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/take-away-option/index.ts new file mode 100644 index 000000000..4f6a53709 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/take-away-option/index.ts @@ -0,0 +1,3 @@ +// start:ng42.barrel +export * from './take-away-option-list.component'; +// end:ng42.barrel diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/take-away-option/take-away-option-list.component.html b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/take-away-option/take-away-option-list.component.html new file mode 100644 index 000000000..ad8fad481 --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/take-away-option/take-away-option-list.component.html @@ -0,0 +1,18 @@ +
+ +
+ +

Möchten Sie die Artikel
zurücklegen lassen oder
sofort mitnehmen?

+ + diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/take-away-option/take-away-option-list.component.ts b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/take-away-option/take-away-option-list.component.ts new file mode 100644 index 000000000..e928b540d --- /dev/null +++ b/apps/page/checkout/src/lib/modals/purchasing-options-list-modal/take-away-option/take-away-option-list.component.ts @@ -0,0 +1,34 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { BranchDTO } from '@swagger/checkout'; +import { first } from 'rxjs/operators'; +import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store'; + +@Component({ + selector: 'page-take-away-option-list', + templateUrl: 'take-away-option-list.component.html', + styleUrls: ['../list-options.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TakeAwayOptionListComponent { + branches$ = this._store.branches$; + selectedBranch$ = this._store.selectedTakeAwayBranch$; + selectedOption$ = this._store.selectedFilterOption$; + optionChipDisabled$ = this._store.fetchingAvailabilities$; + + constructor(private _store: PurchasingOptionsListModalStore) {} + + optionChange(option: string) { + if (this._store.selectedFilterOption === option) { + this._store.selectedFilterOption = undefined; + } else { + this._store.selectedFilterOption = option; + } + } + + async selectBranch(branch: BranchDTO) { + this._store.selectedTakeAwayBranch = branch; + + const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise(); + shoppingCartItems.forEach((item) => this._store.loadTakeAwayAvailability({ item })); + } +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-modal/b2b-delivery-option/b2b-delivery-option.component.html b/apps/page/checkout/src/lib/modals/purchasing-options-modal/b2b-delivery-option/b2b-delivery-option.component.html index 8866dce31..aaee41162 100644 --- a/apps/page/checkout/src/lib/modals/purchasing-options-modal/b2b-delivery-option/b2b-delivery-option.component.html +++ b/apps/page/checkout/src/lib/modals/purchasing-options-modal/b2b-delivery-option/b2b-delivery-option.component.html @@ -8,7 +8,7 @@

Als B2B Kunde können wir Ihnen den Artikel auch liefern.

- {{ availability.price?.value?.value | currency: availability.price?.value?.currency:'code' }} + {{ price?.value?.value | currency: price?.value?.currency:'code' }}
Versandkostenfrei ava['b2b-delivery'])); + readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['b2b-delivery'])); - constructor(private purchasingOptionsModalStore: PurchasingOptionsModalStore) {} + readonly price$ = combineLatest([this.availability$, this.item$]).pipe( + map(([availability, item]) => this._availabilityService.getPriceForAvailability('b2b-delivery', item.catalogAvailability, availability)) + ); + + constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {} select() { - this.purchasingOptionsModalStore.setOption('b2b-delivery'); + this._purchasingOptionsModalStore.setOption('b2b-delivery'); } } diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-modal/delivery-option/delivery-option.component.html b/apps/page/checkout/src/lib/modals/purchasing-options-modal/delivery-option/delivery-option.component.html index 6940f8da8..6b13efe46 100644 --- a/apps/page/checkout/src/lib/modals/purchasing-options-modal/delivery-option/delivery-option.component.html +++ b/apps/page/checkout/src/lib/modals/purchasing-options-modal/delivery-option/delivery-option.component.html @@ -8,7 +8,7 @@ Möchten Sie den Artikel geliefert bekommen?

- {{ price$ | async | currency: availability.price?.value?.currency:'code' }} + {{ price?.value?.value | currency: price?.value?.currency:'code' }}
Versandkostenfrei - Versanddatum {{ availability?.estimatedShippingDate | date }}Versanddatum {{ estimatedShippingDate | date }}
+ + Zustellung zwischen
+ {{ (availability?.estimatedDelivery?.start | date: 'EEE, dd.MM')?.replace('.', '') }} und + {{ (availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM')?.replace('.', '') }}
+
Versandkostenfrei - Versanddatum {{ availability?.estimatedShippingDate | date: 'shortDate' }}Versanddatum {{ estimatedShippingDate | date: 'shortDate' }} + + Zustellung zwischen
+ {{ (availability?.estimatedDelivery?.start | date: 'EEE, dd.MM')?.replace('.', '') }} und + {{ (availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM')?.replace('.', '') }}
+
@@ -41,9 +44,20 @@ {{ price$ | async | currency: item?.catalogAvailability?.price?.value?.currency || 'EUR':'code' }}
- Abholung ab - Versanddatum - {{ (getAvailability(option) | async)?.estimatedShippingDate | date: 'shortDate' }} + + Abholung ab {{ (getAvailability(option) | async)?.estimatedShippingDate | date: 'shortDate' }} + + + + + Versanddatum {{ availability?.estimatedShippingDate | date: 'shortDate' }} + + + Zustellung zwischen {{ (availability?.estimatedDelivery?.start | date: 'EEE, dd.MM')?.replace('.', '') }} und + {{ (availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM')?.replace('.', '') }} + + +
diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.component.scss b/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.component.scss index 1fd80237e..89914bf2a 100644 --- a/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.component.scss +++ b/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.component.scss @@ -110,3 +110,7 @@ img.thumbnail { .quantity-error { @apply text-dark-goldenrod font-bold text-sm mt-2; } + +.hint { + @apply text-dark-goldenrod font-bold text-xl; +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.component.ts b/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.component.ts index 2e80aceda..1ca826d0a 100644 --- a/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.component.ts +++ b/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.component.ts @@ -4,10 +4,10 @@ import { ApplicationService } from '@core/application'; import { DomainCheckoutService } from '@domain/checkout'; import { AddToShoppingCartDTO, AvailabilityDTO, VATType } from '@swagger/checkout'; import { UiModalRef } from '@ui/modal'; -import { debounceTime, first, map, switchMap, withLatestFrom } from 'rxjs/operators'; +import { shareReplay, debounceTime, first, map, switchMap, withLatestFrom } from 'rxjs/operators'; import { combineLatest, Observable } from 'rxjs'; import { PurchasingOptionsModalData } from './purchasing-options-modal.data'; -import { PurchasingOptions, PurchasingOptionsModalStore } from './purchasing-options-modal.store'; +import { PurchasingOptionsModalStore } from './purchasing-options-modal.store'; import { DomainCatalogService } from '@domain/catalog'; import { BreadcrumbService } from '@core/breadcrumb'; @@ -21,7 +21,7 @@ import { BreadcrumbService } from '@core/breadcrumb'; export class PurchasingOptionsModalComponent { readonly item$ = this.purchasingOptionsModalStore.selectItem; - readonly availableOptions$ = this.purchasingOptionsModalStore.selectAvailableOptions; + readonly availableOptions$ = this.purchasingOptionsModalStore.selectAvailableOptions.pipe(shareReplay()); readonly option$ = this.purchasingOptionsModalStore.selectOption; @@ -293,17 +293,11 @@ export class PurchasingOptionsModalComponent { break; } - const shoppingCart = await this.checkoutService.getShoppingCart({ processId }).pipe(first()).toPromise(); - - const existingItem = shoppingCart?.items?.find( - ({ data }) => data.product.ean === item.product.ean && data.features['orderType'] === this.getNameForOption(option) - ); - - if (shoppingCartItem || existingItem) { + if (shoppingCartItem) { await this.checkoutService .updateItemInShoppingCart({ processId, - shoppingCartItemId: shoppingCartItem?.id || existingItem?.id, + shoppingCartItemId: shoppingCartItem?.id, update: { availability: newItem.availability, quantity: newItem.quantity, @@ -367,21 +361,4 @@ export class PurchasingOptionsModalComponent { } this.activeSpinner = undefined; } - - getNameForOption(option: PurchasingOptions) { - switch (option) { - case 'take-away': - return 'Rücklage'; - case 'pick-up': - return 'Abholung'; - case 'delivery': - return 'Versand'; - case 'dig-delivery': - return 'DIG-Versand'; - case 'b2b-delivery': - return 'B2B-Versand'; - } - - return option; - } } diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.data.ts b/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.data.ts index 8d8a18934..f7ab8f326 100644 --- a/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.data.ts +++ b/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.data.ts @@ -1,5 +1,5 @@ import { ItemDTO } from '@swagger/cat'; -import { AvailabilityDTO, BranchDTO, ShoppingCartItemDTO } from '@swagger/checkout'; +import { AvailabilityDTO, ShoppingCartItemDTO } from '@swagger/checkout'; import { PurchasingOptions } from './purchasing-options-modal.store'; export interface PurchasingOptionsModalData { diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.module.ts b/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.module.ts index 8211a379f..1b5e63277 100644 --- a/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.module.ts +++ b/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.module.ts @@ -14,7 +14,6 @@ import { } from './options'; import { PurchasingOptionsModalComponent } from './purchasing-options-modal.component'; -import { PickUpDropdownComponent } from './pick-up-option/pick-up-dropdown/pick-up-dropdown.component'; import { PageCheckoutPipeModule } from '../../pipes/page-checkout-pipe.module'; import { ProductImageModule } from 'apps/cdn/product-image/src/public-api'; import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module'; @@ -24,6 +23,7 @@ import { UiQuantityDropdownModule } from '@ui/quantity-dropdown'; import { PurchasingOptionsModalPriceInputModule } from './price-input/purchasing-options-modal-price-input.module'; import { UiTooltipModule } from '@ui/tooltip'; import { UiCommonModule } from '@ui/common'; +import { UiBranchDropdownModule } from '@ui/branch-dropdown'; @NgModule({ imports: [ @@ -40,6 +40,7 @@ import { UiCommonModule } from '@ui/common'; RouterModule, PurchasingOptionsModalPriceInputModule, UiTooltipModule, + UiBranchDropdownModule, ], exports: [PurchasingOptionsModalComponent], declarations: [ @@ -47,7 +48,6 @@ import { UiCommonModule } from '@ui/common'; B2BDeliveryOptionComponent, TakeAwayOptionComponent, PickUpOptionComponent, - PickUpDropdownComponent, DeliveryOptionComponent, DigDeliveryOptionComponent, ], diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.store.ts b/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.store.ts index 019992c5d..6a47d9053 100644 --- a/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.store.ts +++ b/apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.store.ts @@ -9,7 +9,6 @@ import { AvailabilityDTO, BranchDTO, OLAAvailabilityDTO, ShoppingCartItemDTO, VA import { isBoolean, isNullOrUndefined, isString } from '@utils/common'; import { NEVER, Observable } from 'rxjs'; import { delay, filter, map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; -import { geoDistance } from '@utils/common'; export type PurchasingOptions = 'take-away' | 'pick-up' | 'delivery' | 'dig-delivery' | 'b2b-delivery' | 'download'; @@ -29,8 +28,6 @@ interface PurchasingOptionsModalState { canAdd: boolean; canAddError?: string; canUpgrade: boolean; - // TODO: FilterBranch in der UI Component sortieren und filtern - filterResult?: BranchDTO[]; availabilities: { [key: string]: AvailabilityDTO }; customPrice?: number; customVat?: VATType; @@ -97,8 +94,6 @@ export class PurchasingOptionsModalStore extends ComponentStore s.canAddError); - readonly selectFilterResult = this.select((s) => s.filterResult); - readonly selectFetchingAvailability = this.select((s) => s.fetchingAvailability); readonly selectMaxQuantityError = this.select((s) => s.maxQuantityError); @@ -218,15 +213,12 @@ export class PurchasingOptionsModalStore extends ComponentStore { this.loadAvailability(); - const filterResult = state?.availableBranches; - filterResult.sort((a: BranchDTO, b: BranchDTO) => this.branchSorterFn(a, b, branch)); return { ...state, branch, availability: undefined, canAdd: false, canAddError: undefined, - filterResult, }; }); @@ -317,14 +309,10 @@ export class PurchasingOptionsModalStore extends ComponentStore { const branch = state.branch || state.defaultBranch; - const userBranch = availableBranches.find((b) => b.id === branch.id); - const filterResult = availableBranches; - filterResult.sort((a: BranchDTO, b: BranchDTO) => this.branchSorterFn(a, b, userBranch)); return { ...state, availableBranches, branch, - filterResult, }; }); @@ -352,38 +340,6 @@ export class PurchasingOptionsModalStore extends ComponentStore { - const branch = state.branch || state.defaultBranch; - if (!!filterValue) { - const filterResult = state.availableBranches.filter((b) => { - const name = b.name.toLowerCase(); - const zipCode = b.address?.zipCode; - const city = b.address?.city?.toLowerCase(); - if (!zipCode || !city) { - return name.indexOf(filterValue.toLowerCase()) >= 0; - } else { - return ( - name.indexOf(filterValue.toLowerCase()) >= 0 || - zipCode.indexOf(filterValue) >= 0 || - city.indexOf(filterValue.toLowerCase()) >= 0 - ); - } - }); - filterResult.sort((a: BranchDTO, b: BranchDTO) => this.branchSorterFn(a, b, branch)); - return { - ...state, - filterResult, - }; - } else { - const filterResult = state.availableBranches; - filterResult.sort((a: BranchDTO, b: BranchDTO) => this.branchSorterFn(a, b, branch)); - return { - ...state, - filterResult, - }; - } - }); - loadBranches = this.effect((branchId$: Observable) => branchId$.pipe( switchMap((branchId) => @@ -515,13 +471,6 @@ export class PurchasingOptionsModalStore extends ComponentStore $.pipe( switchMap((_) => diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-modal/take-away-option/take-away-option.component.html b/apps/page/checkout/src/lib/modals/purchasing-options-modal/take-away-option/take-away-option.component.html index 1dfa145a7..10314043d 100644 --- a/apps/page/checkout/src/lib/modals/purchasing-options-modal/take-away-option/take-away-option.component.html +++ b/apps/page/checkout/src/lib/modals/purchasing-options-modal/take-away-option/take-away-option.component.html @@ -7,14 +7,7 @@

Möchten Sie den Artikel zurücklegen lassen oder sofort mitnehmen?

- {{ - availability.price?.value?.value | currency: availability.price?.value?.currency:'code' - }} - - {{ - availability.retailPrice?.value?.value | currency: availability.retailPrice?.value?.currency:'code' - }} - + {{ price?.value?.value | currency: price?.value?.currency:'code' }}
- - -
diff --git a/apps/shared/goods-in-out/src/lib/goods-in-out-order-details/goods-in-out-order-details-item/goods-in-out-order-details-item.component.scss b/apps/shared/goods-in-out/src/lib/goods-in-out-order-details/goods-in-out-order-details-item/goods-in-out-order-details-item.component.scss index b58b6889f..8d2333e68 100644 --- a/apps/shared/goods-in-out/src/lib/goods-in-out-order-details/goods-in-out-order-details-item/goods-in-out-order-details-item.component.scss +++ b/apps/shared/goods-in-out/src/lib/goods-in-out-order-details/goods-in-out-order-details-item/goods-in-out-order-details-item.component.scss @@ -65,8 +65,9 @@ button { @apply flex-grow bg-transparent border-none outline-none text-base mx-4; } - input:disabled { + input.inactive { @apply text-warning font-bold; + @apply flex-grow bg-transparent border-none outline-none text-base mx-4 text-warning font-bold; // ipad color fix -webkit-text-fill-color: rgb(190, 129, 0); opacity: 1; diff --git a/apps/shared/goods-in-out/src/lib/goods-in-out-order-details/goods-in-out-order-details-item/goods-in-out-order-details-item.component.ts b/apps/shared/goods-in-out/src/lib/goods-in-out-order-details/goods-in-out-order-details-item/goods-in-out-order-details-item.component.ts index f4543be04..971216b46 100644 --- a/apps/shared/goods-in-out/src/lib/goods-in-out-order-details/goods-in-out-order-details-item/goods-in-out-order-details-item.component.ts +++ b/apps/shared/goods-in-out/src/lib/goods-in-out-order-details/goods-in-out-order-details-item/goods-in-out-order-details-item.component.ts @@ -38,7 +38,6 @@ export class SharedGoodsInOutOrderDetailsItemComponent extends ComponentStore s.selected)) { @@ -173,17 +172,15 @@ export class SharedGoodsInOutOrderDetailsItemComponent extends ComponentStore { + return { + ngModule: AvModule, + providers: [ + { + provide: AvConfiguration, + useValue: {rootUrl: customParams.rootUrl} + } + ] + } + } +} diff --git a/apps/swagger/availability/src/lib/models.ts b/apps/swagger/availability/src/lib/models.ts index dd2eec75e..649815832 100644 --- a/apps/swagger/availability/src/lib/models.ts +++ b/apps/swagger/availability/src/lib/models.ts @@ -2,10 +2,16 @@ export { ResponseArgsOfIEnumerableOfAvailabilityDTO } from './models/response-ar export { AvailabilityDTO } from './models/availability-dto'; export { PriceDTO } from './models/price-dto'; export { PriceValueDTO } from './models/price-value-dto'; +export { TouchedBase } from './models/touched-base'; export { VATValueDTO } from './models/vatvalue-dto'; export { VATType } from './models/vattype'; export { AvailabilityType } from './models/availability-type'; +export { RangeDTO } from './models/range-dto'; export { ResponseArgs } from './models/response-args'; +export { DialogOfString } from './models/dialog-of-string'; +export { DialogSettings } from './models/dialog-settings'; +export { DialogContentType } from './models/dialog-content-type'; +export { KeyValueDTOOfStringAndString } from './models/key-value-dtoof-string-and-string'; export { IPublicUserInfo } from './models/ipublic-user-info'; export { ProblemDetails } from './models/problem-details'; export { AvailabilityRequestDTO } from './models/availability-request-dto'; diff --git a/apps/swagger/availability/src/lib/models/availability-dto.ts b/apps/swagger/availability/src/lib/models/availability-dto.ts index 2e374762a..d00a76973 100644 --- a/apps/swagger/availability/src/lib/models/availability-dto.ts +++ b/apps/swagger/availability/src/lib/models/availability-dto.ts @@ -1,26 +1,29 @@ /* tslint:disable */ +import { RangeDTO } from './range-dto'; import { PriceDTO } from './price-dto'; import { AvailabilityType } from './availability-type'; export interface AvailabilityDTO { - itemId?: number; - supplierProductNumber?: string; - requestReference?: string; + altAt?: string; + at?: string; ean?: string; - shop?: number; - price?: PriceDTO; - supplier?: string; - supplierId?: number; + estimatedDelivery?: RangeDTO; + isPrebooked?: boolean; + itemId?: number; logistician?: string; logisticianId?: number; + orderReference?: string; + preferred?: number; + price?: PriceDTO; + qty?: number; + requestMessage?: string; + requestReference?: string; + requestStatusCode?: string; + requested?: string; + shop?: number; ssc?: string; sscText?: string; - qty?: number; - isPrebooked?: boolean; - at?: string; - altAt?: string; status: AvailabilityType; - preferred?: number; - requested?: string; - requestStatusCode?: string; - requestMessage?: string; + supplier?: string; + supplierId?: number; + supplierProductNumber?: string; } diff --git a/apps/swagger/availability/src/lib/models/availability-request-dto.ts b/apps/swagger/availability/src/lib/models/availability-request-dto.ts index 863dbc574..e2c23a497 100644 --- a/apps/swagger/availability/src/lib/models/availability-request-dto.ts +++ b/apps/swagger/availability/src/lib/models/availability-request-dto.ts @@ -1,18 +1,18 @@ /* tslint:disable */ import { PriceDTO } from './price-dto'; export interface AvailabilityRequestDTO { - itemId?: string; - supplierProductNumber?: string; + availabilityReference?: string; + branchNumber?: string; ean?: string; - qty: number; + estimatedShipping?: string; + itemId?: string; + name?: string; orderCode?: string; - supplier?: string; preBook?: boolean; price?: PriceDTO; - ssc?: string; - estimatedShipping?: string; + qty: number; shopId?: number; - branchNumber?: string; - availabilityReference?: string; - name?: string; + ssc?: string; + supplier?: string; + supplierProductNumber?: string; } diff --git a/apps/swagger/availability/src/lib/models/availability-type.ts b/apps/swagger/availability/src/lib/models/availability-type.ts index 80bd92e35..a20c74ba7 100644 --- a/apps/swagger/availability/src/lib/models/availability-type.ts +++ b/apps/swagger/availability/src/lib/models/availability-type.ts @@ -1,2 +1,2 @@ /* tslint:disable */ -export type AvailabilityType = 0 | 1 | 2 | 32 | 256 | 1024 | 2048 | 4096 | 8192 | 16384; \ No newline at end of file +export type AvailabilityType = 0 | 1 | 2 | 32 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384; \ No newline at end of file diff --git a/apps/swagger/availability/src/lib/models/dialog-content-type.ts b/apps/swagger/availability/src/lib/models/dialog-content-type.ts new file mode 100644 index 000000000..eca48a225 --- /dev/null +++ b/apps/swagger/availability/src/lib/models/dialog-content-type.ts @@ -0,0 +1,2 @@ +/* tslint:disable */ +export type DialogContentType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128; \ No newline at end of file diff --git a/apps/swagger/availability/src/lib/models/dialog-of-string.ts b/apps/swagger/availability/src/lib/models/dialog-of-string.ts new file mode 100644 index 000000000..927ec617a --- /dev/null +++ b/apps/swagger/availability/src/lib/models/dialog-of-string.ts @@ -0,0 +1,16 @@ +/* tslint:disable */ +import { KeyValueDTOOfStringAndString } from './key-value-dtoof-string-and-string'; +import { DialogContentType } from './dialog-content-type'; +import { DialogSettings } from './dialog-settings'; +export interface DialogOfString { + actions?: Array; + actionsRequired?: number; + area?: string; + content?: string; + contentType: DialogContentType; + description?: string; + displayTimeout?: number; + settings: DialogSettings; + subtitle?: string; + title?: string; +} diff --git a/apps/swagger/availability/src/lib/models/dialog-settings.ts b/apps/swagger/availability/src/lib/models/dialog-settings.ts new file mode 100644 index 000000000..1ac70356d --- /dev/null +++ b/apps/swagger/availability/src/lib/models/dialog-settings.ts @@ -0,0 +1,2 @@ +/* tslint:disable */ +export type DialogSettings = 0 | 1 | 2 | 4; \ No newline at end of file diff --git a/apps/swagger/availability/src/lib/models/ipublic-user-info.ts b/apps/swagger/availability/src/lib/models/ipublic-user-info.ts index 72c286b98..db73d102c 100644 --- a/apps/swagger/availability/src/lib/models/ipublic-user-info.ts +++ b/apps/swagger/availability/src/lib/models/ipublic-user-info.ts @@ -2,6 +2,6 @@ export interface IPublicUserInfo { alias?: string; displayName?: string; - username?: string; isAuthenticated: boolean; + username?: string; } diff --git a/apps/swagger/availability/src/lib/models/key-value-dtoof-string-and-string.ts b/apps/swagger/availability/src/lib/models/key-value-dtoof-string-and-string.ts new file mode 100644 index 000000000..91f9f4d89 --- /dev/null +++ b/apps/swagger/availability/src/lib/models/key-value-dtoof-string-and-string.ts @@ -0,0 +1,11 @@ +/* tslint:disable */ +export interface KeyValueDTOOfStringAndString { + command?: string; + description?: string; + enabled?: boolean; + key?: string; + label?: string; + selected?: boolean; + sort?: number; + value?: string; +} diff --git a/apps/swagger/availability/src/lib/models/price-dto.ts b/apps/swagger/availability/src/lib/models/price-dto.ts index 4747903c6..bcea137fb 100644 --- a/apps/swagger/availability/src/lib/models/price-dto.ts +++ b/apps/swagger/availability/src/lib/models/price-dto.ts @@ -1,7 +1,8 @@ /* tslint:disable */ +import { TouchedBase } from './touched-base'; import { PriceValueDTO } from './price-value-dto'; import { VATValueDTO } from './vatvalue-dto'; -export interface PriceDTO { +export interface PriceDTO extends TouchedBase{ value?: PriceValueDTO; vat?: VATValueDTO; } diff --git a/apps/swagger/availability/src/lib/models/price-value-dto.ts b/apps/swagger/availability/src/lib/models/price-value-dto.ts index 8500aff63..d73c2ad95 100644 --- a/apps/swagger/availability/src/lib/models/price-value-dto.ts +++ b/apps/swagger/availability/src/lib/models/price-value-dto.ts @@ -1,6 +1,7 @@ /* tslint:disable */ -export interface PriceValueDTO { - value?: number; +import { TouchedBase } from './touched-base'; +export interface PriceValueDTO extends TouchedBase{ currency?: string; currencySymbol?: string; + value?: number; } diff --git a/apps/swagger/availability/src/lib/models/problem-details.ts b/apps/swagger/availability/src/lib/models/problem-details.ts index c5229b04c..3145234d6 100644 --- a/apps/swagger/availability/src/lib/models/problem-details.ts +++ b/apps/swagger/availability/src/lib/models/problem-details.ts @@ -1,9 +1,10 @@ /* tslint:disable */ export interface ProblemDetails { - type?: string; - title?: string; - status?: number; detail?: string; + extensions: {[key: string]: any}; instance?: string; - extensions?: {[key: string]: any}; + status?: number; + title?: string; + type?: string; + [prop: string]: any; } diff --git a/apps/swagger/availability/src/lib/models/range-dto.ts b/apps/swagger/availability/src/lib/models/range-dto.ts new file mode 100644 index 000000000..f39443d02 --- /dev/null +++ b/apps/swagger/availability/src/lib/models/range-dto.ts @@ -0,0 +1,5 @@ +/* tslint:disable */ +export interface RangeDTO { + start?: string; + stop?: string; +} diff --git a/apps/swagger/availability/src/lib/models/response-args-of-ienumerable-of-availability-dto.ts b/apps/swagger/availability/src/lib/models/response-args-of-ienumerable-of-availability-dto.ts index f05d4eac0..e71a61d02 100644 --- a/apps/swagger/availability/src/lib/models/response-args-of-ienumerable-of-availability-dto.ts +++ b/apps/swagger/availability/src/lib/models/response-args-of-ienumerable-of-availability-dto.ts @@ -1,6 +1,6 @@ /* tslint:disable */ import { ResponseArgs } from './response-args'; import { AvailabilityDTO } from './availability-dto'; -export interface ResponseArgsOfIEnumerableOfAvailabilityDTO extends ResponseArgs { +export interface ResponseArgsOfIEnumerableOfAvailabilityDTO extends ResponseArgs{ result?: Array; } diff --git a/apps/swagger/availability/src/lib/models/response-args.ts b/apps/swagger/availability/src/lib/models/response-args.ts index de82ca711..6b099bf9e 100644 --- a/apps/swagger/availability/src/lib/models/response-args.ts +++ b/apps/swagger/availability/src/lib/models/response-args.ts @@ -1,9 +1,11 @@ /* tslint:disable */ +import { DialogOfString } from './dialog-of-string'; import { IPublicUserInfo } from './ipublic-user-info'; export interface ResponseArgs { + dialog?: DialogOfString; error: boolean; + invalidProperties?: {[key: string]: string}; message?: string; requestId?: number; userInfo?: IPublicUserInfo; - invalidProperties?: {[key: string]: string}; } diff --git a/apps/swagger/availability/src/lib/models/touched-base.ts b/apps/swagger/availability/src/lib/models/touched-base.ts new file mode 100644 index 000000000..1075419bc --- /dev/null +++ b/apps/swagger/availability/src/lib/models/touched-base.ts @@ -0,0 +1,3 @@ +/* tslint:disable */ +export interface TouchedBase { +} diff --git a/apps/swagger/availability/src/lib/models/vatvalue-dto.ts b/apps/swagger/availability/src/lib/models/vatvalue-dto.ts index a4e56566e..a43a25e00 100644 --- a/apps/swagger/availability/src/lib/models/vatvalue-dto.ts +++ b/apps/swagger/availability/src/lib/models/vatvalue-dto.ts @@ -1,8 +1,9 @@ /* tslint:disable */ +import { TouchedBase } from './touched-base'; import { VATType } from './vattype'; -export interface VATValueDTO { - label?: string; +export interface VATValueDTO extends TouchedBase{ inPercent?: number; - vatType?: VATType; + label?: string; value?: number; + vatType?: VATType; } diff --git a/apps/swagger/availability/src/lib/services/availability.service.ts b/apps/swagger/availability/src/lib/services/availability.service.ts index 5fc41dd47..09a3c922d 100644 --- a/apps/swagger/availability/src/lib/services/availability.service.ts +++ b/apps/swagger/availability/src/lib/services/availability.service.ts @@ -24,6 +24,8 @@ class AvailabilityService extends __BaseService { } /** + * Lieferbarkeitsabfrage für Abholung + * Für jede AvailabilityRequestDTO müssen mindestens folgende Werte gesetzt sein: ItemId oder EAN, Qty, sowie ShopId oder BranchNumber * @param request undefined */ AvailabilityStoreAvailabilityResponse(request: Array): __Observable<__StrictHttpResponse> { @@ -49,6 +51,8 @@ class AvailabilityService extends __BaseService { ); } /** + * Lieferbarkeitsabfrage für Abholung + * Für jede AvailabilityRequestDTO müssen mindestens folgende Werte gesetzt sein: ItemId oder EAN, Qty, sowie ShopId oder BranchNumber * @param request undefined */ AvailabilityStoreAvailability(request: Array): __Observable { @@ -58,6 +62,8 @@ class AvailabilityService extends __BaseService { } /** + * Lieferbarkeitsabfrage für Versand + * Für jede AvailabilityRequestDTO müssen mindestens folgende Werte gesetzt sein: ItemId oder EAN, Qty, sowie ShopId oder BranchNumber * @param request undefined */ AvailabilityShippingAvailabilityResponse(request: Array): __Observable<__StrictHttpResponse> { @@ -83,6 +89,8 @@ class AvailabilityService extends __BaseService { ); } /** + * Lieferbarkeitsabfrage für Versand + * Für jede AvailabilityRequestDTO müssen mindestens folgende Werte gesetzt sein: ItemId oder EAN, Qty, sowie ShopId oder BranchNumber * @param request undefined */ AvailabilityShippingAvailability(request: Array): __Observable { diff --git a/apps/swagger/checkout/src/lib/models.ts b/apps/swagger/checkout/src/lib/models.ts index 8301fd1e3..c4191581d 100644 --- a/apps/swagger/checkout/src/lib/models.ts +++ b/apps/swagger/checkout/src/lib/models.ts @@ -4,9 +4,14 @@ export { OLAAvailabilityDTO } from './models/olaavailability-dto'; export { AvailabilityType } from './models/availability-type'; export { PriceDTO } from './models/price-dto'; export { PriceValueDTO } from './models/price-value-dto'; +export { TouchedBase } from './models/touched-base'; export { VATValueDTO } from './models/vatvalue-dto'; export { VATType } from './models/vattype'; export { ResponseArgs } from './models/response-args'; +export { DialogOfString } from './models/dialog-of-string'; +export { DialogSettings } from './models/dialog-settings'; +export { DialogContentType } from './models/dialog-content-type'; +export { KeyValueDTOOfStringAndString } from './models/key-value-dtoof-string-and-string'; export { IPublicUserInfo } from './models/ipublic-user-info'; export { ProblemDetails } from './models/problem-details'; export { ItemPayload } from './models/item-payload'; @@ -30,7 +35,6 @@ export { TenantDTO } from './models/tenant-dto'; export { ReadOnlyEntityDTOOfTenantDTOAndIReadOnlyTenant } from './models/read-only-entity-dtoof-tenant-dtoand-iread-only-tenant'; export { EntityDTO } from './models/entity-dto'; export { EntityStatus } from './models/entity-status'; -export { TouchedBase } from './models/touched-base'; export { EntityDTOReferenceContainer } from './models/entity-dtoreference-container'; export { ExternalReferenceDTO } from './models/external-reference-dto'; export { ReadOnlyEntityDTOOfLabelDTOAndIReadOnlyLabel } from './models/read-only-entity-dtoof-label-dtoand-iread-only-label'; @@ -112,6 +116,7 @@ export { ReadOnlyEntityDTOOfItemDTOAndIItem } from './models/read-only-entity-dt export { ImageDTO } from './models/image-dto'; export { UrlDTO } from './models/url-dto'; export { AvailabilityDTO } from './models/availability-dto'; +export { DateRangeDTO } from './models/date-range-dto'; export { EntityDTOContainerOfSupplierDTO } from './models/entity-dtocontainer-of-supplier-dto'; export { SupplierDTO } from './models/supplier-dto'; export { SupplierType } from './models/supplier-type'; @@ -157,7 +162,6 @@ export { EntityDTOContainerOfCouponDTO } from './models/entity-dtocontainer-of-c export { CouponDTO } from './models/coupon-dto'; export { CouponType } from './models/coupon-type'; export { ReadOnlyEntityDTOOfCouponDTOAndICoupon } from './models/read-only-entity-dtoof-coupon-dtoand-icoupon'; -export { KeyValueDTOOfStringAndString } from './models/key-value-dtoof-string-and-string'; export { ReadOnlyEntityDTOOfCheckoutDTOAndICheckout } from './models/read-only-entity-dtoof-checkout-dtoand-icheckout'; export { ShippingAddressDTO } from './models/shipping-address-dto'; export { EntityDTOOfDestinationDTOAndIDestination } from './models/entity-dtoof-destination-dtoand-idestination'; diff --git a/apps/swagger/checkout/src/lib/models/address-dto.ts b/apps/swagger/checkout/src/lib/models/address-dto.ts index 502254ee0..9cb2d683e 100644 --- a/apps/swagger/checkout/src/lib/models/address-dto.ts +++ b/apps/swagger/checkout/src/lib/models/address-dto.ts @@ -1,6 +1,7 @@ /* tslint:disable */ +import { TouchedBase } from './touched-base'; import { GeoLocation } from './geo-location'; -export interface AddressDTO { +export interface AddressDTO extends TouchedBase{ apartment?: string; careOf?: string; city?: string; diff --git a/apps/swagger/checkout/src/lib/models/availability-dto.ts b/apps/swagger/checkout/src/lib/models/availability-dto.ts index e096dc025..0ddc3c38c 100644 --- a/apps/swagger/checkout/src/lib/models/availability-dto.ts +++ b/apps/swagger/checkout/src/lib/models/availability-dto.ts @@ -1,11 +1,13 @@ /* tslint:disable */ import { AvailabilityType } from './availability-type'; +import { DateRangeDTO } from './date-range-dto'; import { EntityDTOContainerOfLogisticianDTO } from './entity-dtocontainer-of-logistician-dto'; import { PriceDTO } from './price-dto'; import { EntityDTOContainerOfShopItemDTO } from './entity-dtocontainer-of-shop-item-dto'; import { EntityDTOContainerOfSupplierDTO } from './entity-dtocontainer-of-supplier-dto'; export interface AvailabilityDTO { availabilityType: AvailabilityType; + estimatedDelivery?: DateRangeDTO; estimatedShippingDate?: string; inStock?: number; isPrebooked?: boolean; diff --git a/apps/swagger/checkout/src/lib/models/communication-details-dto.ts b/apps/swagger/checkout/src/lib/models/communication-details-dto.ts index eb5791e6a..03b0ae635 100644 --- a/apps/swagger/checkout/src/lib/models/communication-details-dto.ts +++ b/apps/swagger/checkout/src/lib/models/communication-details-dto.ts @@ -1,5 +1,6 @@ /* tslint:disable */ -export interface CommunicationDetailsDTO { +import { TouchedBase } from './touched-base'; +export interface CommunicationDetailsDTO extends TouchedBase{ email?: string; fax?: string; mobile?: string; diff --git a/apps/swagger/checkout/src/lib/models/date-range-dto.ts b/apps/swagger/checkout/src/lib/models/date-range-dto.ts new file mode 100644 index 000000000..a84f889f9 --- /dev/null +++ b/apps/swagger/checkout/src/lib/models/date-range-dto.ts @@ -0,0 +1,6 @@ +/* tslint:disable */ +import { TouchedBase } from './touched-base'; +export interface DateRangeDTO extends TouchedBase{ + start?: string; + stop?: string; +} diff --git a/apps/swagger/checkout/src/lib/models/dialog-content-type.ts b/apps/swagger/checkout/src/lib/models/dialog-content-type.ts new file mode 100644 index 000000000..eca48a225 --- /dev/null +++ b/apps/swagger/checkout/src/lib/models/dialog-content-type.ts @@ -0,0 +1,2 @@ +/* tslint:disable */ +export type DialogContentType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128; \ No newline at end of file diff --git a/apps/swagger/checkout/src/lib/models/dialog-of-string.ts b/apps/swagger/checkout/src/lib/models/dialog-of-string.ts new file mode 100644 index 000000000..927ec617a --- /dev/null +++ b/apps/swagger/checkout/src/lib/models/dialog-of-string.ts @@ -0,0 +1,16 @@ +/* tslint:disable */ +import { KeyValueDTOOfStringAndString } from './key-value-dtoof-string-and-string'; +import { DialogContentType } from './dialog-content-type'; +import { DialogSettings } from './dialog-settings'; +export interface DialogOfString { + actions?: Array; + actionsRequired?: number; + area?: string; + content?: string; + contentType: DialogContentType; + description?: string; + displayTimeout?: number; + settings: DialogSettings; + subtitle?: string; + title?: string; +} diff --git a/apps/swagger/checkout/src/lib/models/dialog-settings.ts b/apps/swagger/checkout/src/lib/models/dialog-settings.ts new file mode 100644 index 000000000..1ac70356d --- /dev/null +++ b/apps/swagger/checkout/src/lib/models/dialog-settings.ts @@ -0,0 +1,2 @@ +/* tslint:disable */ +export type DialogSettings = 0 | 1 | 2 | 4; \ No newline at end of file diff --git a/apps/swagger/checkout/src/lib/models/organisation-dto.ts b/apps/swagger/checkout/src/lib/models/organisation-dto.ts index 24bc58335..dda4ec124 100644 --- a/apps/swagger/checkout/src/lib/models/organisation-dto.ts +++ b/apps/swagger/checkout/src/lib/models/organisation-dto.ts @@ -1,5 +1,6 @@ /* tslint:disable */ -export interface OrganisationDTO { +import { TouchedBase } from './touched-base'; +export interface OrganisationDTO extends TouchedBase{ costUnit?: string; department?: string; gln?: string; diff --git a/apps/swagger/checkout/src/lib/models/price-dto.ts b/apps/swagger/checkout/src/lib/models/price-dto.ts index 4747903c6..bcea137fb 100644 --- a/apps/swagger/checkout/src/lib/models/price-dto.ts +++ b/apps/swagger/checkout/src/lib/models/price-dto.ts @@ -1,7 +1,8 @@ /* tslint:disable */ +import { TouchedBase } from './touched-base'; import { PriceValueDTO } from './price-value-dto'; import { VATValueDTO } from './vatvalue-dto'; -export interface PriceDTO { +export interface PriceDTO extends TouchedBase{ value?: PriceValueDTO; vat?: VATValueDTO; } diff --git a/apps/swagger/checkout/src/lib/models/price-value-dto.ts b/apps/swagger/checkout/src/lib/models/price-value-dto.ts index 9c74436ba..d73c2ad95 100644 --- a/apps/swagger/checkout/src/lib/models/price-value-dto.ts +++ b/apps/swagger/checkout/src/lib/models/price-value-dto.ts @@ -1,5 +1,6 @@ /* tslint:disable */ -export interface PriceValueDTO { +import { TouchedBase } from './touched-base'; +export interface PriceValueDTO extends TouchedBase{ currency?: string; currencySymbol?: string; value?: number; diff --git a/apps/swagger/checkout/src/lib/models/problem-details.ts b/apps/swagger/checkout/src/lib/models/problem-details.ts index 370f918ed..3145234d6 100644 --- a/apps/swagger/checkout/src/lib/models/problem-details.ts +++ b/apps/swagger/checkout/src/lib/models/problem-details.ts @@ -1,7 +1,7 @@ /* tslint:disable */ export interface ProblemDetails { detail?: string; - extensions?: {[key: string]: any}; + extensions: {[key: string]: any}; instance?: string; status?: number; title?: string; diff --git a/apps/swagger/checkout/src/lib/models/product-dto.ts b/apps/swagger/checkout/src/lib/models/product-dto.ts index 8d5c51c3b..0c95b517d 100644 --- a/apps/swagger/checkout/src/lib/models/product-dto.ts +++ b/apps/swagger/checkout/src/lib/models/product-dto.ts @@ -1,7 +1,8 @@ /* tslint:disable */ +import { TouchedBase } from './touched-base'; import { SizeOfString } from './size-of-string'; import { WeightOfAvoirdupois } from './weight-of-avoirdupois'; -export interface ProductDTO { +export interface ProductDTO extends TouchedBase{ additionalName?: string; catalogProductNumber?: string; contributors?: string; diff --git a/apps/swagger/checkout/src/lib/models/promotion-dto.ts b/apps/swagger/checkout/src/lib/models/promotion-dto.ts index 6aaf0e8b7..20789ba70 100644 --- a/apps/swagger/checkout/src/lib/models/promotion-dto.ts +++ b/apps/swagger/checkout/src/lib/models/promotion-dto.ts @@ -1,5 +1,6 @@ /* tslint:disable */ -export interface PromotionDTO { +import { TouchedBase } from './touched-base'; +export interface PromotionDTO extends TouchedBase{ code?: string; label?: string; points?: number; diff --git a/apps/swagger/checkout/src/lib/models/response-args.ts b/apps/swagger/checkout/src/lib/models/response-args.ts index 321cf370a..6b099bf9e 100644 --- a/apps/swagger/checkout/src/lib/models/response-args.ts +++ b/apps/swagger/checkout/src/lib/models/response-args.ts @@ -1,6 +1,8 @@ /* tslint:disable */ +import { DialogOfString } from './dialog-of-string'; import { IPublicUserInfo } from './ipublic-user-info'; export interface ResponseArgs { + dialog?: DialogOfString; error: boolean; invalidProperties?: {[key: string]: string}; message?: string; diff --git a/apps/swagger/checkout/src/lib/models/vatvalue-dto.ts b/apps/swagger/checkout/src/lib/models/vatvalue-dto.ts index 295376a6b..a43a25e00 100644 --- a/apps/swagger/checkout/src/lib/models/vatvalue-dto.ts +++ b/apps/swagger/checkout/src/lib/models/vatvalue-dto.ts @@ -1,6 +1,7 @@ /* tslint:disable */ +import { TouchedBase } from './touched-base'; import { VATType } from './vattype'; -export interface VATValueDTO { +export interface VATValueDTO extends TouchedBase{ inPercent?: number; label?: string; value?: number; diff --git a/apps/ui/branch-dropdown/README.md b/apps/ui/branch-dropdown/README.md new file mode 100644 index 000000000..d0b224981 --- /dev/null +++ b/apps/ui/branch-dropdown/README.md @@ -0,0 +1,25 @@ +# BranchDropdown + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.4. + +## Code scaffolding + +Run `ng generate component component-name --project branch-dropdown` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project branch-dropdown`. + +> Note: Don't forget to add `--project branch-dropdown` or else it will be added to the default project in your `angular.json` file. + +## Build + +Run `ng build branch-dropdown` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Publishing + +After building your library with `ng build branch-dropdown`, go to the dist folder `cd dist/branch-dropdown` and run `npm publish`. + +## Running unit tests + +Run `ng test branch-dropdown` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/apps/ui/branch-dropdown/karma.conf.js b/apps/ui/branch-dropdown/karma.conf.js new file mode 100644 index 000000000..77f9791e0 --- /dev/null +++ b/apps/ui/branch-dropdown/karma.conf.js @@ -0,0 +1,32 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma'), + ], + client: { + clearContext: false, // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../../../coverage/ui/branch-dropdown'), + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true, + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true, + }); +}; diff --git a/apps/ui/branch-dropdown/ng-package.json b/apps/ui/branch-dropdown/ng-package.json new file mode 100644 index 000000000..a341a2514 --- /dev/null +++ b/apps/ui/branch-dropdown/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/ui/branch-dropdown", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/apps/ui/branch-dropdown/package.json b/apps/ui/branch-dropdown/package.json new file mode 100644 index 000000000..29c449e37 --- /dev/null +++ b/apps/ui/branch-dropdown/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui/branch-dropdown", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^10.2.4", + "@angular/core": "^10.2.4" + }, + "dependencies": { + "tslib": "^2.0.0" + } +} \ No newline at end of file diff --git a/apps/ui/branch-dropdown/src/lib/branch-dropdown-item.component.html b/apps/ui/branch-dropdown/src/lib/branch-dropdown-item.component.html new file mode 100644 index 000000000..2ec61b9b8 --- /dev/null +++ b/apps/ui/branch-dropdown/src/lib/branch-dropdown-item.component.html @@ -0,0 +1,3 @@ + diff --git a/apps/ui/branch-dropdown/src/lib/branch-dropdown-item.component.scss b/apps/ui/branch-dropdown/src/lib/branch-dropdown-item.component.scss new file mode 100644 index 000000000..0ae4e5238 --- /dev/null +++ b/apps/ui/branch-dropdown/src/lib/branch-dropdown-item.component.scss @@ -0,0 +1,11 @@ +button { + @apply w-full text-left list-none px-px-15 py-px-8 cursor-pointer whitespace-nowrap overflow-x-hidden overflow-y-scroll overflow-ellipsis text-xl bg-white border-none outline-none; +} + +button:hover { + @apply bg-disabled-customer; +} + +.active { + @apply bg-disabled-customer; +} diff --git a/apps/ui/branch-dropdown/src/lib/branch-dropdown-item.component.ts b/apps/ui/branch-dropdown/src/lib/branch-dropdown-item.component.ts new file mode 100644 index 000000000..b9c9bd927 --- /dev/null +++ b/apps/ui/branch-dropdown/src/lib/branch-dropdown-item.component.ts @@ -0,0 +1,37 @@ +import { Highlightable } from '@angular/cdk/a11y'; +import { Component, ElementRef, EventEmitter, HostBinding, Input, Output } from '@angular/core'; +import { BranchDTO } from '@swagger/checkout'; + +@Component({ + selector: 'ui-branch-dropdown-item', + templateUrl: 'branch-dropdown-item.component.html', + styleUrls: ['branch-dropdown-item.component.scss'], +}) +export class UiBranchDropdownItemComponent implements Highlightable { + @Input() branch: BranchDTO; + + @Output() click = new EventEmitter(); + + @Input() + isSelected: boolean; + + isActive = false; + + @Input() + @HostBinding('disabled') + disabled?: boolean; + + constructor(private element: ElementRef) {} + + setActiveStyles(): void { + this.isActive = true; + } + + setInactiveStyles(): void { + this.isActive = false; + } + + scrollIntoView() { + this.element?.nativeElement?.scrollIntoView(); + } +} diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-modal/pick-up-option/pick-up-dropdown/pick-up-dropdown.component.html b/apps/ui/branch-dropdown/src/lib/branch-dropdown.component.html similarity index 63% rename from apps/page/checkout/src/lib/modals/purchasing-options-modal/pick-up-option/pick-up-dropdown/pick-up-dropdown.component.html rename to apps/ui/branch-dropdown/src/lib/branch-dropdown.component.html index 983f3f5d6..921523cee 100644 --- a/apps/page/checkout/src/lib/modals/purchasing-options-modal/pick-up-option/pick-up-dropdown/pick-up-dropdown.component.html +++ b/apps/ui/branch-dropdown/src/lib/branch-dropdown.component.html @@ -1,20 +1,21 @@
-
+
+ diff --git a/apps/page/checkout/src/lib/modals/purchasing-options-modal/pick-up-option/pick-up-dropdown/pick-up-dropdown.component.scss b/apps/ui/branch-dropdown/src/lib/branch-dropdown.component.scss similarity index 73% rename from apps/page/checkout/src/lib/modals/purchasing-options-modal/pick-up-option/pick-up-dropdown/pick-up-dropdown.component.scss rename to apps/ui/branch-dropdown/src/lib/branch-dropdown.component.scss index 23b25f30f..21d6ae977 100644 --- a/apps/page/checkout/src/lib/modals/purchasing-options-modal/pick-up-option/pick-up-dropdown/pick-up-dropdown.component.scss +++ b/apps/ui/branch-dropdown/src/lib/branch-dropdown.component.scss @@ -32,16 +32,11 @@ } .dropdown-wrapper { - @apply relative bg-white m-0 rounded-md p-0 -top-px-40; - left: -70px; + @apply relative bg-white m-0 rounded-md p-0; + left: -30px; max-width: 345px; box-shadow: 0px -2px 24px 0px #dce2e9; - li:hover, - li:hover > button { - @apply bg-disabled-customer; - } - .search-input { @apply flex items-center px-px-15 pt-px-15 pb-px-8; } @@ -63,12 +58,4 @@ @apply px-0 m-0 list-none overflow-x-hidden overflow-y-scroll rounded-b-md; max-height: 175px; } - - li > button { - @apply w-full text-left list-none px-px-15 py-px-8 cursor-pointer whitespace-nowrap overflow-x-hidden overflow-y-scroll overflow-ellipsis text-xl bg-white border-none outline-none; - } - - .selected { - @apply bg-disabled-customer !important; - } } diff --git a/apps/ui/branch-dropdown/src/lib/branch-dropdown.component.ts b/apps/ui/branch-dropdown/src/lib/branch-dropdown.component.ts new file mode 100644 index 000000000..3d6b97eec --- /dev/null +++ b/apps/ui/branch-dropdown/src/lib/branch-dropdown.component.ts @@ -0,0 +1,116 @@ +import { ActiveDescendantKeyManager } from '@angular/cdk/a11y'; +import { Component, EventEmitter, HostListener, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; +import { BranchDTO } from '@swagger/checkout'; +import { geoDistance } from '@utils/common'; +import { UiBranchDropdownItemComponent } from './branch-dropdown-item.component'; + +@Component({ + selector: 'ui-branch-dropdown', + templateUrl: 'branch-dropdown.component.html', + styleUrls: ['branch-dropdown.component.scss'], +}) +export class UiBranchDropdownComponent implements OnChanges { + private _keyManager: ActiveDescendantKeyManager; + + @ViewChildren(UiBranchDropdownItemComponent) set fn(items: QueryList) { + if (!this._keyManager) { + this._keyManager = new ActiveDescendantKeyManager(items).withWrap(true); + } + this._keyManager?.setFirstItemActive(); + } + + @Input() + branches: BranchDTO[]; + + filteredBranches: BranchDTO[]; + + @Input() + selected: string; + + @Output() + selectBranch = new EventEmitter(); + + searchFilter: string; + isOpen = false; + + get activeItem() { + return this._keyManager?.activeItem; + } + + constructor() {} + + open() { + this.isOpen = true; + + this.searchFilter = undefined; + this.setFilteredBranches(); + } + + ngOnChanges({ branches }: SimpleChanges): void { + if (branches) { + this.setFilteredBranches(); + } + } + + @HostListener('keydown', ['$event']) + onKeydown(event: KeyboardEvent) { + if (this.isOpen) { + switch (event.key) { + case 'Enter': + this.setBranch(this.activeItem.branch); + this._keyManager.setActiveItem(-1); + event.preventDefault(); + break; + case 'Escape': + this.isOpen = false; + break; + case 'ArrowUp': + case 'ArrowDown': + this._keyManager.onKeydown(event); + this.activeItem?.scrollIntoView(); + break; + } + } + } + + setBranch(branch: BranchDTO) { + this.isOpen = false; + this.selectBranch.emit(branch); + } + + filter(event: string) { + this.setFilteredBranches(event); + } + + setFilteredBranches(filterValue?: string) { + const branches = [...this.branches]; + const currentBranch = branches.find((b) => b.name === this.selected); + if (!!filterValue) { + const filterResult = branches.filter((b) => { + const name = b.name.toLowerCase(); + const zipCode = b.address?.zipCode; + const city = b.address?.city?.toLowerCase(); + if (!zipCode || !city) { + return name.indexOf(filterValue.toLowerCase()) >= 0; + } else { + return ( + name.indexOf(filterValue.toLowerCase()) >= 0 || + zipCode.indexOf(filterValue) >= 0 || + city.indexOf(filterValue.toLowerCase()) >= 0 + ); + } + }); + this.filteredBranches = filterResult.sort((a: BranchDTO, b: BranchDTO) => this.branchSorterFn(a, b, currentBranch)); + } else { + const filterResult = branches; + this.filteredBranches = filterResult.sort((a: BranchDTO, b: BranchDTO) => this.branchSorterFn(a, b, currentBranch)); + } + } + + private branchSorterFn(a: BranchDTO, b: BranchDTO, userBranch: BranchDTO) { + return ( + geoDistance(userBranch?.address?.geoLocation, a?.address?.geoLocation) - + geoDistance(userBranch?.address?.geoLocation, b?.address?.geoLocation) + ); + } +} diff --git a/apps/ui/branch-dropdown/src/lib/branch-dropdown.module.ts b/apps/ui/branch-dropdown/src/lib/branch-dropdown.module.ts new file mode 100644 index 000000000..e7397ce0a --- /dev/null +++ b/apps/ui/branch-dropdown/src/lib/branch-dropdown.module.ts @@ -0,0 +1,14 @@ +import { OverlayModule } from '@angular/cdk/overlay'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { UiIconModule } from '@ui/icon'; +import { UiBranchDropdownItemComponent } from './branch-dropdown-item.component'; +import { UiBranchDropdownComponent } from './branch-dropdown.component'; + +@NgModule({ + imports: [CommonModule, UiIconModule, FormsModule, OverlayModule], + declarations: [UiBranchDropdownComponent, UiBranchDropdownItemComponent], + exports: [UiBranchDropdownComponent, UiBranchDropdownItemComponent], +}) +export class UiBranchDropdownModule {} diff --git a/apps/ui/branch-dropdown/src/lib/branch-dropdown.service.spec.ts b/apps/ui/branch-dropdown/src/lib/branch-dropdown.service.spec.ts new file mode 100644 index 000000000..52a9bf904 --- /dev/null +++ b/apps/ui/branch-dropdown/src/lib/branch-dropdown.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { BranchDropdownService } from './branch-dropdown.service'; + +describe('BranchDropdownService', () => { + let service: BranchDropdownService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BranchDropdownService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/apps/ui/branch-dropdown/src/lib/branch-dropdown.service.ts b/apps/ui/branch-dropdown/src/lib/branch-dropdown.service.ts new file mode 100644 index 000000000..613834836 --- /dev/null +++ b/apps/ui/branch-dropdown/src/lib/branch-dropdown.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class BranchDropdownService { + constructor() {} +} diff --git a/apps/ui/branch-dropdown/src/public-api.ts b/apps/ui/branch-dropdown/src/public-api.ts new file mode 100644 index 000000000..ab9aadc16 --- /dev/null +++ b/apps/ui/branch-dropdown/src/public-api.ts @@ -0,0 +1,7 @@ +/* + * Public API Surface of branch-dropdown + */ + +export * from './lib/branch-dropdown.service'; +export * from './lib/branch-dropdown.component'; +export * from './lib/branch-dropdown.module'; diff --git a/apps/ui/branch-dropdown/src/test.ts b/apps/ui/branch-dropdown/src/test.ts new file mode 100644 index 000000000..504678a21 --- /dev/null +++ b/apps/ui/branch-dropdown/src/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone'; +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/apps/ui/branch-dropdown/tsconfig.lib.json b/apps/ui/branch-dropdown/tsconfig.lib.json new file mode 100644 index 000000000..775bbc57d --- /dev/null +++ b/apps/ui/branch-dropdown/tsconfig.lib.json @@ -0,0 +1,25 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "../../../out-tsc/lib", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableResourceInlining": true + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/apps/ui/branch-dropdown/tsconfig.lib.prod.json b/apps/ui/branch-dropdown/tsconfig.lib.prod.json new file mode 100644 index 000000000..5615c27df --- /dev/null +++ b/apps/ui/branch-dropdown/tsconfig.lib.prod.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "enableIvy": false + } +} diff --git a/apps/ui/branch-dropdown/tsconfig.spec.json b/apps/ui/branch-dropdown/tsconfig.spec.json new file mode 100644 index 000000000..85392ee8f --- /dev/null +++ b/apps/ui/branch-dropdown/tsconfig.spec.json @@ -0,0 +1,17 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "../../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/apps/ui/branch-dropdown/tslint.json b/apps/ui/branch-dropdown/tslint.json new file mode 100644 index 000000000..3aa2f4c6b --- /dev/null +++ b/apps/ui/branch-dropdown/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "lib", + "camelCase" + ], + "component-selector": [ + true, + "element", + "lib", + "kebab-case" + ] + } +} diff --git a/apps/ui/quantity-dropdown/src/lib/quantity-dropdown-content/quantity-dropdown-content.component.html b/apps/ui/quantity-dropdown/src/lib/quantity-dropdown-content/quantity-dropdown-content.component.html new file mode 100644 index 000000000..45994594d --- /dev/null +++ b/apps/ui/quantity-dropdown/src/lib/quantity-dropdown-content/quantity-dropdown-content.component.html @@ -0,0 +1,23 @@ + +
+ + + + +
+
diff --git a/apps/ui/quantity-dropdown/src/lib/quantity-dropdown-content/quantity-dropdown-content.component.scss b/apps/ui/quantity-dropdown/src/lib/quantity-dropdown-content/quantity-dropdown-content.component.scss new file mode 100644 index 000000000..20b3e23fe --- /dev/null +++ b/apps/ui/quantity-dropdown/src/lib/quantity-dropdown-content/quantity-dropdown-content.component.scss @@ -0,0 +1,93 @@ +.quantity-list { + @apply flex flex-col bg-white rounded-card shadow-card z-dropdown; + + button.quantity-list-item { + @apply flex flex-row justify-between items-center outline-none border-none bg-white px-4 py-2 text-base text-left; + + &:first-child { + @apply rounded-t-card; + } + + &:last-child { + @apply rounded-b-card; + } + + &.selected { + @apply bg-disabled-customer; + } + } +} + +.del { + @apply text-brand text-center; +} + +.r90deg { + @apply transform rotate-90 text-ucla-blue; +} + +.r-90deg { + @apply transform -rotate-90 text-ucla-blue; +} + +.close-btn { + @apply border-none outline-none bg-transparent; +} + +::ng-deep .branch ui-quantity-dropdown button.action:disabled { + @apply text-inactive-branch; +} + +.qp { + @apply w-24; + filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.1)); + border-radius: 3px; + + .triangle { + width: 30px; + z-index: -1; + polygon { + fill: white; + } + } + + /* triangle top right*/ + &.x-position-after.y-position-below { + margin-top: 11px; + .triangle { + @apply absolute; + margin-top: -10px; + right: 2px; + } + } + + /* triangle top left*/ + &.x-position-before.y-position-below { + margin-top: 11px; + .triangle { + @apply absolute; + margin-top: -10px; + left: 2px; + } + } + + /* triangle bottom left*/ + &.x-position-before.y-position-above { + margin-bottom: 11px; + .triangle { + @apply absolute transform rotate-180 origin-center; + margin-bottom: -10px; + left: 2px; + } + } + + /* triangle bottom right*/ + &.x-position-after.y-position-above { + margin-bottom: 11px; + .triangle { + @apply absolute transform rotate-180 origin-center; + margin-bottom: -10px; + right: 2px; + } + } +} diff --git a/apps/ui/quantity-dropdown/src/lib/quantity-dropdown-content/quantity-dropdown-content.component.ts b/apps/ui/quantity-dropdown/src/lib/quantity-dropdown-content/quantity-dropdown-content.component.ts new file mode 100644 index 000000000..62e9f0e52 --- /dev/null +++ b/apps/ui/quantity-dropdown/src/lib/quantity-dropdown-content/quantity-dropdown-content.component.ts @@ -0,0 +1,75 @@ +import { + Component, + ChangeDetectionStrategy, + ViewChild, + ElementRef, + Input, + TemplateRef, + ContentChild, + EventEmitter, + Output, + OnChanges, + SimpleChanges, +} from '@angular/core'; +import { UiOverlayPositionX, UiOverlayPositionY, UiOverlayTrigger } from '@ui/common'; + +@Component({ + selector: 'ui-quantity-dropdown-content', + templateUrl: 'quantity-dropdown-content.component.html', + styleUrls: ['quantity-dropdown-content.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class QuantityDropdownContentComponent implements OnChanges, UiOverlayTrigger { + @ViewChild(TemplateRef) + templateRef: TemplateRef; + + @Input() + quantity: number; + + @Input() range?: number = 999; + rangeArray: Array; + + @Input() showTrash = true; + + @ContentChild('content') + content: ElementRef; + + @Input() + xPosition: UiOverlayPositionX; + + @Input() + yPosition: UiOverlayPositionY; + + @Input() + xOffset: number; + + @Input() + yOffset: number; + + @Output() + quantityChange = new EventEmitter(); + + @Output() + enableCustomInput = new EventEmitter(); + + close = () => {}; + + get classList() { + return ['qp', `x-position-${this.xPosition}`, `y-position-${this.yPosition}`]; + } + + constructor() {} + + ngOnChanges({ range }: SimpleChanges): void { + if (range?.currentValue && range.currentValue <= 9) { + this.rangeArray = Array.from(Array(this.range), (_, i) => i + 1); + } else { + this.rangeArray = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + } + } + + setQuantity(quantity: number) { + this.quantity = quantity; + this.quantityChange.emit(quantity); + } +} diff --git a/apps/ui/quantity-dropdown/src/lib/quantity-dropdown.component.html b/apps/ui/quantity-dropdown/src/lib/quantity-dropdown.component.html index f784e1b7f..1b319726f 100644 --- a/apps/ui/quantity-dropdown/src/lib/quantity-dropdown.component.html +++ b/apps/ui/quantity-dropdown/src/lib/quantity-dropdown.component.html @@ -1,7 +1,6 @@ -
-
+
- @@ -16,23 +15,16 @@ *ngIf="customInput" placeholder="..." /> -
- - - -
+ + +