Compare commits

...

63 Commits

Author SHA1 Message Date
Michael Auer
12fb774b73 Merge branch 'release/2.0' 2022-08-18 13:44:03 +02:00
Lorenz Hilpert
8cfb160989 Merged PR 1365: #3322 Leseproben - Link geht über Rand
#3322 Leseproben - Link geht über Rand

Related work items: #3322
2022-08-01 13:06:19 +00:00
Lorenz Hilpert
6325167eda Merged PR 1364: changed query from qss to customer_name
changed query from qss to customer_name

Related work items: #3320
2022-08-01 12:10:58 +00:00
Lorenz Hilpert
8925eae4c5 Merged PR 1362: Merge develop => release/2.0
Related work items: #3180, #3203, #3245, #3293, #3299, #3312
2022-07-29 11:58:46 +00:00
Andreas Schickinger
9282bcd779 Merged PR 1361: #3312 NotificationChannel Auswertung angepasst
#3312 NotificationChannel Auswertung angepasst

Related work items: #3312
2022-07-29 11:55:29 +00:00
Lorenz Hilpert
5aa6499598 #3313 Nachbestellen auf der Wareneingangsliste wirft Fehler 2022-07-29 11:47:03 +02:00
Andreas Schickinger
f766781928 Merged PR 1360: Merge release 2.0 -> develop
Related work items: #3180, #3203, #3245, #3293, #3299
2022-07-28 14:05:13 +00:00
Andreas Schickinger
02834b7102 Merged PR 1359: Merge develop -> release 2.0
Merge develop -> release 2.0

Related work items: #3180, #3203, #3245, #3299
2022-07-28 13:48:10 +00:00
Andreas Schickinger
a9e3430505 Merged PR 1358: #3180 getTakeAwayAvailability angepasst
#3180 getTakeAwayAvailability angepasst

Related work items: #3180
2022-07-28 13:30:06 +00:00
Nino Righi
4b9a23001a Merged PR 1357: #3267 Email Validator Regex updated, Notification Channel Control Logic modified
#3267 Email Validator Regex updated, Notification Channel Control Logic modified
2022-07-28 13:25:55 +00:00
Nino Righi
8de7ec9124 Merged PR 1356: #3267 Bugfix If Both Communication Details are Valid update Customer correctly
#3267 Bugfix If Both Communication Details are Valid update Customer correctly
2022-07-27 16:30:18 +00:00
Andreas Schickinger
19a0a3c7c3 Merged PR 1355: #3180 Lupe wird erst angezeigt, wenn das Bild erfolgreich geladen wurde
#3180 Lupe wird erst angezeigt, wenn das Bild erfolgreich geladen wurde

Related work items: #3180
2022-07-27 16:21:55 +00:00
Nino Righi
42a7d6e4b7 Merged PR 1352: #3267 Checkout Cart Fix Notification Channel disable Order CTA if communication details missing
#3267 Checkout Cart Fix Notification Channel disable Order CTA if communication details missing
2022-07-27 15:21:52 +00:00
Nino Righi
66818b1647 Merged PR 1354: #3304 Customer Search Message Fix
#3304 Customer Search Message Fix
2022-07-27 14:06:07 +00:00
Andreas Schickinger
bc8ba9adc8 Merged PR 1353: #3180 Entkoppelte Ladebereiche für Verfügbarkeiten
#3180 Entkoppelte Ladebereiche für Verfügbarkeiten

Related work items: #3180
2022-07-27 13:16:35 +00:00
Nino Righi
4ae5759361 Merged PR 1351: #3304 Fix WA Autocomplete Dropdown closing after queryParams change
#3304 Fix WA Autocomplete Dropdown closing after queryParams change
2022-07-26 14:57:11 +00:00
Andreas Schickinger
b5cfcf8036 Merged PR 1350: #3245 Zubuchen disabled fixes
#3245 Zubuchen disabled fixes

Related work items: #3245
2022-07-26 14:47:46 +00:00
Lorenz Hilpert
061982cf2c Merged PR 1349: Dashboard // nicht alle Infos werden angezeigt
Related work items: #3299
2022-07-26 12:00:09 +00:00
Nino Righi
0e1422c2c4 Merged PR 1348: #3267 Checkout Cart Notification Disable Order Button if Checkbox is Active b...
#3267 Checkout Cart Notification Disable Order Button if Checkbox is Active but E-Mail or Mobilenumber is missing
2022-07-26 11:09:43 +00:00
Nino Righi
3e534029a0 Merged PR 1347: #3167 Fix Reorder Modal Styling Stock Column Text Centered
#3167 Fix Reorder Modal Styling Stock Column Text Centered
2022-07-25 15:44:42 +00:00
Nino Righi
8d9ee9fe5c Merged PR 1343: #3297 Fix Reorder Modal takes now the correct Quantity
#3297 Fix Reorder Modal takes now the correct Quantity
2022-07-25 15:44:10 +00:00
Lorenz Hilpert
675aa04564 Added Generated package-lock.json - npm version 6 2022-07-25 17:42:30 +02:00
Lorenz Hilpert
88c8885a81 Build Test 2022-07-25 17:26:14 +02:00
Andreas Schickinger
151760aef9 Merged PR 1346: #2303 WA AHF // Zusatz bei Gruppierung in Trefferliste beachten
#3203 WA AHF // Zusatz bei Gruppierung in Trefferliste beachten

Related work items: #3203
2022-07-25 15:15:02 +00:00
Nino Righi
6c89969b60 Merged PR 1345: #3214 Price Diff Modal Styling Adjustments
#3214 Price Diff Modal Styling Adjustments
2022-07-25 14:57:13 +00:00
Nino Righi
0fd5e66c33 Merged PR 1344: #3287 Notification Channel preselect E-Mail if E-Mail and SMS is available
#3287 Notification Channel preselect E-Mail if E-Mail and SMS is available
2022-07-25 14:55:08 +00:00
Lorenz Hilpert
c8aa526e4d Fix Toaster Component => onSlideFinished 2022-07-25 16:29:38 +02:00
Andreas Schickinger
f2c492c6ea Merged PR 1342: #3245 Zubuchen disabled wenn ein Zusatz ausgewählt ist
#3245 Zubuchen disabled wenn ein Zusatz ausgewählt ist

Related work items: #3245
2022-07-25 14:24:21 +00:00
Lorenz Hilpert
11cf845235 Update ToastComponent.timeoutRef to any 2022-07-25 15:25:00 +02:00
Lorenz Hilpert
ae6fbc7c64 update packages 2022-07-25 15:23:45 +02:00
Andreas Schickinger
71eda539f6 Merged PR 1341: Merged PR 1340: #3293 Filtereinstellungen bei Tabwechsel
Merged PR 1340: #3293 Filtereinstellungen bei Tabwechsel

#3293 Filtereinstellungen bei Tabwechsel

Related work items: #3293

Related work items: #3293
2022-07-21 13:51:09 +00:00
Andreas Schickinger
f43b948ac9 Merged PR 1340: #3293 Filtereinstellungen bei Tabwechsel
#3293 Filtereinstellungen bei Tabwechsel

Related work items: #3293
2022-07-21 09:58:55 +00:00
Andreas Schickinger
1b77020b6a Merge branch 'develop' into release/2.0 2022-07-20 13:38:46 +02:00
Andreas Schickinger
1f62040560 Merged PR 1339: #3265 Warenausgabe Scrolling und SilentReload Bugfix
#3265 Warenausgabe Scrolling und SilentReload Bugfix

Related work items: #3265
2022-07-19 15:17:11 +00:00
Nino Righi
cc5c3167b1 Merged PR 1338: #3292 Process Guards Breadcrumbs Fix
#3292 Process Guards Breadcrumbs Fix
2022-07-19 15:01:37 +00:00
Andreas Schickinger
b9b79b949f Merged PR 1337: #3291 Warenausgabe Caching Verhalten und Scrolling angepasst
#3291 Warenausgabe Caching Verhalten und Scrolling angepasst

Related work items: #3291
2022-07-19 13:06:05 +00:00
Nino Righi
a0d729fe6d Merged PR 1336: #3272 Revert WA Tab Naming changes back to current production version
#3272 Revert WA Tab Naming changes back to current production version
2022-07-19 12:31:10 +00:00
Andreas Schickinger
f618dd3865 Merged PR 1335: #3291 WA Listenansicht Statusänderung cleared Cache
#3291 WA Listenansicht Statusänderung cleared Cache

Related work items: #3291
2022-07-18 15:58:59 +00:00
Nino Righi
3fd3f972db Merged PR 1334: #3287 Notifications Deactivate Other Channels except SMS and EMail
#3287 Notifications Deactivate Other Channels except SMS and EMail
2022-07-18 15:32:07 +00:00
Andreas Schickinger
2311655e5e Merged PR 1333: #3285 TK Artikelbilder verzerrt Dashboard fix
#3285 TK Artikelbilder verzerrt Dashboard fix
2022-07-18 14:40:29 +00:00
Nino Righi
c589836097 Merged PR 1332: #3286 Remission Fix Filter Settings change to default after Source Changed
#3286 Remission Fix Filter Settings change to default after Source Changed
2022-07-18 14:34:52 +00:00
Andreas Schickinger
dbc641cfce Merged PR 1331: #3285 TK Bilder verzerrt
#3285 TK Bilder verzerrt

Related work items: #3285
2022-07-18 13:53:01 +00:00
Andreas Schickinger
f13bc58925 Merged PR 1330: #3276 Remission Filter Button nicht mehr disabled
#3276 Remission Filter Button nicht mehr disabled

Related work items: #3276
2022-07-18 13:08:07 +00:00
Andreas Schickinger
94d5892cf1 Merged PR 1328: #3272 Beim Wechsel zwischen WA und Artikelrecherche den Warenkorb nicht mehr...
#3272 Beim Wechsel zwischen WA und Artikelrecherche den Warenkorb nicht mehr verwerfen

Related work items: #3272
2022-07-18 10:18:10 +00:00
Andreas Schickinger
8e32b15f26 Merged PR 1329: #3265 Warenausgabe Tabwechsel fixes
#3265 Warenausgabe Tabwechsel fixes
- Fehlermeldung ScrollPosition
- Suche wird trotz Cache ausgeführt
- Scroll Top bei erneuter Suche im gleichen Tab oder über Filter
- Beim Vorgangswechsel wurde der Filter nicht korrekt resetted

Related work items: #3265
2022-07-18 09:37:27 +00:00
Andreas Schickinger
fe5f0ef2eb Merged PR 1327: #3283 Fallback URL für einen Vorgang von Dashboard auf Artikelsuche geändert
#3283 Fallback URL für einen Vorgang von Dashboard auf Artikelsuche geändert. Dadurch kommt es nicht mehr dazu, dass ein Tab nicht selektierbar "hängen bleibt" und zum Dashboard navigiert. Die Ursache wie es zu dem Problem kam ist noch unbekannt

Related work items: #3283
2022-07-18 09:18:13 +00:00
Andreas Schickinger
daa27d5f2d Merged PR 1326: #3282 Fehlerdialog und Logout bei zu langer Inaktivität
#3282 Fehlerdialog und Logout bei zu langer Inaktivität - PR für Test

Related work items: #3282
2022-07-18 09:14:05 +00:00
Nino Righi
bb7626609e Merged PR 1325: #3272 Prozess Tab Bugfixes
#3272 Prozess Tab Bugfixes
2022-07-14 15:59:36 +00:00
Nino Righi
9ed58b685b Merged PR 1323: #3272 #3275 Tab Process Management Updated, Cart, CheckoutCart, WA
#3272 #3275 Tab Process Management Updated, Cart, CheckoutCart, WA
2022-07-14 14:12:21 +00:00
Andreas Schickinger
4eb81ad30a Merged PR 1324: #3276 Remission Starten Button disabled, wenn Liste läd
#3276 Remission Starten Button disabled, wenn Liste läd

Related work items: #3276
2022-07-14 13:49:26 +00:00
Nino Righi
a1f2cb57b3 Merged PR 1322: #3275 Bestellbestätigungs Prozess Bugfixes
#3275 Bestellbestätigungs Prozess Bugfixes
2022-07-13 15:03:40 +00:00
Andreas Schickinger
62b8e387ca Merged PR 1321: #3270 Listenbestellung B2B Preis wird richtig übernommen
#3270 Listenbestellung B2B Preis wird richtig übernommen

Related work items: #3270
2022-07-13 14:59:36 +00:00
Andreas Schickinger
07498db711 Merged PR 1316: #3270 Listenbestellung Popup Preis wird bei Wechsel der Filteroption aktualis...
#3270 Listenbestellung Popup Preis wird bei Wechsel der Filteroption aktualisiert

