Merge branch 'develop' into release/2.2

This commit is contained in:
Lorenz Hilpert
2023-03-08 11:37:49 +01:00
19 changed files with 168 additions and 54 deletions

View File

@@ -4,7 +4,7 @@ import { Config } from '@core/config';
import { isNullOrUndefined } from '@utils/common';
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
import { BehaviorSubject } from 'rxjs';
import { asapScheduler, BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root',
@@ -15,6 +15,8 @@ export class AuthService {
return this._initialized.asObservable();
}
private _authConfig: AuthConfig;
constructor(private _config: Config, private readonly _oAuthService: OAuthService) {
this._oAuthService.events?.subscribe((event) => {
if (event.type === 'token_received') {
@@ -28,13 +30,14 @@ export class AuthService {
throw new Error('AuthService is already initialized');
}
const authConfig: AuthConfig = this._config.get('@core/auth');
this._authConfig = this._config.get('@core/auth');
authConfig.redirectUri = window.location.origin;
authConfig.silentRefreshRedirectUri = window.location.origin + '/silent-refresh.html';
authConfig.useSilentRefresh = true;
this._authConfig.redirectUri = window.location.origin;
this._oAuthService.configure(authConfig);
this._authConfig.silentRefreshRedirectUri = window.location.origin + '/silent-refresh.html';
this._authConfig.useSilentRefresh = true;
this._oAuthService.configure(this._authConfig);
this._oAuthService.tokenValidationHandler = new JwksValidationHandler();
this._oAuthService.setupAutomaticSilentRefresh();
@@ -91,6 +94,9 @@ export class AuthService {
async logout() {
await this._oAuthService.revokeTokenAndLogout();
asapScheduler.schedule(() => {
window.location.reload();
}, 250);
}
hasRole(role: string | string[]) {
@@ -105,11 +111,15 @@ export class AuthService {
return roles.every((r) => userRoles.includes(r));
}
async silentRefresh() {
async refresh() {
try {
await this._oAuthService.silentRefresh();
if (this._authConfig.responseType.includes('code') && this._authConfig.scope.includes('offline_access')) {
await this._oAuthService.refreshToken();
} else {
await this._oAuthService.silentRefresh();
}
} catch (error) {
console.error('Silent refresh failed', error);
console.error(error);
}
}
}

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable, InjectionToken } from '@angular/core';
import { AuthService } from '@core/auth';
import { SignalrHub, SignalRHubOptions } from '@core/signalr';
import { BehaviorSubject, merge, of } from 'rxjs';
import { filter, map, shareReplay, tap, withLatestFrom } from 'rxjs/operators';
@@ -9,7 +10,16 @@ export const NOTIFICATIONS_HUB_OPTIONS = new InjectionToken<SignalRHubOptions>('
@Injectable()
export class NotificationsHub extends SignalrHub {
updateNotification$ = new BehaviorSubject<MessageBoardItemDTO>(undefined);
constructor(@Inject(NOTIFICATIONS_HUB_OPTIONS) options: SignalRHubOptions) {
get branchNo() {
return String(this._auth.getClaimByKey('branch_no') || this._auth.getClaimByKey('sub'));
}
get sessionStoragesessionStorageKey() {
return `NOTIFICATIONS_BOARD_${this.branchNo}`;
}
constructor(@Inject(NOTIFICATIONS_HUB_OPTIONS) options: SignalRHubOptions, private _auth: AuthService) {
super(options);
}
@@ -31,12 +41,12 @@ export class NotificationsHub extends SignalrHub {
private _storeNotifactions(data: EnvelopeDTO<MessageBoardItemDTO[]>) {
if (data) {
localStorage.setItem('NOTIFICATIONS_BOARD', JSON.stringify(data));
sessionStorage.setItem(this.sessionStoragesessionStorageKey, JSON.stringify(data));
}
}
private _getNotifications(): EnvelopeDTO<MessageBoardItemDTO[]> {
const stringData = localStorage.getItem('NOTIFICATIONS_BOARD');
const stringData = sessionStorage.getItem(this.sessionStoragesessionStorageKey);
if (stringData) {
return JSON.parse(stringData);
}

View File

@@ -66,7 +66,7 @@ export class AppComponent implements OnInit {
if (silentRefreshInterval > 0) {
interval(silentRefreshInterval).subscribe(() => {
if (this._authService.isAuthenticated()) {
this._authService.silentRefresh();
this._authService.refresh();
}
});
}
@@ -135,7 +135,7 @@ export class AppComponent implements OnInit {
onVisibilityChange(event: Event) {
// refresh token when app is in background
if (this._document.hidden && this._authService.isAuthenticated()) {
this._authService.silentRefresh();
this._authService.refresh();
} else if (!this._authService.isAuthenticated()) {
return this._modal
.open({
@@ -145,7 +145,7 @@ export class AppComponent implements OnInit {
})
.afterClosed$.pipe(
tap(() => {
this._authService.login();
this._authService.logout();
})
);
}

View File

@@ -33,7 +33,7 @@ export class HttpErrorInterceptor implements HttpInterceptor {
})
.afterClosed$.pipe(
tap(() => {
this._auth.login();
this._auth.logout();
}),
mergeMap(() => throwError(error))
);

View File

@@ -8,10 +8,10 @@
},
"@core/auth": {
"issuer": "https://sso-test.paragon-data.de",
"clientId": "hug-isa",
"responseType": "id_token token",
"oidc": true,
"scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi"
"clientId": "isa-client",
"responseType": "code",
"scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi",
"showDebugInformation": true
},
"@core/logger": {
"logLevel": "debug"

View File

@@ -8,10 +8,10 @@
},
"@core/auth": {
"issuer": "https://sso-test.paragon-data.de",
"clientId": "hug-isa",
"responseType": "id_token token",
"oidc": true,
"scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi"
"clientId": "isa-client",
"responseType": "code",
"scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi",
"showDebugInformation": true
},
"@core/logger": {
"logLevel": "debug"

View File

@@ -1,7 +1,29 @@
<html>
<body>
<script>
parent.postMessage(location.hash, location.origin);
var checks = [/[\?|&|#]code=/, /[\?|&|#]error=/, /[\?|&|#]token=/, /[\?|&|#]id_token=/];
function isResponse(str) {
if (!str) return false;
for (var i = 0; i < checks.length; i++) {
if (str.match(checks[i])) return true;
}
return false;
}
var message = isResponse(location.hash) ? location.hash : '#' + location.search;
if (window.parent && window.parent !== window) {
// if loaded as an iframe during silent refresh
window.parent.postMessage(message, location.origin);
} else if (window.opener && window.opener !== window) {
// if loaded as a popup during initial login
window.opener.postMessage(message, location.origin);
} else {
// last resort for a popup which has been through redirects and can't use window.opener
localStorage.setItem('auth_hash', message);
localStorage.removeItem('auth_hash');
}
</script>
</body>
</html>

View File

@@ -69,20 +69,15 @@ export class ReorderModalComponent extends ComponentStore<GoodsInListReorderModa
)
);
readonly currentBranch$ = combineLatest([
this._applicationService.getSelectedBranch$(),
this.domainAvailabilityService.getDefaultBranch(),
]).pipe(map(([selectedBranch, defaultBranch]) => selectedBranch ?? defaultBranch));
readonly storeAvailabilities$ = combineLatest([this.orderItem$, this.currentBranch$]).pipe(
switchMap(([item, branch]) =>
readonly storeAvailabilities$ = this.orderItem$.pipe(
switchMap((item) =>
this.domainAvailabilityService
.getPickUpAvailabilities([
{
qty: item.quantity,
ean: item.product.ean,
itemId: item.product?.catalogProductNumber,
shopId: branch.id,
shopId: item?.targetBranchId,
price: item.retailPrice,
},
])

View File

@@ -33,13 +33,30 @@
<ui-select-bullet [ngModel]="selected" (ngModelChange)="setSelected($event)"></ui-select-bullet>
</div>
<div class="item-stock">
<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>
x
<div class="item-stock z-dropdown" [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="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">

View File

@@ -7,7 +7,7 @@ import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { isEqual } from 'lodash';
import { combineLatest } from 'rxjs';
import { debounceTime, switchMap, tap } from 'rxjs/operators';
import { debounceTime, switchMap, map, tap, shareReplay } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
export interface SearchResultItemComponentState {
@@ -83,6 +83,30 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
switchMap((processId) => this.applicationService.getSelectedBranch$(processId))
);
isOrderBranch$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe(
map(([defaultBranch, selectedBranch]) => {
const branch = selectedBranch ?? defaultBranch;
return branch.branchType !== 4;
}),
shareReplay(1)
);
stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe(
map(([defaultBranch, selectedBranch]) => {
if (defaultBranch?.branchType === 4) {
if (!selectedBranch) {
return 'Wählen Sie eine Filiale aus, um den Bestand zu sehen.';
}
} else {
if (selectedBranch && defaultBranch.id !== selectedBranch?.id) {
return 'Sie sehen den Bestand einer anderen Filiale.';
}
}
return '';
}),
shareReplay(1)
);
inStock$ = combineLatest([this.item$, this.selectedBranchId$, this.defaultBranch$]).pipe(
debounceTime(100),
switchMap(([item, branch, defaultBranch]) =>

View File

@@ -7,6 +7,7 @@ import { DomainCatalogModule } from '@domain/catalog';
import { UiCommonModule } from '@ui/common';
import { UiIconModule } from '@ui/icon';
import { UiSelectBulletModule } from '@ui/select-bullet';
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 { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
@@ -28,6 +29,7 @@ import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe
UiSpinnerModule,
UiOrderByFilterModule,
ScrollingModule,
UiTooltipModule,
],
exports: [ArticleSearchResultsComponent, SearchResultItemComponent],
declarations: [

View File

@@ -35,3 +35,7 @@ p {
.select-option {
@apply mt-4 mb-4 border-2 border-solid border-brand text-brand text-cta-l font-bold bg-white rounded-full py-3 px-6;
}
.select-option:disabled {
@apply bg-disabled-branch border-disabled-branch text-white;
}

View File

@@ -9,6 +9,7 @@
</p>
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
<ui-branch-dropdown
class="min-h-[38px]"
[branches]="branches$ | async"
[selected]="selected$ | async"
(selectBranch)="selectBranch($event)"
@@ -18,7 +19,12 @@
>
<div class="grow"></div>
<div>
<button [disabled]="availability.price?.value?.value < 0" type="button" class="select-option" (click)="select()">
<button
[disabled]="availability.price?.value?.value < 0 || !(selected$ | async)"
type="button"
class="select-option"
(click)="select()"
>
Auswählen
</button>
</div>

View File

@@ -13,7 +13,14 @@ import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
})
export class PickUpOptionComponent {
branches$: Observable<BranchDTO[]> = this._purchasingOptionsModalStore.selectAvailableBranches;
selected$: Observable<string> = this._purchasingOptionsModalStore.selectBranch.pipe(map((branch) => branch.name));
selected$: Observable<string> = this._purchasingOptionsModalStore.selectBranch.pipe(
map((branch) => {
// Determins if branch is targetBranch
if (branch.branchType === 1) {
return branch.name;
}
})
);
readonly item$ = this._purchasingOptionsModalStore.selectItem;

View File

@@ -1,10 +1,11 @@
<shared-goods-in-out-order-details (actionHandled)="actionHandled($event)" [itemsSelectable]="true">
<shared-goods-in-out-order-details (actionHandled)="actionHandled($event)" [itemsSelectable]="true" [fetchPartial]="true">
<shared-goods-in-out-order-details-header [order]="order$ | async" (editClick)="navigateToEditPage($event)">
</shared-goods-in-out-order-details-header>
<shared-goods-in-out-order-details-item
*ngFor="let item of items$ | async"
[orderItem]="item"
[order]="order$ | async"
[selected]="true"
></shared-goods-in-out-order-details-item>
<shared-goods-in-out-order-details-tags></shared-goods-in-out-order-details-tags>
</shared-goods-in-out-order-details>

View File

@@ -162,15 +162,11 @@ export class CustomerOrderSearchStore extends ComponentStore<CustomerOrderSearch
switchMap(([options, results, filter, branch]) => {
const queryToken = filter?.getQueryToken() ?? {};
let cachedResultCount: number;
const cached = options?.siletReload && this._cache.get(filter?.getQueryToken());
if (cached) {
const cachedResults = this._cache.get(queryToken);
if (cachedResults?.results?.length > 0) {
this.patchState(cachedResults);
cachedResultCount = cachedResults.results.length;
this._searchResultFromCacheSubject.next({ hits: cachedResults.hits, results: cachedResults.results });
}
}
@@ -180,7 +176,7 @@ export class CustomerOrderSearchStore extends ComponentStore<CustomerOrderSearch
queryToken.take = 50;
} else if (options.siletReload) {
queryToken.skip = 0;
queryToken.take = cachedResultCount || results.length || 50;
queryToken.take = 50;
} else {
queryToken.skip = results.length;
queryToken.take = 50;

View File

@@ -45,6 +45,18 @@ export class SharedGoodsInOutOrderDetailsComponent extends SharedGoodsInOutOrder
}
}
// Von außen auf true setzen um die select-bullets auch ohne die Action FETCHED_PARTIAL anzuzeigen
// Benutzt in Kundenbestellungen #3771
@Input()
get fetchPartial() {
return this.get((s) => s.fetchPartial);
}
set fetchPartial(fetchPartial: boolean) {
if (this.fetchPartial !== fetchPartial) {
this.patchState({ fetchPartial: fetchPartial });
}
}
get receipts() {
return this.get((s) => s.receipts);
}
@@ -216,7 +228,9 @@ export class SharedGoodsInOutOrderDetailsComponent extends SharedGoodsInOutOrder
console.error(error);
}
this.patchState({ fetchPartial: false });
if (action.command.includes('FETCHED_PARTIAL')) {
this.patchState({ fetchPartial: false });
}
this.selectedeOrderItemSubsetIds = [];
setTimeout(() => {

View File

@@ -9,7 +9,7 @@
cdkOverlayOrigin
#trigger="cdkOverlayOrigin"
>
<span>{{ selected }}</span>
<span>{{ selected ?? 'Filiale auswählen' }}</span>
<div class="arrow-icon">
<ui-icon size="15px" icon="arrow_head"></ui-icon>
</div>

View File

@@ -24,8 +24,7 @@ export class UiOverlayTriggerDirective implements OnInit, OnDestroy, OnChanges {
component: UiOverlayTrigger;
@Input()
@HostBinding('disabled')
disabled: boolean;
overlayTriggerDisabled: boolean;
private overlayRef: OverlayRef;
private viewRef: EmbeddedViewRef<any>;
@@ -59,8 +58,15 @@ export class UiOverlayTriggerDirective implements OnInit, OnDestroy, OnChanges {
this._onDestroy$.complete();
}
@HostListener('click')
toggle() {
@HostListener('click', ['$event'])
toggle(event: Event) {
if (this.overlayTriggerDisabled) {
return;
}
event.stopPropagation();
event.preventDefault();
if (this.viewRef) {
this.close();
} else {