Merge branch 'develop' into release/1.5

This commit is contained in:
Lorenz Hilpert
2021-10-25 15:51:13 +02:00
134 changed files with 2854 additions and 365 deletions

View File

@@ -3256,6 +3256,46 @@
}
}
}
},
"@shared/notification-channel-control": {
"projectType": "library",
"root": "apps/shared/notification-channel-control",
"sourceRoot": "apps/shared/notification-channel-control/src",
"prefix": "shared",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/shared/notification-channel-control/tsconfig.lib.json",
"project": "apps/shared/notification-channel-control/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shared/notification-channel-control/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/shared/notification-channel-control/src/test.ts",
"tsConfig": "apps/shared/notification-channel-control/tsconfig.spec.json",
"karmaConfig": "apps/shared/notification-channel-control/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/shared/notification-channel-control/tsconfig.lib.json",
"apps/shared/notification-channel-control/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "sales"

View File

@@ -20,6 +20,14 @@ export class DomainAvailabilityService {
private _stock: StockService
) {}
@memorize()
getSuppliers(): Observable<SupplierDTO[]> {
return this.storeCheckoutService.StoreCheckoutGetSuppliers({}).pipe(
map((response) => response.result),
shareReplay()
);
}
@memorize()
getTakeAwaySupplier(): Observable<SupplierDTO> {
return this.storeCheckoutService.StoreCheckoutGetSuppliers({}).pipe(
@@ -72,8 +80,8 @@ export class DomainAvailabilityService {
availableQuantity: stockInfo.availableQuantity,
availabilityType: quantity <= stockInfo.inStock ? 1024 : 1, // 1024 (=Available)
inStock: stockInfo.inStock,
ssc: quantity <= stockInfo.inStock ? '999' : '',
sscText: quantity <= stockInfo.inStock ? 'Filialentnahme' : '',
supplierSSC: quantity <= stockInfo.inStock ? '999' : '',
supplierSSCText: quantity <= stockInfo.inStock ? 'Filialentnahme' : '',
price,
supplier: { id: supplier?.id },
branchId: stockInfo.branchId,
@@ -363,8 +371,8 @@ export class DomainAvailabilityService {
const availability: AvailabilityDTO = {
availabilityType: quantity <= inStock ? 1024 : 1, // 1024 (=Available)
inStock: inStock,
ssc: quantity <= inStock ? '999' : '',
sscText: quantity <= inStock ? 'Filialentnahme' : '',
supplierSSC: quantity <= inStock ? '999' : '',
supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '',
price,
supplier: { id: supplier?.id },
};

View File

@@ -1,7 +1,9 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ActionHandler } from '@core/command';
import { OrderItemListItemDTO } from '@swagger/oms';
import { ActionHandler, CommandService } from '@core/command';
import { ReorderModalComponent, ReorderResult } from '@modal/reorder';
import { AvailabilityDTO2, OrderItemListItemDTO } from '@swagger/oms';
import { UiModalService } from '@ui/modal';
import { isResponseArgs } from '@utils/object';
import { first } from 'rxjs/operators';
import { DomainOmsService } from '../oms.service';
@@ -9,23 +11,35 @@ import { OrderItemsContext } from './order-items.context';
@Injectable()
export class OrderAtSupplierActionHandler extends ActionHandler<OrderItemsContext> {
constructor(private _domainOmsService: DomainOmsService) {
constructor(private _command: CommandService, private _domainOmsService: DomainOmsService, private _uiModal: UiModalService) {
super('ORDER_AT_SUPPLIER');
}
async handler(data: OrderItemsContext): Promise<OrderItemsContext> {
const updatedItems: OrderItemListItemDTO[] = [];
for (const item of data?.items) {
for (const orderItem of data.items) {
const result = await this._uiModal
.open<ReorderResult, OrderItemListItemDTO>({
content: ReorderModalComponent,
title: 'Artikel bestellen',
data: orderItem,
})
.afterClosed$.toPromise();
try {
const res = await this._domainOmsService
.orderAtSupplier({
orderId: item.orderId,
orderItemId: item.orderItemId,
orderItemSubsetId: item.orderItemSubsetId,
})
.pipe(first())
.toPromise();
updatedItems.push({ ...item, processingStatus: 16 });
if (result.data) {
if (result.data.action === 'REORDER') {
await this.patchOrderItemSubset(result.data.item, result.data.availability);
await this.orderAtSupplier(result.data.item);
updatedItems.push({ ...orderItem, processingStatus: 16 });
} else if (result.data.action === 'NOTAVAILABLE') {
let context = { ...data, items: [orderItem] };
context = await this._command.handleCommand('NOTAVAILABLE', context);
updatedItems.push(...context.items);
}
} else {
return data;
}
} catch (err) {
if (err instanceof HttpErrorResponse && isResponseArgs(err.error)) {
console.error('InvalidProperties: ', err.error.invalidProperties);
@@ -37,6 +51,38 @@ export class OrderAtSupplierActionHandler extends ActionHandler<OrderItemsContex
throw err;
}
}
return { ...data, items: updatedItems };
}
async patchOrderItemSubset(orderItem: OrderItemListItemDTO, availability: AvailabilityDTO2) {
return await this._domainOmsService
.patchOrderItemSubset({
orderId: orderItem.orderId,
orderItemId: orderItem.orderItemId,
orderItemSubsetId: orderItem.orderItemSubsetId,
orderItemSubset: {
ssc: availability.ssc,
sscText: availability.sscText,
supplier: {
id: availability.supplierId,
},
isPrebooked: availability.isPrebooked,
estimatedShippingDate: availability.at,
},
})
.pipe(first())
.toPromise();
}
async orderAtSupplier(orderItem: OrderItemListItemDTO) {
return await this._domainOmsService
.orderAtSupplier({
orderId: orderItem.orderId,
orderItemId: orderItem.orderItemId,
orderItemSubsetId: orderItem.orderItemSubsetId,
})
.pipe(first())
.toPromise();
}
}

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import {
ChangeStockStatusCodeValues,
HistoryDTO,
NotificationChannel,
OrderCheckoutService,
OrderDTO,
OrderItemDTO,
@@ -15,7 +16,7 @@ import {
} from '@swagger/oms';
import { memorize } from '@utils/common';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { map, mergeMap, shareReplay } from 'rxjs/operators';
@Injectable()
export class DomainOmsService {
@@ -162,4 +163,60 @@ export class DomainOmsService {
orderItemSubsetId,
});
}
getNotifications(orderId: number): Observable<{ selected: NotificationChannel; email: string; mobile: string }> {
return this.getOrder(orderId).pipe(
map((order) => ({
selected: order.notificationChannels,
email: order.buyer?.communicationDetails?.email,
mobile: order.buyer?.communicationDetails?.mobile,
}))
);
}
updateNotifications(orderId: number, changes: { selected: NotificationChannel; email: string; mobile: string }) {
const communicationDetails = {
email: changes.email,
mobile: changes.mobile,
};
if (!(changes.selected & 1)) {
delete communicationDetails.email;
}
if (!(changes.selected & 2)) {
delete communicationDetails.mobile;
}
return this.orderService
.OrderPatchOrder({
orderId: orderId,
order: {
notificationChannels: changes.selected,
buyer: { communicationDetails },
},
})
.pipe(map((res) => res.result));
}
getCompletedTasks({
orderId,
orderItemId,
orderItemSubsetId,
}: {
orderId: number;
orderItemId: number;
orderItemSubsetId: number;
}): Observable<Record<string, Date>> {
return this.orderService
.OrderGetOrderItemSubsetTasks({ orderId, orderItemId, orderItemSubsetId, completed: new Date(0).toISOString() })
.pipe(
map((res) =>
res.result.reduce((data, result) => {
data[result.name] = new Date(result.completed);
return data;
}, {} as Record<string, Date>)
)
);
}
}

View File

@@ -144,6 +144,7 @@ export class DomainTaskCalendarService {
* 4 = Completed
* 8 = Overdue
* 16 = Archived
* 32 = Removed
* 64 = Uncompleted
*/
const processingStatusList: ProcessingStatusList = [];

View File

@@ -69,6 +69,7 @@ export class ReturnItemDtoToRemissionProductMapping implements Mapper<ReturnItem
catalogProductNumber: source.product.catalogProductNumber,
features: this.getFeatures(source.assortment), // assortment (semicolon separiert ohne 'SO') z.B. "Wirtschaft|B"
isResidual: source.descendantOf && source.descendantOf.enabled ? true : false || !!source.impediment,
impediment: source.impediment,
} as RemissionProduct;
}
}

View File

@@ -39,6 +39,7 @@ export class ReturnSuggestionDtoToRemissionProductMapping implements Mapper<Retu
!!source.impediment ||
// tslint:disable-next-line: no-string-literal
(source['descendantOf'] && !!source['descendantOf'].enabled),
impediment: source.impediment?.comment !== 'Restmenge' ? source.impediment : undefined,
} as RemissionProduct;
}
}

View File

@@ -1,3 +1,4 @@
import { ImpedimentDTO } from '@swagger/remi';
import { RemissionPlacementType } from '../types/remission-placement-types';
import { RemissionSupplier } from './remission-supplier';
@@ -117,4 +118,6 @@ export interface RemissionProduct {
*
*/
department?: string;
impediment?: ImpedimentDTO;
}

View File

@@ -1,7 +1,3 @@
<button class="close-btn" (click)="close()">
<ui-icon icon="close" size="21px"></ui-icon>
</button>
<h1>Sie haben neue Nachrichten</h1>
<ng-container *ngFor="let notification of groupedNotifications$ | async">

View File

@@ -5,10 +5,6 @@ modal-notifications {
@apply text-xl font-bold text-center mb-10;
}
.close-btn {
@apply absolute right-0 top-0 bg-transparent border-none text-ucla-blue;
}
.notification-card {
@apply text-center text-xl text-inactive-branch block bg-white rounded-t-card font-bold no-underline py-4 border-none outline-none shadow-card -ml-4;
width: calc(100% + 2rem);
@@ -47,7 +43,7 @@ modal-notifications {
.notification-list {
@apply overflow-y-scroll -ml-4;
max-height: calc(100vh - 400px);
max-height: calc(100vh - 450px);
width: calc(100% + 2rem);
overflow-x: hidden;
}

View File

@@ -30,6 +30,7 @@
<span></span>
<span class="number">Bestand</span>
<span>MS</span>
<span title="Vormerker">VM</span>
<span>vsl. Lieferdatum</span>
<span class="number">Preis</span>
<span></span>
@@ -39,6 +40,9 @@
<span class="first-cell">{{ availability.supplier | supplierName }}</span>
<span class="number">{{ availability.qty || 0 }}</span>
<span>{{ availability.ssc }}</span>
<span>
<ui-checkbox *ngIf="availability.supplier !== 'F'" [(ngModel)]="availability.isPrebooked"> </ui-checkbox>
</span>
<span>{{ availability.at | date: 'dd.MM.yy' }}</span>
<span class="number">{{ availability.price?.value?.value | currency: 'EUR':'code' }}</span>
<span>

View File

@@ -46,7 +46,7 @@ hr {
.supplier-grid {
@apply grid font-bold;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
grid-template-columns: repeat(7, 1fr);
span:not(.first-cell) {
@apply text-center;
@@ -62,7 +62,8 @@ hr {
border-bottom: 2px solid #e0ebf5;
}
ui-select-bullet {
ui-select-bullet,
ui-checkbox {
@apply flex justify-center;
}
}

View File

@@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ProductImageModule } from '@cdn/product-image';
import { UiCheckboxModule } from '@ui/checkbox';
import { UiIconModule } from '@ui/icon';
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
import { UiSelectBulletModule } from '@ui/select-bullet';
@@ -19,6 +20,7 @@ import { SupplierNamePipe } from './supplier-name.pipe';
ReactiveFormsModule,
UiSpinnerModule,
UiQuantityDropdownModule,
UiCheckboxModule,
],
exports: [ReorderModalComponent, SupplierNamePipe],
declarations: [ReorderModalComponent, SupplierNamePipe],

View File

@@ -63,7 +63,7 @@
<div class="price">
{{ item.catalogAvailability?.price?.value?.value | currency: item.catalogAvailability?.price?.value?.currency:'code' }}
</div>
<div>{{ store.promotionPoints$ | async }} Lesepunkte</div>
<div *ngIf="store.promotionPoints$ | async; let promotionPoints">{{ promotionPoints }} Lesepunkte</div>
</div>
</div>

View File

@@ -161,7 +161,7 @@
}
.product-description {
@apply flex flex-col flex-grow justify-center px-5 py-5;
@apply flex flex-col flex-grow px-5 py-5;
.info {
@apply whitespace-pre-line;

View File

@@ -7,7 +7,7 @@ import { UiFilterAutocompleteProvider, UiFilterScanProvider } from '@ui/filter';
import { UiFilter } from 'apps/ui/filter/src/lib/next';
import { NativeContainerService } from 'native-container';
import { Observable, Subscription } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { filter, first, map } from 'rxjs/operators';
import { ArticleSearchService } from './article-search.store';
import { FocusSearchboxEvent } from './focus-searchbox.event';
import { ArticleSearchMainAutocompleteProvider, ArticleSearchMainScanProviderService } from './providers';
@@ -61,6 +61,7 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
) {}
ngOnInit() {
this.removeBreadcrumbs();
this.subscriptions.add(
this.application.activatedProcessId$.subscribe((processId) => {
this.setBreadcrumb(processId);
@@ -88,6 +89,16 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
}
}
async removeBreadcrumbs() {
const checkoutCrumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.application.activatedProcessId, ['checkout'])
.pipe(first())
.toPromise();
checkoutCrumbs.forEach(async (crumb) => {
await this.breadcrumb.removeBreadcrumb(crumb.id, true);
});
}
async setBreadcrumb(processId: number) {
await this.breadcrumb.addBreadcrumbIfNotExists({
key: processId,

View File

@@ -74,7 +74,7 @@ ui-searchbox-autocomplete {
}
.info-tooltip-button {
@apply border-font-customer bg-white rounded-md text-base font-bold;
@apply border-font-customer bg-white rounded-md text-base font-bold text-black;
border-style: outset;
width: 31px;
height: 31px;

View File

@@ -55,10 +55,10 @@ ui-filter-input-group-main {
@apply list-none pb-px-15;
button {
@apply flex flex-row items-center outline-none border-none bg-white text-base m-0 p-0;
@apply flex flex-row items-center outline-none border-none bg-white text-black text-base m-0 p-0;
ui-icon {
@apply flex w-px-35 h-px-35 justify-center items-center mr-3 rounded-full;
@apply flex w-px-35 h-px-35 justify-center items-center mr-3 rounded-full text-black;
background-color: #e6eff9;
}

View File

@@ -0,0 +1,20 @@
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
import { NativeContainerService } from 'native-container';
import { CheckoutDummyScanProvider } from './checkout-dummy-scan.provider';
describe('GoodsInSearchMainScanProvider', () => {
let spectator: SpectatorService<CheckoutDummyScanProvider>;
const naticeContainerMock = jasmine.createSpyObj<NativeContainerService>(['openScanner', 'isUiWebview']);
const createProvider = createServiceFactory({
service: CheckoutDummyScanProvider,
});
beforeEach(() => {
spectator = createProvider();
});
it('should create', () => {
expect(spectator.service).toBeTruthy();
});
});

View File

@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { UiFilterScanProvider } from '@ui/filter';
import { NativeContainerService } from 'native-container';
import { Observable } from 'rxjs';
import { catchError, filter, map } from 'rxjs/operators';
@Injectable()
export class CheckoutDummyScanProvider extends UiFilterScanProvider {
for = 'dummy';
constructor(private nativeContainer: NativeContainerService) {
super();
}
scan(): Observable<string> {
return this.nativeContainer.openScanner('scanBook').pipe(
filter((result) => result.status !== 'IN_PROGRESS'),
map((result) => result?.data),
catchError((err) => {
return '';
})
);
}
}

View File

@@ -0,0 +1,98 @@
<div class="wrapper">
<div class="header">
<div class="headline">
<h1 class="title">
Neuanlage
</h1>
<p class="paragraph">
Hier können Sie Artikel manuell<br />
oder per Dummy anlegen.
</p>
</div>
</div>
<form *ngIf="control" [formGroup]="control" (submit)="submit()">
<ui-form-control class="searchbox-control" label="EAN/ISBN">
<ui-searchbox
formControlName="ean"
[query]="query$ | async"
(queryChange)="setQuery($event)"
(search)="search($event)"
(scan)="search($event)"
[loading]="loading$ | async"
[scanProvider]="scanProvider"
[hint]="message$ | async"
tabindex="0"
>
</ui-searchbox>
</ui-form-control>
<ui-form-control label="Titel" requiredMark="*">
<input tabindex="0" uiInput formControlName="name" />
</ui-form-control>
<div class="control-row">
<ui-form-control label="Menge" requiredMark="*">
<input tabindex="0" uiInput formControlName="quantity" />
</ui-form-control>
<ui-form-control class="datepicker" label="vsl. Lieferdatum" requiredMark="*">
<button
tabindex="-1"
class="date-btn"
type="button"
[class.content-selected]="!!(estimatedShippingDate$ | async)"
[uiOverlayTrigger]="uiDatepicker"
#datepicker="uiOverlayTrigger"
>
<strong>
{{ estimatedShippingDate$ | async | date: 'dd.MM.yy' }}
</strong>
<ui-icon icon="arrow_head" class="dp-button-icon" size="20px" [rotate]="datepicker.opened ? '270deg' : '90deg'"> </ui-icon>
</button>
<ui-datepicker
formControlName="estimatedShippingDate"
#uiDatepicker
yPosition="below"
xPosition="after"
[min]="minDate"
[disabledDaysOfWeek]="[0]"
[selected]="estimatedShippingDate$ | async"
(save)="changeEstimatedShippingDate($event); uiDatepicker.close()"
>
<ng-container #content>Übernehmen</ng-container>
</ui-datepicker>
</ui-form-control>
</div>
<ui-form-control label="Autor">
<input tabindex="0" uiInput formControlName="contributors" />
</ui-form-control>
<ui-form-control label="Verlag">
<input tabindex="0" uiInput formControlName="manufacturer" />
</ui-form-control>
<ui-form-control label="Lieferant" requiredMark="*">
<ui-select tabindex="-1" formControlName="supplier">
<ui-select-option *ngFor="let supplier of suppliers$ | async" [label]="supplier.name" [value]="supplier.id"></ui-select-option>
</ui-select>
</ui-form-control>
<div class="control-row">
<ui-form-control class="price" label="Stückpreis" [suffix]="price.value ? '' : ''" requiredMark="*">
<input tabindex="0" #price uiInput formControlName="price" />
</ui-form-control>
<ui-form-control label="MwSt" requiredMark="*">
<ui-select tabindex="-1" formControlName="vat">
<ui-select-option *ngFor="let vat of vats$ | async" [label]="vat.name + '%'" [value]="vat.vatType"></ui-select-option>
</ui-select>
</ui-form-control>
</div>
<div class="actions">
<button
class="cta-secondary"
(click)="nextItem()"
[disabled]="control.invalid || control.disabled || (loading$ | async)"
type="button"
>
Weitere Artikel hinzufügen
</button>
<button class="cta-primary" [disabled]="control.invalid || control.disabled || (loading$ | async)" type="submit">
Bestellung anlegen
</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,110 @@
:host {
@apply block overflow-y-scroll;
max-height: calc(100vh - 135px - 80px);
}
.wrapper {
@apply block bg-white shadow-card rounded-card pt-6 pb-10 mb-40;
.header {
@apply text-right;
.headline {
@apply text-center;
.title {
@apply text-2xl text-page-heading font-bold;
}
.paragraph {
@apply text-2xl mt-1 mb-0;
}
}
}
form {
@apply px-20;
.control-row {
@apply flex flex-row gap-8;
ui-form-control {
width: 50%;
}
}
ui-searchbox {
@apply mt-4 shadow-none border-0 border-solid rounded-none;
border-bottom-width: 2px;
border-color: #e1ebf5;
}
.datepicker {
@apply border-0 border-solid justify-end;
border-bottom-width: 2px;
border-color: #e1ebf5;
.date-btn {
@apply flex flex-row-reverse items-center p-0 mr-1 w-full text-right outline-none border-none bg-transparent text-lg;
}
.content-selected {
@apply flex-row justify-end;
}
.dp-button-icon {
@apply inline-flex ml-2 text-inactive-customer;
transition: transform 200ms ease-in-out;
}
}
.actions {
@apply absolute flex items-center justify-center left-0 right-0 bottom-0;
margin-bottom: 18px;
}
.cta-primary {
@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-inactive-customer;
}
}
.cta-secondary {
@apply bg-white text-brand border-brand font-bold text-lg outline-none border-solid border-2 rounded-full px-6 py-3 mr-4;
&:disabled {
@apply bg-inactive-customer border-inactive-customer text-white;
}
}
}
}
::ng-deep page-checkout-dummy ui-select ui-icon {
@apply text-inactive-customer;
}
::ng-deep page-checkout-dummy ui-select .ui-icon {
@apply w-px-20 h-px-20;
}
::ng-deep page-checkout-dummy .datepicker .input-wrapper {
flex-grow: 0 !important;
}
::ng-deep page-checkout-dummy ui-searchbox .ui-searchbox-input {
@apply pl-0 !important;
}
::ng-deep page-checkout-dummy ui-searchbox .hint {
@apply mr-10 !important;
}
::ng-deep page-checkout-dummy .price .suffix {
margin-top: 7.5px;
}
::ng-deep page-checkout-dummy .searchbox-control .input-wrapper {
@apply block;
}

View File

@@ -0,0 +1,268 @@
import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, Optional } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { UiFilterScanProvider } from '@ui/filter';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { Subject } from 'rxjs';
import { first, shareReplay, takeUntil } from 'rxjs/operators';
import { threadId } from 'worker_threads';
import { CheckoutDummyScanProvider } from './checkout-dummy-scan.provider';
import { CheckoutDummyStore } from './checkout-dummy.store';
@Component({
selector: 'page-checkout-dummy',
templateUrl: 'checkout-dummy.component.html',
styleUrls: ['checkout-dummy.component.scss'],
providers: [
CheckoutDummyStore,
{
provide: UiFilterScanProvider,
useClass: CheckoutDummyScanProvider,
multi: true,
},
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutDummyComponent implements OnInit, OnDestroy {
// Form
control: FormGroup;
// Searchbox
query$ = this._store.query$;
message$ = this._store.message$;
loading$ = this._store.fetching$.pipe(shareReplay());
private _scanProvider: UiFilterScanProvider;
get scanProvider() {
return this._scanProvider;
}
// Datepicker
minDate = this._dateAdapter.addCalendarDays(new Date(), -1);
estimatedShippingDate$ = this._store.estimatedShippingDate$.pipe(shareReplay());
// Dropdowns
vats$ = this._store.vats$;
suppliers$ = this._store.suppliers$;
params: Params;
_onDestroy$ = new Subject<void>();
constructor(
@Inject(UiFilterScanProvider) @Optional() private scanProviders: UiFilterScanProvider[],
private _router: Router,
private _route: ActivatedRoute,
private _application: ApplicationService,
private _breadcrumb: BreadcrumbService,
private _fb: FormBuilder,
private _dateAdapter: DateAdapter,
private _modal: UiModalService,
private _store: CheckoutDummyStore
) {}
ngOnInit() {
this._scanProvider = this.scanProviders?.find((provider) => provider.for === 'dummy');
this.initForm();
this.updateBreadcrumb();
this._store.item$.pipe(takeUntil(this._onDestroy$)).subscribe((item) => {
if (item) {
this.clearForm();
this.populateForm(item);
}
});
const params = this._route.snapshot.queryParams;
if (Object.keys(params).length !== 0) {
this.params = params;
const item = {
ean: params.ean || '',
name: params.name || '',
quantity: params.quantity || '',
estimatedShippingDate: params.estimatedShippingDate || this._dateAdapter.today().toISOString(),
contributors: params.contributors || '',
manufacturer: params.manufacturer || '',
supplier: params.supplier || 5,
price: params.price || '',
vat: params.vat || '',
};
this.populateFormFromParams(item);
}
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
}
async updateBreadcrumb() {
await this._breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: this._application.activatedProcessId,
name: 'Neuanlage',
path: '/cart/dummy',
tags: ['checkout', 'cart', 'dummy'],
section: 'customer',
});
}
setQuery(ean: string) {
if (ean) {
this._store.query = ean;
}
}
search(ean: string) {
this.setQuery(ean);
this._store.search();
}
changeEstimatedShippingDate(date: Date) {
if (!date) {
return;
}
this._store.estimatedShippingDate = date.toISOString();
}
initForm() {
const fb = this._fb;
this.control = fb.group({
ean: fb.control('', Validators.pattern('^[0-9]{13}$')),
name: fb.control('', Validators.required),
quantity: fb.control('', [Validators.required, Validators.pattern('^[0-9]*$'), Validators.min(1)]),
estimatedShippingDate: fb.control(this._dateAdapter.today().toISOString(), Validators.required),
contributors: fb.control(''),
manufacturer: fb.control(''),
supplier: fb.control(5, Validators.required), // 5 === Dummy
price: fb.control('', [Validators.required, Validators.pattern(/^\d+([\,]\d{1,2})?$/)]),
vat: fb.control('', Validators.required),
});
this.changeEstimatedShippingDate(this._dateAdapter.today()); // Update View
}
populateForm(item: ItemDTO) {
this.control.get('ean').setValue(item?.product?.ean);
this.control.get('name').setValue(item?.product?.name);
this.control.get('contributors').setValue(item?.product?.contributors);
this.control.get('manufacturer').setValue(item?.product?.manufacturer);
this.control
.get('price')
.setValue(
item?.catalogAvailability?.price?.value?.value ? String(item?.catalogAvailability?.price?.value?.value).replace('.', ',') : ''
);
this.control.get('vat').setValue(item?.catalogAvailability?.price?.vat?.vatType);
}
populateFormFromParams(item: any) {
this.control.get('name').setValue(item.name);
this.control.get('contributors').setValue(item.contributors);
this.control.get('manufacturer').setValue(item.manufacturer);
this.control.get('price').setValue(item.price ? String(item.price).replace('.', ',') : '');
this.control.get('vat').setValue(Number(item.vat));
this.control.get('quantity').setValue(item.quantity);
this.control.get('ean').setValue(item.ean);
this.control.get('supplier').setValue(Number(item.supplier));
this.control.get('estimatedShippingDate').setValue(item.estimatedShippingDate);
this.changeEstimatedShippingDate(new Date(item.estimatedShippingDate)); // Update View
}
clearForm(withEan?: boolean) {
if (withEan) {
this.control.reset();
} else {
Object.keys(this.control.controls).forEach((key) => {
if (key !== 'ean') {
this.control.get(key).reset();
}
});
}
this.control.get('supplier').setValue(5);
this.control.get('estimatedShippingDate').setValue(this._dateAdapter.today().toISOString());
this.changeEstimatedShippingDate(this._dateAdapter.today());
this.control.markAsUntouched();
}
async nextItem() {
if (this.control.invalid || this.control.disabled) {
this.control.enable();
return;
}
this.control.disable();
try {
const branch = await this._store.currentBranch$.pipe(first()).toPromise();
if (!this.params) {
await this._store.createAddToCartItem(this.control, branch);
this._store.addToCart(() => {});
} else {
await this._store.createAddToCartItem(this.control, branch, true);
this._store.updateCart(() => {});
}
} catch (error) {
this._modal.open({
title: 'Bestellung konnte nicht angelegt werden',
content: UiErrorModalComponent,
data: error,
});
console.error(error);
}
this.clearForm(true);
this.control.enable();
}
async submit() {
if (this.control.invalid || this.control.disabled) {
this.control.enable();
return;
}
this.control.disable();
try {
const branch = await this._store.currentBranch$.pipe(first()).toPromise();
if (!this.params) {
await this._store.createAddToCartItem(this.control, branch);
this._store.addToCart(async () => {
// Set filter for navigation to customer search if customer is not set
const customer = await this._store.customer$.pipe(first()).toPromise();
const customerFilter = await this._store.customerFilter$.pipe(first()).toPromise();
let filter: { [key: string]: string };
if (!customer) {
filter = customerFilter;
this._router.navigate(['/customer', 'search'], { queryParams: { customertype: filter.customertype } });
} else {
this._router.navigate(['/cart', 'review']);
}
});
} else {
await this._store.createAddToCartItem(this.control, branch, true);
this._store.updateCart(async () => {
// Set filter for navigation to customer search if customer is not set
const customer = await this._store.customer$.pipe(first()).toPromise();
const customerFilter = await this._store.customerFilter$.pipe(first()).toPromise();
let filter: { [key: string]: string };
if (!customer) {
filter = customerFilter;
this._router.navigate(['/customer', 'search'], { queryParams: { customertype: filter.customertype } });
} else {
this._router.navigate(['/cart', 'review']);
}
});
}
} catch (error) {
this._modal.open({
title: 'Bestellung konnte nicht angelegt werden',
content: UiErrorModalComponent,
data: error,
});
console.error(error);
}
this.clearForm();
this.control.enable();
}
}

View File

@@ -0,0 +1,33 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { UiCommonModule } from '@ui/common';
import { UiDatepickerModule } from '@ui/datepicker';
import { UiDropdownModule } from '@ui/dropdown';
import { UiFormControlModule } from '@ui/form-control';
import { UiIconModule } from '@ui/icon';
import { UiInputModule } from '@ui/input';
import { UiSearchboxNextModule } from '@ui/searchbox';
import { UiSelectModule } from '@ui/select';
import { CheckoutDummyComponent } from './checkout-dummy.component';
@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
UiIconModule,
UiDatepickerModule,
UiDropdownModule,
UiSelectModule,
UiCommonModule,
UiFormControlModule,
UiInputModule,
UiSearchboxNextModule,
],
exports: [CheckoutDummyComponent],
declarations: [CheckoutDummyComponent],
providers: [],
})
export class CheckoutDummyModule {}

View File

@@ -0,0 +1,393 @@
import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCatalogService } from '@domain/catalog';
import { DomainCheckoutService } from '@domain/checkout';
import { DomainOmsService } from '@domain/oms';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ItemDTO } from '@swagger/cat';
import { AddToShoppingCartDTO, AvailabilityDTO, BranchDTO, DestinationDTO, PriceDTO, ProductDTO, PromotionDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { Observable } from 'rxjs';
import { first, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
interface CheckoutDummyState {
item: ItemDTO;
shoppingCartItemId: number;
addToCartItem: AddToShoppingCartDTO;
estimatedShippingDate: string;
message: string;
query: string;
fetching: boolean;
}
@Injectable()
export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
get item() {
return this.get((s) => s.item);
}
set item(item: ItemDTO) {
if (this.item !== item) {
this.patchState({ item });
}
}
readonly item$ = this.select((s) => s.item);
get shoppingCartItemId() {
return this.get((s) => s.shoppingCartItemId);
}
set shoppingCartItemId(shoppingCartItemId: number) {
if (this.shoppingCartItemId !== shoppingCartItemId) {
this.patchState({ shoppingCartItemId });
}
}
readonly shoppingCartItemId$ = this.select((s) => s.shoppingCartItemId);
get addToCartItem() {
return this.get((s) => s.addToCartItem);
}
set addToCartItem(addToCartItem: AddToShoppingCartDTO) {
if (this.addToCartItem !== addToCartItem) {
this.patchState({ addToCartItem });
}
}
readonly addToCartItem$ = this.select((s) => s.addToCartItem);
get query() {
return this.get((s) => s.query);
}
set query(query: string) {
if (this.query !== query) {
this.patchState({ query });
}
}
readonly query$ = this.select((s) => s.query);
get fetching() {
return this.get((s) => s.fetching);
}
set fetching(fetching: boolean) {
this.patchState({ fetching });
}
readonly fetching$ = this.select((s) => s.fetching);
get message() {
return this.get((s) => s.message);
}
set message(message: string) {
this.patchState({ message });
}
readonly message$ = this.select((s) => s.message);
get estimatedShippingDate() {
return this.get((s) => s.message);
}
set estimatedShippingDate(estimatedShippingDate: string) {
this.patchState({ estimatedShippingDate });
}
readonly processId$ = this._application.activatedProcessId$;
readonly customer$ = this.processId$.pipe(switchMap((processId) => this._checkoutService.getBuyer({ processId })));
readonly customerFeatures$ = this.processId$.pipe(switchMap((processId) => this._checkoutService.getCustomerFeatures({ processId })));
readonly customerFilter$ = this.customerFeatures$.pipe(
withLatestFrom(this.processId$),
switchMap(([customerFeatures, processId]) => this._checkoutService.canSetCustomer({ processId, customerFeatures })),
map((res) => res.filter)
);
readonly estimatedShippingDate$ = this.select((s) => s.estimatedShippingDate);
readonly vats$ = this._omsService.getVATs();
readonly suppliers$ = this._availabilityService.getSuppliers().pipe(
map((suppliers) =>
suppliers
.filter((supplier) => {
const displaySupplierIds = [2, 3, 4, 5, 6, 8, 10, 13, 15, 16];
return displaySupplierIds.find((id) => id === supplier.id);
})
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
)
);
readonly currentBranch$ = this._availabilityService.getCurrentBranch();
constructor(
private _application: ApplicationService,
private _omsService: DomainOmsService,
private _checkoutService: DomainCheckoutService,
private _availabilityService: DomainAvailabilityService,
private _catalogService: DomainCatalogService,
private _modal: UiModalService
) {
super({
item: undefined,
shoppingCartItemId: undefined,
addToCartItem: undefined,
estimatedShippingDate: '',
query: '',
message: '',
fetching: false,
});
}
search = this.effect(($) =>
$.pipe(
tap((_) => this.patchState({ fetching: true })),
withLatestFrom(this.query$),
switchMap(([_, ean]) =>
this.searchRequest(ean).pipe(
tapResponse(
(res) => {
const item = res.result[0];
if (!!item && item?.product?.format !== 'EB' && item?.product?.format !== 'DL') {
this.patchState({
item: res.result[0],
message: '',
fetching: false,
});
} else {
this.patchState({
item: undefined,
message: 'Keine Suchergebnisse',
fetching: false,
});
}
},
(error: Error) => {
this._modal.open({
title: 'Fehler bei der Suche nach der EAN',
content: UiErrorModalComponent,
data: error,
});
this.patchState({ fetching: false, message: '' });
console.error('CheckoutDummyStore.search()', error);
}
)
)
)
)
);
addToCart = this.effect((cb$: Observable<Function>) =>
cb$.pipe(
tap((_) => this.patchState({ fetching: true })),
withLatestFrom(this.processId$, this.addToCartItem$),
switchMap(([cb, processId, newItem]) =>
this.addToCartRequest(processId, newItem).pipe(
tapResponse(
(res) => {
this.patchState({
fetching: false,
});
cb?.call(undefined);
},
(error: Error) => {
this._modal.open({
title: 'Fehler beim Hinzufügen zum Warenkorb',
content: UiErrorModalComponent,
data: error,
});
this.patchState({ fetching: false });
console.error('CheckoutDummyStore.addToCart()', error);
}
)
)
)
)
);
updateCart = this.effect((cb$: Observable<Function>) =>
cb$.pipe(
tap((_) => this.patchState({ fetching: true })),
withLatestFrom(this.processId$, this.addToCartItem$, this.shoppingCartItemId$),
switchMap(([cb, processId, newItem, shoppingCartItemId]) => {
const availability = newItem.availability;
const quantity = newItem.quantity;
const destination = newItem.destination;
return this.updateCartRequest({ processId, shoppingCartItemId, availability, quantity, destination }).pipe(
tapResponse(
(res) => {
this.patchState({
fetching: false,
});
cb?.call(undefined);
},
(error: Error) => {
this._modal.open({
title: 'Fehler beim Updaten des Warenkorbs',
content: UiErrorModalComponent,
data: error,
});
this.patchState({ fetching: false });
console.error('CheckoutDummyStore.updateCart()', error);
}
)
);
})
)
);
searchRequest(ean: string) {
const queryToken = {
input: {
qs: ean,
},
take: 1,
doNotTrack: true,
};
return this._catalogService.search({ queryToken });
}
addToCartRequest(processId: number, newItem: AddToShoppingCartDTO) {
return this._checkoutService.addItemToShoppingCart({ processId, items: [newItem] });
}
updateCartRequest({
processId,
shoppingCartItemId,
availability,
quantity,
destination,
}: {
processId: number;
shoppingCartItemId: number;
availability: AvailabilityDTO;
quantity: number;
destination: DestinationDTO;
}) {
return this._checkoutService.updateItemInShoppingCart({
processId,
shoppingCartItemId,
update: {
availability,
quantity,
destination,
},
});
}
async createAddToCartItem(control: FormGroup, branch: BranchDTO, update?: boolean) {
let item: ItemDTO;
const quantity = Number(control.get('quantity').value);
const price = this._createPriceDTO(control);
let promoPoints: number;
// Check if item exists or ean inside the control changed in the meantime
if (!!this.item && this.item.product.ean === control.get('ean').value) {
item = this.item;
promoPoints = await this._getPromoPoints({ itemId: item.id, quantity, price: price.value.value });
} else {
item = undefined;
}
const availability = this._createAvailabilityDTO({ price, control });
const product = this._createProductDTO({ item, control });
const newItem: AddToShoppingCartDTO = {
quantity,
availability,
product,
promotion: !!item ? { points: promoPoints } : undefined,
destination: {
data: { target: 1, targetBranch: { id: branch.id } },
},
};
if (update) {
const shoppingCart = await this._checkoutService
.getShoppingCart({ processId: this._application.activatedProcessId })
.pipe(first())
.toPromise();
const existingItem = shoppingCart?.items?.find(
(i) => i?.data?.product?.ean === i?.data?.product?.ean && i?.data?.features['orderType'] === 'Abholung'
);
this.patchState({ addToCartItem: newItem, shoppingCartItemId: existingItem?.id });
}
this.patchState({ addToCartItem: newItem });
}
private async _getPromoPoints({ itemId, quantity, price }: { itemId: number; quantity: number; price: number }) {
let points: number;
try {
points = await this._catalogService
.getPromotionPoints({
items: [
{
id: itemId,
quantity,
price,
},
],
})
.pipe(
first(),
map((response) => response.result.itemId)
)
.toPromise();
} catch (error) {
this._modal.open({
title: 'Fehler beim Abfragen der Lesepunkte',
content: UiErrorModalComponent,
data: error,
});
console.error('CheckoutDummyStore._getPromoPoints()', error);
}
return points;
}
private _createPriceDTO(control: FormGroup): PriceDTO {
return {
value: {
value: Number(String(control.get('price').value).replace(',', '.')),
currency: 'EUR',
},
vat: {
vatType: control.get('vat').value || null,
},
};
}
private _createAvailabilityDTO({ price, control }: { price: PriceDTO; control: FormGroup }): AvailabilityDTO {
return {
availabilityType: 1024,
supplier: {
id: control.get('supplier').value,
},
price,
estimatedShippingDate: control.get('estimatedShippingDate').value || null,
supplyChannel: 'MANUALLY',
};
}
private _createProductDTO({ item, control }: { item?: ItemDTO; control: FormGroup }): ProductDTO {
const formValues: Partial<ProductDTO> = {
ean: control.get('ean').value,
name: control.get('name').value,
contributors: control.get('contributors').value,
manufacturer: control.get('manufacturer').value,
};
return !!item
? {
catalogProductNumber: String(item.id),
...item.product,
...formValues,
}
: {
catalogProductNumber: '',
...formValues,
};
}
}

View File

@@ -13,7 +13,8 @@
</p>
<div class="btn-wrapper">
<a class="cta-article" [routerLink]="['/product', 'search']">Artikel suchen</a>
<a class="cta-primary" [routerLink]="['/product', 'search']">Artikel suchen</a>
<a class="cta-secondary" [routerLink]="['/cart', 'dummy']">Neuanlage</a>
</div>
</div>
</div>
@@ -60,6 +61,7 @@
<ng-container *ngFor="let group of groupedItems$ | async; let lastGroup = last">
<div class="row item-group-header">
<ui-icon
*ngIf="group.orderType !== 'Dummy'"
class="icon-order-type"
[size]="group.orderType === 'B2B-Versand' ? '50px' : '25px'"
[icon]="
@@ -76,8 +78,9 @@
: 'truck'
"
></ui-icon>
<div class="label">
{{ group.orderType }}
<div class="label" [class.dummy]="group.orderType === 'Dummy'">
{{ group.orderType !== 'Dummy' ? group.orderType : 'Manuelle Anlage / Dummy Bestellung' }}
<a *ngIf="group.orderType === 'Dummy'" class="cta-secondary" [routerLink]="['/cart', 'dummy']">Hinzufügen</a>
</div>
</div>
<ng-container *ngIf="group.orderType === 'Versand' || group.orderType === 'B2B-Versand' || group.orderType === 'DIG-Versand'">
@@ -126,24 +129,50 @@
<img class="book-icon" [src]="'/assets/images/Icon_' + item?.product?.format + '.svg'" alt="book-icon" />
{{ item?.product?.manufacturer + ' | ' + item?.product?.contributors | trim: 30 }}
</div>
<div *ngIf="group.orderType !== 'Rücklage' && group.orderType !== 'Download'" class="product-delivery">
<div
*ngIf="group.orderType !== 'Rücklage' && group.orderType !== 'Download' && group.orderType !== 'Dummy'"
class="product-delivery"
>
{{ group.orderType }} {{ group.orderType === 'Abholung' ? 'ab' : '' }}
{{ item?.availability?.estimatedShippingDate | date }}
</div>
<div *ngIf="group.orderType === 'Dummy'" class="product-delivery">
Abholung {{ group.orderType === 'Dummy' ? 'ab' : '' }}
{{ item?.availability?.estimatedShippingDate ? (item?.availability?.estimatedShippingDate | date) : '-' }}
</div>
</div>
<div class="product-price">
{{ item?.unitPrice?.value?.value | currency: item?.unitPrice?.value?.currency:'code' }}
</div>
<div class="product-quantity">
<ui-quantity-dropdown
*ngIf="group.orderType !== 'Dummy'; else quantityDummy"
[ngModel]="item?.quantity"
(ngModelChange)="updateItemQuantity(item, $event)"
[showSpinner]="showQuantityControlSpinnerItemId === item.id"
>
</ui-quantity-dropdown>
<ng-template #quantityDummy>
{{ item?.quantity }}
</ng-template>
</div>
<div>
<button class="cta-edit" (click)="changeItem(item)" [disabled]="showChangeButtonSpinnerItemId">
<div class="product-actions">
<button
*ngIf="group.orderType === 'Dummy'"
class="cta-edit"
(click)="changeDummyItem(item)"
[disabled]="showChangeButtonSpinnerItemId"
>
<ui-spinner [show]="showChangeButtonSpinnerItemId === item.id">
Ändern
</ui-spinner>
</button>
<button
*ngIf="group.orderType !== 'Download' && group.orderType !== 'Dummy'"
class="cta-edit"
(click)="changeItem(item)"
[disabled]="showChangeButtonSpinnerItemId"
>
<ui-spinner [show]="showChangeButtonSpinnerItemId === item.id">
Ändern
</ui-spinner>
@@ -170,7 +199,7 @@
<span class="shipping-cost-info">ohne Versandkosten</span>
</div>
<button
class="cta-order"
class="cta-primary"
(click)="order()"
[disabled]="showOrderButtonSpinner || specialCommentIsDirty"
[class.special-comment-dirty]="specialCommentIsDirty"

View File

@@ -32,7 +32,7 @@
}
.btn-wrapper {
@apply mt-px-40 text-center;
@apply mt-px-40 flex flex-col items-center;
}
a {
@@ -62,9 +62,12 @@
@apply bg-transparent text-brand font-bold text-xl outline-none border-none;
}
.cta-order,
.cta-article {
@apply bg-brand text-white font-bold text-lg outline-none border-none rounded-full px-6 py-3;
.cta-primary {
@apply bg-brand text-white font-bold text-lg outline-none border-brand border-solid border-2 rounded-full px-6 py-3;
}
.cta-secondary {
@apply bg-white text-brand border-brand font-bold text-lg outline-none px-6 py-3 mt-4;
}
.cta-order.special-comment-dirty {
@@ -101,6 +104,15 @@ hr {
width: 200px;
}
.dummy {
@apply w-full flex justify-between items-center;
.cta-secondary {
@apply mt-0 pr-0;
text-decoration: none;
}
}
.icon-order-type {
@apply text-font-customer mr-3;
}
@@ -158,6 +170,10 @@ hr {
height: 80px;
}
.product-actions {
min-width: 80px;
}
.footer {
@apply absolute bottom-0 left-0 right-0 p-7;
box-shadow: 0px -2px 24px 0px #dce2e9;

View File

@@ -1,9 +1,9 @@
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import { AvailabilityDTO, BranchDTO, DestinationDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { AvailabilityDTO, DestinationDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
import { PrintModalData, PrintModalComponent } from '@modal/printer';
import { first, map, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
@@ -14,8 +14,6 @@ import { Subject, NEVER } from 'rxjs';
import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainPrinterService } from '@domain/printer';
import { HttpErrorResponse } from '@angular/common/http';
import { ModalAvailabilitiesComponent } from '@modal/availabilities';
@Component({
selector: 'page-checkout-review',
@@ -23,9 +21,8 @@ import { ModalAvailabilitiesComponent } from '@modal/availabilities';
styleUrls: ['checkout-review.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutReviewComponent implements OnDestroy {
export class CheckoutReviewComponent implements OnInit {
private _orderCompleted = new Subject<void>();
private _onDestroy$ = new Subject<void>();
shoppingCart$ = this.applicationService.activatedProcessId$.pipe(
takeUntil(this._orderCompleted),
@@ -53,11 +50,17 @@ export class CheckoutReviewComponent implements OnDestroy {
map((items) =>
items.reduce((grouped, item) => {
// let index = grouped.findIndex((g) => g?.orderType === item?.features?.orderType && g.destination?.id === item?.destination?.id);
let index = grouped.findIndex((g) => g?.orderType === item?.features?.orderType);
let index = grouped.findIndex((g) => g?.orderType === 'Dummy' || g?.orderType === item?.features?.orderType);
let group = index !== -1 ? grouped[index] : undefined;
if (!group) {
group = { orderType: item.features.orderType, destination: item.destination?.data, items: [] };
group = {
orderType: item?.availability?.supplyChannel === 'MANUALLY' ? 'Dummy' : item.features.orderType,
destination: item.destination?.data,
items: [],
};
}
group.items = [...group.items, item]?.sort((a, b) => a.destination?.data?.targetBranch?.id - b.destination?.data?.targetBranch?.id);
if (index !== -1) {
@@ -90,15 +93,26 @@ export class CheckoutReviewComponent implements OnDestroy {
if (displayOrders.length === 0) {
return NEVER;
}
return this.domainCatalogService
.getPromotionPoints({
items: displayOrders.map((i) => ({
id: Number(i.product?.catalogProductNumber),
quantity: i.quantity,
price: i.unitPrice?.value?.value,
})),
const items = displayOrders
.map((i) => {
if (i?.product?.catalogProductNumber) {
return {
id: Number(i.product?.catalogProductNumber),
quantity: i.quantity,
price: i.unitPrice?.value?.value,
};
}
})
.pipe(map((response) => Object.values(response.result).reduce((sum, points) => sum + points, 0)));
.filter((item) => item !== undefined);
if (items.length !== 0) {
return this.domainCatalogService
.getPromotionPoints({
items,
})
.pipe(map((response) => Object.values(response.result).reduce((sum, points) => sum + points, 0)));
} else {
return NEVER;
}
})
);
@@ -127,27 +141,47 @@ export class CheckoutReviewComponent implements OnDestroy {
private domainCatalogService: DomainCatalogService,
private breadcrumb: BreadcrumbService,
private domainPrinterService: DomainPrinterService
) {
this.breadcrumb
.getBreadcrumbsByKeyAndTag$(this.applicationService.activatedProcessId, 'checkout')
.pipe(first())
.subscribe(async (crumbs) => {
for await (const crumb of crumbs) {
this.breadcrumb.removeBreadcrumb(crumb.id);
}
this.breadcrumb.addBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
name: 'Warenkorb',
path: '/cart/review',
tags: ['checkout', 'cart'],
section: 'customer',
});
});
) {}
async ngOnInit() {
await this.removeBreadcrumbs();
await this.updateBreadcrumb();
}
ngOnDestroy(): void {
this._onDestroy$.next();
this._onDestroy$.complete();
async updateBreadcrumb() {
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
name: 'Warenkorb',
path: '/cart/review',
tags: ['checkout', 'cart'],
section: 'customer',
});
}
async removeBreadcrumbs() {
const checkoutDummyCrumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['checkout', 'cart', 'dummy'])
.pipe(first())
.toPromise();
checkoutDummyCrumbs.forEach(async (crumb) => {
await this.breadcrumb.removeBreadcrumb(crumb.id, true);
});
}
changeDummyItem(shoppingCartItem: ShoppingCartItemDTO) {
this.router.navigate(['/cart', 'dummy'], {
queryParams: {
price: shoppingCartItem?.availability?.price?.value?.value,
vat: shoppingCartItem?.availability?.price?.vat?.vatType,
supplier: shoppingCartItem?.availability?.supplier?.id,
estimatedShippingDate: shoppingCartItem?.estimatedShippingDate,
manufacturer: shoppingCartItem?.product?.manufacturer,
name: shoppingCartItem?.product?.name,
contributors: shoppingCartItem?.product?.contributors,
ean: shoppingCartItem?.product?.ean,
quantity: shoppingCartItem?.quantity,
},
});
}
async changeItem(shoppingCartItem: ShoppingCartItemDTO) {

View File

@@ -105,7 +105,9 @@
<div class="footer">
<div class="overview">
<span class="promotion-points">{{ totalItemCount$ | async }} Artikel | {{ totalReadingPoints$ | async }} Lesepunkte</span>
<span *ngIf="totalReadingPoints$ | async; let totalReadingPoints" class="promotion-points"
>{{ totalItemCount$ | async }} Artikel | {{ totalReadingPoints }} Lesepunkte</span
>
<div class="price-wrapper">
<div class="total-price">Gesamtsumme {{ totalPrice$ | async | currency: ' ' }} {{ totalPriceCurrency$ | async }}</div>

View File

@@ -140,12 +140,12 @@
box-shadow: 0px -2px 24px 0px #dce2e9;
.overview {
@apply flex flex-row items-center justify-between;
@apply flex flex-row items-center justify-end;
width: 95%;
}
.promotion-points {
@apply text-ucla-blue text-regular font-bold;
@apply text-ucla-blue text-regular font-bold flex-grow;
}
.total-price {

View File

@@ -11,6 +11,7 @@ import { DisplayOrderItemDTO } from '@swagger/oms';
import { BreadcrumbService } from '@core/breadcrumb';
import { ApplicationService } from '@core/application';
import { DomainPrinterService } from '@domain/printer';
import { NEVER } from 'rxjs';
@Component({
selector: 'page-checkout-summary',
@@ -31,19 +32,27 @@ export class CheckoutSummaryComponent {
);
totalReadingPoints$ = this.displayOrders$.pipe(
switchMap((displayOrders) =>
this.domainCatalogService
.getPromotionPoints({
items: displayOrders
.reduce<DisplayOrderItemDTO[]>((items, order) => [...items, ...order.items], [])
.map((i) => ({
switchMap((displayOrders) => {
const items = displayOrders
.reduce<DisplayOrderItemDTO[]>((items, order) => [...items, ...order.items], [])
.map((i) => {
if (i?.product?.catalogProductNumber) {
return {
id: Number(i.product?.catalogProductNumber),
quantity: i.quantity,
price: i.price?.value?.value,
})),
};
}
})
.pipe(map((response) => Object.values(response.result).reduce((sum, points) => sum + points, 0)))
)
.filter((item) => item !== undefined);
if (items.length !== 0) {
return this.domainCatalogService
.getPromotionPoints({ items })
.pipe(map((response) => Object.values(response.result).reduce((sum, points) => sum + points, 0)));
} else {
return NEVER;
}
})
);
totalPrice$ = this.displayOrders$.pipe(

View File

@@ -0,0 +1,11 @@
<form *ngIf="control" [formGroup]="control">
<ui-form-control label="MwSt" variant="default">
<ui-select formControlName="vat">
<ui-select-option *ngFor="let vat of vats$ | async" [label]="vat.name + '%'" [value]="vat.vatType"></ui-select-option>
</ui-select>
</ui-form-control>
<ui-form-control class="price" label="Preis" variant="default">
<input uiInput formControlName="price" />
</ui-form-control>
</form>

View File

@@ -0,0 +1,11 @@
form {
@apply grid grid-flow-col items-center justify-end gap-4 mb-2;
}
ui-form-control {
@apply w-32;
}
::ng-deep page-purchasing-options-modal-price-input ui-form-control .input-wrapper input {
@apply w-20;
}

View File

@@ -0,0 +1,47 @@
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DomainOmsService } from '@domain/oms';
import { VATDTO } from '@swagger/oms';
import { Observable, Subscription } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
@Component({
selector: 'page-purchasing-options-modal-price-input',
templateUrl: 'purchasing-options-modal-price-input.component.html',
styleUrls: ['purchasing-options-modal-price-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PurchasingOptionsModalPriceInputComponent implements OnInit {
control: FormGroup;
vats$: Observable<VATDTO[]> = this._omsService.getVATs().pipe(shareReplay());
@Output()
priceChanged = new EventEmitter<number>();
@Output()
vatChanged = new EventEmitter<VATDTO>();
private _subscriptions = new Subscription();
constructor(private _omsService: DomainOmsService, private _fb: FormBuilder) {}
ngOnInit() {
this.initForm();
}
initForm() {
const fb = this._fb;
this.control = fb.group({
price: fb.control(undefined, [Validators.required, Validators.pattern(/^\d+([\,]\d{1,2})?$/), Validators.max(99999)]),
vat: fb.control(undefined, [Validators.required]),
});
this._subscriptions.add(
this.control.get('price').valueChanges.subscribe((price) => this.priceChanged.emit(Number(String(price).replace(',', '.'))))
);
this._subscriptions.add(this.control.get('vat').valueChanges.subscribe(this.vatChanged));
this.control.markAllAsTouched();
}
}

View File

@@ -0,0 +1,16 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { UiFormControlModule } from '@ui/form-control';
import { UiInputModule } from '@ui/input';
import { UiSelectModule } from '@ui/select';
import { PurchasingOptionsModalPriceInputComponent } from './purchasing-options-modal-price-input.component';
@NgModule({
imports: [CommonModule, UiFormControlModule, UiInputModule, UiSelectModule, FormsModule, ReactiveFormsModule],
exports: [PurchasingOptionsModalPriceInputComponent],
declarations: [PurchasingOptionsModalPriceInputComponent],
providers: [],
})
export class PurchasingOptionsModalPriceInputModule {}

View File

@@ -38,7 +38,7 @@
<div class="grow"></div>
<div class="format">{{ item?.product?.formatDetail }}</div>
<div class="price">
{{ item?.catalogAvailability?.price?.value?.value | currency: item?.catalogAvailability?.price?.value?.currency:'code' }}
{{ item?.catalogAvailability?.price?.value?.value | currency: item?.catalogAvailability?.price?.value?.currency || 'EUR':'code' }}
</div>
<div class="date" *ngIf="option$ | async; let option">
<ng-container *ngIf="option === 'pick-up'">Abholung ab</ng-container>
@@ -60,12 +60,21 @@
<div class="quantity-error" *ngIf="quantityError$ | async; let message">{{ message }}</div>
</div>
</div>
<div class="custom-price" *ngIf="showCustomPrice$ | async">
<page-purchasing-options-modal-price-input
(priceChanged)="changeCustomPrice($event)"
(vatChanged)="changeCustomVat($event)"
></page-purchasing-options-modal-price-input>
</div>
<hr />
<div class="summary-row" *ngIf="quantity$ | async; let quantity">
<div class="reading-points">{{ quantity }} Artikel | {{ promoPoints$ | async }} Lesepunkte</div>
<div class="reading-points">
{{ quantity }} Artikel
<ng-container *ngIf="promoPoints$ | async; let promoPoints"> | {{ promoPoints }} Lesepunkte </ng-container>
</div>
<div class="subtotal">
Zwischensumme
{{ item?.catalogAvailability?.price?.value?.value * quantity | currency: item?.catalogAvailability?.price?.value?.currency:'code' }}
{{ (price$ | async) * quantity | currency: item?.catalogAvailability?.price?.value?.currency || 'EUR':'code' }}
<div class="shipping-cost" *ngIf="showDeliveryInfo$ | async">
ohne Versandkosten
</div>
@@ -76,7 +85,7 @@
<button
*ngIf="canContinueShopping$ | async"
class="cta-continue-shopping"
[disabled]="(fetching$ | async) || (canContinueShopping$ | async) === false"
[disabled]="(fetching$ | async) || (canContinueShopping$ | async) === false || (customPriceInvalid$ | async) === true"
(click)="continue('continue-shopping')"
>
Weiter einkaufen
@@ -86,7 +95,7 @@
</button>
<button
*ngIf="showTakeAwayButton$ | async"
[disabled]="(fetching$ | async) || (canAdd$ | async) === false"
[disabled]="(fetching$ | async) || (canAdd$ | async) === false || (customPriceInvalid$ | async) === true"
class="cta-continue"
(click)="continue()"
>
@@ -96,7 +105,7 @@
*ngIf="showDefaultContinueButton$ | async"
class="cta-continue"
(click)="continue()"
[disabled]="(fetching$ | async) || (canAdd$ | async) === false"
[disabled]="(fetching$ | async) || (canAdd$ | async) === false || (customPriceInvalid$ | async) === true"
>
Fortfahren
</button>

View File

@@ -88,6 +88,10 @@ img.thumbnail {
::ng-deep.spin {
@apply text-brand;
}
&:disabled {
@apply text-inactive-branch border-inactive-branch;
}
}
.cta-continue,

View File

@@ -2,9 +2,9 @@ import { Component, ChangeDetectionStrategy } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { AddToShoppingCartDTO, AvailabilityDTO } from '@swagger/checkout';
import { AddToShoppingCartDTO, AvailabilityDTO, VATType } from '@swagger/checkout';
import { UiModalRef } from '@ui/modal';
import { first, map, switchMap } from 'rxjs/operators';
import { debounceTime, first, map, switchMap } from 'rxjs/operators';
import { combineLatest, Observable } from 'rxjs';
import { PurchasingOptionsModalData } from './purchasing-options-modal.data';
import { PurchasingOptions, PurchasingOptionsModalStore } from './purchasing-options-modal.store';
@@ -45,6 +45,14 @@ export class PurchasingOptionsModalComponent {
readonly quantityError$ = this.purchasingOptionsModalStore.selectQuantityError;
readonly showCustomPrice$ = this.item$.pipe(map((item) => !item?.catalogAvailability?.price?.value?.value));
readonly customPriceInvalid$ = combineLatest([
this.showCustomPrice$,
this.purchasingOptionsModalStore.selectCustomPrice,
this.purchasingOptionsModalStore.selectCustomVat,
]).pipe(map(([showCustomPrice, customPrice, customVat]) => showCustomPrice && (!customPrice || !customVat)));
readonly showTakeAwayButton$ = combineLatest([
this.option$,
this.purchasingOptionsModalStore.selectFetchingAvailability,
@@ -105,15 +113,24 @@ export class PurchasingOptionsModalComponent {
map((buyer) => buyer.source)
);
readonly promoPoints$ = combineLatest([this.item$, this.quantity$]).pipe(
switchMap(([item, quantity]) =>
price$ = combineLatest([this.item$, this.purchasingOptionsModalStore.selectCustomPrice]).pipe(
map(([item, customPrice]) => item?.catalogAvailability?.price?.value?.value ?? customPrice ?? 0)
);
vat$ = combineLatest([this.item$, this.purchasingOptionsModalStore.selectCustomVat]).pipe(
map(([item, customVat]) => item?.catalogAvailability?.price.vat ?? customVat)
);
readonly promoPoints$ = combineLatest([this.item$, this.quantity$, this.price$]).pipe(
debounceTime(250),
switchMap(([item, quantity, price]) =>
this.domainCatalogService
.getPromotionPoints({
items: [
{
id: item.id,
quantity: quantity,
price: item.catalogAvailability.price?.value?.value,
price: price,
},
],
})
@@ -145,6 +162,14 @@ export class PurchasingOptionsModalComponent {
}
}
changeCustomVat(vat: VATType) {
this.purchasingOptionsModalStore.setCustomVat(vat);
}
changeCustomPrice(price: number) {
this.purchasingOptionsModalStore.setCustomPrice(price);
}
backToSetOptions() {
this.purchasingOptionsModalStore.setOption(undefined);
}
@@ -173,6 +198,8 @@ export class PurchasingOptionsModalComponent {
const branch = await this.branch$.pipe(first()).toPromise();
const shoppingCartItem = await this.purchasingOptionsModalStore.selectShoppingCartItem.pipe(first()).toPromise();
const canAdd = await this.canAdd$.pipe(first()).toPromise();
const customPrice = await this.purchasingOptionsModalStore.selectCustomPrice.pipe(first()).toPromise();
const customVat = await this.purchasingOptionsModalStore.selectCustomVat.pipe(first()).toPromise();
if (canAdd || navigate === 'add-customer-data') {
const newItem: AddToShoppingCartDTO = {
@@ -187,6 +214,18 @@ export class PurchasingOptionsModalComponent {
newItem.product.catalogProductNumber = String(item.id);
if (!!customPrice && !!customVat) {
newItem.availability.price = {
value: {
value: customPrice,
currency: 'EUR',
},
vat: {
vatType: customVat,
},
};
}
switch (option) {
case 'take-away':
case 'pick-up':

View File

@@ -21,6 +21,7 @@ import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
import { KeyNavigationModule } from '../../shared/key-navigation/key-navigation.module';
import { RouterModule } from '@angular/router';
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
import { PurchasingOptionsModalPriceInputModule } from './price-input/purchasing-options-modal-price-input.module';
@NgModule({
imports: [
@@ -34,6 +35,7 @@ import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
UiSpinnerModule,
KeyNavigationModule,
RouterModule,
PurchasingOptionsModalPriceInputModule,
],
exports: [PurchasingOptionsModalComponent],
declarations: [

View File

@@ -5,7 +5,7 @@ import { DomainCheckoutService } from '@domain/checkout';
import { CrmCustomerService } from '@domain/crm';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ItemDTO } from '@swagger/cat';
import { AvailabilityDTO, BranchDTO, OLAAvailabilityDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { AvailabilityDTO, BranchDTO, OLAAvailabilityDTO, ShoppingCartItemDTO, VATType } from '@swagger/checkout';
import { isBoolean, isNullOrUndefined, isString } from '@utils/common';
import { NEVER, Observable } from 'rxjs';
import { delay, filter, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
@@ -32,6 +32,8 @@ interface PurchasingOptionsModalState {
// TODO: FilterBranch in der UI Component sortieren und filtern
filterResult?: BranchDTO[];
availabilities: { [key: string]: AvailabilityDTO };
customPrice?: number;
customVat?: VATType;
}
@Injectable()
@@ -67,6 +69,10 @@ export class PurchasingOptionsModalStore extends ComponentStore<PurchasingOption
readonly selectQuantity = this.select((s) => s.quantity);
readonly selectCustomPrice = this.select((s) => s.customPrice);
readonly selectCustomVat = this.select((s) => s.customVat);
readonly selectAvailabilities = this.select((s) => s.availabilities);
readonly selectAvailability = this.select((s) => s.availabilities[s.option]);
@@ -266,6 +272,20 @@ export class PurchasingOptionsModalStore extends ComponentStore<PurchasingOption
};
});
readonly setCustomPrice = this.updater((state, customPrice: number) => {
return {
...state,
customPrice,
};
});
readonly setCustomVat = this.updater((state, customVat: VATType) => {
return {
...state,
customVat,
};
});
readonly setAvailableOptions = this.updater((state, availableOptions: PurchasingOptions[]) => {
let option = state.option;
if (availableOptions?.length === 1 && availableOptions[0] === 'download') {

View File

@@ -1,5 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CheckoutDummyComponent } from './checkout-dummy/checkout-dummy.component';
import { CheckoutReviewComponent } from './checkout-review/checkout-review.component';
import { CheckoutSummaryComponent } from './checkout-summary/checkout-summary.component';
import { PageCheckoutComponent } from './page-checkout.component';
@@ -11,6 +12,7 @@ const routes: Routes = [
children: [
{ path: 'summary', component: CheckoutSummaryComponent },
{ path: 'review', component: CheckoutReviewComponent },
{ path: 'dummy', component: CheckoutDummyComponent },
{ path: '', pathMatch: 'full', redirectTo: 'review' },
],
},

View File

@@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ShellBreadcrumbModule } from '@shell/breadcrumb';
import { CheckoutDummyModule } from './checkout-dummy/checkout-dummy.module';
import { CheckoutReviewModule } from './checkout-review/checkout-review.module';
import { CheckoutSummaryModule } from './checkout-summary/checkout-summary.module';
import { PageCheckoutModalsModule } from './page-checkout-modals.module';
@@ -14,6 +15,7 @@ import { PageCheckoutComponent } from './page-checkout.component';
CheckoutSummaryModule,
PageCheckoutRoutingModule,
CheckoutReviewModule,
CheckoutDummyModule,
ShellBreadcrumbModule,
],
declarations: [PageCheckoutComponent],

View File

@@ -46,7 +46,9 @@
<div class="orders">
<ng-container *ngFor="let orderItem of order.items; let i = index">
<ng-container
*ngIf="i === 0 || order.items[i - 1].data.features.orderType !== orderItem.data.features.orderType"
*ngIf="
i === 0 || order.items[i - 1].data.features.orderType !== orderItem.data.features.orderType || !hasDummyItems(orderItem.data)
"
[ngSwitch]="orderItem.data.features.orderType"
>
<div *ngSwitchCase="'Rücklage'" class="order-category">
@@ -54,8 +56,15 @@
<h2>Rücklage</h2>
</div>
<div *ngSwitchCase="'Abholung'" class="order-category">
<ui-icon class="icon" icon="box_out" size="18px"></ui-icon>
<h2>Abholung</h2>
<ng-container *ngIf="hasDummyItems(orderItem.data); else abholung">
<div class="order-category-dummy">
<h2>Manuelle Anlage / Dummy Bestellung</h2>
</div>
</ng-container>
<ng-template #abholung>
<ui-icon class="icon" icon="box_out" size="18px"></ui-icon>
<h2>Abholung</h2>
</ng-template>
</div>
<div #download *ngSwitchCase="'Download'" class="order-category">
<ui-icon class="icon" icon="download" size="18px"></ui-icon>

View File

@@ -10,6 +10,10 @@
@apply flex flex-col bg-white mx-10 mt-5 mb-5;
}
.order-category-dummy {
@apply flex flex-row items-center mt-5 ml-0;
}
.order-category {
@apply flex flex-row items-center mt-5;

View File

@@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainOmsService } from '@domain/oms';
import { OrderDTO } from '@swagger/oms';
import { EntityDTOContainerOfOrderItemDTO, OrderDTO, OrderItemDTO } from '@swagger/oms';
import { combineLatest, Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
@@ -17,6 +17,7 @@ export class CustomerOrderDetailsComponent implements OnInit {
customerId: number;
orderId: number;
order$: Observable<OrderDTO>;
manuallyOrDummyOrderItems$: Observable<EntityDTOContainerOfOrderItemDTO[]>;
branchName$: Observable<string>;
constructor(
@@ -47,4 +48,8 @@ export class CustomerOrderDetailsComponent implements OnInit {
params: {},
});
}
hasDummyItems(orderItem: OrderItemDTO): boolean {
return !!orderItem?.subsetItems?.find((subsetItem) => subsetItem?.data?.supplyChannel === 'MANUALLY');
}
}

View File

@@ -11,6 +11,20 @@
</span>
</div>
<ng-container *ngIf="showUpdateComment$ | async">
<hr />
<div class="update-comment">
<div class="update-comment-heading">
Update-Notiz
</div>
<ui-icon icon="refresh" size="19px"></ui-icon>
<div class="grow">
{{ info.updateComment }}
</div>
</div>
</ng-container>
<hr />
<div class="info-body">
@@ -80,17 +94,6 @@
</div>
</ng-container>
<ng-container *ngIf="showUpdateComment$ | async">
<hr *ngIf="info?.attachments === 0 || info?.articles?.length === 0" />
<div class="update-comment">
<ui-icon icon="refresh" size="19px"></ui-icon>
<div class="grow">
{{ info.updateComment }}
</div>
</div>
<hr *ngIf="!(showNotes$ | async)" />
</ng-container>
<ng-container *ngIf="showNotes$ | async">
<hr />
<div class="notes">

View File

@@ -111,10 +111,14 @@ hr {
}
.update-comment {
@apply flex flex-row items-center text-lg;
@apply flex flex-row items-center;
.update-comment-heading {
@apply flex flex-row items-center text-lg font-bold;
}
ui-icon {
@apply mr-3 text-cool-grey;
@apply mx-3 text-cool-grey;
}
}

View File

@@ -113,8 +113,10 @@ export class TaskInfoComponent implements OnChanges {
shareReplay()
);
showUpdateComment$ = combineLatest([this.processingStatus$, this.info$]).pipe(
map(([processingStatus, info]) => info.updateComment && processingStatus.includes('Removed') && !!info.successor?.id)
showUpdateComment$ = combineLatest([this.info$, this.processingStatus$]).pipe(
map(
([info, processingStatus]) => !!info.updateComment && ((info.successor && processingStatus.includes('Removed')) || info.predecessor)
)
);
@HostBinding('class')

View File

@@ -7,7 +7,7 @@ import { DisplayInfoDTO } from '@swagger/eis';
import { DateAdapter } from '@ui/common';
import { UiModalService } from '@ui/modal';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { first, map, withLatestFrom } from 'rxjs/operators';
@Component({
selector: 'page-task-list',
@@ -56,13 +56,21 @@ export class TaskListComponent {
return false;
});
}),
withLatestFrom(this.selected$),
map(([list, date]) =>
list.filter((item) =>
!!item?.successor
? this.dateAdapter.equals({ first: new Date(item.publicationDate || item.taskDate), second: date, precision: 'day' })
: item
)
),
map((list) => list.sort(this.moveRemovedToEnd.bind(this)))
);
ongoingItems$ = combineLatest([this.items$, this.selected$]).pipe(
map(([items, selectedDate]) => {
map(([items, selectedDate]) =>
// Filter Aufgaben die vor dem aktuellen Tag gestartet sind und nicht überfällig oder abgeschlossen sind
const list = items.filter((item) => {
items.filter((item) => {
const type = this.domainTaskCalendarService.getInfoType(item);
const processingStatus = this.domainTaskCalendarService.getProcessingStatusList(item);
if (
@@ -76,15 +84,34 @@ export class TaskListComponent {
return new Date(item.publicationDate || item.taskDate) < this.today;
}
return false;
});
return this.sort(list, ['Overdue', 'InProcess', 'Approved', 'Completed']);
})
})
),
withLatestFrom(this.selected$),
map(([list, date]) =>
list.filter((item) =>
!!item?.successor
? this.dateAdapter.equals({ first: new Date(item.publicationDate || item.taskDate), second: date, precision: 'day' })
: item
)
),
map((list) => this.sort(list, ['Overdue', 'InProcess', 'Approved', 'Completed', 'Removed'])),
map((list) => list.sort(this.moveRemovedToEnd.bind(this)))
);
selectedItems$ = combineLatest([this.items$, this.selected$]).pipe(
map(([items, date]) =>
items.filter((item) =>
this.dateAdapter.equals({ first: this.domainTaskCalendarService.getDisplayInfoDate(item), second: date, precision: 'day' })
this.dateAdapter.equals({
first: this.domainTaskCalendarService.getDisplayInfoDate(item),
second: date,
precision: 'day',
})
)
),
withLatestFrom(this.selected$),
map(([list, date]) =>
list.filter((item) =>
!!item?.successor ? this.dateAdapter.equals({ first: new Date(item.publicationDate), second: date, precision: 'day' }) : item
)
),
// Sortierung der aufgaben nach Rot => Gelb => Grau => Grün

View File

@@ -16,18 +16,12 @@
<ui-icon icon="dashboard" size="26px"></ui-icon>
</button>
</div>
<!-- <a class="align-right nav-link" (click)="goToDashboard()" *ngIf="module === 0">
<lib-icon *ngIf="router.url !== '/dashboard'" width="25px" height="26px" name="Infoboard_inactive"></lib-icon>
<lib-icon *ngIf="router.url === '/dashboard'" width="25px" height="26px" name="Infoboard"></lib-icon>
</a>
<a class="align-right" *ngIf="module === 1">
<lib-icon width="25px" height="26px" name="Infoboard-branch"></lib-icon>
</a> -->
<div class="align-center">
<button
class="header-icon notification"
type="button"
[class.active]="notificationCount$ | async"
[disabled]="(notificationCount$ | async) === 0"
(click)="openNotifications()"
>
<div class="notification-counter" *ngIf="notificationCount$ | async; let count">{{ count }}</div>

View File

@@ -104,6 +104,9 @@ export class HeaderComponent implements OnInit, OnDestroy {
this._modal.open({
content: ModalNotificationsComponent,
data: notifications,
config: {
showScrollbarY: false,
},
});
}
}

View File

@@ -34,6 +34,9 @@
</div>
<div class="actions">
<ng-container *ngIf="!loading; else spinner">
<app-button (action)="productNotFound()" [outline]="true" class="cta-not-found">
Artikel nicht gefunden
</app-button>
<app-button [disabled]="quantityControl.invalid" (action)="addProduct()" primaryBorders="true" primary="true"
>Exemplare remittieren</app-button
>

View File

@@ -8,6 +8,10 @@
}
}
.cta-not-found {
@apply mr-7;
}
.modal-wrapper {
font-family: 'Open Sans';
line-height: 21px;

View File

@@ -71,6 +71,15 @@ export class RemissionAddProductToShippingDocumentPartiallyDialogComponent imple
});
}
productNotFound() {
this.loading = true;
this.add.emit({
quantity: 0,
placementType: undefined,
predefinedRemissionQuantity: undefined,
});
}
placementUpdated(placementType: RemissionPlacementType) {
this.placementType = placementType;
}

View File

@@ -2,7 +2,10 @@
<div class="card-wrapper">
<app-remission-list-card-header [product]="product"></app-remission-list-card-header>
<div class="restmenge" *ngIf="product.isResidual">Restmenge</div>
<div class="restmenge" *ngIf="product.isResidual || product.impediment?.comment">
{{ product.impediment?.comment ? product.impediment?.comment : 'Restmenge' }}
<ng-container *ngIf="product.impediment?.attempts"> ({{ product.impediment?.attempts }}) </ng-container>
</div>
<div class="card-details">
<div class="icon">

View File

@@ -14,8 +14,8 @@
</div>
<div class="image" *ngIf="pageStatus === 0">
<ng-container *ngIf="emptyOrInvalidBarcode">
<app-barcode-scanner *ngIf="isSafari" (scan)="handleScanResult($event)"></app-barcode-scanner>
<lib-remission-container-scanner *ngIf="isNative" #scanner (scan)="handleScanResult($event)"></lib-remission-container-scanner>
<!-- <app-barcode-scanner *ngIf="isSafari" (scan)="handleScanResult($event)"></app-barcode-scanner> -->
<lib-remission-container-scanner *ngIf="isNative" (scan)="handleScanResult($event)"></lib-remission-container-scanner>
<ui-searchbox *ngIf="isDesktop" placeholder="Wannennummer scannen" (search)="handleScanResult($event)"></ui-searchbox>
</ng-container>
</div>

View File

@@ -36,6 +36,7 @@ import { UiSearchboxNextComponent } from '@ui/searchbox';
import { isResponseArgs } from '@utils/object';
import { HttpErrorResponse } from '@angular/common/http';
import { ResponseArgs } from '@swagger/cat';
import { RemissionContainerScannerScanditComponent } from 'shared/public_api';
@Component({
selector: 'app-remission-finish',
@@ -107,6 +108,9 @@ export class RemissionFinishComponent implements OnInit, OnDestroy {
@ViewChild(UiSearchboxNextComponent, { static: false })
searchbox: UiSearchboxNextComponent;
@ViewChild(RemissionContainerScannerScanditComponent, { static: false })
scanner: RemissionContainerScannerScanditComponent;
constructor(
private store: Store,
private router: Router,
@@ -273,6 +277,10 @@ export class RemissionFinishComponent implements OnInit, OnDestroy {
})
.afterClosed$.toPromise();
if (this.scanner) {
this.scanner.code = '';
}
if (result.data) {
this.completeScan(barcode);
} else {

View File

@@ -205,4 +205,5 @@
<g id="dashboard" transform="matrix(1.31094,0,0,1.31094,-0.207962,-1.09593)">
<path d="M23.017,15.072L19.649,15.072C19.649,15.072 19.649,1.924 19.649,1.924C19.649,1.315 19.188,0.836 18.638,0.836L1.71,0.836C1.161,0.836 0.7,1.315 0.7,1.924L0.7,21.301C0.702,23.487 2.316,25.244 4.288,25.246L20.83,25.246C21.684,25.245 22.503,24.87 23.106,24.199C23.698,23.539 24.03,22.647 24.027,21.717C24.027,21.718 24.027,16.159 24.027,16.159C24.027,15.55 23.566,15.072 23.017,15.072ZM4.289,23.055C3.419,23.054 2.729,22.262 2.72,21.299C2.72,21.299 2.72,3.011 2.72,3.011C2.72,3.011 17.628,3.011 17.628,3.011C17.628,3.011 17.628,21.718 17.628,21.718C17.628,22.179 17.711,22.633 17.871,23.055C17.871,23.055 4.289,23.055 4.289,23.055L4.289,23.055ZM22.007,21.71C21.973,22.414 21.464,22.979 20.828,22.979C20.194,22.979 19.685,22.417 19.649,21.716C19.649,21.71 19.649,17.246 19.649,17.246C19.649,17.246 22.007,17.246 22.007,17.246L22.007,21.71ZM15.174,17.136L15.174,17.135C15.178,16.848 15.076,16.572 14.894,16.368C14.702,16.151 14.438,16.032 14.164,16.032C14.164,16.032 5.311,16.032 5.311,16.032C4.762,16.032 4.301,16.511 4.301,17.12C4.301,17.728 4.762,18.207 5.311,18.207L14.164,18.207C14.708,18.207 15.166,17.737 15.174,17.136ZM15.174,12.013L15.174,12C15.174,11.718 15.071,11.448 14.893,11.248C14.7,11.033 14.437,10.914 14.164,10.914C14.164,10.914 5.311,10.914 5.311,10.914C4.762,10.914 4.301,11.393 4.301,12.002C4.301,12.61 4.762,13.089 5.311,13.089C5.311,13.089 14.164,13.089 14.164,13.089C14.71,13.089 15.169,12.616 15.174,12.013ZM14.833,12.268C14.83,12.276 14.827,12.284 14.824,12.292C14.842,12.297 14.857,12.299 14.865,12.301L14.833,12.268ZM14.835,12.261L14.835,12.262L14.842,12.269L14.835,12.261ZM15.174,6.884C15.174,6.275 14.713,5.797 14.164,5.797C14.164,5.797 5.311,5.797 5.311,5.797C4.762,5.797 4.301,6.275 4.301,6.884C4.301,7.493 4.762,7.971 5.311,7.971L14.164,7.971C14.713,7.971 15.174,7.493 15.174,6.884Z" style="fill-rule:nonzero;"/>
</g>
<path id="checked" d="M25.151,8.013C24.854,8.055 24.58,8.197 24.374,8.415C19.929,12.869 16.602,16.545 12.361,20.844L7.533,16.766C7.169,16.457 6.667,16.366 6.218,16.527C5.768,16.689 5.44,17.079 5.356,17.549C5.272,18.019 5.447,18.498 5.813,18.805L11.584,23.688C12.115,24.134 12.899,24.098 13.387,23.605C18.15,18.832 21.553,15.005 26.26,10.288C26.674,9.887 26.782,9.265 26.53,8.748C26.277,8.231 25.721,7.934 25.151,8.013Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.00155,0,0,1.00155,0,0)">
<g id="Tablet_ISA_Kunde_Warenausgabe_Detailseite_001_009_eingetroffen-Benachrichtigung_001" serif:id="Tablet_ISA_Kunde_Warenausgabe_Detailseite_001_009_eingetroffen+Benachrichtigung_001">
<g id="Bookmark_Benachrichtigung_Mail-Copy">
<g id="Group-3-Copy-2">
<path id="Rectangle" d="M12.445,0L43.555,0C46.01,-0 48,1.99 48,4.445L48,4.474L8,4.474L8,4.445C8,1.99 9.99,0 12.445,0Z" style="fill:rgb(31,70,108);"/>
<path id="Rectangle1" serif:id="Rectangle" d="M4.445,0L42.781,0L42.781,43.481C42.781,45.936 40.791,47.926 38.336,47.926C37.644,47.926 36.961,47.764 36.342,47.454L23.376,40.948C22.121,40.318 20.643,40.319 19.388,40.949L6.44,47.451C4.246,48.552 1.574,47.667 0.473,45.473C0.162,44.854 -0,44.171 0,43.478L0,4.445C-0,1.99 1.99,0 4.445,0Z" style="fill:rgb(85,117,150);"/>
</g>
<path id="Mail" d="M30.886,11L31.059,11.008C32.1,11.097 32.919,11.935 32.994,12.969L33,13.125L33,27.022L32.992,27.194C32.903,28.236 32.064,29.057 31.032,29.133L30.876,29.138L11.115,29.138L10.942,29.13C9.901,29.041 9.081,28.202 9.006,27.168L9,27.011L9,13.115L9.008,12.942C9.097,11.9 9.935,11.081 10.969,11.006L11.125,11L30.886,11ZM11.103,14.827L11.117,27.034L30.882,27.02L30.881,14.84L21.668,22.612L21.56,22.69C21.391,22.796 21.197,22.852 20.999,22.852C20.792,22.852 20.59,22.79 20.417,22.676L20.316,22.601L11.103,14.827ZM29.656,13.103L12.359,13.115L20.999,20.409L29.656,13.103Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="Tablet_ISA_Kunde_Warenausgabe_Detailseite_001_009_eingetroffen-Benachrichtigung_003" serif:id="Tablet_ISA_Kunde_Warenausgabe_Detailseite_001_009_eingetroffen+Benachrichtigung_003">
<g id="Bookmark_Benachrichtigung_SMS-Copy-2">
<g id="Group-3-Copy-2">
<path id="Rectangle" d="M12.445,0L43.555,0C46.01,-0 48,1.99 48,4.445L48,4.474L8,4.474L8,4.445C8,1.99 9.99,0 12.445,0Z" style="fill:rgb(31,70,108);"/>
<path id="Rectangle1" serif:id="Rectangle" d="M4.445,0L42.781,0L42.781,43.481C42.781,45.936 40.791,47.926 38.336,47.926C37.644,47.926 36.961,47.764 36.342,47.454L23.376,40.948C22.121,40.318 20.643,40.319 19.388,40.949L6.44,47.451C4.246,48.552 1.574,47.667 0.473,45.473C0.162,44.854 -0,44.171 0,43.478L0,4.445C-0,1.99 1.99,0 4.445,0Z" style="fill:rgb(85,117,150);"/>
</g>
<path id="Shape" d="M29.607,10.8C31.527,10.8 33.096,12.307 33.195,14.202L33.2,14.393L33.2,23.771C33.2,25.692 31.693,27.26 29.798,27.36L29.607,27.365L19.651,27.364L14.438,31.541C14.306,31.658 14.147,31.739 13.978,31.779L13.849,31.801L13.718,31.807C13.55,31.805 13.384,31.767 13.228,31.693C12.89,31.521 12.659,31.196 12.606,30.823L12.594,30.681L12.594,27.364L12.393,27.365C10.537,27.365 9.009,25.957 8.82,24.15L8.805,23.962L8.8,23.771L8.8,14.393C8.8,12.473 10.307,10.904 12.202,10.805L12.393,10.8L29.607,10.8ZM29.607,13.051L12.393,13.051C11.696,13.051 11.122,13.583 11.057,14.264L11.051,14.393L11.051,23.771C11.051,24.469 11.583,25.042 12.264,25.107L12.393,25.114L13.72,25.114C14.3,25.114 14.778,25.553 14.839,26.116L14.845,26.239L14.845,28.323L18.561,25.373C18.722,25.245 18.912,25.161 19.117,25.128L19.272,25.114L29.607,25.114C30.304,25.114 30.878,24.581 30.943,23.9L30.949,23.771L30.949,14.393C30.949,13.652 30.348,13.051 29.607,13.051Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -16,7 +16,7 @@
button,
a {
@apply outline-none cursor-pointer;
@apply text-black outline-none cursor-pointer;
}
button:disabled,

View File

@@ -94,6 +94,10 @@
<div class="value">{{ orderItem?.processingStatusDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
</ng-container>
</div>
<div class="detail">
<div class="label">Benachrichtigung</div>
<div class="value">{{ (notificationsChannel$ | async | notificationsChannel) || '-' }}</div>
</div>
</div>
</div>

View File

@@ -49,7 +49,7 @@
@apply flex flex-row my-1;
.label {
width: 130px;
width: 145px;
}
.value {

View File

@@ -4,8 +4,8 @@ import { DomainOmsService } from '@domain/oms';
import { KeyValueDTOOfStringAndString, OrderItemListItemDTO } from '@swagger/oms';
import { DateAdapter } from '@ui/common';
import { cloneDeep } from 'lodash';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, of } from 'rxjs';
import { catchError, filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
import { SharedGoodsInOutOrderDetailsComponent } from '../goods-in-out-order-details.component';
@Component({
@@ -26,6 +26,19 @@ export class SharedGoodsInOutOrderDetailsHeaderComponent {
orderItem$ = this.host.orderItems$.pipe(map((orderItems) => orderItems[0]));
notificationsChannel$ = this.orderItem$.pipe(
switchMap((oi) => {
if (oi?.orderId) {
return this.omsService.getNotifications(oi?.orderId).pipe(
map((res) => res.selected),
catchError(() => of(0))
);
}
return of({ selected: 0 });
})
);
changeDateLoader$ = new BehaviorSubject<boolean>(false);
changeStatusLoader$ = new BehaviorSubject<boolean>(false);
changeStatusDisabled$ = this.host.changeActionDisabled$;

View File

@@ -1,6 +1,22 @@
<ng-container *ngIf="orderItem$ | async; let orderItem">
<div class="goods-in-out-order-details-features">
<img *ngIf="orderItem?.features?.prebooked" src="/assets/images/tag_icon_preorder.svg" [alt]="orderItem?.features?.prebooked" />
<ng-container *ngIf="notificationsSent$ | async; let notificationsSent">
<ng-container *ngIf="notificationsSent?.NOTIFICATION_EMAIL">
<img [uiOverlayTrigger]="emailTooltip" src="/assets/images/email_bookmark.svg" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #emailTooltip [closeable]="true">
Per E-Mail benachrichtigt <br />
{{ notificationsSent?.NOTIFICATION_EMAIL | date: 'dd.MM.yyyy | HH:mm' }} Uhr
</ui-tooltip>
</ng-container>
<ng-container *ngIf="notificationsSent?.NOTIFICATION_SMS">
<img [uiOverlayTrigger]="smsTooltip" src="/assets/images/sms_bookmark.svg" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #smsTooltip [closeable]="true">
Per SMS benachrichtigt <br />
{{ notificationsSent?.NOTIFICATION_SMS | date: 'dd.MM.yyyy | HH:mm' }} Uhr
</ui-tooltip>
</ng-container>
</ng-container>
</div>
<div class="goods-in-out-order-details-item">
<div class="goods-in-out-order-details-item-thumbnail">
@@ -60,6 +76,10 @@
<div class="label">Meldenummer</div>
<div class="value">{{ orderItem.ssc }} - {{ orderItem.sscText }}</div>
</div>
<div class="detail">
<div class="label">Vormerker</div>
<div class="value">{{ orderItem.isPrebooked ? 'Ja' : 'Nein' }}</div>
</div>
<div class="detail">
<div class="label">MwSt</div>
<div class="value">{{ orderItem.retailPrice?.vat?.inPercent }}%</div>

View File

@@ -8,6 +8,12 @@ button {
.goods-in-out-order-details-features {
@apply absolute grid grid-flow-col gap-2 -top-1 right-6;
img {
width: 48px;
height: 48px;
z-index: 1;
}
}
.goods-in-out-order-details-item {

View File

@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, OnDestroy, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { DomainOmsService, DomainReceiptService } from '@domain/oms';
import { HistoryComponent } from '@modal/history';
@@ -6,8 +6,8 @@ import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { OrderItemListItemDTO, ReceiptDTO, ReceiptType } from '@swagger/oms';
import { UiModalService } from '@ui/modal';
import { isEqual } from 'lodash';
import { combineLatest } from 'rxjs';
import { filter, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
import { combineLatest, NEVER } from 'rxjs';
import { catchError, filter, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
import { SharedGoodsInOutOrderDetailsStore } from '../goods-in-out-order-details.store';
export interface SharedGoodsInOutOrderDetailsItemComponentState {
@@ -25,7 +25,7 @@ export interface SharedGoodsInOutOrderDetailsItemComponentState {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SharedGoodsInOutOrderDetailsItemComponent extends ComponentStore<SharedGoodsInOutOrderDetailsItemComponentState>
implements OnDestroy {
implements OnInit, OnDestroy {
@Input()
get orderItem() {
return this.get((s) => s.orderItem);
@@ -52,6 +52,15 @@ export class SharedGoodsInOutOrderDetailsItemComponent extends ComponentStore<Sh
readonly orderItem$ = this.select((s) => s.orderItem);
notificationsSent$ = this.orderItem$.pipe(
filter((oi) => !!oi),
switchMap((oi) =>
this._omsService
.getCompletedTasks({ orderId: oi.orderId, orderItemId: oi.orderItemId, orderItemSubsetId: oi.orderItemSubsetId })
.pipe(catchError(() => NEVER))
)
);
canChangeQuantity$ = combineLatest([this.orderItem$, this._host.fetchPartial$]).pipe(
map(([item, partialPickup]) => ([16, 8192].includes(item?.processingStatus) || partialPickup) && item.quantity > 1)
);
@@ -123,6 +132,8 @@ export class SharedGoodsInOutOrderDetailsItemComponent extends ComponentStore<Sh
});
}
ngOnInit() {}
ngOnDestroy() {
// Remove Prev OrderItem from selected list
this._host.selectOrderItem(this.orderItem, false);

View File

@@ -3,13 +3,13 @@ import { Component, ChangeDetectionStrategy, ContentChildren, QueryList } from '
import { BreadcrumbService } from '@core/breadcrumb';
import { CommandService } from '@core/command';
import { OrderItemsContext } from '@domain/oms';
import { SharedGoodsInOutOrderDetailsTagsComponent } from '@shared/goods-in-out';
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 { 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';
import { SharedGoodsInOutOrderDetailsStore } from './goods-in-out-order-details.store';
@Component({

View File

@@ -17,6 +17,7 @@ import { UiCommonModule } from '@ui/common';
import { UiSliderModule } from '@ui/slider';
import { UiSelectBulletModule } from '@ui/select-bullet';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
import { UiTooltipModule } from '@ui/tooltip';
@NgModule({
imports: [
@@ -33,6 +34,7 @@ import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
UiSliderModule,
UiSelectBulletModule,
UiSpinnerModule,
UiTooltipModule,
],
exports: [
SharedGoodsInOutOrderDetailsComponent,

View File

@@ -7,7 +7,7 @@
</div>
<ui-form-control label="Vorgang-ID" variant="inline" statusLabel="Nicht Änderbar">
<input uiInput formControlName="orderId" />
<input uiInput formControlName="orderNumber" />
</ui-form-control>
<ui-form-control label="Bestelldatum" variant="inline" statusLabel="Nicht Änderbar">
@@ -18,6 +18,8 @@
<input uiInput formControlName="clientChannel" />
</ui-form-control>
<shared-notification-channel-control formGroupName="notificationChannel"></shared-notification-channel-control>
<ui-form-control label="Kundennummer" variant="inline" statusLabel="Nicht Änderbar">
<input uiInput formControlName="buyerNumber" />
</ui-form-control>
@@ -127,6 +129,10 @@
<input class="ssc-text" uiInput formControlName="sscText" />
</div>
<ui-form-control label="Vormerker" variant="inline" statusLabel="Nicht Änderbar">
<input uiInput formControlName="isPrebooked" />
</ui-form-control>
<ui-form-control label="MwSt" variant="inline">
<ui-select formControlName="vat">
<ui-select-option *ngFor="let vat of vats$ | async" [label]="vat.name + '%'" [value]="vat.vatType"></ui-select-option>

View File

@@ -10,10 +10,12 @@ import {
Output,
SimpleChanges,
} from '@angular/core';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { DomainOmsService } from '@domain/oms';
import { OrderItemListItemDTO, StockStatusCodeDTO, VATDTO } from '@swagger/oms';
import { emailNotificationValidator, mobileNotificationValidator } from '@shared/notification-channel-control';
import { NotificationChannel, OrderDTO, OrderItemListItemDTO, StockStatusCodeDTO, VATDTO } from '@swagger/oms';
import { DateAdapter } from '@ui/common';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { UiSelectOptionComponent } from '@ui/select';
import { Observable, Subscription } from 'rxjs';
import { first, shareReplay } from 'rxjs/operators';
@@ -36,12 +38,21 @@ export class SharedGoodsInOutOrderEditComponent implements OnChanges, OnDestroy
@Input()
items: OrderItemListItemDTO[];
@Input()
order: OrderDTO;
expanded: boolean[];
showTagsComponent: boolean[];
control: FormGroup;
notificationsGroup = new FormGroup({
selected: new FormControl(1),
email: new FormControl('', emailNotificationValidator),
mobile: new FormControl('', mobileNotificationValidator),
});
minDate = this.dateAdapter.addCalendarDays(new Date(), -1);
vats$: Observable<VATDTO[]> = this.omsService.getVATs().pipe(shareReplay());
@@ -67,7 +78,8 @@ export class SharedGoodsInOutOrderEditComponent implements OnChanges, OnDestroy
private datePipe: DatePipe,
private omsService: DomainOmsService,
private dateAdapter: DateAdapter,
private cdr: ChangeDetectorRef
private cdr: ChangeDetectorRef,
private _modal: UiModalService
) {}
ngOnDestroy(): void {
@@ -75,24 +87,30 @@ export class SharedGoodsInOutOrderEditComponent implements OnChanges, OnDestroy
}
ngOnChanges({ items }: SimpleChanges) {
if (items.currentValue) {
if (items?.currentValue) {
this.expanded = new Array(items.currentValue.length);
this.expanded[0] = true;
this.showTagsComponent = items.currentValue?.map((item) => !!item.compartmentCode);
this.initForm(items.currentValue);
this.updateNotificationsGroup();
}
}
initForm(items: OrderItemListItemDTO[]) {
const fb = this.fb;
if (items.length === 0) {
return;
}
const fb = this.fb;
this.control = fb.group({
orderId: fb.control({ value: items[0].orderId, disabled: true }),
orderNumber: fb.control({ value: items[0].orderNumber, disabled: true }),
orderDate: fb.control({ value: this.datePipe.transform(items[0].orderDate), disabled: true }),
clientChannel: fb.control({ value: this.environmentChannelPipe.transform(items[0].clientChannel), disabled: true }),
buyerNumber: fb.control({ value: items[0].buyerNumber, disabled: true }),
items: fb.array([]),
notificationChannel: this.notificationsGroup,
});
items.forEach(async (item, index) => {
@@ -129,6 +147,7 @@ export class SharedGoodsInOutOrderEditComponent implements OnChanges, OnDestroy
supplier: fb.control({ value: item.supplier, disabled: true }),
ssc: fb.control(item.ssc, [Validators.required, validateSsc(statusCodes)]),
sscText: fb.control({ value: '', disabled: true }),
isPrebooked: fb.control({ value: item.isPrebooked ? 'Ja' : 'Nein', disabled: true }),
vat: fb.control(item.vatType),
specialComment: fb.control(item.specialComment),
});
@@ -148,6 +167,21 @@ export class SharedGoodsInOutOrderEditComponent implements OnChanges, OnDestroy
});
}
async updateNotificationsGroup() {
const control = this.control?.getRawValue();
const orderId = control?.orderId;
try {
if (orderId) {
const notifications = await this.omsService.getNotifications(+orderId).toPromise();
this.notificationsGroup.reset(notifications);
}
} catch (error) {
this._modal.open({ content: UiErrorModalComponent, data: error, title: 'Fehler beim abrufen der Benachrichtigung' });
}
}
changeEstimatedDeliveryDate(date: Date, item: OrderItemListItemDTO) {
if (!date) {
return;
@@ -170,44 +204,64 @@ export class SharedGoodsInOutOrderEditComponent implements OnChanges, OnDestroy
try {
const control = this.control.getRawValue();
const orderId = control.orderId;
if (this.notificationsGroup.dirty) {
try {
await this.omsService.updateNotifications(orderId, this.notificationsGroup.getRawValue()).toPromise();
} catch (error) {
this._modal.open({ content: UiErrorModalComponent, data: error, title: 'Fehler beim aktualisieren der Benachrichtigung' });
throw error;
}
}
for (const itemCtrl of control.items) {
const orderItemId = itemCtrl.orderItemId;
const orderItemSubsetId = itemCtrl.orderItemSubsetId;
await this.omsService
.patchOrderItem({
orderId,
orderItemId,
orderItem: {
product: { ean: itemCtrl.ean },
grossPrice: {
vat: { vatType: itemCtrl.vat },
value: { value: Number(String(itemCtrl.price).replace(',', '.')) },
try {
await this.omsService
.patchOrderItem({
orderId,
orderItemId,
orderItem: {
product: { ean: itemCtrl.ean },
grossPrice: {
vat: { vatType: itemCtrl.vat },
value: { value: Number(String(itemCtrl.price).replace(',', '.')) },
},
},
},
})
.pipe(first())
.toPromise();
})
.pipe(first())
.toPromise();
} catch (error) {
this._modal.open({ content: UiErrorModalComponent, data: error, title: 'Fehler beim aktualisieren des Bestellpostens' });
throw error;
}
await this.omsService
.patchOrderItemSubset({
orderId,
orderItemId,
orderItemSubsetId,
orderItemSubset: {
compartmentCode:
itemCtrl.compartmentInfo && itemCtrl.compartmentCode
? itemCtrl.compartmentCode.replace('_' + itemCtrl.compartmentInfo, '')
: itemCtrl.compartmentCode,
compartmentInfo: itemCtrl.compartmentInfo || '',
estimatedShippingDate: itemCtrl.estimatedShippingDate || null,
compartmentStop: itemCtrl.pickUpDeadline || null,
specialComment: itemCtrl.specialComment || '',
ssc: itemCtrl.ssc,
sscText: itemCtrl.sscText !== '' ? itemCtrl.sscText.substring(3) : '',
},
})
.pipe(first())
.toPromise();
try {
await this.omsService
.patchOrderItemSubset({
orderId,
orderItemId,
orderItemSubsetId,
orderItemSubset: {
compartmentCode:
itemCtrl.compartmentInfo && itemCtrl.compartmentCode
? itemCtrl.compartmentCode.replace('_' + itemCtrl.compartmentInfo, '')
: itemCtrl.compartmentCode,
compartmentInfo: itemCtrl.compartmentInfo || '',
estimatedShippingDate: itemCtrl.estimatedShippingDate || null,
compartmentStop: itemCtrl.pickUpDeadline || null,
specialComment: itemCtrl.specialComment || '',
ssc: itemCtrl.ssc,
sscText: itemCtrl.sscText !== '' ? itemCtrl.sscText.substring(3) : '',
},
})
.pipe(first())
.toPromise();
} catch (error) {
this._modal.open({ content: UiErrorModalComponent, data: error, title: 'Fehler beim aktualisieren des Bestellpostens' });
throw error;
}
}
this.navigateBack();
} catch (error) {

View File

@@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { SharedGoodsInOutOrderDetailsModule } from '@shared/goods-in-out';
import { NotificationChannelControlModule } from '@shared/notification-channel-control';
import { UiCommonModule } from '@ui/common';
import { UiDatepickerModule } from '@ui/datepicker';
import { UiDropdownModule } from '@ui/dropdown';
@@ -10,6 +10,7 @@ import { UiIconModule } from '@ui/icon';
import { UiInputModule } from '@ui/input';
import { UiSelectModule } from '@ui/select';
import { ProductImageModule } from 'apps/cdn/product-image/src/public-api';
import { SharedGoodsInOutOrderDetailsModule } from '../goods-in-out-order-details';
import { PipesModule } from '../pipes/pipes.module';
import { SharedGoodsInOutOrderEditComponent } from './goods-in-out-order-edit.component';
@@ -28,6 +29,7 @@ import { SharedGoodsInOutOrderEditComponent } from './goods-in-out-order-edit.co
UiDatepickerModule,
UiDropdownModule,
SharedGoodsInOutOrderDetailsModule,
NotificationChannelControlModule,
],
exports: [SharedGoodsInOutOrderEditComponent],
declarations: [SharedGoodsInOutOrderEditComponent],

View File

@@ -0,0 +1,29 @@
import { Pipe, PipeTransform } from '@angular/core';
import { NotificationChannel } from '@swagger/oms';
@Pipe({
name: 'notificationsChannel',
})
export class NotificationsChannelPipe implements PipeTransform {
static channels = new Map<NotificationChannel, string>([
[1, 'E-Mail'],
[2, 'SMS'],
[4, 'Telefon'],
[8, 'Fax'],
[16, 'Brief'],
]);
transform(value: NotificationChannel = 0): any {
const result: string[] = [];
const channelKeys = Array.from(NotificationsChannelPipe.channels.keys());
channelKeys.forEach((key) => {
if (value & key) {
result.push(NotificationsChannelPipe.channels.get(key));
}
});
return result.join(' | ');
}
}

View File

@@ -8,6 +8,7 @@ import { TitlePipe } from './title.pipe';
import { ShowTagsPipe } from './show-tags.pipe';
import { ShelfEditActionInProgressPipe } from './shelf-edit-action-in-progress.pipe';
import { ShowCoverCompartmentCodePipe } from './show-cover-compartment-code.pipe';
import { NotificationsChannelPipe } from './notifications-channel.pipe';
@NgModule({
imports: [],
@@ -22,6 +23,7 @@ import { ShowCoverCompartmentCodePipe } from './show-cover-compartment-code.pipe
ShelfEditActionInProgressPipe,
ShowCoverCompartmentCodePipe,
ProcessingStatusOptionsPipe,
NotificationsChannelPipe,
],
declarations: [
ProcessingStatusPipe,
@@ -34,6 +36,7 @@ import { ShowCoverCompartmentCodePipe } from './show-cover-compartment-code.pipe
ShelfEditActionInProgressPipe,
ShowCoverCompartmentCodePipe,
ProcessingStatusOptionsPipe,
NotificationsChannelPipe,
],
providers: [],
})

View File

@@ -0,0 +1,25 @@
# NotificationChannelControl
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.4.
## Code scaffolding
Run `ng generate component component-name --project notification-channel-control` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project notification-channel-control`.
> Note: Don't forget to add `--project notification-channel-control` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build notification-channel-control` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build notification-channel-control`, go to the dist folder `cd dist/notification-channel-control` and run `npm publish`.
## Running unit tests
Run `ng test notification-channel-control` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma'),
],
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../../coverage/shared/notification-channel-control'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true,
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true,
});
};

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/shared/notification-channel-control",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "@shared/notification-channel-control",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^10.2.4",
"@angular/core": "^10.2.4"
},
"dependencies": {
"tslib": "^2.0.0"
}
}

View File

@@ -0,0 +1,5 @@
// start:ng42.barrel
export * from './notification-channel-control.component';
export * from './notification-channel-control.module';
export * from './validators';
// end:ng42.barrel

View File

@@ -0,0 +1,40 @@
<div class="nc-heading">
<label for="notificationChannel">
Benachrichtigung
</label>
<ui-checkbox-group
[ngModel]="notificationChannels$ | async"
(ngModelChange)="setNotificationChannels($event)"
[disabled]="notificationChannelControl?.disabled"
>
<ui-checkbox [value]="1" design="filled">E-Mail</ui-checkbox>
<ui-checkbox [value]="2" design="filled">SMS</ui-checkbox>
</ui-checkbox-group>
<div class="expand"></div>
<button *ngIf="displayToggle" type="button" class="more-toggle" (click)="toggle()" [class.open]="open$ | async">
<ui-icon icon="arrow_head" size="16px"></ui-icon>
</button>
</div>
<div class="nc-content" [class.open]="open$ | async">
<div class="nc-control-wrapper" *ngIf="displayEmail">
<label for="email">E-Mail</label>
<div class="input-wrapper" [class.has-error]="emailControl.touched && emailControl?.errors">
<input type="email" name="email" id="email" [formControl]="emailControl" placeholder="E-Mail*" />
<ng-container *ngIf="emailControl.touched && emailControl?.errors; let errors">
<span class="error" *ngIf="errors.required">Das Fehld E-Mail ist ein Pflichtfeld</span>
<span class="error" *ngIf="errors.pattern">Keine gültige E-Mail Adresse</span>
</ng-container>
</div>
</div>
<div class="nc-control-wrapper" *ngIf="displayMobile">
<label for="mobile">SMS</label>
<div class="input-wrapper" [class.has-error]="mobileControl.touched && mobileControl?.errors">
<input type="tel" name="mobile" id="mobile" [formControl]="mobileControl" placeholder="SMS*" />
<ng-container *ngIf="mobileControl.touched && mobileControl?.errors; let errors">
<span class="error" *ngIf="errors.required">Das Fehld SMS ist ein Pflichtfeld</span>
<span class="error" *ngIf="errors.pattern">Keine gültige Mobilnummer</span>
</ng-container>
</div>
</div>
</div>

View File

@@ -0,0 +1,90 @@
:host {
@apply block;
}
.nc-heading {
@apply flex flex-row items-center px-5 py-6 bg-white;
label {
width: 154px;
}
.expand {
@apply flex-grow;
}
.more-toggle {
@apply bg-transparent outline-none border-none;
ui-icon {
@apply transition-all transform rotate-90;
}
&.open {
ui-icon {
@apply -rotate-90;
}
}
}
}
.nc-content {
@apply h-0 overflow-hidden transition-all bg-white px-5 py-0;
&.open {
@apply h-auto overflow-auto;
}
}
.nc-control-wrapper {
@apply flex flex-row items-start pb-6;
label {
@apply mt-1;
width: 154px;
}
.input-wrapper {
@apply flex flex-col flex-grow pb-1;
input {
@apply flex-grow outline-none text-base font-bold border-0 border-b-2 border-solid pb-px-2;
}
input:disabled {
@apply bg-white;
}
&.has-error input {
@apply border-brand;
}
.error {
@apply text-right text-sm font-bold text-brand;
}
}
}
::ng-deep .customer shared-notification-channel-control {
.nc-control-wrapper {
.input-wrapper {
input {
@apply border-glitter;
}
}
}
}
::ng-deep .branch shared-notification-channel-control {
.nc-control-wrapper {
.input-wrapper {
input {
@apply border-munsell;
}
ui-icon {
@apply text-white !important;
}
}
}
}

View File

@@ -0,0 +1,122 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ControlContainer, FormGroup } from '@angular/forms';
import { ComponentStore } from '@ngrx/component-store';
import { NotificationChannel } from '@swagger/oms';
import { NEVER, Observable } from 'rxjs';
import { map, startWith, tap } from 'rxjs/operators';
export interface NotificationChannelControlComponentState {
open: boolean;
}
@Component({
selector: 'shared-notification-channel-control',
templateUrl: './notification-channel-control.component.html',
styleUrls: ['./notification-channel-control.component.scss'],
})
export class NotificationChannelControlComponent extends ComponentStore<NotificationChannelControlComponentState> implements OnInit {
notificationGroup: FormGroup;
get notificationChannelControl() {
return this.notificationGroup.get('selected');
}
get emailControl() {
return this.notificationGroup.get('email');
}
get displayEmail() {
return !!(this.notificationChannelControl.value & 1) && this.emailControl;
}
get mobileControl() {
return this.notificationGroup.get('mobile');
}
get displayMobile() {
return !!(this.notificationChannelControl.value & 2) && this.mobileControl;
}
get displayToggle() {
return this.displayEmail || this.displayMobile;
}
get hasError() {
return !!(this.mobileControl?.errors || this.emailControl?.errors);
}
readonly open$ = this.select((s) => s.open);
readonly options: NotificationChannel[] = [1, 2];
get notificationChannels() {
const value = this.notificationChannelControl?.value;
let values: NotificationChannel[] = [];
this.options?.forEach((option) => {
if (value & option) {
values.push(option);
}
});
return values;
}
notificationChannels$: Observable<NotificationChannel[]>;
constructor(private _notificationsGroup: ControlContainer, private _cdr: ChangeDetectorRef) {
super({
open: false,
});
}
ngOnInit(): void {
if (this._notificationsGroup.control instanceof FormGroup) {
this.notificationGroup = this._notificationsGroup.control;
}
this.initNotificationChannels$();
}
initNotificationChannels$() {
if (this.notificationGroup) {
this.notificationChannels$ = this.notificationChannelControl.valueChanges.pipe(startWith(this.notificationChannelControl.value)).pipe(
map((value) => {
let values = [];
this.options?.forEach((option) => {
if (value & option) {
values.push(option);
}
});
return values;
}),
tap(() => {
this.updateValidity();
if (this.hasError) {
this.toggle(true);
}
this._cdr.markForCheck();
})
);
} else {
this.notificationChannels$ = NEVER;
}
}
setNotificationChannels(notificationChannels: NotificationChannel[]) {
const notificationChannel = notificationChannels.reduce((val, current) => val | current, 0) as NotificationChannel;
this.notificationChannelControl.setValue(notificationChannel);
this.notificationChannelControl.markAsDirty();
}
toggle(value?: boolean) {
this.patchState({ open: value ?? !this.get((s) => s.open) });
}
updateValidity() {
this.emailControl?.updateValueAndValidity();
this.mobileControl?.updateValueAndValidity();
}
}

View File

@@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { UiCheckboxModule } from '@ui/checkbox';
import { UiIconModule } from '@ui/icon';
import { NotificationChannelControlComponent } from './notification-channel-control.component';
@NgModule({
declarations: [NotificationChannelControlComponent],
imports: [CommonModule, UiCheckboxModule, FormsModule, ReactiveFormsModule, UiIconModule],
exports: [NotificationChannelControlComponent],
})
export class NotificationChannelControlModule {}

View File

@@ -0,0 +1,26 @@
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])+)\])/;
export const emailNotificationValidator: ValidatorFn = (control: AbstractControl) => {
if (control.parent) {
const notificationChannel = control.parent.get('selected').value;
if (notificationChannel & 1) {
return Validators.required(control) ?? Validators.pattern(emailRegexPattern)(control);
}
}
return null;
};
export const mobileNotificationValidator: ValidatorFn = (control: AbstractControl) => {
if (control.parent) {
const notificationChannel = control.parent.get('selected').value;
if (notificationChannel & 2) {
return Validators.required(control) ?? UiValidators.phone(control);
}
}
return null;
};

View File

@@ -0,0 +1,5 @@
/*
* Public API Surface of notification-channel-control
*/
export * from './lib';

View File

@@ -0,0 +1,24 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(
path: string,
deep?: boolean,
filter?: RegExp
): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@@ -0,0 +1,25 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"enableIvy": false
}
}

View File

@@ -0,0 +1,17 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@@ -0,0 +1,17 @@
{
"extends": "../../../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"shared",
"camelCase"
],
"component-selector": [
true,
"element",
"shared",
"kebab-case"
]
}
}

View File

@@ -30,6 +30,7 @@ export { TenantDTO } from './models/tenant-dto';
export { ReadOnlyEntityDTOOfTenantDTOAndIReadOnlyTenant } from './models/read-only-entity-dtoof-tenant-dtoand-iread-only-tenant';
export { EntityDTO } from './models/entity-dto';
export { EntityStatus } from './models/entity-status';
export { TouchedBase } from './models/touched-base';
export { EntityDTOReferenceContainer } from './models/entity-dtoreference-container';
export { ExternalReferenceDTO } from './models/external-reference-dto';
export { ReadOnlyEntityDTOOfLabelDTOAndIReadOnlyLabel } from './models/read-only-entity-dtoof-label-dtoand-iread-only-label';

View File

@@ -20,4 +20,5 @@ export interface AvailabilityDTO {
supplierProductNumber?: string;
supplierSSC?: string;
supplierSSCText?: string;
supplyChannel?: string;
}

View File

@@ -1,6 +1,7 @@
/* tslint:disable */
import { TouchedBase } from './touched-base';
import { EntityStatus } from './entity-status';
export interface EntityDTO {
export interface EntityDTO extends TouchedBase{
changed?: string;
created?: string;
id?: number;

View File

@@ -1,6 +1,7 @@
/* tslint:disable */
import { TouchedBase } from './touched-base';
import { ExternalReferenceDTO } from './external-reference-dto';
export interface EntityDTOReferenceContainer {
export interface EntityDTOReferenceContainer extends TouchedBase{
displayLabel?: string;
enabled?: boolean;
externalReference?: ExternalReferenceDTO;

View File

@@ -1,6 +1,7 @@
/* tslint:disable */
import { TouchedBase } from './touched-base';
import { EntityDTOReferenceContainer } from './entity-dtoreference-container';
export interface EntityReferenceDTO {
export interface EntityReferenceDTO extends TouchedBase{
pId?: string;
reference?: EntityDTOReferenceContainer;
source?: number;

View File

@@ -1,6 +1,7 @@
/* tslint:disable */
import { TouchedBase } from './touched-base';
import { EntityStatus } from './entity-status';
export interface ExternalReferenceDTO {
export interface ExternalReferenceDTO extends TouchedBase{
externalChanged?: string;
externalCreated?: string;
externalNumber?: string;

View File

@@ -1,5 +1,6 @@
/* tslint:disable */
export interface GeoLocation {
import { TouchedBase } from './touched-base';
export interface GeoLocation extends TouchedBase{
altitude?: number;
latitude?: number;
longitude?: number;

View File

@@ -0,0 +1,3 @@
/* tslint:disable */
export interface TouchedBase {
}

View File

@@ -209,6 +209,13 @@ export { HistoryDTO } from './models/history-dto';
export { DiffDTO } from './models/diff-dto';
export { ResponseArgsOfOrderItemSubsetDTO } from './models/response-args-of-order-item-subset-dto';
export { StatusValues } from './models/status-values';
export { ListResponseArgsOfOrderItemSubsetTaskListItemDTO } from './models/list-response-args-of-order-item-subset-task-list-item-dto';
export { ResponseArgsOfIEnumerableOfOrderItemSubsetTaskListItemDTO } from './models/response-args-of-ienumerable-of-order-item-subset-task-list-item-dto';
export { OrderItemSubsetTaskListItemDTO } from './models/order-item-subset-task-list-item-dto';
export { EntityDTOContainerOfOrderItemSubsetTransitionDTO } from './models/entity-dtocontainer-of-order-item-subset-transition-dto';
export { OrderItemSubsetTransitionDTO } from './models/order-item-subset-transition-dto';
export { ReadOnlyEntityDTOOfOrderItemSubsetTransitionDTOAndIOrderItemStatusTransition } from './models/read-only-entity-dtoof-order-item-subset-transition-dtoand-iorder-item-status-transition';
export { OrderItemStatusValuesDTO } from './models/order-item-status-values-dto';
export { ResponseArgsOfPayerDTO } from './models/response-args-of-payer-dto';
export { ResponseArgsOfShippingAddressDTO } from './models/response-args-of-shipping-address-dto';
export { ResponseArgsOfIEnumerableOfBranchDTO } from './models/response-args-of-ienumerable-of-branch-dto';

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