Abholfach Infrastruktur - WE und WA

This commit is contained in:
Lorenz Hilpert
2023-09-18 14:23:16 +02:00
parent b421c8b08c
commit fd63ce8b3c
46 changed files with 1212 additions and 116 deletions

View File

@@ -86,6 +86,28 @@ export class ApplicationService {
return this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch));
}
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
async createCustomerProcess(processId?: number): Promise<ApplicationProcess> {
const processes = await this.getProcesses$('customer').pipe(first()).toPromise();
const processIds = processes.filter((x) => this.REGEX_PROCESS_NAME.test(x.name)).map((x) => +x.name.split(' ')[1]);
const maxId = processIds.length > 0 ? Math.max(...processIds) : 0;
const process: ApplicationProcess = {
id: processId ?? Date.now(),
type: 'cart',
name: `Vorgang ${maxId + 1}`,
section: 'customer',
closeable: true,
};
await this.createProcess(process);
return process;
}
async createProcess(process: ApplicationProcess) {
const existingProcess = await this.getProcessById$(process?.id).pipe(first()).toPromise();
if (existingProcess?.id === process?.id) {

View File

@@ -0,0 +1,16 @@
import { Injectable, inject } from '@angular/core';
import { AbholfachService, QueryTokenDTO } from '@swagger/oms';
import { PickupShelfIOService } from './pickup-shelf-io.service';
@Injectable({ providedIn: 'root' })
export class PickupShelfService extends PickupShelfIOService {
private _abholfachService = inject(AbholfachService);
getQuerySettings() {
return this._abholfachService.AbholfachWareneingangQuerySettings();
}
search(queryToken: QueryTokenDTO) {
return this._abholfachService.AbholfachWareneingang(queryToken);
}
}

View File

@@ -0,0 +1,10 @@
import { Injectable } from '@angular/core';
import { ListResponseArgsOfDBHOrderItemListItemDTO, QueryTokenDTO, ResponseArgsOfQuerySettingsDTO } from '@swagger/oms';
import { Observable } from 'rxjs';
@Injectable()
export abstract class PickupShelfIOService {
abstract getQuerySettings(): Observable<ResponseArgsOfQuerySettingsDTO>;
abstract search(queryToken: QueryTokenDTO): Observable<ListResponseArgsOfDBHOrderItemListItemDTO>;
}

View File

@@ -0,0 +1,16 @@
import { Injectable, inject } from '@angular/core';
import { AbholfachService, QueryTokenDTO } from '@swagger/oms';
import { PickupShelfIOService } from './pickup-shelf-io.service';
@Injectable({ providedIn: 'root' })
export class PickupShelfOutService extends PickupShelfIOService {
private _abholfachService = inject(AbholfachService);
getQuerySettings() {
return this._abholfachService.AbholfachWarenausgabeQuerySettings();
}
search(queryToken: QueryTokenDTO) {
return this._abholfachService.AbholfachWarenausgabe(queryToken);
}
}

View File

@@ -0,0 +1,4 @@
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class PickupShelfInService {}

View File

@@ -0,0 +1,4 @@
export * from './lib/pickup-shelf-in.service';
export * from './lib/pickup-shelf-io.service';
export * from './lib/pickup-shelf-out.service';
export * from './lib/pickup-shelf.service';

View File

@@ -1,4 +1,4 @@
import { isDevMode, NgModule } from '@angular/core';
import { inject, isDevMode, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {
CanActivateCartGuard,
@@ -22,6 +22,9 @@ import { MainComponent } from './main.component';
import { PreviewComponent } from './preview';
import { BranchSectionResolver, CustomerSectionResolver, ProcessIdResolver } from './resolvers';
import { TokenLoginComponent, TokenLoginModule } from './token-login';
import { ApplicationService } from '@core/application';
import { ProcessIdGuard } from './guards/process-id.guard';
import { ActivateProcessIdGuard } from './guards/activate-process-id.guard';
const routes: Routes = [
{
@@ -36,111 +39,116 @@ const routes: Routes = [
canActivate: [IsAuthenticatedGuard],
children: [
{
path: '',
canActivate: [],
path: 'kunde',
component: MainComponent,
children: [
{
path: 'kunde',
component: MainComponent,
children: [
{
path: 'dashboard',
loadChildren: () => import('@page/dashboard').then((m) => m.DashboardModule),
},
{
path: 'product',
loadChildren: () => import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductGuard],
},
{
path: ':processId/product',
loadChildren: () => import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'order',
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersGuard],
},
{
path: ':processId/order',
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'customer',
loadChildren: () => import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerGuard],
},
{
path: ':processId/customer',
loadChildren: () => import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'cart',
loadChildren: () => import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartGuard],
},
{
path: ':processId/cart',
loadChildren: () => import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartWithProcessIdGuard],
},
{
path: 'goods/out',
loadChildren: () => import('@page/goods-out').then((m) => m.GoodsOutModule),
canActivate: [CanActivateGoodsOutGuard],
},
{
path: ':processId/goods/out',
loadChildren: () => import('@page/goods-out').then((m) => m.GoodsOutModule),
canActivate: [CanActivateGoodsOutWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
],
resolve: { section: CustomerSectionResolver },
path: 'dashboard',
loadChildren: () => import('@page/dashboard').then((m) => m.DashboardModule),
},
{
path: 'filiale',
component: MainComponent,
children: [
{
path: 'task-calendar',
loadChildren: () => import('@page/task-calendar').then((m) => m.PageTaskCalendarModule),
canActivate: [CanActivateTaskCalendarGuard],
},
{
path: 'goods/in',
loadChildren: () => import('@page/goods-in').then((m) => m.GoodsInModule),
canActivate: [CanActivateGoodsInGuard],
},
{
path: 'remission',
loadChildren: () => import('@page/remission').then((m) => m.PageRemissionModule),
canActivate: [CanActivateRemissionGuard],
},
{
path: 'package-inspection',
loadChildren: () => import('@page/package-inspection').then((m) => m.PackageInspectionModule),
canActivate: [CanActivatePackageInspectionGuard],
},
{
path: 'assortment',
loadChildren: () => import('@page/assortment').then((m) => m.AssortmentModule),
canActivate: [CanActivateAssortmentGuard],
},
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
],
resolve: { section: BranchSectionResolver },
path: 'product',
loadChildren: () => import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductGuard],
},
{ path: '**', redirectTo: 'kunde', pathMatch: 'full' },
{
path: ':processId/product',
loadChildren: () => import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'order',
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersGuard],
},
{
path: ':processId/order',
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'customer',
loadChildren: () => import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerGuard],
},
{
path: ':processId/customer',
loadChildren: () => import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'cart',
loadChildren: () => import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartGuard],
},
{
path: ':processId/cart',
loadChildren: () => import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartWithProcessIdGuard],
},
{
path: 'pickup-shelf',
canActivate: [ProcessIdGuard],
// NOTE: This is a workaround for the canActivate guard not being called
children: [],
},
{
path: ':processId/pickup-shelf',
canActivate: [ActivateProcessIdGuard],
loadChildren: () => import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
},
{
path: 'goods/out',
loadChildren: () => import('@page/goods-out').then((m) => m.GoodsOutModule),
canActivate: [CanActivateGoodsOutGuard],
},
{
path: ':processId/goods/out',
loadChildren: () => import('@page/goods-out').then((m) => m.GoodsOutModule),
canActivate: [CanActivateGoodsOutWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
],
resolve: { section: CustomerSectionResolver },
},
{
path: 'filiale',
component: MainComponent,
children: [
{
path: 'task-calendar',
loadChildren: () => import('@page/task-calendar').then((m) => m.PageTaskCalendarModule),
canActivate: [CanActivateTaskCalendarGuard],
},
{
path: 'goods/in',
loadChildren: () => import('@page/goods-in').then((m) => m.GoodsInModule),
canActivate: [CanActivateGoodsInGuard],
},
{
path: 'remission',
loadChildren: () => import('@page/remission').then((m) => m.PageRemissionModule),
canActivate: [CanActivateRemissionGuard],
},
{
path: 'package-inspection',
loadChildren: () => import('@page/package-inspection').then((m) => m.PackageInspectionModule),
canActivate: [CanActivatePackageInspectionGuard],
},
{
path: 'assortment',
loadChildren: () => import('@page/assortment').then((m) => m.AssortmentModule),
canActivate: [CanActivateAssortmentGuard],
},
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
],
resolve: { section: BranchSectionResolver },
},
{ path: '**', redirectTo: 'kunde', pathMatch: 'full' },
],
},
];