Related work items: #3270
2022-07-12 15:33:29 +00:00
Nino Righi
2adc8c6f5d Merged PR 1320: #3275 Made Cart Checkout Process reusable
#3275 Made Cart Checkout Process reusable
2022-07-12 15:31:56 +00:00
Lorenz Hilpert
f15a43f303 Merged PR 1319: #3274 Warenausgabe - Filter
#3274 Warenausgabe - Filter
2022-07-12 13:12:59 +00:00
Nino Righi
e35aea5a7e Merged PR 1318: #3273 #3271 Notifications Cart Show Toggle and Bugfix
#3273 #3271 Notifications Cart Show Toggle and Bugfix
2022-07-12 09:02:00 +00:00
Lorenz Hilpert
0e1ed9d8cc Merge branch 'feature/#3265-Scroll-Position-Bug' into develop 2022-07-11 16:13:05 +02:00
Lorenz Hilpert
f62ef06e51 Remove Process Tabs in Goods Out 2022-07-11 16:06:47 +02:00
Andreas Schickinger
30f4d4588f Merged PR 1315: #3269 Bestellbestaetigung fehlerhafte Anzeige
#3269 Bestellbestaetigung fehlerhafte Anzeige

Related work items: #3269
2022-07-11 12:59:22 +00:00
Lorenz Hilpert
e102396dab #3265 Scroll Position Bug 2022-07-11 12:03:10 +02:00
Andreas Schickinger
f60815ef63 Merged PR 1314: #3268 Artikelformat wird überall auf undefined geprüft. Fehlendes FormatIcon...
#3268 Artikelformat wird überall auf undefined geprüft. Fehlendes FormatIcon in PurchasingOptions implementiert

Related work items: #3268
2022-07-08 13:59:37 +00:00
Andreas Schickinger
7b11b53774 Merged PR 1313: #3267 Warenkorb NotificationChannels nur speichern, wenn Haken und Value gese...
#3267 Warenkorb NotificationChannels nur speichern, wenn Haken und Value gesetzt wurden

Related work items: #3267
2022-07-08 08:11:35 +00:00
Nino Righi
abff7715ee Merged PR 1312: #3256 Fix Display Toast Notification
#3256 Fix Display Toast Notification
2022-07-08 08:09:07 +00:00
77 changed files with 4111 additions and 6414 deletions

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core';
import { CacheOptions } from './cache-options';
import { Cached } from './cached';
import { sha1 } from 'object-hash';
@Injectable({
providedIn: 'root',
@@ -60,7 +59,15 @@ export class CacheService {
}
private getKey(token: Object) {
return sha1(token);
return this.hash(JSON.stringify(token));
}
private hash(data: string): string {
let hash = 0;
for (let i = 0; i < data.length; i++) {
hash = data.charCodeAt(i) + ((hash << 5) - hash);
}
return hash.toString(16);
}
private serialize(data: Cached): string {

View File

@@ -1,9 +1,4 @@
<div
class="toast-main"
[style.width]="width"
[@slideAnimation]="{ value: animationState }"
(@slideAnimation.done)="onSlideFinished($event)"
>
<div class="toast-main" [style.width]="width" [@slideAnimation]="{ value: animationState }" (@slideAnimation.done)="onSlideFinished()">
<button class="absolute top-2 right-2 p-6 border-none bg-transparent" (click)="close()">
<ui-icon icon="close" size="20px"></ui-icon>
</button>

View File

@@ -11,9 +11,9 @@ import { TOAST_CONFIG_TOKEN } from './tokens';
animations: [toastAnimations.slideToast],
})
export class ToastComponent implements OnInit, OnDestroy {
timeoutRef: NodeJS.Timeout;
timeoutRef?: any;
animationState: ToastAnimationState = 'default';
width: string = '55.25rem';
width = '55.25rem';
constructor(
@Inject(TOAST_CONFIG_TOKEN) public readonly data: Toast,
@@ -40,7 +40,7 @@ export class ToastComponent implements OnInit, OnDestroy {
this.animationState = 'closing';
}
onSlideFinished(event: AnimationEvent) {
onSlideFinished() {
if (this.animationState === 'closing') {
this._ref.close();
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { ItemDTO, SearchService } from '@swagger/cat';
import { ItemDTO } from '@swagger/cat';
import { AvailabilityDTO, BranchDTO, OLAAvailabilityDTO, StoreCheckoutService, SupplierDTO } from '@swagger/checkout';
import { combineLatest, Observable, of } from 'rxjs';
import {
@@ -37,7 +37,7 @@ export class DomainAvailabilityService {
@memorize()
getTakeAwaySupplier(): Observable<SupplierDTO> {
return this.storeCheckoutService.StoreCheckoutGetSuppliers({}).pipe(
map(({ result }) => result.find((supplier) => supplier?.supplierNumber === 'F')),
map(({ result }) => result?.find((supplier) => supplier?.supplierNumber === 'F')),
shareReplay()
);
}
@@ -125,8 +125,13 @@ export class DomainAvailabilityService {
getTakeAwayAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
return this.getCurrentStock().pipe(
switchMap((s) => this._stock.StockInStock({ articleIds: [item.itemId], stockId: s.id })),
withLatestFrom(this.getTakeAwaySupplier(), this.getCurrentBranch()),
switchMap((s) =>
combineLatest([
this._stock.StockInStock({ articleIds: [item.itemId], stockId: s.id }),
this.getTakeAwaySupplier(),
this.getCurrentBranch(),
])
),
map(([response, supplier, branch]) => {
const price = item?.price;
return this._mapToTakeAwayAvailability({ response, supplier, branch, quantity, price });

View File

@@ -1,4 +1,6 @@
// start:ng42.barrel
export * from './info-feed-item';
export * from './info-feed';
export * from './kpi-feed-item';
export * from './kpi-feed';
export * from './products-feed';

View File

@@ -0,0 +1,4 @@
export interface InfoFeedItem {
heading: string;
text: string;
}

View File

@@ -0,0 +1,7 @@
import { FeedDTO } from '@swagger/isa';
import { InfoFeedItem } from './info-feed-item';
export interface InfoFeed extends FeedDTO {
type: 'info';
items: InfoFeedItem[];
}

View File

@@ -26,7 +26,7 @@ export class ReOrderActionHandler extends ActionHandler<OrderItemsContext> {
content: ReorderModalComponent,
title: 'Artikel nachbestellen',
data: {
item: orderItem,
item: { ...orderItem, quantity: data.itemQuantity?.get(orderItem.orderItemSubsetId) ?? orderItem.quantity },
showReasons: true,
},
})

View File

@@ -55,7 +55,7 @@ export class DomainGoodsService {
return this.abholfachService.AbholfachWareneingang({
filter: { orderitemprocessingstatus: '16;128;8192;1048576' },
input: {
qs: customerNumber,
customer_name: customerNumber,
},
});
}

View File

@@ -1,11 +1,12 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateCustomerWithProcessIdGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService) {}
constructor(private readonly _applicationService: ApplicationService, private readonly _breadcrumbService: BreadcrumbService) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const process = await this._applicationService
@@ -25,16 +26,42 @@ export class CanActivateCustomerWithProcessIdGuard implements CanActivate {
id: +route.params.processId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes)}`,
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
});
}
await this.removeBreadcrumbWithSameProcessId(route);
this._applicationService.activateProcess(+route.params.processId);
return true;
}
// Fix #3292: Alle Breadcrumbs die nichts mit dem aktuellen Prozess zu tun haben, müssen removed werden
async removeBreadcrumbWithSameProcessId(route: ActivatedRouteSnapshot) {
const crumbs = await this._breadcrumbService
.getBreadcrumbByKey$(+route.params.processId)
.pipe(first())
.toPromise();
// Entferne alle Crumbs die nichts mit der Kundensuche zu tun haben
if (crumbs.length > 1) {
const crumbsToRemove = crumbs.filter((crumb) => crumb.tags.find((tag) => tag === 'customer') === undefined);
for (const crumb of crumbsToRemove) {
await this._breadcrumbService.removeBreadcrumb(crumb.id);
}
}
}
processNumber(processes: ApplicationProcess[]) {
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
return !!processNumbers && processNumbers?.length > 0 ? Math.max(...processNumbers) + 1 : 1;
return !!processNumbers && processNumbers.length > 0 ? this.findMissingNumber(processNumbers) : 1;
}
findMissingNumber(processNumbers: number[]) {
for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
if (!processNumbers.find((number) => number === missingNumber)) {
return missingNumber;
}
}
return Math.max(...processNumbers) + 1;
}
}

View File

@@ -1,27 +1,117 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationService } from '@core/application';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateCustomerGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _router: Router) {}
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,
private readonly _router: Router
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
let lastActivatedProcessId = (
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'cart').pipe(first()).toPromise()
)?.id;
if (!lastActivatedProcessId) {
lastActivatedProcessId = Date.now();
await this._applicationService.createProcess({
id: lastActivatedProcessId,
type: 'cart',
section: 'customer',
name: `Vorgang ${processes.length + 1}`,
});
const lastActivatedCartCheckoutProcessId = (
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'cart-checkout').pipe(first()).toPromise()
)?.id;
const lastActivatedGoodsOutProcessId = (
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'goods-out').pipe(first()).toPromise()
)?.id;
const activatedProcessId = await this._applicationService.getActivatedProcessId$().pipe(first()).toPromise();
// Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
if (!!lastActivatedCartCheckoutProcessId && lastActivatedCartCheckoutProcessId === activatedProcessId) {
await this.fromCartCheckoutProcess(processes, lastActivatedCartCheckoutProcessId);
return false;
} else if (!!lastActivatedGoodsOutProcessId && lastActivatedGoodsOutProcessId === activatedProcessId) {
await this.fromGoodsOutProcess(processes, lastActivatedGoodsOutProcessId);
return false;
}
if (!lastActivatedProcessId) {
await this.fromCartProcess(processes);
return false;
} else {
await this._router.navigate(['/kunde', String(lastActivatedProcessId), 'customer']);
}
await this._router.navigate(['/kunde', lastActivatedProcessId, 'customer']);
return false;
}
// Bei offener Artikelsuche/Kundensuche und Klick auf Footer Kundensuche
async fromCartProcess(processes: ApplicationProcess[]) {
const newProcessId = Date.now();
await this._applicationService.createProcess({
id: newProcessId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
});
await this._router.navigate(['/kunde', String(newProcessId), 'customer']);
}
// Bei offener Bestellbestätigung und Klick auf Footer Kundensuche
async fromCartCheckoutProcess(processes: ApplicationProcess[], processId: number) {
// Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
this._checkoutService.removeProcess({ processId });
// Ändere type cart-checkout zu cart
this._applicationService.patchProcess(processId, {
id: processId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
data: {},
});
// Navigation
await this._router.navigate(['/kunde', String(processId), 'customer']);
}
// Bei offener Warenausgabe und Klick auf Footer Kundensuche
async fromGoodsOutProcess(processes: ApplicationProcess[], processId: number) {
const buyer = await this._checkoutService.getBuyer({ processId }).pipe(first()).toPromise();
const customerFeatures = await this._checkoutService.getCustomerFeatures({ processId }).pipe(first()).toPromise();
const name = buyer
? customerFeatures?.b2b
? buyer.organisation?.name
? buyer.organisation?.name
: buyer.lastName
: buyer.lastName
: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`;
// Ändere type goods-out zu cart
this._applicationService.patchProcess(processId, {
id: processId,
type: 'cart',
section: 'customer',
name,
});
// Navigation
await this._router.navigate(['/kunde', String(processId), 'customer']);
}
processNumber(processes: ApplicationProcess[]) {
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
return !!processNumbers && processNumbers.length > 0 ? this.findMissingNumber(processNumbers) : 1;
}
findMissingNumber(processNumbers: number[]) {
for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
if (!processNumbers.find((number) => number === missingNumber)) {
return missingNumber;
}
}
return Math.max(...processNumbers) + 1;
}
}

View File

@@ -1,11 +1,12 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateGoodsOutWithProcessIdGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService) {}
constructor(private readonly _applicationService: ApplicationService, private readonly _breadcrumbService: BreadcrumbService) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const process = await this._applicationService
@@ -14,19 +15,36 @@ export class CanActivateGoodsOutWithProcessIdGuard implements CanActivate {
.toPromise();
if (!process) {
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
// const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
await this._applicationService.createProcess({
id: +route.params.processId,
type: 'goods-out',
section: 'customer',
name: `Warenausgabe ${this.processNumber(processes)}`,
name: `Warenausgabe`,
});
}
await this.removeBreadcrumbWithSameProcessId(route);
this._applicationService.activateProcess(+route.params.processId);
return true;
}
// Fix #3292: Alle Breadcrumbs die nichts mit dem aktuellen Prozess zu tun haben, müssen removed werden
async removeBreadcrumbWithSameProcessId(route: ActivatedRouteSnapshot) {
const crumbs = await this._breadcrumbService
.getBreadcrumbByKey$(+route.params.processId)
.pipe(first())
.toPromise();
// Entferne alle Crumbs die nichts mit der Warenausgabe zu tun haben
if (crumbs.length > 1) {
const crumbsToRemove = crumbs.filter((crumb) => crumb.tags.find((tag) => tag === 'goods-out') === undefined);
for (const crumb of crumbsToRemove) {
await this._breadcrumbService.removeBreadcrumb(crumb.id);
}
}
}
processNumber(processes: ApplicationProcess[]) {
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
return !!processNumbers && processNumbers?.length > 0 ? Math.max(...processNumbers) + 1 : 1;

View File

@@ -1,44 +1,164 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationService } from '@core/application';
import { Config } from '@core/config';
import { first, map } from 'rxjs/operators';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateGoodsOutGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _router: Router) {}
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,
private readonly _router: Router
) {}
// !!! Ticket #3272 Code soll vorerst bestehen bleiben. Prozess Warenausgabe soll wieder Vorgang heißen (wie aktuell im Produktiv), bis zum neuen Navigationskonzept
// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
// async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
// const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
// let lastActivatedProcessId = (
// await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'goods-out').pipe(first()).toPromise()
// )?.id;
// const lastActivatedCartProcessId = (
// await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'cart').pipe(first()).toPromise()
// )?.id;
// const lastActivatedCartCheckoutProcessId = (
// await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'cart-checkout').pipe(first()).toPromise()
// )?.id;
// const activatedProcessId = await this._applicationService.getActivatedProcessId$().pipe(first()).toPromise();
// // Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
// if (!!lastActivatedCartProcessId && lastActivatedCartProcessId === activatedProcessId) {
// await this.fromCartProcess(processes, route, lastActivatedCartProcessId);
// return false;
// } else if (!!lastActivatedCartCheckoutProcessId && lastActivatedCartCheckoutProcessId === activatedProcessId) {
// await this.fromCartCheckoutProcess(processes, route, lastActivatedCartCheckoutProcessId);
// return false;
// }
// if (!lastActivatedProcessId) {
// await this.fromGoodsOutProcess(processes, route);
// return false;
// } else {
// await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)]));
// }
// return false;
// }
// // Bei offener Warenausgabe und Klick auf Footer Warenausgabe
// async fromGoodsOutProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot) {
// const newProcessId = Date.now();
// await this._applicationService.createProcess({
// id: newProcessId,
// type: 'goods-out',
// section: 'customer',
// name: `Warenausgabe ${this.processNumber(processes.filter((process) => process.type === 'goods-out'))}`,
// });
// await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(newProcessId)]));
// }
// // Bei offener Artikelsuche/Kundensuche und Klick auf Footer Warenausgabe
// async fromCartProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot, processId: number) {
// // Ändere type cart zu goods-out
// this._applicationService.patchProcess(processId, {
// id: processId,
// type: 'goods-out',
// section: 'customer',
// name: `Warenausgabe ${this.processNumber(processes.filter((process) => process.type === 'goods-out'))}`,
// });
// // Navigation
// await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(processId)]));
// }
// // Bei offener Bestellbestätigung, Artikelsuche/Kundensuche und Klick auf Footer Warenausgabe
// async fromCartCheckoutProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot, processId: number) {
// // Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
// this._checkoutService.removeProcess({ processId });
// // Ändere type cart-checkout zu goods-out
// this._applicationService.patchProcess(processId, {
// id: processId,
// type: 'goods-out',
// section: 'customer',
// name: `Warenausgabe ${this.processNumber(processes.filter((process) => process.type === 'goods-out'))}`,
// data: {},
// });
// // Navigation
// await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(processId)]));
// }
// !!! Ticket #3272 Code soll vorerst bestehen bleiben. Prozess Warenausgabe soll wieder Vorgang heißen (wie aktuell im Produktiv), bis zum neuen Navigationskonzept
// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const processesIds = await this._applicationService
.getProcesses$('customer')
.pipe(
first(),
map((p) => {
return p.filter((process) => process.type === 'goods-out').map((process) => +process.name.replace('Warenausgabe', '').trim());
})
)
.toPromise();
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
let lastActivatedProcessId = (
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'goods-out').pipe(first()).toPromise()
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'cart').pipe(first()).toPromise()
)?.id;
console.log(processesIds);
const lastActivatedCartCheckoutProcessId = (
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'cart-checkout').pipe(first()).toPromise()
)?.id;
// if (!lastActivatedProcessId) {
lastActivatedProcessId = Date.now();
await this._applicationService.createProcess({
id: lastActivatedProcessId,
type: 'goods-out',
section: 'customer',
name: `Warenausgabe ${Math.max(...processesIds, 0) + 1}`,
});
// }
const activatedProcessId = await this._applicationService.getActivatedProcessId$().pipe(first()).toPromise();
// Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
if (!!lastActivatedCartCheckoutProcessId && lastActivatedCartCheckoutProcessId === activatedProcessId) {
await this.fromCartCheckoutProcess(processes, route, lastActivatedCartCheckoutProcessId);
return false;
}
if (!lastActivatedProcessId) {
await this.fromGoodsOutProcess(processes, route);
return false;
} else {
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)]));
}
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)]));
return false;
}
// Bei offener Warenausgabe und Klick auf Footer Warenausgabe
async fromGoodsOutProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot) {
const newProcessId = Date.now();
await this._applicationService.createProcess({
id: newProcessId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
});
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(newProcessId)]));
}
// Bei offener Bestellbestätigung und Klick auf Footer Warenausgabe
async fromCartCheckoutProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot, processId: number) {
// Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
this._checkoutService.removeProcess({ processId });
// Ändere type cart-checkout zu goods-out
this._applicationService.patchProcess(processId, {
id: processId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
data: {},
});
// Navigation
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(processId)]));
}
getUrlFromSnapshot(route: ActivatedRouteSnapshot, url: string[] = []): string[] {
url.push(...route.url.map((segment) => segment.path));
if (route.firstChild) {
@@ -46,4 +166,18 @@ export class CanActivateGoodsOutGuard implements CanActivate {
}
return url.filter((segment) => !!segment);
}
processNumber(processes: ApplicationProcess[]) {
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
return !!processNumbers && processNumbers.length > 0 ? this.findMissingNumber(processNumbers) : 1;
}
findMissingNumber(processNumbers: number[]) {
for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
if (!processNumbers.find((number) => number === missingNumber)) {
return missingNumber;
}
}
return Math.max(...processNumbers) + 1;
}
}

