Merged PR 820: #2100 Abholfachbereinigungsliste

#2100 Abholfachbereinigungsliste

Related work items: #2100
This commit is contained in:
Andreas Schickinger
2021-09-09 15:04:26 +00:00
committed by Nino Righi
parent d627a263c3
commit 061b962359
17 changed files with 446 additions and 6 deletions

View File

@@ -13,6 +13,8 @@ export class BackToStockActionHandler extends ChangeOrderItemStatusBaseActionHan
getStatusValues(orderItem: OrderItemListItemDTO, context: OrderItemsContext): StatusValues {
return {
processingStatus: 262144,
shippingDelayComment: context?.shippingDelayComment,
quantity: context.itemQuantity?.get(orderItem.orderItemSubsetId),
};
}
}

View File

@@ -10,4 +10,6 @@ export interface OrderItemsContext {
itemQuantity?: Map<number, number>;
receipts?: ReceiptDTO[];
shippingDelayComment?: string;
}

View File

@@ -62,4 +62,8 @@ export class DomainGoodsService {
goodsInList(queryToken: QueryTokenDTO) {
return this.abholfachService.AbholfachWareneingangsliste(queryToken);
}
goodsInCleanupList() {
return this.abholfachService.AbholfachAbholfachbereinigungsliste();
}
}

View File

@@ -1,5 +1,7 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { GoodsInCleanupListComponent } from './goods-in-cleanup-list/goods-in-cleanup-list.component';
import { GoodsInCleanupListModule } from './goods-in-cleanup-list/goods-in-cleanup-list.module';
import { GoodsInDetailsComponent } from './goods-in-details';
import { GoodsInEditModule } from './goods-in-edit';
import { GoodsInEditComponent } from './goods-in-edit/goods-in-edit.component';
@@ -27,6 +29,7 @@ const routes: Routes = [
{ path: 'details/order/:orderNumber/item/:orderItemId/:processingStatus', component: GoodsInDetailsComponent },
{ path: 'details/order/:orderNumber/item/:orderItemId/:processingStatus/edit', component: GoodsInEditComponent },
{ path: 'list', component: GoodsInListComponent },
{ path: 'cleanup', component: GoodsInCleanupListComponent },
],
},
];
@@ -38,6 +41,7 @@ const routes: Routes = [
GoodsInSearchMainModule,
GoodsInEditModule,
GoodsInListModule,
GoodsInCleanupListModule,
],
exports: [RouterModule],
})

View File

@@ -0,0 +1,50 @@
<div class="header">
<button *ngIf="!(allItemsSelected$ | async)" class="cta-select-all" [disabled]="loading$ | async" (click)="selectAll()">
Alle auswählen
</button>
<button *ngIf="allItemsSelected$ | async" class="cta-unselect-all" [disabled]="loading$ | async" (click)="unselectAll()">
Alle abwählen
</button>
<div class="hits">{{ hits$ | async }} Titel</div>
</div>
<div class="scroll-container">
<shared-goods-in-out-order-group *ngFor="let bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn">
<ng-container *ngFor="let orderNumberGroup of bueryNumberGroup.items | groupBy: byOrderNumberFn; let lastOrderNumber = last">
<ng-container
*ngFor="let processingStatusGroup of orderNumberGroup.items | groupBy: byProcessingStatusFn; let lastProcessingStatus = last"
>
<ng-container
*ngFor="let compartmentCodeGroup of processingStatusGroup.items | groupBy: byCompartmentCodeFn; let lastCompartmentCode = last"
>
<shared-goods-in-out-order-group-item
*ngFor="let item of compartmentCodeGroup.items; let firstItem = first"
[item]="item"
[showCompartmentCode]="firstItem"
(selectedChange)="setSelectedItem(item, $event)"
selectable="true"
showSupplier="true"
[quantityEditable]="item.quantity > 1 && (selectedOrderItemSubsetIds$ | async)?.includes(item.orderItemSubsetId)"
></shared-goods-in-out-order-group-item>
<div class="divider" *ngIf="!lastCompartmentCode"></div>
</ng-container>
<div class="divider" *ngIf="!lastProcessingStatus"></div>
</ng-container>
<div class="divider" *ngIf="!lastOrderNumber"></div>
</ng-container>
</shared-goods-in-out-order-group>
<div *ngIf="loading$ | async; let loading" [uiLoader]="loading"><div class="loading-text">Inhalte werden geladen</div></div>
</div>
<div class="actions" *ngIf="actions$ | async; let actions">
<button
[disabled]="(changeActionLoader$ | async) || (loading$ | async)"
class="cta-action"
*ngFor="let action of actions"
[class.cta-action-primary]="action.selected"
[class.cta-action-secondary]="!action.selected"
(click)="handleAction(action)"
>
<ui-spinner [show]="(changeActionLoader$ | async) || (loading$ | async)">{{ action.label }}</ui-spinner>
</button>
</div>