View File

@@ -0,0 +1,27 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';
import { ApplicationService } from '@core/application';
import { take } from 'rxjs/operators';
export const ActivateProcessIdGuard: CanActivateFn = async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const application = inject(ApplicationService);
const processIdStr = route.params.processId;
if (!processIdStr) {
return false;
}
const processId = Number(processIdStr);
// Check if Process already exists
const process = await application.getProcessById$(processId).pipe(take(1)).toPromise();
if (!process) {
application.createCustomerProcess(processId);
}
application.activateProcess(processId);
return true;
};

View File

@@ -0,0 +1,35 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { ApplicationService } from '@core/application';
import { take } from 'rxjs/operators';
export const ProcessIdGuard: CanActivateFn = async (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean | UrlTree> => {
const application = inject(ApplicationService);
const router = inject(Router);
const process = await application.getLastActivatedProcessWithSection$('customer').pipe(take(1)).toPromise();
const processId = process?.id ?? Date.now();
const originalUrl = state.url?.split('?')[0] ?? '';
let url: string = '';
if (originalUrl.startsWith('/kunde')) {
url = originalUrl.replace('/kunde', `/kunde/${processId}`);
} else {
url = originalUrl.replace('/filiale', `/filiale/${processId}`);
}
if (originalUrl === url) {
return true;
}
return router.createUrlTree([url], {
queryParams: route.queryParams,
fragment: route.fragment,
});
};

View File

@@ -0,0 +1,261 @@
import { DestroyRef, inject } from '@angular/core';
import { PickupShelfStore } from './store';
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { log, logAsync } from '@utils/common';
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
import { take } from 'rxjs/operators';
import { NavigationRoute } from '@shared/services';
import { DBHOrderItemListItemDTO } from '@swagger/oms';
import { isEmpty } from 'lodash';
/**
* Enthält die gemeinsame Logik für die Suche und verbindet die Komponenten mit dem Store.
*/
export abstract class PickupShelfBaseComponent {
protected destroyRef = inject(DestroyRef);
protected store = inject(PickupShelfStore);
protected router = inject(Router);
protected activatedRoute = inject(ActivatedRoute);
protected breadcrumbService = inject(BreadcrumbService);
constructor() {
/**
* Wenn die Suche erfolgreich war, wird der Benutzer auf die Liste oder Detailseite des gefundenen Artikels weitergeleitet.
*/
this.store.fetchListResponse$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async (response) => {
const queryParams = this.activatedRoute.snapshot.queryParams;
const filterQueryParams = this.store.filter.getQueryParams();
if (response.hits === 1) {
const detailsPath = await this.getPathForDetail(response.result[0]);
await this.router.navigate(detailsPath.path, { queryParams: { ...queryParams, ...filterQueryParams, ...detailsPath.queryParams } });
} else if (response.hits > 1) {
const listPath = await this.getPathFoList();
await this.router.navigate(listPath.path, { queryParams: { ...queryParams, ...filterQueryParams, ...listPath.queryParams } });
} else {
// TODO: Fehlermeldung in Suchbux
await this.router.navigate([], { queryParams: { ...queryParams, ...filterQueryParams } });
}
});
/**
* Checkt ob sich die ProzessId in der URL geändert hat und aktualisiert den Store.
* Zusätzlich wird die Breadcrumb aktualisiert.
*/
this.router.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
if (event instanceof NavigationEnd) {
this.runCheck();
}
});
this.runCheck();
}
protected runCheck() {
this._checkAndUpdateProcessIdInStore();
this._checkAndUpdateQueryParamsInStore();
this._checkAndUpdateBreadcrumb();
}
/**
* Aktualiisiert die ProzessId im Store.
* @returns void
*/
@log
private _checkAndUpdateProcessIdInStore() {
const processIdStr = this.activatedRoute.snapshot.params.processId;
if (!processIdStr) {
return;
}
const processId = Number(processIdStr);
this.store.setProcessId(processId);
}
/**
* Update die QueryParams im Store.
* @returns void
*/
@log
private _checkAndUpdateQueryParamsInStore() {
const queryParams = this.activatedRoute.snapshot.queryParams;
if (isEmpty(queryParams)) {
this.store.setQueryParams(undefined);
} else {
this.store.setQueryParams(queryParams);
}
}
/**
* Aktualisiert die Breadcrumb.
*/
@logAsync
private async _checkAndUpdateBreadcrumb() {
const breadcrumb = this.findDataInActivatedRouteSnapshot(this.activatedRoute.snapshot, 'breadcrumb', 2);
await this._checkAndUpdateMainBreadcrumb(breadcrumb);
await this._checkAndUpdateListBreadcrumb(breadcrumb);
await this._checkAndUpdateFilterBreadcrumb(breadcrumb);
}
/**
* Sucht die Breadcrumb anhand des Tags.
* @param tag Der gesuchte Tag
* @returns Breadcumb
*/
@logAsync
private async _getBreadcrumbByTag(tag: string) {
const breadcrumbs = await this.breadcrumbService
.getBreadcrumbsByKeyAndTags$(this.store.processId, ['pickup-shelf', tag])
.pipe(takeUntilDestroyed(this.destroyRef), take(1))
.toPromise();
return breadcrumbs[0];
}
/**
* Update die Main Breadcrumb.
* @param breadcrumb Der Aktuelle Breadcrumb Tag
*/
@logAsync
private async _checkAndUpdateMainBreadcrumb(breadcrumb: string) {
let mainBreadcrumb: Breadcrumb = await this._getBreadcrumbByTag('main');
if (!mainBreadcrumb) {
const name = await this.getNameForMainBreadcrumb();
const path = await this.getPathForMain();
mainBreadcrumb = {
key: this.store.processId,
name,
tags: ['pickup-shelf', 'main'],
path: path?.path,
section: 'customer',
params: { ...path?.queryParams, ...this.activatedRoute.snapshot.queryParams },
};
this.breadcrumbService.addBreadcrumb(mainBreadcrumb);
} else {
const name = await this.getNameForMainBreadcrumb();
const path = await this.getPathForMain();
const changes: Partial<Breadcrumb> = {
name,
path: path?.path,
params: { ...path?.queryParams, ...this.activatedRoute.snapshot.queryParams },
};
this.breadcrumbService.patchBreadcrumb(mainBreadcrumb.id, changes);
}
}
abstract getNameForMainBreadcrumb(): Promise<string>;
abstract getPathForMain(): Promise<NavigationRoute>;
/**
* Update die List Breadcrumb.
* @param breadcrumb Der Aktuelle Breadcrumb Tag
*/
@logAsync
private async _checkAndUpdateListBreadcrumb(breadcrumb: string) {
let listBreadcrumb: Breadcrumb = await this._getBreadcrumbByTag('list');
const shouldHaveBreadcrumb = ['list', 'list-filter'].includes(breadcrumb);
if (shouldHaveBreadcrumb && !listBreadcrumb) {
const name = await this.getNameForListBreadcrumb();
const path = await this.getPathFoList();
listBreadcrumb = {
key: this.store.processId,
name,
tags: ['pickup-shelf', 'list'],
path: path?.path,
section: 'customer',
params: { ...path?.queryParams, ...this.activatedRoute.snapshot.queryParams },
};
this.breadcrumbService.addBreadcrumb(listBreadcrumb);
} else if (shouldHaveBreadcrumb && listBreadcrumb) {
const name = await this.getNameForListBreadcrumb();
const path = await this.getPathFoList();
const changes: Partial<Breadcrumb> = {
name,
path: path?.path,
params: { ...path?.queryParams, ...this.activatedRoute.snapshot.queryParams },
};
this.breadcrumbService.patchBreadcrumb(listBreadcrumb.id, changes);
} else if (!shouldHaveBreadcrumb && listBreadcrumb) {
this.breadcrumbService.removeBreadcrumb(listBreadcrumb.id);
}
}
abstract getNameForListBreadcrumb(): Promise<string>;
abstract getPathFoList(): Promise<NavigationRoute>;
/**
* Update die Filter Breadcrumb.
* @param breadcrumb Der Aktuelle Breadcrumb Tag
*/
@logAsync
private async _checkAndUpdateFilterBreadcrumb(breadcrumb: string) {
let filterBreadcrumb: Breadcrumb = await this._getBreadcrumbByTag('filter');
const shouldHaveBreadcrumb = ['filter'].includes(breadcrumb);
if (shouldHaveBreadcrumb && !filterBreadcrumb) {
const name = await this.getNameForFilterBreadcrumb();
const path = await this.getPathForFilter();
filterBreadcrumb = {
key: this.store.processId,
name,
tags: ['pickup-shelf', 'filter'],
path: path?.path,
section: 'customer',
params: { ...path?.queryParams, ...this.activatedRoute.snapshot.queryParams },
};
this.breadcrumbService.addBreadcrumb(filterBreadcrumb);
} else if (shouldHaveBreadcrumb && filterBreadcrumb) {
const name = await this.getNameForFilterBreadcrumb();
const path = await this.getPathForFilter();
const changes: Partial<Breadcrumb> = {
name,
path: path?.path,
params: { ...path?.queryParams, ...this.activatedRoute.snapshot.queryParams },
};
this.breadcrumbService.patchBreadcrumb(filterBreadcrumb.id, changes);
} else if (!shouldHaveBreadcrumb && filterBreadcrumb) {
this.breadcrumbService.removeBreadcrumb(filterBreadcrumb.id);
}
}
abstract getNameForFilterBreadcrumb(): Promise<string>;
abstract getPathForFilter(): Promise<NavigationRoute>;
@log
// TODO: Ort für auslagerung finden
findDataInActivatedRouteSnapshot(snapshot: ActivatedRouteSnapshot, data: string, depth: number) {
if (!snapshot) {
return;
}
if (snapshot.data[data]) {
return snapshot.data[data];
}
if (depth > 0) {
const children = snapshot.children;
for (const child of children) {
const result = this.findDataInActivatedRouteSnapshot(child, data, depth - 1);
if (result) {
return result;
}
}
}
}
abstract getPathForDetail(item: DBHOrderItemListItemDTO): Promise<NavigationRoute>;
}