View File

@@ -1,11 +1,12 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateProductWithProcessIdGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService) {}
constructor(private readonly _applicationService: ApplicationService, private readonly _breadcrumbService: BreadcrumbService) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const process = await this._applicationService
@@ -25,16 +26,45 @@ export class CanActivateProductWithProcessIdGuard implements CanActivate {
id: +route.params.processId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes)}`,
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
});
}
await this.removeBreadcrumbWithSameProcessId(route);
this._applicationService.activateProcess(+route.params.processId);
return true;
}
// Fix #3292: Alle Breadcrumbs die nichts mit dem aktuellen Prozess zu tun haben, müssen removed werden
async removeBreadcrumbWithSameProcessId(route: ActivatedRouteSnapshot) {
const crumbs = await this._breadcrumbService
.getBreadcrumbByKey$(+route.params.processId)
.pipe(first())
.toPromise();
// Entferne alle Crumbs die nichts mit der Artikelsuche zu tun haben
if (crumbs.length > 1) {
const crumbsToRemove = crumbs.filter((crumb) => crumb.tags.find((tag) => tag === 'catalog') === undefined);
for (const crumb of crumbsToRemove) {
await this._breadcrumbService.removeBreadcrumb(crumb.id);
}
}
}
processNumber(processes: ApplicationProcess[]) {
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
return !!processNumbers && processNumbers?.length > 0 ? Math.max(...processNumbers) + 1 : 1;
return !!processNumbers && processNumbers.length > 0 ? this.findMissingNumber(processNumbers) : 1;
}
findMissingNumber(processNumbers: number[]) {
// Ticket #3272 Bei Klick auf "+" bzw. neuen Prozess hinzufügen soll der neue Tab immer die höchste Nummer haben (wie aktuell im Produktiv)
// ----------------------------------------------------------------------------------------------------------------------------------------
// for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
// if (!processNumbers.find((number) => number === missingNumber)) {
// return missingNumber;
// }
// }
return Math.max(...processNumbers) + 1;
}
}

View File

@@ -1,11 +1,16 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationService } from '@core/application';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateProductGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _router: Router) {}
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,
private readonly _router: Router
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
@@ -13,20 +18,90 @@ export class CanActivateProductGuard implements CanActivate {
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'cart').pipe(first()).toPromise()
)?.id;
if (!lastActivatedProcessId) {
lastActivatedProcessId = Date.now();
await this._applicationService.createProcess({
id: lastActivatedProcessId,
type: 'cart',
section: 'customer',
name: `Vorgang ${processes.length + 1}`,
});
const lastActivatedCartCheckoutProcessId = (
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'cart-checkout').pipe(first()).toPromise()
)?.id;
const lastActivatedGoodsOutProcessId = (
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'goods-out').pipe(first()).toPromise()
)?.id;
const activatedProcessId = await this._applicationService.getActivatedProcessId$().pipe(first()).toPromise();
// Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
if (!!lastActivatedCartCheckoutProcessId && lastActivatedCartCheckoutProcessId === activatedProcessId) {
await this.fromCartCheckoutProcess(processes, route, lastActivatedCartCheckoutProcessId);
return false;
} else if (!!lastActivatedGoodsOutProcessId && lastActivatedGoodsOutProcessId === activatedProcessId) {
await this.fromGoodsOutProcess(processes, route, lastActivatedGoodsOutProcessId);
return false;
}
if (!lastActivatedProcessId) {
await this.fromCartProcess(processes, route);
return false;
} else {
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)]));
}
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)]));
return false;
}
// Bei offener Artikelsuche/Kundensuche und Klick auf Footer Artikelsuche
async fromCartProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot) {
const newProcessId = Date.now();
await this._applicationService.createProcess({
id: newProcessId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
});
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(newProcessId)]));
}
// Bei offener Warenausgabe und Klick auf Footer Artikelsuche
async fromGoodsOutProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot, processId: number) {
const buyer = await this._checkoutService.getBuyer({ processId }).pipe(first()).toPromise();
const customerFeatures = await this._checkoutService.getCustomerFeatures({ processId }).pipe(first()).toPromise();
const name = buyer
? customerFeatures?.b2b
? buyer.organisation?.name
? buyer.organisation?.name
: buyer.lastName
: buyer.lastName
: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`;
// Ändere type goods-out zu cart
this._applicationService.patchProcess(processId, {
id: processId,
type: 'cart',
section: 'customer',
name,
});
// Navigation
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(processId)]));
}
// Bei offener Bestellbestätigung und Klick auf Footer Artikelsuche
async fromCartCheckoutProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot, processId: number) {
// Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
this._checkoutService.removeProcess({ processId });
// Ändere type cart-checkout zu cart
this._applicationService.patchProcess(processId, {
id: processId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
data: {},
});
// Navigation
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(processId)]));
}
getUrlFromSnapshot(route: ActivatedRouteSnapshot, url: string[] = []): string[] {
url.push(...route.url.map((segment) => segment.path));
if (route.firstChild) {
@@ -34,4 +109,18 @@ export class CanActivateProductGuard implements CanActivate {
}
return url.filter((segment) => !!segment);
}
processNumber(processes: ApplicationProcess[]) {
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
return !!processNumbers && processNumbers.length > 0 ? this.findMissingNumber(processNumbers) : 1;
}
findMissingNumber(processNumbers: number[]) {
for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
if (!processNumbers.find((number) => number === missingNumber)) {
return missingNumber;
}
}
return Math.max(...processNumbers) + 1;
}
}

View File