View File

@@ -0,0 +1,55 @@
:host {
@apply block relative;
}
.header {
@apply text-right;
.hits {
@apply text-active-branch mb-3 font-semibold text-base;
}
.cta-select-all,
.cta-unselect-all {
@apply bg-transparent text-brand text-base font-bold border-none px-1 -mr-1 mb-1;
&:disabled {
@apply text-inactive-branch;
}
}
}
.scroll-container {
@apply h-full overflow-y-scroll relative grid grid-flow-row gap-3;
max-height: calc(100vh - 350px);
}
.loading-text {
@apply mt-2 text-center font-semibold text-base text-inactive-branch;
}
.divider {
@apply h-px-2;
}
.actions {
@apply fixed bottom-28 inline-grid grid-flow-col gap-7;
left: 50%;
transform: translateX(-50%);
.cta-action {
@apply border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap;
&:disabled {
@apply bg-inactive-branch border-inactive-branch text-white;
}
}
.cta-action-primary {
@apply bg-brand text-white;
}
.cta-action-secondary {
@apply bg-white text-brand;
}
}

View File

@@ -0,0 +1,125 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core';
import { BreadcrumbService } from '@core/breadcrumb';
import { CommandService } from '@core/command';
import { OrderItemsContext } from '@domain/oms';
import { GoodsInOutOrderGroupItemComponent } from '@shared/goods-in-out';
import { KeyValueDTOOfStringAndString, OrderItemListItemDTO } from '@swagger/oms';
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { first, map, shareReplay, takeUntil } from 'rxjs/operators';
import { GoodsInCleanupListStore } from './goods-in-cleanup-list.store';
@Component({
selector: 'page-goods-in-cleanup-list',
templateUrl: 'goods-in-cleanup-list.component.html',
styleUrls: ['goods-in-cleanup-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [GoodsInCleanupListStore],
})
export class GoodsInCleanupListComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChildren(GoodsInOutOrderGroupItemComponent) listItems: QueryList<GoodsInOutOrderGroupItemComponent>;
items$ = this._store.results$;
hits$ = this._store.hits$;
loading$ = this._store.fetching$;
selectedOrderItemSubsetIds$ = this._store.selectedOrderItemSubsetIds$;
actions$ = combineLatest([this.items$, this.selectedOrderItemSubsetIds$]).pipe(
map(([items, selectedOrderItemSubsetIds]) =>
items?.find((item) => selectedOrderItemSubsetIds?.find((orderItemSubsetId) => item.orderItemSubsetId === orderItemSubsetId))
),
map((item) => item?.actions)
);
allItemsSelected$: Observable<boolean>;
byBuyerNumberFn = (item: OrderItemListItemDTO) => item.buyerNumber;
byOrderNumberFn = (item: OrderItemListItemDTO) => item.orderNumber;
byProcessingStatusFn = (item: OrderItemListItemDTO) => item.processingStatus;
byCompartmentCodeFn = (item: OrderItemListItemDTO) => item.compartmentCode;
changeActionLoader$ = new BehaviorSubject<boolean>(false);
private _onDestroy$ = new Subject<void>();
constructor(private _breadcrumb: BreadcrumbService, private _store: GoodsInCleanupListStore, private _commandService: CommandService) {}
ngOnInit(): void {
this._store.search();
this.updateBreadcrumb();
}
ngAfterViewInit(): void {
this.viewChildrenChanged();
this.listItems.changes.pipe(takeUntil(this._onDestroy$)).subscribe(() => this.viewChildrenChanged());
}
ngOnDestroy(): void {
this._onDestroy$.next();
this._onDestroy$.complete();
}
viewChildrenChanged() {
this.allItemsSelected$ = combineLatest(this.listItems.map((listItem) => listItem.selected$)).pipe(
map((selected) => selected.every((s) => s)),
shareReplay()
);
}
async updateBreadcrumb() {
await this._breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: 'goods-in',
name: 'Abholfachbereinigungsliste',
path: '/goods/in/cleanup',
section: 'branch',
tags: ['goods-in', 'cleanup'],
});
}
setSelectedItem(item: OrderItemListItemDTO, selected: boolean) {
const included = this._store.selectedOrderItemSubsetIds.includes(item.orderItemSubsetId);
if (!included && selected) {
this._store.patchState({
selectedOrderItemSubsetIds: [...this._store.selectedOrderItemSubsetIds, item.orderItemSubsetId],
});
} else if (included && !selected) {
this._store.patchState({
selectedOrderItemSubsetIds: this._store.selectedOrderItemSubsetIds.filter((id) => id !== item?.orderItemSubsetId),
});
}
}
selectAll() {
this.listItems.forEach((listItem) => listItem.setSelected(true));
}
unselectAll() {
this.listItems.forEach((listItem) => listItem.setSelected(false));
}
async handleAction(action: KeyValueDTOOfStringAndString) {
this.changeActionLoader$.next(true);
const items = await this.items$.pipe(first()).toPromise();
const selectedSubsetIds = await this.selectedOrderItemSubsetIds$.pipe(first()).toPromise();
const itemsToUpdate = items.filter((item) => selectedSubsetIds.includes(item.orderItemSubsetId));
const itemQuantity = new Map(items.map((item) => [item.orderItemSubsetId, item.quantity]));
const data: OrderItemsContext = { items: itemsToUpdate, shippingDelayComment: `Aktion: ${action.label}`, itemQuantity };
try {
await this._commandService.handleCommand(action.command, data);
this._store.search();
this.unselectAll();
} catch (error) {
console.error(error);
}
this.changeActionLoader$.next(false);
}
}