View File

@@ -0,0 +1,3 @@
export abstract class PickupShelfDetailsBaseComponent {
constructor() {}
}

View File

@@ -0,0 +1,3 @@
:host {
@apply block;
}

View File

@@ -0,0 +1,17 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { PickupShelfDetailsBaseComponent } from '../../pickup-shelf-details-base.component';
@Component({
selector: 'page-pickup-shelf-in-details',
templateUrl: 'pickup-shelf-in-details.component.html',
styleUrls: ['pickup-shelf-in-details.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-pickup-shelf-in-details' },
standalone: true,
imports: [],
})
export class PickupShelfInDetailsComponent extends PickupShelfDetailsBaseComponent {
constructor() {
super();
}
}

View File

@@ -0,0 +1,3 @@
:host {
@apply block;
}

View File

@@ -0,0 +1,2 @@
<shared-breadcrumb></shared-breadcrumb>
<shared-splitscreen></shared-splitscreen>

View File

@@ -0,0 +1,49 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { BreadcrumbModule } from '@shared/components/breadcrumb';
import { SharedSplitscreenComponent } from '@shared/components/splitscreen';
import { PickupShelfStore } from '../store';
import { provideComponentStore } from '@ngrx/component-store';
import { PickupShelfIOService, PickupShelfInService } from '@domain/pickup-shelf';
import { PickupShelfBaseComponent } from '../pickup-shelf-base.component';
import { NavigationRoute, PickupShelfInNavigationService } from '@shared/services';
import { DBHOrderItemListItemDTO } from '@swagger/oms';
@Component({
selector: 'page-pickup-shelf-in',
templateUrl: 'pickup-shelf-in.component.html',
styleUrls: ['pickup-shelf-in.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-pickup-shelf-in' },
standalone: true,
imports: [BreadcrumbModule, SharedSplitscreenComponent],
providers: [provideComponentStore(PickupShelfStore), { provide: PickupShelfIOService, useClass: PickupShelfInService }],
})
export class PickupShelfInComponent extends PickupShelfBaseComponent {
private _pickupShelfInNavigationService = inject(PickupShelfInNavigationService);
async getPathForMain(): Promise<NavigationRoute> {
return this._pickupShelfInNavigationService.defaultRoute();
}
async getNameForMainBreadcrumb(): Promise<string> {
return 'Wareneingang';
}
async getNameForListBreadcrumb(): Promise<string> {
return 'Liste';
}
async getPathFoList(): Promise<NavigationRoute> {
return this._pickupShelfInNavigationService.listRoute();
}
async getNameForFilterBreadcrumb(): Promise<string> {
return 'Filter';
}
async getPathForFilter(): Promise<NavigationRoute> {
return this._pickupShelfInNavigationService.filterRoute();
}
async getPathForDetail(item: DBHOrderItemListItemDTO): Promise<NavigationRoute> {
return this._pickupShelfInNavigationService.detailRoute({ item });
}
}