@@ -1,13 +1,13 @@
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHandler, Injectable } from '@angular/core';
import { AuthService } from '@core/auth';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { UiDialogModalComponent, UiErrorModalComponent, UiModalService } from '@ui/modal';
@Injectable()
export class IsaErrorHandler implements ErrorHandler {
constructor(private _modal: UiModalService, private _authService: AuthService) {}
handleError(error: any): void {
async handleError(error: any): Promise<void> {
console.error(error);
// Bei Klick auf Abbrechen auf der Login Seite erneut zur Login Seite weiterleiten
@@ -16,6 +16,22 @@ export class IsaErrorHandler implements ErrorHandler {
return;
}
if (error instanceof HttpErrorResponse && error?.status === 401) {
await this._modal
.open({
content: UiDialogModalComponent,
title: 'Sitzung abgelaufen',
data: {
content: 'Sie waren zu lange nicht in der ISA aktiv. Bitte melden Sie sich erneut an',
actions: [{ command: 'CLOSE', selected: true, label: 'Erneut anmelden' }],
},
})
.afterClosed$.toPromise();
this._authService.logout();
return;
}
this._modal.open({
content: UiErrorModalComponent,
title:

View File

@@ -385,7 +385,7 @@ describe('ShellComponent', () => {
spyOn(router, 'navigate');
await spectator.component.activateProcess(1);
expect(router.navigate).toHaveBeenCalledWith(['/kunde']);
expect(router.navigate).toHaveBeenCalledWith(['/kunde', 1, 'product']);
});
});

View File

@@ -105,7 +105,7 @@ export class ShellComponent {
if (latestCrumb) {
await this._router.navigate([latestCrumb.path], { queryParams: latestCrumb.params });
} else {
await this._router.navigate(['/kunde']);
await this._router.navigate(['/kunde', activatedProcessId, 'product']);
}
}

View File

@@ -53,7 +53,7 @@ hr {
}
span.number {
@apply text-right;
@apply text-center;
}
span {

View File

@@ -4,8 +4,8 @@
<div class="product-details">
<div class="product-image">
<button class="image-button" (click)="showImages()">
<img [src]="item.imageId | productImage: 195:315:true" alt="product image" />
<ui-icon icon="search_add" size="22px"></ui-icon>
<img (load)="loadImage()" [src]="item.imageId | productImage: 195:315:true" alt="product image" />
<ui-icon *ngIf="imageLoaded$ | async" icon="search_add" size="22px"></ui-icon>
</button>
<button (click)="showReviews()" class="recessions" *ngIf="item.reviews?.length > 0">
@@ -36,7 +36,7 @@
<div class="row">
<div>
<div class="format">
<div class="format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
class="format-icon"
@@ -84,18 +84,33 @@
<div data-name="product-ean">{{ item.product?.ean }}</div>
<div class="right">
<div class="availability-icons">
<div class="fetching medium" *ngIf="fetchingAvailabilities$ | async"></div>
<ng-container *ngIf="!(fetchingAvailabilities$ | async)">
<div class="fetching xsmall" *ngIf="store.fetchingTakeAwayAvailability$ | async; else showAvailabilityTakeAwayIcon"></div>
<ng-template #showAvailabilityTakeAwayIcon>
<ui-icon *ngIf="store.isTakeAwayAvailabilityAvailable$ | async" icon="shopping_bag" size="18px"></ui-icon>
<ui-icon *ngIf="store.isPickUpAvailabilityAvailable$ | async" icon="box_out" size="18px"></ui-icon>
<ui-icon class="truck" *ngIf="showDeliveryTruck$ | async" icon="truck" size="30px"></ui-icon>
<ui-icon class="truck_b2b" *ngIf="showDeliveryB2BTruck$ | async" 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>
</ng-container>
<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>

View File

@@ -108,6 +108,10 @@
animation: load 0.75s linear infinite;
}
.xsmall {
@apply w-6;
}
.small {
@apply w-16;
}
@@ -117,7 +121,7 @@
}
.availability-icons {
@apply flex flex-row justify-end text-dark-cerulean mt-4;
@apply flex flex-row items-center justify-end text-dark-cerulean mt-4;
ui-icon {
@apply mx-1;
@@ -168,7 +172,7 @@
}
.product-text {
@apply flex flex-col whitespace-pre-line mb-px-100;
@apply flex flex-col whitespace-pre-line mb-px-100 break-words;
h3 {
@apply my-4;

View File

@@ -8,7 +8,7 @@ import { UiModalService } from '@ui/modal';
import { ModalReviewsComponent } from '@modal/reviews';
import { PurchasingOptionsModalComponent, PurchasingOptionsModalData } from 'apps/page/checkout/src/lib/modals/purchasing-options-modal';
import { PurchasingOptions } from 'apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.store';
import { combineLatest, Subscription } from 'rxjs';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { filter, first, map, shareReplay } from 'rxjs/operators';
import { ArticleDetailsStore } from './article-details.store';
import { ModalImagesComponent } from 'apps/modal/images/src/public-api';
@@ -32,6 +32,8 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
private readonly subscriptions = new Subscription();
showRecommendations: boolean;
imageLoaded$ = new BehaviorSubject<boolean>(false);
fetchingAvailabilities$ = combineLatest([
this.store.fetchingDeliveryAvailability$,
this.store.fetchingDeliveryB2BAvailability$,
@@ -268,4 +270,8 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
const element = this.elementRef.nativeElement.closest('.main-wrapper');
element?.scrollTo({ top: 0, behavior: 'smooth' });
}
loadImage() {
this.imageLoaded$.next(true);
}
}

View File

@@ -39,7 +39,7 @@
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}
</div>
<div class="item-format">
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
loading="lazy"

View File

@@ -176,7 +176,15 @@
</strong>
<span class="shipping-cost-info">ohne Versandkosten</span>
</div>
<button class="cta-primary" (click)="order()" [disabled]="showOrderButtonSpinner">
<button
class="cta-primary"
(click)="order()"
[disabled]="
showOrderButtonSpinner ||
((primaryCtaLabel$ | async) === 'Bestellen' && !(checkNotificationChannelControl$ | async)) ||
control.invalid
"
>
<ui-spinner [show]="showOrderButtonSpinner">
{{ primaryCtaLabel$ | async }}
</ui-spinner>

View File

@@ -70,7 +70,7 @@ button {
@apply bg-brand text-white font-bold text-lg outline-none border-brand border-solid border-2 rounded-full px-6 py-3;
&:disabled {
@apply bg-inactive-customer border-none;
@apply bg-inactive-customer border-solid border-inactive-customer cursor-not-allowed;
}
}

View File

@@ -37,6 +37,8 @@ export interface CheckoutReviewComponentState {
export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewComponentState> implements OnInit {
private _orderCompleted = new Subject<void>();
checkNotificationChannelControl$ = new BehaviorSubject<boolean>(true);
get shoppingCart() {
return this.get((s) => s.shoppingCart);
}
@@ -192,7 +194,8 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
communicationDetails$ = this.applicationService.activatedProcessId$.pipe(
takeUntil(this._orderCompleted),
switchMap((processId) => this.domainCheckoutService.getBuyerCommunicationDetails({ processId }))
switchMap((processId) => this.domainCheckoutService.getBuyerCommunicationDetails({ processId })),
map((communicationDetails) => communicationDetails ?? { email: undefined, mobile: undefined })
);
notificationChannelLoading$ = new Subject<boolean>();
@@ -317,26 +320,51 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
const communicationDetails = await this.communicationDetails$.pipe(first()).toPromise();
this.control = fb.group({
notificationChannel: new FormGroup({
selected: new FormControl(notificationChannel),
selected: new FormControl((notificationChannel & 3) === 3 || communicationDetails.email ? 1 : notificationChannel),
email: new FormControl(communicationDetails ? communicationDetails.email : '', emailNotificationValidator),
mobile: new FormControl(communicationDetails ? communicationDetails.mobile : '', mobileNotificationValidator),
}),
});
}
async onNotificationChange(notificationChannels: NotificationChannel[]) {
async onNotificationChange(notificationChannels?: NotificationChannel[]) {
this.notificationChannelLoading$.next(true);
try {
const control = this.control?.getRawValue();
const notificationChannel = notificationChannels.reduce((val, current) => val | current, 0) as NotificationChannel;
const notificationChannel = notificationChannels
? (notificationChannels.reduce((val, current) => val | current, 0) as NotificationChannel)
: control?.notificationChannel?.selected || 0;
const processId = await this.applicationService.activatedProcessId$.pipe(first()).toPromise();
const email = control?.notificationChannel?.email;
const mobile = control?.notificationChannel?.mobile;
this.domainCheckoutService.setBuyerCommunicationDetails({ processId, email, mobile });
// Check if E-Mail and Mobilnumber is available if E-Mail or SMS checkbox is active
if (notificationChannel === 3 && (!email || !mobile)) {
this.checkNotificationChannelControl$.next(false);
} else if (notificationChannel === 2 && !mobile) {
this.checkNotificationChannelControl$.next(false);
} else if (notificationChannel === 1 && !email) {
this.checkNotificationChannelControl$.next(false);
} else {
this.checkNotificationChannelControl$.next(true);
}
// NotificationChannel nur speichern, wenn Haken und Value gesetzt
let setNotificationChannel = 0;
if ((notificationChannel & 1) === 1 && email) {
setNotificationChannel += 1;
}
if ((notificationChannel & 2) === 2 && mobile) {
setNotificationChannel += 2;
}
if (notificationChannel > 0) {
this.setCommunicationDetails({ processId, notificationChannel, email, mobile });
}
this.domainCheckoutService.setNotificationChannels({
processId,
notificationChannels: (notificationChannel as NotificationChannel) || 0,
notificationChannels: (setNotificationChannel as NotificationChannel) || 0,
});
} catch (error) {
this.uiModal.open({ content: UiErrorModalComponent, data: error, title: 'Fehler beim setzen des Benachrichtigungskanals' });
@@ -345,6 +373,29 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
this.notificationChannelLoading$.next(false);
}
setCommunicationDetails({
processId,
notificationChannel,
email,
mobile,
}: {
processId: number;
notificationChannel: number;
email: string;
mobile: string;
}) {
const emailValid = this.control?.get('notificationChannel')?.get('email')?.valid;
const mobileValid = this.control?.get('notificationChannel')?.get('mobile')?.valid;
if (notificationChannel === 3 && emailValid && mobileValid) {
this.domainCheckoutService.setBuyerCommunicationDetails({ processId, email, mobile });
} else if (notificationChannel === 1 && emailValid) {
this.domainCheckoutService.setBuyerCommunicationDetails({ processId, email });
} else if (notificationChannel === 2 && mobileValid) {
this.domainCheckoutService.setBuyerCommunicationDetails({ processId, mobile });
}
}
openDummyModal(data?: any) {
this.uiModal.open({
content: CheckoutDummyComponent,
@@ -761,6 +812,8 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
} else {
try {
this.showOrderButtonSpinner = true;
// Ticket #3287 Um nur E-Mail und SMS Benachrichtigungen zu setzen und um alle anderen Benachrichtigungskanäle wie z.B. Brief zu deaktivieren
await this.onNotificationChange();
const orders = await this.domainCheckoutService.completeCheckout({ processId }).toPromise();
const orderIds = orders.map((order) => order.id).join(',');
this._orderCompleted.next();

View File

@@ -28,7 +28,7 @@
}}</a>
</div>
<div class="item-format">
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
src="assets/images/Icon_{{ item?.product?.format }}.svg"

View File

@@ -70,10 +70,12 @@
<div class="product-details">
<div class="info-row">
<img class="order-icon" [src]="'/assets/images/Icon_' + order?.product?.format + '.svg'" alt="book-icon" />
<span class="format">{{ order.product?.format }}</span>
<ng-container *ngIf="order?.product?.format && order?.product?.formatDetail">
<img class="order-icon" [src]="'/assets/images/Icon_' + order?.product?.format + '.svg'" alt="book-icon" />
<span class="format">{{ order.product?.format }}</span>
</ng-container>
<ng-container *ngIf="order?.product?.contributors">
<span class="separator">|</span>
<span class="separator" *ngIf="order?.product?.format && order?.product?.formatDetail">|</span>
<span class="contributors">{{ order?.product?.contributors }}</span>
</ng-container>
</div>

View File

@@ -140,6 +140,7 @@ export class CheckoutSummaryComponent implements OnDestroy {
.pipe(first())
.toPromise();
// Wenn es keine Bestellabschluss Prozesse mehr gibt, werden alle Orders aus dem Store removed um die Performance zu verbessern
if (!checkoutProcess) {
this.domainCheckoutService.removeAllOrders();
}

View File

@@ -23,7 +23,7 @@
</div>
</ng-container>
<div class="item-format">
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
src="assets/images/Icon_{{ item?.product?.format }}.svg"

View File

@@ -94,17 +94,16 @@ export class PurchasingOptionsListItemComponent {
})
);
price$ = this.fetchingAvailabilities$.pipe(
filter((fetching) => !fetching),
price$ = combineLatest([this.fetchingAvailabilities$, this._store.selectedFilterOption$]).pipe(
filter(([fetching]) => !fetching),
withLatestFrom(
this._store.selectedFilterOption$,
this.takeAwayAvailabilities$,
this.pickUpAvailabilities$,
this.deliveryAvailabilities$,
this.deliveryDigAvailabilities$,
this.deliveryB2bAvailabilities$
),
map(([_, option, takeAway, pickUp, delivery, deliveryDig, deliveryB2b]) => {
map(([[_, option], takeAway, pickUp, delivery, deliveryDig, deliveryB2b]) => {
let availability;
switch (option) {
case 'take-away':
@@ -114,7 +113,12 @@ export class PurchasingOptionsListItemComponent {
availability = pickUp;
break;
case 'delivery':
availability = deliveryDig || delivery || deliveryB2b;
if (deliveryDig || delivery) {
availability = deliveryDig || delivery;
} else {
availability = deliveryB2b;
option = 'b2b-delivery';
}
break;
default:
return this.item.availability?.price;

View File

@@ -169,6 +169,7 @@ export class PurchasingOptionsListModalComponent implements OnInit {
const deliveryDigAvailabilities = await this._store.deliveryDigAvailabilities$.pipe(first()).toPromise();
const selectedTakeAwayBranch = await this._store.selectedTakeAwayBranch$.pipe(first()).toPromise();
const selectedPickUpBranch = await this._store.selectedPickUpBranch$.pipe(first()).toPromise();
let option = this._store.selectedFilterOption;
for (const item of items) {
let availability;
@@ -190,11 +191,12 @@ export class PurchasingOptionsListModalComponent implements OnInit {
availability = deliveryDigAvailabilities[item.product.catalogProductNumber];
} else if (deliveryB2bAvailabilities[item.product.catalogProductNumber]) {
availability = deliveryB2bAvailabilities[item.product.catalogProductNumber];
option = 'b2b-delivery';
}
break;
}
const price = this._availability.getPriceForAvailability(this._store.selectedFilterOption, item.availability, availability);
const price = this._availability.getPriceForAvailability(option, item.availability, availability);
// Negative Preise und nicht vorhandene Availability ignorieren
if (price?.value?.value < 0 || !availability) {

View File

@@ -39,7 +39,15 @@
<h6 class="title">{{ item?.product?.contributors }} - {{ item?.product?.name }}</h6>
<strong class="can-add-error" *ngIf="canAddError$ | async; let canAddError">{{ canAddError }}</strong>
<div class="grow"></div>
<div class="format">{{ item?.product?.formatDetail }}</div>
<div class="format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
</div>
<div class="price">
{{ price$ | async | currency: item?.catalogAvailability?.price?.value?.currency || 'EUR':'code' }}
</div>

View File

@@ -70,6 +70,14 @@ img.thumbnail {
}
}
.format {
@apply flex flex-row items-center whitespace-nowrap;
img {
@apply mr-2;
}
}
.quantity {
@apply self-end flex flex-col justify-end;

View File

@@ -3,8 +3,9 @@ import { ActivatedRoute } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { SearchComponentStoreService } from '@store/search-component-store';
import { UiFilter } from '@ui/filter';
import { NEVER, Observable, Subject } from 'rxjs';
import { filter, map, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
import { isEqual } from 'lodash';
import { combineLatest, NEVER, Observable, Subject } from 'rxjs';
import { debounceTime, filter, map, switchMap, takeUntil } from 'rxjs/operators';
@Component({
selector: 'page-customer-search-main',
@@ -34,9 +35,16 @@ export class CustomerSearchMainComponent implements OnInit, OnDestroy {
ngOnInit() {
this.processId$ = this._activatedRoute.parent.parent.data.pipe(map((d) => +d.processId));
this._activatedRoute.url
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.processId$))
.subscribe(([_, processId]) => this.removeBreadcrumbs(processId));
combineLatest([this.processId$, this._activatedRoute.queryParams])
.pipe(takeUntil(this._onDestroy$), debounceTime(50))
.subscribe(([processId, queryParams]) => {
const currentQueryParams = this._store.filter?.getQueryParams();
if (!isEqual(currentQueryParams, queryParams)) {
this._store.resetFilter(queryParams);
}
this.removeBreadcrumbs(processId);
this.addOrUpdateBreadcrumb(processId, queryParams);
});
this.filter$
.pipe(
@@ -58,6 +66,17 @@ export class CustomerSearchMainComponent implements OnInit, OnDestroy {
this._breadcrumb.removeBreadcrumbsByKeyAndTags(processId, ['customer', 'details']);
}
addOrUpdateBreadcrumb(processId: number, queryParams: Record<string, string>) {
this._breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: processId,
name: 'Kundensuche',
path: `/kunde/${processId}/customer/search`,
params: queryParams,
section: 'customer',
tags: ['customer', 'filter', 'main'],
});
}
updateCustomerCreateQueryParams(value: string) {
if (this.isValidEmail(value)) {
this.customerCreateQueryParams = { email: value };

View File

@@ -1,4 +1,5 @@
<ng-container *ngFor="let feed of feeds$ | async">
<page-kpi-card *ngIf="feed?.type === 'kpi'" [feed]="feed"></page-kpi-card>
<page-products-card class="tablet:col-span-2" *ngIf="feed?.type === 'products'" [feed]="feed"></page-products-card>
<page-info-card class="tablet:col-span-2" *ngIf="feed?.type === 'info'" [feed]="feed"></page-info-card>
</ng-container>

View File

@@ -5,11 +5,12 @@ import { UiProgressModule } from '@ui/progress';
import { UiSliderModule } from '@ui/slider';
import { DashboardRoutingModule } from './dashboard-routing-module';
import { DashboardComponent } from './dashboard.component';
import { InfoCardComponent } from './info-card/info-card.component';
import { KpiCardComponent } from './kpi-card/kpi-card.component';
import { ProductsCardComponent } from './products-card/product-card.component';
@NgModule({
declarations: [DashboardComponent, KpiCardComponent, ProductsCardComponent],
declarations: [DashboardComponent, KpiCardComponent, ProductsCardComponent, InfoCardComponent],
imports: [CommonModule, DashboardRoutingModule, UiProgressModule, UiSliderModule, ProductImageModule],
exports: [DashboardComponent],
})

View File

@@ -0,0 +1,12 @@
<div class="info">
<div class="title">{{ feed?.label }}</div>
<div class="text-2xl font-bold mt-2">{{ feed?.headline }}</div>
<div class="desc" [innerHtml]="feed?.desc"></div>
</div>
<div class="info-item" *ngFor="let item of feed?.items">
<div class="text-2xl font-bold">{{ item?.heading }}</div>
<div class="desc" [innerHtml]="item?.text"></div>
</div>
<img *ngIf="feed?.emphasize === 10" src="assets/images/recommendation_tag.png" class="emphasize-tag" />

View File

@@ -0,0 +1,25 @@
:host {
@apply grid grid-flow-row gap-2 bg-white rounded p-4 pb-10 shadow relative;
}
.info {
.title {
@apply font-bold uppercase;
color: var(--page-dashboard-card-title-color);
}
}
::ng-deep page-info-card {
.info-item {
.desc p {
@apply mt-4;
}
}
.desc p {
@apply mt-4;
}
}
.emphasize-tag {
@apply absolute top-0 right-0 mr-8 -mt-1 w-12 z-popover;
}

View File

@@ -0,0 +1,15 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { InfoFeed } from '@domain/isa';
@Component({
selector: 'page-info-card',
templateUrl: 'info-card.component.html',
styleUrls: ['info-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InfoCardComponent {
@Input()
feed: InfoFeed;
constructor() {}
}

View File

@@ -1,5 +1,5 @@
:host {
@apply grid grid-flow-col gap-4 bg-white rounded p-4 shadow;
@apply grid grid-flow-col gap-4 bg-white rounded p-4 shadow relative;
grid-template-columns: 1fr auto;
}

View File

@@ -12,3 +12,5 @@
[alt]="item?.product?.name"
/>
</ui-slider>
<img *ngIf="feed?.emphasize === 10" src="assets/images/recommendation_tag.png" class="emphasize-tag" />

View File

@@ -1,5 +1,5 @@
:host {
@apply grid grid-flow-row gap-4 bg-white rounded p-4 shadow;
@apply grid grid-flow-row gap-4 bg-white rounded p-4 shadow relative;
@screen tablet {
@apply grid-flow-col grid-cols-2;
@@ -33,3 +33,7 @@ ui-slider {
::ng-deep page-products-card .desc ul li {
@apply list-disc ml-8;
}
.emphasize-tag {
@apply absolute top-0 right-0 mr-8 -mt-1 w-12 z-popover;
}

View File

@@ -64,7 +64,8 @@ export class GoodsInCleanupListComponent implements OnInit, OnDestroy {
byProcessingStatusFn = (item: OrderItemListItemDTO) => item.processingStatus;
byCompartmentCodeFn = (item: OrderItemListItemDTO) => item.compartmentCode;
byCompartmentCodeFn = (item: OrderItemListItemDTO) =>
!!item.compartmentInfo ? `${item.compartmentCode}_${item.compartmentInfo}` : item.compartmentCode;
changeActionLoader$ = new BehaviorSubject<boolean>(false);

View File

@@ -47,7 +47,8 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
byProcessingStatusFn = (item: OrderItemListItemDTO) => item.processingStatus;
byCompartmentCodeFn = (item: OrderItemListItemDTO) => item.compartmentCode;
byCompartmentCodeFn = (item: OrderItemListItemDTO) =>
!!item.compartmentInfo ? `${item.compartmentCode}_${item.compartmentInfo}` : item.compartmentCode;
constructor(
private _breadcrumb: BreadcrumbService,
@@ -154,6 +155,7 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
if (!response?.dialog) {
this._toast.create({
title: 'Abholfachremission',
text: response?.message,
});
}

View File

@@ -68,7 +68,8 @@ export class GoodsInReservationComponent implements OnInit, OnDestroy {
byProcessingStatusFn = (item: OrderItemListItemDTO) => item.processingStatus;
byCompartmentCodeFn = (item: OrderItemListItemDTO) => item.compartmentCode;
byCompartmentCodeFn = (item: OrderItemListItemDTO) =>
!!item.compartmentInfo ? `${item.compartmentCode}_${item.compartmentInfo}` : item.compartmentCode;
constructor(
private _breadcrumb: BreadcrumbService,

View File

@@ -3,10 +3,10 @@ import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/
import { ActivatedRoute } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { Config } from '@core/config';
import { UiFilterAutocompleteProvider } from '@ui/filter';
import { UiFilter, UiFilterAutocompleteProvider } from '@ui/filter';
import { isEqual } from 'lodash';
import { Subject } from 'rxjs';
import { filter, first, map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { combineLatest, Subject } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import { GoodsInSearchStore } from './goods-in-search.store';
import { GoodsInSearchMainAutocompleteProvider } from './providers/goods-in-search-main-autocomplete.provider';
@@ -35,14 +35,8 @@ export class GoodsInSearchComponent implements OnInit, OnDestroy {
private _onDestroy$ = new Subject();
showFilterOverlay = false;
initialFilter$ = this._goodsInSearchStore.filter$.pipe(
filter((filter) => !!filter),
first()
);
hasFilter$ = this._goodsInSearchStore.filter$.pipe(
withLatestFrom(this.initialFilter$),
map(([filter, initialFilter]) => !isEqual(filter?.getQueryParams(), initialFilter?.getQueryParams()))
hasFilter$ = combineLatest([this._goodsInSearchStore.filter$, this._goodsInSearchStore.defaultSettings$]).pipe(
map(([filter, defaultFilter]) => !isEqual(filter?.getQueryParams(), UiFilter.create(defaultFilter).getQueryParams()))
);
constructor(

View File

@@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { Config } from '@core/config';
import { Subscription } from 'rxjs';
import { first } from 'rxjs/operators';
import { first, debounceTime } from 'rxjs/operators';
import { GoodsInSearchStore } from '../goods-in-search.store';
@Component({
@@ -37,9 +37,14 @@ export class GoodsInSearchMainComponent implements OnInit, OnDestroy {
})
);
this._goodsInSearchStore.setQueryParams(this._activatedRoute.snapshot.queryParams);
this._subscriptions.add(
this._activatedRoute.queryParams.pipe(debounceTime(50)).subscribe((queryParams) => {
this._goodsInSearchStore.setQueryParams(queryParams);
this.removeBreadcrumbs();
this.removeBreadcrumbs();
this.updateBreadcrumb(queryParams);
})
);
}
ngOnDestroy() {
@@ -76,7 +81,6 @@ export class GoodsInSearchMainComponent implements OnInit, OnDestroy {
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), ['goods-in', 'preview'])
.pipe(first())
.toPromise();
resultCrumbs.forEach((crumb) => {
this._breadcrumb.removeBreadcrumb(crumb.id, true);
});
@@ -138,19 +142,20 @@ export class GoodsInSearchMainComponent implements OnInit, OnDestroy {
this._goodsInSearchStore.search();
}
async updateBreadcrumb() {
async updateBreadcrumb(queryParams: Record<string, string>) {
await this._breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: this._config.get('process.ids.goodsIn'),
name: 'Abholfach',
path: '/filiale/goods/in',
tags: ['goods-in', 'main', 'filter'],
section: 'branch',
params: this._goodsInSearchStore.filter?.getQueryParams(),
params: queryParams,
});
}
async updateQueryParams() {
await this._router.navigate([], { queryParams: this._goodsInSearchStore.filter?.getQueryParams() });
this.updateBreadcrumb();
const queryParams = this._goodsInSearchStore.filter?.getQueryParams();
await this._router.navigate([], { queryParams });
this.updateBreadcrumb(queryParams);
}
}

View File

@@ -34,7 +34,8 @@ export class GoodsInSearchResultsComponent implements OnInit, OnDestroy {
byProcessingStatusFn = (item: OrderItemListItemDTO) => item.processingStatus;
byCompartmentCodeFn = (item: OrderItemListItemDTO) => item.compartmentCode;
byCompartmentCodeFn = (item: OrderItemListItemDTO) =>
!!item.compartmentInfo ? `${item.compartmentCode}_${item.compartmentInfo}` : item.compartmentCode;
private _onDestroy$ = new Subject();

View File

@@ -63,18 +63,17 @@ export class GoodsOutSearchFilterComponent implements OnInit, OnDestroy {
}
async applyFilter() {
this._goodsOutSearchStore.clearResults();
this._goodsOutSearchStore.setFilter(this.filter);
this.message = undefined;
await this.updateQueryParams();
this._goodsOutSearchStore.searchResult$.pipe(takeUntil(this._onDestroy$), take(1)).subscribe((result) => {
if (result.error) {
if (result.results.error) {
} else {
if (result.hits > 0) {
if (result.hits === 1) {
const orderItem = result.result[0];
if (result.results.hits > 0) {
if (result.results.hits === 1) {
const orderItem = result.results.result[0];
this._router.navigate([this.getDetailsPath(orderItem)]);
} else {
this._router.navigate(['/kunde', this.processId, 'goods', 'out', 'results'], {
@@ -91,7 +90,7 @@ export class GoodsOutSearchFilterComponent implements OnInit, OnDestroy {
}
});
this._goodsOutSearchStore.search({});
this._goodsOutSearchStore.search({ clear: true });
}
async updateBreadcrumb() {

View File

@@ -2,11 +2,10 @@ import { animate, style, transition, trigger } from '@angular/animations';
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { Config } from '@core/config';
import { UiFilterAutocompleteProvider } from '@ui/filter';
import { UiFilter, UiFilterAutocompleteProvider } from '@ui/filter';
import { isEqual } from 'lodash';
import { Subject } from 'rxjs';
import { filter, first, map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { combineLatest, Subject } from 'rxjs';
import { map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { GoodsOutSearchStore } from './goods-out-search.store';
import { GoodsOutSearchMainAutocompleteProvider } from './providers/goods-out-search-main-autocomplete.provider';
@@ -34,14 +33,8 @@ export class GoodsOutSearchComponent implements OnInit, OnDestroy {
private _onDestroy$ = new Subject();
showFilterOverlay = false;
initialFilter$ = this._goodsOutSearchStore.filter$.pipe(
filter((filter) => !!filter),
first()
);
hasFilter$ = this._goodsOutSearchStore.filter$.pipe(
withLatestFrom(this.initialFilter$),
map(([filter, initialFilter]) => !isEqual(filter?.getQueryParams(), initialFilter?.getQueryParams()))
hasFilter$ = combineLatest([this._goodsOutSearchStore.filter$, this._goodsOutSearchStore.defaultSettings$]).pipe(
map(([filter, defaultFilter]) => !isEqual(filter?.getQueryParams(), UiFilter.create(defaultFilter).getQueryParams()))
);
processId$ = this._activatedRoute.data.pipe(map((data) => +data.processId));
@@ -49,20 +42,18 @@ export class GoodsOutSearchComponent implements OnInit, OnDestroy {
constructor(
private _goodsOutSearchStore: GoodsOutSearchStore,
private _breadcrumb: BreadcrumbService,
private _activatedRoute: ActivatedRoute,
private readonly _config: Config
private _activatedRoute: ActivatedRoute
) {}
ngOnInit() {
this._goodsOutSearchStore.loadSettings();
this.processId$.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._activatedRoute.queryParams)).subscribe(([processId, params]) => {
if (params && Object.keys(params).length === 0) {
this._goodsOutSearchStore.setQueryParams(params);
this._goodsOutSearchStore.loadSettings();
} else {
// this._goodsOutSearchStore.resetFilter(params);
}
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({
key: processId,

View File

@@ -7,7 +7,7 @@ import { ListResponseArgsOfOrderItemListItemDTO, OrderItemListItemDTO, QuerySett
import { UiFilter } from '@ui/filter';
import { isResponseArgs } from '@utils/object';
import { Observable, Subject } from 'rxjs';
import { switchMap, mergeMap, withLatestFrom, filter, take, tap } from 'rxjs/operators';
import { switchMap, filter, tap, first, map } from 'rxjs/operators';
export interface GoodsOutSearchState {
defaultSettings?: QuerySettingsDTO;
@@ -15,9 +15,9 @@ export interface GoodsOutSearchState {
filter?: UiFilter;
message?: string;
fetching: boolean;
silentFetching: boolean;
hits: number;
results: OrderItemListItemDTO[];
searchOptions?: { take?: number; skip?: number };
}
@Injectable()
@@ -52,23 +52,24 @@ export class GoodsOutSearchStore extends ComponentStore<GoodsOutSearchState> {
readonly fetching$ = this.select((s) => s.fetching);
get silentFetching() {
return this.get((s) => s.silentFetching);
}
get queryParams() {
return this.get((s) => s.queryParams);
}
readonly queryParams$ = this.select((s) => s.queryParams);
get searchOptions() {
return this.get((s) => s.searchOptions);
}
set searchOptions(searchOptions: { take?: number; skip?: number }) {
this.patchState({ searchOptions });
}
private _searchResultSubject = new Subject<ListResponseArgsOfOrderItemListItemDTO>();
private _searchResultSubject = new Subject<{ results: ListResponseArgsOfOrderItemListItemDTO; cached: boolean }>();
readonly searchResult$ = this._searchResultSubject.asObservable();
private _searchResultFromCacheSubject = new Subject<{ hits: number; results: OrderItemListItemDTO[] }>();
readonly searchResultFromCache$ = this._searchResultFromCacheSubject.asObservable();
private _searchResultClearedSubject = new Subject<void>();
readonly searchResultCleared = this._searchResultClearedSubject.asObservable();
@@ -76,34 +77,24 @@ export class GoodsOutSearchStore extends ComponentStore<GoodsOutSearchState> {
constructor(private _domainGoodsInService: DomainGoodsService, private _cache: CacheService) {
super({
fetching: false,
silentFetching: false,
hits: 0,
results: [],
});
this.loadDefaultSettings();
}
loadSettings = this.effect(($) =>
$.pipe(
switchMap(() =>
this._domainGoodsInService.goodsOutQuerySettings().pipe(
tapResponse(
(res) => {
this.setDefaultSettings(res.result);
const filter = UiFilter.create(res.result);
if (this.queryParams) {
filter.fromQueryParams(this.queryParams);
}
this.setFilter(filter);
},
(err) => {
console.error('GoodsInSearchStore.loadSettings()', err);
}
)
)
)
)
);
async loadDefaultSettings() {
const defaultSettings = await this._domainGoodsInService
.goodsOutQuerySettings()
.pipe(map((res) => res?.result))
.toPromise();
setDefaultSettings(defaultSettings: QuerySettingsDTO) {
const filter = UiFilter.create(defaultSettings);
if (this.queryParams) {
filter.fromQueryParams(this.queryParams);
}
this.setFilter(filter);
this.patchState({ defaultSettings });
}
@@ -115,26 +106,16 @@ export class GoodsOutSearchStore extends ComponentStore<GoodsOutSearchState> {
}
}
resetFilter(defaultQueryParams?: Record<string, string>) {
resetFilter(queryParams?: Record<string, string>) {
const filter = UiFilter.create(this.defaultSettings);
if (!!defaultQueryParams) {
filter?.fromQueryParams(defaultQueryParams);
if (!!queryParams) {
filter?.fromQueryParams(queryParams);
}
this.patchState({ queryParams });
this.setFilter(filter);
}
clearResults() {
this.patchState({
fetching: false,
hits: 0,
message: undefined,
results: [],
});
this._searchResultClearedSubject.next();
}
setQueryParams(queryParams: Record<string, string>) {
this.patchState({ queryParams });
if (this.filter instanceof UiFilter) {
@@ -143,73 +124,116 @@ export class GoodsOutSearchStore extends ComponentStore<GoodsOutSearchState> {
}
}
searchRequest(options?: { take?: number; skip?: number; reload?: boolean }) {
return this.filter$.pipe(
filter((f) => f instanceof UiFilter),
take(1),
mergeMap((filter) =>
this._domainGoodsInService.searchWarenausgabe({
...filter.getQueryToken(),
skip: options.reload ? 0 : options?.skip ?? this.results.length,
take: options.reload ? this.results.length : options?.take ?? 50,
})
)
);
}
// searchRequest(options: { skip?: number; take?: number }) {
// console.log('GoodsInSearchStore.searchRequest()', options);
// return this.filter$.pipe(
// filter((f) => f instanceof UiFilter),
// take(1),
// mergeMap((filter) => {
// console.log('GoodsInSearchStore.searchRequest()', filter);
// return this._domainGoodsInService.searchWarenausgabe({
// ...filter.getQueryToken(),
// skip: options.skip,
// take: options.take,
// });
// })
// );
// }
search = this.effect((options$: Observable<{ siletReload?: boolean }>) =>
search = this.effect((options$: Observable<{ clear?: boolean; siletReload?: boolean }>) =>
options$.pipe(
tap((options) => {
switchMap((options) => {
return this.results$.pipe(
map((results) => [options, results]),
first()
);
}),
switchMap(([options, results]) => {
return this.filter$.pipe(
filter((f) => f instanceof UiFilter),
map((filter) => [options, results, filter]),
first()
);
}),
tap(([options, _, filter]: [{ clear?: boolean; siletReload?: boolean }, OrderItemListItemDTO[], UiFilter]) => {
if (!options?.siletReload) {
this.patchState({ fetching: true });
} else {
this.patchState({ silentFetching: true });
}
if (options?.clear) {
this._searchResultClearedSubject.next();
this._cache.delete(filter?.getQueryToken());
}
}),
withLatestFrom(this.results$),
switchMap(([_options, _results]) => {
const queryToken = this.filter?.getQueryToken();
switchMap(([options, results, filter]) => {
const queryToken = filter?.getQueryToken() ?? {};
if (queryToken && this._cache.get(queryToken)) {
const cached = this._cache.get(queryToken);
let cachedResultCount: number;
this.patchState(cached);
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 });
}
}
return this.searchRequest({ ...this.searchOptions, reload: _options.siletReload }).pipe(
if (options.clear) {
queryToken.skip = 0;
queryToken.take = 50;
} else if (options.siletReload) {
queryToken.skip = 0;
queryToken.take = cachedResultCount || results.length || 50;
} else {
queryToken.skip = results.length;
queryToken.take = 50;
}
return this._domainGoodsInService.searchWarenausgabe(queryToken).pipe(
tapResponse(
(res) => {
let results: OrderItemListItemDTO[] = [];
if (_options.siletReload) {
results = res.result;
let _results: OrderItemListItemDTO[] = [];
if (options.siletReload) {
_results = res.result;
} else if (options.clear) {
_results = res.result;
} else {
results = [...(_results ?? []), ...(res.result ?? [])];
_results = [...results, ...(res.result ?? [])];
}
this.patchState({
hits: res.hits,
results,
results: _results,
fetching: false,
silentFetching: false,
});
this._cache.set(filter?.getQueryToken(), {
hits: res.hits,
results: _results,
fetching: false,
});
if (queryToken) {
this._cache.set(queryToken, {
hits: res.hits,
results,
fetching: false,
});
}
this._searchResultSubject.next(res);
this._searchResultSubject.next({ results: res, cached });
},
(err: Error) => {
if (err instanceof HttpErrorResponse && isResponseArgs(err.error)) {
this._searchResultSubject.next(err.error);
this._searchResultSubject.next({ results: err.error, cached });
} else {
this._searchResultSubject.next({
error: true,
message: err.message,
results: {
error: true,
message: err.message,
},
cached,
});
}
this.patchState({ fetching: false });
this.patchState({ fetching: false, silentFetching: false });
console.error('GoodsInSearchStore.search()', err);
}
)
@@ -217,38 +241,4 @@ export class GoodsOutSearchStore extends ComponentStore<GoodsOutSearchState> {
})
)
);
reload = this.effect(($) =>
$.pipe(
tap((_) => this.patchState({ fetching: true })),
withLatestFrom(this.results$),
switchMap(([_, results]) =>
this.searchRequest({ take: results?.length ?? 0, skip: 0 }).pipe(
tapResponse(
(res) => {
this.patchState({
hits: res.hits,
results: res.result,
fetching: false,
});
this._searchResultSubject.next(res);
},
(err: Error) => {
if (err instanceof HttpErrorResponse && isResponseArgs(err.error)) {
this._searchResultSubject.next(err.error);
} else {
this._searchResultSubject.next({
error: true,
message: err.message,
});
}
this.patchState({ fetching: false });
console.error('GoodsInSearchStore.reload()', err);
}
)
)
)
)
);
}

View File

@@ -2,9 +2,9 @@ import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ChangeDetectorRe
import { ActivatedRoute, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { OrderItemListItemDTO } from '@swagger/oms';
import { debounce } from 'lodash';
import { combineLatest, Subscription } from 'rxjs';
import { debounceTime, first, map } from 'rxjs/operators';
import { debounce, isEqual } from 'lodash';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, first, map, withLatestFrom } from 'rxjs/operators';
import { GoodsOutSearchStore } from '../goods-out-search.store';
@Component({
@@ -18,6 +18,8 @@ export class GoodsOutSearchMainComponent implements OnInit, OnDestroy {
loading$ = this._goodsOutSearchStore.fetching$;
queryChanged$ = new BehaviorSubject<boolean>(false);
message: string;
lastProcessId: number | undefined;
@@ -39,18 +41,25 @@ export class GoodsOutSearchMainComponent implements OnInit, OnDestroy {
) {}
ngOnInit() {
// Clear scroll position
localStorage.removeItem(`SCROLL_POSITION_${this.processId}`);
this._subscriptions.add(
this._goodsOutSearchStore.filter$.subscribe(() => {
this._goodsOutSearchStore.filter$.subscribe((f) => {
this._cdr.markForCheck();
})
);
this._subscriptions.add(
combineLatest([this.processId$, this._activatedRoute.queryParams])
.pipe(debounceTime(50))
.subscribe(([processId, queryParams]) => {
this._goodsOutSearchStore.setQueryParams(queryParams);
.pipe(debounceTime(50), withLatestFrom(this.queryChanged$))
.subscribe(([[processId, queryParams], queryChanged]) => {
if (!isEqual(queryParams, this._goodsOutSearchStore.filter?.getQueryParams()) && !queryChanged) {
this._goodsOutSearchStore.resetFilter(queryParams);
}
this.queryChanged$.next(false);
this.removeBreadcrumbs(processId);
this.updateBreadcrumb(processId, queryParams);
})
);
}
@@ -79,16 +88,15 @@ export class GoodsOutSearchMainComponent implements OnInit, OnDestroy {
}
async search() {
this._goodsOutSearchStore.clearResults();
await this.updateQueryParams(this.processId);
this.message = undefined;
this._goodsOutSearchStore.searchResult$.pipe(first()).subscribe((result) => {
if (result.error) {
if (result.results.error) {
} else {
if (result.hits > 0) {
if (result.hits === 1) {
const orderItem = result.result[0];
if (result.results.hits > 0) {
if (result.results.hits === 1) {
const orderItem = result.results.result[0];
this._router.navigate([this.getDetailsPath(orderItem, this.processId)]);
} else {
this._router.navigate(['/kunde', this.processId, 'goods', 'out', 'results'], {
@@ -103,8 +111,7 @@ export class GoodsOutSearchMainComponent implements OnInit, OnDestroy {
this._cdr.markForCheck();
});
this._goodsOutSearchStore.searchOptions = { take: 50, skip: 0 };
this._goodsOutSearchStore.search({});
this._goodsOutSearchStore.search({ clear: true });
}
async updateBreadcrumb(processId: number, params: Record<string, string>) {
@@ -131,5 +138,8 @@ export class GoodsOutSearchMainComponent implements OnInit, OnDestroy {
: `/kunde/${processId}/goods/out/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}`;
}
queryChangeDebounce = debounce(() => this.updateQueryParams(this.processId), 500);
queryChangeDebounce = debounce(async () => {
this.queryChanged$.next(true);
await this.updateQueryParams(this.processId);
}, 500);
}

View File

@@ -5,6 +5,7 @@
(reachEnd)="loadMore()"
[deltaEnd]="150"
[itemLength]="itemLength$ | async"
[initialScroll]="scrollTo"
>
<ng-container *ngIf="processId$ | async; let processId">
<shared-goods-in-out-order-group *ngFor="let bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn">

View File

@@ -3,13 +3,14 @@ import { debounceTime, first, map, shareReplay, takeUntil, withLatestFrom } from
import { KeyValueDTOOfStringAndString, OrderItemListItemDTO } from '@swagger/oms';
import { ActivatedRoute, Router } from '@angular/router';
import { GoodsOutSearchStore } from '../goods-out-search.store';
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
import { BehaviorSubject, combineLatest, Subject, Subscription } from 'rxjs';
import { BreadcrumbService } from '@core/breadcrumb';
import { ComponentStore } from '@ngrx/component-store';
import { CommandService } from '@core/command';
import { OrderItemsContext } from '@domain/oms';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { UiScrollContainerComponent } from '@ui/scroll-container';
import { UiFilter } from '@ui/filter';
export interface GoodsOutSearchResultsState {
selectedOrderItemSubsetIds: number[];
@@ -64,7 +65,8 @@ export class GoodsOutSearchResultsComponent extends ComponentStore<GoodsOutSearc
byProcessingStatusFn = (item: OrderItemListItemDTO) => item.processingStatus;
byCompartmentCodeFn = (item: OrderItemListItemDTO) => item.compartmentCode;
byCompartmentCodeFn = (item: OrderItemListItemDTO) =>
!!item.compartmentInfo ? `${item.compartmentCode}_${item.compartmentInfo}` : item.compartmentCode;
processId$ = this._activatedRoute.parent.data.pipe(map((data) => +data.processId));
@@ -74,6 +76,10 @@ export class GoodsOutSearchResultsComponent extends ComponentStore<GoodsOutSearc
trackByFn = (item: OrderItemListItemDTO) => `${item.orderId}${item.orderItemId}${item.orderItemSubsetId}`;
private _searchResultSubscription: Subscription;
scrollTo: number;
constructor(
private _goodsOutSearchStore: GoodsOutSearchStore,
private _router: Router,
@@ -87,23 +93,47 @@ export class GoodsOutSearchResultsComponent extends ComponentStore<GoodsOutSearc
});
}
saveScrollPosition(processId: number, scrollPosition: number) {
localStorage.setItem(`SCROLL_POSITION_${processId}`, JSON.stringify(scrollPosition));
}
getScrollPosition(processId: number): number | undefined {
try {
const scroll_position = localStorage.getItem(`SCROLL_POSITION_${processId}`);
return scroll_position ? JSON.parse(scroll_position) : undefined;
} catch {
return undefined;
}
}
removeScrollPosition(processId: number) {
localStorage.removeItem(`SCROLL_POSITION_${processId}`);
}
ngOnInit() {
this.processId$
.pipe(takeUntil(this._onDestroy$), debounceTime(1), withLatestFrom(this._activatedRoute.queryParams))
.subscribe(([processId, params]) => {
this._goodsOutSearchStore.setQueryParams(params);
this.updateBreadcrumb(processId, params);
this.initInitialSearch(processId, params);
this.createBreadcrumb(processId, params);
this.removeBreadcrumbs(processId);
if (this.previousProcessId && processId !== this.previousProcessId) {
this._goodsOutSearchStore.clearResults();
this._goodsOutSearchStore.search({ siletReload: true });
.pipe(takeUntil(this._onDestroy$), debounceTime(10), withLatestFrom(this._activatedRoute.queryParams))
.subscribe(async ([processId, params]) => {
if (this.previousProcessId && this.previousProcessId !== processId) {
this.saveScrollPosition(this.previousProcessId, this.scrollContainer?.scrollPos);
}
this.previousProcessId = processId;
if (!(this._goodsOutSearchStore.filter instanceof UiFilter)) {
await this._goodsOutSearchStore.loadDefaultSettings();
}
this._goodsOutSearchStore.resetFilter(params);
this.updateBreadcrumb(processId, params);
if (this.previousProcessId !== processId) {
this.initInitialSearch(processId);
this.createBreadcrumb(processId, params);
this.removeBreadcrumbs(processId);
this.previousProcessId = processId;
this._goodsOutSearchStore.search({ siletReload: true });
}
});
this._goodsOutSearchStore.searchResultCleared.pipe(takeUntil(this._onDestroy$)).subscribe((_) => this.clearSelectedItems());
@@ -113,7 +143,12 @@ export class GoodsOutSearchResultsComponent extends ComponentStore<GoodsOutSearc
this._onDestroy$.next();
this._onDestroy$.complete();
// this.updateBreadcrumb(this._goodsOutSearchStore.filter?.getQueryParams());
if (this._searchResultSubscription) {
this._searchResultSubscription.unsubscribe();
}
this.updateBreadcrumb(this.previousProcessId, this._goodsOutSearchStore.filter?.getQueryParams());
this.saveScrollPosition(this.previousProcessId, this.scrollContainer?.scrollPos);
}
async removeBreadcrumbs(processId: number) {
@@ -142,9 +177,6 @@ export class GoodsOutSearchResultsComponent extends ComponentStore<GoodsOutSearc
}
async updateBreadcrumb(processId: number, queryParams: Record<string, string>) {
const scroll_position = this.scrollContainer?.scrollPos;
const take = this._goodsOutSearchStore.results?.length;
if (queryParams) {
const crumbs = await this._breadcrumb
.getBreadcrumbsByKeyAndTags$(processId, ['goods-out', 'results', 'filter'])
@@ -152,7 +184,7 @@ export class GoodsOutSearchResultsComponent extends ComponentStore<GoodsOutSearc
.toPromise();
const name = queryParams.main_qs ? queryParams.main_qs : 'Alle Artikel';
const params = { ...queryParams, scroll_position, take };
const params = { ...queryParams };
for (const crumb of crumbs) {
this._breadcrumb.patchBreadcrumb(crumb.id, {
@@ -169,36 +201,53 @@ export class GoodsOutSearchResultsComponent extends ComponentStore<GoodsOutSearc
return input?.replace('ORD:', '') ?? 'Alle';
}
initInitialSearch(processId: number, params: Record<string, string>) {
if (this._goodsOutSearchStore.hits === 0) {
this._goodsOutSearchStore.searchResult$.pipe(takeUntil(this._onDestroy$)).subscribe(async (result) => {
if (result.hits === 0) {
await this._router.navigate([`/kunde/${processId}/goods/out`], {
queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
});
} else {
await this.createBreadcrumb(processId, params);
if (result.hits === 1) {
await this.navigateToDetails(processId, result.result[0]);
} else {
if (!!this._goodsOutSearchStore.searchOptions?.take) {
this._goodsOutSearchStore.searchOptions = undefined;
this.scrollContainer.scrollTo(Number(scroll_position ?? 0));
}
}
}
});
this._goodsOutSearchStore.search({});
initInitialSearch(processId: number) {
if (this._searchResultSubscription) {
this._searchResultSubscription.unsubscribe();
}
const { scroll_position, take } = this._goodsOutSearchStore.queryParams;
if (!!take && !!scroll_position) {
this._goodsOutSearchStore.searchOptions = { take: Number(take) };
}
this._searchResultSubscription = new Subscription();
this._searchResultSubscription.add(
this._goodsOutSearchStore.searchResult$.subscribe(async (result) => {
const queryParams = this._goodsOutSearchStore.filter?.getQueryParams();
if (result.results.hits === 0) {
await this._router.navigate([`/kunde/${processId}/goods/out`], {
queryParams,
});
} else {
await this.createBreadcrumb(processId, queryParams);
if (result.results.hits === 1) {
await this.navigateToDetails(processId, result.results.result[0]);
} else if (!result.cached) {
const scrollPos = this.getScrollPosition(processId) || 0;
this.scrollTo = scrollPos;
this.scrollContainer?.scrollTo(scrollPos);
this.removeScrollPosition(processId);
}
}
})
);
this._searchResultSubscription.add(
this._goodsOutSearchStore.searchResultFromCache$.pipe(takeUntil(this._onDestroy$)).subscribe(async (result) => {
if (result?.hits > 0) {
const scrollPos = this.getScrollPosition(processId) || 0;
this.scrollTo = scrollPos;
this.scrollContainer?.scrollTo(scrollPos);
this.removeScrollPosition(processId);
}
})
);
}
async loadMore() {
if (this._goodsOutSearchStore.hits > this._goodsOutSearchStore.results.length && !this._goodsOutSearchStore.fetching) {
if (
this._goodsOutSearchStore.hits > this._goodsOutSearchStore.results.length &&
!this._goodsOutSearchStore.fetching &&
!this._goodsOutSearchStore.silentFetching
) {
this.saveScrollPosition(this.previousProcessId, this.scrollContainer?.scrollPos);
this._goodsOutSearchStore.search({});
}
}
@@ -248,8 +297,9 @@ export class GoodsOutSearchResultsComponent extends ComponentStore<GoodsOutSearc
items: this.selectedItems,
};
try {
this.saveScrollPosition(this.previousProcessId, this.scrollContainer?.scrollPos);
await this._commandService.handleCommand(action.command, commandData);
this._goodsOutSearchStore.reload();
this._goodsOutSearchStore.search({ siletReload: true, clear: true });
this.clearSelectedItems();
this.loadingFetchedActionButton$.next(false);
} catch (error) {

View File

@@ -14,7 +14,7 @@
<div class="grid grid-flow-row gap-1">
<div class="flex flex-row">
<div class="w-32">Format</div>
<div data-name="format" class="flex-grow">
<div data-name="format" class="flex-grow" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img class="inline" src="/assets/images/Icon_{{ item.product.format }}.svg" [alt]="item.product.formatDetail" />
<span class="ml-1 font-bold">{{ item.product.formatDetail }}</span>
</div>

View File

@@ -14,15 +14,11 @@
></ui-filter>
<div class="sticky-cta-wrapper">
<button class="cta-reset-filter" (click)="resetFilter()" [disabled]="store.fetching$ | async">
<span>
Filter zurücksetzen
</span>
<button class="cta-reset-filter" (click)="resetFilter()">
Filter zurücksetzen
</button>
<button class="apply-filter" (click)="applyFilter(filter)" [disabled]="store.fetching$ | async">
<ui-spinner [show]="store.fetching$ | async">
Filter anwenden
</ui-spinner>
<button class="apply-filter" (click)="applyFilter(filter)">
Filter anwenden
</button>
</div>
</ng-container>

View File

@@ -34,13 +34,11 @@
</div>
<div class="grid grid-flow-row gap-1 w-48">
<div class="overflow-hidden overflow-ellipsis whitespace-nowrap" *ngIf="!!item.formatDetail">
<img
*ngIf="!!item.dto.product.format"
class="inline"
src="/assets/images/Icon_{{ item.dto.product.format }}.svg"
[alt]="item.formatDetail"
/>
<div
class="overflow-hidden overflow-ellipsis whitespace-nowrap"
*ngIf="!!item.format && !!item.formatDetail && item.format !== 'UNKNOWN'"
>
<img class="inline" src="/assets/images/Icon_{{ item.dto.product.format }}.svg" [alt]="item.formatDetail" />
<span class="ml-1 font-bold">{{ item.formatDetail }}</span>
</div>
<div class="font-bold">

View File

@@ -461,10 +461,10 @@ export class RemissionListComponentStore extends ComponentStore<RemissionState>
this.clearCache();
await this._router.navigate([], {
queryParams: {
source: this.selectedSource,
supplier: supplier.id,
scroll_position: 0,
},
queryParamsHandling: 'merge',
});
}
@@ -478,9 +478,9 @@ export class RemissionListComponentStore extends ComponentStore<RemissionState>
await this._router.navigate([], {
queryParams: {
source,
supplier: this.selectedSupplier.id,
scroll_position: 0,
},
queryParamsHandling: 'merge',
});
}
}

View File

@@ -97,7 +97,8 @@
<ng-container>
<a
*ngIf="showStartRemissionAction$ | async"
routerLink="../create"
[class.disabled]="fetching$ | async"
[routerLink]="(fetching$ | async) ? null : '../create'"
[queryParams]="queryParams$ | async"
routerLinkParam
class="bg-brand text-white font-bold text-lg px-6 py-3 rounded-full"

View File

@@ -38,7 +38,7 @@
left: 50%;
transform: translateX(-50%);
button:disabled {
.disabled {
@apply cursor-not-allowed bg-inactive-branch;
}
}

View File

@@ -2,15 +2,16 @@ import { ActivatedRoute, Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { Config } from '@core/config';
import { DomainRemissionService } from '@domain/remission';
import { createComponentFactory, Spectator } from '@ngneat/spectator';
import { RemissionComponent } from './remission.component';
describe('RemissionComponent', () => {
xdescribe('RemissionComponent', () => {
let spectator: Spectator<RemissionComponent>;
const createComponent = createComponentFactory({
component: RemissionComponent,
mocks: [BreadcrumbService, Config, ActivatedRoute, ApplicationService, Router],
mocks: [BreadcrumbService, Config, ActivatedRoute, ApplicationService, Router, DomainRemissionService],
});
beforeEach(() => {

View File

@@ -44,3 +44,8 @@ h1 {
@apply text-brand border-2 border-none bg-white font-bold text-lg px-4 py-2 rounded-full mr-2;
}
}
::ng-deep page-article-list-modal ui-slider .ui-slider-wrapper {
display: grid !important;
grid-auto-flow: column !important;
}

View File

@@ -19,10 +19,9 @@
uiFocus
type="text"
[ngModel]="inputValue$ | async"
(ngModelChange)="inputValueSubject.next($event)"
(ngModelChange)="inputValueSubject.next($event); setCompartmentInfo(inputValue)"
placeholder="..."
[size]="controlSize$ | async"
(blur)="setCompartmentInfo(inputValue)"
maxlength="15"
/>
</button>

View File

@@ -11,7 +11,7 @@
<div class="goods-in-out-order-details-action-wrapper">
<button
[disabled]="changeActionDisabled$ | async"
[disabled]="addToPreviousCompartmentActionDisabled$ | async"
*ngIf="addToPreviousCompartmentAction$ | async; let action"
class="cta-action shadow-action"
[class.cta-action-primary]="action.selected"

View File

@@ -34,7 +34,7 @@
button {
&:disabled {
@apply bg-inactive-customer border-inactive-customer text-white;
@apply bg-inactive-customer border-inactive-customer text-white cursor-not-allowed;
}
}
}

View File

@@ -6,7 +6,7 @@ import { OrderItemsContext } from '@domain/oms';
import { KeyValueDTOOfStringAndString, OrderItemListItemDTO } from '@swagger/oms';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { BehaviorSubject, combineLatest, merge, of, Subscription } from 'rxjs';
import { first, switchMap } from 'rxjs/operators';
import { first, map, switchMap } from 'rxjs/operators';
import { SharedGoodsInOutOrderDetailsCoversComponent } from './goods-in-out-order-details-covers';
import { SharedGoodsInOutOrderDetailsItemComponent } from './goods-in-out-order-details-item';
import { SharedGoodsInOutOrderDetailsTagsComponent } from './goods-in-out-order-details-tags';
@@ -53,6 +53,10 @@ export class SharedGoodsInOutOrderDetailsComponent extends SharedGoodsInOutOrder
changeActionLoader$ = new BehaviorSubject<string>(undefined);
changeActionDisabled$ = new BehaviorSubject<boolean>(false);
addToPreviousCompartmentActionDisabled$ = combineLatest([this.compartmentInfo$, this.changeActionDisabled$]).pipe(
map(([compartmentInfo, changeActionDisabled]) => !!compartmentInfo || changeActionDisabled)
);
@Output()
actionHandled = new EventEmitter<{
orderItemsContext: OrderItemsContext;

View File

@@ -23,7 +23,10 @@
</div>
<div class="item-data-wrapper">
<div class="item-data">
<div class="item-format">
<div
class="item-format"
[class.invisible]="!item?.product?.format || !item?.product?.formatDetail || item?.product?.format === 'UNKNOWN'"
>
<img
class="format-icon"
*ngIf="item?.product?.format && item?.product?.format !== 'UNKNOWN'"

View File

@@ -26,7 +26,11 @@
</ng-container>
</div>
<div class="nc-generate" *ngIf="channelActionName && notificationChannels.length !== 2">
<button [disabled]="channelActionLoading" type="button" (click)="channelActionEvent.emit(notificationChannels)">
<button
[disabled]="channelActionLoading || emailControl?.errors?.required || emailControl?.errors?.pattern"
type="button"
(click)="channelActionEvent.emit(notificationChannels)"
>
{{ channelActionName }}
</button>
</div>
@@ -42,7 +46,17 @@
</ng-container>
</div>
<div class="nc-generate" *ngIf="channelActionName">
<button [disabled]="channelActionLoading" type="button" (click)="channelActionEvent.emit(notificationChannels)">
<button
[disabled]="
channelActionLoading ||
mobileControl?.errors?.required ||
mobileControl?.errors?.pattern ||
emailControl?.errors?.required ||
emailControl?.errors?.pattern
"
type="button"
(click)="channelActionEvent.emit(notificationChannels)"
>
{{ channelActionName }}
</button>
</div>

View File

@@ -84,7 +84,7 @@
.nc-generate {
button:disabled {
@apply text-disabled-customer;
@apply text-disabled-customer cursor-not-allowed;
}
}
}
@@ -104,7 +104,7 @@
.nc-generate {
button:disabled {
@apply text-disabled-branch;
@apply text-disabled-branch cursor-not-allowed;
}
}
}

View File

@@ -40,7 +40,7 @@ export class SharedNotificationChannelControlComponent extends ComponentStore<Sh
}
get displayEmail() {
return !!(this.notificationChannelControl.value & 1) && this.emailControl && !this.communicationDetails?.email;
return !!(this.notificationChannelControl.value & 1) && this.emailControl;
}
get mobileControl() {
@@ -48,7 +48,7 @@ export class SharedNotificationChannelControlComponent extends ComponentStore<Sh
}
get displayMobile() {
return !!(this.notificationChannelControl.value & 2) && this.mobileControl && !this.communicationDetails?.mobile;
return !!(this.notificationChannelControl.value & 2) && this.mobileControl;
}
get displayToggle() {

View File

@@ -2,7 +2,7 @@ import { AbstractControl, ValidatorFn, Validators } from '@angular/forms';
import { UiValidators } from '@ui/validators';
// RFC 5322 Official Standard
const emailRegexPattern = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
const emailRegexPattern = /^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
export const emailNotificationValidator: ValidatorFn = (control: AbstractControl) => {
if (control.parent) {

View File

@@ -14,10 +14,10 @@
}
button.cancel {
@apply px-5 py-3 bg-white text-brand text-cta-l rounded-full border-none outline-none;
@apply bg-white text-brand border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none;
}
button.confirm {
@apply px-5 py-3 bg-brand text-white text-cta-l rounded-full border-none outline-none;
@apply bg-brand text-white border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none;
}
}

View File

@@ -18,7 +18,7 @@ export class OpenDialogInterceptor implements HttpInterceptor {
if (response?.body?.dialog?.area === 'dialog') {
this.openDialog(response.body.dialog);
}
if (response?.body?.dialog?.area === 'toast') {
if (response?.body?.dialog?.area === 'Toaster') {
this.createToast(response.body.dialog);
}
}
@@ -27,7 +27,7 @@ export class OpenDialogInterceptor implements HttpInterceptor {
if (error?.error?.dialog?.area === 'dialog') {
this.openDialog(error.error.dialog);
}
if (error?.error?.dialog?.area === 'toast') {
if (error?.error?.dialog?.area === 'Toaster') {
this.createToast(error.error.dialog);
}
return throwError(error);

View File

@@ -1,4 +1,15 @@
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild,
} from '@angular/core';
@Component({
selector: 'ui-scroll-container',
@@ -6,7 +17,7 @@ import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Ou
styleUrls: ['scroll-container.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UiScrollContainerComponent {
export class UiScrollContainerComponent implements OnInit {
@ViewChild('scrollContainer', { read: ElementRef, static: false })
scrollContainer: ElementRef;
@@ -35,12 +46,20 @@ export class UiScrollContainerComponent {
@Input() useLoadAnimation = true;
@Input() initialScroll: number;
get containerHeightString() {
return `calc(100vh - ${this.containerHeight}rem)`;
}
constructor() {}
ngOnInit(): void {
if (this.initialScroll !== undefined) {
this.scrollTo(this.initialScroll);
}
}
createSkeletons() {
if (this.itemLength && this.itemLength !== 0) {
return Array.from(Array(this.itemLength - 1), (_, i) => i);

9025
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -66,7 +66,6 @@
"lodash": "^4.17.21",
"ng2-pdf-viewer": "^6.4.1",
"ngx-device-detector": "^2.0.6",
"object-hash": "^2.1.1",
"rxjs": "^6.6.7",
"socket.io": "^2.2.0",
"tslib": "^2.0.0",
@@ -81,16 +80,16 @@
"@angular/cli": "^12.2.13",
"@angular/compiler-cli": "~12.2.14",
"@angular/language-service": "~12.2.14",
"@ngneat/spectator": "^9.0.0",
"@ngneat/spectator": "^8.0.0",
"@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3",
"@types/lodash": "^4.14.168",
"@types/node": "^12.11.1",
"@types/node": "^18.6.1",
"@types/object-hash": "^2.1.0",
"@types/uuid": "^8.3.0",
"codelyzer": "^6.0.0",
"husky": "^4.2.3",
"jasmine-core": "~3.6.0",
"jasmine-core": "~3.8.0",
"jasmine-marbles": "^0.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.3.9",