Compare commits

...

43 Commits

Author SHA1 Message Date
Lorenz Hilpert
ddd1b81d0d Merge branch 'feature/responsive-customer-orders' into feature/responsive-customer-orders-breakpoints 2023-05-24 15:45:20 +02:00
Nino
c79a1cdad1 #2605 Focus Searchbox on Navigation Click - Finish first Version of Article Search Responsive Design - Reset customer Orders back to pre Responsive 2023-05-23 17:56:38 +02:00
Nino
edf978f5cf #3387 Hover Styling on Article Search Result List 2023-05-23 16:52:12 +02:00
Nino
4bfe35a8b9 Bugfix OrderBy Tablet Navigation 2023-05-23 16:07:46 +02:00
Nino
4b7c26b009 Update new Filter and Branch Selector Changes from ISA-Integration 2023-05-22 17:26:15 +02:00
Nino
2a442dde85 Filter Design angepasst 2023-05-17 16:33:11 +02:00
Nino
45bb39f466 Merge branch 'feature/responsive-customer-orders' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into feature/responsive-customer-orders 2023-05-17 16:26:18 +02:00
Nino
b2731432ed #4009 Alle Filter Entfernen eingebaut in Artikelsuche Filter Overlay 2023-05-17 16:25:46 +02:00
Nino Righi
8c424bab43 Merge branch 'feature/responsive-customer-orders' into feature/responsive-customer-orders-breakpoints 2023-05-16 16:33:54 +02:00
Nino Righi
571c748a66 Show Outlet Bugfix 2023-05-16 16:26:14 +02:00
Nino Righi
c104f36255 Updated Breakpoint, set MaxWidth to 1920px 2023-05-16 14:31:28 +02:00
Lorenz Hilpert
672245c467 Breakpoints RD 2023-05-16 10:54:47 +02:00
Nino Righi
0ae92e34c6 Breakpoints Handling Improved 2023-05-15 18:44:03 +02:00
Nino Righi
ce72ce48b2 New Breakpoints Update 2023-05-12 17:48:27 +02:00
Nino Righi
bd5ec27425 Merge branch 'develop' into feature/responsive-customer-orders 2023-05-11 16:54:54 +02:00
Nino Righi
27a83242c5 Filter Adjustment and Bugfix 2023-05-09 18:01:56 +02:00
Nino Righi
7bf23c4bcc Improved Routing and QueryParams handling 2023-05-09 17:34:30 +02:00
Nino Righi
0bc80e12aa #4000 Reset Selected Branch on Shell Navigation click, Product Search and Customer Orders 2023-05-09 14:40:44 +02:00
Nino Righi
b586e3da9e Responsive Design - Tablet Styling Bugfixes and Adjustments - Filter Layout - Container Heights - Routing Bugfixes 2023-05-08 17:09:15 +02:00
Nino Righi
a433f2cfe4 Responsive Design Article Result List Add Item To Cart via Select Bullets 2023-05-05 16:50:21 +02:00
Nino Righi
ff368d68b7 Responsive Design Order by Ipad fix 2023-05-04 15:09:05 +02:00
Nino Righi
40ebd72263 Article Search Results Searchbox Responsive Design and Filter Box Shadow Fix 2023-05-03 17:00:11 +02:00
Nino Righi
11eca1e25a Added new Icons, Fixed Searchbox Autocomplete 2023-05-03 16:20:12 +02:00
Nino Righi
b8d0153232 Filter Responsive Design 2023-05-02 18:08:18 +02:00
Nino Righi
0a195e78e5 OrderBy Responsive Design 2023-05-02 15:37:48 +02:00
Nino Righi
97c26f5fa1 Finished Article Details Styling and Started Filter Responsive Design 2023-04-28 18:24:57 +02:00
Nino Righi
8dac64d5b8 Merge branch 'develop' into feature/responsive-customer-orders 2023-04-28 10:21:46 +02:00
Nino Righi
2c295b0797 Article Details Page Responsive Design Layout and Styling Refactor 2023-04-27 18:31:28 +02:00
Nino Righi
4344f4617c Merge branch 'develop' into feature/responsive-customer-orders 2023-04-27 10:56:36 +02:00
Nino Righi
c451d2b329 Update Styling Catalog Main and Results fullscreen 2023-04-26 18:00:14 +02:00
Nino Righi
96d1a4b826 Page Catalog Layout Changes 2023-04-26 15:55:07 +02:00
Nino Righi
958a388fb5 Updated Navigation Service Implementation, Unit Test Fixes 2023-04-25 17:09:13 +02:00
Nino Righi
a908767f08 Merge branch 'develop' into feature/responsive-customer-orders 2023-04-24 17:41:36 +02:00
Nino Righi
3c0406031a Process, Breadcrumb, Shell Article Search Navigation Update 2023-04-24 16:47:31 +02:00
Nino Righi
79bb9b8c11 Routing and Breadcrumb Update Article Search, Created Navigation Service 2023-04-21 17:12:55 +02:00
Nino Righi
341b202bc4 Kundenbestellungen Zwischencommit 2023-04-20 18:10:00 +02:00
Nino Righi
f284dc1db5 Article Search and Customer Order Filter Navigation Update Based on previous Routes, Updated Routing on Tablet 2023-04-18 17:36:08 +02:00
Nino Righi
2144ec838c Customer Order Splitscreen Navigation Update 2023-04-06 16:12:29 +02:00
Nino Righi
bff10cb2ff Customer Order Changes 2023-04-05 11:04:38 +02:00
Nino Righi
d303b1444b Zoom Update and Article Result List Styling Update 2023-04-03 17:54:08 +02:00
Nino Righi
150e7965ee Zwischencommit 2023-03-29 12:57:05 +02:00
Nino Righi
3fcf3d9396 Responsive Design Splitscreen Article Search Results Update 2023-03-28 15:08:31 +02:00
Nino Righi
52278b8baf Initial Responsive Design Implementation based on Article Search 2023-03-24 17:19:59 +01:00
117 changed files with 2854 additions and 1375 deletions

View File

@@ -1504,5 +1504,8 @@
} }
} }
} }
},
"cli": {
"analytics": false
} }
} }

View File