View File

@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { routes } from './routes';
import { provideComponentStore } from '@ngrx/component-store';
import { PickupShelfStore } from '../store/pickup-shelf.store';
import { PickupShelfIOService, PickupShelfInService } from '@domain/pickup-shelf';
@NgModule({
imports: [RouterModule.forChild(routes)],
})
export class PickupShelfInModule {}

View File

@@ -0,0 +1,19 @@
import { Routes } from '@angular/router';
import { PickupShelfInComponent } from './pickup-shelf-in.component';
import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
import { PickupShelfInDetailsComponent } from './pickup-shelf-in-details/pickup-shelf-in-details.component';
export const routes: Routes = [
{
path: '',
component: PickupShelfInComponent,
children: [
{ path: '', data: { breadcrumb: 'main' } },
{ path: 'list', data: { breadcrumb: 'list' } },
{ path: 'list/filter', component: PickupShelfFilterComponent, data: { breadcrumb: 'filter' } },
{ path: 'order/:orderId/:orderItemId', component: PickupShelfInDetailsComponent, data: { breadcrumb: 'details' } },
// { path: 'main', outlet: 'side' },
// { path: 'list', outlet: 'side' },
],
},
];

View File

@@ -0,0 +1,3 @@
:host {
@apply block;
}

View File