View File

@@ -0,0 +1,14 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { GoodsInOutOrderGroupModule } from '@shared/goods-in-out';
import { UiCommonModule } from '@ui/common';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
import { GoodsInCleanupListComponent } from './goods-in-cleanup-list.component';
@NgModule({
imports: [CommonModule, UiCommonModule, GoodsInOutOrderGroupModule, UiSpinnerModule],
exports: [GoodsInCleanupListComponent],
declarations: [GoodsInCleanupListComponent],
providers: [],
})
export class GoodsInCleanupListModule {}

View File

@@ -0,0 +1,101 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DomainGoodsService } from '@domain/oms';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ListResponseArgsOfOrderItemListItemDTO, OrderItemListItemDTO } from '@swagger/oms';
import { isResponseArgs } from '@utils/object';
import { Subject } from 'rxjs';
import { switchMap, tap, withLatestFrom } from 'rxjs/operators';
export interface GoodsInCleanupListState {
message?: string;
fetching: boolean;
hits: number;
results: OrderItemListItemDTO[];
selectedOrderItemSubsetIds: number[];
}
@Injectable()
export class GoodsInCleanupListStore extends ComponentStore<GoodsInCleanupListState> {
get results() {
return this.get((s) => s.results);
}
readonly results$ = this.select((s) => s.results);
get hits() {
return this.get((s) => s.hits);
}
readonly hits$ = this.select((s) => s.hits);
get fetching() {
return this.get((s) => s.fetching);
}
readonly fetching$ = this.select((s) => s.fetching);
get selectedOrderItemSubsetIds() {
return this.get((s) => s.selectedOrderItemSubsetIds);
}
selectedOrderItemSubsetIds$ = this.select((s) => s.selectedOrderItemSubsetIds);
private _searchResultSubject = new Subject<ListResponseArgsOfOrderItemListItemDTO>();
readonly searchResult$ = this._searchResultSubject.asObservable();
constructor(private _domainGoodsInService: DomainGoodsService) {
super({
fetching: false,
hits: 0,
results: [],
selectedOrderItemSubsetIds: [],
});
}
clearResults() {
this.patchState({
fetching: false,
hits: 0,
message: undefined,
results: [],
});
}
searchRequest(options?: { take?: number; skip?: number }) {
return this._domainGoodsInService.goodsInCleanupList();
}
search = this.effect(($) =>
$.pipe(
tap((_) => this.patchState({ fetching: true })),
withLatestFrom(this.results$),
switchMap(([_, _results]) =>
this.searchRequest().pipe(
tapResponse(
(res) => {
const results = [...(_results ?? []), ...(res.result ?? [])];
this.patchState({
hits: res.hits,
results,
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('GoodsInCleanupListStore.search()', err);
}
)
)
)
)
);
}

View File

@@ -1,6 +1,9 @@
<a class="goods-in-list-navigation" [routerLink]="['/goods/in', 'list']">
Wareneingangsliste
</a>
<a class="goods-in-list-navigation" [routerLink]="['/goods/in', 'cleanup']">
Abholfachbereinigungsliste
</a>
<div class="search-main">
<h1 class="search-main-title">Bestellpostensuche</h1>
<p class="search-main-paragraph">

View File

@@ -3,7 +3,7 @@
}
.goods-in-list-navigation {
@apply text-center text-2xl text-active-branch block bg-white rounded-t-card font-bold no-underline py-5;
@apply text-center text-2xl text-active-branch block bg-white rounded-t-card font-bold no-underline py-5 shadow-card;
}
.search-main {

View File

@@ -49,6 +49,7 @@ export class GoodsInSearchMainComponent implements OnInit, OnDestroy {
const detailsCrumbs = await this._breadcrumb.getBreadcrumbsByKeyAndTags$('goods-in', ['goods-in', 'details']).pipe(first()).toPromise();
const editCrumbs = await this._breadcrumb.getBreadcrumbsByKeyAndTags$('goods-in', ['goods-in', 'edit']).pipe(first()).toPromise();
const listCrumbs = await this._breadcrumb.getBreadcrumbsByKeyAndTags$('goods-in', ['goods-in', 'list']).pipe(first()).toPromise();
const cleanupCrumbs = await this._breadcrumb.getBreadcrumbsByKeyAndTags$('goods-in', ['goods-in', 'cleanup']).pipe(first()).toPromise();
resultCrumbs.forEach((crumb) => {
this._breadcrumb.removeBreadcrumb(crumb.id, true);
@@ -65,6 +66,10 @@ export class GoodsInSearchMainComponent implements OnInit, OnDestroy {
listCrumbs.forEach((crumb) => {
this._breadcrumb.removeBreadcrumb(crumb.id, true);
});
cleanupCrumbs.forEach((crumb) => {
this._breadcrumb.removeBreadcrumb(crumb.id, true);
});
}
async search() {

View File

@@ -42,15 +42,34 @@
</div>
<div class="label-value">
<div class="label">Menge</div>
<div class="value">{{ item.quantity }}x</div>
<ui-quantity-dropdown
*ngIf="quantityEditable$ | async; else showQuantity"
[showTrash]="false"
[range]="item?.quantity"
[(ngModel)]="item.quantity"
[showSpinner]="false"
>
</ui-quantity-dropdown>
<ng-template #showQuantity>
<div class="value">{{ item.quantity }}x</div>
</ng-template>
</div>
<div class="item-price">
{{ item.price | currency: 'EUR':'code' }}
</div>
<div class="label-value">
<div class="label">Zielfiliale</div>
<div class="value">{{ item.targetBranch }}</div>
<div class="label-value" *ngIf="showSupplier; else showBranch">
<div class="label">Lieferant</div>
<div class="value">{{ item.supplier }}</div>
</div>
<ng-template #showBranch>
<div class="label-value">
<div class="label">Zielfiliale</div>
<div class="value">{{ item.targetBranch }}</div>
</div>
</ng-template>
</div>
<div class="item-data-selector">
<ui-select-bullet *ngIf="selectable" [ngModel]="selected" (ngModelChange)="setSelected($event)"></ui-select-bullet>

View File

@@ -93,3 +93,7 @@
ui-select-bullet {
@apply p-5;
}
ui-quantity-dropdown {
height: 22px;
}

View File

@@ -9,6 +9,7 @@ export interface GoodsInOutOrderGroupItemState {
item?: OrderItemListItemDTO;
selected: boolean;
selectable: boolean;
quantityEditable: boolean;
}
@Component({
@@ -41,6 +42,7 @@ export class GoodsInOutOrderGroupItemComponent extends ComponentStore<GoodsInOut
this.patchState({ selected });
}
}
readonly selected$ = this.select((s) => s.selected);
@Output()
selectedChange = new EventEmitter<boolean>();
@@ -55,15 +57,30 @@ export class GoodsInOutOrderGroupItemComponent extends ComponentStore<GoodsInOut
}
}
@Input()
get quantityEditable() {
return this.get((s) => s.quantityEditable);
}
set quantityEditable(quantityEditable: boolean) {
if (this.quantityEditable !== quantityEditable) {
this.patchState({ quantityEditable });
}
}
readonly quantityEditable$ = this.select((s) => s.quantityEditable);
showChangeDate$ = this.item$.pipe(map((item) => [256, 512, 1024, 2048, 4069].includes(item?.processingStatus)));
@Input()
showCompartmentCode: boolean;
@Input()
showSupplier: boolean;
constructor() {
super({
selected: false,
selectable: false,
quantityEditable: false,
});
}

View File

@@ -8,9 +8,10 @@ import { ProductImageModule } from '@cdn/product-image';
import { PipesModule } from '../pipes/pipes.module';
import { UiSelectBulletModule } from '@ui/select-bullet';
import { FormsModule } from '@angular/forms';
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
@NgModule({
imports: [CommonModule, UiIconModule, ProductImageModule, PipesModule, UiSelectBulletModule, FormsModule],
imports: [CommonModule, UiIconModule, ProductImageModule, PipesModule, UiSelectBulletModule, FormsModule, UiQuantityDropdownModule],
exports: [GoodsInOutOrderGroupComponent, GoodsInOutOrderGroupItemComponent],
declarations: [GoodsInOutOrderGroupComponent, GoodsInOutOrderGroupItemComponent],
})

View File

@@ -25,6 +25,7 @@ class AbholfachService extends __BaseService {
static readonly AbholfachWarenausgabeQuerySettingsPath = '/warenausgabe/s/settings';
static readonly AbholfachWarenausgabeAutocompletePath = '/warenausgabe/s/complete';
static readonly AbholfachWarenausgabePath = '/warenausgabe/s';
static readonly AbholfachAbholfachbereinigungslistePath = '/abholfach/abholfachbereinigungsliste';
constructor(
config: __Configuration,
@@ -311,6 +312,39 @@ class AbholfachService extends __BaseService {
__map(_r => _r.body as ListResponseArgsOfOrderItemListItemDTO)
);
}
/**
* Abholfachbereinigungsliste
*/
AbholfachAbholfachbereinigungslisteResponse(): __Observable<__StrictHttpResponse<ListResponseArgsOfOrderItemListItemDTO>> {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/abholfach/abholfachbereinigungsliste`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ListResponseArgsOfOrderItemListItemDTO>;
})
);
}
/**
* Abholfachbereinigungsliste
*/
AbholfachAbholfachbereinigungsliste(): __Observable<ListResponseArgsOfOrderItemListItemDTO> {
return this.AbholfachAbholfachbereinigungslisteResponse().pipe(
__map(_r => _r.body as ListResponseArgsOfOrderItemListItemDTO)
);
}
}
module AbholfachService {