@@ -22,7 +22,7 @@ export interface Breadcrumb {
/** /**
* Url * Url
*/ */
path: string; path: string | any[];
/** /**
* Query Parameter * Query Parameter

View File

@@ -3,11 +3,17 @@ import { Platform } from '@angular/cdk/platform';
import { NativeContainerService } from 'native-container'; import { NativeContainerService } from 'native-container';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
const MATCH_TABLET = '(max-width: 1023px)'; const MATCH_TABLET = '(max-width: 1024px)';
const MATCH_DESKTOP_SMALL = '(min-width: 1024px and max-width: 1439px)'; const MATCH_DESKTOP_SMALL = '(min-width: 1025px) and (max-width: 1439px)';
const MATCH_DESKTOP = '(min-width: 1440px)'; const MATCH_DESKTOP = '(min-width: 1280px)';
const MATCH_DESKTOP_LARGE = '(min-width: 1440px)';
const MATCH_DESKTOP_X_LARGE = '(min-width: 1920px)';
const MATCH_DESKTOP_XX_LARGE = '(min-width: 2736px)';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -37,6 +43,24 @@ export class EnvironmentService {
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP); matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP);
matchDesktopLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_LARGE);
}
matchDesktopLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_LARGE);
matchDesktopXLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_X_LARGE);
}
matchDesktopXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_X_LARGE);
matchDesktopXXLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_XX_LARGE);
}
matchDesktopXXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_XX_LARGE);
/** /**
* @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead. * @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead.
*/ */

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application'; import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout'; import { DomainCheckoutService } from '@domain/checkout';
import { ProductCatalogNavigationService } from '@shared/services';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
@@ -9,6 +10,7 @@ export class CanActivateProductGuard implements CanActivate {
constructor( constructor(
private readonly _applicationService: ApplicationService, private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService, private readonly _checkoutService: DomainCheckoutService,
private readonly _navigationService: ProductCatalogNavigationService,
private readonly _router: Router private readonly _router: Router
) {} ) {}
@@ -38,7 +40,7 @@ export class CanActivateProductGuard implements CanActivate {
} }
if (!lastActivatedProcessId) { if (!lastActivatedProcessId) {
await this.fromCartProcess(processes, route); await this.fromCartProcess(processes);
return false; return false;
} else { } else {
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)])); await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)]));
@@ -48,7 +50,7 @@ export class CanActivateProductGuard implements CanActivate {
} }
// Bei offener Artikelsuche/Kundensuche und Klick auf Footer Artikelsuche // Bei offener Artikelsuche/Kundensuche und Klick auf Footer Artikelsuche
async fromCartProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot) { async fromCartProcess(processes: ApplicationProcess[]) {
const newProcessId = Date.now(); const newProcessId = Date.now();
await this._applicationService.createProcess({ await this._applicationService.createProcess({
id: newProcessId, id: newProcessId,
@@ -57,7 +59,7 @@ export class CanActivateProductGuard implements CanActivate {
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`, name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
}); });
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(newProcessId)])); await this._navigationService.navigateToProductSearch({ processId: newProcessId });
} }
// Bei offener Warenausgabe und Klick auf Footer Artikelsuche // Bei offener Warenausgabe und Klick auf Footer Artikelsuche

View File

@@ -263,6 +263,16 @@
"name": "event-available", "name": "event-available",
"data": "M438 830 296 688l58-58 84 84 168-168 58 58-226 226ZM200 976q-33 0-56.5-23.5T120 896V336q0-33 23.5-56.5T200 256h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840 336v560q0 33-23.5 56.5T760 976H200Zm0-80h560V496H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Z", "data": "M438 830 296 688l58-58 84 84 168-168 58 58-226 226ZM200 976q-33 0-56.5-23.5T120 896V336q0-33 23.5-56.5T200 256h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840 336v560q0 33-23.5 56.5T760 976H200Zm0-80h560V496H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Z",
"viewBox": "0 96 960 960" "viewBox": "0 96 960 960"
},
{
"name": "remove",
"data": "M200 606v-60h560v60H200Z",
"viewBox": "0 96 960 960"
},
{
"name": "add",
"data": "M450 856V606H200v-60h250V296h60v250h250v60H510v250h-60Z",
"viewBox": "0 96 960 960"
} }
], ],
"aliases": [ "aliases": [

View File

@@ -264,6 +264,16 @@
"name": "event-available", "name": "event-available",
"data": "M438 830 296 688l58-58 84 84 168-168 58 58-226 226ZM200 976q-33 0-56.5-23.5T120 896V336q0-33 23.5-56.5T200 256h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840 336v560q0 33-23.5 56.5T760 976H200Zm0-80h560V496H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Z", "data": "M438 830 296 688l58-58 84 84 168-168 58 58-226 226ZM200 976q-33 0-56.5-23.5T120 896V336q0-33 23.5-56.5T200 256h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840 336v560q0 33-23.5 56.5T760 976H200Zm0-80h560V496H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Z",
"viewBox": "0 96 960 960" "viewBox": "0 96 960 960"
},
{
"name": "remove",
"data": "M200 606v-60h560v60H200Z",
"viewBox": "0 96 960 960"
},
{
"name": "add",
"data": "M450 856V606H200v-60h250V296h60v250h250v60H510v250h-60Z",
"viewBox": "0 96 960 960"
} }
], ],
"aliases": [ "aliases": [

View File

@@ -264,6 +264,16 @@
"name": "event-available", "name": "event-available",
"data": "M438 830 296 688l58-58 84 84 168-168 58 58-226 226ZM200 976q-33 0-56.5-23.5T120 896V336q0-33 23.5-56.5T200 256h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840 336v560q0 33-23.5 56.5T760 976H200Zm0-80h560V496H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Z", "data": "M438 830 296 688l58-58 84 84 168-168 58 58-226 226ZM200 976q-33 0-56.5-23.5T120 896V336q0-33 23.5-56.5T200 256h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840 336v560q0 33-23.5 56.5T760 976H200Zm0-80h560V496H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Z",
"viewBox": "0 96 960 960" "viewBox": "0 96 960 960"
},
{
"name": "remove",
"data": "M200 606v-60h560v60H200Z",
"viewBox": "0 96 960 960"
},
{
"name": "add",
"data": "M450 856V606H200v-60h250V296h60v250h250v60H510v250h-60Z",
"viewBox": "0 96 960 960"
} }
], ],
"aliases": [ "aliases": [

View File

@@ -20,7 +20,7 @@
<div class="page-price-update-item__item-details"> <div class="page-price-update-item__item-details">
<div class="page-price-update-item__item-contributors flex flex-row"> <div class="page-price-update-item__item-contributors flex flex-row">
{{ environment.isTablet() ? (item?.product?.contributors | substr: 42) : item?.product?.contributors }} {{ environment.isTablet() ? (item?.product?.contributors | substr: 38) : item?.product?.contributors }}
</div> </div>
<div <div
@@ -43,7 +43,7 @@
src="assets/images/Icon_{{ item?.product?.format }}.svg" src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail" [alt]="item?.product?.formatDetail"
/> />
{{ item?.product?.formatDetail }} {{ environment.isTablet() ? (item?.product?.formatDetail | substr: 25) : item?.product?.formatDetail }}
</div> </div>
</div> </div>

View File

@@ -1,124 +1,240 @@
<ng-container *ngIf="!showRecommendations"> <ng-container *ngIf="!showRecommendations">
<div #detailsContainer class="product-card"> <div class="page-article-details__container px-5 relative">
<ng-container *ngIf="store.item$ | async; let item"> <ng-container *ngIf="store.item$ | async; let item">
<div class="product-details"> <div class="page-article-details__product-details mb-3">
<div class="product-image"> <div class="page-article-details__product-bookmark justify-self-end">
<button class="image-button" (click)="showImages()"> <div *ngIf="showArchivBadge$ | async" class="archiv-badge">
<img (load)="loadImage()" [src]="item.imageId | productImage: 195:315:true" alt="product image" /> <button [uiOverlayTrigger]="archivTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
<ui-icon *ngIf="imageLoaded$ | async" icon="search_add" size="22px"></ui-icon> <img src="/assets/images/bookmark_benachrichtigung_archiv.svg" alt="Archiv Badge" />
</button> <ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #archivTooltip [closeable]="true">
<ng-container *ngIf="isAvailable$ | async; else notAvailable">
Archivtitel. Wird nicht mehr gedruckt. Artikel ist bestellbar, weil lieferbar.
</ng-container>
<ng-template #notAvailable>
Archivtitel. Wird nicht mehr gedruckt. Nicht bestellbar.
</ng-template>
</ui-tooltip>
</button>
</div>
<div *ngIf="showSubscriptionBadge$ | async">
<button [uiOverlayTrigger]="subscribtionTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
<img src="/assets/images/bookmark_subscription.svg" alt="Fortsetzungsartikel Badge" />
</button>
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #subscribtionTooltip [closeable]="true"
>Artikel ist ein Fortsetzungsartikel,<br />
Artikel muss über eine Aboabteilung<br />
bestellt werden.
</ui-tooltip>
</div>
<div *ngIf="showPromotionBadge$ | async" class="promotion-badge">
<button [uiOverlayTrigger]="promotionTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
<ui-icon-badge icon="gift" alt="Prämienkatalog Badge"></ui-icon-badge>
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #promotionTooltip [closeable]="true">
Dieser Artikel befindet sich im Prämienkatalog.
</ui-tooltip>
</button>
</div>
</div>
<button (click)="showReviews()" class="recessions" *ngIf="item.reviews?.length > 0"> <div class="page-article-details__product-image-recessions flex flex-col items-center">
<div class="page-article-details__product-image">
<button class="border-none outline-none bg-transparent relative" (click)="showImages()">
<img
class="max-h-[19.6875rem] max-w-[12.1875rem] rounded-card"
(load)="loadImage()"
[src]="item.imageId | productImage: 195:315:true"
alt="product image"
/>
<ui-icon
class="absolute text-[#A7B9CB] inline-block bottom-[14px] right-[18px]"
*ngIf="imageLoaded$ | async"
icon="search_add"
size="25px"
></ui-icon>
</button>
</div>
<button
(click)="showReviews()"
class="page-article-details__product-recessions flex flex-col mt-2 items-center bg-transparent border-none outline-none"
*ngIf="item.reviews?.length > 0"
>
<ui-stars [rating]="store.reviewRating$ | async"></ui-stars> <ui-stars [rating]="store.reviewRating$ | async"></ui-stars>
<div class="cta-recessions">{{ item.reviews.length }} Rezensionen</div> <div class="text-regular text-[#0556B4] font-bold">{{ item.reviews.length }} Rezensionen</div>
</button> </button>
</div> </div>
<div class="product-info"> <div class="page-article-details__product-contributors">
<div class="row" [class.bookmark-badge-gap]="isBadgeVisible$ | async"> <a
<div> *ngFor="let contributor of contributors$ | async; let last = last"
<a class="text-[#0556B4] font-semibold no-underline text-base"
*ngFor="let contributor of contributors$ | async; let last = last" [routerLink]="resultsPath"
class="autor" [queryParams]="{ main_qs: contributor, main_author: 'author' }"
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search', 'results']" >
[queryParams]="{ main_qs: contributor, main_author: 'author' }" {{ contributor }}{{ last ? '' : ';' }}
> </a>
{{ contributor }}{{ last ? '' : ';' }} </div>
</a>
<div class="page-article-details__product-print justify-self-end" [class.mt-4]="isBadgeVisible$ | async">
<button class="bg-transparent text-brand font-bold text-lg outline-none border-none p-0" (click)="print()">Drucken</button>
</div>
<div class="page-article-details__product-title text-2xl font-bold mb-6">
{{ item.product?.name }}
</div>
<div class="page-article-details__product-misc flex flex-col mb-4">
<div
class="page-article-details__product-format flex items-center font-bold text-sm"
*ngIf="item?.product?.format && item?.product?.formatDetail"
>
<img
*ngIf="item?.product?.format !== '--'"
class="flex mr-2 h-[1.125rem]"
[src]="'/assets/images/Icon_' + item.product?.format + '.svg'"
[alt]="item.product?.formatDetail"
/>
{{ item.product?.formatDetail }}
</div>
<div class="page-article-details__product-volume" *ngIf="item?.product?.volume">Band/Reihe {{ item?.product?.volume }}</div>
<div class="page-article-details__product-publication">{{ publicationDate$ | async }}</div>
</div>
<div class="page-article-details__product-price-info flex flex-col mb-4">
<div
class="page-article-details__product-price font-bold text-xl self-end"
*ngIf="item.catalogAvailability?.price?.value?.value; else retailPrice"
>
{{ item.catalogAvailability?.price?.value?.value | currency: item.catalogAvailability?.price?.value?.currency:'code' }}
</div>
<ng-template #retailPrice>
<div
class="page-article-details__product-price font-bold text-xl self-end"
*ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability"
>
{{ takeAwayAvailability?.retailPrice?.value?.value | currency: takeAwayAvailability?.retailPrice?.value?.currency:'code' }}
</div> </div>
</ng-template>
<button class="cta-print right" (click)="print()">Drucken</button> <div class="page-article-details__product-points self-end" *ngIf="store.promotionPoints$ | async; let promotionPoints">
</div> {{ promotionPoints }} Lesepunkte
<div class="title">
{{ item.product?.name }}
</div> </div>
<div class="row"> <!-- TODO: Ticket PREISGEBUNDEN -->
<div> <div class="page-article-details__product-price-bound self-end"></div>
<div class="format" *ngIf="item?.product?.format && item?.product?.formatDetail"> </div>
<img
*ngIf="item?.product?.format !== '--'" <div class="page-article-details__product-origin-infos flex flex-col mb-4">
class="format-icon" <div class="page-article-details__product-manufacturer" data-name="product-manufacturer">{{ item.product?.manufacturer }}</div>
[src]="'/assets/images/Icon_' + item.product?.format + '.svg'"
[alt]="item.product?.formatDetail" <div class="page-article-details__product-language" *ngIf="item?.product?.locale" data-name="product-language">
/> {{ item?.product?.locale }}
{{ item.product?.formatDetail }} </div>
</div>
<div class="page-article-details__product-stock flex justify-end items-center">
<div class="h-5 w-16 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]" *ngIf="store.fetchingTakeAwayAvailability$ | async"></div>
<div
class="flex flex-row py-4 pl-4"
[uiOverlayTrigger]="tooltip"
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
*ngIf="!(store.fetchingTakeAwayAvailability$ | async)"
>
<ng-container *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
<ui-icon class="mr-2 mb-1" icon="home" size="15px"></ui-icon>
<span class="font-bold text-sm">{{ takeAwayAvailability.inStock || 0 }}x</span>
</ng-container>
</div>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
<div class="page-article-details__product-ean-specs flex flex-col">
<div class="page-article-details__product-ean" data-name="product-ean">{{ item.product?.ean }}</div>
<div class="page-article-details__product-specs">
<ng-container *ngIf="item?.specs?.length > 0">
{{ (item?.specs)[0]?.value }}
</ng-container>
</div>
</div>
<div class="page-article-details__product-availabilities flex flex-row items-center justify-end mt-4">
<div
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
*ngIf="store.fetchingTakeAwayAvailability$ | async; else showAvailabilityTakeAwayIcon"
></div>
<ng-template #showAvailabilityTakeAwayIcon>
<div
*ngIf="store.isTakeAwayAvailabilityAvailable$ | async"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center"
>
<ui-icon class="mx-1" icon="shopping_bag" size="18px"> </ui-icon>
</div>
</ng-template>
<div
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
*ngIf="store.fetchingPickUpAvailability$ | async; else showAvailabilityPickUpIcon"
></div>
<ng-template #showAvailabilityPickUpIcon>
<div
*ngIf="store.isPickUpAvailabilityAvailable$ | async"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
>
<ui-icon class="mx-1" icon="box_out" size="18px"></ui-icon>
</div>
</ng-template>
<div
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
*ngIf="store.fetchingDeliveryAvailability$ | async; else showAvailabilityDeliveryIcon"
></div>
<ng-template #showAvailabilityDeliveryIcon>
<div
*ngIf="showDeliveryTruck$ | async"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
>
<ui-icon class="-mb-px-5 -mt-px-5 mx-1" icon="truck" size="30px"></ui-icon>
</div>
</ng-template>
<div
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
*ngIf="store.fetchingDeliveryB2BAvailability$ | async; else showAvailabilityDeliveryB2BIcon"
></div>
<ng-template #showAvailabilityDeliveryB2BIcon>
<div
*ngIf="showDeliveryB2BTruck$ | async"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
>
<ui-icon class="-mb-px-10 -mt-px-10 mx-1" icon="truck_b2b" size="30px"> </ui-icon>
</div>
</ng-template>
<span *ngIf="store.isDownload$ | async" class="flex flex-row items-center">
<div class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3">
<ui-icon class="mx-1" icon="download" size="18px"></ui-icon>
<span class="font-bold">Download</span>
</div>
</span>
</div>
<div class="page-article-details__shelf-ssc">
<div class="page-article-details__ssc flex justify-end my-2 font-bold text-lg">
<div class="w-52 h-px-20 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]" *ngIf="fetchingAvailabilities$ | async"></div>
<ng-container *ngIf="!(fetchingAvailabilities$ | async)">
<div *ngIf="store.sscText$ | async; let sscText">
{{ sscText }}
</div> </div>
<div *ngIf="item?.product?.volume">Band/Reihe {{ item?.product?.volume }}</div> </ng-container>
<div>{{ publicationDate$ | async }}</div>
</div>
<div class="right">
<div class="price" *ngIf="item.catalogAvailability?.price?.value?.value; else retailPrice">
{{ item.catalogAvailability?.price?.value?.value | currency: item.catalogAvailability?.price?.value?.currency:'code' }}
</div>
<ng-template #retailPrice>
<div class="price" *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
{{
takeAwayAvailability?.retailPrice?.value?.value | currency: takeAwayAvailability?.retailPrice?.value?.currency:'code'
}}
</div>
</ng-template>
<div *ngIf="store.promotionPoints$ | async; let promotionPoints">{{ promotionPoints }} Lesepunkte</div>
</div>
</div> </div>
<div class="row stock"> <div class="page-article-details__shelfinfo" *ngIf="store.isDownload$ | async">
<div data-name="product-manufacturer">{{ item.product?.manufacturer }}</div>
<div class="right quantity" [uiOverlayTrigger]="tooltip" [overlayTriggerDisabled]="!(stockTooltipText$ | async)">
<div class="fetching small" *ngIf="store.fetchingTakeAwayAvailability$ | async"></div>
<ng-container *ngIf="!(store.fetchingTakeAwayAvailability$ | async)">
<ng-container *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
<ui-icon icon="home" size="22px"></ui-icon>
{{ takeAwayAvailability.inStock || 0 }}x
</ng-container>
</ng-container>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
</div>
<div *ngIf="item?.product?.locale" data-name="product-language">{{ item?.product?.locale }}</div>
<div class="row">
<div data-name="product-ean">{{ item.product?.ean }}</div>
<div class="right">
<div class="availability-icons">
<div class="fetching xsmall" *ngIf="store.fetchingTakeAwayAvailability$ | async; else showAvailabilityTakeAwayIcon"></div>
<ng-template #showAvailabilityTakeAwayIcon>
<ui-icon *ngIf="store.isTakeAwayAvailabilityAvailable$ | async" icon="shopping_bag" size="18px"> </ui-icon>
</ng-template>
<div class="fetching xsmall" *ngIf="store.fetchingPickUpAvailability$ | async; else showAvailabilityPickUpIcon"></div>
<ng-template #showAvailabilityPickUpIcon>
<ui-icon *ngIf="store.isPickUpAvailabilityAvailable$ | async" icon="box_out" size="18px"></ui-icon>
</ng-template>
<div class="fetching xsmall" *ngIf="store.fetchingDeliveryAvailability$ | async; else showAvailabilityDeliveryIcon"></div>
<ng-template #showAvailabilityDeliveryIcon>
<ui-icon *ngIf="showDeliveryTruck$ | async" class="truck" icon="truck" size="30px"></ui-icon>
</ng-template>
<div
class="fetching xsmall"
*ngIf="store.fetchingDeliveryB2BAvailability$ | async; else showAvailabilityDeliveryB2BIcon"
></div>
<ng-template #showAvailabilityDeliveryB2BIcon>
<ui-icon *ngIf="showDeliveryB2BTruck$ | async" class="truck_b2b" icon="truck_b2b" size="40px"> </ui-icon>
</ng-template>
<span *ngIf="store.isDownload$ | async" class="download-icon">
<ui-icon icon="download" size="18px"></ui-icon>
<span class="label">Download</span>
</span>
</div>
</div>
</div>
<div class="shelfinfo right" *ngIf="store.isDownload$ | async">
<ng-container <ng-container
*ngIf=" *ngIf="
item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label; item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label;
@@ -145,24 +261,7 @@
</ng-container> </ng-container>
</ng-template> </ng-template>
</div> </div>
<div class="page-article-details__shelfinfo text-right" *ngIf="!(store.isDownload$ | async)">
<div class="row">
<div class="specs">
<ng-container *ngIf="item?.specs?.length > 0">
{{ (item?.specs)[0]?.value }}
</ng-container>
</div>
<div class="right ssc">
<div class="fetching" *ngIf="fetchingAvailabilities$ | async"></div>
<ng-container *ngIf="!(fetchingAvailabilities$ | async)">
<div *ngIf="store.sscText$ | async; let sscText">
{{ sscText }}
</div>
</ng-container>
</div>
</div>
<div class="shelfinfo right" *ngIf="!(store.isDownload$ | async)">
<ng-container <ng-container
*ngIf=" *ngIf="
item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label; item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label;
@@ -186,109 +285,115 @@
</ng-template> </ng-template>
</div> </div>
</div> </div>
</div>
<div class="bookmark"> <div class="page-article-details__product-formats-container mt-3" *ngIf="item.family?.length > 0">
<div *ngIf="showArchivBadge$ | async" class="archiv-badge"> <hr class="bg-[#E6EFF9] border-t-2" />
<button [uiOverlayTrigger]="archivTooltip" class="bookmark-badge"> <div class="pt-3">
<img src="/assets/images/bookmark_benachrichtigung_archiv.svg" alt="Archiv Badge" /> <div class="page-article-details__product-formats">
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #archivTooltip [closeable]="true"> <span class="mr-2">Auch verfügbar als</span>
<ng-container *ngIf="isAvailable$ | async; else notAvailable">
Archivtitel. Wird nicht mehr gedruckt. Artikel ist bestellbar, weil lieferbar. <ui-slider [scrollDistance]="250">
</ng-container> <a
<ng-template #notAvailable> class="mr-4 text-[#0556B4] font-bold no-underline px-2"
Archivtitel. Wird nicht mehr gedruckt. Nicht bestellbar. *ngFor="let format of item.family"
</ng-template> [routerLink]="getDetailsPath(format.product.ean)"
</ui-tooltip> [queryParamsHandling]="!(isTablet$ | async) ? 'preserve' : ''"
</button> >
</div> <span class="flex items-center">
<div *ngIf="showSubscriptionBadge$ | async"> <img
<button [uiOverlayTrigger]="subscribtionTooltip" class="bookmark-badge"> class="mr-2"
<img src="/assets/images/bookmark_subscription.svg" alt="Fortsetzungsartikel Badge" /> *ngIf="!!format.product?.format"
</button> [src]="'/assets/images/OF_Icon_' + format.product?.format + '.svg'"
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #subscribtionTooltip [closeable]="true" alt="format icon"
>Artikel ist ein Fortsetzungsartikel,<br /> />
Artikel muss über eine Aboabteilung<br /> {{ format.product?.formatDetail }}
bestellt werden. <span class="ml-1">{{ format.catalogAvailability?.price?.value?.value | currency: '€' }}</span>
</ui-tooltip> </span>
</div> </a>
<div *ngIf="showPromotionBadge$ | async" class="promotion-badge"> </ui-slider>
<button [uiOverlayTrigger]="promotionTooltip" class="bookmark-badge">
<ui-icon-badge icon="gift" alt="Prämienkatalog Badge"></ui-icon-badge>
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #promotionTooltip [closeable]="true">
Dieser Artikel befindet sich im Prämienkatalog.
</ui-tooltip>
</button>
</div> </div>
</div> </div>
</div> </div>
<div class="product-actions"> <hr class="bg-[#E6EFF9] border-t-2 my-3" />
<button *ngIf="!(store.isDownload$ | async)" class="cta-availabilities" (click)="showAvailabilities()">
Vorrätig in anderer Filiale?
</button>
<button
class="cta-continue"
(click)="showPurchasingModal()"
[disabled]="
!(isAvailable$ | async) || (fetchingAvailabilities$ | async) || (item?.features && (item?.features)[0]?.key === 'PFO')
"
>
In den Warenkorb
</button>
</div>
<hr /> <div
<ng-container *ngIf="item.family?.length > 0"> #description
<div class="product-formats"> class="page-article-details__product-description flex flex-col flex-grow overflow-hidden overflow-y-scroll"
<span class="label">Auch verfügbar als</span> *ngIf="item.texts?.length > 0"
>
<ui-slider [scrollDistance]="250"> <div class="whitespace-pre-line">
<a
class="product-family"
*ngFor="let format of item.family"
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'details', 'ean', format.product.ean]"
>
<span class="format-detail">
<img
*ngIf="!!format.product?.format"
[src]="'/assets/images/OF_Icon_' + format.product?.format + '.svg'"
alt="format icon"
/>
{{ format.product?.formatDetail }}
<span class="price">{{ format.catalogAvailability?.price?.value?.value | currency: '€' }}</span>
</span>
</a>
</ui-slider>
</div>
<hr />
</ng-container>
<div class="product-description" *ngIf="item.texts?.length > 0">
<div class="info">
{{ item.texts[0].value }} {{ item.texts[0].value }}
</div> </div>
<div class="product-text"> <button class="font-bold flex flex-row text-[#0556B4] items-center mt-2" *ngIf="!showMore" (click)="showMore = !showMore">
Mehr <ui-icon class="ml-2" size="15px" icon="arrow"></ui-icon>
</button>
<div
*ngIf="showMore"
class="page-article-details__product-description-text flex flex-col whitespace-pre-line mb-px-100 break-words"
>
<span *ngFor="let text of item.texts | slice: 1"> <span *ngFor="let text of item.texts | slice: 1">
<h3 class="header">{{ text.label }}</h3> <h3 class="my-4 text-regular font-bold">{{ text.label }}</h3>
{{ text.value }} {{ text.value }}
</span> </span>
<button class="scroll-top-cta" (click)="scrollTop()">
<ui-icon class="arrow" icon="arrow" size="20px"></ui-icon> <button class="font-bold flex flex-row text-[#0556B4] items-center mt-2" (click)="showMore = !showMore">
<ui-icon class="transform ml-0 mr-2 rotate-180" size="15px" icon="arrow"></ui-icon> Weniger
</button>
<button class="page-article-details__scroll-top-cta" (click)="scrollTop(description)">
<ui-icon class="text-[#0556B4]" icon="arrow" size="20px"></ui-icon>
</button> </button>
</div> </div>
</div> </div>
<ng-container *ngIf="!showRecommendations">
<div
*ngIf="store.item$ | async; let item"
class="page-article-details__actions w-full absolute text-center left-0 bottom-10 z-fixed"
>
<button
*ngIf="!(store.isDownload$ | async)"
class="text-brand border-2 border-brand bg-white font-bold text-lg px-[1.375rem] py-4 rounded-full mr-px-30"
(click)="showAvailabilities()"
>
Bestände in anderen Filialen
</button>
<button
class="text-white bg-brand border-brand font-bold text-lg px-[1.375rem] py-4 rounded-full border-none no-underline"
(click)="showPurchasingModal()"
[disabled]="
!(isAvailable$ | async) || (fetchingAvailabilities$ | async) || (item?.features && (item?.features)[0]?.key === 'PFO')
"
>
In den Warenkorb
</button>
</div>
</ng-container>
<div class="page-article-details__product-recommendations -mx-5">
<button
*ngIf="store.item$ | async; let item"
class="shadow-[#dce2e9_0px_-2px_18px_0px] sticky bottom-4 border-none outline-none left-0 right-0 flex items-center px-5 h-14 min-h-[3.5rem] bg-white w-full"
(click)="showRecommendations = true"
>
<span class="uppercase text-[#0556B4] font-bold text-small">Empfehlungen</span>
<img class="absolute right-5 bottom-3 h-12" src="assets/images/recommendation_tag.png" alt="recommendation icon" />
</button>
</div>
</ng-container> </ng-container>
</div> </div>
<button *ngIf="store.item$ | async; let item" class="product-recommendations" (click)="showRecommendations = true">
<span class="label">Empfehlungen</span>
<img src="assets/images/recommendation_tag.png" alt="recommendation icon" />
</button>
</ng-container> </ng-container>
<div class="recommendations-overlay" @slideYAnimation *ngIf="showRecommendations"> <div class="page-article-details__recommendations-overlay absolute top-16 rounded-t-card" @slideYAnimation *ngIf="showRecommendations">
<button class="product-button" (click)="showRecommendations = false">{{ (store.item$ | async)?.product?.name }}</button> <button
class="h-[3.75rem] shadow-[0_-2px_24px_0_#dce2e9] flex flex-row justify-center items-center w-full text-xl bg-white text-ucla-blue font-bold border-none outline-none rounded-t-card"
(click)="showRecommendations = false"
>
{{ (store.item$ | async)?.product?.name }}
</button>
<page-article-recommendations (close)="showRecommendations = false"></page-article-recommendations> <page-article-recommendations (close)="showRecommendations = false"></page-article-recommendations>
</div> </div>

View File

@@ -1,269 +1,99 @@
:host { :host {
@apply flex flex-col; @apply box-border block h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
} }
.product-card { .page-article-details__container {
@apply flex flex-col bg-white w-full rounded-card shadow-card; @apply h-full w-full bg-white rounded-card shadow-card flex flex-col;
.product-details {
@apply flex flex-row p-5;
.bookmark {
@apply absolute flex;
top: 52px;
right: 25px;
z-index: 100;
}
.bookmark-badge {
@apply p-0 m-0 outline-none border-none bg-transparent relative;
}
.promotion-badge {
margin-top: -1px;
}
.bookmark-badge-gap {
@apply mt-px-35;
}
.product-image {
@apply flex flex-col items-center justify-start mr-5;
.recessions {
@apply flex flex-col items-center mt-4 bg-transparent border-none outline-none;
.cta-recessions {
@apply text-regular text-dark-cerulean font-bold mt-2;
}
}
.image-button {
@apply border-none outline-none bg-transparent relative;
ui-icon {
@apply absolute text-dark-cerulean inline-block;
bottom: 1rem;
right: 1rem;
}
}
img {
@apply rounded-xl shadow-card;
box-shadow: 0 0 18px 0 #b8b3b7;
max-height: 315px;
max-width: 195px;
}
}
.product-info {
@apply w-full;
.title {
@apply text-3xl font-bold mb-6;
}
.format,
.ssc,
.quantity {
@apply font-bold text-lg;
}
.stock {
min-height: 44px;
}
.quantity {
@apply flex justify-end mt-4;
ui-icon {
@apply mr-1 text-ucla-blue;
}
}
.format {
@apply flex items-center;
.format-icon {
@apply flex mr-2;
height: 18px;
}
}
.ssc {
@apply flex justify-end my-2;
}
.price {
@apply font-bold text-xl;
}
.shelfinfo {
@apply text-ucla-blue;
}
.fetching {
@apply w-52 h-px-20;
background-color: #e6eff9;
animation: load 0.75s linear infinite;
}
.xsmall {
@apply w-6;
}
.small {
@apply w-16;
}
.medium {
@apply w-40;
}
.availability-icons {
@apply flex flex-row items-center justify-end text-dark-cerulean mt-4;
ui-icon {
@apply mx-1;
}
.truck {
@apply -mb-px-5 -mt-px-5;
}
.truck_b2b {
@apply -mb-px-10 -mt-px-10;
}
.label {
@apply font-bold;
}
.download-icon {
@apply flex flex-row items-center;
}
}
.cta-print {
@apply bg-transparent text-brand font-bold text-xl outline-none border-none p-0;
}
}
.row {
@apply grid items-end;
grid-template-columns: auto auto;
}
}
.right {
@apply text-right self-start;
}
hr {
@apply bg-glitter h-1;
}
.product-description {
@apply flex flex-col flex-grow px-5 py-5;
min-height: calc(100vh - 769px);
.info {
@apply whitespace-pre-line;
}
.product-text {
@apply flex flex-col whitespace-pre-line mb-px-100 break-words;
h3 {
@apply my-4;
}
.header {
@apply text-regular font-bold;
}
.scroll-top-cta {
@apply flex items-center justify-center self-end border-none outline-none bg-white relative rounded p-0 mt-8 mr-4;
box-shadow: 0px 0px 20px 0px rgba(89, 100, 112, 0.5);
transform: rotate(-90deg);
border-radius: 100%;
width: 58px;
height: 58px;
.arrow {
color: #1f466c;
}
}
}
}
.product-actions {
@apply text-right px-5 py-4;
.cta-availabilities {
@apply text-brand border-none border-brand bg-white font-bold text-lg px-4 py-2 rounded-full;
}
.cta-continue {
@apply text-white bg-brand font-bold text-lg px-4 py-2 rounded-full border-none ml-4 no-underline;
&:disabled {
@apply bg-inactive-branch;
}
}
}
.product-formats {
@apply grid whitespace-nowrap items-center px-5 py-4;
grid-template-rows: auto;
grid-template-columns: auto 1fr;
max-width: 100%;
.label {
@apply mr-2;
}
.product-family {
@apply mr-4 text-active-customer font-bold no-underline px-2;
.format-detail {
@apply flex items-center;
img {
@apply mr-2;
}
}
.price {
@apply ml-1;
}
}
}
} }
.product-recommendations { .page-article-details__product-details {
@apply sticky bottom-0 border-none outline-none left-0 right-0 flex items-center px-5 h-16 bg-white w-full; @apply grid gap-x-5;
box-shadow: #dce2e9 0px -2px 18px 0px; grid-template-columns: max-content auto;
grid-template-rows: 2.1875rem repeat(11, minmax(auto, max-content));
.label { grid-template-areas:
@apply uppercase text-active-customer font-bold text-small; '. . . bookmark'
} 'image contributors contributors contributors'
'image title title print'
img { 'image title title .'
@apply absolute right-5 bottom-5 h-12; 'image misc misc price'
} 'image misc misc price'
'image origin origin stock'
'image origin origin stock'
'image specs availabilities availabilities'
'image specs ssc ssc'
'image . ssc ssc'
'image . ssc ssc';
} }
.recommendations-overlay { .page-article-details__product-bookmark {
@apply absolute w-full top-0 rounded-t-card; grid-area: bookmark;
top: 56px; }
.product-button { .page-article-details__product-image-recessions {
@apply flex flex-row justify-center items-center w-full text-xl bg-white text-ucla-blue font-bold border-none outline-none rounded-t-card; grid-area: image;
box-shadow: 0 -2px 24px 0 #dce2e9; }
height: 60px;
.page-article-details__product-contributors {
grid-area: contributors;
}
.page-article-details__product-print {
grid-area: print;
}
.page-article-details__product-title {
grid-area: title;
}
.page-article-details__product-misc {
grid-area: misc;
}
.page-article-details__product-price-info {
grid-area: price;
}
.page-article-details__product-origin-infos {
grid-area: origin;
}
.page-article-details__product-stock {
grid-area: stock;
}
.page-article-details__product-ean-specs {
grid-area: specs;
}
.page-article-details__product-availabilities {
grid-area: availabilities;
}
.page-article-details__shelf-ssc {
grid-area: ssc;
}
.page-article-details__product-description-text {
word-break: break-word;
}
.page-article-details__product-formats {
@apply grid whitespace-nowrap items-center max-w-full;
grid-template-rows: auto;
grid-template-columns: auto 1fr;
}
.page-article-details__scroll-top-cta {
@apply flex items-center justify-center self-end border-none outline-none bg-white relative rounded p-0 mt-8 mr-4;
box-shadow: 0px 0px 20px 0px rgba(89, 100, 112, 0.5);
transform: rotate(-90deg);
border-radius: 100%;
width: 58px;
height: 58px;
}
.page-article-details__actions {
&:disabled {
@apply bg-inactive-branch;
} }
} }
.autor {
@apply text-active-customer font-bold no-underline;
}

View File

@@ -8,7 +8,7 @@ import { BranchDTO } from '@swagger/checkout';
import { UiModalService } from '@ui/modal'; import { UiModalService } from '@ui/modal';
import { ModalReviewsComponent } from '@modal/reviews'; import { ModalReviewsComponent } from '@modal/reviews';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, filter, first, map, shareReplay, switchMap } from 'rxjs/operators'; import { debounceTime, filter, first, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';
import { ArticleDetailsStore } from './article-details.store'; import { ArticleDetailsStore } from './article-details.store';
import { ModalImagesComponent } from 'apps/modal/images/src/public-api'; import { ModalImagesComponent } from 'apps/modal/images/src/public-api';
import { ProductImageService } from 'apps/cdn/product-image/src/public-api'; import { ProductImageService } from 'apps/cdn/product-image/src/public-api';
@@ -20,6 +20,8 @@ import { DateAdapter } from '@ui/common';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal'; import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
import { DomainAvailabilityService } from '@domain/availability'; import { DomainAvailabilityService } from '@domain/availability';
import { EnvironmentService } from '@core/environment';
import { ProductCatalogNavigationService } from '@shared/services';
@Component({ @Component({
selector: 'page-article-details', selector: 'page-article-details',
@@ -113,6 +115,19 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
shareReplay(1) shareReplay(1)
); );
get isTablet$() {
return this._environment.matchTablet$.pipe(
map((state) => state?.matches),
shareReplay()
);
}
get resultsPath() {
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId);
}
showMore: boolean = false;
constructor( constructor(
public readonly applicationService: ApplicationService, public readonly applicationService: ApplicationService,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
@@ -126,6 +141,8 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
public elementRef: ElementRef, public elementRef: ElementRef,
private _purchaseOptionsModalService: PurchaseOptionsModalService, private _purchaseOptionsModalService: PurchaseOptionsModalService,
private _availability: DomainAvailabilityService, private _availability: DomainAvailabilityService,
private _navigationService: ProductCatalogNavigationService,
private _environment: EnvironmentService,
private _router: Router private _router: Router
) {} ) {}
@@ -161,16 +178,30 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
filter((f) => !!f) filter((f) => !!f)
); );
const more$ = this.activatedRoute.params.subscribe(() => (this.showMore = false));
this.subscriptions.add(processIdSubscription); this.subscriptions.add(processIdSubscription);
this.subscriptions.add(more$);
this.subscriptions.add(this.store.loadItemById(id$)); this.subscriptions.add(this.store.loadItemById(id$));
this.subscriptions.add(this.store.loadItemByEan(ean$)); this.subscriptions.add(this.store.loadItemByEan(ean$));
this.subscriptions.add(this.store.item$.pipe(filter((item) => !!item)).subscribe((item) => this.updateBreadcrumb(item))); this.subscriptions.add(
this.store.item$
.pipe(
withLatestFrom(this.isTablet$),
filter(([item, isTablet]) => !!item)
)
.subscribe(([item, isTablet]) => (isTablet ? this.updateBreadcrumb(item) : this.updateBreadcrumbDesktop(item)))
);
} }
ngOnDestroy() { ngOnDestroy() {
this.subscriptions.unsubscribe(); this.subscriptions.unsubscribe();
} }
getDetailsPath(ean?: string) {
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, ean });
}
async updateBreadcrumb(item: ItemDTO) { async updateBreadcrumb(item: ItemDTO) {
const crumbs = await this.breadcrumb const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details', `${item.id}`]) .getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details', `${item.id}`])
@@ -184,13 +215,41 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
this.breadcrumb.addBreadcrumbIfNotExists({ this.breadcrumb.addBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId, key: this.applicationService.activatedProcessId,
name: item.product?.name, name: item.product?.name,
path: `/kunde/${this.applicationService.activatedProcessId}/product/details/${item.id}`, path: this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: item.id }),
params: this.activatedRoute.snapshot.queryParams, params: this.activatedRoute.snapshot.queryParams,
tags: ['catalog', 'details', `${item.id}`], tags: ['catalog', 'details', `${item.id}`],
section: 'customer', section: 'customer',
}); });
} }
async updateBreadcrumbDesktop(item: ItemDTO) {
const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details'])
.pipe(first())
.toPromise();
if (crumbs.length === 0) {
this.breadcrumb.addBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
name: item.product?.name,
path: this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: item.id }),
params: this.activatedRoute.snapshot.queryParams,
tags: ['catalog', 'details', `${item.id}`],
section: 'customer',
});
} else {
const crumb = crumbs.find((_) => true);
this.breadcrumb.patchBreadcrumb(crumb.id, {
key: this.applicationService.activatedProcessId,
name: item.product?.name,
path: this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: item.id }),
params: this.activatedRoute.snapshot.queryParams,
tags: ['catalog', 'details', `${item.id}`],
section: 'customer',
});
}
}
async print() { async print() {
const item = await this.store.item$.pipe(first()).toPromise(); const item = await this.store.item$.pipe(first()).toPromise();
this.uiModal.open({ this.uiModal.open({
@@ -302,9 +361,8 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
} }
} }
scrollTop() { scrollTop(div: HTMLDivElement) {
const element = this.elementRef.nativeElement.closest('.main-wrapper'); div?.scrollTo({ top: 0, behavior: 'smooth' });
element?.scrollTo({ top: 0, behavior: 'smooth' });
} }
loadImage() { loadImage() {

View File

@@ -1,7 +1,6 @@
:host { :host {
@apply flex flex-col bg-white; @apply flex flex-col bg-white h-[calc(100vh-16.5rem-3.75rem)] desktop-small:h-[calc(100vh-15.1rem-3.75rem)];
box-shadow: 0px -2px 24px 0px #dce2e9; box-shadow: 0px -2px 24px 0px #dce2e9;
height: calc(100vh - 342px);
} }
h1 { h1 {

View File

@@ -1,13 +1 @@
<button class="filter" [class.active]="hasFilter$ | async" (click)="filterActive$.next(true); shellFilterOverlay.open()">
<ui-icon size="20px" icon="filter_alit"></ui-icon>
<span class="label">Filter</span>
</button>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<shell-filter-overlay #shellFilterOverlay>
<page-article-search-filter
*ngIf="filterActive$ | async"
(close)="filterActive$.next(false); shellFilterOverlay.close()"
></page-article-search-filter>
</shell-filter-overlay>

View File

@@ -1,17 +1,3 @@
:host { :host {
@apply flex flex-col w-full box-content relative; @apply flex flex-col w-full h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)] box-content relative;
}
.filter {
@apply font-sans flex self-end items-center mb-4 font-bold bg-wild-blue-yonder border-0 text-regular py-px-8 px-px-15 rounded-filter justify-center z-sticky;
width: 106px;
min-width: 106px;
.label {
@apply ml-px-5;
}
&.active {
@apply bg-active-customer text-white ml-px-5;
}
} }

View File

@@ -1,13 +1,15 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb'; import { BreadcrumbService } from '@core/breadcrumb';
import { UiFilterAutocompleteProvider } from '@ui/filter'; import { Observable, Subject } from 'rxjs';
import { isEqual } from 'lodash'; import { map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { filter, first, map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { ArticleSearchService } from './article-search.store'; import { ArticleSearchService } from './article-search.store';
import { FocusSearchboxEvent } from './focus-searchbox.event'; import { FocusSearchboxEvent } from './focus-searchbox.event';
import { ArticleSearchMainAutocompleteProvider } from './providers'; import { ArticleSearchMainAutocompleteProvider } from './providers';
import { ProductCatalogNavigationService } from '@shared/services';
import { FilterAutocompleteProvider } from 'apps/shared/components/filter/src/lib';
import { isEqual } from 'lodash';
import { EnvironmentService } from '@core/environment';
@Component({ @Component({
selector: 'page-article-search', selector: 'page-article-search',
@@ -15,9 +17,8 @@ import { ArticleSearchMainAutocompleteProvider } from './providers';
styleUrls: ['article-search.component.scss'], styleUrls: ['article-search.component.scss'],
providers: [ providers: [
FocusSearchboxEvent, FocusSearchboxEvent,
ArticleSearchService,
{ {
provide: UiFilterAutocompleteProvider, provide: FilterAutocompleteProvider,
useClass: ArticleSearchMainAutocompleteProvider, useClass: ArticleSearchMainAutocompleteProvider,
multi: true, multi: true,
}, },
@@ -28,22 +29,16 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
private _onDestroy$ = new Subject(); private _onDestroy$ = new Subject();
private _processId$: Observable<number>; private _processId$: Observable<number>;
initialFilter$ = this._articleSearch.filter$.pipe( get isTablet() {
filter((filter) => !!filter), return this._environmentService.matchTablet();
first() }
);
hasFilter$ = this._articleSearch.filter$.pipe(
withLatestFrom(this.initialFilter$),
map(([filter, initialFilter]) => !isEqual(filter?.getQueryParams(), initialFilter?.getQueryParams()))
);
filterActive$ = new BehaviorSubject<boolean>(false);
constructor( constructor(
private _breadcrumb: BreadcrumbService, private _breadcrumb: BreadcrumbService,
private _router: Router,
private _articleSearch: ArticleSearchService, private _articleSearch: ArticleSearchService,
private _activatedRoute: ActivatedRoute private _activatedRoute: ActivatedRoute,
private _navigationService: ProductCatalogNavigationService,
private _environmentService: EnvironmentService
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -60,12 +55,17 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$)) .pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$))
.subscribe(([state, processId]) => { .subscribe(([state, processId]) => {
if (state.searchState === '') { if (state.searchState === '') {
const params = state.filter.getQueryParams();
if (state.hits === 1) { if (state.hits === 1) {
const item = state.items.find((f) => f); const item = state.items.find((f) => f);
this._router.navigate(['/kunde', processId, 'product', 'details', item.id]); this._navigationService.navigateToDetails({
processId,
itemId: item.id,
queryParams: this.isTablet ? undefined : params,
});
} else { } else {
const params = state.filter.getQueryParams(); this._navigationService.navigateToResults({
this._router.navigate(['/kunde', processId, 'product', 'search', 'results'], { processId,
queryParams: params, queryParams: params,
}); });
} }
@@ -73,6 +73,20 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
}); });
} }
cleanupQueryParams(params: Record<string, string> = {}) {
const clean = { ...params };
for (const key in clean) {
if (Object.prototype.hasOwnProperty.call(clean, key)) {
if (clean[key] == undefined) {
delete clean[key];
}
}
}
return clean;
}
initProcessId() { initProcessId() {
this._processId$ = this._activatedRoute.parent.data.pipe(map((data) => Number(data.processId))); this._processId$ = this._activatedRoute.parent.data.pipe(map((data) => Number(data.processId)));
} }
@@ -96,7 +110,7 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
await this._breadcrumb.addBreadcrumbIfNotExists({ await this._breadcrumb.addBreadcrumbIfNotExists({
key: processId, key: processId,
name: 'Artikelsuche', name: 'Artikelsuche',
path: `/kunde/${processId}/product`, path: this._navigationService.getArticleSearchBasePath(processId),
params: queryParams, params: queryParams,
tags: ['catalog', 'main'], tags: ['catalog', 'main'],
section: 'customer', section: 'customer',

View File

@@ -6,12 +6,13 @@ import { ArticleSearchComponent } from './article-search.component';
import { SearchResultsModule } from './search-results/search-results.module'; import { SearchResultsModule } from './search-results/search-results.module';
import { SearchMainModule } from './search-main/search-main.module'; import { SearchMainModule } from './search-main/search-main.module';
import { SearchFilterModule } from './search-filter/search-filter.module'; import { SearchFilterModule } from './search-filter/search-filter.module';
import { ArticleSearchService } from './article-search.store';
import { SharedFilterOverlayModule } from '@shared/components/filter-overlay'; import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
@NgModule({ @NgModule({
imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule, SharedFilterOverlayModule], imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule, SharedFilterOverlayModule],
exports: [ArticleSearchComponent], exports: [ArticleSearchComponent],
declarations: [ArticleSearchComponent], declarations: [ArticleSearchComponent],
providers: [], providers: [ArticleSearchService],
}) })
export class ArticleSearchModule {} export class ArticleSearchModule {}

View File

@@ -3,24 +3,31 @@ import { DomainCatalogService } from '@domain/catalog';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { debounceTime, map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import { debounceTime, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { UiFilter } from '@ui/filter';
import { ComponentStore, tapResponse } from '@ngrx/component-store'; import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ItemDTO, QueryTokenDTO } from '@swagger/cat'; import { ItemDTO, QueryTokenDTO, UISettingsDTO } from '@swagger/cat';
import { ApplicationService } from '@core/application'; import { ApplicationService } from '@core/application';
import { BranchDTO } from '@swagger/checkout'; import { BranchDTO } from '@swagger/checkout';
import { Filter } from 'apps/shared/components/filter/src/lib';
export interface ArticleSearchState { export interface ArticleSearchState {
processId: number; processId: number;
filter: UiFilter; filter: Filter;
searchState: '' | 'fetching' | 'empty' | 'error'; searchState: '' | 'fetching' | 'empty' | 'error';
items: ItemDTO[]; items: ItemDTO[];
hits: number; hits: number;
selectedBranch: BranchDTO; selectedBranch: BranchDTO;
selectedItemIds: number[]; selectedItemIds: number[];
defaultSettings?: UISettingsDTO;
} }
@Injectable() @Injectable()
export class ArticleSearchService extends ComponentStore<ArticleSearchState> { export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
get defaultSettings() {
return this.get((s) => s.defaultSettings);
}
readonly defaultSettings$ = this.select((s) => s.defaultSettings);
get processId() { get processId() {
return this.get((s) => s.processId); return this.get((s) => s.processId);
} }
@@ -100,19 +107,19 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
} }
async setDefaultFilter(defaultQueryParams?: Record<string, string>) { async setDefaultFilter(defaultQueryParams?: Record<string, string>) {
const filter = await this.catalog const defaultSettings = await this.catalog.getSettings().toPromise();
.getSettings()
.pipe(map((settings) => UiFilter.create(settings))) const filter = Filter.create(defaultSettings);
.toPromise();
if (!!defaultQueryParams) { if (!!defaultQueryParams) {
filter?.fromQueryParams(defaultQueryParams); filter?.fromQueryParams(defaultQueryParams);
} }
this.setFilter(filter); this.setFilter(filter);
this.patchState({ defaultSettings });
} }
setFilter(filter: UiFilter) { setFilter(filter: Filter) {
this.patchState({ filter }); this.patchState({ filter });
} }

View File

@@ -1,18 +1,18 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { DomainCatalogService } from '@domain/catalog'; import { DomainCatalogService } from '@domain/catalog';
import { UiFilterAutocomplete, UiFilterAutocompleteProvider, UiInput } from '@ui/filter'; import { FilterAutocomplete, FilterAutocompleteProvider, FilterInput } from 'apps/shared/components/filter/src/lib';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
@Injectable() @Injectable()
export class ArticleSearchMainAutocompleteProvider extends UiFilterAutocompleteProvider { export class ArticleSearchMainAutocompleteProvider extends FilterAutocompleteProvider {
for = 'catalog'; for = 'catalog';
constructor(private domainCatalogSearch: DomainCatalogService) { constructor(private domainCatalogSearch: DomainCatalogService) {
super(); super();
} }
complete(input: UiInput): Observable<UiFilterAutocomplete[]> { complete(input: FilterInput): Observable<FilterAutocomplete[]> {
const token = input?.parent?.parent?.getQueryToken(); const token = input?.parent?.parent?.getQueryToken();
const filter = token?.filter; const filter = token?.filter;
const type = Object.keys(token?.input).join(';'); const type = Object.keys(token?.input).join(';');

View File

@@ -1,30 +1,33 @@
<ng-container *ngIf="filter$ | async; let filter"> <ng-container *ngIf="filter$ | async; let filter">
<div class="catalog-search-filter-content"> <div class="catalog-search-filter-content">
<button class="btn-close" type="button" (click)="close.emit()"> <div class="w-full flex flex-row justify-end items-center">
<ui-icon icon="close" size="20px"></ui-icon> <button (click)="clearFilter(filter)" class="text-[#0556B4] mr-[0.8125rem]">Alle Filter entfernen</button>
</button> <button class="text-black p-4 outline-none border-none bg-transparent" type="button" (click)="closeFilter()">
<ui-icon icon="close" size="15px"></ui-icon>
</button>
</div>
<div class="catalog-search-filter-content-main"> <div class="catalog-search-filter-content-main -mt-12">
<h1 class="text-3xl font-bold text-center py-4">Filter</h1> <h1 class="text-2xl text-[1.625rem] font-bold text-center pt-6 pb-10">Filter</h1>
<ui-filter <shared-filter
[filter]="filter" [filter]="filter"
[loading]="fetching$ | async" [loading]="fetching$ | async"
(search)="applyFilter(filter)" (search)="applyFilter(filter)"
[hint]="searchboxHint$ | async" [hint]="searchboxHint$ | async"
resizeInputOptionsToElement="page-article-search-filter .cta-wrapper"
[scanner]="true" [scanner]="true"
></ui-filter> ></shared-filter>
</div>
<div class="cta-wrapper">
<button class="cta-reset-filter" (click)="resetFilter(filter)" [disabled]="fetching$ | async">
Filter zurücksetzen
</button>
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="fetching$ | async">
<ui-spinner [show]="fetching$ | async">
Filter anwenden
</ui-spinner>
</button>
</div> </div>
</div> </div>
<div class="cta-wrapper">
<button class="cta-reset-filter" (click)="resetFilter(filter)" [disabled]="fetching$ | async">
Filter zurücksetzen
</button>
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="fetching$ | async">
<ui-spinner [show]="fetching$ | async">
Filter anwenden
</ui-spinner>
</button>
</div>
</ng-container> </ng-container>

View File

@@ -1,15 +1,11 @@
:host { :host {
@apply block bg-glitter; @apply block bg-white h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
} }
.catalog-search-filter-content { .catalog-search-filter-content {
@apply relative mx-auto p-4; @apply relative mx-auto p-4;
} }
.btn-close {
@apply absolute text-cool-grey top-3 p-4 right-4 outline-none border-none bg-transparent;
}
.catalog-search-filter-content-main { .catalog-search-filter-content-main {
h1.title { h1.title {
@apply text-center; @apply text-center;
@@ -17,14 +13,12 @@
} }
.cta-wrapper { .cta-wrapper {
@apply fixed bottom-8 whitespace-nowrap; @apply text-center whitespace-nowrap absolute bottom-8 left-0 w-full;
left: 50%;
transform: translateX(-50%);
} }
.cta-reset-filter, .cta-reset-filter,
.cta-apply-filter { .cta-apply-filter {
@apply text-lg font-bold px-6 py-3 rounded-full border-solid border-2 border-brand outline-none mx-2; @apply text-lg font-bold px-6 py-[0.85rem] rounded-full border-solid border-2 border-brand outline-none mx-2;
&:disabled { &:disabled {
@apply bg-inactive-branch cursor-not-allowed border-none text-white; @apply bg-inactive-branch cursor-not-allowed border-none text-white;
@@ -38,3 +32,7 @@
.cta-apply-filter { .cta-apply-filter {
@apply text-white bg-brand; @apply text-white bg-brand;
} }
::ng-deep page-article-search-filter shared-filter shared-filter-input-group-main {
@apply desktop:hidden px-16;
}

View File

@@ -1,8 +1,13 @@
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { UiFilter, UiFilterComponent } from '@ui/filter'; import { ApplicationService } from '@core/application';
import { Observable } from 'rxjs'; import { EnvironmentService } from '@core/environment';
import { map, take } from 'rxjs/operators'; import { Observable, Subject } from 'rxjs';
import { first, map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store'; import { ArticleSearchService } from '../article-search.store';
import { ActivatedRoute } from '@angular/router';
import { ProductCatalogNavigationService } from '@shared/services';
import { Filter, FilterComponent } from 'apps/shared/components/filter/src/lib';
import { BreadcrumbService } from '@core/breadcrumb';
@Component({ @Component({
selector: 'page-article-search-filter', selector: 'page-article-search-filter',
@@ -10,51 +15,104 @@ import { ArticleSearchService } from '../article-search.store';
styleUrls: ['search-filter.component.scss'], styleUrls: ['search-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ArticleSearchFilterComponent implements OnInit { export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
@Output() @Output()
close = new EventEmitter(); close = new EventEmitter();
_processId$ = this._activatedRoute.parent.data.pipe(map((data) => Number(data.processId)));
fetching$: Observable<boolean>; fetching$: Observable<boolean>;
filter$: Observable<UiFilter>; filter$: Observable<Filter>;
searchboxHint$ = this.articleSearch.searchboxHint$; searchboxHint$ = this.articleSearch.searchboxHint$;
@ViewChild(UiFilterComponent, { static: false }) @ViewChild(FilterComponent, { static: false })
uiFilterComponent: UiFilterComponent; uiFilterComponent: FilterComponent;
constructor(private articleSearch: ArticleSearchService) {} get isTablet() {
return this._environment.matchTablet();
}
private _onDestroy$ = new Subject();
constructor(
private articleSearch: ArticleSearchService,
private _environment: EnvironmentService,
private _activatedRoute: ActivatedRoute,
public application: ApplicationService,
private _navigationService: ProductCatalogNavigationService,
private _breadcrumb: BreadcrumbService
) {}
ngOnInit() { ngOnInit() {
this.fetching$ = this.articleSearch.fetching$; this.fetching$ = this.articleSearch.fetching$;
this.filter$ = this.articleSearch.filter$.pipe( this.filter$ = this.articleSearch.filter$.pipe(map((filter) => Filter.create(filter)));
map((filter) => UiFilter.create(filter))
// tap((filter) =>
// filter.fromQueryParams({
// main_qs: 'harry potter',
// filter_format: 'eb;!hc',
// filter_dbhwgr: '110;121',
// main_author: 'author',
// filter_region: '9780*|9781*;97884*',
// 'filter_reading-age': '1-10',
// main_stock: '1-',
// })
// )
);
} }
applyFilter(value: UiFilter) { ngOnDestroy(): void {
this._onDestroy$.next();
this._onDestroy$.complete();
}
async closeFilter(): Promise<void> {
const processId = await this._processId$.pipe(first()).toPromise();
const itemId = this._navigationService.getOutletParams(this._activatedRoute)?.right?.id;
if (this.isTablet) {
return this.closeFilterTablet(processId);
}
if (!itemId) {
if (this._navigationService.getOutletLocations(this._activatedRoute)?.left === 'search') {
return await this._navigationService.navigateToProductSearch({ processId, queryParamsHandling: 'preserve' });
}
return await this._navigationService.navigateToResults({ processId, queryParamsHandling: 'preserve' });
} else {
return await this._navigationService.navigateToDetails({ processId, itemId, queryParamsHandling: 'preserve' });
}
}
async closeFilterTablet(processId: number): Promise<void> {
const latestBreadcrumb = await this._breadcrumb.getLastActivatedBreadcrumbByKey$(processId).pipe(first()).toPromise();
if (latestBreadcrumb?.tags?.find((tag) => tag === 'results')) {
return await this._navigationService.navigateToResults({ processId, queryParamsHandling: 'preserve' });
} else {
return await this._navigationService.navigateToProductSearch({ processId, queryParamsHandling: 'preserve' });
}
}
applyFilter(value: Filter) {
this.uiFilterComponent?.cancelAutocomplete(); this.uiFilterComponent?.cancelAutocomplete();
this.articleSearch.setFilter(value); this.articleSearch.setFilter(value);
this.articleSearch.search({ clear: true }); this.articleSearch.search({ clear: true });
this.articleSearch.searchCompleted.pipe(take(1)).subscribe((s) => { this.articleSearch.searchCompleted
if (s.searchState === '') { .pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$))
this.close.emit(); .subscribe(([state, processId]) => {
} if (state.searchState === '') {
}); const params = state.filter.getQueryParams();
if (state.hits === 1 && this.isTablet) {
const item = state.items.find((f) => f);
this._navigationService.navigateToDetails({
processId,
itemId: item.id,
queryParams: this.isTablet ? undefined : params,
});
} else if (this.isTablet) {
this._navigationService.navigateToResults({
processId,
queryParams: params,
});
}
}
});
} }
resetFilter(value: UiFilter) { clearFilter(value: Filter) {
value.unselectAll();
}
resetFilter(value: Filter) {
const queryParams = { main_qs: value?.getQueryParams()?.main_qs || '' }; const queryParams = { main_qs: value?.getQueryParams()?.main_qs || '' };
this.articleSearch.setDefaultFilter(queryParams); this.articleSearch.setDefaultFilter(queryParams);
} }

View File

@@ -1,13 +1,13 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { UiFilterNextModule } from '@ui/filter'; import { RouterModule } from '@angular/router';
import { UiIconModule } from '@ui/icon'; import { UiIconModule } from '@ui/icon';
import { UiSpinnerModule } from '@ui/spinner'; import { UiSpinnerModule } from '@ui/spinner';
import { ArticleSearchFilterComponent } from './search-filter.component'; import { ArticleSearchFilterComponent } from './search-filter.component';
import { FilterNextModule } from 'apps/shared/components/filter/src/lib';
@NgModule({ @NgModule({
imports: [CommonModule, UiFilterNextModule, UiIconModule, UiSpinnerModule], imports: [CommonModule, RouterModule, FilterNextModule, UiIconModule, UiSpinnerModule],
exports: [ArticleSearchFilterComponent], exports: [ArticleSearchFilterComponent],
declarations: [ArticleSearchFilterComponent], declarations: [ArticleSearchFilterComponent],
providers: [], providers: [],

View File

@@ -1,26 +1,50 @@
<div class="card-search-article"> <div class="bg-white rounded py-10 px-4 text-center shadow-[0_-2px_24px_0_#dce2e9] h-full">
<h1 class="title">Artikelsuche</h1> <h1 class="text-2xl text-[1.625rem] font-bold mb-[0.375rem]">Artikelsuche</h1>
<p class="info"> <p class="text-lg mb-10">
Welchen Artikel suchen Sie? Welchen Artikel suchen Sie?
</p> </p>
<ng-container *ngIf="filter$ | async; let filter"> <ng-container *ngIf="filter$ | async; let filter">
<ui-filter-filter-group-main [inputGroup]="filter?.filter | group: 'main'"></ui-filter-filter-group-main> <shared-filter-filter-group-main
<ui-filter-input-group-main class="mb-8 w-full"
[hint]="searchboxHint$ | async" *ngIf="!(isDesktop$ | async)"
[loading]="fetching$ | async" [inputGroup]="filter?.filter | group: 'main'"
[inputGroup]="filter?.input | group: 'main'" ></shared-filter-filter-group-main>
(search)="search(filter)" <div class="flex flex-row px-12 justify-center desktop:px-0">
[showDescription]="false" <shared-filter-input-group-main
[scanner]="true" class="block w-full mr-3 desktop:mx-auto"
></ui-filter-input-group-main> [hint]="searchboxHint$ | async"
[loading]="fetching$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search(filter)"
[showDescription]="false"
[scanner]="true"
></shared-filter-input-group-main>
<a
*ngIf="!(isDesktop$ | async)"
class="page-search-main__filter w-[6.75rem] h-14 rounded-card font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
[class.active]="hasFilter$ | async"
[routerLink]="filterRoute"
queryParamsHandling="preserve"
>
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
Filter
</a>
</div>
<div class="recent-searches-wrapper"> <div class="flex flex-col items-start ml-12 desktop:ml-8 py-6 bg-white">
<h3 class="recent-searches-header">Deine letzten Suchanfragen</h3> <h3 class="text-sm font-bold mb-3">Deine letzten Suchanfragen</h3>
<ul> <ul class="flex flex-col justify-start overflow-hidden items-start m-0 p-0 bg-white w-full">
<li class="recent-searches-items" *ngFor="let recentQuery of history$ | async"> <li class="list-none pb-3" *ngFor="let recentQuery of history$ | async">
<button (click)="setQueryHistory(filter, recentQuery.friendlyName)"> <button
<ui-icon icon="search" size="15px"></ui-icon> class="flex flex-row items-center outline-none border-none bg-white text-black text-base m-0 p-0"
<p>{{ recentQuery.friendlyName }}</p> (click)="setQueryHistory(filter, recentQuery.friendlyName)"
>
<ui-icon
class="flex w-8 h-8 justify-center items-center mr-3 rounded-full text-black bg-[#edeff0]"
icon="search"
size="0.875rem"
></ui-icon>
<p class="m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-[25rem]">{{ recentQuery.friendlyName }}</p>
</button> </button>
</li> </li>
</ul> </ul>

View File

@@ -1,71 +1,13 @@
:host { :host {
@apply flex flex-col box-border; @apply flex flex-col box-border h-full;
} }
.title { .page-search-main__filter {
@apply text-page-heading font-bold; &.active {
} @apply bg-[#596470] text-white ml-px-5;
}
.info {
@apply text-2xl mt-1 mb-px-30;
}
.filter-chips {
@apply flex flex-row justify-center;
}
.card-search-article {
@apply bg-white rounded p-4 text-center;
box-shadow: 0 -2px 24px 0 #dce2e9;
}
.card-search-article {
min-height: calc(100vh - 380px);
}
ui-filter-filter-group-main {
@apply mb-8 w-full;
} }
::ng-deep page-article-search-main ui-filter-filter-group-main .ui-filter-chip:not(.selected) { ::ng-deep page-article-search-main ui-filter-filter-group-main .ui-filter-chip:not(.selected) {
@apply bg-glitter; @apply bg-glitter;
} }
ui-filter-input-group-main {
@apply block mx-auto;
max-width: 600px;
}
.recent-searches-wrapper {
@apply flex flex-col mx-auto items-start py-6 bg-white;
width: 50%;
z-index: 0;
.recent-searches-header {
@apply text-sm font-bold mb-4;
}
ul {
@apply flex flex-col justify-start overflow-hidden items-start m-0 p-0 bg-white w-full;
z-index: 0;
.recent-searches-items {
@apply list-none pb-px-15;
button {
@apply flex flex-row items-center outline-none border-none bg-white text-black text-base m-0 p-0;
ui-icon {
@apply flex w-px-35 h-px-35 justify-center items-center mr-3 rounded-full text-black;
background-color: #e6eff9;
}
p {
@apply m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis;
max-width: 400px;
}
}
}
}
}

View File

@@ -3,11 +3,13 @@ import { ActivatedRoute } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb'; import { BreadcrumbService } from '@core/breadcrumb';
import { ApplicationService } from '@core/application'; import { ApplicationService } from '@core/application';
import { DomainCatalogService } from '@domain/catalog'; import { DomainCatalogService } from '@domain/catalog';
import { UiFilter, UiFilterInputGroupMainComponent } from '@ui/filter';
import { combineLatest, NEVER, Subscription } from 'rxjs'; import { combineLatest, NEVER, Subscription } from 'rxjs';
import { catchError, debounceTime, first, switchMap } from 'rxjs/operators'; import { catchError, debounceTime, first, switchMap, map, shareReplay } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store'; import { ArticleSearchService } from '../article-search.store';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { EnvironmentService } from '@core/environment';
import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/filter/src/lib';
import { ProductCatalogNavigationService } from '@shared/services';
@Component({ @Component({
selector: 'page-article-search-main', selector: 'page-article-search-main',
@@ -26,15 +28,39 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
subscriptions = new Subscription(); subscriptions = new Subscription();
@ViewChild(UiFilterInputGroupMainComponent, { static: false }) hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
uiInputGroupMain: UiFilterInputGroupMainComponent; map(([filter, defaultFilter]) => {
const filterQueryParams = filter?.getQueryParams();
return !isEqual(this.resetQueryParamsQueryAndOrderBy(filterQueryParams), Filter.create(defaultFilter).getQueryParams());
})
);
@ViewChild(FilterInputGroupMainComponent, { static: false })
sharedFilterInputGroupMain: FilterInputGroupMainComponent;
get isDesktop$() {
return this._environment.matchDesktop$.pipe(
map((state) => state?.matches),
shareReplay()
);
}
get filterRoute() {
const itemId = this._navigationService?.getOutletParams(this.route)?.right?.id;
return this._navigationService.getArticleSearchResultsAndFilterPath({
processId: this.application.activatedProcessId,
itemId,
});
}
constructor( constructor(
private searchService: ArticleSearchService, private searchService: ArticleSearchService,
private catalog: DomainCatalogService, private catalog: DomainCatalogService,
private route: ActivatedRoute, private route: ActivatedRoute,
private application: ApplicationService, private application: ApplicationService,
private breadcrumb: BreadcrumbService private breadcrumb: BreadcrumbService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -44,7 +70,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
.subscribe(async ([processId, queryParams]) => { .subscribe(async ([processId, queryParams]) => {
const processChanged = processId !== this.searchService.processId; const processChanged = processId !== this.searchService.processId;
if (!(this.searchService.filter instanceof UiFilter)) { if (!(this.searchService.filter instanceof Filter)) {
await this.searchService.setDefaultFilter(); await this.searchService.setDefaultFilter();
} }
@@ -57,6 +83,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
const cleanQueryParams = this.cleanupQueryParams(queryParams); const cleanQueryParams = this.cleanupQueryParams(queryParams);
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) { if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
// Reset Filter on Product Search Shell Navigation click
await this.searchService.setDefaultFilter(queryParams); await this.searchService.setDefaultFilter(queryParams);
} }
@@ -85,16 +112,30 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
this.updateBreadcrumb(this.searchService.processId, this.searchService.filter.getQueryParams()); this.updateBreadcrumb(this.searchService.processId, this.searchService.filter.getQueryParams());
} }
search(filter: UiFilter) { search(filter: Filter) {
this.uiInputGroupMain.cancelAutocomplete(); this.sharedFilterInputGroupMain.cancelAutocomplete();
this.searchService.setFilter(filter); this.searchService.setFilter(filter);
this.searchService.search({ clear: true }); this.searchService.search({ clear: true });
} }
setQueryHistory(filter: UiFilter, query: string) { setQueryHistory(filter: Filter, query: string) {
filter.fromQueryParams({ main_qs: query }); filter.fromQueryParams({ main_qs: query });
} }
resetQueryParamsQueryAndOrderBy(params: Record<string, string> = {}) {
const clean = { ...params };
for (const key in clean) {
if (key === 'main_qs' || key?.includes('order_by')) {
clean[key] = undefined;
} else if (key?.includes('order_by')) {
delete clean[key];
}
}
return clean;
}
cleanupQueryParams(params: Record<string, string> = {}) { cleanupQueryParams(params: Record<string, string> = {}) {
const clean = { ...params }; const clean = { ...params };

View File

@@ -1,11 +1,12 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { UiFilterNextModule } from '@ui/filter';
import { UiIconModule } from '@ui/icon'; import { UiIconModule } from '@ui/icon';
import { ArticleSearchMainComponent } from './search-main.component'; import { ArticleSearchMainComponent } from './search-main.component';
import { FilterNextModule } from 'apps/shared/components/filter/src/lib';
import { RouterModule } from '@angular/router';
@NgModule({ @NgModule({
imports: [CommonModule, UiIconModule, UiFilterNextModule], imports: [CommonModule, RouterModule, UiIconModule, FilterNextModule],
exports: [ArticleSearchMainComponent], exports: [ArticleSearchMainComponent],
declarations: [ArticleSearchMainComponent], declarations: [ArticleSearchMainComponent],
providers: [], providers: [],

View File

@@ -1,17 +1,43 @@
<div class="thumbnail animation"></div> <ng-container *ngIf="!(mainOutletActive$ | async); else mainOutlet">
<div class="col"> <div class="bg-ucla-blue rounded-card w-[4.375rem] h-[5.625rem] animate-[load_1s_linear_infinite]"></div>
<div class="author animation"></div> <div class="flex flex-col flex-grow">
<div class="row"> <div class="h-4 bg-ucla-blue ml-4 mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="title animation"></div> <div class="flex flex-row justify-between flex-grow">
<div class="price animation"></div> <div class="h-6 bg-ucla-blue ml-4 w-[12.5rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-6 bg-ucla-blue ml-4 w-[4.6875rem] animate-[load_1s_linear_infinite]"></div>
</div>
<div class="flex-grow"></div>
<div class="flex flex-row justify-between flex-grow">
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue ml-4 w-[3.125rem] animate-[load_1s_linear_infinite]"></div>
</div>
<div class="flex flex-row justify-between flex-grow">
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
</div> </div>
<div class="space"></div> </ng-container>
<div class="row">
<div class="category animation"></div> <ng-template #mainOutlet>
<div class="stock animation"></div> <div class="bg-ucla-blue rounded-card w-[3rem] h-[4.125rem] animate-[load_1s_linear_infinite]"></div>
<div class="flex flex-col ml-4 w-[30%]">
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-6 bg-ucla-blue mb-2 w-[12.5rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-6 bg-ucla-blue w-[12.5rem] animate-[load_1s_linear_infinite]"></div>
</div> </div>
<div class="row"> <div class="flex flex-col ml-14 w-[30%]">
<div class="manufacturer animation"></div> <div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="ava animation"></div> <div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div> </div>
</div> <div class="flex flex-col ml-10 w-[20%]">
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
<div class="flex flex-col ml-2 w-[20%] items-end">
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
</ng-template>

View File

@@ -1,61 +1,3 @@
:host { :host {
@apply flex flex-row rounded-card bg-white mb-2 p-4; @apply flex flex-row rounded-card bg-white mb-2 p-4 w-full h-[212px] desktop-small:h-[181px];
height: 187px;
}
.thumbnail {
width: 70px;
height: 90px;
@apply bg-ucla-blue rounded-card;
}
.space {
@apply flex-grow;
}
.col {
@apply flex flex-col flex-grow;
}
.row {
@apply flex flex-row justify-between flex-grow;
}
.author {
width: 150px;
@apply h-4 bg-ucla-blue ml-4 mb-2;
}
.title {
width: 300px;
@apply h-6 bg-ucla-blue ml-4;
}
.price {
width: 100px;
@apply h-6 bg-ucla-blue ml-4;
}
.category {
width: 200px;
@apply h-4 bg-ucla-blue ml-4;
}
.manufacturer {
width: 200px;
@apply h-4 bg-ucla-blue ml-4;
}
.stock {
width: 75px;
@apply h-4 bg-ucla-blue ml-4;
}
.ava {
width: 150px;
@apply h-4 bg-ucla-blue ml-4;
}
.animation {
animation: load 1s linear infinite;
} }

View File

@@ -1,4 +1,7 @@
import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Component, ChangeDetectionStrategy, HostBinding } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProductCatalogNavigationService } from '@shared/services';
import { shareReplay } from 'rxjs/operators';
@Component({ @Component({
selector: 'page-search-result-item-loading', selector: 'page-search-result-item-loading',
@@ -7,5 +10,13 @@ import { Component, ChangeDetectionStrategy } from '@angular/core';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class SearchResultItemLoadingComponent { export class SearchResultItemLoadingComponent {
constructor() {} get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
}
constructor(private _navigationService: ProductCatalogNavigationService, private _activatedRoute: ActivatedRoute) {}
@HostBinding('style') get class() {
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
}
} }

View File

@@ -1,81 +1,122 @@
<a class="product-list-result-content" [routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'details', item?.id]"> <a
<div class="item-thumbnail"> class="page-search-result-item__item-card hover p-5 desktop-small:px-4 desktop-small:py-[0.625rem] h-[13.25rem] desktop-small:h-[11.3125rem] bg-white border border-solid border-transparent rounded-card"
<img loading="lazy" *ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.name" /> [class.page-search-result-item__item-card-main]="mainOutletActive$ | async"
</div> [routerLink]="detailsPath"
[routerLinkActive]="!isTablet && !(mainOutletActive$ | async) ? 'active' : ''"
<div class="item-contributors"> [queryParamsHandling]="!isTablet ? 'preserve' : ''"
<a >
*ngFor="let contributor of contributors; let last = last" <div class="page-search-result-item__item-thumbnail text-center mr-4 w-[50px] h-[79px]">
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search', 'results']" <img
[queryParams]="{ main_qs: contributor, main_author: 'author' }" class="page-search-result-item__item-image w-[50px] h-[79px]"
(click)="$event?.stopPropagation()" loading="lazy"
> *ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl"
{{ contributor }}{{ last ? '' : ';' }} [src]="thumbnailUrl"
</a> [alt]="item?.product?.name"
/>
</div> </div>
<div <div
class="item-title" class="page-search-result-item__item-grid-container"
[class.xl]="item?.product?.name?.length >= 35" [class.page-search-result-item__item-grid-container-main]="mainOutletActive$ | async"
[class.lg]="item?.product?.name?.length >= 40"
[class.md]="item?.product?.name?.length >= 50"
[class.sm]="item?.product?.name?.length >= 60"
[class.xs]="item?.product?.name?.length >= 100"
> >
{{ item?.product?.name }} <div
</div> class="page-search-result-item__item-contributors desktop-small:text-sm font-bold text-[#0556B4] text-ellipsis overflow-hidden max-w-[24rem] whitespace-nowrap"
>
<a
*ngFor="let contributor of contributors; let last = last"
[routerLink]="resultsPath"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
</div>
<div class="item-price"> <div
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }} class="page-search-result-item__item-title font-bold text-2xl"
</div> [class.text-xl]="item?.product?.name?.length >= 35 && isTablet"
[class.text-lg]="item?.product?.name?.length >= 40 && isTablet"
[class.text-md]="item?.product?.name?.length >= 50 && isTablet"
[class.text-sm]="item?.product?.name?.length >= 60 || !isTablet"
[class.text-xs]="item?.product?.name?.length >= 100 || (!isTablet && item?.product?.name?.length >= 70)"
>
{{ item?.product?.name }}
</div>
<div *ngIf="selectable" class="item-data-selector"> <div class="page-search-result-item__item-format desktop-small:text-sm">
<ui-select-bullet [ngModel]="selected" (ngModelChange)="setSelected($event)"></ui-select-bullet> <div *ngIf="item?.product?.format && item?.product?.formatDetail" class="font-bold flex flex-row">
</div> <img
class="mr-3"
<div class="item-stock z-dropdown" [uiOverlayTrigger]="tooltip" [overlayTriggerDisabled]="!(stockTooltipText$ | async)"> *ngIf="item?.product?.format !== '--'"
<ng-container *ngIf="isOrderBranch$ | async"> loading="lazy"
<div class="flex flex-row items-center justify-between"> src="assets/images/Icon_{{ item?.product?.format }}.svg"
<ui-icon icon="home" size="1em"></ui-icon> [alt]="item?.product?.formatDetail"
<span />
*ngIf="inStock$ | async; let stock" {{ item?.product?.formatDetail | substr: 25 }}
[class.skeleton]="stock.inStock === undefined"
class="min-w-[1rem] text-right inline-block"
>{{ stock?.inStock }}</span
>
<span>x</span>
</div> </div>
</ng-container> </div>
<ng-container *ngIf="!(isOrderBranch$ | async)">
<div class="flex flex-row items-center justify-between z-dropdown">
<ui-icon class="block" icon="home" size="1em"></ui-icon>
<span class="min-w-[1rem] text-center inline-block">-</span>
<span>x</span>
</div>
</ng-container>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
<!-- <div class="item-stock"><ui-icon icon="home" size="1em"></ui-icon> {{ item?.stockInfos | stockInfos }} x</div> -->
<div class="item-ssc" [class.xs]="item?.catalogAvailability?.sscText?.length >= 60"> <div class="page-search-result-item__item-manufacturer desktop-small:text-sm">
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }} {{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }}
</div> </div>
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail"> <div class="page-search-result-item__item-misc desktop-small:text-sm">
<img {{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
*ngIf="item?.product?.format !== '--'" {{ publicationDate }}
loading="lazy" </div>
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
</div>
<div class="item-misc"> <div class="page-search-result-item__item-price desktop-small:text-sm font-bold justify-self-end">
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }} <br /> {{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span> </div>
{{ publicationDate }}
<div class="page-search-result-item__item-select-bullet justify-self-end">
<input
*ngIf="selectable"
(click)="$event.stopPropagation()"
[ngModel]="selected$ | async"
(ngModelChange)="setSelected()"
class="isa-select-bullet"
type="checkbox"
/>
</div>
<div
class="page-search-result-item__item-stock desktop-small:text-sm font-bold z-dropdown justify-self-start"
[class.justify-self-end]="!(mainOutletActive$ | async)"
[uiOverlayTrigger]="tooltip"
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
>
<ng-container *ngIf="isOrderBranch$ | async">
<div class="flex flex-row items-center justify-between">
<ui-icon icon="home" size="1em"></ui-icon>
<span
*ngIf="inStock$ | async; let stock"
[class.skeleton]="stock.inStock === undefined"
class="min-w-[1rem] text-right inline-block"
>{{ stock?.inStock }}</span
>
<span>x</span>
</div>
</ng-container>
<ng-container *ngIf="!(isOrderBranch$ | async)">
<div class="flex flex-row items-center justify-between z-dropdown">
<ui-icon class="block" icon="home" size="1em"></ui-icon>
<span class="min-w-[1rem] text-center inline-block">-</span>
<span>x</span>
</div>
</ng-container>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
<div
class="page-search-result-item__item-ssc desktop-small:text-sm justify-self-start"
[class.justify-self-end]="!(mainOutletActive$ | async)"
[class.xs]="item?.catalogAvailability?.sscText?.length >= 60"
>
<strong>{{ item?.catalogAvailability?.ssc }}</strong> -
{{ !isTablet ? item?.catalogAvailability?.sscText : (item?.catalogAvailability?.sscText | substr: 18) }}
</div>
</div> </div>
</a> </a>

View File

@@ -1,113 +1,90 @@
.product-list-result-content { :host {
@apply text-black no-underline grid; @apply flex flex-col w-full h-[13.25rem] desktop-small:h-[11.3125rem];
grid-template-columns: 102px 50% auto; }
grid-template-rows: auto;
.page-search-result-item__item-card {
@apply grid grid-flow-col;
grid-template-columns: 3.9375rem auto;
box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5);
}
.page-search-result-item__item-grid-container {
@apply grid grid-flow-row gap-[0.375rem];
grid-template-areas: grid-template-areas:
'item-thumbnail item-contributors item-contributors' 'contributors contributors contributors'
'item-thumbnail item-title item-price' 'title title price'
'item-thumbnail item-title item-data-selector' 'title title price'
'item-thumbnail item-format item-stock' 'title title select'
'item-thumbnail item-misc item-ssc'; 'format format select'
'manufacturer manufacturer stock'
'misc ssc ssc';
} }
.item-thumbnail { .page-search-result-item__item-grid-container-main {
grid-area: item-thumbnail; @apply gap-x-4;
width: 70px; grid-template-rows: 1.3125rem 1.3125rem auto;
@apply mr-8; grid-template-columns: 30% 30% 20% 8.8% auto;
img { grid-template-areas:
max-width: 100%; 'contributors format stock price .'
max-height: 150px; 'title manufacturer ssc . select'
@apply rounded-card shadow-cta; 'title misc . . .';
} }
}
.page-search-result-item__item-contributors {
.item-contributors { grid-area: contributors;
grid-area: item-contributors; }
height: 22px;
text-overflow: ellipsis; .page-search-result-item__item-price {
overflow: hidden; grid-area: price;
max-width: 600px; }
white-space: nowrap;
.page-search-result-item__item-title {
a { grid-area: title;
@apply text-active-customer font-bold no-underline; }
}
} .page-search-result-item__item-format {
grid-area: format;
.item-title { }
grid-area: item-title;
@apply font-bold text-2xl; .page-search-result-item__item-manufacturer {
height: 64px; grid-area: manufacturer;
max-height: 64px; }
}
.page-search-result-item__item-misc {
.item-title.xl { grid-area: misc;
@apply font-bold text-xl; }
}
.page-search-result-item__item-select-bullet {
.item-title.lg { grid-area: select;
@apply font-bold text-lg; }
}
.page-search-result-item__item-stock {
.item-title.md { grid-area: stock;
@apply font-bold text-base; }
}
.page-search-result-item__item-ssc {
.item-title.sm { grid-area: ssc;
@apply font-bold text-sm; }
}
.page-search-result-item__item-image {
.item-title.xs { box-shadow: 0px 6px 18px rgba(0, 0, 0, 0.197935);
@apply font-bold text-xs; }
}
.active,
.item-price { .hover:hover {
grid-area: item-price; @apply bg-[#D8DFE5] border border-solid border-[#0556B4];
@apply font-bold text-xl text-right;
} .page-search-result-item__item-select-bullet {
.isa-select-bullet::before {
.item-format { @apply bg-[#fff];
grid-area: item-format; }
@apply flex flex-row items-center font-bold text-lg whitespace-nowrap;
.isa-select-bullet:checked:before {
img { @apply bg-[#596470];
@apply mr-2; }
}
} .isa-select-bullet:hover::before {
@apply bg-[#778490];
.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-misc {
grid-area: item-misc;
}
.item-ssc {
grid-area: item-ssc;
@apply font-bold text-right;
}
.item-ssc.xs {
@apply font-bold text-xs;
}
.item-data-selector {
@apply w-full flex justify-end;
grid-area: item-data-selector;
}
ui-select-bullet {
@apply p-4 -m-4 z-dropdown;
}
@media (min-width: 1025px) {
.item-contributors {
max-width: 780px;
} }
} }

View File

@@ -1,14 +1,17 @@
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output } from '@angular/core'; import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, HostListener, HostBinding } from '@angular/core';
import { ApplicationService } from '@core/application'; import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { DomainAvailabilityService, DomainInStockService } from '@domain/availability'; import { DomainAvailabilityService, DomainInStockService } from '@domain/availability';
import { ComponentStore } from '@ngrx/component-store'; import { ComponentStore } from '@ngrx/component-store';
import { ItemDTO } from '@swagger/cat'; import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common'; import { DateAdapter } from '@ui/common';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { combineLatest } from 'rxjs'; import { combineLatest } from 'rxjs';
import { debounceTime, switchMap, map, shareReplay, filter } from 'rxjs/operators'; import { debounceTime, switchMap, map, shareReplay, filter, first } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store'; import { ArticleSearchService } from '../article-search.store';
import { ProductCatalogNavigationService } from '@shared/services';
import { ActivatedRoute } from '@angular/router';
export interface SearchResultItemComponentState { export interface SearchResultItemComponentState {
item?: ItemDTO; item?: ItemDTO;
@@ -36,16 +39,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
readonly item$ = this.select((s) => s.item); readonly item$ = this.select((s) => s.item);
@Input() selected$ = this._articleSearchService.selectedItemIds$.pipe(map((selectedItemIds) => selectedItemIds.includes(this.item?.id)));
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() @Input()
get selectable() { get selectable() {
@@ -58,7 +52,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
} }
@Output() @Output()
selectedChange = new EventEmitter<boolean>(); selectedChange = new EventEmitter<ItemDTO>();
get contributors() { get contributors() {
return this.item?.product?.contributors?.split(';').map((val) => val.trim()); return this.item?.product?.contributors?.split(';').map((val) => val.trim());
@@ -77,6 +71,22 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
return ''; return '';
} }
get isTablet() {
return this._environment.matchTablet();
}
get detailsPath() {
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: this.item?.id });
}
get resultsPath() {
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId);
}
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
}
defaultBranch$ = this._availability.getDefaultBranch(); defaultBranch$ = this._availability.getDefaultBranch();
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe( selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
@@ -123,7 +133,10 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
private _articleSearchService: ArticleSearchService, private _articleSearchService: ArticleSearchService,
public applicationService: ApplicationService, public applicationService: ApplicationService,
private _stockService: DomainInStockService, private _stockService: DomainInStockService,
private _availability: DomainAvailabilityService private _availability: DomainAvailabilityService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService,
private _activatedRoute: ActivatedRoute
) { ) {
super({ super({
selected: false, selected: false,
@@ -131,7 +144,16 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
}); });
} }
setSelected(selected: boolean) { setSelected() {
this._articleSearchService.setSelected({ selected, itemId: this.item?.id }); const isSelected = this._articleSearchService.selectedItemIds.includes(this.item?.id);
this._articleSearchService.setSelected({ selected: !isSelected, itemId: this.item?.id });
if (!this.isTablet) {
this.selectedChange.emit(this.item);
}
}
@HostBinding('style') get class() {
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
} }
} }

View File

@@ -1,32 +1,77 @@
<div class="filter-wrapper"> <div
<div class="hits" *ngIf="hits$ | async; let hits">{{ hits }} Titel</div> class="page-search-results__header bg-background-liste flex items-end justify-between"
<ui-order-by-filter [orderBy]="(filter$ | async)?.orderBy" (selectedOrderByChange)="search(); updateBreadcrumbs()"> </ui-order-by-filter> [class.pb-4]="!(mainOutletActive$ | async)"
[class.flex-col]="!(mainOutletActive$ | async)"
>
<div class="flex flex-row w-full desktop-small:w-min" [class.desktop:w-full]="!(mainOutletActive$ | async)">
<shared-filter-input-group-main
*ngIf="filter$ | async; let filter"
class="block mr-3 w-full desktop-small:w-[23.5rem]"
[class.desktop:w-full]="!(mainOutletActive$ | async)"
[hint]="searchboxHint$ | async"
[loading]="fetching$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search(filter)"
[showDescription]="false"
[scanner]="true"
></shared-filter-input-group-main>
<a
class="page-search-results__filter w-[6.75rem] h-14 rounded-card font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
[class.active]="hasFilter$ | async"
[routerLink]="filterRoute"
queryParamsHandling="preserve"
>
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
Filter
</a>
</div>
<div
*ngIf="hits$ | async; let hits"
class="page-search-results__items-count inline-flex flex-row items-center pr-5 text-sm"
[class.mb-4]="mainOutletActive$ | async"
>
{{ hits ??
0 }}
Titel
</div>
</div>
<div class="page-search-results__order-by" [class.page-search-results__order-by-main]="mainOutletActive$ | async">
<shared-order-by-filter
[groupBy]="(mainOutletActive$ | async) ? [2, 2, 1, 1] : []"
[orderBy]="(filter$ | async)?.orderBy"
(selectedOrderByChange)="search(); updateBreadcrumbs()"
>
</shared-order-by-filter>
</div> </div>
<cdk-virtual-scroll-viewport <cdk-virtual-scroll-viewport
#scrollContainer #scrollContainer
class="product-list scroll-bar scroll-bar-margin" class="product-list"
[itemSize]="187" [itemSize]="(mainOutletActive$ | async) ? 187 : 98"
minBufferPx="1200" minBufferPx="2800"
maxBufferPx="1200" maxBufferPx="2800"
(scrolledIndexChange)="scrolledIndexChange($event)" (scrolledIndexChange)="scrolledIndexChange($event)"
> >
<div class="product-list-result" *cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId"> <search-result-item
<search-result-item class="page-search-results__result-item"
[selected]="item | searchResultSelected: searchService.selectedItemIds" [class.page-search-results__result-item-main]="mainOutletActive$ | async"
[selectable]="isSelectable(item)" *cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId"
[item]="item" (selectedChange)="addToCart($event)"
></search-result-item> [selectable]="isSelectable(item)"
</div> [item]="item"
></search-result-item>
<page-search-result-item-loading *ngIf="fetching$ | async"></page-search-result-item-loading> <page-search-result-item-loading *ngIf="fetching$ | async"></page-search-result-item-loading>
</cdk-virtual-scroll-viewport> </cdk-virtual-scroll-viewport>
<div class="actions"> <div *ngIf="isTablet" class="actions z-fixed">
<button <button
[disabled]="loading$ | async" [disabled]="loading$ | async"
*ngIf="(selectedItemIds$ | async)?.length > 0" *ngIf="(selectedItemIds$ | async)?.length > 0"
class="cta-cart cta-action-primary" class="cta-cart cta-action-primary"
(click)="addSelectedItemsToCart()" (click)="addToCart()"
> >
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner> <ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button> </button>

View File

@@ -1,34 +1,36 @@
:host { :host {
@apply box-border grid; @apply box-border grid h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
max-height: calc(100vh - 364px); grid-template-rows: auto auto 1fr;
height: 100vh;
grid-template-rows: auto 1fr;
} }
.product-list { .product-list {
@apply m-0 p-0 mt-2; @apply m-0 p-0 mt-px-2;
} }
.product-list-result { .page-search-results__result-item {
@apply list-none bg-white rounded-card p-4 mb-2; @apply mb-px-10;
height: 187px;
max-height: 187px;
} }
.filter-wrapper { .page-search-results__result-item-main {
@apply block relative; @apply mb-[5px];
}
.hits { .page-search-results__order-by {
@apply text-inactive-branch font-semibold absolute top-2 right-0; @apply bg-white rounded-t-card px-6 desktop-small:px-8;
} }
ui-order-by-filter { .page-search-results__order-by-main {
@apply mx-auto; @apply pl-[4.9375rem] px-4;
}
.page-search-results__filter {
&.active {
@apply bg-[#596470] text-white ml-px-5;
} }
} }
.actions { .actions {
@apply fixed bottom-28 inline-grid grid-flow-col gap-7; @apply fixed bottom-16 inline-grid grid-flow-col gap-7;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
@@ -44,3 +46,24 @@
@apply bg-brand text-white; @apply bg-brand text-white;
} }
} }
::ng-deep page-search-results .page-search-results__order-by-main shared-order-by-filter {
@apply grid grid-flow-col justify-items-start gap-x-4 justify-start;
grid-template-columns: 30% 30% 20% 8.8% auto;
.group {
@apply desktop-small:justify-start;
}
.group:last-child {
@apply justify-self-end;
.order-by-filter-button {
@apply ml-12 mr-0;
}
}
.order-by-filter-button {
@apply ml-0 mr-12;
}
}

View File

@@ -3,18 +3,20 @@ import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, ViewC
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application'; import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb'; import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { DomainCheckoutService } from '@domain/checkout'; import { DomainCheckoutService } from '@domain/checkout';
import { ItemDTO } from '@swagger/cat'; import { ItemDTO } from '@swagger/cat';
import { AddToShoppingCartDTO } from '@swagger/checkout'; import { AddToShoppingCartDTO } from '@swagger/checkout';
import { UiFilter } from '@ui/filter';
import { UiErrorModalComponent, UiModalService } from '@ui/modal'; import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { CacheService } from 'apps/core/cache/src/public-api'; import { CacheService } from 'apps/core/cache/src/public-api';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, first, map, switchMap } from 'rxjs/operators'; import { debounceTime, first, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store'; import { ArticleSearchService } from '../article-search.store';
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component'; import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
import { SearchResultItemComponent } from './search-result-item.component'; import { SearchResultItemComponent } from './search-result-item.component';
import { ProductCatalogNavigationService } from '@shared/services';
import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/filter/src/lib';
@Component({ @Component({
selector: 'page-search-results', selector: 'page-search-results',
@@ -27,6 +29,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
@ViewChild('scrollContainer', { static: true }) @ViewChild('scrollContainer', { static: true })
scrollContainer: CdkVirtualScrollViewport; scrollContainer: CdkVirtualScrollViewport;
@ViewChild(FilterInputGroupMainComponent, { static: false })
sharedFilterInputGroupMain: FilterInputGroupMainComponent;
results$ = this.searchService.items$; results$ = this.searchService.items$;
fetching$ = this.searchService.fetching$; fetching$ = this.searchService.fetching$;
hits$ = this.searchService.hits$; hits$ = this.searchService.hits$;
@@ -35,6 +40,8 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
selectedItemIds$ = this.searchService.selectedItemIds$; selectedItemIds$ = this.searchService.selectedItemIds$;
searchboxHint$ = this.searchService.searchboxHint$;
selectedItems$ = combineLatest([this.results$, this.selectedItemIds$]).pipe( selectedItems$ = combineLatest([this.results$, this.selectedItemIds$]).pipe(
map(([items, selectedItemIds]) => { map(([items, selectedItemIds]) => {
return items?.filter((item) => selectedItemIds?.find((selectedItemId) => item.id === selectedItemId)); return items?.filter((item) => selectedItemIds?.find((selectedItemId) => item.id === selectedItemId));
@@ -47,14 +54,39 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
trackByItemId: TrackByFunction<ItemDTO> = (index, item) => item.id; trackByItemId: TrackByFunction<ItemDTO> = (index, item) => item.id;
get isTablet() {
return this._environment.matchTablet();
}
hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
map(([filter, defaultFilter]) => {
const filterQueryParams = filter?.getQueryParams();
return !isEqual(this.resetQueryParamsQueryAndOrderBy(filterQueryParams), Filter.create(defaultFilter).getQueryParams());
})
);
get filterRoute() {
const itemId = this._navigationService?.getOutletParams(this.route)?.right?.id;
return this._navigationService.getArticleSearchResultsAndFilterPath({
processId: this.application.activatedProcessId,
itemId,
});
}
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this.route).pipe(shareReplay());
}
constructor( constructor(
public searchService: ArticleSearchService, public searchService: ArticleSearchService,
private route: ActivatedRoute, private route: ActivatedRoute,
private application: ApplicationService, public application: ApplicationService,
private breadcrumb: BreadcrumbService, private breadcrumb: BreadcrumbService,
private cache: CacheService, private cache: CacheService,
private _uiModal: UiModalService, private _uiModal: UiModalService,
private _checkoutService: DomainCheckoutService private _checkoutService: DomainCheckoutService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -72,7 +104,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id; const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id;
if (processChanged) { if (processChanged) {
if (!!this.searchService.processId && this.searchService.filter instanceof UiFilter) { if (!!this.searchService.processId && this.searchService.filter instanceof Filter) {
this.cacheCurrentData( this.cacheCurrentData(
this.searchService.processId, this.searchService.processId,
this.searchService.filter.getQueryParams(), this.searchService.filter.getQueryParams(),
@@ -87,7 +119,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.searchService.setBranch(selectedBranch); this.searchService.setBranch(selectedBranch);
} }
if (!(this.searchService.filter instanceof UiFilter)) { if (!(this.searchService.filter instanceof Filter)) {
await this.searchService.setDefaultFilter(); await this.searchService.setDefaultFilter();
} }
@@ -117,6 +149,27 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
await this.removeDetailsBreadcrumb(processId); await this.removeDetailsBreadcrumb(processId);
}) })
); );
this.subscriptions.add(
this.searchService.searchCompleted.pipe(withLatestFrom(this.application.activatedProcessId$)).subscribe(([state, processId]) => {
if (state.searchState === '') {
const params = state.filter.getQueryParams();
if ((state.hits === 1 && this.isTablet) || (!this.isTablet && !this._navigationService.mainOutletActive(this.route))) {
const item = state.items.find((f) => f);
this._navigationService.navigateToDetails({
processId,
itemId: item.id,
queryParams: this.isTablet ? undefined : params,
});
} else if (this.isTablet || this._navigationService.mainOutletActive(this.route)) {
this._navigationService.navigateToResults({
processId,
queryParams: params,
});
}
}
})
);
} }
ngOnDestroy() { ngOnDestroy() {
@@ -127,7 +180,26 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.unselectAll(); this.unselectAll();
} }
search() { resetQueryParamsQueryAndOrderBy(params: Record<string, string> = {}) {
const clean = { ...params };
for (const key in clean) {
if (key === 'main_qs') {
clean[key] = undefined;
} else if (key?.includes('order_by')) {
delete clean[key];
}
}
return clean;
}
search(filter?: Filter) {
if (!!filter) {
this.sharedFilterInputGroupMain.cancelAutocomplete();
this.searchService.setFilter(filter);
}
this.searchService.search({ clear: true }); this.searchService.search({ clear: true });
} }
@@ -187,7 +259,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
await this.breadcrumb.addBreadcrumbIfNotExists({ await this.breadcrumb.addBreadcrumbIfNotExists({
key: processId, key: processId,
name, name,
path: `/kunde/${this.application.activatedProcessId}/product/search/results`, path: this._navigationService.getArticleSearchResultsPath(this.application.activatedProcessId),
params: queryParams, params: queryParams,
section: 'customer', section: 'customer',
tags: ['catalog', 'filter', 'results'], tags: ['catalog', 'filter', 'results'],
@@ -250,29 +322,44 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.searchService.patchState({ selectedItemIds: [] }); this.searchService.patchState({ selectedItemIds: [] });
} }
async addSelectedItemsToCart() { async addToCart(item?: ItemDTO) {
this.loading$.next(true); this.loading$.next(true);
const selectedItems = await this.selectedItems$.pipe(first()).toPromise();
if (!!item) {
await this.addItemsToCart(item);
} else {
await this.addItemsToCart();
}
this.loading$.next(false);
}
private _createShoppingCartItem(item: ItemDTO): AddToShoppingCartDTO {
return {
quantity: 1,
availability: {
availabilityType: item?.catalogAvailability?.status,
price: item?.catalogAvailability?.price,
supplierProductNumber: item?.ids?.dig ? String(item?.ids?.dig) : item?.product?.supplierProductNumber,
},
product: {
catalogProductNumber: String(item?.id),
...item?.product,
},
itemType: item?.type,
promotion: { points: item?.promoPoints },
};
}
async addItemsToCart(item?: ItemDTO) {
const selectedItems = !item ? await this.selectedItems$.pipe(first()).toPromise() : [item];
const items: AddToShoppingCartDTO[] = []; const items: AddToShoppingCartDTO[] = [];
const canAddItemsPayload = []; const canAddItemsPayload = [];
for (const item of selectedItems) { for (const item of selectedItems) {
const shoppingCartItem = this._createShoppingCartItem(item);
const isDownload = item?.product?.format === 'EB' || item?.product?.format === 'DL'; const isDownload = item?.product?.format === 'EB' || item?.product?.format === 'DL';
const shoppingCartItem: AddToShoppingCartDTO = {
quantity: 1,
availability: {
availabilityType: item?.catalogAvailability?.status,
price: item?.catalogAvailability?.price,
supplierProductNumber: item?.ids?.dig ? String(item.ids?.dig) : item?.product?.supplierProductNumber,
},
product: {
catalogProductNumber: String(item?.id),
...item?.product,
},
itemType: item.type,
promotion: { points: item?.promoPoints },
};
if (isDownload) { if (isDownload) {
shoppingCartItem.destination = { data: { target: 16 } }; shoppingCartItem.destination = { data: { target: 16 } };
canAddItemsPayload.push({ canAddItemsPayload.push({

View File

@@ -8,7 +8,6 @@ import { UiCommonModule } from '@ui/common';
import { UiIconModule } from '@ui/icon'; import { UiIconModule } from '@ui/icon';
import { UiSelectBulletModule } from '@ui/select-bullet'; import { UiSelectBulletModule } from '@ui/select-bullet';
import { UiTooltipModule } from '@ui/tooltip'; import { UiTooltipModule } from '@ui/tooltip';
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 { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component'; import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
import { StockInfosPipe } from './order-by-filter/stick-infos.pipe'; import { StockInfosPipe } from './order-by-filter/stick-infos.pipe';
@@ -16,6 +15,9 @@ import { SearchResultItemLoadingComponent } from './search-result-item-loading.c
import { SearchResultItemComponent } from './search-result-item.component'; import { SearchResultItemComponent } from './search-result-item.component';
import { ArticleSearchResultsComponent } from './search-results.component'; import { ArticleSearchResultsComponent } from './search-results.component';
import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe'; import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe';
import { FilterAutocompleteProvider, FilterNextModule, OrderByFilterModule } from 'apps/shared/components/filter/src/lib';
import { FocusSearchboxEvent } from '../focus-searchbox.event';
import { ArticleSearchMainAutocompleteProvider } from '../providers';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -27,9 +29,10 @@ import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe
UiIconModule, UiIconModule,
UiSelectBulletModule, UiSelectBulletModule,
UiSpinnerModule, UiSpinnerModule,
UiOrderByFilterModule, OrderByFilterModule,
ScrollingModule, ScrollingModule,
UiTooltipModule, UiTooltipModule,
FilterNextModule,
], ],
exports: [ArticleSearchResultsComponent, SearchResultItemComponent], exports: [ArticleSearchResultsComponent, SearchResultItemComponent],
declarations: [ declarations: [
@@ -40,6 +43,13 @@ import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe
SearchResultSelectedPipe, SearchResultSelectedPipe,
AddedToCartModalComponent, AddedToCartModalComponent,
], ],
providers: [], providers: [
FocusSearchboxEvent,
{
provide: FilterAutocompleteProvider,
useClass: ArticleSearchMainAutocompleteProvider,
multi: true,
},
],
}) })
export class SearchResultsModule {} export class SearchResultsModule {}

View File

@@ -2,10 +2,65 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { ArticleDetailsComponent } from './article-details/article-details.component'; import { ArticleDetailsComponent } from './article-details/article-details.component';
import { ArticleSearchComponent } from './article-search/article-search.component'; import { ArticleSearchComponent } from './article-search/article-search.component';
import { ArticleSearchFilterComponent } from './article-search/search-filter/search-filter.component';
import { ArticleSearchMainComponent } from './article-search/search-main/search-main.component'; import { ArticleSearchMainComponent } from './article-search/search-main/search-main.component';
import { ArticleSearchResultsComponent } from './article-search/search-results/search-results.component'; import { ArticleSearchResultsComponent } from './article-search/search-results/search-results.component';
import { PageCatalogComponent } from './page-catalog.component'; import { PageCatalogComponent } from './page-catalog.component';
const auxiliaryRoutes = [
{
path: 'search',
component: ArticleSearchComponent,
outlet: 'left',
children: [
{
path: '',
component: ArticleSearchMainComponent,
},
],
},
{
path: 'filter',
component: ArticleSearchFilterComponent,
outlet: 'right',
},
{
path: 'filter/:id',
component: ArticleSearchFilterComponent,
outlet: 'right',
},
{
path: 'results',
component: ArticleSearchResultsComponent,
outlet: 'left',
},
{
path: 'results',
component: ArticleSearchResultsComponent,
outlet: 'main',
},
{
path: 'results/:id',
component: ArticleSearchResultsComponent,
outlet: 'left',
},
{
path: 'results/ean/:ean',
component: ArticleSearchResultsComponent,
outlet: 'left',
},
{
path: 'details/ean/:ean',
component: ArticleDetailsComponent,
outlet: 'right',
},
{
path: 'details/:id',
component: ArticleDetailsComponent,
outlet: 'right',
},
];
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
@@ -19,12 +74,16 @@ const routes: Routes = [
path: '', path: '',
component: ArticleSearchMainComponent, component: ArticleSearchMainComponent,
}, },
{
path: 'results',
component: ArticleSearchResultsComponent,
},
], ],
}, },
{
path: 'results',
component: ArticleSearchResultsComponent,
},
{
path: 'filter',
component: ArticleSearchFilterComponent,
},
{ {
path: 'details/ean/:ean', path: 'details/ean/:ean',
component: ArticleDetailsComponent, component: ArticleDetailsComponent,
@@ -33,6 +92,7 @@ const routes: Routes = [
path: 'details/:id', path: 'details/:id',
component: ArticleDetailsComponent, component: ArticleDetailsComponent,
}, },
...auxiliaryRoutes,
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',

View File

@@ -8,4 +8,25 @@
> >
</shared-branch-selector> </shared-branch-selector>
</shared-breadcrumb> </shared-breadcrumb>
<router-outlet></router-outlet>
<ng-container *ngIf="routerEvents$ | async">
<ng-container *ngIf="!(isDesktop$ | async); else desktop">
<router-outlet></router-outlet>
</ng-container>
<ng-template #desktop>
<ng-container *ngIf="showMainOutlet$ | async">
<router-outlet name="main"></router-outlet>
</ng-container>
<div class="grid grid-cols-[minmax(31rem,.5fr)_1fr] gap-6">
<div *ngIf="showLeftOutlet$ | async" class="block">
<router-outlet name="left"></router-outlet>
</div>
<div *ngIf="showRightOutlet$ | async">
<router-outlet name="right"></router-outlet>
</div>
</div>
</ng-template>
</ng-container>

View File

@@ -1,5 +1,5 @@
:host { :host {
@apply block relative; @apply block relative h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
} }
shell-breadcrumb { shell-breadcrumb {

View File

@@ -1,4 +1,5 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ApplicationService } from '@core/application'; import { ApplicationService } from '@core/application';
import { AuthService } from '@core/auth'; import { AuthService } from '@core/auth';
import { EnvironmentService } from '@core/environment'; import { EnvironmentService } from '@core/environment';
@@ -6,8 +7,8 @@ import { BranchSelectorComponent } from '@shared/components/branch-selector';
import { BreadcrumbComponent } from '@shared/components/breadcrumb'; import { BreadcrumbComponent } from '@shared/components/breadcrumb';
import { BranchDTO } from '@swagger/checkout'; import { BranchDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalService } from '@ui/modal'; import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { BehaviorSubject, from, fromEvent, Observable, Subject } from 'rxjs'; import { fromEvent, Observable, Subject } from 'rxjs';
import { first, map, switchMap, takeUntil } from 'rxjs/operators'; import { first, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
@Component({ @Component({
selector: 'page-catalog', selector: 'page-catalog',
@@ -28,16 +29,41 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
_onDestroy$ = new Subject<boolean>(); _onDestroy$ = new Subject<boolean>();
get isTablet$() {
return this._environmentService.matchTablet$.pipe(
map((state) => state.matches),
shareReplay()
);
}
get isDesktop$() {
return this._environmentService.matchDesktop$.pipe(
map((state) => {
return state.matches;
}),
shareReplay()
);
}
routerEvents$ = this._router.events.pipe(shareReplay());
showMainOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'main')));
showLeftOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'left')));
showRightOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'right')));
constructor( constructor(
public application: ApplicationService, public application: ApplicationService,
private _uiModal: UiModalService, private _uiModal: UiModalService,
public auth: AuthService, public auth: AuthService,
private _environmentService: EnvironmentService, private _environmentService: EnvironmentService,
private _renderer: Renderer2 private _renderer: Renderer2,
private _activatedRoute: ActivatedRoute,
private _router: Router
) {} ) {}
ngOnInit() { ngOnInit() {
// this.auth.getClaims();
this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId))); this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId)));
this.selectedBranch$ = this.activatedProcessId$.pipe(switchMap((processId) => this.application.getSelectedBranch$(Number(processId)))); this.selectedBranch$ = this.activatedProcessId$.pipe(switchMap((processId) => this.application.getSelectedBranch$(Number(processId))));
@@ -46,19 +72,19 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
if (this._environmentService.isTablet()) { fromEvent(this.branchSelectorRef.nativeElement, 'focusin')
fromEvent(this.branchSelectorRef.nativeElement, 'focusin') .pipe(takeUntil(this._onDestroy$), withLatestFrom(this.isTablet$))
.pipe(takeUntil(this._onDestroy$)) .subscribe(([_, isTablet]) => {
.subscribe((_) => { if (isTablet) {
this._renderer.setStyle(this.branchSelectorRef?.nativeElement, 'width', this.branchSelectorWidth); this._renderer.setStyle(this.branchSelectorRef?.nativeElement, 'width', this.branchSelectorWidth);
}); }
});
fromEvent(this.branchSelectorRef.nativeElement, 'focusout') fromEvent(this.branchSelectorRef.nativeElement, 'focusout')
.pipe(takeUntil(this._onDestroy$)) .pipe(takeUntil(this._onDestroy$))
.subscribe((_) => { .subscribe((_) => {
this._renderer.removeStyle(this.branchSelectorRef?.nativeElement, 'width'); this._renderer.removeStyle(this.branchSelectorRef?.nativeElement, 'width');
}); });
}
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb'; import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { DomainCustomerOrderService, DomainGoodsService, DomainOmsService, OrderItemsContext } from '@domain/oms'; import { DomainCustomerOrderService, DomainGoodsService, DomainOmsService, OrderItemsContext } from '@domain/oms';
import { ComponentStore, tapResponse } from '@ngrx/component-store'; import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ListResponseArgsOfOrderItemListItemDTO, OrderItemListItemDTO, OrderItemProcessingStatusValue } from '@swagger/oms'; import { ListResponseArgsOfOrderItemListItemDTO, OrderItemListItemDTO, OrderItemProcessingStatusValue } from '@swagger/oms';
@@ -63,13 +64,18 @@ export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderD
private _onDestroy$ = new Subject(); private _onDestroy$ = new Subject();
get isTablet() {
return this._environment.matchTablet();
}
constructor( constructor(
private _activatedRoute: ActivatedRoute, private _activatedRoute: ActivatedRoute,
private _domainGoodsInService: DomainCustomerOrderService, private _domainGoodsInService: DomainCustomerOrderService,
private _omsService: DomainOmsService, private _omsService: DomainOmsService,
private _breadcrumb: BreadcrumbService, private _breadcrumb: BreadcrumbService,
private _router: Router, private _router: Router,
private _uiModal: UiModalService private _uiModal: UiModalService,
private _environment: EnvironmentService
) { ) {
super({ super({
fetching: false, fetching: false,
@@ -187,19 +193,37 @@ export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderD
} }
navigateToEditPage(orderItem: OrderItemListItemDTO) { navigateToEditPage(orderItem: OrderItemListItemDTO) {
// if (this.isTablet) {
this._router.navigate([this.getEditPath(orderItem)], { this._router.navigate([this.getEditPath(orderItem)], {
queryParams: { orderNumber: orderItem.orderNumber, buyerNumber: orderItem.buyerNumber, archive: this.archive }, queryParams: { buyerNumber: orderItem.buyerNumber, archive: this.archive },
}); });
// } else {
// this._router.navigate(this.getEditPathDesktop(orderItem), {
// queryParamsHandling: 'preserve',
// queryParams: { buyerNumber: orderItem.buyerNumber, archive: this.archive },
// });
// }
} }
navigateToDetailsPage(item: OrderItemListItemDTO) { navigateToDetailsPage(item: OrderItemListItemDTO) {
// if (this.isTablet) {
this._router.navigate([this.getDetailsPath(item)], { this._router.navigate([this.getDetailsPath(item)], {
queryParams: { buyerNumber: item.buyerNumber, archive: this.archive }, queryParams: { buyerNumber: item.buyerNumber, archive: this.archive },
}); });
// } else {
// this._router.navigate(this.getDetailsPathDesktop(item), {
// queryParamsHandling: 'preserve',
// queryParams: { buyerNumber: item.buyerNumber, archive: this.archive },
// });
// }
} }
navigateToLandingPage() { navigateToLandingPage() {
// if (this.isTablet) {
this._router.navigate([`/kunde/${this.processId}/order`]); this._router.navigate([`/kunde/${this.processId}/order`]);
// } else {
// this._router.navigate(['/kunde', this.processId, 'order', { outlets: { main: null, left: 'search', right: 'filter' } }]);
// }
} }
async actionHandled(handler: { orderItemsContext: OrderItemsContext; command: string; navigation: 'details' | 'main' | 'reservation' }) { async actionHandled(handler: { orderItemsContext: OrderItemsContext; command: string; navigation: 'details' | 'main' | 'reservation' }) {
@@ -218,9 +242,65 @@ export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderD
: `/kunde/${this.processId}/order/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}`; : `/kunde/${this.processId}/order/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}`;
} }
// getDetailsPathDesktop(item: OrderItemListItemDTO) {
// return item?.compartmentCode
// ? [
// '/kunde',
// this.processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(item?.compartmentCode), item?.processingStatus],
// },
// },
// ]
// : [
// '/kunde',
// this.processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'order', encodeURIComponent(item?.orderNumber), item?.processingStatus],
// },
// },
// ];
// }
getEditPath(item: OrderItemListItemDTO) { getEditPath(item: OrderItemListItemDTO) {
return item?.compartmentCode return item?.compartmentCode
? `/kunde/${this.processId}/order/details/compartment/${encodeURIComponent(item?.compartmentCode)}/${item?.processingStatus}/edit` ? `/kunde/${this.processId}/order/details/compartment/${encodeURIComponent(item?.compartmentCode)}/${item?.processingStatus}/edit`
: `/kunde/${this.processId}/order/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}/edit`; : `/kunde/${this.processId}/order/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}/edit`;
} }
// getEditPathDesktop(item: OrderItemListItemDTO) {
// return item?.compartmentCode
// ? [
// '/kunde',
// this.processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(item?.compartmentCode), item?.processingStatus, 'edit'],
// },
// },
// ]
// : [
// '/kunde',
// this.processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'order', encodeURIComponent(item?.orderNumber), item?.processingStatus, 'edit'],
// },
// },
// ];
// }
} }

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb'; import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { DomainCustomerOrderService, DomainGoodsService, DomainOmsService } from '@domain/oms'; import { DomainCustomerOrderService, DomainGoodsService, DomainOmsService } from '@domain/oms';
import { OrderItemListItemDTO } from '@swagger/oms'; import { OrderItemListItemDTO } from '@swagger/oms';
import { UiMessageModalComponent, UiModalService } from '@ui/modal'; import { UiMessageModalComponent, UiModalService } from '@ui/modal';
@@ -48,12 +49,17 @@ export class CustomerOrderEditComponent implements OnInit {
shareReplay() shareReplay()
); );
get isTablet() {
return this._environment.matchTablet();
}
constructor( constructor(
private _activatedRoute: ActivatedRoute, private _activatedRoute: ActivatedRoute,
private _breadcrumb: BreadcrumbService, private _breadcrumb: BreadcrumbService,
private _domainGoodsInService: DomainCustomerOrderService, private _domainGoodsInService: DomainCustomerOrderService,
private _router: Router, private _router: Router,
private _uiModal: UiModalService private _uiModal: UiModalService,
private _environment: EnvironmentService
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -84,6 +90,8 @@ export class CustomerOrderEditComponent implements OnInit {
const processingStatus = options?.processingStatus ? options.processingStatus : this._activatedRoute.snapshot.params.processingStatus; const processingStatus = options?.processingStatus ? options.processingStatus : this._activatedRoute.snapshot.params.processingStatus;
const buyerNumber = this._activatedRoute.snapshot.queryParams.buyerNumber; const buyerNumber = this._activatedRoute.snapshot.queryParams.buyerNumber;
const archive = this._activatedRoute.snapshot.queryParams.archive; const archive = this._activatedRoute.snapshot.queryParams.archive;
// if (this.isTablet) {
compartmentCode compartmentCode
? this._router.navigate( ? this._router.navigate(
[`/kunde/${this.processId}/order/details/compartment/${encodeURIComponent(compartmentCode)}/${processingStatus}`], [`/kunde/${this.processId}/order/details/compartment/${encodeURIComponent(compartmentCode)}/${processingStatus}`],
@@ -92,6 +100,42 @@ export class CustomerOrderEditComponent implements OnInit {
: this._router.navigate([`/kunde/${this.processId}/order/details/order/${encodeURIComponent(orderNumber)}/${processingStatus}`], { : this._router.navigate([`/kunde/${this.processId}/order/details/order/${encodeURIComponent(orderNumber)}/${processingStatus}`], {
queryParams: { buyerNumber, archive }, queryParams: { buyerNumber, archive },
}); });
// } else {
// compartmentCode
// ? this._router.navigate(
// [
// '/kunde',
// this.processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(compartmentCode), processingStatus],
// },
// },
// ],
// { queryParamsHandling: 'preserve', queryParams: { buyerNumber, archive } }
// )
// : this._router.navigate(
// [
// '/kunde',
// this.processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'order', encodeURIComponent(orderNumber), processingStatus],
// },
// },
// ],
// {
// queryParamsHandling: 'preserve',
// queryParams: { buyerNumber, archive },
// }
// );
// }
} }
openModalIfItemsHaveDifferentCustomers(items: OrderItemListItemDTO[]) { openModalIfItemsHaveDifferentCustomers(items: OrderItemListItemDTO[]) {

View File

@@ -2,11 +2,70 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { CustomerOrderDetailsComponent, CustomerOrderDetailsModule } from './customer-order-details'; import { CustomerOrderDetailsComponent, CustomerOrderDetailsModule } from './customer-order-details';
import { CustomerOrderEditComponent } from './customer-order-edit'; import { CustomerOrderEditComponent } from './customer-order-edit';
import { CustomerOrderSearchComponent, CustomerOrderSearchModule } from './customer-order-search'; import { CustomerOrderSearchComponent, CustomerOrderSearchFilterComponent, CustomerOrderSearchModule } from './customer-order-search';
import { CustomerOrderSearchMainComponent, CustomerOrderSearchMainModule } from './customer-order-search/search-main'; import { CustomerOrderSearchMainComponent, CustomerOrderSearchMainModule } from './customer-order-search/search-main';
import { CustomerOrderSearchResultsComponent, CustomerOrderSearchResultsModule } from './customer-order-search/search-results'; import { CustomerOrderSearchResultsComponent, CustomerOrderSearchResultsModule } from './customer-order-search/search-results';
import { CustomerOrderComponent } from './customer-order.component'; import { CustomerOrderComponent } from './customer-order.component';
// const auxiliaryRoutes = [
// {
// path: 'search',
// component: CustomerOrderSearchComponent,
// outlet: 'left',
// children: [
// {
// path: '',
// component: CustomerOrderSearchMainComponent,
// },
// ],
// },
// {
// path: 'filter',
// component: CustomerOrderSearchFilterComponent,
// outlet: 'right',
// },
// {
// path: 'filter/:compartmentCode/:processingStatus',
// component: CustomerOrderSearchFilterComponent,
// outlet: 'right',
// },
// {
// path: 'filter/:orderNumber/:processingStatus',
// component: CustomerOrderSearchFilterComponent,
// outlet: 'right',
// },
// {
// path: 'results',
// component: CustomerOrderSearchResultsComponent,
// outlet: 'left',
// },
// {
// path: 'results',
// component: CustomerOrderSearchResultsComponent,
// outlet: 'main',
// },
// {
// path: 'details/compartment/:compartmentCode/:processingStatus',
// component: CustomerOrderDetailsComponent,
// outlet: 'right',
// },
// {
// path: 'details/order/:orderNumber/:processingStatus',
// component: CustomerOrderDetailsComponent,
// outlet: 'right',
// },
// {
// path: 'details/compartment/:compartmentCode/:processingStatus/edit',
// component: CustomerOrderEditComponent,
// outlet: 'right',
// },
// {
// path: 'details/order/:orderNumber/:processingStatus/edit',
// component: CustomerOrderEditComponent,
// outlet: 'right',
// },
// ];
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
@@ -20,6 +79,10 @@ const routes: Routes = [
{ path: 'results', component: CustomerOrderSearchResultsComponent }, { path: 'results', component: CustomerOrderSearchResultsComponent },
], ],
}, },
{
path: 'filter',
component: CustomerOrderSearchFilterComponent,
},
{ {
path: 'details/compartment/:compartmentCode/:processingStatus', path: 'details/compartment/:compartmentCode/:processingStatus',
component: CustomerOrderDetailsComponent, component: CustomerOrderDetailsComponent,
@@ -36,6 +99,7 @@ const routes: Routes = [
path: 'details/order/:orderNumber/:processingStatus/edit', path: 'details/order/:orderNumber/:processingStatus/edit',
component: CustomerOrderEditComponent, component: CustomerOrderEditComponent,
}, },
// ...auxiliaryRoutes,
], ],
}, },
]; ];

View File

@@ -1,31 +1,33 @@
<div class="goods-out-search-filter-content"> <ng-container *ngIf="filter$ | async; let filter">
<button class="btn-close" type="button" (click)="close.emit()"> <div class="goods-out-search-filter-content">
<ui-icon icon="close" size="20px"></ui-icon> <a class="btn-close" type="button" [routerLink]="closeFilterRouterPath" queryParamsHandling="preserve">
</button> <ui-icon icon="close" size="20px"></ui-icon>
</a>
<div class="goods-out-search-filter-content-main"> <div class="goods-out-search-filter-content-main">
<h1 class="title">Filter</h1> <h1 class="title">Filter</h1>
<ui-filter <ui-filter
[filter]="filter" [filter]="filter"
[loading]="loading$ | async" [loading]="loading$ | async"
(search)="applyFilter()" (search)="applyFilter(filter)"
[hint]="message" [hint]="message"
resizeInputOptionsToElement="page-goods-out-search-filter .cta-wrapper" resizeInputOptionsToElement="page-goods-out-search-filter .cta-wrapper"
[scanner]="true" [scanner]="true"
> >
<page-order-branch-id-input *uiFilterCustomInput="'order_branch_id'; let input" [input]="input"> </page-order-branch-id-input> <page-order-branch-id-input *uiFilterCustomInput="'order_branch_id'; let input" [input]="input"> </page-order-branch-id-input>
</ui-filter> </ui-filter>
</div>
</div> </div>
</div>
<div class="cta-wrapper"> <div class="cta-wrapper">
<button class="cta-reset-filter" (click)="resetFilter()" [disabled]="loading$ | async"> <button class="cta-reset-filter" (click)="resetFilter(filter)" [disabled]="loading$ | async">
Filter zurücksetzen Filter zurücksetzen
</button> </button>
<button class="cta-apply-filter" (click)="applyFilter()" [disabled]="loading$ | async"> <button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="loading$ | async">
<ui-spinner [show]="loading$ | async"> <ui-spinner [show]="loading$ | async">
Filter anwenden Filter anwenden
</ui-spinner> </ui-spinner>
</button> </button>
</div> </div>
</ng-container>

View File

@@ -22,9 +22,7 @@
} }
.cta-wrapper { .cta-wrapper {
@apply fixed bottom-8 whitespace-nowrap; @apply text-center whitespace-nowrap;
left: 50%;
transform: translateX(-50%);
} }
.cta-reset-filter, .cta-reset-filter,

View File

@@ -9,13 +9,15 @@ import {
Input, Input,
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { Router } from '@angular/router'; import { ActivatedRoute, Params, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb'; import { BreadcrumbService } from '@core/breadcrumb';
import { OrderItemListItemDTO } from '@swagger/oms'; import { OrderItemListItemDTO } from '@swagger/oms';
import { UiFilter, UiFilterComponent } from '@ui/filter'; import { UiFilter, UiFilterComponent } from '@ui/filter';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators'; import { map, take, takeUntil } from 'rxjs/operators';
import { CustomerOrderSearchStore } from '../customer-order-search.store'; import { CustomerOrderSearchStore } from '../customer-order-search.store';
import { EnvironmentService } from '@core/environment';
import { ApplicationService } from '@core/application';
@Component({ @Component({
selector: 'page-customer-order-search-filter', selector: 'page-customer-order-search-filter',
@@ -27,12 +29,14 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
@Output() @Output()
close = new EventEmitter(); close = new EventEmitter();
filter: UiFilter;
loading$: Observable<boolean>; loading$: Observable<boolean>;
message: string; message: string;
get isTablet() {
return this._environment.matchTablet();
}
@Input() @Input()
processId: number; processId: number;
@@ -41,11 +45,57 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
@ViewChild(UiFilterComponent, { static: false }) @ViewChild(UiFilterComponent, { static: false })
uiFilter: UiFilterComponent; uiFilter: UiFilterComponent;
filter$: Observable<UiFilter>;
get leftOutletLocation(): string {
return this._activatedRoute?.parent?.children?.find((childRoute) => childRoute?.outlet === 'left')?.snapshot?.routeConfig?.path ?? '';
}
get rightOutletParams(): Params {
return this._activatedRoute?.parent?.children?.find((childRoute) => childRoute?.outlet === 'right')?.snapshot?.params;
}
get closeFilterRouterPath() {
// if (!this.isTablet) {
// if (this.leftOutletLocation.includes('results')) {
// const params = this.rightOutletParams;
// const orderNumber = params?.orderNumber;
// const compartmentCode = params?.compartmentCode;
// const processingStatus = params?.processingStatus;
// return orderNumber
// ? [
// '/kunde',
// this.application.activatedProcessId,
// 'order',
// { outlets: { main: null, left: 'results', right: ['details', 'order', orderNumber, processingStatus] } },
// ]
// : [
// '/kunde',
// this.application.activatedProcessId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', compartmentCode, processingStatus],
// },
// },
// ];
// }
// return ['/kunde', this.application.activatedProcessId, 'order', { outlets: { main: null, left: 'search', right: 'filter' } }];
// } else {
return ['/kunde', this.application.activatedProcessId, 'order', 'results'];
// }
}
constructor( constructor(
private _goodsOutSearchStore: CustomerOrderSearchStore, private _goodsOutSearchStore: CustomerOrderSearchStore,
private _breadcrumb: BreadcrumbService, private _breadcrumb: BreadcrumbService,
private _cdr: ChangeDetectorRef, private _cdr: ChangeDetectorRef,
private _router: Router private _router: Router,
private _environment: EnvironmentService,
private _activatedRoute: ActivatedRoute,
public application: ApplicationService
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -59,23 +109,23 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
} }
private _initSettings() { private _initSettings() {
this.filter = UiFilter.create(this._goodsOutSearchStore.filter); this.filter$ = this._goodsOutSearchStore.filter$.pipe(map((filter) => UiFilter.create(filter)));
} }
private _initLoading$() { private _initLoading$() {
this.loading$ = this._goodsOutSearchStore.fetching$; this.loading$ = this._goodsOutSearchStore.fetching$;
} }
async resetFilter() { async resetFilter(filter: UiFilter) {
const queryParams = { main_qs: this.filter?.getQueryParams()?.main_qs || '' }; const queryParams = { main_qs: filter?.getQueryParams()?.main_qs || '' };
this._goodsOutSearchStore.resetFilter(queryParams); this._goodsOutSearchStore.resetFilter(queryParams);
this._initSettings(); this._initSettings();
this._cdr.markForCheck(); this._cdr.markForCheck();
} }
async applyFilter() { async applyFilter(filter: UiFilter) {
this.uiFilter?.cancelAutocomplete(); this.uiFilter?.cancelAutocomplete();
this._goodsOutSearchStore.setFilter(this.filter); this._goodsOutSearchStore.setFilter(filter);
this.message = undefined; this.message = undefined;
await this.updateQueryParams(); await this.updateQueryParams();
@@ -84,6 +134,7 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
if (result.results.error) { if (result.results.error) {
} else { } else {
if (result.results.hits > 0) { if (result.results.hits > 0) {
// if (this.isTablet) {
if (result.results.hits === 1) { if (result.results.hits === 1) {
const orderItem = result.results.result[0]; const orderItem = result.results.result[0];
this._router.navigate([this.getDetailsPath(orderItem)]); this._router.navigate([this.getDetailsPath(orderItem)]);
@@ -92,6 +143,18 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
queryParams: this._goodsOutSearchStore.filter.getQueryParams(), queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
}); });
} }
// } else {
// const orderItem = result.results.result[0];
// if (result.results.hits === 1) {
// this._router.navigate(this.getDetailsPathDesktop(orderItem, this.processId), {
// queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
// });
// } else {
// this._router.navigate(this.getDetailsPathDesktop(orderItem, this.processId), {
// queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
// });
// }
// }
this.close.emit(); this.close.emit();
} else { } else {
@@ -126,4 +189,32 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
? `/kunde/${this.processId}/order/details/compartment/${encodeURIComponent(item?.compartmentCode)}/${item?.processingStatus}` ? `/kunde/${this.processId}/order/details/compartment/${encodeURIComponent(item?.compartmentCode)}/${item?.processingStatus}`
: `/kunde/${this.processId}/order/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}`; : `/kunde/${this.processId}/order/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}`;
} }
// getDetailsPathDesktop(item: OrderItemListItemDTO, processId: number) {
// return item?.compartmentCode
// ? [
// '/kunde',
// processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(item?.compartmentCode), item?.processingStatus],
// },
// },
// ]
// : [
// '/kunde',
// processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'order', encodeURIComponent(item?.orderNumber), item?.processingStatus],
// },
// },
// ];
// }
} }

View File

@@ -25,18 +25,31 @@ export class OrderBranchIdInputComponent extends AbstractUiFilterInputDirective
ngOnInit() { ngOnInit() {
this.control.setValue({ id: Number(this.value) }); this.control.setValue({ id: Number(this.value) });
const uiInputChangesSub = this.uiInput?.changes?.subscribe((changes) => {
const controlValue = this.control?.value?.id;
const changesValue = Number(changes?.target?.value);
if (controlValue !== changesValue) {
this.control.setValue(changesValue && !isNaN(changesValue) ? { id: changesValue } : undefined);
}
});
const onInputChangeSub = this.onUiInputChange$.subscribe((input) => { const onInputChangeSub = this.onUiInputChange$.subscribe((input) => {
if (this.control.value !== input.value) { const controlValue = this.control?.value?.id;
this.control.setValue(input.value ? { id: Number(input.value) } : undefined); const inputValue = Number(input?.value);
if (controlValue !== inputValue) {
this.control.setValue(inputValue && !isNaN(inputValue) ? { id: inputValue } : undefined);
} }
}); });
const onControlValueChangeSub = this.control.valueChanges.subscribe((value) => { const onControlValueChangeSub = this.control.valueChanges.subscribe((value) => {
if (this.value !== value) { if (!value) {
this.setValue(value ? String(value?.id) : undefined); this.setValue(undefined);
} else if (this.value !== String(value?.id)) {
this.setValue(String(value?.id));
} }
}); });
this._subscriptions.add(uiInputChangesSub);
this._subscriptions.add(onInputChangeSub); this._subscriptions.add(onInputChangeSub);
this._subscriptions.add(onControlValueChangeSub); this._subscriptions.add(onControlValueChangeSub);
} }

View File

@@ -1,9 +1,9 @@
<div class="flex flex-row justify-end mb-4"> <!-- <div *ngIf="isTablet" class="flex flex-row justify-end mb-4">
<button class="filter" [class.active]="hasFilter$ | async" (click)="shellFilterOverlay.open()"> <button class="filter" [class.active]="hasFilter$ | async" (click)="shellFilterOverlay.open()">
<ui-icon size="20px" icon="filter_alit"></ui-icon> <ui-icon size="20px" icon="filter_alit"></ui-icon>
<span class="label">Filter</span> <span class="label">Filter</span>
</button> </button>
</div> </div> -->
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@@ -8,6 +8,7 @@ import { combineLatest, Subject } from 'rxjs';
import { map, takeUntil, withLatestFrom } from 'rxjs/operators'; import { map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { CustomerOrderSearchStore } from './customer-order-search.store'; import { CustomerOrderSearchStore } from './customer-order-search.store';
import { CustomerOrderSearchMainAutocompleteProvider } from './providers/customer-order-search-main-autocomplete.provider'; import { CustomerOrderSearchMainAutocompleteProvider } from './providers/customer-order-search-main-autocomplete.provider';
import { EnvironmentService } from '@core/environment';
@Component({ @Component({
selector: 'page-customer-order-search', selector: 'page-customer-order-search',
@@ -15,7 +16,6 @@ import { CustomerOrderSearchMainAutocompleteProvider } from './providers/custome
styleUrls: ['customer-order-search.component.scss'], styleUrls: ['customer-order-search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ providers: [
CustomerOrderSearchStore,
{ {
provide: UiFilterAutocompleteProvider, provide: UiFilterAutocompleteProvider,
useClass: CustomerOrderSearchMainAutocompleteProvider, useClass: CustomerOrderSearchMainAutocompleteProvider,
@@ -32,28 +32,25 @@ import { CustomerOrderSearchMainAutocompleteProvider } from './providers/custome
export class CustomerOrderSearchComponent implements OnInit, OnDestroy { export class CustomerOrderSearchComponent implements OnInit, OnDestroy {
private _onDestroy$ = new Subject(); private _onDestroy$ = new Subject();
get isTablet() {
return this._environment.matchTablet();
}
hasFilter$ = combineLatest([this._goodsOutSearchStore.filter$, this._goodsOutSearchStore.defaultSettings$]).pipe( hasFilter$ = combineLatest([this._goodsOutSearchStore.filter$, this._goodsOutSearchStore.defaultSettings$]).pipe(
map(([filter, defaultFilter]) => !isEqual(filter?.getQueryParams(), UiFilter.create(defaultFilter).getQueryParams())) map(([filter, defaultFilter]) => !isEqual(filter?.getQueryParams(), UiFilter.create(defaultFilter).getQueryParams()))
); );
processId$ = this._activatedRoute.data.pipe(map((data) => +data.processId)); processId$ = this._activatedRoute.parent.data.pipe(map((data) => +data.processId));
constructor( constructor(
private _goodsOutSearchStore: CustomerOrderSearchStore, private _goodsOutSearchStore: CustomerOrderSearchStore,
private _breadcrumb: BreadcrumbService, private _breadcrumb: BreadcrumbService,
private _activatedRoute: ActivatedRoute private _activatedRoute: ActivatedRoute,
private _environment: EnvironmentService
) {} ) {}
ngOnInit() { ngOnInit() {
this.processId$.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._activatedRoute.queryParams)).subscribe(([processId]) => { this.processId$.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._activatedRoute.queryParams)).subscribe(([processId]) => {
// if (params && Object.keys(params).length === 0) {
// console.log('params is empty');
// this._goodsOutSearchStore.setQueryParams(params);
// this._goodsOutSearchStore.loadSettings();
// } else {
// // this._goodsOutSearchStore.resetFilter(params);
// }
this._breadcrumb.addOrUpdateBreadcrumbIfNotExists({ this._breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: processId, key: processId,
name: 'Kundenbestellung', name: 'Kundenbestellung',

View File

@@ -8,6 +8,7 @@ import { RouterModule } from '@angular/router';
import { UiFilterNextModule } from '@ui/filter'; import { UiFilterNextModule } from '@ui/filter';
import { SharedFilterOverlayModule } from '@shared/components/filter-overlay'; import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
import { UiSpinnerModule } from '@ui/spinner'; import { UiSpinnerModule } from '@ui/spinner';
import { CustomerOrderSearchStore } from './customer-order-search.store';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -20,6 +21,7 @@ import { UiSpinnerModule } from '@ui/spinner';
OrderBranchIdInputComponent, OrderBranchIdInputComponent,
], ],
exports: [CustomerOrderSearchComponent], exports: [CustomerOrderSearchComponent],
providers: [CustomerOrderSearchStore],
declarations: [CustomerOrderSearchComponent, CustomerOrderSearchFilterComponent], declarations: [CustomerOrderSearchComponent, CustomerOrderSearchFilterComponent],
}) })
export class CustomerOrderSearchModule {} export class CustomerOrderSearchModule {}

View File

@@ -7,7 +7,7 @@
} }
.search-main { .search-main {
@apply bg-white text-center rounded-t-card py-5 shadow-card; @apply bg-white text-center rounded-t-card p-5 shadow-card;
height: calc(100vh - 380px); height: calc(100vh - 380px);
.search-main-title { .search-main-title {

View File

@@ -7,6 +7,7 @@ import { debounce, isEqual } from 'lodash';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, first, map, withLatestFrom } from 'rxjs/operators'; import { debounceTime, first, map, withLatestFrom } from 'rxjs/operators';
import { CustomerOrderSearchStore } from '../customer-order-search.store'; import { CustomerOrderSearchStore } from '../customer-order-search.store';
import { EnvironmentService } from '@core/environment';
@Component({ @Component({
selector: 'page-customer-order-search-main', selector: 'page-customer-order-search-main',
@@ -28,10 +29,14 @@ export class CustomerOrderSearchMainComponent implements OnInit, OnDestroy {
private _subscriptions = new Subscription(); private _subscriptions = new Subscription();
get processId() { get processId() {
return +this._activatedRoute.snapshot.data.processId; return +this._activatedRoute.parent.parent.snapshot.data.processId;
} }
processId$ = this._activatedRoute.data.pipe(map((data) => +data.processId)); processId$ = this._activatedRoute.parent.parent.data.pipe(map((data) => +data.processId));
get isTablet() {
return this._environment.matchTablet();
}
@ViewChild(UiFilterInputGroupMainComponent, { static: false }) @ViewChild(UiFilterInputGroupMainComponent, { static: false })
filterInputGroup: UiFilterInputGroupMainComponent; filterInputGroup: UiFilterInputGroupMainComponent;
@@ -41,7 +46,8 @@ export class CustomerOrderSearchMainComponent implements OnInit, OnDestroy {
private _cdr: ChangeDetectorRef, private _cdr: ChangeDetectorRef,
private _router: Router, private _router: Router,
private _activatedRoute: ActivatedRoute, private _activatedRoute: ActivatedRoute,
private _breadcrumb: BreadcrumbService private _breadcrumb: BreadcrumbService,
private _environment: EnvironmentService
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -106,14 +112,29 @@ export class CustomerOrderSearchMainComponent implements OnInit, OnDestroy {
if (result.results.error) { if (result.results.error) {
} else { } else {
if (result.results.hits > 0) { if (result.results.hits > 0) {
// if (this.isTablet) {
if (result.results.hits === 1) { if (result.results.hits === 1) {
const orderItem = result.results.result[0]; const orderItem = result.results.result[0];
this._router.navigate([this.getDetailsPath(orderItem, this.processId)]); this._router.navigate([this.getDetailsPath(orderItem, this.processId)], {
queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
});
} else { } else {
this._router.navigate(['/kunde', this.processId, 'order', 'results'], { this._router.navigate(['/kunde', this.processId, 'order', 'results'], {
queryParams: this._goodsOutSearchStore.filter.getQueryParams(), queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
}); });
} }
// } else {
// const orderItem = result.results.result[0];
// if (result.results.hits === 1) {
// this._router.navigate(this.getDetailsPathDesktop(orderItem, this.processId), {
// queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
// });
// } else {
// this._router.navigate(this.getDetailsPathDesktop(orderItem, this.processId), {
// queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
// });
// }
// }
} else { } else {
this.message = 'keine Suchergebnisse'; this.message = 'keine Suchergebnisse';
} }
@@ -149,6 +170,34 @@ export class CustomerOrderSearchMainComponent implements OnInit, OnDestroy {
: `/kunde/${processId}/order/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}`; : `/kunde/${processId}/order/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}`;
} }
// getDetailsPathDesktop(item: OrderItemListItemDTO, processId: number) {
// return item?.compartmentCode
// ? [
// '/kunde',
// processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(item?.compartmentCode), item?.processingStatus],
// },
// },
// ]
// : [
// '/kunde',
// processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'order', encodeURIComponent(item?.orderNumber), item?.processingStatus],
// },
// },
// ];
// }
queryChangeDebounce = debounce(async () => { queryChangeDebounce = debounce(async () => {
this.queryChanged$.next(true); this.queryChanged$.next(true);
await this.updateQueryParams(this.processId); await this.updateQueryParams(this.processId);

View File

@@ -0,0 +1,45 @@
<a
class="page-customer-order-item__item-card p-5 desktop:p-px-10 h-[212px] desktop:h-[181px] bg-white rounded-card"
[routerLink]="detailsLink"
[queryParams]="queryParams"
[queryParamsHandling]="!isTablet ? 'preserve' : ''"
>
{{ item?.product?.name }}
<!-- <div class="page-search-result-item__item-thumbnail text-center mr-4 w-[50px] h-[79px]">
<img
class="page-search-result-item__item-image w-[50px] h-[79px]"
loading="lazy"
*ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl"
[src]="thumbnailUrl"
[alt]="item?.product?.name"
/>
</div>
<div class="grid-container-test">
<div
class="page-search-result-item__item-title font-bold text-2xl"
[class.text-xl]="item?.product?.name?.length >= 35"
[class.text-lg]="item?.product?.name?.length >= 40"
[class.text-md]="item?.product?.name?.length >= 50"
[class.text-sm]="item?.product?.name?.length >= 60 || !isTablet"
[class.text-xs]="item?.product?.name?.length >= 100 || (!isTablet && item?.product?.name?.length >= 70)"
>
{{ item?.product?.name }}
</div>
<div class="page-search-result-item__item-special-comment font-bold text-sm">
{{ item?.specialComment }}
</div>
<div class="page-search-result-item__item-select-bullet justify-self-end">
<input
*ngIf="selectable"
(click)="$event.stopPropagation()"
[ngModel]="selected$ | async"
(ngModelChange)="setSelected()"
class="isa-select-bullet"
type="checkbox"
/>
</div>
</div> -->
</a>

View File

@@ -0,0 +1,3 @@
:host {
@apply flex flex-col w-full h-[212px] desktop:h-[181px] bg-white;
}

View File

@@ -0,0 +1,124 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { ComponentStore } from '@ngrx/component-store';
import { OrderItemListItemDTO } from '@swagger/oms';
import { isEqual } from 'lodash';
import { map } from 'rxjs/operators';
import { CustomerOrderSearchStore } from '../customer-order-search.store';
export interface CustomerOrderItemComponentState {
item?: OrderItemListItemDTO;
selected: boolean;
selectable: boolean;
}
@Component({
selector: 'page-customer-order-item',
templateUrl: 'customer-order-item.component.html',
styleUrls: ['customer-order-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerOrderItemComponent extends ComponentStore<CustomerOrderItemComponentState> implements OnInit {
@Input()
get item() {
return this.get((s) => s.item);
}
set item(item: OrderItemListItemDTO) {
if (!isEqual(this.item, item)) {
this.patchState({ item });
}
}
readonly item$ = this.select((s) => s.item);
// selected$ = this._articleSearchService.selectedItemIds$.pipe(map((selectedItemIds) => selectedItemIds.includes(this.item?.id)));
@Input()
selected: boolean;
@Input()
get selectable() {
return this.get((s) => s.selectable);
}
set selectable(selectable: boolean) {
if (this.selectable !== selectable) {
this.patchState({ selectable });
}
}
@Output()
selectedChange = new EventEmitter<boolean>();
get isTablet() {
return this._environment.matchTablet();
}
get queryParams() {
const compartmentCode = this.item?.compartmentCode;
const archive = !!this._customerOrderStore.filter?.getQueryParams()?.main_archive || false;
if (compartmentCode) {
return {
buyerNumber: this.item?.buyerNumber,
archive,
};
} else {
return {
archive,
};
}
}
get detailsLink() {
const orderNumber = this.item?.orderNumber;
const processingStatus = this.item?.processingStatus;
const compartmentCode = this.item?.compartmentCode;
// if (this.isTablet) {
if (compartmentCode) {
return [
`/kunde/${this._applicationService.activatedProcessId}/order/details/compartment/${encodeURIComponent(
compartmentCode
)}/${processingStatus}`,
];
} else {
return [
`/kunde/${this._applicationService.activatedProcessId}/order/details/order/${encodeURIComponent(orderNumber)}/${processingStatus}`,
];
}
// } else {
// if (compartmentCode) {
// return [
// '/kunde',
// this._applicationService.activatedProcessId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(compartmentCode), processingStatus],
// },
// },
// ];
// } else {
// return [
// '/kunde',
// this._applicationService.activatedProcessId,
// 'order',
// { outlets: { main: null, left: 'results', right: ['details', 'order', encodeURIComponent(orderNumber), processingStatus] } },
// ];
// }
// }
}
constructor(
private _environment: EnvironmentService,
private _customerOrderStore: CustomerOrderSearchStore,
private _applicationService: ApplicationService
) {
super({ selected: false, selectable: false });
}
ngOnInit() {}
}

View File

@@ -1,7 +1,28 @@
<div class="hits">{{ hits$ | async }} Titel</div> <div class="page-customer-order-search-results__header bg-background-liste flex flex-col items-end pb-4">
<a
[class.active]="hasFilter$ | async"
class="page-customer-order-search-results__filter h-14 rounded-card font-bold px-5 mb-4 text-lg bg-cadet-blue flex flex-row flex-nowrap items-center justify-center"
[routerLink]="filterRoute"
queryParamsHandling="preserve"
>
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
Filter
</a>
<div
*ngIf="hits$ | async; let hits"
class="page-customer-order-search-results__items-count inline-flex flex-row items-center pr-5 text-sm"
>
{{ hits ??
0 }}
Titel
</div>
</div>
<ui-scroll-container <ui-scroll-container
*ngIf="!(listEmpty$ | async); else emptyMessage" *ngIf="!(listEmpty$ | async); else emptyMessage"
[loading]="loading$ | async" [showScrollbar]="false"
[showScrollArrow]="false"
(reachEnd)="loadMore()" (reachEnd)="loadMore()"
[deltaEnd]="150" [deltaEnd]="150"
[itemLength]="itemLength$ | async" [itemLength]="itemLength$ | async"
@@ -10,6 +31,22 @@
> >
<ng-container *ngIf="processId$ | async; let processId"> <ng-container *ngIf="processId$ | async; let processId">
<shared-goods-in-out-order-group *ngFor="let bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn"> <shared-goods-in-out-order-group *ngFor="let bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn">
<!-- <div class="page-customer-order-search__items-list" *ngFor="let bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn">
<ng-container *ngIf="bueryNumberGroup.items[0]; let firstItem">
<div class="page-customer-order-search__item-header-group bg-white text-xl rounded-t-card p-4 font-bold mb-px-2">
<h3 class="mt-0 mb-4">
{{ firstItem?.organisation }}
<ng-container *ngIf="!!firstItem?.organisation && (!!firstItem?.firstName || !!firstItem?.lastName)"> - </ng-container>
{{ firstItem?.lastName }}
{{ firstItem?.firstName }}
</h3>
<h4 class="mt-0 mb-0">
{{ firstItem?.buyerNumber }}
ans lager (nicht abgeholt) bezahlt
</h4>
</div>
</ng-container> -->
<ng-container *ngFor="let orderNumberGroup of bueryNumberGroup.items | groupBy: byOrderNumberFn; let lastOrderNumber = last"> <ng-container *ngFor="let orderNumberGroup of bueryNumberGroup.items | groupBy: byOrderNumberFn; let lastOrderNumber = last">
<ng-container <ng-container
*ngFor="let processingStatusGroup of orderNumberGroup.items | groupBy: byProcessingStatusFn; let lastProcessingStatus = last" *ngFor="let processingStatusGroup of orderNumberGroup.items | groupBy: byProcessingStatusFn; let lastProcessingStatus = last"
@@ -26,6 +63,17 @@
[selected]="item | goodsOutItemSelected: selectedOrderItemSubsetIds" [selected]="item | goodsOutItemSelected: selectedOrderItemSubsetIds"
(selectedChange)="setSelectedItem(item, $event)" (selectedChange)="setSelectedItem(item, $event)"
></shared-goods-in-out-order-group-item> ></shared-goods-in-out-order-group-item>
<!-- <page-customer-order-item
*ngFor="let item of compartmentCodeGroup.items; let firstItem = first; trackBy: trackByFn"
class="mb-px-10"
[selectable]="item | goodsOutItemSelectable: selectionRules:selectedItems"
[selected]="item | goodsOutItemSelected: selectedOrderItemSubsetIds"
[item]="item"
(click)="navigateToDetails(processId, item)"
(selectedChange)="setSelectedItem(item, $event)"
></page-customer-order-item> -->
<div class="divider" *ngIf="!lastCompartmentCode"></div> <div class="divider" *ngIf="!lastCompartmentCode"></div>
</ng-container> </ng-container>
<div class="divider" *ngIf="!lastProcessingStatus"></div> <div class="divider" *ngIf="!lastProcessingStatus"></div>

View File

@@ -1,11 +1,11 @@
// :host {
// @apply box-border block h-[100vh] max-h-[calc(100vh-364px)] desktop:max-h-[calc(100vh-300px)];
// }
:host { :host {
@apply block relative; @apply block relative;
} }
.hits {
@apply text-active-customer text-right mb-3 font-semibold text-base;
}
.empty-message { .empty-message {
@apply bg-white text-center font-semibold text-inactive-customer py-10 rounded-card; @apply bg-white text-center font-semibold text-inactive-customer py-10 rounded-card;
} }
@@ -18,6 +18,10 @@ shared-goods-in-out-order-group-item {
@apply cursor-pointer; @apply cursor-pointer;
} }
// page-customer-order-item {
// @apply cursor-pointer;
// }
.actions { .actions {
@apply fixed bottom-28 inline-grid grid-flow-col gap-7; @apply fixed bottom-28 inline-grid grid-flow-col gap-7;
left: 50%; left: 50%;
@@ -40,6 +44,10 @@ shared-goods-in-out-order-group-item {
} }
} }
// ::ng-deep page-customer-order-search-results ui-scroll-container .scroll-container {
// max-height: calc(100vh - 25.5rem) !important;
// }
::ng-deep .desktop page-goods-out-search-results ui-scroll-container { ::ng-deep .desktop page-goods-out-search-results ui-scroll-container {
.scrollbar-gap::-webkit-scrollbar-track { .scrollbar-gap::-webkit-scrollbar-track {
margin-bottom: 7.25rem; margin-bottom: 7.25rem;

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, TrackByFunction } from '@angular/core'; import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, TrackByFunction } from '@angular/core';
import { debounceTime, first, map, shareReplay, takeUntil, withLatestFrom } from 'rxjs/operators'; import { debounceTime, filter, first, map, shareReplay, takeUntil, withLatestFrom } from 'rxjs/operators';
import { KeyValueDTOOfStringAndString, OrderItemListItemDTO } from '@swagger/oms'; import { KeyValueDTOOfStringAndString, OrderItemListItemDTO } from '@swagger/oms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, ChildrenOutletContexts, Params, Router } from '@angular/router';
import { CustomerOrderSearchStore } from '../customer-order-search.store'; import { CustomerOrderSearchStore } from '../customer-order-search.store';
import { BehaviorSubject, combineLatest, Observable, Subject, Subscription } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable, Subject, Subscription } from 'rxjs';
import { BreadcrumbService } from '@core/breadcrumb'; import { BreadcrumbService } from '@core/breadcrumb';
@@ -11,6 +11,8 @@ import { OrderItemsContext } from '@domain/oms';
import { UiErrorModalComponent, UiModalService } from '@ui/modal'; import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { UiScrollContainerComponent } from '@ui/scroll-container'; import { UiScrollContainerComponent } from '@ui/scroll-container';
import { UiFilter } from '@ui/filter'; import { UiFilter } from '@ui/filter';
import { EnvironmentService } from '@core/environment';
import { isEqual } from 'lodash';
export interface CustomerOrderSearchResultsState { export interface CustomerOrderSearchResultsState {
selectedOrderItemSubsetIds: number[]; selectedOrderItemSubsetIds: number[];
@@ -80,13 +82,51 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
scrollTo: number; scrollTo: number;
filter$ = this._goodsOutSearchStore.filter$;
initialFilter$ = this.filter$.pipe(
filter((filter) => !!filter),
first()
);
hasFilter$ = this.filter$.pipe(
withLatestFrom(this.initialFilter$),
map(([filter, initialFilter]) => !isEqual(filter?.getQueryParams(), initialFilter?.getQueryParams()))
);
get isTablet() {
return this._environment.matchTablet();
}
get rightOutletParams(): Params {
return this._activatedRoute?.parent?.children?.find((childRoute) => childRoute?.outlet === 'right')?.snapshot?.params;
}
get filterRoute() {
const processId = this._activatedRoute?.parent?.snapshot?.data?.processId;
// if (!this.isTablet) {
// const orderNumber = this.rightOutletParams?.orderNumber;
// const compartmentCode = this.rightOutletParams?.compartmentCode;
// const processingStatus = this.rightOutletParams?.processingStatus;
// return [
// '/kunde',
// processId,
// 'order',
// { outlets: { main: null, left: 'results', right: ['filter', orderNumber ?? compartmentCode, processingStatus] } },
// ];
// } else {
return ['/kunde', processId, 'order', 'filter'];
// }
}
constructor( constructor(
private _goodsOutSearchStore: CustomerOrderSearchStore, private _goodsOutSearchStore: CustomerOrderSearchStore,
private _router: Router, private _router: Router,
private _activatedRoute: ActivatedRoute, private _activatedRoute: ActivatedRoute,
private _breadcrumb: BreadcrumbService, private _breadcrumb: BreadcrumbService,
private _commandService: CommandService, private _commandService: CommandService,
private _modal: UiModalService private _modal: UiModalService,
private _environment: EnvironmentService
) { ) {
super({ super({
selectedOrderItemSubsetIds: [], selectedOrderItemSubsetIds: [],
@@ -263,6 +303,7 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
const compartmentCode = orderItem.compartmentCode; const compartmentCode = orderItem.compartmentCode;
const archive = !!this._goodsOutSearchStore.filter?.getQueryParams()?.main_archive || false; const archive = !!this._goodsOutSearchStore.filter?.getQueryParams()?.main_archive || false;
// if (this.isTablet) {
if (compartmentCode) { if (compartmentCode) {
this._router.navigate([`/kunde/${processId}/order/details/compartment/${encodeURIComponent(compartmentCode)}/${processingStatus}`], { this._router.navigate([`/kunde/${processId}/order/details/compartment/${encodeURIComponent(compartmentCode)}/${processingStatus}`], {
queryParams: { queryParams: {
@@ -277,6 +318,46 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
}, },
}); });
} }
// } else {
// if (compartmentCode) {
// this._router.navigate(
// [
// '/kunde',
// processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(compartmentCode), processingStatus],
// },
// },
// ],
// {
// queryParamsHandling: 'preserve',
// queryParams: {
// buyerNumber: orderItem.buyerNumber,
// archive,
// },
// }
// );
// } else {
// this._router.navigate(
// [
// '/kunde',
// processId,
// 'order',
// { outlets: { main: null, left: 'results', right: ['details', 'order', encodeURIComponent(orderNumber), processingStatus] } },
// ],
// {
// queryParamsHandling: 'preserve',
// queryParams: {
// archive,
// },
// }
// );
// }
// }
} }
setSelectedItem(item: OrderItemListItemDTO, selected: boolean) { setSelectedItem(item: OrderItemListItemDTO, selected: boolean) {

View File

@@ -8,10 +8,18 @@ import { CustomerOrderItemSelectablePipe } from './customer-order-item-selectabl
import { CustomerOrderItemSelectedPipe } from './customer-order-item-selectede.pipe'; import { CustomerOrderItemSelectedPipe } from './customer-order-item-selectede.pipe';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module'; import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
import { UiScrollContainerModule } from '@ui/scroll-container'; import { UiScrollContainerModule } from '@ui/scroll-container';
import { UiIconModule } from '@ui/icon';
import { RouterModule } from '@angular/router';
import { CustomerOrderItemComponent } from './customer-order-item.component';
@NgModule({ @NgModule({
imports: [CommonModule, GoodsInOutOrderGroupModule, UiCommonModule, UiSpinnerModule, UiScrollContainerModule], imports: [CommonModule, RouterModule, GoodsInOutOrderGroupModule, UiCommonModule, UiIconModule, UiSpinnerModule, UiScrollContainerModule],
exports: [CustomerOrderSearchResultsComponent], exports: [CustomerOrderSearchResultsComponent, CustomerOrderItemComponent],
declarations: [CustomerOrderSearchResultsComponent, CustomerOrderItemSelectablePipe, CustomerOrderItemSelectedPipe], declarations: [
CustomerOrderSearchResultsComponent,
CustomerOrderItemSelectablePipe,
CustomerOrderItemSelectedPipe,
CustomerOrderItemComponent,
],
}) })
export class CustomerOrderSearchResultsModule {} export class CustomerOrderSearchResultsModule {}

View File

@@ -2,3 +2,4 @@ export * from './customer-order-item-selectable.pipe';
export * from './customer-order-item-selectede.pipe'; export * from './customer-order-item-selectede.pipe';
export * from './customer-order-search-results.component'; export * from './customer-order-search-results.component';
export * from './customer-order-search-results.module'; export * from './customer-order-search-results.module';
export * from './customer-order-item.component';

View File

@@ -8,4 +8,19 @@
</shared-branch-selector> </shared-branch-selector>
</shared-breadcrumb> </shared-breadcrumb>
<!-- <ng-container *ngIf="isTablet; else desktop"> -->
<router-outlet></router-outlet> <router-outlet></router-outlet>
<!-- </ng-container>
<ng-template #desktop>
<router-outlet name="main"></router-outlet>
<div class="grid desktop:grid-cols-[31rem_auto]">
<div class="mr-6 hidden desktop:block">
<router-outlet name="left"></router-outlet>
</div>
<div>
<router-outlet name="right"></router-outlet>
</div>
</div>
</ng-template> -->

View File

@@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application'; import { ApplicationService } from '@core/application';
import { AuthService } from '@core/auth'; import { AuthService } from '@core/auth';
import { BreadcrumbService } from '@core/breadcrumb'; import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { BranchDTO } from '@swagger/checkout'; import { BranchDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalService } from '@ui/modal'; import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -19,6 +20,10 @@ export class CustomerOrderComponent implements OnInit {
selectedBranch$: Observable<BranchDTO>; selectedBranch$: Observable<BranchDTO>;
get isTablet() {
return this._environmentService.matchTablet();
}
onCustomerOrderDetailsPage$: Observable<boolean>; onCustomerOrderDetailsPage$: Observable<boolean>;
constructor( constructor(
@@ -26,6 +31,7 @@ export class CustomerOrderComponent implements OnInit {
private _activatedRoute: ActivatedRoute, private _activatedRoute: ActivatedRoute,
private _uiModal: UiModalService, private _uiModal: UiModalService,
private _breadcrumb: BreadcrumbService, private _breadcrumb: BreadcrumbService,
private _environmentService: EnvironmentService,
public auth: AuthService public auth: AuthService
) {} ) {}

View File

@@ -10,7 +10,15 @@
</button> </button>
</div> --> </div> -->
<input #autofocus="uiAutofocus" autofocus type="text" [formControl]="control" (keydown.enter)="searchTasks()" placeholder="Suchen" /> <input
id="searchbox"
#autofocus="uiAutofocus"
autofocus
type="text"
[formControl]="control"
(keydown.enter)="searchTasks()"
placeholder="Suchen"
/>
<div class="actions"> <div class="actions">
<button class="clear" (click)="clearSearch(); autofocus.focus()" *ngIf="control.value.length"> <button class="clear" (click)="clearSearch(); autofocus.focus()" *ngIf="control.value.length">

View File

@@ -19,7 +19,7 @@
} }
::ng-deep shared-branch-selector ui-autocomplete .ui-autocomplete-output-wrapper { ::ng-deep shared-branch-selector ui-autocomplete .ui-autocomplete-output-wrapper {
@apply overflow-hidden overflow-y-auto max-w-content rounded-b-md; @apply overflow-hidden overflow-y-auto rounded-b-md;
max-height: 350px; max-height: 350px;
width: 100%; width: 100%;
left: unset; left: unset;
@@ -28,6 +28,26 @@
button { button {
@apply py-2; @apply py-2;
} }
@screen desktop-small {
@apply max-w-screen-desktop-small;
}
@screen desktop {
@apply max-w-screen-desktop;
}
@screen desktop-large {
@apply max-w-screen-desktop-large;
}
@screen desktop-x-large {
@apply max-w-screen-desktop-x-large;
}
@screen desktop-xx-large {
@apply max-w-screen-desktop-xx-large;
}
} }
.shared-branch-selector-autocomplete-option.shared-branch-selector-selected { .shared-branch-selector-autocomplete-option.shared-branch-selector-selected {

View File

@@ -207,14 +207,18 @@ export class BranchSelectorComponent implements OnInit, OnDestroy, AfterViewInit
clear() { clear() {
this.setBranch(); this.setBranch();
this.emitValues();
this.complete.next(''); this.complete.next('');
this.onChange(undefined);
this.onTouched();
this.valueChange.emit(undefined);
} }
emitValues(branch?: BranchDTO) { emitValues(branch?: BranchDTO) {
this.onChange(branch); if (!!branch) {
this.onTouched(); this.onChange(branch);
this.valueChange.emit(branch); this.onTouched();
this.valueChange.emit(branch);
}
} }
closeAndClearAutocomplete() { closeAndClearAutocomplete() {

View File

@@ -1,6 +1,25 @@
div { div {
@apply z-fixed mx-auto; @apply z-fixed mx-auto;
height: calc(100vh - 8.375rem); height: calc(100vh - 8.375rem);
@apply w-content;
background-color: var(--bg-color); background-color: var(--bg-color);
@screen desktop-small {
@apply max-w-screen-desktop-small;
}
@screen desktop {
@apply max-w-screen-desktop;
}
@screen desktop-large {
@apply max-w-screen-desktop-large;
}
@screen desktop-x-large {
@apply max-w-screen-desktop-x-large;
}
@screen desktop-xx-large {
@apply max-w-screen-desktop-xx-large;
}
} }

View File

@@ -1,7 +1,7 @@
<div> <div>
<div class="inputs overflow-y-auto"> <div class="inputs">
<button <button
class="ui-input" class="ui-input border border-solid border-[#EDEFF0]"
type="button" type="button"
*ngFor="let input of uiInputGroup?.input" *ngFor="let input of uiInputGroup?.input"
[class.active]="activeInput === input" [class.active]="activeInput === input"
@@ -11,7 +11,12 @@
<div class="grow"> <div class="grow">
{{ input?.label }} {{ input?.label }}
</div> </div>
<ui-icon icon="arrow_head" size="1rem"></ui-icon> <span class="w-[14px] h-[14px] flex items-center justify-center text-2xl text-[#596470]" *ngIf="activeInput !== input"
><ui-svg-icon icon="add"></ui-svg-icon
></span>
<span class="w-[14px] h-[14px] flex items-center justify-center text-2xl text-black" *ngIf="activeInput === input"
><ui-svg-icon class="mr-[1px]" icon="remove"></ui-svg-icon
></span>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,34 +1,19 @@
:host { :host {
@apply grid grid-flow-col gap-2; @apply grid grid-flow-col;
grid-template-columns: 240px 1fr; grid-template-columns: 240px 1fr;
} }
ui-icon {
@apply transition transform text-cool-grey;
}
.inputs { .inputs {
@apply grid grid-flow-row gap-2; @apply grid grid-flow-row gap-2;
max-height: calc(100vh - 377px);
.ui-input { .ui-input {
@apply flex flex-row items-center border-none outline-none p-4 font-bold text-base bg-white text-black text-left rounded-card transition transform; @apply flex flex-row items-center outline-none p-4 text-lg bg-white text-black text-left rounded-card transition transform mr-2;
} box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5);
.has-options {
@apply text-white bg-active-customer;
ui-icon {
@apply text-white;
}
} }
.ui-input.active { .ui-input.active {
@apply text-black bg-white pr-6 -mr-2 rounded-r-none; @apply text-black bg-white font-bold pr-6 mr-0 rounded-r-none border-none;
box-shadow: 0px 6px 24px rgba(206, 212, 219, 0.8);
ui-icon {
@apply rotate-180 text-cool-grey;
}
} }
.space-right { .space-right {
@@ -40,6 +25,13 @@ ui-icon {
} }
} }
.has-options {
@apply text-black bg-white font-bold;
span {
@apply text-black;
}
}
::ng-deep ui-filter-filter-group-filter .input-options-wrapper { ::ng-deep ui-filter-filter-group-filter .input-options-wrapper {
min-height: 150px; min-height: 150px;
} }

View File

@@ -1,4 +1,5 @@
<ui-searchbox <shared-searchbox
class="w-full"
[placeholder]="uiInput?.placeholder" [placeholder]="uiInput?.placeholder"
[query]="uiInput?.value" [query]="uiInput?.value"
(queryChange)="onQueryChange($event)" (queryChange)="onQueryChange($event)"
@@ -14,7 +15,7 @@
{{ item.display }} {{ item.display }}
</button> </button>
</ui-autocomplete> </ui-autocomplete>
</ui-searchbox> </shared-searchbox>
<ng-container *ngIf="showDescription && uiInput?.description"> <ng-container *ngIf="showDescription && uiInput?.description">
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button"> <button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
i i

View File

@@ -3,10 +3,9 @@
} }
.info-tooltip-button { .info-tooltip-button {
@apply border-font-customer border-2 border-solid bg-white rounded-md text-base font-bold absolute; @apply border-font-customer ml-4 border-2 border-solid bg-white rounded-md text-base font-bold absolute;
border-style: outset; border-style: outset;
width: 31px; width: 31px;
height: 31px; height: 31px;
top: 14px; top: 14px;
right: -45px;
} }

View File

@@ -3,12 +3,12 @@ import { CommonModule } from '@angular/common';
import { FilterInputGroupMainComponent } from './filter-input-group-main.component'; import { FilterInputGroupMainComponent } from './filter-input-group-main.component';
import { UiAutocompleteModule } from '@ui/autocomplete'; import { UiAutocompleteModule } from '@ui/autocomplete';
import { UiSearchboxNextModule } from '@ui/searchbox';
import { UiTooltipModule } from 'apps/ui/tooltip/src/public-api'; import { UiTooltipModule } from 'apps/ui/tooltip/src/public-api';
import { UiCommonModule } from '@ui/common'; import { UiCommonModule } from '@ui/common';
import { SearchboxModule } from '@shared/components/searchbox';
@NgModule({ @NgModule({
imports: [CommonModule, UiCommonModule, UiSearchboxNextModule, UiAutocompleteModule, UiTooltipModule], imports: [CommonModule, UiCommonModule, SearchboxModule, UiAutocompleteModule, UiTooltipModule],
exports: [FilterInputGroupMainComponent], exports: [FilterInputGroupMainComponent],
declarations: [FilterInputGroupMainComponent], declarations: [FilterInputGroupMainComponent],
}) })

View File

@@ -1,10 +1,10 @@
<ui-filter-filter-group-main [inputGroup]="uiFilter?.filter | group: 'main'"></ui-filter-filter-group-main> <shared-filter-filter-group-main [inputGroup]="uiFilter?.filter | group: 'main'"></shared-filter-filter-group-main>
<ui-filter-input-group-main <shared-filter-input-group-main
*ngIf="uiFilter?.input | group: 'main'; let inputGroupMain" *ngIf="uiFilter?.input | group: 'main'; let inputGroupMain"
[inputGroup]="inputGroupMain" [inputGroup]="inputGroupMain"
(search)="emitSearch($event)" (search)="emitSearch($event)"
[loading]="loading" [loading]="loading"
[hint]="hint" [hint]="hint"
[scanner]="scanner" [scanner]="scanner"
></ui-filter-input-group-main> ></shared-filter-input-group-main>
<ui-filter-filter-group-filter [inputGroup]="uiFilter?.filter | group: 'filter'"></ui-filter-filter-group-filter> <shared-filter-filter-group-filter [inputGroup]="uiFilter?.filter | group: 'filter'"></shared-filter-filter-group-filter>

View File

@@ -1,8 +1,8 @@
:host { :host {
@apply grid grid-flow-row gap-8; @apply grid grid-flow-row gap-[1.875rem];
} }
ui-filter-input-group-main { shared-filter-input-group-main {
@apply mx-auto w-full; @apply mx-auto w-full;
max-width: 600px; max-width: 600px;
} }

View File

@@ -1,4 +1,3 @@
import { DOCUMENT } from '@angular/common';
import { import {
Component, Component,
ChangeDetectionStrategy, ChangeDetectionStrategy,
@@ -7,11 +6,6 @@ import {
SimpleChanges, SimpleChanges,
Output, Output,
EventEmitter, EventEmitter,
ElementRef,
Renderer2,
Inject,
AfterViewInit,
HostListener,
ViewChild, ViewChild,
ContentChildren, ContentChildren,
QueryList, QueryList,
@@ -26,7 +20,7 @@ import { IFilter, Filter } from './tree/filter';
styleUrls: ['filter.component.scss'], styleUrls: ['filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FilterComponent implements OnChanges, AfterViewInit { export class FilterComponent implements OnChanges {
@Input() @Input()
loading: boolean; loading: boolean;
@@ -39,9 +33,6 @@ export class FilterComponent implements OnChanges, AfterViewInit {
@Input() @Input()
filter: IFilter; filter: IFilter;
@Input()
resizeInputOptionsToElement: string | HTMLElement;
@Input() @Input()
scanner = false; scanner = false;
@@ -55,7 +46,7 @@ export class FilterComponent implements OnChanges, AfterViewInit {
return this.filter instanceof Filter && this.filter; return this.filter instanceof Filter && this.filter;
} }
constructor(private elementRef: ElementRef, private renderer: Renderer2, @Inject(DOCUMENT) private document: Document) {} constructor() {}
ngOnChanges({ filter }: SimpleChanges): void { ngOnChanges({ filter }: SimpleChanges): void {
if (filter) { if (filter) {
@@ -65,36 +56,6 @@ export class FilterComponent implements OnChanges, AfterViewInit {
} }
} }
ngAfterViewInit() {
setTimeout(() => this.resizeInputOptions(), 500);
}
getHtmlElement(element: string | HTMLElement): HTMLElement {
return typeof element === 'string' ? this.document.querySelector(element) : element;
}
@HostListener('window:resize', ['$event'])
@HostListener('window:click', ['$event'])
resizeInputOptions() {
if (this.resizeInputOptionsToElement) {
const inputOptions = this.elementRef.nativeElement.querySelector('.input-options');
const targetElement = this.getHtmlElement(this.resizeInputOptionsToElement);
// set the max-height of the input-options to the height of the actions, depending on the distance between the two
if (inputOptions && targetElement) {
const distanceBetweenElements = this.getDistanceBetweenElements(inputOptions, targetElement) - 15;
this.renderer.setStyle(inputOptions, 'max-height', `${distanceBetweenElements}px`);
}
}
}
getDistanceBetweenElements(element1: HTMLElement, element2: HTMLElement) {
const rect1 = element1.getBoundingClientRect();
const rect2 = element2.getBoundingClientRect();
return Math.abs(rect1.top - rect2.top);
}
emitSearch(query: string) { emitSearch(query: string) {
setTimeout(() => { setTimeout(() => {
this.search.emit(query); this.search.emit(query);

View File

@@ -5,5 +5,5 @@ export * from './providers';
export * from './shared'; export * from './shared';
export * from './testing'; export * from './testing';
export * from './tree'; export * from './tree';
export * from './ui-filter.component'; export * from './filter.component';
export * from './ui-filter.module'; export * from './filter.module';

View File

@@ -1,19 +1,21 @@
<div class="order-by-filter-button-wrapper"> <ng-container *ngIf="orderByKeys">
<button <div class="group w-full flex flex-row justify-between" *ngFor="let group of groupedOrderByKeys">
[attr.data-label]="label" <button
class="order-by-filter-button" *ngFor="let label of group.keys"
type="button" [attr.data-label]="label"
*ngFor="let label of orderByKeys" class="order-by-filter-button"
(click)="setActive(label)" type="button"
> (click)="setActive(label)"
<span> >
{{ label }} <span>
</span> {{ label }}
<ui-icon </span>
[class.asc]="label === activeOrderBy?.label && !activeOrderBy.desc" <ui-icon
[class.desc]="label === activeOrderBy?.label && activeOrderBy.desc" [class.asc]="label === activeOrderBy?.label && !activeOrderBy.desc"
icon="arrow" [class.desc]="label === activeOrderBy?.label && activeOrderBy.desc"
size="14px" icon="arrow"
></ui-icon> size="10px"
</button> ></ui-icon>
</div> </button>
</div>
</ng-container>

View File

@@ -1,28 +1,23 @@
:host { :host {
@apply box-border flex justify-center items-center; @apply box-border flex flex-row items-center justify-center h-[3.3125rem] w-full;
min-height: 30px;
}
.order-by-filter-button-wrapper {
@apply flex flex-row items-center justify-center;
} }
.order-by-filter-button { .order-by-filter-button {
@apply bg-transparent outline-none border-none text-regular font-bold flex flex-row justify-center items-center m-0 py-2 px-2; @apply bg-transparent outline-none border-none text-regular font-bold flex flex-row justify-center items-center;
} }
::ng-deep .tablet ui-order-by-filter .order-by-filter-button { ::ng-deep .tablet shared-order-by-filter .order-by-filter-button {
@apply mx-0; @apply mx-0;
} }
ui-icon { ui-icon {
@apply hidden transform ml-2 rounded-full p-1; @apply hidden transform ml-1 rounded-full p-1 bg-[#AEB7C1] h-[20px] w-[20px] items-center justify-center;
transition: 250ms all ease-in-out; transition: 250ms all ease-in-out;
} }
ui-icon.asc, ui-icon.asc,
ui-icon.desc { ui-icon.desc {
@apply flex rounded-full visible text-white; @apply flex rounded-full visible text-black;
} }
ui-icon.asc { ui-icon.asc {
@@ -32,23 +27,3 @@ ui-icon.asc {
ui-icon.desc { ui-icon.desc {
@apply rotate-90; @apply rotate-90;
} }
::ng-deep .customer ui-order-by-filter {
ui-icon {
@apply bg-active-customer;
}
.order-by-filter-button {
@apply text-active-customer;
}
}
::ng-deep .branch ui-order-by-filter {
ui-icon {
@apply bg-active-branch;
}
.order-by-filter-button {
@apply text-active-branch;
}
}

View File

@@ -1,5 +1,5 @@
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef, Output, EventEmitter, OnInit } from '@angular/core'; import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef, Output, EventEmitter, OnInit } from '@angular/core';
import { UiOrderBy } from '@ui/filter'; import { OrderBy } from '../tree';
@Component({ @Component({
selector: 'shared-order-by-filter', selector: 'shared-order-by-filter',
@@ -9,15 +9,48 @@ import { UiOrderBy } from '@ui/filter';
}) })
export class OrderByFilterComponent implements OnInit { export class OrderByFilterComponent implements OnInit {
@Input() @Input()
orderBy: UiOrderBy[]; orderBy: OrderBy[];
@Input()
groupBy?: number[] = []; // example input: [2, 1, 2] should result in: [[item, item], [item], [item, item]]
@Output() @Output()
selectedOrderByChange = new EventEmitter<UiOrderBy>(); selectedOrderByChange = new EventEmitter<OrderBy>();
get orderByKeys() { get orderByKeys() {
return this.orderBy?.map((ob) => ob.label).filter((key, idx, self) => self.indexOf(key) === idx); return this.orderBy?.map((ob) => ob.label).filter((key, idx, self) => self.indexOf(key) === idx);
} }
get groupedOrderByKeys(): Array<{ group: number; keys: string[] }> {
let orderByKeys = this.orderByKeys; // copy
const grouped: Array<{ group: number; keys: string[] }> = [];
if (this.groupBy?.length > 1) {
// group keys based on given input groupBy array structure
for (let [outerIndex, group] of this.groupBy?.entries()) {
let item = {
group: outerIndex,
keys: [],
};
for (let [innerIndex, key] of orderByKeys.entries()) {
if (innerIndex < group) {
item.keys.push(key);
} else {
break;
}
}
// Filter already grouped keys from initial array
orderByKeys = orderByKeys.filter((key) => !item.keys.find((itemKey) => itemKey === key));
grouped.push(item);
}
} else {
grouped.push({ group: 1, keys: this.orderByKeys });
}
return grouped;
}
get activeOrderBy() { get activeOrderBy() {
return this.orderBy?.find((f) => f.selected); return this.orderBy?.find((f) => f.selected);
} }
@@ -31,7 +64,7 @@ export class OrderByFilterComponent implements OnInit {
setActive(orderBy: string) { setActive(orderBy: string) {
const active = this.activeOrderBy; const active = this.activeOrderBy;
const orderBys = this.orderBy?.filter((f) => f.label === orderBy); const orderBys = this.orderBy?.filter((f) => f.label === orderBy);
let next: UiOrderBy; let next: OrderBy;
if (orderBys?.length) { if (orderBys?.length) {
if (active?.label !== orderBy) { if (active?.label !== orderBy) {

View File

@@ -3,19 +3,9 @@
} }
button.ui-filter-chip { button.ui-filter-chip {
@apply grid grid-flow-col gap-2 items-center rounded-full text-base px-4 py-3 bg-white text-inactive-customer border-none font-bold; @apply grid grid-flow-col gap-2 items-center rounded-full text-base px-4 py-3 bg-[#EDEFF0] border-none font-semibold;
}
/** styling branch bereich **/
::ng-deep .branch ui-filter-input-chip button.ui-filter-chip {
@apply text-inactive-branch;
} }
button.ui-filter-chip.selected { button.ui-filter-chip.selected {
@apply bg-active-customer text-white; @apply bg-[#596470] text-white;
}
/** styling branch bereich **/
::ng-deep .branch ui-filter-input-chip button.ui-filter-chip.selected {
@apply bg-active-branch;
} }

View File

@@ -20,5 +20,5 @@
</button> </button>
</div> </div>
<ng-container *ngIf="uiOption?.expanded"> <ng-container *ngIf="uiOption?.expanded">
<ui-input-option-bool class="ml-10" *ngFor="let subOption of uiOption?.values" [option]="subOption"></ui-input-option-bool> <shared-input-option-bool class="ml-10" *ngFor="let subOption of uiOption?.values" [option]="subOption"></shared-input-option-bool>
</ng-container> </ng-container>

View File

@@ -18,6 +18,6 @@
} }
} }
::ng-deep ui-input-option-bool ui-checkbox ui-icon { ::ng-deep shared-input-option-bool ui-checkbox ui-icon {
@apply text-cool-grey; @apply text-cool-grey;
} }

View File

@@ -28,13 +28,13 @@
min-height: 24px; min-height: 24px;
} }
::ng-deep ui-input-option-date-range ui-datepicker.dp-left { ::ng-deep shared-input-option-date-range ui-datepicker.dp-left {
.dp { .dp {
left: 102px; left: 102px;
} }
} }
::ng-deep ui-input-option-date-range ui-datepicker.dp-right { ::ng-deep shared-input-option-date-range ui-datepicker.dp-right {
.dp { .dp {
right: -6px; right: -6px;
} }

View File

@@ -16,6 +16,5 @@
} }
ui-form-control input { ui-form-control input {
max-width: 200px; @apply px-0 rounded-none max-w-[12rem];
@apply px-0 rounded-none;
} }

View File

@@ -20,5 +20,5 @@
</button> </button>
</div> </div>
<ng-container *ngIf="uiOption?.expanded"> <ng-container *ngIf="uiOption?.expanded">
<ui-input-option-tri-state *ngFor="let subOption of uiOption?.values" [option]="subOption"></ui-input-option-tri-state> <shared-input-option-tri-state *ngFor="let subOption of uiOption?.values" [option]="subOption"></shared-input-option-tri-state>
</ng-container> </ng-container>

View File

@@ -10,7 +10,7 @@ ui-switch {
@apply px-4 py-2 flex flex-row justify-between items-center; @apply px-4 py-2 flex flex-row justify-between items-center;
.btn-expand { .btn-expand {
@apply border-none outline-none bg-transparent text-cool-grey; @apply border-none outline-none bg-transparent text-[#0556B4];
ui-icon { ui-icon {
@apply transition-all transform rotate-90; @apply transition-all transform rotate-90;
@@ -22,6 +22,6 @@ ui-switch {
} }
} }
::ng-deep ui-input-option-bool ui-checkbox ui-icon { ::ng-deep shared-input-option-bool ui-checkbox ui-icon {
@apply text-cool-grey; @apply text-[#0556B4];
} }

View File

@@ -1,4 +1,4 @@
<div class="input-options-wrapper"> <div class="input-options-wrapper h-[calc(100vh-34.5rem)] desktop:h-[calc(100vh-28rem)]">
<div class="hidden-overflow"> <div class="hidden-overflow">
<div class="input-options-header" [class.header-shadow]="scrollPersantage > 0"> <div class="input-options-header" [class.header-shadow]="scrollPersantage > 0">
<button type="button" (click)="setSelected(undefined)"> <button type="button" (click)="setSelected(undefined)">
@@ -13,30 +13,31 @@
</button> </button>
</div> </div>
</div> </div>
<div class="input-options-wrapper"> <div class="input-options-wrapper h-[calc(100vh-38.5rem)] desktop:h-[calc(100vh-32rem)]">
<p class="input-desription"> <p class="input-desription">
{{ uiInputOptions?.parent?.description }} {{ uiInputOptions?.parent?.description }}
</p> </p>
<ng-container *ngIf="uiInputOptions?.parent?.type === 2 || uiInputOptions?.parent?.type === 4"> <ng-container *ngIf="uiInputOptions?.parent?.type === 2 || uiInputOptions?.parent?.type === 4">
<div class="input-options" #inputOptionsConainter (scroll)="markForCheck()"> <div class="input-options" #inputOptionsConainter (scroll)="markForCheck()">
<ng-container *ngIf="uiInputOptions?.parent?.type === 2"> <ng-container *ngIf="uiInputOptions?.parent?.type === 2">
<ui-input-option-bool <shared-input-option-bool
*ngFor="let option of uiInputOptions?.values" *ngFor="let option of uiInputOptions?.values"
[option]="option" [option]="option"
(optionChange)="optionChange($event)" (optionChange)="optionChange($event)"
></ui-input-option-bool> ></shared-input-option-bool>
</ng-container> </ng-container>
<ng-container *ngIf="uiInputOptions?.parent?.type === 4"> <ng-container *ngIf="uiInputOptions?.parent?.type === 4">
<ui-input-option-tri-state *ngFor="let option of uiInputOptions?.values" [option]="option"> </ui-input-option-tri-state> <shared-input-option-tri-state *ngFor="let option of uiInputOptions?.values" [option]="option"> </shared-input-option-tri-state>
</ng-container> </ng-container>
</div> </div>
<div class="w-full h-[6rem]"></div>
<button class="cta-scroll" [class.up]="scrollPersantage > 20" *ngIf="scrollable" (click)="scroll(20)"> <button class="cta-scroll" [class.up]="scrollPersantage > 20" *ngIf="scrollable" (click)="scroll(20)">
<ui-icon icon="arrow" size="20px"></ui-icon> <ui-icon icon="arrow" size="20px"></ui-icon>
</button> </button>
</ng-container> </ng-container>
<ui-input-option-date-range *ngIf="uiInputOptions?.parent?.type === 128" [options]="uiInputOptions?.values"> <shared-input-option-date-range *ngIf="uiInputOptions?.parent?.type === 128" [options]="uiInputOptions?.values">
</ui-input-option-date-range> </shared-input-option-date-range>
<ui-input-option-number-range *ngIf="uiInputOptions?.parent?.type === 4096" [options]="uiInputOptions?.values"> <shared-input-option-number-range *ngIf="uiInputOptions?.parent?.type === 4096" [options]="uiInputOptions?.values">
</ui-input-option-number-range> </shared-input-option-number-range>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
:host { :host {
@apply block; @apply block;
box-shadow: 0px 6px 24px rgba(206, 212, 219, 0.8);
} }
.input-desription { .input-desription {
@@ -19,7 +20,7 @@
@apply grid grid-flow-col justify-end items-center mb-2; @apply grid grid-flow-col justify-end items-center mb-2;
button { button {
@apply bg-transparent p-4 text-base outline-none border-none font-semibold text-inactive-customer; @apply bg-transparent p-4 text-base outline-none border-none text-[#0556B4];
} }
&.header-shadow { &.header-shadow {
@@ -36,7 +37,8 @@
} }
.cta-scroll { .cta-scroll {
@apply absolute bottom-4 right-4 shadow-cta border-none outline-none bg-white w-10 h-10 rounded-full flex flex-row flex-nowrap items-center justify-center text-active-customer; @apply absolute bottom-4 right-4 border-none outline-none bg-white w-[3.625rem] h-[3.625rem] rounded-full flex flex-row flex-nowrap items-center justify-center text-[#596470];
box-shadow: 0px 0px 20px rgba(89, 100, 112, 0.5);
ui-icon { ui-icon {
@apply transition-transform transform rotate-90; @apply transition-transform transform rotate-90;
@@ -46,15 +48,3 @@
@apply -rotate-90; @apply -rotate-90;
} }
} }
::ng-deep .branch ui-filter-input-options {
.cta-scroll {
@apply text-cool-grey;
}
.input-options-header {
button {
@apply text-cool-grey;
}
}
}

View File

@@ -22,6 +22,12 @@ export class FilterInputTextComponent extends AbstractUiFilterInputDirective imp
this.control.setValue(this.value); this.control.setValue(this.value);
this.updateValidator(); this.updateValidator();
const uiInputChangesSub = this.uiInput?.changes?.subscribe((changes) => {
if (this.control.value !== changes?.target?.value) this.control.setValue(changes?.target?.value);
this.updateValidator();
});
const onInputChangeSub = this.onUiInputChange$.subscribe((input) => { const onInputChangeSub = this.onUiInputChange$.subscribe((input) => {
if (this.control.value !== input.value) this.control.setValue(input.value); if (this.control.value !== input.value) this.control.setValue(input.value);
@@ -32,6 +38,7 @@ export class FilterInputTextComponent extends AbstractUiFilterInputDirective imp
if (this.value !== value) this.setValue(value); if (this.value !== value) this.setValue(value);
}); });
this._subscriptions.add(uiInputChangesSub);
this._subscriptions.add(onInputChangeSub); this._subscriptions.add(onInputChangeSub);
this._subscriptions.add(onControlValueChangeSub); this._subscriptions.add(onControlValueChangeSub);
} }

View File

@@ -201,6 +201,21 @@ export class Filter implements IFilter {
return target; return target;
} }
unselectAll() {
this.filter?.forEach((uiInputGroup) =>
uiInputGroup?.input?.forEach((uiInput) => {
uiInput?.setSelected(undefined);
uiInput?.setValue(undefined);
})
);
this.input?.forEach((uiInputGroup) =>
uiInputGroup?.input?.forEach((uiInput) => {
uiInput?.setSelected(undefined);
uiInput?.setValue(undefined);
})
);
}
static getQueryParamsFromQueryTokenDTO(queryToken: QueryTokenDTO): Record<string, string> { static getQueryParamsFromQueryTokenDTO(queryToken: QueryTokenDTO): Record<string, string> {
const queryParams: Record<string, string> = {}; const queryParams: Record<string, string> = {};

View File

@@ -11,10 +11,7 @@
} }
.goods-in-out-order-details-action-wrapper { .goods-in-out-order-details-action-wrapper {
@apply fixed bottom-28 inline-flex flex-row-reverse flex-wrap justify-center items-center; @apply fixed bottom-28 left-[50%] inline-flex desktop:flex translate-x-[-50%] desktop:translate-x-[15%] w-[80%] desktop:w-fit flex-row-reverse flex-wrap justify-center items-center;
width: 80%;
left: 50%;
transform: translateX(-50%);
.cta-action { .cta-action {
@apply border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap m-2; @apply border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap m-2;

View File

@@ -64,7 +64,7 @@ input {
} }
.actions { .actions {
@apply flex justify-center items-center fixed left-0 right-0; @apply flex justify-center items-center desktop:translate-x-[60%] desktop:w-fit fixed left-0 right-0 desktop:left-[unset] desktop:right-[unset];
bottom: 110px; bottom: 110px;
.cta-close { .cta-close {

View File

@@ -1,5 +1,6 @@
<div class="searchbox-input-wrapper"> <div class="searchbox-input-wrapper">
<input <input
id="searchbox"
class="searchbox-input" class="searchbox-input"
#input #input
type="text" type="text"

View File

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

View File

@@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { NavigationDetails, OutletLocations, OutletParams } from './defs';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export abstract class BaseNavigationService {
constructor() {}
abstract getOutletLocations(activatedRoute: ActivatedRoute): OutletLocations;
abstract getOutletParams(activatedRoute: ActivatedRoute): OutletParams;
abstract mainOutletActive$(activatedRoute: ActivatedRoute): Observable<boolean>;
abstract mainOutletActive(activatedRoute: ActivatedRoute): boolean;
protected abstract _navigateTo(navigation: NavigationDetails): Promise<boolean>;
}

View File

@@ -0,0 +1,5 @@
// start:ng42.barrel
export * from './navigation-details.model';
export * from './outlet-locations.model';
export * from './outlet-params.model';
// end:ng42.barrel

View File

@@ -0,0 +1,5 @@
export interface NavigationDetails {
routerLink: any[];
queryParams?: Record<string, string>;
queryParamsHandling?: 'merge' | 'preserve' | '';
}

View File

@@ -0,0 +1,5 @@
export interface OutletLocations {
main: string;
left: string;
right: string;
}

View File

@@ -0,0 +1,7 @@
import { Params } from '@angular/router';
export interface OutletParams {
main: Params;
left: Params;
right: Params;
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { NavigationService } from './navigation.service';
describe('NavigationService', () => {
let service: NavigationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(NavigationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,50 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { EnvironmentService } from '@core/environment';
import { NavigationDetails, OutletLocations, OutletParams } from './defs';
import { BaseNavigationService } from './base-navigation.service';
import { Observable, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class NavigationService extends BaseNavigationService {
constructor(private _router: Router, private _environment: EnvironmentService) {
super();
}
getOutletLocations(activatedRoute: ActivatedRoute): OutletLocations {
return {
main: activatedRoute?.parent?.children?.find((childRoute) => childRoute?.outlet === 'main')?.snapshot?.routeConfig?.path,
left: activatedRoute?.parent?.children?.find((childRoute) => childRoute?.outlet === 'left')?.snapshot?.routeConfig?.path,
right: activatedRoute?.parent?.children?.find((childRoute) => childRoute?.outlet === 'right')?.snapshot?.routeConfig?.path,
};
}
getOutletParams(activatedRoute: ActivatedRoute): OutletParams {
return {
main: activatedRoute?.parent?.children?.find((childRoute) => childRoute?.outlet === 'main')?.snapshot?.params,
left: activatedRoute?.parent?.children?.find((childRoute) => childRoute?.outlet === 'left')?.snapshot?.params,
right: activatedRoute?.parent?.children?.find((childRoute) => childRoute?.outlet === 'right')?.snapshot?.params,
};
}
mainOutletActive$(activatedRoute: ActivatedRoute): Observable<boolean> {
return combineLatest([this._environment.matchDesktopSmall$, this._environment.matchDesktop$]).pipe(
map(
([desktopSmallState, desktopState]) =>
!!this.getOutletLocations(activatedRoute)?.main && (desktopSmallState.matches || desktopState.matches)
)
);
}
mainOutletActive(activatedRoute: ActivatedRoute): boolean {
return !!this.getOutletLocations(activatedRoute)?.main && (this._environment.matchDesktopSmall() || this._environment.matchDesktop());
}
protected async _navigateTo(navigation: NavigationDetails): Promise<boolean> {
return await this._router.navigate(navigation.routerLink, {
queryParams: navigation?.queryParams ?? {},
queryParamsHandling: navigation?.queryParamsHandling ?? '',
});
}
}

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