@@ -0,0 +1,17 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { PickupShelfDetailsBaseComponent } from '../../pickup-shelf-details-base.component';
@Component({
selector: 'page-pickup-shelf-out-details',
templateUrl: 'pickup-shelf-out-details.component.html',
styleUrls: ['pickup-shelf-out-details.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-pickup-shelf-out-details' },
standalone: true,
imports: [],
})
export class PickupShelfOutDetailsComponent extends PickupShelfDetailsBaseComponent {
constructor() {
super();
}
}

View File

@@ -0,0 +1,3 @@
:host {
@apply block;
}

View File

@@ -0,0 +1,4 @@
<div>Hits {{ hits$ | async }}</div>
<div *ngFor="let item of list$ | async">
{{ item.orderNumber }}
</div>

View File

@@ -0,0 +1,22 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { PickupShelfStore } from '../../store';
import { AsyncPipe, NgFor } from '@angular/common';
@Component({
selector: 'page-pcikup-shelf-out-list',
templateUrl: 'pickup-shelf-out-list.component.html',
styleUrls: ['pickup-shelf-out-list.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-pcikup-shelf-out-list' },
standalone: true,
imports: [AsyncPipe, NgFor],
})
export class PickupShelfOutListComponent {
store = inject(PickupShelfStore);
list$ = this.store.list$;
hits$ = this.store.listHits$;
constructor() {}
}

View File

@@ -0,0 +1,3 @@
:host {
@apply block;
}

View File

@@ -0,0 +1 @@
<page-pcikup-shelf-filter></page-pcikup-shelf-filter>

View File

@@ -0,0 +1,15 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { PickupShelfFilterComponent } from '../../shared/pickup-shelf-filter/pickup-shelf-filter.component';
@Component({
selector: 'page-pickup-shelf-out-main',
templateUrl: 'pickup-shelf-out-main.component.html',
styleUrls: ['pickup-shelf-out-main.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-pickup-shelf-out-main' },
standalone: true,
imports: [PickupShelfFilterComponent],
})
export class PickupShelfOutMainComponent {
constructor() {}
}

View File

@@ -0,0 +1,3 @@
:host {
@apply block;
}

View File

@@ -0,0 +1,2 @@
<shared-breadcrumb [key]="store.processId$ | async" [tags]="['pickup-shelf']"></shared-breadcrumb>
<shared-splitscreen></shared-splitscreen>

View File

@@ -0,0 +1,60 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { BreadcrumbModule } from '@shared/components/breadcrumb';
import { SharedSplitscreenComponent } from '@shared/components/splitscreen';
import { PickupShelfStore } from '../store';
import { provideComponentStore } from '@ngrx/component-store';
import { PickupShelfIOService, PickupShelfOutService } from '@domain/pickup-shelf';
import { PickupShelfBaseComponent } from '../pickup-shelf-base.component';
import { NavigationRoute, PickupShelfOutNavigationService } from '@shared/services';
import { AsyncPipe } from '@angular/common';
import { DBHOrderItemListItemDTO } from '@swagger/oms';
import { logAsync } from '@utils/common';
@Component({
selector: 'page-pickup-shelf-out',
templateUrl: 'pickup-shelf-out.component.html',
styleUrls: ['pickup-shelf-out.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-pickup-shelf-out' },
standalone: true,
imports: [BreadcrumbModule, SharedSplitscreenComponent, AsyncPipe],
providers: [provideComponentStore(PickupShelfStore), { provide: PickupShelfIOService, useClass: PickupShelfOutService }],
})
export class PickupShelfOutComponent extends PickupShelfBaseComponent {
private _pickupShelfOutNavigationService = inject(PickupShelfOutNavigationService);
@logAsync
async getPathForMain(): Promise<NavigationRoute> {
return this._pickupShelfOutNavigationService.defaultRoute({ processId: this.store.processId });
}
@logAsync
async getNameForMainBreadcrumb(): Promise<string> {
return 'Warenausgabe';
}
@logAsync
async getNameForListBreadcrumb(): Promise<string> {
return 'Liste';
}
@logAsync
async getPathFoList(): Promise<NavigationRoute> {
return this._pickupShelfOutNavigationService.listRoute({ processId: this.store.processId });
}
@logAsync
async getNameForFilterBreadcrumb(): Promise<string> {
return 'Filter';
}
@logAsync
async getPathForFilter(): Promise<NavigationRoute> {
return this._pickupShelfOutNavigationService.filterRoute({ processId: this.store.processId });
}
@logAsync
async getPathForDetail(item: DBHOrderItemListItemDTO): Promise<NavigationRoute> {
return this._pickupShelfOutNavigationService.detailRoute({ processId: this.store.processId, item });
}
}

