Kulturpass - loader anzeige

This commit is contained in:
Lorenz Hilpert
2023-06-23 17:32:49 +02:00
parent 17aed38316
commit 9b8ea11866
11 changed files with 190 additions and 94 deletions

View File

@@ -0,0 +1,6 @@
{
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,11 @@
.shared-loader {
@apply relative block h-full;
}
.shared-loader__spinner {
@apply absolute inset-0 flex items-center justify-center;
}
.shared-loader__background {
@apply bg-white bg-opacity-75;
}

View File

@@ -0,0 +1,6 @@
<div class="shared-loader__spinner" *ngIf="showLoader" [class.shared-loader__background]="showBackground">
<ui-icon class="animate-spin" icon="spinner" [size]="spinnerSizeInRem"></ui-icon>
</div>
<div [class.invisible]="showLoader && contentHidden">
<ng-content></ng-content>
</div>

View File

@@ -0,0 +1,43 @@
import { BooleanInput, NumberInput, coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';
import { NgIf } from '@angular/common';
import { Component, ChangeDetectionStrategy, ViewEncapsulation, Input } from '@angular/core';
import { UiIconModule } from '@ui/icon';
@Component({
selector: 'shared-loader',
templateUrl: 'loader.component.html',
styleUrls: ['loader.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: { class: 'shared-loader' },
standalone: true,
imports: [NgIf, UiIconModule],
})
export class LoaderComponent {
@Input() loading: BooleanInput;
@Input() background: BooleanInput;
@Input() spinnerSize: NumberInput = 24;
@Input() hideContent: BooleanInput;
get showLoader() {
console.log('showLoader', this.loading);
return coerceBooleanProperty(this.loading);
}
get showBackground() {
return coerceBooleanProperty(this.background);
}
get spinnerSizeInRem() {
return coerceNumberProperty(this.spinnerSize) / 16 + 'rem';
}
get contentHidden() {
return coerceBooleanProperty(this.hideContent);
}
constructor() {}
}

View File

@@ -0,0 +1,9 @@
import { NgModule } from '@angular/core';
import { LoaderComponent } from './loader.component';
@NgModule({
imports: [LoaderComponent],
exports: [LoaderComponent],
})
export class LoaderModule {}

View File

@@ -0,0 +1,2 @@
export * from './lib/loader.component';
export * from './lib/loader.module';

View File

@@ -1,4 +1,8 @@
:host {
@apply block;
}
.wrapper {
@apply grid max-h-[calc(100vh-8rem)] h-[calc(100vh-8rem)];
grid-template-rows: repeat(4, auto) 1fr auto;
}

View File

@@ -1,64 +1,70 @@
<div>
<h1 class="text-center text-2xl font-bold">Kulturpass Warenkorb</h1>
<p class="text-center text-lg">Bitte scannen Sie Artikel, um den Code einzulösen.</p>
</div>
<div>
<shared-kulturpass-order-searchbox (addToShoppingCart)="addToShoppingCart($event)"></shared-kulturpass-order-searchbox>
</div>
<div class="border-y border-solid border-[#EFF1F5] py-3 px-4 -mx-4 flex flex-row items-center">
<div class="w-[6.875rem]">
Wertgutschein
</div>
<div class="ml-4 font-bold">
{{ kulturpassCode$ | async }}
</div>
</div>
<div class="border-b border-solid border-[#EFF1F5] py-3 px-4 -mx-4 flex flex-row items-center">
<div class="w-[6.875rem]">
Filiale
</div>
<div class="ml-4 font-bold">
{{ branch$ | async | branchName: 'name-address' }}
</div>
</div>
<div class="overflow-y-auto -mx-4 scroll-bar">
<div *ngIf="emptyShoppingCart$ | async" class="h-full grid items-center justify-center">
<h3 class="text-xl font-bold text-center text-gray-500">
Warenkorb ist leer, bitte suchen oder scannen <br />
Sie Artikel um den Warenkob zu füllen.
</h3>
</div>
<shared-kulturpass-order-item
class="border-b border-solid border-[#EFF1F5]"
*ngFor="let item of items$ | async; trackBy: trackItemById"
[item]="item"
(quantityChange)="quantityChange($event)"
>
</shared-kulturpass-order-item>
</div>
<div class="flex flex-row justify-evenly items-stretch border-t border-solid border-[#EFF1F5] py-3 px-4 -mx-4">
<div class="grid grid-flow-row text-sm">
<div class="grid grid-cols-2 gap-5">
<div>Guthaben</div>
<div class="font-bold text-right">{{ credit$ | async | currency: 'EUR' }}</div>
<shared-loader [loading]="fetchShoppingCart$ | async" background="true" spinnerSize="36">
<div class="wrapper">
<div>
<h1 class="text-center text-2xl font-bold">Kulturpass Warenkorb</h1>
<p class="text-center text-lg">Bitte scannen Sie Artikel, um den Code einzulösen.</p>
</div>
<div class="grid grid-cols-2 gap-5">
<div>Summe</div>
<div class="font-bold text-right">{{ total$ | async | currency: 'EUR' }}</div>
<div>
<shared-kulturpass-order-searchbox (addToShoppingCart)="addToShoppingCart($event)"></shared-kulturpass-order-searchbox>
</div>
<div class="grid grid-cols-2 gap-5">
<div>Restbetrag</div>
<div class="font-bold text-right" [class.text-brand]="negativeBalance$ | async">{{ balance$ | async | currency: 'EUR' }}</div>
<div class="border-y border-solid border-[#EFF1F5] py-3 px-4 -mx-4 flex flex-row items-center">
<div class="w-[6.875rem]">
Wertgutschein
</div>
<div class="ml-4 font-bold">
{{ kulturpassCode$ | async }}
</div>
</div>
<div class="border-b border-solid border-[#EFF1F5] py-3 px-4 -mx-4 flex flex-row items-center">
<div class="w-[6.875rem]">
Filiale
</div>
<div class="ml-4 font-bold">
{{ branch$ | async | branchName: 'name-address' }}
</div>
</div>
<div class="overflow-y-auto -mx-4 scroll-bar">
<div *ngIf="emptyShoppingCart$ | async" class="h-full grid items-center justify-center">
<h3 class="text-xl font-bold text-center text-gray-500">
Warenkorb ist leer, bitte suchen oder scannen <br />
Sie Artikel um den Warenkob zu füllen.
</h3>
</div>
<shared-kulturpass-order-item
class="border-b border-solid border-[#EFF1F5]"
*ngFor="let item of items$ | async; trackBy: trackItemById"
[item]="item"
(quantityChange)="quantityChange($event)"
>
</shared-kulturpass-order-item>
</div>
<div class="flex flex-row justify-evenly items-stretch border-t border-solid border-[#EFF1F5] py-3 px-4 -mx-4">
<div class="grid grid-flow-row text-sm">
<div class="grid grid-cols-2 gap-5">
<div>Guthaben</div>
<div class="font-bold text-right">{{ credit$ | async | currency: 'EUR' }}</div>
</div>
<div class="grid grid-cols-2 gap-5">
<div>Summe</div>
<div class="font-bold text-right">{{ total$ | async | currency: 'EUR' }}</div>
</div>
<div class="grid grid-cols-2 gap-5">
<div>Restbetrag</div>
<div class="font-bold text-right" [class.text-brand]="negativeBalance$ | async">{{ balance$ | async | currency: 'EUR' }}</div>
</div>
</div>
<div class="grid items-end justify-end">
<button
type="button"
class="bg-brand text-white px-6 py-3 font-bold rounded-full disabled:bg-disabled-branch disabled:text-active-branch"
[disabled]="orderButtonDisabled$ | async"
(click)="order()"
>
<shared-loader [loading]="ordering$ | async" hideContent="true">
Kauf abschließen und Rechnung drucken
</shared-loader>
</button>
</div>
</div>
</div>
<div class="grid items-end justify-end">
<button
type="button"
class="bg-brand text-white px-6 py-3 font-bold rounded-full disabled:bg-disabled-branch disabled:text-active-branch"
[disabled]="invalidShoppingCart$ | async"
(click)="order()"
>
Kauf abschließen und Rechnung drucken
</button>
</div>
</div>
</shared-loader>

View File

@@ -10,6 +10,7 @@ import { CommonModule } from '@angular/common';
import { BranchNamePipe } from '@shared/pipes/branch';
import { combineLatest } from 'rxjs';
import { KulturpassOrderItemComponent } from './kulturpass-order-item/kulturpass-order-item.component';
import { LoaderComponent } from '@shared/components/loader';
@Component({
selector: 'shared-kulturpass-order-modal',
@@ -18,12 +19,14 @@ import { KulturpassOrderItemComponent } from './kulturpass-order-item/kulturpass
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'shared-kulturpass-order-modal' },
standalone: true,
imports: [KulturpassOrderSearchboxComponent, CommonModule, BranchNamePipe, KulturpassOrderItemComponent],
imports: [KulturpassOrderSearchboxComponent, CommonModule, BranchNamePipe, KulturpassOrderItemComponent, LoaderComponent],
providers: [provideComponentStore(KulturpassOrderModalStore)],
})
export class KulturpassOrderModalComponent implements OnInit {
branch$ = this._store.branch$;
fetchShoppingCart$ = this._store.fetchShoppingCart$;
items$ = this._store.shoppingCart$.pipe(map((shoppingCart) => shoppingCart?.items?.map((item) => item.data) ?? []));
trackItemById = (index: number, item: ShoppingCartItemDTO) => item.id;
@@ -40,8 +43,10 @@ export class KulturpassOrderModalComponent implements OnInit {
emptyShoppingCart$ = this.items$.pipe(map((items) => items.length === 0));
invalidShoppingCart$ = combineLatest([this.emptyShoppingCart$, this.negativeBalance$]).pipe(
map(([emptyShoppingCart, negativeBalance]) => emptyShoppingCart || negativeBalance)
ordering$ = this._store.ordering$;
orderButtonDisabled$ = combineLatest([this.emptyShoppingCart$, this.negativeBalance$, this.ordering$]).pipe(
map(([emptyShoppingCart, negativeBalance, ordering]) => emptyShoppingCart || negativeBalance || ordering)
);
constructor(private _modalRef: UiModalRef<void, KulturpassOrderModalData>, private _store: KulturpassOrderModalStore) {}

View File

@@ -3,7 +3,7 @@ import { ComponentStore, OnStoreInit, tapResponse } from '@ngrx/component-store'
import { AddToShoppingCartDTO, BranchDTO, CheckoutDTO, ShoppingCartDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { DomainCheckoutService } from '@domain/checkout';
import { switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { BranchService, BuyerDTO, OrderDTO, OrderItemListItemDTO, ResponseArgsOfIEnumerableOfBranchDTO } from '@swagger/oms';
import { BranchService, OrderDTO, OrderItemListItemDTO, ResponseArgsOfIEnumerableOfBranchDTO } from '@swagger/oms';
import { Observable } from 'rxjs';
import { AuthService } from '@core/auth';
import { UiModalService } from '@ui/modal';
@@ -11,8 +11,10 @@ import { UiModalService } from '@ui/modal';
export interface KulturpassOrderModalState {
orderItemListItem?: OrderItemListItemDTO;
shoppingCart?: ShoppingCartDTO;
fetchShoppingCart?: boolean;
branch?: BranchDTO;
order?: OrderDTO;
ordering?: boolean;
}
@Injectable()
@@ -35,6 +37,10 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
return this.get((s) => s.order);
}
get fetchShoppingCart() {
return this.get((s) => s.fetchShoppingCart);
}
constructor(
private _checkoutService: DomainCheckoutService,
private _branchService: BranchService,
@@ -60,6 +66,12 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
readonly updateOrder = this.updater((state, order: OrderDTO) => ({ ...state, order }));
readonly fetchShoppingCart$ = this.select((state) => state.fetchShoppingCart);
readonly updateFetchShoppingCart = this.updater((state, fetchShoppingCart: boolean) => ({ ...state, fetchShoppingCart }));
readonly ordering$ = this.select((state) => state.ordering);
loadBranch = this.effect(($) =>
$.pipe(switchMap(() => this._branchService.BranchGetBranches({}).pipe(tapResponse(this.handleBranchResponse, this.handleBranchError))))
);
@@ -78,6 +90,7 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
orderItemListItem$.pipe(
tap((orderItemListItem) => {
this.patchState({ orderItemListItem });
this.updateFetchShoppingCart(true);
}),
switchMap((orderItemListItem) =>
this._checkoutService
@@ -91,32 +104,16 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
this.patchState({ shoppingCart: res });
this._checkoutService.setBuyer({ processId: this.processId, buyer: this.order.buyer });
this._checkoutService.setPayer({ processId: this.processId, payer: this.order.billing?.data });
this.updateFetchShoppingCart(false);
};
handleCreateShoppingCartError = (err: any) => {
this._modal.error('Fehler beim Laden des Warenkorbs', err);
this.updateFetchShoppingCart(false);
};
// startCheckoutProcess = this.effect((orderItemListItem$: Observable<OrderItemListItemDTO>) =>
// orderItemListItem$.pipe(
// tap((orderItemListItem) => {
// this._checkoutService.removeProcess({ processId: this.processId });
// this.patchState({ orderItemListItem });
// }),
// switchMap((orderItemListItem) =>
// this._checkoutService
// .getCheckout({ processId: this.processId, refresh: true })
// .pipe(tapResponse(this.handleCreateCheckoutResponse, this.handleCreateCheckoutError))
// )
// )
// );
// handleCreateCheckoutResponse = (res: CheckoutDTO) => {
// this.updateCheckout(res);
// };
// handleCreateCheckoutError = (err: any) => {};
addItem = this.effect((add$: Observable<AddToShoppingCartDTO>) =>
add$.pipe(
withLatestFrom(this.orderItemListItem$),
@@ -160,6 +157,7 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
orderItems = this.effect(($: Observable<void>) =>
$.pipe(
tap(() => this.patchState({ ordering: true })),
withLatestFrom(this.orderItemListItem$),
switchMap(([_, orderItemListItem]) =>
this._checkoutService
@@ -172,11 +170,10 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
)
);
handleOrderResponse = (res: any) => {
console.log(res);
};
handleOrderResponse = (res: any) => {};
handleOrderError = (err: any) => {
this._modal.error('Fehler beim Bestellen', err);
this.patchState({ ordering: false });
};
}

View File

@@ -6,6 +6,8 @@ import { Observable, Subject } from 'rxjs';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCatalogService } from '@domain/catalog';
import { AddToShoppingCartDTO, AvailabilityDTO, BranchDTO } from '@swagger/checkout';
import { tap } from 'rxjs/operators';
import { UiModalService } from '@ui/modal';
export interface KulturpassOrderSearchboxState {
query: string;
@@ -31,7 +33,11 @@ export class KulturpassOrderSearchboxStore extends ComponentStore<KulturpassOrde
hint$ = new Subject<string>();
constructor(private _catalogService: DomainCatalogService, private _availabilityService: DomainAvailabilityService) {
constructor(
private _catalogService: DomainCatalogService,
private _availabilityService: DomainAvailabilityService,
private _modal: UiModalService
) {
super({ query: '', fetching: false });
}
@@ -62,11 +68,12 @@ export class KulturpassOrderSearchboxStore extends ComponentStore<KulturpassOrde
};
handleBranchError = (err) => {
console.log(err);
this._modal.error('Fehler beim Laden der Filiale', err);
};
readonly search = this.effect(($) =>
$.pipe(
tap(() => this.patchState({ fetching: true })),
withLatestFrom(this.query$),
switchMap(([_, query]) =>
this._catalogService.getDetailsByEan({ ean: query }).pipe(tapResponse(this.handleSearchResponse, this.handleSearchError))
@@ -86,7 +93,7 @@ export class KulturpassOrderSearchboxStore extends ComponentStore<KulturpassOrde
handleSearchError = (err) => {
this.hint$.next('Artikel nicht gefunden');
this.patchState({ query: '' });
this.patchState({ query: '', fetching: false });
};
fetchAvailability = this.effect((item$: Observable<ItemDTO>) =>
@@ -109,7 +116,7 @@ export class KulturpassOrderSearchboxStore extends ComponentStore<KulturpassOrde
handleAvailabilityResponse = (item: ItemDTO) => (availability: AvailabilityDTO) => {
if (!this._availabilityService.isAvailable({ availability }) || availability.inStock < 1) {
this.hint$.next('Artikel nicht vorrätig');
this.patchState({ query: '' });
this.patchState({ query: '', fetching: false });
return;
}
@@ -131,11 +138,11 @@ export class KulturpassOrderSearchboxStore extends ComponentStore<KulturpassOrde
this.addToShoppingCart$.next(addToShoppingCartDTO);
this.patchState({ query: '' });
this.patchState({ query: '', fetching: false });
};
handleAvailabilityError = (err) => {
this.hint$.next('Artikel nicht vorrätig');
this.patchState({ query: '' });
this._modal.error('Fehler beim Laden der Verfügbarkeit', err);
this.patchState({ query: '', fetching: false });
};
}