View File

@@ -0,0 +1,8 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { routes } from './routes';
@NgModule({
imports: [RouterModule.forChild(routes)],
})
export class PickupShelfOutModule {}

View File

@@ -0,0 +1,23 @@
import { Routes } from '@angular/router';
import { PickupShelfOutComponent } from './pickup-shelf-out.component';
import { PickupShelfOutMainComponent } from './pickup-shelf-out-main/pickup-shelf-out-main.component';
import { PickupShelfOutListComponent } from './pickup-shelf-out-list/pickup-shelf-out-list.component';
import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
import { PickupShelfOutDetailsComponent } from './pickup-shelf-out-details/pickup-shelf-out-details.component';
export const routes: Routes = [
{
path: '',
component: PickupShelfOutComponent,
children: [
{ path: '', component: PickupShelfOutMainComponent, data: { breadcrumb: 'main' } },
{ path: 'list', component: PickupShelfOutListComponent, data: { breadcrumb: 'list' } },
{ path: 'list/filter', component: PickupShelfFilterComponent, data: { breadcrumb: 'filter' } },
{ path: 'order/:orderId/:processingStatus', component: PickupShelfOutDetailsComponent, data: { breadcrumb: 'details' } },
{ path: 'compartment/:compartment', component: PickupShelfOutDetailsComponent, data: { breadcrumb: 'details' } },
// { path: 'main', outlet: 'side' },
// { path: 'list', outlet: 'side' },
],
},
];

View File

@@ -0,0 +1,7 @@
:host {
@apply block bg-white p-4;
}
h1 {
@apply text-2xl font-bold text-center pb-6;
}

View File

@@ -0,0 +1,9 @@
<div class="text-right">
<button type="button" (click)="clearFilter()">Alle Filter entfernen</button>
</div>
<h1>Filter</h1>
<div *ngIf="filter$ | async; let filter">
<shared-filter [filter]="filter" (search)="applyFilter(filter)"></shared-filter>
<button type="button" (click)="resetFilter()">Filter zurücksetzen</button>
<button type="button" (click)="applyFilter(filter)">Filter anwenden</button>
</div>

View File

@@ -0,0 +1,34 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { Filter, FilterModule } from '@shared/components/filter';
import { PickupShelfStore } from '../../store';
import { AsyncPipe, NgIf } from '@angular/common';
@Component({
selector: 'page-pcikup-shelf-filter',
templateUrl: 'pickup-shelf-filter.component.html',
styleUrls: ['pickup-shelf-filter.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-pcikup-shelf-filter' },
standalone: true,
imports: [FilterModule, AsyncPipe, NgIf],
})
export class PickupShelfFilterComponent {
store = inject(PickupShelfStore);
filter$ = this.store.filter$;
constructor() {}
resetFilter() {
this.store.setQueryParams(undefined);
}
applyFilter(filter: Filter) {
this.store.setQueryParams(filter.getQueryParams());
this.store.fetchList();
}
clearFilter() {
this.store.setQueryParams({});
}
}

View File

@@ -0,0 +1 @@
export * from './pickup-shelf.store';

View File

@@ -0,0 +1,65 @@
import { Filter } from '@shared/components/filter';
import { PickupShelfState } from './pickup-shelf.state';
import { isEmpty } from 'lodash';
export function selectProcessId(state: PickupShelfState) {
return state.processId;
}
export function selectFetchingQuerySettings(state: PickupShelfState) {
return state.fetchingQuerySettings ?? false;
}
export function selectQuerySettings(state: PickupShelfState) {
return state.querySettings;
}
export function selectQueryParams(state: PickupShelfState) {
return state.queryParams;
}
export function selectDefaultFilter(state: PickupShelfState) {
const querySettings = selectQuerySettings(state);
return querySettings ? Filter.create(querySettings) : undefined;
}
export function selectFilter(state: PickupShelfState) {
const queryParams = selectQueryParams(state);
const defaultFilter = selectDefaultFilter(state);
if (!defaultFilter) {
return undefined;
}
const filter = Filter.create(defaultFilter);
// Wenn queryParams undefined ist, dann wird der Filter nicht gesetzt
if (queryParams === undefined) {
return filter;
}
// Wenn queryParams ein leeres Objekt ist, dann wird der Filter gesetzt, aber ohne Werte (leerer Filter)
if (isEmpty(queryParams)) {
filter.unselectAllFilterOptions();
return filter;
}
// Wenn queryParams ein Objekt mit Werten ist, dann wird der Filter gesetzt
filter.fromQueryParams(queryParams);
return filter;
}
export function selectFetchingList(state: PickupShelfState) {
return state.fetchingList ?? false;
}
export function selectList(state: PickupShelfState) {
return state.list ?? [];
}
export function selectListHits(state: PickupShelfState) {
return state.listHits ?? 0;
}

View File

@@ -0,0 +1,14 @@
import { DBHOrderItemListItemDTO, QuerySettingsDTO } from '@swagger/oms';
export interface PickupShelfState {
processId?: number;
fetchingQuerySettings?: boolean;
querySettings?: QuerySettingsDTO;
queryParams?: Record<string, string>;
fetchingList?: boolean;
list?: DBHOrderItemListItemDTO[];
listHits?: number;
}

View File

@@ -0,0 +1,160 @@
import { ComponentStore, tapResponse, OnStoreInit } from '@ngrx/component-store';
import { PickupShelfState } from './pickup-shelf.state';
import { PickupShelfIOService } from '@domain/pickup-shelf';
import { delayWhen, switchMap, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
import { ListResponseArgsOfDBHOrderItemListItemDTO, ResponseArgsOfQuerySettingsDTO } from '@swagger/oms';
import { DestroyRef, Injectable, inject } from '@angular/core';
import { ToasterService } from '@shared/shell';
import { UiModalService } from '@ui/modal';
import * as Selectors from './pickup-shelf.selectors';
import { Subject } from 'rxjs';
@Injectable()
export class PickupShelfStore extends ComponentStore<PickupShelfState> implements OnStoreInit {
private _destronyRef = inject(DestroyRef);
private _cancelListRequests = new Subject<void>();
private _pickupShelfIOService = inject(PickupShelfIOService);
private _toasterService = inject(ToasterService);
private _modalService = inject(UiModalService);
readonly processId$ = this.select(Selectors.selectProcessId);
private _fetchListResponse = new Subject<ListResponseArgsOfDBHOrderItemListItemDTO>();
readonly fetchListResponse$ = this._fetchListResponse.asObservable();
get processId() {
return this.get(Selectors.selectProcessId);
}
readonly filter$ = this.select(Selectors.selectFilter);
get filter() {
return this.get(Selectors.selectFilter);
}
readonly list$ = this.select(Selectors.selectList);
get list() {
return this.get(Selectors.selectList);
}
readonly listHits$ = this.select(Selectors.selectListHits);
get listHits() {
return this.get(Selectors.selectListHits);
}
constructor() {
// Nicht entfernen sonst wird der Store nicht initialisiert
super({});
this._destronyRef.onDestroy(() => {
this.cancelListRequests();
this._fetchListResponse.complete();
});
}
setProcessId(processId: number) {
if (this.processId === processId) {
return;
}
this.cancelListRequests();
this.patchState({ processId: processId, fetchingList: false, list: [], listHits: 0, queryParams: {} });
}
setQueryParams(queryParams: Record<string, string> | undefined) {
this.patchState({ queryParams });
}
cancelListRequests() {
this._cancelListRequests.next();
}
ngrxOnStoreInit = () => {
this.fetchQuerySettings();
};
fetchQuerySettings = this.effect((trigger$) =>
trigger$.pipe(
tap(() => this.patchState({ fetchingQuerySettings: true })),
switchMap(() =>
this._pickupShelfIOService.getQuerySettings().pipe(tapResponse(this.fetchQuerySettingsDone, this.fetchQuerySettingsError))
)
)
);
private fetchQuerySettingsDone = (resp: ResponseArgsOfQuerySettingsDTO) => {
this.patchState({ fetchingQuerySettings: false, querySettings: resp.result });
};
private fetchQuerySettingsError = (err: any) => {
this._modalService.error('Fehler beim Laden der Filtereinstellungen', err);
this.patchState({ fetchingQuerySettings: false });
};
delayWhenFilterIsNotReady = delayWhen(() => this.filter$.pipe(tap((filter) => !!filter)));
fetchList = this.effect((trigger$) =>
trigger$.pipe(
this.delayWhenFilterIsNotReady,
tap(this.beforeFetchList),
withLatestFrom(this.filter$),
switchMap(([_, filter]) =>
this._pickupShelfIOService
.search(filter.getQueryToken())
.pipe(takeUntil(this._cancelListRequests), tapResponse(this.fetchListDone, this.fetchListError))
)
)
);
private beforeFetchList = () => {
this.cancelListRequests();
this.patchState({ fetchingList: true, list: [], listHits: 0 });
};
private fetchListDone = (resp: ListResponseArgsOfDBHOrderItemListItemDTO) => {
this.patchState({ fetchingList: false, list: resp.result, listHits: resp.hits });
this._fetchListResponse.next(resp);
};
private fetchListError = (err: any) => {
this._modalService.error('Fehler beim Laden der Liste', err);
this.patchState({ fetchingList: false });
};
fetchMoreList = this.effect((trigger$) =>
trigger$.pipe(
// TODO: Skip if fethcing is already in progress
tap(this.beforeFetchMoreList),
withLatestFrom(this.filter$, this.listHits$),
switchMap(([_, filter, listHits]) => {
const queryToken = filter.getQueryToken();
queryToken.skip = listHits;
return this._pickupShelfIOService
.search(queryToken)
.pipe(takeUntil(this._cancelListRequests), tapResponse(this.fetchMoreListDone, this.fetchMoreListError));
})
)
);
private beforeFetchMoreList = () => {
this.cancelListRequests();
this.patchState({ fetchingList: true });
};
private fetchMoreListDone = (resp: ListResponseArgsOfDBHOrderItemListItemDTO) => {
this.patchState({ fetchingList: false, list: [...this.list, ...resp.result], listHits: resp.hits });
};
private fetchMoreListError = (err: any) => {
this._modalService.error('Fehler beim Laden der Liste', err);
this.patchState({ fetchingList: false });
};
}

View File

@@ -0,0 +1,2 @@
export * from './lib/pickup-shelf-out/pickup-shelf-out.module';
export * from './lib/pickup-shelf-in/pickup-shelf-in.module';

View File

@@ -0,0 +1,68 @@
import { Injectable, inject } from '@angular/core';
import { NavigationRoute } from './navigation-route';
import { Router } from '@angular/router';
import { Config } from '@core/config';
import { DBHOrderItemListItemDTO } from '@swagger/oms';
@Injectable({ providedIn: 'root' })
export class PickupShelfInNavigationService {
private readonly _router = inject(Router);
private readonly _config = inject(Config);
defaultRoute(): NavigationRoute {
const path = ['/filiale', 'pickup-shelf'].filter((v) => !!v);
const queryParams = {};
const urlTree = this._router.createUrlTree(path, { queryParams });
return {
path,
queryParams,
urlTree,
};
}
listRoute(): NavigationRoute {
const path = ['/filiale', 'pickup-shelf', 'list'].filter((v) => !!v);
const queryParams = {};
const urlTree = this._router.createUrlTree(path, { queryParams });
return {
path,
queryParams,
urlTree,
};
}
filterRoute(): NavigationRoute {
const path = ['/filiale', 'pickup-shelf', 'list', 'filter'].filter((v) => !!v);
const queryParams = {};
const urlTree = this._router.createUrlTree(path, { queryParams });
return {
path,
queryParams,
urlTree,
};
}
detailRoute({ item }: { item: DBHOrderItemListItemDTO }): NavigationRoute {
const path = ['/filiale', 'pickup-shelf', 'order', item.orderId, item.orderItemId].filter((v) => !!v);
const queryParams = {};
const urlTree = this._router.createUrlTree(path, { queryParams });
return {
path,
queryParams,
urlTree,
};
}
}

View File

@@ -0,0 +1,71 @@
import { Injectable, inject } from '@angular/core';
import { NavigationRoute } from './navigation-route';
import { Router } from '@angular/router';
import { DBHOrderItemListItemDTO } from '@swagger/oms';
@Injectable({ providedIn: 'root' })
export class PickupShelfOutNavigationService {
private readonly _router = inject(Router);
defaultRoute({ processId }: { processId?: number }): NavigationRoute {
const path = ['/kunde', processId ? processId : undefined, 'pickup-shelf'].filter((v) => !!v);
const queryParams = {};
const urlTree = this._router.createUrlTree(path, { queryParams });
return {
path,
queryParams,
urlTree,
};
}
listRoute({ processId }: { processId?: number }): NavigationRoute {
const path = ['/kunde', processId ? processId : undefined, 'pickup-shelf', 'list'].filter((v) => !!v);
const queryParams = {};
const urlTree = this._router.createUrlTree(path, { queryParams });
return {
path,
queryParams,
urlTree,
};
}
filterRoute({ processId }: { processId?: number }): NavigationRoute {
const path = ['/kunde', processId ? processId : undefined, 'pickup-shelf', 'list', 'filter'].filter((v) => !!v);
const queryParams = {};
const urlTree = this._router.createUrlTree(path, { queryParams });
return {
path,
queryParams,
urlTree,
};
}
detailRoute({ processId, item }: { processId?: number; item: DBHOrderItemListItemDTO }): NavigationRoute {
let path: any[];
if (item.compartmentCode) {
path = ['/kunde', processId ? processId : undefined, 'pickup-shelf', 'compartment', item.compartmentCode].filter((v) => !!v);
} else {
path = ['/kunde', processId ? processId : undefined, 'pickup-shelf', 'order', item.orderId, item.processingStatus].filter((v) => !!v);
}
const queryParams = {};
const urlTree = this._router.createUrlTree(path, { queryParams });
return {
path,
queryParams,
urlTree,
};
}
}

View File

@@ -10,4 +10,6 @@ export * from './lib/customer-create.navigation';
export * from './lib/customer-search.navigation';
export * from './lib/navigation-route';
export * from './lib/navigation.service';
export * from './lib/pickup-shelf-out-navigation.service';
export * from './lib/pickup-shelf-in-navigation.service';
export * from './lib/defs';

View File

@@ -71,23 +71,7 @@ export class ShellProcessBarComponent implements OnInit {
static REGEX_PROCESS_NAME = /^Vorgang \d+$/;
async createCartProcess() {
const processes = await this._app.getProcesses$('customer').pipe(first()).toPromise();
const processIds = processes.filter((x) => ShellProcessBarComponent.REGEX_PROCESS_NAME.test(x.name)).map((x) => +x.name.split(' ')[1]);
const maxId = processIds.length > 0 ? Math.max(...processIds) : 0;
const process: ApplicationProcess = {
id: Date.now(),
type: 'cart',
name: `Vorgang ${maxId + 1}`,
section: 'customer',
closeable: true,
};
this._app.createProcess(process);
return process;
return this._app.createCustomerProcess();
}
async navigateTo(target: string, process: ApplicationProcess) {

View File

@@ -30,6 +30,9 @@
"@core/*": [
"apps/core/*/src/public-api.ts"
],
"@domain/*": [
"apps/domain/*/src/public-api.ts"
],
"@domain/availability": [
"apps/domain/availability/src/public-api.ts"
],