Merge branch 'feature/rd-customer' into develop

This commit is contained in:
Lorenz Hilpert
2023-07-13 19:09:53 +02:00
140 changed files with 5250 additions and 354 deletions

View File

@@ -2,8 +2,8 @@ import { NgModule } from '@angular/core';
import { ProductImagePipe } from './product-image.pipe';
@NgModule({
declarations: [ProductImagePipe],
imports: [],
declarations: [],
imports: [ProductImagePipe],
exports: [ProductImagePipe],
})
export class ProductImageModule {}

View File

@@ -3,6 +3,8 @@ import { ProductImageService } from './product-image.service';
@Pipe({
name: 'productImage',
standalone: true,
pure: true,
})
export class ProductImagePipe implements PipeTransform {
constructor(private imageService: ProductImageService) {}

View File

@@ -71,12 +71,12 @@ const routes: Routes = [
},
{
path: 'customer',
loadChildren: () => import('@page/customer').then((m) => m.PageCustomerModule),
loadChildren: () => import('@page/customer-rd').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerGuard],
},
{
path: ':processId/customer',
loadChildren: () => import('@page/customer').then((m) => m.PageCustomerModule),
loadChildren: () => import('@page/customer-rd').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},

View File

@@ -243,10 +243,19 @@
"name": "arrow-drop-up",
"data": "m280-400 200-201 200 201H280Z",
"viewBox": "0 -960 960 960"
},
{
"name": "refresh",
"data": "M480-160q-133 0-226.5-93.5T160-480q0-133 93.5-226.5T480-800q85 0 149 34.5T740-671v-129h60v254H546v-60h168q-38-60-97-97t-137-37q-109 0-184.5 75.5T220-480q0 109 75.5 184.5T480-220q83 0 152-47.5T728-393h62q-29 105-115 169t-195 64Z",
"viewBox": "0 -960 960 960"
}
],
"aliases": [
{
"alias": "barcode-scanner",
"name": "barcode-scan"
},
{
"alias": "d-account",
"name": "account"
@@ -255,6 +264,30 @@
"alias": "d-no-account",
"name": "package-variant-closed"
},
{
"alias": "Rücklage",
"name": "isa-shopping-bag"
},
{
"alias": "Abholung",
"name": "isa-box-out"
},
{
"alias": "Versand",
"name": "isa-truck"
},
{
"alias": "DIG-Versand",
"name": "isa-truck"
},
{
"alias": "B2B-Versand",
"name": "isa-b2b-truck"
},
{
"alias": "Download",
"name": "isa-download"
},
{
"name": "isa-audio",
"alias": "AU"

View File

@@ -67,3 +67,18 @@
@apply block bg-gray-300 h-6;
animation: load 1s ease-in-out infinite;
}
@layer components {
.input-control {
@apply rounded border border-solid border-[#AEB7C1] px-4 py-[1.125rem] outline-none;
}
// .input-control:focus,
// .input-control:not(:placeholder-shown) {
// @apply bg-white;
// }
.input-control.ng-touched.ng-invalid {
@apply border-brand;
}
}

View File

@@ -8,7 +8,7 @@
</div>
<div class="grid grid-cols-2 gap-4">
<span class="text-[22px] font-bold"> {{ customer?.lastName }} {{ customer?.firstName }} </span>
<span class="text-[22px] font-bold"> {{ customerName }} </span>
<div>
<div class="flex flex-col">
<div class="flex flex-row items-center">

View File

@@ -15,5 +15,19 @@ export class CustomerResultListItemFullComponent {
@Input()
customer: CustomerInfoDTO;
get customerName() {
if (!this.customer) {
return '';
}
const isB2b = this.customer.features?.some((f) => f.key === 'b2b') && this.customer.organisation?.name;
if (!isB2b) {
return `${this.customer.lastName} ${this.customer.firstName}`;
} else {
return `${this.customer.organisation.name}`;
}
}
constructor() {}
}

View File

@@ -1,3 +1,7 @@
:host {
@apply flex flex-col bg-surface text-surface-content h-[11.313rem] mb-[0.625rem] p-4;
}
:host.active {
@apply border border-solid border-[#0556B4] bg-[#D8DFE5];
}

View File

@@ -8,7 +8,7 @@
</div>
<div class="flex flex-col justify-between grow">
<span class="text-[22px] font-bold"> {{ customer?.lastName }} {{ customer?.firstName }} </span>
<span class="text-[22px] font-bold"> {{ customerName }} </span>
<div>
<div class="flex flex-col">
<div class="flex flex-row items-center">

View File

@@ -15,5 +15,19 @@ export class CustomerResultListItemComponent {
@Input()
customer: CustomerInfoDTO;
get customerName() {
if (!this.customer) {
return '';
}
const isB2b = this.customer.features?.some((f) => f.key === 'b2b') && this.customer.organisation?.name;
if (!isB2b) {
return `${this.customer.lastName} ${this.customer.firstName}`;
} else {
return `${this.customer.organisation.name}`;
}
}
constructor() {}
}

View File

@@ -1,19 +1,35 @@
<cdk-virtual-scroll-viewport itemSize="98" class="h-[calc(100vh-18.875rem)]" *ngIf="!compact">
<cdk-virtual-scroll-viewport
itemSize="98"
class="h-[calc(100vh-18.875rem)]"
*ngIf="!compact"
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<a
*cdkVirtualFor="let customer of customers$ | async; trackBy: trackByFn"
*cdkVirtualFor="let customer of customers; trackBy: trackByFn"
[routerLink]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })?.path"
[queryParams]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })?.queryParams"
[queryParamsHandling]="'merge'"
routerLinkActive
#rla="routerLinkActive"
>
<page-customer-result-list-item-full [customer]="customer"></page-customer-result-list-item-full>
<page-customer-result-list-item-full [class.active]="rla.isActive" [customer]="customer"></page-customer-result-list-item-full>
</a>
</cdk-virtual-scroll-viewport>
<cdk-virtual-scroll-viewport itemSize="98" class="h-[calc(100vh-20.75rem)]" *ngIf="compact">
<cdk-virtual-scroll-viewport
itemSize="98"
class="h-[calc(100vh-20.75rem)]"
*ngIf="compact"
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<a
*cdkVirtualFor="let customer of customers$ | async; trackBy: trackByFn"
*cdkVirtualFor="let customer of customers; trackBy: trackByFn"
[routerLink]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })?.path"
[queryParams]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })?.queryParams"
[queryParamsHandling]="'merge'"
routerLinkActive
#rla="routerLinkActive"
>
<page-customer-result-list-item [customer]="customer"></page-customer-result-list-item>
<page-customer-result-list-item [class.active]="rla.isActive" [customer]="customer"></page-customer-result-list-item>
</a>
</cdk-virtual-scroll-viewport>

View File

@@ -1,9 +1,8 @@
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output } from '@angular/core';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, ViewChild } from '@angular/core';
import { CustomerInfoDTO } from '@swagger/crm';
import { CrmCustomerService } from '@domain/crm';
import { map } from 'rxjs/operators';
import { BooleanInput, NumberInput, coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';
import { CustomerSearchNavigation } from '../../navigations';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
@Component({
selector: 'page-customer-result-list',
@@ -21,7 +20,8 @@ export class CustomerResultListComponent {
this._compact = coerceBooleanProperty(value);
}
customers$ = this._customerService.getCustomers('Lorenz Hilpert').pipe(map((res) => res.result));
@Input()
customers: CustomerInfoDTO[];
@Input()
selected: CustomerInfoDTO;
@@ -29,6 +29,9 @@ export class CustomerResultListComponent {
@Output()
selectedChange = new EventEmitter<CustomerInfoDTO>();
@Output()
endReached = new EventEmitter<void>();
private _processId: NumberInput;
@Input()
get processId() {
@@ -40,5 +43,19 @@ export class CustomerResultListComponent {
trackByFn = (_: number, item: CustomerInfoDTO) => item?.id;
constructor(private _customerService: CrmCustomerService, public customerSearchNavigation: CustomerSearchNavigation) {}
@ViewChild(CdkVirtualScrollViewport, { static: false })
viewport: CdkVirtualScrollViewport;
@Input()
set scrollIndex(index: number) {
this.viewport?.scrollToIndex(index);
}
constructor(public customerSearchNavigation: CustomerSearchNavigation) {}
scrolledIndexChange(index: number) {
if (index && this.customers.length <= this.viewport?.getRenderedRange()?.end) {
this.endReached.emit();
}
}
}

View File

@@ -1,4 +1,4 @@
<ui-checkbox [formControl]="control" [tabindex]="tabIndexStart" [autofocus]="focusAfterInit" [readonly]="readonly">
<shared-checkbox [formControl]="control" [tabindex]="tabIndexStart" [autofocus]="focusAfterInit" [readonly]="readonly">
Bitte unterschreiben Sie die Teilnahmebedingungen im Kundenkartenformular, um die Prämiennutzung zu ermöglichen.
{{ requiredMark ? '*' : '' }}
</ui-checkbox>
</shared-checkbox>

View File

@@ -2,12 +2,11 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AcceptAGBFormBlockComponent } from './accept-agb-form-block.component';
import { UiCheckboxModule } from '@ui/checkbox';
import { ReactiveFormsModule } from '@angular/forms';
import { UiCommonModule } from '@ui/common';
import { CheckboxComponent } from '@shared/components/checkbox';
@NgModule({
imports: [CommonModule, UiCommonModule, UiCheckboxModule, ReactiveFormsModule],
imports: [CommonModule, CheckboxComponent, ReactiveFormsModule],
exports: [AcceptAGBFormBlockComponent],
declarations: [AcceptAGBFormBlockComponent],
})

View File

@@ -1,25 +1,62 @@
<ng-container [formGroup]="control">
<ui-form-control label="Straße" [requiredMark]="requiredMarks.includes('street') ? '*' : ''">
<input uiInput type="text" formControlName="street" [tabindex]="tabIndexStart" [autofocus]="focusAfterInit" [readonly]="readonly" />
</ui-form-control>
<ui-form-control label="Hausnummer" [requiredMark]="requiredMarks.includes('streetNumber') ? '*' : ''">
<input uiInput type="text" formControlName="streetNumber" [tabindex]="tabIndexStart + 1" [readonly]="readonly" />
</ui-form-control>
<ui-form-control label="PLZ" [requiredMark]="requiredMarks.includes('zipCode') ? '*' : ''">
<input uiInput type="text" formControlName="zipCode" [tabindex]="tabIndexStart + 2" [readonly]="readonly" />
</ui-form-control>
<ui-form-control label="Ort" [requiredMark]="requiredMarks.includes('city') ? '*' : ''">
<input uiInput type="text" formControlName="city" [tabindex]="tabIndexStart + 3" [readonly]="readonly" />
</ui-form-control>
<shared-form-control label="Straße">
<input
placeholder="Straße"
class="input-control"
type="text"
formControlName="street"
[tabindex]="tabIndexStart"
[autofocus]="focusAfterInit"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control label="Hausnummer">
<input
placeholder="Hausnummer"
class="input-control"
type="text"
formControlName="streetNumber"
[tabindex]="tabIndexStart + 1"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control label="PLZ">
<input
placeholder="PLZ"
class="input-control"
type="text"
formControlName="zipCode"
[tabindex]="tabIndexStart + 2"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control label="Ort">
<input
placeholder="Ort"
class="input-control"
type="text"
formControlName="city"
[tabindex]="tabIndexStart + 3"
[readonly]="readonly"
/>
</shared-form-control>
<ui-form-control class="col-span-2" label="Adresszusatz" [clearable]="false" [requiredMark]="requiredMarks.includes('info') ? '*' : ''">
<input uiInput type="text" formControlName="info" [tabindex]="tabIndexStart + 4" [readonly]="readonly" />
</ui-form-control>
<shared-form-control class="col-span-2" label="Adresszusatz">
<input
placeholder="Adresszusatz"
class="input-control"
type="text"
formControlName="info"
[tabindex]="tabIndexStart + 4"
[readonly]="readonly"
/>
</shared-form-control>
<ui-form-control class="col-span-2" label="Land" [clearable]="true" [requiredMark]="requiredMarks.includes('country') ? '*' : ''">
<ui-select formControlName="country" [tabindex]="tabIndexStart + 5" [readonly]="readonly">
<ui-select-option *ngFor="let country of countries || (countries$ | async)" [label]="country.name" [value]="country.isO3166_A_3">
</ui-select-option>
</ui-select>
</ui-form-control>
<shared-form-control class="col-span-2" label="Land">
<shared-select placeholder="Land" formControlName="country" [tabindex]="tabIndexStart + 5" [readonly]="readonly">
<shared-select-option *ngFor="let country of countries || (countries$ | async)" [value]="country.isO3166_A_3">
{{ country.name }}
</shared-select-option>
</shared-select>
</shared-form-control>
</ng-container>

View File

@@ -2,14 +2,12 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AddressFormBlockComponent } from './address-form-block.component';
import { UiFormControlModule } from '@ui/form-control';
import { UiInputModule } from '@ui/input';
import { UiSelectModule } from '@ui/select';
import { ReactiveFormsModule } from '@angular/forms';
import { UiCommonModule } from '@ui/common';
import { FormControlComponent } from '@shared/components/form-control';
import { SelectModule } from '@shared/components/select';
@NgModule({
imports: [CommonModule, UiCommonModule, UiFormControlModule, UiInputModule, UiSelectModule, ReactiveFormsModule],
imports: [CommonModule, ReactiveFormsModule, FormControlComponent, SelectModule],
exports: [AddressFormBlockComponent],
declarations: [AddressFormBlockComponent],
})

View File

@@ -1,3 +1,12 @@
<ui-form-control label="Geburtsdatum (TT.MM.JJJJ)" [requiredMark]="requiredMark ? '*' : ''">
<input uiDateInput type="text" [formControl]="control" [tabindex]="tabIndexStart" [autofocus]="focusAfterInit" [readonly]="readonly" />
</ui-form-control>
<shared-form-control label="Geburtsdatum (TT.MM.JJJJ)">
<input
placeholder="Geburtsdatum"
class="input-control"
uiDateInput
type="text"
[formControl]="control"
[tabindex]="tabIndexStart"
[autofocus]="focusAfterInit"
[readonly]="readonly"
/>
</shared-form-control>

View File

@@ -2,13 +2,11 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BirthDateFormBlockComponent } from './birth-date-form-block.component';
import { UiFormControlModule } from '@ui/form-control';
import { UiInputModule } from '@ui/input';
import { ReactiveFormsModule } from '@angular/forms';
import { UiCommonModule } from '@ui/common';
import { FormControlComponent } from '@shared/components/form-control';
@NgModule({
imports: [CommonModule, ReactiveFormsModule, UiFormControlModule, UiInputModule, UiCommonModule],
imports: [CommonModule, ReactiveFormsModule, FormControlComponent],
exports: [BirthDateFormBlockComponent],
declarations: [BirthDateFormBlockComponent],
})

View File

@@ -1,6 +1,12 @@
<ui-checkbox [formControl]="deviatingAddress" [tabindex]="tabIndexStart" [autofocus]="focusAfterInit" [readonly]="readonly">
<shared-checkbox
class="mb-4"
[formControl]="deviatingAddress"
[tabindex]="tabIndexStart"
[autofocus]="focusAfterInit"
[readonly]="readonly"
>
<ng-content></ng-content>
</ui-checkbox>
</shared-checkbox>
<div class="address-block" *ngIf="control.value.deviatingAddress">
<div class="wrapper">
<app-organisation-form-block

View File

@@ -2,26 +2,24 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DeviatingAddressFormBlockComponent } from './deviating-address-form-block.component';
import { UiCheckboxModule } from '@ui/checkbox';
import { AddressFormBlockModule } from '../address';
import { NameFormBlockModule } from '../name';
import { ReactiveFormsModule } from '@angular/forms';
import { OrganisationFormBlockModule } from '../organisation';
import { EmailFormBlockModule } from '../email';
import { PhoneNumbersFormBlockModule } from '../phone-numbers';
import { UiCommonModule } from '@ui/common';
import { CheckboxComponent } from '@shared/components/checkbox';
@NgModule({
imports: [
CommonModule,
ReactiveFormsModule,
UiCheckboxModule,
NameFormBlockModule,
AddressFormBlockModule,
OrganisationFormBlockModule,
EmailFormBlockModule,
PhoneNumbersFormBlockModule,
UiCommonModule,
CheckboxComponent,
],
exports: [DeviatingAddressFormBlockComponent],
declarations: [DeviatingAddressFormBlockComponent],

View File

@@ -1,3 +1,11 @@
<ui-form-control label="E-Mail" [requiredMark]="requiredMark ? '*' : ''">
<input uiInput type="mail" [formControl]="control" [tabindex]="tabIndexStart" [autofocus]="focusAfterInit" [readonly]="readonly" />
</ui-form-control>
<shared-form-control label="E-Mail">
<input
placeholder="E-Mail"
type="mail"
class="input-control"
[formControl]="control"
[tabindex]="tabIndexStart"
[autofocus]="focusAfterInit"
[readonly]="readonly"
/>
</shared-form-control>

View File

@@ -3,12 +3,10 @@ import { CommonModule } from '@angular/common';
import { EmailFormBlockComponent } from './email-form-block.component';
import { ReactiveFormsModule } from '@angular/forms';
import { UiFormControlModule } from '@ui/form-control';
import { UiInputModule } from '@ui/input';
import { UiCommonModule } from '@ui/common';
import { FormControlComponent } from '@shared/components/form-control';
@NgModule({
imports: [CommonModule, UiCommonModule, ReactiveFormsModule, UiFormControlModule, UiInputModule],
imports: [CommonModule, ReactiveFormsModule, FormControlComponent],
exports: [EmailFormBlockComponent],
declarations: [EmailFormBlockComponent],
})

View File

@@ -1,6 +1,6 @@
<div class="interests-description">Geben Sie Interessen an, um Ihre persönlichen Kontoangaben zu verfeinern.</div>
<div class="interests-wrapper" [formGroup]="control">
<ui-checkbox
<shared-checkbox
*ngFor="let pair of interests | keyvalue; let idx = index"
[formControlName]="pair.key"
[tabindex]="tabIndexStart + idx"
@@ -8,5 +8,5 @@
[readonly]="readonly"
>
{{ pair.value }}
</ui-checkbox>
</shared-checkbox>
</div>

View File

@@ -2,12 +2,11 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { InterestsFormBlockComponent } from './interests-form-block.component';
import { UiCheckboxModule } from '@ui/checkbox';
import { ReactiveFormsModule } from '@angular/forms';
import { UiCommonModule } from '@ui/common';
import { CheckboxComponent } from '@shared/components/checkbox';
@NgModule({
imports: [CommonModule, UiCommonModule, ReactiveFormsModule, UiCheckboxModule],
imports: [CommonModule, ReactiveFormsModule, CheckboxComponent],
exports: [InterestsFormBlockComponent],
declarations: [InterestsFormBlockComponent],
})

View File

@@ -1,16 +1,18 @@
<ng-container [formGroup]="control">
<label class="grid grid-flow-row gap-1">
<span>Anrede</span>
<shared-select formControlName="gender" placeholder="Anrede" [readonly]="readonly" [tabindex]="tabIndexStart">
<shared-form-control label="Anrede">
<shared-select
formControlName="gender"
placeholder="Anrede"
[readonly]="readonly"
[tabindex]="tabIndexStart"
[autofocus]="focusAfterInit"
>
<shared-select-option [value]="2">Herr</shared-select-option>
<shared-select-option [value]="4">Frau</shared-select-option>
</shared-select>
</label>
<label class="grid grid-flow-row gap-1">
<span>Titel</span>
</shared-form-control>
<shared-form-control label="Titel">
<shared-select formControlName="title" placeholder="Titel" [readonly]="readonly" [tabindex]="tabIndexStart + 1">
<shared-select-option value="Dipl.-Ing.">Dipl.-Ing.</shared-select-option>
<shared-select-option value="Dr.">Dr.</shared-select-option>
@@ -19,29 +21,27 @@
<shared-select-option value="Prof. Dr.">Prof. Dr.</shared-select-option>
<shared-select-option value="RA">RA</shared-select-option>
</shared-select>
</label>
</shared-form-control>
<!-- <ui-form-control [clearable]="!readonly" label="Anrede" [requiredMark]="requiredMarks.includes('gender') ? '*' : ''">
<ui-select formControlName="gender" [tabindex]="tabIndexStart" [autofocus]="focusAfterInit" [readonly]="readonly">
<ui-select-option [value]="2" label="Herr"></ui-select-option>
<ui-select-option [value]="4" label="Frau"></ui-select-option>
</ui-select>
</ui-form-control> -->
<!-- <ui-form-control [clearable]="!readonly" label="Titel" [requiredMark]="requiredMarks.includes('title') ? '*' : ''">
<ui-select formControlName="title" [tabindex]="tabIndexStart + 1" [readonly]="readonly">
<ui-select-option value="Dipl.-Ing." label="Dipl.-Ing."></ui-select-option>
<ui-select-option value="Dr." label="Dr."></ui-select-option>
<ui-select-option value="Dr. med." label="Dr. med."></ui-select-option>
<ui-select-option value="Prof." label="Prof."></ui-select-option>
<ui-select-option value="Prof. Dr." label="Prof. Dr."></ui-select-option>
<ui-select-option value="RA" label="RA"></ui-select-option>
</ui-select>
</ui-form-control> -->
<shared-form-control label="Nachname">
<input
class="input-control"
placeholder="Nachname"
type="text"
formControlName="lastName"
[tabindex]="tabIndexStart + 2"
[readonly]="readonly"
/>
</shared-form-control>
<ui-form-control label="Nachname" [requiredMark]="requiredMarks.includes('firstName') ? '*' : ''">
<input uiInput type="text" formControlName="lastName" [tabindex]="tabIndexStart + 2" [readonly]="readonly" />
</ui-form-control>
<ui-form-control label="Vorname" [requiredMark]="requiredMarks.includes('lastName') ? '*' : ''">
<input uiInput type="text" formControlName="firstName" [tabindex]="tabIndexStart + 3" [readonly]="readonly" />
</ui-form-control>
<shared-form-control label="Vorname">
<input
class="input-control"
placeholder="Vorname"
type="text"
formControlName="firstName"
[tabindex]="tabIndexStart + 3"
[readonly]="readonly"
/>
</shared-form-control>
</ng-container>

View File

@@ -2,14 +2,12 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NameFormBlockComponent } from './name-form-block.component';
import { UiFormControlModule } from '@ui/form-control';
import { UiInputModule } from '@ui/input';
import { ReactiveFormsModule } from '@angular/forms';
import { UiCommonModule } from '@ui/common';
import { SelectModule } from '@shared/components/select';
import { FormControlComponent } from '@shared/components/form-control';
@NgModule({
imports: [CommonModule, UiCommonModule, ReactiveFormsModule, UiFormControlModule, UiInputModule, SelectModule],
imports: [CommonModule, ReactiveFormsModule, SelectModule, FormControlComponent],
exports: [NameFormBlockComponent],
declarations: [NameFormBlockComponent],
})

View File

@@ -1,3 +1,3 @@
<ui-checkbox [formControl]="control" [tabindex]="tabIndexStart" [autofocus]="focusAfterInit" [readonly]="readonly">
<shared-checkbox [formControl]="control" [tabindex]="tabIndexStart" [autofocus]="focusAfterInit" [readonly]="readonly">
Erhalten Sie 250 Lesepunkte mit der Anmeldung zum Newsletter und eine Geburtstagsüberraschung
</ui-checkbox>
</shared-checkbox>

View File

@@ -2,12 +2,11 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NewsletterFormBlockComponent } from './newsletter-form-block.component';
import { UiCheckboxModule } from '@ui/checkbox';
import { ReactiveFormsModule } from '@angular/forms';
import { UiCommonModule } from '@ui/common';
import { CheckboxComponent } from '@shared/components/checkbox';
@NgModule({
imports: [CommonModule, UiCommonModule, ReactiveFormsModule, UiCheckboxModule],
imports: [CommonModule, ReactiveFormsModule, CheckboxComponent],
exports: [NewsletterFormBlockComponent],
declarations: [NewsletterFormBlockComponent],
})

View File

@@ -1,13 +1,35 @@
<ng-container [formGroup]="control">
<ui-form-control class="col-span-2" label="Firmenname" [requiredMark]="requiredMarks.includes('name') ? '*' : ''">
<input uiInput type="text" formControlName="name" [tabindex]="tabIndexStart" [autofocus]="focusAfterInit" [readonly]="readonly" />
</ui-form-control>
<shared-form-control class="col-span-2" label="Firmenname">
<input
placeholder="Firmenname"
class="input-control"
type="text"
formControlName="name"
[tabindex]="tabIndexStart"
[autofocus]="focusAfterInit"
[readonly]="readonly"
/>
</shared-form-control>
<ng-container *ngIf="appearence === 'default'">
<ui-form-control label="Abteilung" [requiredMark]="requiredMarks.includes('department') ? '*' : ''">
<input uiInput type="text" formControlName="department" [tabindex]="tabIndexStart + 1" [readonly]="readonly" />
</ui-form-control>
<ui-form-control label="USt-ID" [requiredMark]="requiredMarks.includes('vatId') ? '*' : ''">
<input uiInput type="text" formControlName="vatId" [tabindex]="tabIndexStart + 2" [readonly]="readonly" />
</ui-form-control>
<shared-form-control label="Abteilung">
<input
placeholder="Abteilung"
class="input-control"
type="text"
formControlName="department"
[tabindex]="tabIndexStart + 1"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control label="USt-ID">
<input
placeholder="USt-ID"
class="input-control"
type="text"
formControlName="vatId"
[tabindex]="tabIndexStart + 2"
[readonly]="readonly"
/>
</shared-form-control>
</ng-container>
</ng-container>

View File

@@ -2,13 +2,11 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { OrganisationFormBlockComponent } from './organisation-form-block.component';
import { UiFormControlModule } from '@ui/form-control';
import { UiInputModule } from '@ui/input';
import { ReactiveFormsModule } from '@angular/forms';
import { UiCommonModule } from '@ui/common';
import { FormControlComponent } from '@shared/components/form-control';
@NgModule({
imports: [CommonModule, UiCommonModule, ReactiveFormsModule, UiFormControlModule, UiInputModule],
imports: [CommonModule, ReactiveFormsModule, FormControlComponent],
exports: [OrganisationFormBlockComponent],
declarations: [OrganisationFormBlockComponent],
})

View File

@@ -1,6 +1,14 @@
<ui-form-control label="Kundenkartencode" requiredMark="*" class="flex-grow">
<input uiInput type="text" [formControl]="control" [tabindex]="tabIndexStart" [readonly]="readonly" [autofocus]="focusAfterInit" />
</ui-form-control>
<shared-form-control label="Kundenkartencode" class="flex-grow">
<input
placeholder="Kundenkartencode"
class="input-control"
type="text"
[formControl]="control"
[tabindex]="tabIndexStart"
[readonly]="readonly"
[autofocus]="focusAfterInit"
/>
</shared-form-control>
<button type="button" *ngIf="!readonly && canScan()" (click)="scan()">
<shared-icon icon="barcode-scan" [size]="32"></shared-icon>
</button>

View File

@@ -3,13 +3,11 @@ import { CommonModule } from '@angular/common';
import { P4mNumberFormBlockComponent } from './p4m-number-form-block.component';
import { ReactiveFormsModule } from '@angular/forms';
import { UiFormControlModule } from '@ui/form-control';
import { UiInputModule } from '@ui/input';
import { UiCommonModule } from '@ui/common';
import { IconComponent } from '@shared/components/icon';
import { FormControlComponent } from '@shared/components/form-control';
@NgModule({
imports: [CommonModule, UiCommonModule, ReactiveFormsModule, UiFormControlModule, UiInputModule, IconComponent],
imports: [CommonModule, ReactiveFormsModule, FormControlComponent, IconComponent],
exports: [P4mNumberFormBlockComponent],
declarations: [P4mNumberFormBlockComponent],
})

View File

@@ -1,8 +1,23 @@
<ng-container [formGroup]="control">
<ui-form-control label="Festnetznummer" [requiredMark]="requiredMarks.includes('phone') ? '*' : ''">
<input uiInput type="tel" formControlName="phone" [tabindex]="tabIndexStart" [autofocus]="focusAfterInit" [readonly]="readonly" />
</ui-form-control>
<ui-form-control label="Mobilnummer" [clearable]="false" [requiredMark]="requiredMarks.includes('mobile') ? '*' : ''">
<input uiInput type="tel" formControlName="mobile" [tabindex]="tabIndexStart + 1" [readonly]="readonly" />
</ui-form-control>
<shared-form-control label="Festnetznummer">
<input
placeholder="Festnetznummer"
class="input-control"
type="tel"
formControlName="phone"
[tabindex]="tabIndexStart"
[autofocus]="focusAfterInit"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control label="Mobilnummer">
<input
placeholder="Mobilnummer"
class="input-control"
type="tel"
formControlName="mobile"
[tabindex]="tabIndexStart + 1"
[readonly]="readonly"
/>
</shared-form-control>
</ng-container>

View File

@@ -1,14 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UiFormControlModule } from '@ui/form-control';
import { UiInputModule } from '@ui/input';
import { ReactiveFormsModule } from '@angular/forms';
import { PhoneNumbersFormBlockComponent } from './phone-numbers-form-block.component';
import { UiCommonModule } from '@ui/common';
import { FormControlComponent } from '@shared/components/form-control';
@NgModule({
imports: [CommonModule, UiCommonModule, ReactiveFormsModule, UiFormControlModule, UiInputModule],
imports: [CommonModule, ReactiveFormsModule, FormControlComponent],
exports: [PhoneNumbersFormBlockComponent],
declarations: [PhoneNumbersFormBlockComponent],
})

View File

@@ -0,0 +1 @@
export * from './kundenkarte.component';

View File

@@ -0,0 +1,38 @@
<div class="wrapper text-center" [@cardFlip]="state" (@cardFlip.done)="flipAnimationDone($event)">
<div *ngIf="cardDetails" class="card-main">
<div class="icons text-brand">
<button *ngIf="isCustomerCard && frontside" class="icon-barcode" (click)="flipCard()">
<shared-icon [size]="35" icon="barcode-scanner"></shared-icon>
</button>
<button *ngIf="isCustomerCard && !frontside" class="icon-back" (click)="flipCard()">
<shared-icon [size]="35" icon="refresh"></shared-icon>
</button>
<!-- <div *ngIf="!isCustomerCard" class="icon-delete"><ui-icon (click)="onDeletePartnerCard()" size="25px" icon="trash"></ui-icon></div> -->
</div>
<div class="headline">
<p *ngIf="isCustomerCard && frontside">Ihre Lesepunkte</p>
<p *ngIf="isCustomerCard && !frontside">Kartennummer</p>
<p *ngIf="!isCustomerCard">Partnerkartennummer</p>
</div>
<div class="mt-2">
<div *ngIf="!isCustomerCard || (isCustomerCard && !frontside)" class="card-number">{{ cardDetails.code }}</div>
<div *ngIf="isCustomerCard && frontside" class="points">{{ cardDetails.totalPoints | number }}</div>
</div>
<div class="barcode-button">
<div *ngIf="!isCustomerCard || (isCustomerCard && !frontside)" class="barcode-field">
<img class="barcode" src="/assets/images/barcode.png" alt="Barcode" />
</div>
<div *ngIf="isCustomerCard && frontside">
<button class="button" (click)="onRewardShop()">Zum Prämienshop</button>
</div>
</div>
</div>
<div class="card-bottom">
<div *ngIf="!isCustomerCard || (isCustomerCard && !frontside)" class="customer-name">
<p>{{ cardDetails.firstName }} {{ cardDetails.lastName }}</p>
</div>
<div *ngIf="isCustomerCard && frontside" class="logo ml-2">
<img class="logo-picture" src="/assets/images/Hugendubel_Logo.png" alt="Hugendubel Logo" />
</div>
</div>
</div>

View File

@@ -0,0 +1,91 @@
.wrapper {
@apply box-border rounded-customerCard relative flex flex-col;
width: 421px;
height: 266px;
margin-bottom: 42px;
box-shadow: 0px 21px 64px 0px #4e4e4e;
perspective: 1000px;
}
ui-icon {
color: #f70400;
}
.icons {
@apply absolute;
right: 21px;
top: 19px;
}
.headline {
@apply text-wild-blue-yonder mt-px-15 text-p3 font-bold;
}
.card-number,
.points {
@apply text-white -mt-px-15 font-bold;
font-size: 3.5rem;
}
.card-number {
@apply -mt-px-10 text-4xl;
}
.button {
@apply border-none outline-none bg-brand text-white font-bold text-p1 px-px-25 py-px-15 rounded-full mt-px-15;
}
.barcode-field {
@apply bg-white mt-px-10 box-border;
width: 201px;
height: 80px;
img {
@apply mx-auto;
}
}
.barcode {
@apply mt-px-15;
}
.barcode-button {
@apply box-border w-full flex flex-col items-center justify-items-center;
}
.customer-name {
@apply w-full text-left font-bold ml-px-20;
}
.logo {
@apply w-full text-right;
margin-top: 13px;
margin-right: 16px;
margin-bottom: 14px;
}
.logo-picture {
width: 101px;
height: 22px;
}
.card-main {
@apply box-border rounded-t-customerCard flex flex-col bg-onyx;
width: 421px;
height: 217px;
transition: transform 0.8s;
transform-style: preserve-3d;
}
.card-bottom {
@apply box-border rounded-b-customerCard flex flex-row items-center text-black;
width: 421px;
height: 49px;
transition: transform 0.8s;
transform-style: preserve-3d;
}
.icon-barcode,
.icon-back {
@apply bg-transparent border-none outline-none;
}

View File

@@ -0,0 +1,67 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { DecimalPipe, NgIf } from '@angular/common';
import { Component, Input, OnInit } from '@angular/core';
import { IconComponent } from '@shared/components/icon';
import { BonusCardInfoDTO } from '@swagger/crm';
@Component({
selector: 'page-customer-kundenkarte',
templateUrl: 'kundenkarte.component.html',
styleUrls: ['kundenkarte.component.scss'],
standalone: true,
imports: [IconComponent, NgIf, DecimalPipe],
animations: [
trigger('cardFlip', [
state(
'front',
style({
transform: 'rotateY(0deg)',
})
),
state(
'back',
style({
transform: 'rotateY(0deg)',
})
),
state(
'flip',
style({
transform: 'rotateY(90deg)',
})
),
transition('front <=> flip', [animate('400ms')]),
transition('back <=> flip', [animate('400ms')]),
]),
],
})
export class KundenkarteComponent implements OnInit {
@Input() cardDetails: BonusCardInfoDTO;
@Input() isCustomerCard: boolean;
frontside: boolean;
state: 'front' | 'flip' | 'back' = 'front';
constructor() {}
ngOnInit() {
this.frontside = true;
}
onRewardShop(): void {}
onDeletePartnerCard(): void {}
flipCard() {
this.state = 'flip';
}
flipAnimationDone(event: any) {
if (event.fromState === 'front') {
this.frontside = false;
this.state = 'back';
} else if (event.fromState === 'back') {
this.frontside = true;
this.state = 'front';
}
}
}

View File

@@ -30,7 +30,7 @@ app-newsletter-form-block,
app-accept-agb-form-block,
app-interests-form-block,
app-deviating-address-form-block {
@apply mt-8;
@apply mt-4;
}
.spacer {

View File

@@ -1,7 +1,6 @@
import { Component, ChangeDetectionStrategy, ViewChild } from '@angular/core';
import { ValidatorFn, Validators } from '@angular/forms';
import { CustomerDTO } from '@swagger/crm';
import { map } from 'rxjs/operators';
import { DeviatingAddressFormBlockComponent } from '../../components/form-blocks';
import { AddressFormBlockComponent, AddressFormBlockData } from '../../components/form-blocks/address';
import { NameFormBlockData } from '../../components/form-blocks/name/name-form-block-data';

View File

@@ -43,6 +43,7 @@
</app-accept-agb-form-block>
<app-newsletter-form-block
class="mb-4"
#newsletterBlock
[tabIndexStart]="p4mBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('newsletter', $event)"

View File

@@ -35,6 +35,7 @@ export class CreateStoreCustomerComponent extends AbstractCreateCustomer {
nameRequiredMarks: (keyof NameFormBlockData)[] = ['gender', 'firstName', 'lastName'];
nameValidationFns: Record<string, ValidatorFn[]> = {
title: [],
gender: [Validators.required, Validators.min(1)],
firstName: [Validators.required],
lastName: [Validators.required],

View File

@@ -27,6 +27,7 @@
</app-accept-agb-form-block>
<app-newsletter-form-block
class="mb-4"
#newsletterBlock
[tabIndexStart]="p4mBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('newsletter', $event)"

View File

@@ -0,0 +1,7 @@
:host {
@apply block bg-surface text-surface-content rounded px-6 py-8;
}
form {
@apply grid grid-cols-2 gap-x-8;
}

View File

@@ -0,0 +1,84 @@
<div class="flex flex-row justify-end -mt-4 -mr-2">
<a
*ngIf="detailsRoute$ | async; let detailsRoute"
[routerLink]="detailsRoute.path"
[queryParams]="detailsRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
>
<shared-icon icon="close" [size]="32"></shared-icon>
</a>
</div>
<h1 class="text-2xl text-center font-bold mb-6">Rechnungsadresse hinzufügen</h1>
<form [formGroup]="formGroup" (ngSubmit)="save()">
<shared-form-control label="Anrede">
<shared-select formControlName="gender" placeholder="Anrede" tabindex="1" [autofocus]="true">
<shared-select-option [value]="2">Herr</shared-select-option>
<shared-select-option [value]="4">Frau</shared-select-option>
</shared-select>
</shared-form-control>
<shared-form-control label="Titel">
<shared-select formControlName="title" placeholder="Titel" tabindex="2">
<shared-select-option value="Dipl.-Ing.">Dipl.-Ing.</shared-select-option>
<shared-select-option value="Dr.">Dr.</shared-select-option>
<shared-select-option value="Dr. med.">Dr. med.</shared-select-option>
<shared-select-option value="Prof.">Prof.</shared-select-option>
<shared-select-option value="Prof. Dr.">Prof. Dr.</shared-select-option>
<shared-select-option value="RA">RA</shared-select-option>
</shared-select>
</shared-form-control>
<shared-form-control label="Nachname">
<input class="input-control" placeholder="Nachname" type="text" formControlName="lastName" tabindex="3" />
</shared-form-control>
<shared-form-control label="Vorname">
<input class="input-control" placeholder="Vorname" type="text" formControlName="firstName" tabindex="4" />
</shared-form-control>
<shared-form-control label="Firma" class="col-span-2">
<input class="input-control" placeholder="Firma" type="text" formControlName="organisation" tabindex="5" />
</shared-form-control>
<shared-form-control label="Straße">
<input class="input-control" placeholder="Straße" type="text" formControlName="street" tabindex="6" />
</shared-form-control>
<shared-form-control label="Hausnummer">
<input class="input-control" placeholder="Hausnummer" type="text" formControlName="streetNumber" tabindex="7" />
</shared-form-control>
<shared-form-control label="PLZ">
<input class="input-control" placeholder="PLZ" type="text" formControlName="zipCode" tabindex="8" />
</shared-form-control>
<shared-form-control label="Ort">
<input class="input-control" placeholder="Ort" type="text" formControlName="city" tabindex="9" />
</shared-form-control>
<shared-form-control label="Adresszusatz" class="col-span-2">
<input class="input-control" placeholder="Adresszusatz" type="text" formControlName="info" tabindex="10" />
</shared-form-control>
<shared-form-control class="col-span-2" label="Land">
<shared-select placeholder="Land" formControlName="country" tabindex="11">
<shared-select-option *ngFor="let country of countries$ | async" [value]="country.isO3166_A_3">
{{ country.name }}
</shared-select-option>
</shared-select>
</shared-form-control>
<div class="text-center col-span-2">
<shared-checkbox>Diese Rechnungsadresse als Standard Adresse festlegen</shared-checkbox>
</div>
<div class="mt-6 text-center col-span-2">
<button
[disabled]="formGroup.invalid || formGroup.disabled"
type="submit"
class="px-5 py-3 font-bold text-lg rounded-full bg-brand text-white"
>
Speichern
</button>
</div>
</form>

View File

@@ -0,0 +1,108 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CheckboxComponent } from '@shared/components/checkbox';
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { SelectModule } from '@shared/components/select';
import { FormControlComponent } from '@shared/components/form-control';
import { CrmCustomerService } from '@domain/crm';
import { AddressDTO, Gender, PayerDTO } from '@swagger/crm';
import { map } from 'rxjs/operators';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { AddressSelectionModalService } from '@shared/modals/address-selection-modal';
import { CustomerSearchStore } from '../store';
import { CustomerSearchNavigation } from '../../navigations';
import { IconComponent } from '@shared/components/icon';
import { combineLatest } from 'rxjs';
import { RouterLink } from '@angular/router';
@Component({
selector: 'page-add-billing-address-main-view',
templateUrl: 'add-billing-address-main-view.component.html',
styleUrls: ['add-billing-address-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-add-billing-address-main-view' },
standalone: true,
imports: [
IconComponent,
RouterLink,
AsyncPipe,
NgIf,
NgForOf,
ReactiveFormsModule,
SelectModule,
FormControlComponent,
CheckboxComponent,
],
})
export class AddBillingAddressMainViewComponent {
formGroup = new FormGroup({
gender: new FormControl<Gender>(0, [Validators.required, Validators.min(1)]),
title: new FormControl<string>(undefined),
firstName: new FormControl<string>(undefined, [Validators.required]),
lastName: new FormControl<string>(undefined, [Validators.required]),
organisation: new FormControl<string>(undefined),
street: new FormControl<string>(undefined, [Validators.required]),
streetNumber: new FormControl<string>(undefined, [Validators.required]),
zipCode: new FormControl<string>(undefined, [Validators.required]),
city: new FormControl<string>(undefined, [Validators.required]),
country: new FormControl<string>('DEU', [Validators.required]),
info: new FormControl<string>(undefined),
isDefault: new FormControl<boolean>(false),
});
countries$ = this._customerService.getCountries().pipe(map((res) => res.result));
detailsRoute$ = combineLatest([this._store.processId$, this._store.customerId$]).pipe(
map(([processId, customerId]) => this._navigation.detailsRoute({ processId, customerId }))
);
constructor(
private _customerService: CrmCustomerService,
private _addressSelection: AddressSelectionModalService,
private _store: CustomerSearchStore,
private _navigation: CustomerSearchNavigation
) {}
async save() {
if (this.formGroup.invalid) {
this.formGroup.markAllAsTouched();
return;
}
try {
this.formGroup.disable();
const formData = this.formGroup.value;
const address: AddressDTO = {
street: formData.street,
streetNumber: formData.streetNumber,
zipCode: formData.zipCode,
city: formData.city,
country: formData.country,
info: formData.info,
};
const addressValidationResult = await this._addressSelection.validateAddress(address);
if (addressValidationResult === undefined) {
this.formGroup.enable();
return;
}
const payer: PayerDTO = {
gender: formData.gender,
title: formData.title,
firstName: formData.firstName,
lastName: formData.lastName,
organisation: formData.organisation ? { name: formData.organisation } : undefined,
address: addressValidationResult,
};
const result = await this._customerService.createPayer(this._store.customerId, payer, formData.isDefault);
this._navigation.navigateToDetails({ processId: this._store.processId, customerId: this._store.customerId });
} catch (error) {
this.formGroup.enable();
}
}
}

View File

@@ -0,0 +1,7 @@
:host {
@apply block bg-surface text-surface-content rounded px-6 py-8;
}
form {
@apply grid grid-cols-2 gap-x-8;
}

View File

@@ -0,0 +1,101 @@
<div class="flex flex-row justify-end -mt-4 -mr-2">
<a
*ngIf="detailsRoute$ | async; let detailsRoute"
[routerLink]="detailsRoute.path"
[queryParams]="detailsRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
>
<shared-icon icon="close" [size]="32"></shared-icon>
</a>
</div>
<h1 class="text-2xl text-center font-bold mb-6">Lieferadresse hinzufügen</h1>
<form [formGroup]="formGroup" (ngSubmit)="save()">
<ng-container *ngIf="isBusinessKonto$ | async">
<shared-form-control label="Firma" class="col-span-2">
<input class="input-control" placeholder="Firma" type="text" formControlName="organisation" tabindex="1" />
</shared-form-control>
<shared-form-control label="Abteilung">
<input class="input-control" placeholder="Abteilung" type="text" formControlName="department" tabindex="2" />
</shared-form-control>
<shared-form-control label="USt-ID">
<input class="input-control" placeholder="Abteilung" type="text" formControlName="vatId" tabindex="3" />
</shared-form-control>
</ng-container>
<shared-form-control label="Anrede">
<shared-select formControlName="gender" placeholder="Anrede" tabindex="4" [autofocus]="true">
<shared-select-option [value]="2">Herr</shared-select-option>
<shared-select-option [value]="4">Frau</shared-select-option>
</shared-select>
</shared-form-control>
<shared-form-control label="Titel">
<shared-select formControlName="title" placeholder="Titel" tabindex="5">
<shared-select-option value="Dipl.-Ing.">Dipl.-Ing.</shared-select-option>
<shared-select-option value="Dr.">Dr.</shared-select-option>
<shared-select-option value="Dr. med.">Dr. med.</shared-select-option>
<shared-select-option value="Prof.">Prof.</shared-select-option>
<shared-select-option value="Prof. Dr.">Prof. Dr.</shared-select-option>
<shared-select-option value="RA">RA</shared-select-option>
</shared-select>
</shared-form-control>
<shared-form-control label="Nachname">
<input class="input-control" placeholder="Nachname" type="text" formControlName="lastName" tabindex="6" />
</shared-form-control>
<shared-form-control label="Vorname">
<input class="input-control" placeholder="Vorname" type="text" formControlName="firstName" tabindex="7" />
</shared-form-control>
<ng-container *ngIf="!(isBusinessKonto$ | async)">
<shared-form-control label="Firma" class="col-span-2">
<input class="input-control" placeholder="Firma" type="text" formControlName="organisation" tabindex="8" />
</shared-form-control>
</ng-container>
<shared-form-control label="Straße">
<input class="input-control" placeholder="Straße" type="text" formControlName="street" tabindex="9" />
</shared-form-control>
<shared-form-control label="Hausnummer">
<input class="input-control" placeholder="Hausnummer" type="text" formControlName="streetNumber" tabindex="10" />
</shared-form-control>
<shared-form-control label="PLZ">
<input class="input-control" placeholder="PLZ" type="text" formControlName="zipCode" tabindex="11" />
</shared-form-control>
<shared-form-control label="Ort">
<input class="input-control" placeholder="Ort" type="text" formControlName="city" tabindex="12" />
</shared-form-control>
<shared-form-control label="Adresszusatz" class="col-span-2">
<input class="input-control" placeholder="Adresszusatz" type="text" formControlName="info" tabindex="13" />
</shared-form-control>
<shared-form-control class="col-span-2" label="Land">
<shared-select placeholder="Land" formControlName="country" tabindex="14">
<shared-select-option *ngFor="let country of countries$ | async" [value]="country.isO3166_A_3">
{{ country.name }}
</shared-select-option>
</shared-select>
</shared-form-control>
<div class="text-center col-span-2">
<shared-checkbox>Diese Lieferadresse als Standard Adresse festlegen</shared-checkbox>
</div>
<div class="mt-6 text-center col-span-2">
<button
[disabled]="formGroup.invalid || formGroup.disabled"
type="submit"
class="px-5 py-3 font-bold text-lg rounded-full bg-brand text-white"
tabindex="15"
>
Speichern
</button>
</div>
</form>

View File

@@ -0,0 +1,137 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { CheckboxComponent } from '@shared/components/checkbox';
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { SelectModule } from '@shared/components/select';
import { FormControlComponent } from '@shared/components/form-control';
import { CrmCustomerService } from '@domain/crm';
import { AddressDTO, Gender, ShippingAddressDTO } from '@swagger/crm';
import { map, takeUntil } from 'rxjs/operators';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { AddressSelectionModalService } from '@shared/modals/address-selection-modal';
import { CustomerSearchStore } from '../store';
import { CustomerSearchNavigation } from '../../navigations';
import { Subject, combineLatest } from 'rxjs';
import { RouterLink } from '@angular/router';
import { IconComponent } from '@shared/components/icon';
@Component({
selector: 'page-add-shipping-address-main-view',
templateUrl: 'add-shipping-address-main-view.component.html',
styleUrls: ['add-shipping-address-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-add-shipping-address-main-view' },
standalone: true,
imports: [
RouterLink,
IconComponent,
AsyncPipe,
NgIf,
NgForOf,
ReactiveFormsModule,
SelectModule,
FormControlComponent,
CheckboxComponent,
],
})
export class AddShippingAddressMainViewComponent implements OnInit, OnDestroy {
private _onDestroy = new Subject<void>();
detailsRoute$ = combineLatest([this._store.processId$, this._store.customerId$]).pipe(
map(([processId, customerId]) => this._navigation.detailsRoute({ processId, customerId }))
);
formGroup = new FormGroup({
gender: new FormControl<Gender>(0, [Validators.required, Validators.min(1)]),
title: new FormControl<string>(undefined),
firstName: new FormControl<string>(undefined, [Validators.required]),
lastName: new FormControl<string>(undefined, [Validators.required]),
organisation: new FormControl<string>(undefined),
department: new FormControl<string>(undefined),
vatId: new FormControl<string>(undefined),
street: new FormControl<string>(undefined, [Validators.required]),
streetNumber: new FormControl<string>(undefined, [Validators.required]),
zipCode: new FormControl<string>(undefined, [Validators.required]),
city: new FormControl<string>(undefined, [Validators.required]),
country: new FormControl<string>('DEU', [Validators.required]),
info: new FormControl<string>(undefined),
isDefault: new FormControl<boolean>(false),
});
countries$ = this._customerService.getCountries().pipe(map((res) => res.result));
isBusinessKonto$ = this._store.isBusinessKonto$;
constructor(
private _customerService: CrmCustomerService,
private _addressSelection: AddressSelectionModalService,
private _store: CustomerSearchStore,
private _navigation: CustomerSearchNavigation
) {}
ngOnInit() {
this._store.customer$.pipe(takeUntil(this._onDestroy)).subscribe(() => {
if (this._store.isBusinessKonto) {
this.formGroup.controls.organisation.setValidators([Validators.required]);
} else {
this.formGroup.controls.organisation.clearValidators();
}
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
async save() {
if (this.formGroup.invalid) {
this.formGroup.markAllAsTouched();
return;
}
try {
this.formGroup.disable();
const formData = this.formGroup.value;
const address: AddressDTO = {
street: formData.street,
streetNumber: formData.streetNumber,
zipCode: formData.zipCode,
city: formData.city,
country: formData.country,
info: formData.info,
};
const addressValidationResult = await this._addressSelection.validateAddress(address);
if (addressValidationResult === undefined) {
this.formGroup.enable();
return;
}
const addOrganization = this._store.isBusinessKonto || formData.organisation !== undefined;
const shippingAddress: ShippingAddressDTO = {
gender: formData.gender,
title: formData.title,
firstName: formData.firstName,
lastName: formData.lastName,
organisation: addOrganization
? {
name: formData.organisation,
department: formData.department,
vatId: formData.vatId,
}
: undefined,
address: addressValidationResult,
};
const result = await this._customerService.createShippingAddress(this._store.customerId, shippingAddress, formData.isDefault);
this._navigation.navigateToDetails({ processId: this._store.processId, customerId: this._store.customerId });
} catch (error) {
this.formGroup.enable();
}
}
}

View File

@@ -1,5 +1,6 @@
<page-split-screen class="max-h-[calc(100vh-13.875rem)] h-[calc(100vh-13.875rem)]" [side]="side$ | async">
<page-customer-results-side-view *sideTemplate="'results'"></page-customer-results-side-view>
<page-customer-main-side-view [processId]="processId$ | async" *sideTemplate="'main'"></page-customer-main-side-view>
<page-customer-order-details-side-view *sideTemplate="'order-details'"></page-customer-order-details-side-view>
<router-outlet></router-outlet>
</page-split-screen>

View File

@@ -1,11 +1,12 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { BehaviorSubject, Subscription } from 'rxjs';
import { BehaviorSubject, Subject, Subscription } from 'rxjs';
import { CustomerSearchStore } from './store/customer-search.store';
import { provideComponentStore } from '@ngrx/component-store';
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
import { filter, first, switchMap } from 'rxjs/operators';
import { delay, filter, first, switchMap, takeUntil } from 'rxjs/operators';
import { CustomerSearchNavigation } from '../navigations';
import { CustomerCreateNavigation } from '../navigations/customer-create.navigation';
@Component({
selector: 'page-customer-search',
@@ -45,12 +46,15 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
private _customerChangedSubscription: Subscription;
private _onDestroy$ = new Subject<void>();
constructor(
private _store: CustomerSearchStore,
private _activatedRoute: ActivatedRoute,
private _router: Router,
private _breadcrumbService: BreadcrumbService,
private _navigation: CustomerSearchNavigation
private _navigation: CustomerSearchNavigation,
private _createNavigation: CustomerCreateNavigation
) {}
ngOnInit(): void {
@@ -59,11 +63,12 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
this.checkAndUpdateCustomerId();
this.checkBreadcrumbs();
this._customerChangedSubscription = this._store.customer$.subscribe((c) => {
// Wenn der delay nicht angewendet wird, dann wird die Breacrumb zwei mal angelegt
this._customerChangedSubscription = this._store.customer$.pipe(takeUntil(this._onDestroy$), delay(1)).subscribe((c) => {
this.checkDetailsBreadcrumb();
});
this._eventsSubscription = this._router.events.subscribe((event) => {
this._eventsSubscription = this._router.events.pipe(takeUntil(this._onDestroy$)).subscribe((event) => {
if (event instanceof NavigationEnd) {
this.checkAndUpdateProcessId();
this.checkAndUpdateSide();
@@ -71,12 +76,56 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
this.checkBreadcrumbs();
}
});
this._store.customerListResponse$.pipe(takeUntil(this._onDestroy$)).subscribe(async ([response, filter, processId]) => {
if (this._store.processId === processId) {
if (response.hits === 1) {
// Navigate to details page
const customer = response.result[0];
if (customer.id < 0) {
// navigate to create customer
const route = this._createNavigation.createCustomerRoute({ processId, customerInfo: customer });
await this._router.navigate(route.path, { queryParams: route.queryParams });
} else {
const route = this._navigation.detailsRoute({ processId, customerId: customer.id });
await this._router.navigate(route.path, { queryParams: route.queryParams });
}
} else if (response.hits > 1) {
const route = this._navigation.listRoute({ processId, filter });
if (
[
'details',
'history',
'edit',
'add-billing-address',
'edit-billing-address',
'add-shipping-address',
'edit-shipping-address',
'orders',
'order-details',
'order-details-history',
].includes(this._breadcrumb)
) {
await this._router.navigate([], { queryParams: route.queryParams });
} else {
await this._router.navigate(route.path, { queryParams: route.queryParams });
}
}
this.checkBreadcrumbs();
}
});
}
ngOnDestroy(): void {
this._eventsSubscription.unsubscribe();
this._customerChangedSubscription.unsubscribe();
this.side$.complete();
this._onDestroy$.next();
this._onDestroy$.complete();
this._store.ngOnDestroy();
}
checkAndUpdateProcessId() {
@@ -88,8 +137,11 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
processId = this.parentSnapshot?.data?.processId;
}
processId = +processId;
if (processId !== this._store.processId) {
this._store.setProcessId(processId);
this._store.reset(this._activatedRoute.snapshot.queryParams);
}
}
@@ -121,21 +173,22 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
}
}
async checkBreadcrumbs() {
let breadcumb: string;
async checkBreadcrumbs(force = false) {
let breadcrumb: string;
breadcumb = this.snapshot.data?.breadcumb;
breadcrumb = this.snapshot.data?.breadcrumb;
if (!breadcumb) {
breadcumb = this.firstChildSnapshot.data?.breadcumb;
if (!breadcrumb) {
breadcrumb = this.firstChildSnapshot.data?.breadcrumb;
}
if (breadcumb !== this._breadcrumb) {
this._breadcrumb = breadcumb;
await this.checkMainBreadcrumb();
await this.checkDetailsBreadcrumb();
await this.checkHistoryBreadcrumb();
}
this._breadcrumb = breadcrumb;
await this.checkCreateCustomerBreadcrumb();
await this.checkMainBreadcrumb();
await this.checkSearchBreadcrumb();
await this.checkDetailsBreadcrumb();
await this.checkHistoryBreadcrumb();
await this.checkEditBreadcrumb();
}
getBreadcrumbs(): Promise<Breadcrumb[]> {
@@ -154,26 +207,103 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
const navigation = this._navigation.defaultRoute({ processId: this._store.processId });
const breadcrumb: Breadcrumb = {
key: this._store.processId,
tags: ['customer', 'main'],
tags: ['customer', 'search', 'main'],
name: 'Kundensuche',
path: navigation.urlTree.toString(),
params: navigation.queryParams,
params: { ...this._store.queryParams, ...navigation.queryParams },
section: 'customer',
};
this._breadcrumbService.addBreadcrumb(breadcrumb);
} else {
this._breadcrumbService.patchBreadcrumb(mainBreadcrumb.id, {
params: { ...this.snapshot.queryParams, ...(mainBreadcrumb.params ?? {}) },
});
}
}
async getCreateCustomerBreadcrumb(): Promise<Breadcrumb | undefined> {
const breadcrumbs = await this.getBreadcrumbs();
return breadcrumbs.find((b) => b.tags.includes('create') && b.tags.includes('customer'));
}
async checkCreateCustomerBreadcrumb() {
const createCustomerBreadcrumb = await this.getCreateCustomerBreadcrumb();
if (createCustomerBreadcrumb) {
this._breadcrumbService.removeBreadcrumb(createCustomerBreadcrumb.id);
}
}
async getSearchBreadcrumb(): Promise<Breadcrumb | undefined> {
const breadcrumbs = await this.getBreadcrumbs();
return breadcrumbs.find((b) => b.tags.includes('list') && b.tags.includes('search'));
}
async checkSearchBreadcrumb() {
const searchBreadcrumb = await this.getSearchBreadcrumb();
if (
[
'search',
'details',
'history',
'edit',
'history',
'orders',
'order-details',
'add-billing-address',
'edit-billing-address',
'add-shipping-address',
'edit-shipping-address',
].includes(this._breadcrumb)
) {
const name = this._store.queryParams?.main_qs || 'Suche';
if (!searchBreadcrumb) {
const navigation = this._navigation.listRoute({ processId: this._store.processId });
const breadcrumb: Breadcrumb = {
key: this._store.processId,
tags: ['customer', 'search', 'list'],
name,
path: navigation.urlTree.toString(),
params: { ...this._store.queryParams, ...navigation.queryParams },
section: 'customer',
};
this._breadcrumbService.addBreadcrumb(breadcrumb);
} else {
this._breadcrumbService.patchBreadcrumb(searchBreadcrumb.id, { params: this.snapshot.queryParams, name });
}
} else {
if (searchBreadcrumb) {
this._breadcrumbService.removeBreadcrumb(searchBreadcrumb.id);
}
}
}
async getDetailsBreadcrumb(): Promise<Breadcrumb | undefined> {
const breadcrumbs = await this.getBreadcrumbs();
return breadcrumbs.find((b) => b.tags.includes('details'));
return breadcrumbs.find((b) => b.tags.includes('details') && b.tags.includes('search'));
}
async checkDetailsBreadcrumb() {
const detailsBreadcrumb = await this.getDetailsBreadcrumb();
if (['details', 'history'].includes(this._breadcrumb)) {
if (
[
'details',
'history',
'edit',
'history',
'orders',
'order-details',
'add-billing-address',
'edit-billing-address',
'add-shipping-address',
'edit-shipping-address',
].includes(this._breadcrumb)
) {
const customer = this._store.customer;
const fullName = `${customer?.firstName ?? ''} ${customer?.lastName ?? ''}`.trim();
@@ -182,10 +312,10 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
const breadcrumb: Breadcrumb = {
key: this._store.processId,
tags: ['customer', 'details'],
tags: ['customer', 'details', 'search'],
name: fullName || 'Kunde',
path: navigation.urlTree.toString(),
params: navigation.queryParams,
params: { ...this._store.queryParams, ...navigation.queryParams },
section: 'customer',
};
@@ -196,7 +326,7 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
this._breadcrumbService.patchBreadcrumb(detailsBreadcrumb.id, {
name: fullName,
path: navigation.urlTree.toString(),
params: navigation.queryParams,
params: { ...this._store.queryParams, ...navigation.queryParams },
});
}
} else if (detailsBreadcrumb) {
@@ -218,17 +348,56 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
const breadcrumb: Breadcrumb = {
key: this._store.processId,
tags: ['customer', 'history'],
tags: ['customer', 'search', 'history'],
name: 'Historie',
path: navigation.urlTree.toString(),
params: navigation.queryParams,
params: { ...this._store.queryParams, ...navigation.queryParams },
section: 'customer',
};
this._breadcrumbService.addBreadcrumb(breadcrumb);
} else {
const navigation = this._navigation.historyRoute({ processId: this._store.processId, customerId: this._store.customerId });
this._breadcrumbService.patchBreadcrumb(historyBreadcrumb.id, {
path: navigation.urlTree.toString(),
params: { ...this._store.queryParams, ...navigation.queryParams },
});
}
} else if (historyBreadcrumb) {
this._breadcrumbService.removeBreadcrumb(historyBreadcrumb.id);
}
}
async getEditBreadcrumb(): Promise<Breadcrumb | undefined> {
const breadcrumbs = await this.getBreadcrumbs();
return breadcrumbs.find((b) => b.tags.includes('edit'));
}
async checkEditBreadcrumb() {
const editBreadcrumb = await this.getEditBreadcrumb();
if (this._breadcrumb === 'edit') {
if (!editBreadcrumb) {
const navigation = this._navigation.editRoute({
processId: this._store.processId,
customerId: this._store.customerId,
isB2b: this._store.isBusinessKonto,
});
const breadcrumb: Breadcrumb = {
key: this._store.processId,
tags: ['customer', 'search', 'edit'],
name: 'Bearbeiten',
path: navigation.urlTree.toString(),
params: { ...this._store.queryParams, ...navigation.queryParams },
section: 'customer',
};
this._breadcrumbService.addBreadcrumb(breadcrumb);
}
} else if (editBreadcrumb) {
this._breadcrumbService.removeBreadcrumb(editBreadcrumb.id);
}
}
}

View File

@@ -10,6 +10,7 @@ import { CustomerHistoryMainViewModule } from './history-main-view/history-main-
import { CustomerFilterMainViewModule } from './filter-main-view/filter-main-view.module';
import { SplitScreenModule } from '../components/split-screen/split-screen.module';
import { MainSideViewModule } from './main-side-view/main-side-view.module';
import { OrderDetailsSideViewComponent } from './order-details-side-view/order-details-side-view.component';
@NgModule({
imports: [
@@ -22,6 +23,7 @@ import { MainSideViewModule } from './main-side-view/main-side-view.module';
CustomerHistoryMainViewModule,
CustomerFilterMainViewModule,
MainSideViewModule,
OrderDetailsSideViewComponent,
],
exports: [CustomerSearchComponent],
declarations: [CustomerSearchComponent],

View File

@@ -0,0 +1,71 @@
<div class="grid grid-flow-col items-center justify-between px-4 py-2 border-t-2 border-solid border-surface-2">
<h3 class="font-bold text-xl">Rechnungsadresse</h3>
<a
*ngIf="addBillingAddressRoute$ | async; let addRoute"
type="button"
class="text-brand font-bold"
[routerLink]="addRoute.path"
[queryParams]="addRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Hinzufügen
</a>
</div>
<div class="grid grid-flow-row">
<label
class="px-4 py-3 font-bold border-t-2 border-solid border-surface-2 flex flex-row justify-between items-center"
*ngIf="showCustomerAddress$ | async"
>
<input
*ngIf="isNotBusinessKonto$ | async"
name="shipping-address"
type="radio"
[checked]="!(customer$ | async)"
(change)="selectCustomerAddress()"
/>
<div class="ml-2 flex flex-row justify-between items-center grow">
<span class="truncate">
{{ customer$ | async | address }}
</span>
<a
*ngIf="editRoute$ | async; let editRoute"
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
class="text-brand font-bold"
type="button"
>
Bearbeiten
</a>
</div>
</label>
<label
class="px-4 py-3 font-bold border-t-2 border-solid border-surface-2 cursor-pointer flex flex-row items-center justify-start"
*ngFor="let assignedPayer of assignedPayers$ | async"
>
<input
name="assigned-payer"
type="radio"
[value]="assignedPayer"
[checked]="(selectedPayer$ | async)?.payer.id === assignedPayer.payer.id"
(change)="selectPayer(assignedPayer)"
/>
<div class="flex flex-row justify-between items-center grow">
<span class="ml-2">
{{ assignedPayer.payer.data | address }}
</span>
<ng-container *ngIf="canEditAddress$ | async">
<a
*ngIf="editRoute(assignedPayer.payer.id); let editRoute"
class="text-brand font-bold"
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Bearbeiten
</a>
</ng-container>
</div>
</label>
</div>

View File

@@ -0,0 +1,200 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, Host } from '@angular/core';
import { CustomerSearchStore } from '../../store';
import { CrmCustomerService } from '@domain/crm';
import { map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { Observable, Subject, combineLatest } from 'rxjs';
import { AssignedPayerDTO, CustomerDTO, ListResponseArgsOfAssignedPayerDTO } from '@swagger/crm';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { CustomerPipesModule } from '@shared/pipes/customer';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { UiModalService } from '@ui/modal';
import { CustomerSearchNavigation } from '../../../navigations';
import { RouterLink } from '@angular/router';
import { CustomerDetailsViewMainComponent } from '../details-main-view.component';
import { PayerDTO } from '@swagger/checkout';
interface DetailsMainViewBillingAddressesComponentState {
assignedPayers: AssignedPayerDTO[];
selectedPayer: AssignedPayerDTO;
}
@Component({
selector: 'page-details-main-view-billing-addresses',
templateUrl: 'details-main-view-billing-addresses.component.html',
styleUrls: ['details-main-view-billing-addresses.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-details-main-view-billing-addresses' },
standalone: true,
imports: [NgIf, NgFor, AsyncPipe, CustomerPipesModule, RouterLink],
})
export class DetailsMainViewBillingAddressesComponent extends ComponentStore<DetailsMainViewBillingAddressesComponentState>
implements OnInit, OnDestroy {
assignedPayers$ = this.select((state) => state.assignedPayers);
selectedPayer$ = this.select((state) => state.selectedPayer);
isNotBusinessKonto$ = this._store.isBusinessKonto$.pipe(map((isBusinessKonto) => !isBusinessKonto));
showCustomerAddress$ = combineLatest([this._store.isBusinessKonto$, this._store.isMitarbeiter$, this._store.isKundenkarte$]).pipe(
map(([isBusinessKonto, isMitarbeiter, isKundenkarte]) => isBusinessKonto || isMitarbeiter || isKundenkarte)
);
get showCustomerAddress() {
return this._store.isBusinessKonto || this._store.isMitarbeiter;
}
canAddNewAddress$ = combineLatest([
this._store.isOnlinekonto$,
this._store.isOnlineKontoMitKundenkarte$,
this._store.isKundenkarte$,
this._store.isMitarbeiter$,
]).pipe(
map(
([isOnlinekonto, isOnlineKontoMitKundenkarte, isKundenkarte, isMitarbeiter]) =>
isOnlinekonto || isOnlineKontoMitKundenkarte || isKundenkarte || isMitarbeiter
)
);
canEditAddress$ = combineLatest([this._store.isKundenkarte$]).pipe(map(([isKundenkarte]) => isKundenkarte));
customer$ = this._store.customer$;
private _onDestroy$ = new Subject<void>();
editRoute$ = combineLatest([this._store.processId$, this._store.customerId$, this._store.isBusinessKonto$]).pipe(
map(([processId, customerId, isB2b]) => this._navigation.editRoute({ processId, customerId, isB2b }))
);
addBillingAddressRoute$ = combineLatest([this.canAddNewAddress$, this._store.processId$, this._store.customerId$]).pipe(
map(([canAddNewAddress, processId, customerId]) =>
canAddNewAddress ? this._navigation.addBillingAddressRoute({ processId, customerId }) : undefined
)
);
constructor(
@Host() private _host: CustomerDetailsViewMainComponent,
private _store: CustomerSearchStore,
private _customerService: CrmCustomerService,
private _modal: UiModalService,
private _navigation: CustomerSearchNavigation
) {
super({
assignedPayers: [],
selectedPayer: undefined,
});
}
editRoute(payerId: number) {
return this._navigation.editBillingAddressRoute({ customerId: this._store.customerId, payerId, processId: this._store.processId });
}
ngOnInit() {
this._store.customerId$.pipe(takeUntil(this._onDestroy$)).subscribe((customerId) => {
this.resetStore();
if (customerId) {
this.loadAssignedPayers(customerId);
}
});
combineLatest([this.selectedPayer$, this._store.customer$])
.pipe(takeUntil(this._onDestroy$))
.subscribe(([selectedPayer, customer]) => {
if (selectedPayer) {
this._host.setPayer(this._createPayerFromCrmPayerDTO(selectedPayer));
} else if (this.showCustomerAddress) {
this._host.setPayer(this._createPayerFormCustomer(customer));
}
});
}
_createPayerFromCrmPayerDTO(assignedPayer: AssignedPayerDTO): PayerDTO {
const payer = assignedPayer.payer.data;
return {
reference: { id: payer.id },
payerType: payer.payerType as any,
payerNumber: payer.payerNumber,
payerStatus: payer.payerStatus,
gender: payer.gender,
title: payer.title,
firstName: payer.firstName,
lastName: payer.lastName,
communicationDetails: payer.communicationDetails ? { ...payer.communicationDetails } : undefined,
organisation: payer.organisation ? { ...payer.organisation } : undefined,
address: payer.address ? { ...payer.address } : undefined,
source: payer.id,
};
}
_createPayerFormCustomer(customer: CustomerDTO): PayerDTO {
return {
reference: { id: customer.id },
payerType: customer.customerType as any,
payerNumber: customer.customerNumber,
payerStatus: 0,
gender: customer.gender,
title: customer.title,
firstName: customer.firstName,
lastName: customer.lastName,
communicationDetails: customer.communicationDetails ? { ...customer.communicationDetails } : undefined,
organisation: customer.organisation ? { ...customer.organisation } : undefined,
address: customer.address ? { ...customer.address } : undefined,
};
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
}
loadAssignedPayers = this.effect((customerId$: Observable<number>) =>
customerId$.pipe(
switchMap((customerId) =>
this._customerService
.getAssignedPayers({ customerId })
.pipe(tapResponse(this.handleLoadAssignedPayersResponse, this.handleLoadAssignedPayersError))
)
)
);
handleLoadAssignedPayersResponse = (response: ListResponseArgsOfAssignedPayerDTO) => {
const selectedPayer = response.result.reduce<AssignedPayerDTO>((selected, payer) => {
if (!selected) {
return payer;
}
if (new Date(selected.isDefault).getTime() < new Date(payer.isDefault).getTime()) {
return payer;
}
return selected;
}, undefined);
this.patchState({
assignedPayers: response.result,
selectedPayer,
});
};
handleLoadAssignedPayersError = (err: any) => {
this._modal.error('Laden der Rechnungsadressen fehlgeschlagen', err);
};
resetStore() {
this.patchState({
assignedPayers: [],
selectedPayer: undefined,
});
}
selectPayer(payer: AssignedPayerDTO) {
this.patchState({
selectedPayer: payer,
});
}
selectCustomerAddress() {
this.patchState({
selectedPayer: undefined,
});
}
}

View File

@@ -0,0 +1,57 @@
<div class="grid grid-flow-col items-center justify-between px-4 py-2 border-t-2 border-solid border-surface-2">
<h3 class="font-bold text-xl">Lieferadresse</h3>
<a
*ngIf="addShippingAddressRoute$ | async; let addRoute"
type="button"
class="text-brand font-bold"
[routerLink]="addRoute.path"
[queryParams]="addRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Hinzufügen
</a>
</div>
<div class="grid grid-flow-row">
<label
class="px-4 py-3 font-bold border-t-2 border-solid border-surface-2 cursor-pointer flex flex-row justify-start items-center"
*ngIf="showCustomerAddress$ | async"
>
<input name="shipping-address" type="radio" [checked]="!(selectedShippingAddress$ | async)" (change)="selectCustomerAddress()" />
<div class="ml-2 flex flex-row justify-between items-center grow">
<span class="truncate">
{{ customer$ | async | address }}
</span>
<a
*ngIf="editRoute$ | async; let editRoute"
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
class="text-brand font-bold"
type="button"
>
Bearbeiten
</a>
</div>
</label>
<label
class="px-4 py-3 font-bold border-t-2 border-solid border-surface-2 cursor-pointer flex flex-row justify-start items-center"
*ngFor="let shippingAddress of shippingAddresses$ | async"
>
<input
name="shipping-address"
type="radio"
[value]="shippingAddress"
[checked]="(selectedShippingAddress$ | async)?.id === shippingAddress.id"
(change)="selectShippingAddress(shippingAddress)"
/>
<div class="ml-2 flex flex-row justify-between items-center grow">
<span class="ml-2">
{{ shippingAddress | address }}
</span>
<a *ngIf="canEditAddress$ | async" class="text-brand font-bold" type="button">
Bearbeiten
</a>
</div>
</label>
</div>

View File

@@ -0,0 +1,193 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, Host } from '@angular/core';
import { CustomerSearchStore } from '../../store';
import { CrmCustomerService } from '@domain/crm';
import { map, switchMap, takeUntil } from 'rxjs/operators';
import { Observable, Subject, combineLatest } from 'rxjs';
import { CustomerDTO, ListResponseArgsOfAssignedPayerDTO, ShippingAddressDTO } from '@swagger/crm';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { CustomerPipesModule } from '@shared/pipes/customer';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { UiModalService } from '@ui/modal';
import { CustomerSearchNavigation } from '../../../navigations';
import { RouterLink } from '@angular/router';
import { CustomerDetailsViewMainComponent } from '../details-main-view.component';
interface DetailsMainViewDeliveryAddressesComponentState {
shippingAddresses: ShippingAddressDTO[];
selectedShippingAddress: ShippingAddressDTO;
}
@Component({
selector: 'page-details-main-view-delivery-addresses',
templateUrl: 'details-main-view-delivery-addresses.component.html',
styleUrls: ['details-main-view-delivery-addresses.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-details-main-view-delivery-addresses' },
standalone: true,
imports: [NgIf, NgFor, AsyncPipe, CustomerPipesModule, RouterLink],
})
export class DetailsMainViewDeliveryAddressesComponent extends ComponentStore<DetailsMainViewDeliveryAddressesComponentState>
implements OnInit, OnDestroy {
shippingAddresses$ = this.select((state) => state.shippingAddresses);
selectedShippingAddress$ = this.select((state) => state.selectedShippingAddress);
get selectedShippingAddress() {
return this.get((s) => s.selectedShippingAddress);
}
private _onDestroy$ = new Subject<void>();
showCustomerAddress$ = combineLatest([this._store.isBusinessKonto$, this._store.isMitarbeiter$, this._store.isKundenkarte$]).pipe(
map(([isBusinessKonto, isMitarbeiter, isKundenkarte]) => isBusinessKonto || isMitarbeiter || isKundenkarte)
);
get showCustomerAddress() {
return this._store.isBusinessKonto || this._store.isMitarbeiter;
}
customer$ = this._store.customer$;
canAddNewAddress$ = combineLatest([
this._store.isOnlinekonto$,
this._store.isOnlineKontoMitKundenkarte$,
this._store.isKundenkarte$,
this._store.isBusinessKonto$,
this._store.isMitarbeiter$,
]).pipe(
map(
([isOnlinekonto, isOnlineKontoMitKundenkarte, isKundenkarte, isBusinessKonto, isMitarbeiter]) =>
isOnlinekonto || isOnlineKontoMitKundenkarte || isKundenkarte || isBusinessKonto || isMitarbeiter
)
);
editRoute$ = combineLatest([this._store.processId$, this._store.customerId$, this._store.isBusinessKonto$]).pipe(
map(([processId, customerId, isB2b]) => this._navigation.editRoute({ processId, customerId, isB2b }))
);
addShippingAddressRoute$ = combineLatest([this.canAddNewAddress$, this._store.processId$, this._store.customerId$]).pipe(
map(([canAddNewAddress, processId, customerId]) =>
canAddNewAddress ? this._navigation.addShippingAddressRoute({ processId, customerId }) : undefined
)
);
canEditAddress$ = combineLatest([this._store.isKundenkarte$, this._store.isBusinessKonto$, this._store.isMitarbeiter$]).pipe(
map(([isKundenkarte, isBusinessKonto, isMitarbeiter]) => isKundenkarte || isBusinessKonto || isMitarbeiter)
);
constructor(
@Host() private _host: CustomerDetailsViewMainComponent,
private _store: CustomerSearchStore,
private _customerService: CrmCustomerService,
private _modal: UiModalService,
private _navigation: CustomerSearchNavigation
) {
super({
shippingAddresses: [],
selectedShippingAddress: undefined,
});
}
ngOnInit() {
this._store.customerId$.pipe(takeUntil(this._onDestroy$)).subscribe((customerId) => {
this.resetStore();
if (customerId) {
this.loadShippingAddresses(customerId);
}
});
combineLatest([this.selectedShippingAddress$, this._store.customer$])
.pipe(takeUntil(this._onDestroy$))
.subscribe(([selectedShippingAddress, customer]) => {
if (selectedShippingAddress) {
this._host.setShippingAddress(this._createShippingAddressFromShippingAddress(selectedShippingAddress));
} else if (this.showCustomerAddress) {
this._host.setShippingAddress(this._createShippingAddressFromCustomer(customer));
}
});
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
}
_createShippingAddressFromCustomer(customer: CustomerDTO) {
return {
reference: { id: customer.id },
gender: customer?.gender,
title: customer?.title,
firstName: customer?.firstName,
lastName: customer?.lastName,
communicationDetails: customer?.communicationDetails ? { ...customer?.communicationDetails } : undefined,
organisation: customer?.organisation ? { ...customer?.organisation } : undefined,
address: customer?.address ? { ...customer?.address } : undefined,
};
}
_createShippingAddressFromShippingAddress(address: ShippingAddressDTO) {
return {
reference: { id: address.id },
gender: address.gender,
title: address.title,
firstName: address.firstName,
lastName: address.lastName,
communicationDetails: address.communicationDetails ? { ...address.communicationDetails } : undefined,
organisation: address.organisation ? { ...address.organisation } : undefined,
address: address.address ? { ...address.address } : undefined,
source: address.id,
};
}
loadShippingAddresses = this.effect((customerId$: Observable<number>) =>
customerId$.pipe(
switchMap((customerId) =>
this._customerService
.getShippingAddresses({ customerId })
.pipe(tapResponse(this.handleLoadShippingAddressesResponse, this.handleLoadAssignedPayersError))
)
)
);
handleLoadShippingAddressesResponse = (response: ListResponseArgsOfAssignedPayerDTO) => {
const selectedShippingAddress = response.result.reduce<ShippingAddressDTO>((selected, shipping) => {
if (!this.showCustomerAddress && !selected) {
return shipping;
}
if (selected && shipping && new Date(selected.isDefault).getTime() < new Date(shipping.isDefault).getTime()) {
return shipping;
}
return selected;
}, undefined);
this.patchState({
shippingAddresses: response.result,
selectedShippingAddress,
});
};
handleLoadAssignedPayersError = (err: any) => {
this._modal.error('Laden der Lieferadressen fehlgeschlagen', err);
};
resetStore() {
this.patchState({
shippingAddresses: [],
selectedShippingAddress: undefined,
});
}
selectShippingAddress(shippingAddress: ShippingAddressDTO) {
this.patchState({
selectedShippingAddress: shippingAddress,
});
}
selectCustomerAddress() {
this.patchState({
selectedShippingAddress: undefined,
});
}
}

View File

@@ -1,9 +1,9 @@
:host {
@apply block bg-surface text-surface-content rounded-[0.313rem] mb-3;
@apply block bg-surface text-surface-content rounded-[0.313rem] mb-3 relative;
}
.data-label {
@apply w-[10.75rem];
@apply w-52;
}
.data-value {

View File

@@ -1,98 +1,175 @@
<ng-container *ngIf="fetching$ | async; else customerTemplate"></ng-container>
<ng-template #customerTemplate>
<div class="customer-details-header grid grid-flow-row pt-1 px-1 pb-6">
<div class="customer-details-header-actions flex flex-row justify-end pt-1 px-1">
<div class="max-h-[calc(100vh-15.375rem)] overflow-scroll">
<div class="customer-details-header grid grid-flow-row pt-1 px-1 pb-6">
<div class="customer-details-header-actions flex flex-row justify-end pt-1 px-1">
<a
*ngIf="ordersRoute$ | async; let ordersRoute"
class="btn btn-label font-bold text-brand"
[routerLink]="ordersRoute.path"
[queryParams]="ordersRoute.queryParams"
[queryParamsHandling]="'merge'"
>Bestellungen</a
>
<a
*ngIf="kundenkarteRoute$ | async; let kundenkarteRoute"
class="btn btn-label font-bold text-brand"
[routerLink]="kundenkarteRoute.path"
[queryParams]="kundenkarteRoute.queryParams"
[queryParamsHandling]="'merge'"
>Kundenkarte</a
>
<a
*ngIf="historyRoute$ | async; let historyRoute"
class="btn btn-label font-bold text-brand"
[routerLink]="historyRoute.path"
[queryParams]="historyRoute.queryParams"
[queryParamsHandling]="'merge'"
>Historie</a
>
</div>
<div class="customer-details-header-body text-center -mt-3">
<h1 class="text-[1.625rem] font-bold">
{{ (isBusinessKonto$ | async) ? 'Firmendetails' : 'Kundendetails' }}
</h1>
<p>Sind Ihre Kundendaten korrekt?</p>
</div>
</div>
<div class="customer-details-customer-type flex flex-row justify-between items-center bg-surface-2 text-surface-2-content">
<div class="pl-4 font-bold">{{ customerType$ | async }}</div>
<a
*ngIf="historyRoute$ | async; let historyRoute"
*ngIf="editRoute$ | async; let editRoute"
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
class="btn btn-label font-bold text-brand"
[routerLink]="historyRoute.path"
[queryParams]="historyRoute.queryParams"
>Historie</a
type="button"
>Bearbeiten</a
>
</div>
<div class="customer-details-header-body text-center -mt-3">
<h1 class="text-[1.625rem] font-bold">Kundendetails</h1>
<p>Sind Ihre Kundendaten korrekt?</p>
<div class="customer-details-customer-main-data px-5 py-3 grid grid-flow-row gap-3">
<div class="flex flex-row">
<div class="data-label">Erstellungsdatum</div>
<div class="data-value">{{ created$ | async | date: 'dd.MM.yyyy' }} | {{ created$ | async | date: 'hh:mm' }} Uhr</div>
</div>
<div class="flex flex-row">
<div class="data-label">Kundennummer</div>
<div class="data-value">{{ customerNumber$ | async }}</div>
</div>
<div class="flex flex-row" *ngIf="customerNumberDig$ | async; let customerNumberDig">
<div class="data-label">Kundennummer-DIG</div>
<div class="data-value">{{ customerNumberDig }}</div>
</div>
<div class="flex flex-row" *ngIf="customerNumberBeeline$ | async; let customerNumberBeeline">
<div class="data-label">Kundennummer-BEELINE</div>
<div class="data-value">{{ customerNumberBeeline }}</div>
</div>
</div>
</div>
<div class="customer-details-customer-type flex flex-row justify-between items-center bg-surface-2 text-surface-2-content">
<div class="pl-4 font-bold">{{ customerType$ | async }}</div>
<button class="btn btn-label font-bold text-brand" type="button">Bearbeiten</button>
<ng-container *ngIf="isBusinessKonto$ | async">
<div class="customer-details-customer-main-row">
<div class="data-label">Firmenname</div>
<div class="data-value">{{ organisationName$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Abteilung</div>
<div class="data-value">{{ department$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">USt-ID</div>
<div class="data-value">{{ vatId$ | async }}</div>
</div>
</ng-container>
<div class="customer-details-customer-main-row">
<div class="data-label">Anrede</div>
<div class="data-value">{{ gender$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Titel</div>
<div class="data-value">{{ title$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Nachname</div>
<div class="data-value">{{ lastName$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Vorname</div>
<div class="data-value">{{ firstName$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">E-Mail</div>
<div class="data-value">{{ email$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Straße</div>
<div class="data-value">{{ street$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Hausnr.</div>
<div class="data-value">{{ streetNumber$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">PLZ</div>
<div class="data-value">{{ zipCode$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Ort</div>
<div class="data-value">{{ city$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Adresszusatz</div>
<div class="data-value">{{ info$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Land</div>
<div class="data-value">{{ country$ | async | country }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Festnetznr.</div>
<div class="data-value">{{ landline$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Mobilnr.</div>
<div class="data-value">{{ mobile$ | async }}</div>
</div>
<ng-container *ngIf="!(isBusinessKonto$ | async)">
<div class="customer-details-customer-main-row">
<div class="data-label">Abteilung</div>
<div class="data-value">{{ department$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">USt-ID</div>
<div class="data-value">{{ vatId$ | async }}</div>
</div>
</ng-container>
<page-details-main-view-billing-addresses></page-details-main-view-billing-addresses>
<page-details-main-view-delivery-addresses></page-details-main-view-delivery-addresses>
</div>
<div class="customer-details-customer-main-data px-5 py-3 grid grid-flow-row gap-3">
<div class="flex flex-row">
<div class="data-label">Erstellungsdatum</div>
<div class="data-value">{{ created$ | async | date: 'dd.MM.yyyy' }} | {{ created$ | async | date: 'hh:mm' }} Uhr</div>
</div>
<div class="flex flex-row">
<div class="data-label">Kundennummer</div>
<div class="data-value">{{ customerNumber$ | async }}</div>
</div>
<div class="flex flex-row" *ngIf="customerNumberDig$ | async; let customerNumberDig">
<div class="data-label">Kundennummer-DIG</div>
<div class="data-value">{{ customerNumberDig }}</div>
</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Anrede</div>
<div class="data-value">{{ gender$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Titel</div>
<div class="data-value">{{ title$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Nachname</div>
<div class="data-value">{{ lastName$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Vorname</div>
<div class="data-value">{{ firstName$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">E-Mail</div>
<div class="data-value">{{ email$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Straße</div>
<div class="data-value">{{ street$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Hausnr.</div>
<div class="data-value">{{ streetNumber$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">PLZ</div>
<div class="data-value">{{ zipCode$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Ort</div>
<div class="data-value">{{ city$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Adresszusatz</div>
<div class="data-value">{{ info$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Land</div>
<div class="data-value">{{ country$ | async | country }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Festnetznr.</div>
<div class="data-value">{{ landline$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Mobilnr.</div>
<div class="data-value">{{ mobile$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Abteilung</div>
<div class="data-value">{{ department$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">USt-ID</div>
<div class="data-value">{{ vatId$ | async }}</div>
</div>
<button
*ngIf="shoppingCartHasNoItems$ | async"
type="button"
(click)="continue()"
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
[disabled]="showLoader$ | async"
>
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">
Weiter zur Artielsuche
</shared-loader>
</button>
<button
*ngIf="shoppingCartHasItems$ | async"
type="button"
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
[disabled]="showLoader$ | async"
>
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">
Weiter zum Warenkorb
</shared-loader>
</button>
</ng-template>

View File

@@ -1,23 +1,123 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { Subject, combineLatest } from 'rxjs';
import { first, map, switchMap, takeUntil } from 'rxjs/operators';
import { CustomerSearchNavigation } from '../../navigations';
import { CustomerSearchStore } from '../store';
import { CrmCustomerService } from '@domain/crm';
import { ShippingAddressDTO, NotificationChannel, ShoppingCartDTO, PayerDTO, BuyerDTO } from '@swagger/checkout';
import { DomainCheckoutService } from '@domain/checkout';
import { CantAddCustomerToCartData, CantAddCustomerToCartModalComponent, CantSelectGuestModalComponent } from '../../modals';
import { UiModalService } from '@ui/modal';
import { ComponentStore } from '@ngrx/component-store';
import { ApplicationService } from '@core/application';
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
import { Router } from '@angular/router';
const GENDER_MAP = {
2: 'Herr',
4: 'Frau',
};
export interface CustomerDetailsViewMainState {
isBusy: boolean;
shoppingCart: ShoppingCartDTO;
shippingAddress: ShippingAddressDTO;
payer: PayerDTO;
}
const log = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
// log the function name the input and return value
// get the function that is being decorated
const originalMethod = descriptor.value;
// replace the function being decorated with a new function
descriptor.value = function (...args: any[]) {
const now = Date.now();
let result;
console.group(`Method: ${propertyKey}`);
// log the input
console.log(`args: ${JSON.stringify(args)}`);
try {
// call the original function
result = originalMethod.apply(this, args);
// log the output
console.log(`returned: ${JSON.stringify(result)}`);
// return the result of calling the original method
} catch (error) {
console.error(`error: ${error}`);
throw error;
}
console.log(`execution time: ${Date.now() - now}ms`);
console.groupEnd();
return result;
};
// return the descriptor
return descriptor;
};
const logAsync = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
// log the function name the input and return value
// get the function that is being decorated
const originalMethod = descriptor.value;
// replace the function being decorated with a new function
descriptor.value = async function (...args: any[]) {
const now = Date.now();
let result;
console.group(`Method: ${propertyKey}`);
// log the input
console.log(`args: ${JSON.stringify(args)}`);
try {
// call the original function
result = await originalMethod.apply(this, args);
// log the output
console.log(`returned: ${JSON.stringify(result)}`);
// return the result of calling the original method
} catch (error) {
console.error(`error: ${error}`);
throw error;
}
console.log(`execution time: ${Date.now() - now}ms`);
console.groupEnd();
return result;
};
// return the descriptor
return descriptor;
};
@Component({
selector: 'page-customer-details-main-view',
templateUrl: 'details-main-view.component.html',
styleUrls: ['details-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerDetailsViewMainComponent {
export class CustomerDetailsViewMainComponent extends ComponentStore<CustomerDetailsViewMainState> implements OnInit, OnDestroy {
private _onDestroy$ = new Subject();
fetching$ = this._store.fetchingCustomer$;
isBusy$ = this.select((s) => s.isBusy);
showLoader$ = combineLatest([this.fetching$, this.isBusy$]).pipe(map(([fetching, isBusy]) => fetching || isBusy));
processId$ = this._store.processId$;
customerId$ = this._store.customerId$;
@@ -26,6 +126,26 @@ export class CustomerDetailsViewMainComponent {
map(([processId, customerId]) => this._navigation.historyRoute({ processId, customerId }))
);
ordersRoute$ = combineLatest([this.processId$, this.customerId$]).pipe(
map(([processId, customerId]) => this._navigation.ordersRoute({ processId, customerId }))
);
isB2b$ = this._store.select((s) => s.customer?.features?.['b2b']);
editRoute$ = combineLatest([this.processId$, this.customerId$, this.isB2b$]).pipe(
map(([processId, customerId, isB2b]) => this._navigation.editRoute({ processId, customerId, isB2b }))
);
hasKundenkarte$ = combineLatest([this._store.isKundenkarte$, this._store.isOnlineKontoMitKundenkarte$]).pipe(
map(([isKundenkarte, isOnlineKontoMitKundenkarte]) => isKundenkarte || isOnlineKontoMitKundenkarte)
);
kundenkarteRoute$ = combineLatest([this.hasKundenkarte$, this.processId$, this.customerId$]).pipe(
map(([hasKundenkarte, processId, customerId]) =>
hasKundenkarte ? this._navigation.kundenkarteRoute({ processId, customerId }) : undefined
)
);
customerType$ = this._store.select((s) => s.customer?.features?.find((f) => f.enabled)?.description);
created$ = this._store.select((s) => s.customer?.created);
@@ -34,6 +154,8 @@ export class CustomerDetailsViewMainComponent {
customerNumberDig$ = this._store.select((s) => s.customer?.linkedRecords?.find((r) => r.repository === 'dig')?.number);
customerNumberBeeline$ = this._store.select((s) => s.customer?.linkedRecords?.find((r) => r.repository === 'beeline')?.number);
gender$ = this._store.select((s) => GENDER_MAP[s.customer?.gender]);
title$ = this._store.select((s) => s.customer?.title);
@@ -60,9 +182,329 @@ export class CustomerDetailsViewMainComponent {
mobile$ = this._store.select((s) => s.customer?.communicationDetails?.mobile);
organisationName$ = this._store.select((s) => s.customer?.organisation?.name);
department$ = this._store.select((s) => s.customer?.organisation?.department);
vatId$ = this._store.select((s) => s.customer?.organisation?.vatId);
constructor(private _store: CustomerSearchStore, private _navigation: CustomerSearchNavigation) {}
isBusinessKonto$ = this._store.isBusinessKonto$;
shoppingCartHasItems$ = this.select((s) => s.shoppingCart?.items?.length > 0);
shoppingCartHasNoItems$ = this.shoppingCartHasItems$.pipe(map((hasItems) => !hasItems));
get isBusy() {
return this.get((s) => s.isBusy);
}
get shoppingCart() {
return this.get((s) => s.shoppingCart);
}
get shoppingCartHasItems() {
return this.shoppingCart?.items?.length > 0;
}
get processId() {
return this._store.processId;
}
get customer() {
return this._store.customer;
}
get customerFeatures() {
const customerFeatures = this.customer.features;
const features: { [key: string]: string } = {};
for (const feature of customerFeatures) {
features[feature.key] = feature.key;
}
return features;
}
get shippingAddress() {
return this.get((s) => s.shippingAddress);
}
get buyer() {
const customer = this.customer;
return {
source: customer.id,
reference: { id: customer.id },
buyerType: customer.customerType as any,
buyerNumber: customer.customerNumber,
gender: customer.gender,
title: customer.title,
firstName: customer.firstName,
lastName: customer.lastName,
dateOfBirth: customer.dateOfBirth,
communicationDetails: customer.communicationDetails ? { ...customer.communicationDetails } : undefined,
organisation: customer.organisation ? { ...customer.organisation } : undefined,
address: customer.address ? { ...customer.address } : undefined,
};
}
get payer() {
return this.get((s) => s.payer);
}
constructor(
private _store: CustomerSearchStore,
private _navigation: CustomerSearchNavigation,
private _checkoutService: DomainCheckoutService,
private _modalService: UiModalService,
private _application: ApplicationService,
private _catalogNavigation: ProductCatalogNavigationService,
private _checkoutNavigation: CheckoutNavigationService,
private _router: Router
) {
super({ isBusy: true, shoppingCart: undefined, shippingAddress: undefined, payer: undefined });
}
setIsBusy(isBusy: boolean) {
this.patchState({ isBusy });
}
@log
setShippingAddress(address: ShippingAddressDTO) {
this.patchState({ shippingAddress: address });
}
@log
setPayer(payer: PayerDTO) {
this.patchState({ payer });
}
ngOnInit() {
this.processId$
.pipe(
takeUntil(this._onDestroy$),
switchMap((pid) =>
this._checkoutService.getShoppingCart({
processId: pid,
latest: true,
})
)
)
.subscribe((shoppingCart) => {
this.patchState({ shoppingCart });
});
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
}
@logAsync
async continue() {
if (this.isBusy) return;
this.setIsBusy(true);
if (this.shoppingCartHasItems && !(await this._canAddCustomerAsync())) {
return;
}
if (this.shippingAddress && !this._canAddShippingAddressAsync()) {
return;
}
if (!(await this._updateDestinationAsync())) {
return;
}
if (this._isGuestWithOrder()) {
this._modalService.open({
content: CantSelectGuestModalComponent,
data: this.customer,
});
return;
}
this._patchProcessName();
const currentBuyer = await this._getCurrentBuyer();
this._setCustomer();
this._setBuyer();
await this._updateNotifcationChannelsAsync(currentBuyer);
this._setPayer();
this._setShippingAddress();
if (this.shoppingCartHasItems) {
// Navigation zum Warenkorb
const path = this._checkoutNavigation.getCheckoutReviewPath(this.processId);
this._router.navigate(path);
} else {
// Navigation zur Artikelsuche
const path = this._catalogNavigation.getArticleSearchBasePath(this.processId);
this._router.navigate(path);
}
try {
} catch (error) {
this._modalService.error('Warenkorb kann dem Kunden nicht zugewiesen werden', error);
} finally {
this.setIsBusy(false);
}
}
@logAsync
_getCurrentBuyer() {
return this._checkoutService.getBuyer({ processId: this.processId }).pipe(first()).toPromise();
}
@logAsync
async _canAddCustomerAsync() {
const res = await this._checkoutService
.canSetCustomer({
processId: this.processId,
customerFeatures: this.customerFeatures,
})
.toPromise();
if (res.ok) {
return true;
}
const upgradeableTo = res.create;
const data: CantAddCustomerToCartData = {
message: res.message,
upgradeableTo,
customer: this.customer,
attributes: this.customer.attributes.map((a) => a.data),
required: res.create,
};
await this._modalService
.open({
content: CantAddCustomerToCartModalComponent,
data,
})
.afterClosed$.toPromise();
return false;
}
@logAsync
async _canAddShippingAddressAsync() {
const res = await this._checkoutService
.canAddDestination({
processId: this.processId,
destinationDTO: {
target: 2,
shippingAddress: this.shippingAddress,
},
})
.toPromise();
if (typeof res === 'string') {
throw new Error(res);
}
return res;
}
@logAsync
async _updateDestinationAsync() {
const res = await this._checkoutService
.setDestinationForCustomer({
processId: this.processId,
customerFeatures: this.customerFeatures,
})
.toPromise();
if (!res.ok) {
this._modalService.open({
content: CantAddCustomerToCartModalComponent,
data: {
message: '',
customer: this.customer,
required: res.create,
upgradeableTo: undefined,
attributes: this.customer.attributes.map((s) => s.data),
} as CantAddCustomerToCartData,
});
}
return res.ok;
}
@log
_isGuestWithOrder() {
const isGuest = this.customer.features.some((f) => f.key === 'guest');
if (!isGuest) return false;
const hasOrder = this.customer.orderCount > 0;
return hasOrder;
}
@log
_patchProcessName() {
let name = `${this.customer.firstName} ${this.customer.lastName}`;
if (this._store.isBusinessKonto) {
name = `${this.customer.organisation?.name}`;
}
this._application.patchProcess(this.processId, {
name,
});
}
@log
_setCustomer() {
this._checkoutService.setCustomer({
processId: this.processId,
customerDto: this.customer,
});
}
@log
_setBuyer() {
this._checkoutService.setBuyer({
processId: this.processId,
buyer: this.buyer,
});
}
@logAsync
async _updateNotifcationChannelsAsync(currentBuyer: BuyerDTO | undefined) {
if (currentBuyer?.buyerNumber !== this.customer.customerNumber) {
const notificationChannels =
this.customer.notificationChannels === (3 as NotificationChannel) ? 1 : this.customer.notificationChannels;
this._checkoutService.setNotificationChannels({
processId: this.processId,
notificationChannels,
});
}
}
@log
_setPayer() {
if (this.payer) {
this._checkoutService.setPayer({
processId: this.processId,
payer: this.payer,
});
}
}
@log
_setShippingAddress() {
if (this.shippingAddress) {
this._checkoutService.setShippingAddress({
processId: this.processId,
shippingAddress: this.shippingAddress,
});
}
}
}

View File

@@ -4,9 +4,19 @@ import { CommonModule } from '@angular/common';
import { CustomerDetailsViewMainComponent } from './details-main-view.component';
import { CountryPipe } from '@shared/pipes/country';
import { RouterModule } from '@angular/router';
import { DetailsMainViewBillingAddressesComponent } from './details-main-view-billing-addresses/details-main-view-billing-addresses.component';
import { DetailsMainViewDeliveryAddressesComponent } from './details-main-view-delivery-addresses/details-main-view-delivery-addresses.component';
import { LoaderComponent } from '@shared/components/loader';
@NgModule({
imports: [CommonModule, CountryPipe, RouterModule],
imports: [
LoaderComponent,
CommonModule,
CountryPipe,
RouterModule,
DetailsMainViewBillingAddressesComponent,
DetailsMainViewDeliveryAddressesComponent,
],
exports: [CustomerDetailsViewMainComponent],
declarations: [CustomerDetailsViewMainComponent],
})

View File

@@ -0,0 +1,7 @@
:host {
@apply block bg-surface text-surface-content rounded px-6 py-8;
}
form {
@apply grid grid-cols-2 gap-x-8;
}

View File

@@ -0,0 +1,84 @@
<div class="flex flex-row justify-end -mt-4 -mr-2">
<a
*ngIf="detailsRoute$ | async; let detailsRoute"
[routerLink]="detailsRoute.path"
[queryParams]="detailsRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
>
<shared-icon icon="close" [size]="32"></shared-icon>
</a>
</div>
<h1 class="text-2xl text-center font-bold mb-6">Rechnungsadresse bearbeiten</h1>
<form [formGroup]="formGroup" (ngSubmit)="save()">
<shared-form-control label="Anrede">
<shared-select formControlName="gender" placeholder="Anrede" tabindex="1" [autofocus]="true">
<shared-select-option [value]="2">Herr</shared-select-option>
<shared-select-option [value]="4">Frau</shared-select-option>
</shared-select>
</shared-form-control>
<shared-form-control label="Titel">
<shared-select formControlName="title" placeholder="Titel" tabindex="2">
<shared-select-option value="Dipl.-Ing.">Dipl.-Ing.</shared-select-option>
<shared-select-option value="Dr.">Dr.</shared-select-option>
<shared-select-option value="Dr. med.">Dr. med.</shared-select-option>
<shared-select-option value="Prof.">Prof.</shared-select-option>
<shared-select-option value="Prof. Dr.">Prof. Dr.</shared-select-option>
<shared-select-option value="RA">RA</shared-select-option>
</shared-select>
</shared-form-control>
<shared-form-control label="Nachname">
<input class="input-control" placeholder="Nachname" type="text" formControlName="lastName" tabindex="3" />
</shared-form-control>
<shared-form-control label="Vorname">
<input class="input-control" placeholder="Vorname" type="text" formControlName="firstName" tabindex="4" />
</shared-form-control>
<shared-form-control label="Firma" class="col-span-2">
<input class="input-control" placeholder="Firma" type="text" formControlName="organisation" tabindex="5" />
</shared-form-control>
<shared-form-control label="Straße">
<input class="input-control" placeholder="Straße" type="text" formControlName="street" tabindex="6" />
</shared-form-control>
<shared-form-control label="Hausnummer">
<input class="input-control" placeholder="Hausnummer" type="text" formControlName="streetNumber" tabindex="7" />
</shared-form-control>
<shared-form-control label="PLZ">
<input class="input-control" placeholder="PLZ" type="text" formControlName="zipCode" tabindex="8" />
</shared-form-control>
<shared-form-control label="Ort">
<input class="input-control" placeholder="Ort" type="text" formControlName="city" tabindex="9" />
</shared-form-control>
<shared-form-control label="Adresszusatz" class="col-span-2">
<input class="input-control" placeholder="Adresszusatz" type="text" formControlName="info" tabindex="10" />
</shared-form-control>
<shared-form-control class="col-span-2" label="Land">
<shared-select placeholder="Land" formControlName="country" tabindex="11">
<shared-select-option *ngFor="let country of countries$ | async" [value]="country.isO3166_A_3">
{{ country.name }}
</shared-select-option>
</shared-select>
</shared-form-control>
<div class="text-center col-span-2">
<shared-checkbox>Diese Rechnungsadresse als Standard Adresse festlegen</shared-checkbox>
</div>
<div class="mt-6 text-center col-span-2">
<button
[disabled]="formGroup.invalid || formGroup.disabled"
type="submit"
class="px-5 py-3 font-bold text-lg rounded-full bg-brand text-white"
>
Speichern
</button>
</div>
</form>

View File

@@ -0,0 +1,153 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { CheckboxComponent } from '@shared/components/checkbox';
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { SelectModule } from '@shared/components/select';
import { FormControlComponent } from '@shared/components/form-control';
import { CrmCustomerService } from '@domain/crm';
import { AddressDTO, Gender, PayerDTO } from '@swagger/crm';
import { map, switchMap, takeUntil } from 'rxjs/operators';
import { AsyncPipe, NgForOf } from '@angular/common';
import { AddressSelectionModalService } from '@shared/modals/address-selection-modal';
import { CustomerSearchStore } from '../store';
import { CustomerSearchNavigation } from '../../navigations';
import { ComponentStore } from '@ngrx/component-store';
import { Subject, combineLatest } from 'rxjs';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { UiModalService } from '@ui/modal';
import { IconComponent } from '@shared/components/icon';
export interface EditBillingAddressMainViewState {
payer?: PayerDTO;
}
@Component({
selector: 'page-edit-billing-address-main-view',
templateUrl: 'edit-billing-address-main-view.component.html',
styleUrls: ['edit-billing-address-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-edit-billing-address-main-view' },
standalone: true,
imports: [AsyncPipe, RouterLink, IconComponent, NgForOf, ReactiveFormsModule, SelectModule, FormControlComponent, CheckboxComponent],
})
export class EditBillingAddressMainViewComponent extends ComponentStore<EditBillingAddressMainViewState> implements OnInit, OnDestroy {
private _onDestroy$ = new Subject<void>();
detailsRoute$ = combineLatest([this._store.processId$, this._store.customerId$]).pipe(
map(([processId, customerId]) => this._navigation.detailsRoute({ processId, customerId }))
);
formGroup = new FormGroup({
gender: new FormControl<Gender>(0, [Validators.required, Validators.min(1)]),
title: new FormControl<string>(undefined),
firstName: new FormControl<string>(undefined, [Validators.required]),
lastName: new FormControl<string>(undefined, [Validators.required]),
organisation: new FormControl<string>(undefined),
street: new FormControl<string>(undefined, [Validators.required]),
streetNumber: new FormControl<string>(undefined, [Validators.required]),
zipCode: new FormControl<string>(undefined, [Validators.required]),
city: new FormControl<string>(undefined, [Validators.required]),
country: new FormControl<string>('DEU', [Validators.required]),
info: new FormControl<string>(undefined),
isDefault: new FormControl<boolean>(false),
});
countries$ = this._customerService.getCountries().pipe(map((res) => res.result));
payerId$ = this._activatedRoute.params.pipe(
map((params) => params.payerId),
switchMap((payerId) => this._customerService.getPayer(payerId).pipe(map((res) => res.result)))
);
get payerId() {
return this.get((s) => s.payer?.id);
}
get payer() {
return this.get((s) => s.payer);
}
constructor(
private _customerService: CrmCustomerService,
private _addressSelection: AddressSelectionModalService,
private _store: CustomerSearchStore,
private _navigation: CustomerSearchNavigation,
private _activatedRoute: ActivatedRoute,
private _modal: UiModalService
) {
super({ payer: undefined });
}
ngOnInit() {
this.payerId$.pipe(takeUntil(this._onDestroy$)).subscribe((payer) => {
this.patchState({ payer });
this.patchFormGroup(payer);
});
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
}
patchFormGroup(payer: PayerDTO) {
this.formGroup.patchValue({
gender: payer.gender ?? 0,
title: payer.title,
lastName: payer.lastName,
firstName: payer.firstName,
organisation: payer.organisation?.name,
street: payer.address.street,
streetNumber: payer.address.streetNumber,
zipCode: payer.address.zipCode,
city: payer.address.city,
country: payer.address.country,
info: payer.address.info,
});
}
async save() {
if (this.formGroup.invalid) {
this.formGroup.markAllAsTouched();
return;
}
try {
this.formGroup.disable();
const formData = this.formGroup.value;
const address: AddressDTO = {
street: formData.street,
streetNumber: formData.streetNumber,
zipCode: formData.zipCode,
city: formData.city,
country: formData.country,
info: formData.info,
};
const addressValidationResult = await this._addressSelection.validateAddress(address);
if (addressValidationResult === undefined) {
this.formGroup.enable();
return;
}
const payer: PayerDTO = {
...this.payer,
gender: formData.gender,
title: formData.title,
firstName: formData.firstName,
lastName: formData.lastName,
organisation: formData.organisation ? { name: formData.organisation } : undefined,
address: addressValidationResult,
};
const result = await this._customerService.updatePayer(this._store.customerId, this.payerId, payer, formData.isDefault).toPromise();
this._navigation.navigateToDetails({ processId: this._store.processId, customerId: this._store.customerId });
} catch (error) {
this.formGroup.enable();
this._modal.error('Fehler beim speichern der Rechnungsadresse', error);
}
}
}

View File

@@ -0,0 +1,101 @@
<div class="customer-edit-header-actions flex flex-row justify-end pt-1 px-1">
<a
*ngIf="detailsRoute$ | async; let route"
[routerLink]="route.path"
[queryParams]="route.queryParams"
[queryParamsHandling]="'merge'"
class="btn btn-label"
>
<ui-icon [icon]="'close'"></ui-icon>
</a>
</div>
<div class="header">
<h1>Firmendetails</h1>
</div>
<form *ngIf="control" [formGroup]="control" (ngSubmit)="submit()">
<ng-container formGroupName="organisation">
<ui-form-control label="Firmenname" variant="inline">
<input uiInput type="text" formControlName="name" tabindex="1" />
</ui-form-control>
<ui-form-control label="Abteilung" variant="inline">
<input uiInput type="text" formControlName="department" tabindex="2" />
</ui-form-control>
<ui-form-control label="USt ID" variant="inline">
<input uiInput type="text" formControlName="vatId" tabindex="3" />
</ui-form-control>
</ng-container>
<ui-form-control [clearable]="true" label="Anrede" variant="inline">
<ui-select formControlName="gender" tabindex="4">
<ui-select-option [value]="2" label="Herr"></ui-select-option>
<ui-select-option [value]="4" label="Frau"></ui-select-option>
</ui-select>
</ui-form-control>
<ui-form-control [clearable]="true" label="Titel" variant="inline">
<ui-select formControlName="title" tabindex="5">
<ui-select-option value="Dipl.-Ing." label="Dipl.-Ing."></ui-select-option>
<ui-select-option value="Dr." label="Dr."></ui-select-option>
<ui-select-option value="Dr. med." label="Dr. med."></ui-select-option>
<ui-select-option value="Prof." label="Prof."></ui-select-option>
<ui-select-option value="Prof. Dr." label="Prof. Dr."></ui-select-option>
<ui-select-option value="RA" label="RA"></ui-select-option>
</ui-select>
</ui-form-control>
<ui-form-control label="Nachname" variant="inline">
<input uiInput type="text" formControlName="lastName" tabindex="6" />
</ui-form-control>
<ui-form-control label="Vorname" variant="inline">
<input uiInput type="text" formControlName="firstName" tabindex="7" />
</ui-form-control>
<ng-container formGroupName="communicationDetails">
<ui-form-control label="E-Mail" variant="inline">
<input uiInput type="mail" formControlName="email" tabindex="8" />
</ui-form-control>
<ui-form-control label="Festnetznummer" variant="inline">
<input uiInput type="tel" formControlName="phone" tabindex="9" />
</ui-form-control>
<ui-form-control label="Mobilnummer" variant="inline">
<input uiInput type="tel" formControlName="mobile" tabindex="10" />
</ui-form-control>
</ng-container>
<ng-container formGroupName="address">
<ui-form-control label="Straße" variant="inline">
<input uiInput type="text" formControlName="street" tabindex="11" />
</ui-form-control>
<ui-form-control label="Hausnummer" variant="inline">
<input uiInput type="text" formControlName="streetNumber" tabindex="12" />
</ui-form-control>
<ui-form-control label="PLZ" variant="inline">
<input uiInput type="text" formControlName="zipCode" tabindex="13" />
</ui-form-control>
<ui-form-control label="Ort" variant="inline">
<input uiInput type="text" formControlName="city" tabindex="14" />
</ui-form-control>
<ui-form-control label="Adresszusatz" variant="inline">
<input uiInput type="text" formControlName="info" tabindex="15" />
</ui-form-control>
<ui-form-control [clearable]="true" label="Land" variant="inline">
<ui-select formControlName="country" tabindex="16">
<ui-select-option
*ngFor="let country of countries$ | async"
[label]="country.name"
[value]="country.isO3166_A_3"
></ui-select-option>
</ui-select>
</ui-form-control>
</ng-container>
<div class="actions">
<button class="btn-cancel" type="button" (click)="cancel()">Abbrechen</button>
<button class="btn-save" type="submit" [disabled]="control.invalid || control.disabled" [ngSwitch]="control.enabled">
<ng-container *ngSwitchCase="true">
Speichern
</ng-container>
<ui-icon class="spin" icon="loading" size="18px" *ngSwitchCase="false"></ui-icon>
</button>
</div>
</form>

View File

@@ -0,0 +1,24 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CustomerDataEditComponent } from './customer-data-edit.component';
import { CommonModule } from '@angular/common';
import { UiCommonModule } from '@ui/common';
import { UiFormControlModule } from '@ui/form-control';
import { UiSelectModule } from '@ui/select';
import { UiIconModule } from '@ui/icon';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
@Component({
selector: 'page-customer-data-edit-b2b',
templateUrl: 'customer-data-edit-b2b.component.html',
styleUrls: ['customer-data-edit.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, UiCommonModule, UiFormControlModule, UiSelectModule, UiIconModule, ReactiveFormsModule, RouterLink],
})
export class CustomerDataEditB2BComponent extends CustomerDataEditComponent {
afterInitForm = (control) => {
control.get('lastName').setValidators([]);
control.get('firstName').setValidators([]);
};
}

View File

@@ -0,0 +1,102 @@
<div class="customer-edit-header-actions flex flex-row justify-end pt-1 px-1">
<a
*ngIf="detailsRoute$ | async; let route"
[routerLink]="route.path"
[queryParams]="route.queryParams"
[queryParamsHandling]="'merge'"
class="btn btn-label"
>
<ui-icon [icon]="'close'"></ui-icon>
</a>
</div>
<div class="header">
<h1>Kundendetails</h1>
</div>
<form *ngIf="control" [formGroup]="control" (ngSubmit)="submit()">
<ui-form-control [clearable]="true" label="Anrede" variant="inline">
<ui-select formControlName="gender" tabindex="1">
<ui-select-option [value]="2" label="Herr"></ui-select-option>
<ui-select-option [value]="4" label="Frau"></ui-select-option>
</ui-select>
</ui-form-control>
<ui-form-control [clearable]="true" label="Titel" variant="inline">
<ui-select formControlName="title" tabindex="2">
<ui-select-option value="Dipl.-Ing." label="Dipl.-Ing."></ui-select-option>
<ui-select-option value="Dr." label="Dr."></ui-select-option>
<ui-select-option value="Dr. med." label="Dr. med."></ui-select-option>
<ui-select-option value="Prof." label="Prof."></ui-select-option>
<ui-select-option value="Prof. Dr." label="Prof. Dr."></ui-select-option>
<ui-select-option value="RA" label="RA"></ui-select-option>
</ui-select>
</ui-form-control>
<ui-form-control label="Nachname" variant="inline">
<input uiInput type="text" formControlName="lastName" tabindex="3" />
</ui-form-control>
<ui-form-control label="Vorname" variant="inline">
<input uiInput type="text" formControlName="firstName" tabindex="4" />
</ui-form-control>
<ui-form-control label="E-Mail" formGroupName="communicationDetails" variant="inline">
<input uiInput type="mail" formControlName="email" tabindex="5" />
</ui-form-control>
<ng-container formGroupName="address">
<ui-form-control label="Straße" variant="inline">
<input uiInput type="text" formControlName="street" tabindex="6" />
</ui-form-control>
<ui-form-control label="Hausnummer" variant="inline">
<input uiInput type="text" formControlName="streetNumber" tabindex="7" />
</ui-form-control>
<ui-form-control label="PLZ" variant="inline">
<input uiInput type="text" formControlName="zipCode" tabindex="8" />
</ui-form-control>
<ui-form-control label="Ort" variant="inline">
<input uiInput type="text" formControlName="city" tabindex="9" />
</ui-form-control>
<ui-form-control label="Adresszusatz" variant="inline">
<input uiInput type="text" formControlName="info" tabindex="10" />
</ui-form-control>
<ui-form-control [clearable]="true" label="Land" variant="inline">
<ui-select formControlName="country" tabindex="11">
<ui-select-option
*ngFor="let country of countries$ | async"
[label]="country.name"
[value]="country.isO3166_A_3"
></ui-select-option>
</ui-select>
</ui-form-control>
</ng-container>
<ng-container formGroupName="communicationDetails">
<ui-form-control label="Festnetznummer" variant="inline">
<input uiInput type="tel" formControlName="phone" tabindex="12" />
</ui-form-control>
<ui-form-control label="Mobilnummer" variant="inline">
<input uiInput type="tel" formControlName="mobile" tabindex="13" />
</ui-form-control>
</ng-container>
<ui-form-control label="Geburtsdatum" variant="inline">
<input uiDateInput type="text" formControlName="dateOfBirth" tabindex="14" />
</ui-form-control>
<ng-container formGroupName="organisation">
<ui-form-control label="Firmenname" variant="inline">
<input uiInput type="text" formControlName="name" tabindex="15" />
</ui-form-control>
<ui-form-control label="Abteilung" variant="inline">
<input uiInput type="text" formControlName="department" tabindex="16" />
</ui-form-control>
<ui-form-control label="USt ID" variant="inline">
<input uiInput type="text" formControlName="vatId" tabindex="17" />
</ui-form-control>
</ng-container>
<div class="actions">
<button class="btn-cancel" type="button" (click)="cancel()">Abbrechen</button>
<button class="btn-save" type="submit" [disabled]="control.invalid || control.disabled" [ngSwitch]="control.enabled">
<ng-container *ngSwitchCase="true">
Speichern
</ng-container>
<ui-icon class="spin" icon="loading" size="18px" *ngSwitchCase="false"></ui-icon>
</button>
</div>
</form>

View File

@@ -0,0 +1,19 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CustomerDataEditComponent } from './customer-data-edit.component';
import { CommonModule } from '@angular/common';
import { UiCommonModule } from '@ui/common';
import { UiFormControlModule } from '@ui/form-control';
import { UiSelectModule } from '@ui/select';
import { ReactiveFormsModule } from '@angular/forms';
import { UiIconModule } from '@ui/icon';
import { RouterLink } from '@angular/router';
@Component({
selector: 'page-customer-data-edit-b2c',
templateUrl: 'customer-data-edit-b2c.component.html',
styleUrls: ['customer-data-edit.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, UiCommonModule, UiFormControlModule, UiSelectModule, UiIconModule, ReactiveFormsModule, RouterLink],
})
export class CustomerDataEditB2CComponent extends CustomerDataEditComponent {}

View File

@@ -0,0 +1,54 @@
:host {
@apply flex flex-col box-border shadow-card rounded;
}
.customer-edit-header-actions {
@apply bg-white;
}
.header {
@apply bg-white;
h1 {
@apply text-center text-h3 my-8 font-bold;
}
}
form {
@apply grid flex-col gap-px-2 bg-transparent;
ui-form-control {
@apply p-4 bg-white;
ui-select {
@apply ml-8;
}
input {
@apply ml-8;
}
}
.actions {
@apply text-center my-8;
button {
@apply rounded-full outline-none p-4 text-p1 border-brand border-solid border-2 px-px-25 py-px-15 mx-4 font-bold;
&.btn-cancel {
@apply text-brand bg-white;
}
&.btn-save {
@apply text-white bg-brand;
}
&:disabled {
@apply bg-inactive-branch border-none;
}
}
}
}
.spin {
@apply animate-spin;
}

View File

@@ -0,0 +1,158 @@
import { Location } from '@angular/common';
import { HttpErrorResponse } from '@angular/common/http';
import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core';
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { CrmCustomerService } from '@domain/crm';
import { CountryDTO, CustomerDTO, KeyValueDTOOfStringAndString } from '@swagger/crm';
import { UiValidators } from '@ui/validators';
import { Observable, combineLatest } from 'rxjs';
import { first, map, switchMap } from 'rxjs/operators';
import { validateEmail } from '../../validators/email-validator';
import { genderLastNameValidator } from '../../validators/gender-b2b-validator';
import { camelCase } from 'lodash';
import { CustomerSearchStore } from '../store';
import { CustomerSearchNavigation, NavigationRoute } from '../../navigations';
@Component({ template: '' })
export abstract class CustomerDataEditComponent implements OnInit {
customer$: Observable<CustomerDTO>;
customerId$: Observable<number>;
countries$: Observable<CountryDTO[]>;
customerFeatures$: Observable<KeyValueDTOOfStringAndString[]>;
customerType$: Observable<string>;
isWebshopOrGuest$: Observable<boolean>;
customerFeatureB2bOrB2c$: Observable<string>;
control: UntypedFormGroup;
detailsRoute$: Observable<NavigationRoute>;
get customerId() {
return Number(this.activatedRoute.snapshot.params['customerId']);
}
afterInitForm?: (control: AbstractControl) => void;
constructor(
private customerService: CrmCustomerService,
private activatedRoute: ActivatedRoute,
private fb: UntypedFormBuilder,
private cdr: ChangeDetectorRef,
private location: Location,
private _store: CustomerSearchStore,
private _navigation: CustomerSearchNavigation
) {}
ngOnInit() {
this.customerId$ = this.activatedRoute.params.pipe(map((p) => Number(p['customerId'])));
this.customer$ = this.customerId$.pipe(
switchMap((id) => this.customerService.getCustomer(id, 2)),
map((cr) => cr.result)
);
this.customerFeatures$ = this.customer$.pipe(map((customer: CustomerDTO) => Object.values(customer.features).filter((f) => f.enabled)));
this.customerType$ = this.customerFeatures$.pipe(
map(
(features: KeyValueDTOOfStringAndString[]) =>
features.find((f) => f.key === 'store' || f.key === 'webshop' || f.key === 'b2b' || f.key === 'guest')?.key
)
);
this.isWebshopOrGuest$ = this.customerType$.pipe(map((type) => type === 'webshop' || type === 'guest'));
this.customerFeatureB2bOrB2c$ = this.customerType$.pipe(map((type) => (type === 'b2b' ? 'b2b' : 'b2c')));
this.countries$ = this.isWebshopOrGuest$.pipe(
switchMap((webshopOrGuest) => {
if (!webshopOrGuest) {
return this.customerService.getCountries().pipe(map((p) => p.result));
} else {
return this.customerService.getCountries().pipe(
map((p) => p.result),
map((countries) => countries.filter((country) => country.name === 'Deutschland'))
);
}
})
);
this.initForm();
this.detailsRoute$ = combineLatest([this._store.processId$, this._store.customerId$]).pipe(
map(([processId, customerId]) => this._navigation.detailsRoute({ processId, customerId }))
);
}
async initForm() {
const { fb } = this;
const customerDTO = await this.customer$.pipe(first()).toPromise();
const customerType = await this.customerType$.pipe(first()).toPromise();
const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise();
const isB2b = customerType === 'b2b';
const isBranch = customerType === 'store';
const isWebshop = customerType === 'webshop';
const isCard = customerFeatures.find((feature) => feature.key === 'p4muser');
this.control = fb.group(
{
gender: fb.control(customerDTO?.gender, [isBranch ? Validators.min(1) : () => null, isBranch ? Validators.required : () => null]),
title: fb.control(customerDTO?.title),
lastName: fb.control(customerDTO?.lastName, [Validators.required]),
firstName: fb.control(customerDTO?.firstName, [Validators.required]),
dateOfBirth: fb.control(customerDTO?.dateOfBirth, [isWebshop && isCard ? Validators.required : () => null, UiValidators.date]),
communicationDetails: fb.group({
email: fb.control(customerDTO?.communicationDetails?.email, [validateEmail]),
phone: fb.control(customerDTO?.communicationDetails?.phone),
mobile: fb.control(customerDTO?.communicationDetails?.mobile),
}),
organisation: fb.group({
name: fb.control(customerDTO?.organisation?.name),
vatId: fb.control(customerDTO?.organisation?.vatId),
department: fb.control(customerDTO?.organisation?.department),
}),
address: fb.group({
street: fb.control(customerDTO?.address?.street),
streetNumber: fb.control(customerDTO?.address?.streetNumber),
zipCode: fb.control(customerDTO?.address?.zipCode),
city: fb.control(customerDTO?.address?.city),
country: fb.control(customerDTO?.address?.country),
info: fb.control(customerDTO?.address?.info),
}),
},
{ validators: genderLastNameValidator(isB2b) }
);
if (typeof this.afterInitForm === 'function') {
this.afterInitForm.call(this, this.control);
}
this.control.markAllAsTouched();
this.cdr.markForCheck();
}
cancel() {
this.location.back();
}
async submit() {
if (!this.control.valid || !this.control.enabled) {
return;
}
this.control.disable();
try {
await this.customerService.patchCustomer(this.customerId, this.control.value).toPromise();
this.location.back();
} catch (error) {
this.control.enable();
if (error instanceof HttpErrorResponse) {
if (error.error.invalidProperties) {
const invProps = error.error.invalidProperties;
const keys = Object.keys(invProps);
for (const key of keys) {
this.control.get('address')?.get(camelCase(key))?.setErrors({ validateAddress: invProps[key] });
}
}
}
console.error(error);
}
}
}

View File

@@ -0,0 +1,7 @@
:host {
@apply block bg-surface text-surface-content rounded px-6 py-8;
}
form {
@apply grid grid-cols-2 gap-x-8;
}

View File

@@ -0,0 +1,101 @@
<div class="flex flex-row justify-end -mt-4 -mr-2">
<a
*ngIf="detailsRoute$ | async; let detailsRoute"
[routerLink]="detailsRoute.path"
[queryParams]="detailsRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
>
<shared-icon icon="close" [size]="32"></shared-icon>
</a>
</div>
<h1 class="text-2xl text-center font-bold mb-6">Lieferadresse bearbeiten</h1>
<form [formGroup]="formGroup" (ngSubmit)="save()">
<ng-container *ngIf="isBusinessKonto$ | async">
<shared-form-control label="Firma" class="col-span-2">
<input class="input-control" placeholder="Firma" type="text" formControlName="organisation" tabindex="1" />
</shared-form-control>
<shared-form-control label="Abteilung">
<input class="input-control" placeholder="Abteilung" type="text" formControlName="department" tabindex="2" />
</shared-form-control>
<shared-form-control label="USt-ID">
<input class="input-control" placeholder="Abteilung" type="text" formControlName="vatId" tabindex="3" />
</shared-form-control>
</ng-container>
<shared-form-control label="Anrede">
<shared-select formControlName="gender" placeholder="Anrede" tabindex="4" [autofocus]="true">
<shared-select-option [value]="2">Herr</shared-select-option>
<shared-select-option [value]="4">Frau</shared-select-option>
</shared-select>
</shared-form-control>
<shared-form-control label="Titel">
<shared-select formControlName="title" placeholder="Titel" tabindex="5">
<shared-select-option value="Dipl.-Ing.">Dipl.-Ing.</shared-select-option>
<shared-select-option value="Dr.">Dr.</shared-select-option>
<shared-select-option value="Dr. med.">Dr. med.</shared-select-option>
<shared-select-option value="Prof.">Prof.</shared-select-option>
<shared-select-option value="Prof. Dr.">Prof. Dr.</shared-select-option>
<shared-select-option value="RA">RA</shared-select-option>
</shared-select>
</shared-form-control>
<shared-form-control label="Nachname">
<input class="input-control" placeholder="Nachname" type="text" formControlName="lastName" tabindex="6" />
</shared-form-control>
<shared-form-control label="Vorname">
<input class="input-control" placeholder="Vorname" type="text" formControlName="firstName" tabindex="7" />
</shared-form-control>
<ng-container *ngIf="!(isBusinessKonto$ | async)">
<shared-form-control label="Firma" class="col-span-2">
<input class="input-control" placeholder="Firma" type="text" formControlName="organisation" tabindex="8" />
</shared-form-control>
</ng-container>
<shared-form-control label="Straße">
<input class="input-control" placeholder="Straße" type="text" formControlName="street" tabindex="9" />
</shared-form-control>
<shared-form-control label="Hausnummer">
<input class="input-control" placeholder="Hausnummer" type="text" formControlName="streetNumber" tabindex="10" />
</shared-form-control>
<shared-form-control label="PLZ">
<input class="input-control" placeholder="PLZ" type="text" formControlName="zipCode" tabindex="11" />
</shared-form-control>
<shared-form-control label="Ort">
<input class="input-control" placeholder="Ort" type="text" formControlName="city" tabindex="12" />
</shared-form-control>
<shared-form-control label="Adresszusatz" class="col-span-2">
<input class="input-control" placeholder="Adresszusatz" type="text" formControlName="info" tabindex="13" />
</shared-form-control>
<shared-form-control class="col-span-2" label="Land">
<shared-select placeholder="Land" formControlName="country" tabindex="14">
<shared-select-option *ngFor="let country of countries$ | async" [value]="country.isO3166_A_3">
{{ country.name }}
</shared-select-option>
</shared-select>
</shared-form-control>
<div class="text-center col-span-2">
<shared-checkbox>Diese Lieferadresse als Standard Adresse festlegen</shared-checkbox>
</div>
<div class="mt-6 text-center col-span-2">
<button
[disabled]="formGroup.invalid || formGroup.disabled"
type="submit"
class="px-5 py-3 font-bold text-lg rounded-full bg-brand text-white"
tabindex="15"
>
Speichern
</button>
</div>
</form>

View File

@@ -0,0 +1,137 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { CheckboxComponent } from '@shared/components/checkbox';
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { SelectModule } from '@shared/components/select';
import { FormControlComponent } from '@shared/components/form-control';
import { CrmCustomerService } from '@domain/crm';
import { AddressDTO, Gender, ShippingAddressDTO } from '@swagger/crm';
import { map, takeUntil } from 'rxjs/operators';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { AddressSelectionModalService } from '@shared/modals/address-selection-modal';
import { CustomerSearchStore } from '../store';
import { CustomerSearchNavigation } from '../../navigations';
import { Subject, combineLatest } from 'rxjs';
import { IconComponent } from '@shared/components/icon';
import { RouterLink } from '@angular/router';
@Component({
selector: 'page-edit-shipping-address-main-view',
templateUrl: 'edit-shipping-address-main-view.component.html',
styleUrls: ['edit-shipping-address-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-edit-shipping-address-main-view' },
standalone: true,
imports: [
IconComponent,
RouterLink,
AsyncPipe,
NgIf,
NgForOf,
ReactiveFormsModule,
SelectModule,
FormControlComponent,
CheckboxComponent,
],
})
export class EditShippingAddressMainViewComponent implements OnInit, OnDestroy {
private _onDestroy = new Subject<void>();
detailsRoute$ = combineLatest([this._store.processId$, this._store.customerId$]).pipe(
map(([processId, customerId]) => this._navigation.detailsRoute({ processId, customerId }))
);
formGroup = new FormGroup({
gender: new FormControl<Gender>(0, [Validators.required, Validators.min(1)]),
title: new FormControl<string>(undefined),
firstName: new FormControl<string>(undefined, [Validators.required]),
lastName: new FormControl<string>(undefined, [Validators.required]),
organisation: new FormControl<string>(undefined),
department: new FormControl<string>(undefined),
vatId: new FormControl<string>(undefined),
street: new FormControl<string>(undefined, [Validators.required]),
streetNumber: new FormControl<string>(undefined, [Validators.required]),
zipCode: new FormControl<string>(undefined, [Validators.required]),
city: new FormControl<string>(undefined, [Validators.required]),
country: new FormControl<string>('DEU', [Validators.required]),
info: new FormControl<string>(undefined),
isDefault: new FormControl<boolean>(false),
});
countries$ = this._customerService.getCountries().pipe(map((res) => res.result));
isBusinessKonto$ = this._store.isBusinessKonto$;
constructor(
private _customerService: CrmCustomerService,
private _addressSelection: AddressSelectionModalService,
private _store: CustomerSearchStore,
private _navigation: CustomerSearchNavigation
) {}
ngOnInit() {
this._store.customer$.pipe(takeUntil(this._onDestroy)).subscribe(() => {
if (this._store.isBusinessKonto) {
this.formGroup.controls.organisation.setValidators([Validators.required]);
} else {
this.formGroup.controls.organisation.clearValidators();
}
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
async save() {
if (this.formGroup.invalid) {
this.formGroup.markAllAsTouched();
return;
}
try {
this.formGroup.disable();
const formData = this.formGroup.value;
const address: AddressDTO = {
street: formData.street,
streetNumber: formData.streetNumber,
zipCode: formData.zipCode,
city: formData.city,
country: formData.country,
info: formData.info,
};
const addressValidationResult = await this._addressSelection.validateAddress(address);
if (addressValidationResult === undefined) {
this.formGroup.enable();
return;
}
const addOrganization = this._store.isBusinessKonto || formData.organisation !== undefined;
const shippingAddress: ShippingAddressDTO = {
gender: formData.gender,
title: formData.title,
firstName: formData.firstName,
lastName: formData.lastName,
organisation: addOrganization
? {
name: formData.organisation,
department: formData.department,
vatId: formData.vatId,
}
: undefined,
address: addressValidationResult,
};
const result = await this._customerService.createShippingAddress(this._store.customerId, shippingAddress, formData.isDefault);
this._navigation.navigateToDetails({ processId: this._store.processId, customerId: this._store.customerId });
} catch (error) {
this.formGroup.enable();
}
}
}

View File

@@ -1,4 +1,4 @@
<ng-container *ngIf="fetching$ | async; else filterTemplate"></ng-container>
<ng-container *ngIf="fetchingFilterSettings$ | async; else filterTemplate"></ng-container>
<ng-template #filterTemplate>
<div>
@@ -13,7 +13,12 @@
</div>
</div>
<div class="px-3 bg-surface-2 text-surface-2-content">
<shared-filter [filter]="filter$ | async"></shared-filter>
<shared-filter
*ngIf="filter$ | async; let filter"
[filter]="filter"
[loading]="fetching$ | async"
(search)="search(filter)"
></shared-filter>
</div>
</div>
</ng-template>

View File

@@ -4,6 +4,7 @@ import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators';
import { CustomerSearchNavigation, NavigationRoute } from '../../navigations';
import { combineLatest } from 'rxjs';
import { Filter } from '@shared/components/filter';
@Component({
selector: 'page-customer-filter-main-view',
@@ -12,7 +13,9 @@ import { combineLatest } from 'rxjs';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerFilterMainViewComponent {
fetching$ = this._store.fetchingFilter$;
fetchingFilterSettings$ = this._store.fetchingFilter$;
fetching$ = this._store.fetchingCustomerList$;
filter$ = this._store.filter$;
@@ -33,4 +36,9 @@ export class CustomerFilterMainViewComponent {
private _activatedRoute: ActivatedRoute,
private _navigation: CustomerSearchNavigation
) {}
search(filter: Filter) {
this._store.setFilter(filter);
this._store.search();
}
}

View File

@@ -4,7 +4,13 @@
<div>
<div class="customer-history-header">
<div class="customer-history-header-actions flex flex-row justify-end pt-1 px-1">
<a *ngIf="detailsRoute$ | async; let route" [routerLink]="route.path" [queryParams]="route.queryParams" class="btn btn-label">
<a
*ngIf="detailsRoute$ | async; let route"
[routerLink]="route.path"
[queryParams]="route.queryParams"
[queryParamsHandling]="'merge'"
class="btn btn-label"
>
<ui-icon [icon]="'close'"></ui-icon>
</a>
</div>

View File

@@ -0,0 +1,3 @@
:host {
@apply grid grid-flow-row justify-center items-center gap-4 bg-surface text-surface-content rounded px-4 py-6;
}

View File

@@ -0,0 +1,18 @@
<h1 class="text-center text-2xl font-bold">Kundenkarte</h1>
<p class="text-center text-xl">
Alle Infos zu Ihrer Kundenkarte <br />
und allen Partnerkarten.
</p>
<page-customer-kundenkarte
*ngFor="let karte of primaryKundenkarte$ | async"
[cardDetails]="karte"
[isCustomerCard]="true"
></page-customer-kundenkarte>
<p class="text-center text-xl font-bold" *ngIf="(partnerKundenkarte$ | async)?.length">Partnerkarten</p>
<page-customer-kundenkarte
*ngFor="let karte of partnerKundenkarte$ | async"
[cardDetails]="karte"
[isCustomerCard]="false"
></page-customer-kundenkarte>

View File

@@ -0,0 +1,47 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { CustomerSearchStore } from '../store';
import { ActivatedRoute } from '@angular/router';
import { Subject } from 'rxjs';
import { map, share, switchMap } from 'rxjs/operators';
import { CrmCustomerService } from '@domain/crm';
import { KundenkarteComponent } from '../../components/kundenkarte';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
@Component({
selector: 'page-customer-kundenkarte-main-view',
templateUrl: 'kundenkarte-main-view.component.html',
styleUrls: ['kundenkarte-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-customer-kundenkarte-main-view' },
standalone: true,
imports: [KundenkarteComponent, NgFor, AsyncPipe, NgIf],
})
export class KundenkarteMainViewComponent implements OnInit, OnDestroy {
private _onDestroy$ = new Subject<void>();
customerId$ = this._activatedRoute.params.pipe(map((params) => params.customerId));
kundenkarte$ = this.customerId$.pipe(
switchMap((customerId) =>
this._customerService.getCustomerCard(customerId).pipe(map((response) => response.result?.filter((f) => f.isActive)))
),
share()
);
primaryKundenkarte$ = this.kundenkarte$.pipe(map((kundenkarte) => kundenkarte?.filter((k) => k.isPrimary)));
partnerKundenkarte$ = this.kundenkarte$.pipe(map((kundenkarte) => kundenkarte?.filter((k) => !k.isPrimary)));
constructor(private _store: CustomerSearchStore, private _activatedRoute: ActivatedRoute, private _customerService: CrmCustomerService) {}
ngOnInit() {
this.customerId$.subscribe((customerId) => {
this._store.selectCustomer(customerId);
});
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
}
}

View File

@@ -6,7 +6,13 @@
>
<div class="text-center pt-10 px-8 rounded-card side-view-shadow grow">
<h1 class="text-[1.625rem] font-bold">Kundensuche</h1>
<p class="text-lg mt-2">
Wir legen Ihnen gerne ein Onlinekonto an, dort können Sie Ihre Bestellungen einsehen.
<p class="text-lg mt-2 mb-6">
Haben Sie ein Konto bei uns?
</p>
<shared-filter-input-group-main
*ngIf="filter$ | async; let filter"
[inputGroup]="filter?.input | group: 'main'"
(search)="search(filter)"
[loading]="fetching$ | async"
></shared-filter-input-group-main>
</div>

View File

@@ -3,6 +3,10 @@ import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CustomerCreateNavigation } from '../../navigations/customer-create.navigation';
import { NumberInput } from '@angular/cdk/coercion';
import { Filter, FilterModule } from '@shared/components/filter';
import { CustomerSearchStore } from '../store';
import { map, tap } from 'rxjs/operators';
import { cloneDeep } from 'lodash';
@Component({
selector: 'page-customer-main-side-view',
@@ -10,7 +14,7 @@ import { NumberInput } from '@angular/cdk/coercion';
styleUrls: ['main-side-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, RouterModule],
imports: [CommonModule, RouterModule, FilterModule],
})
export class MainSideViewComponent {
@Input()
@@ -20,5 +24,14 @@ export class MainSideViewComponent {
return this._navigation.defaultRoute({ processId: this.processId });
}
constructor(private _navigation: CustomerCreateNavigation) {}
filter$ = this._store.filter$;
fetching$ = this._store.fetchingCustomerList$;
constructor(private _navigation: CustomerCreateNavigation, private _store: CustomerSearchStore) {}
search(filter: Filter) {
this._store.setFilter(filter);
this._store.search();
}
}

View File

@@ -0,0 +1,7 @@
:host {
@apply block bg-surface text-surface-content rounded-[0.313rem] mb-3;
}
::ng-deep page-customer-history-main-view shared-history-list .scroll-container {
@apply h-[calc(100vh-24rem)];
}

View File

@@ -0,0 +1,29 @@
<ng-container *ngIf="fetching$ | async; else historyTemplate"></ng-container>
<ng-template #historyTemplate>
<div>
<div class="customer-history-header">
<div class="customer-history-header-actions flex flex-row justify-end pt-1 px-1">
<a *ngIf="detailsRoute$ | async; let route" [routerLink]="route.path" [queryParams]="route.queryParams" class="btn btn-label">
<shared-icon [icon]="'close'" [size]="32"></shared-icon>
</a>
</div>
<div class="customer-history-header-body text-center -mt-3">
<h1 class="text-[1.625rem] font-bold">Historie</h1>
</div>
<div class="customer-history-header-info flex flex-row justify-evenly items-center my-5">
<div class="flex flex-row">
<div class="w-36">Kundenname</div>
<div class="grow font-bold">{{ customerName$ | async }}</div>
</div>
<div class="flex flex-row">
<div class="w-36">Kundennummer</div>
<div class="grow font-bold">{{ customerNumber$ | async }}</div>
</div>
</div>
</div>
<div class="px-3 bg-surface-2 text-surface-2-content">
<shared-history-list [history]="history$ | async"> </shared-history-list>
</div>
</div>
</ng-template>

View File

@@ -0,0 +1,111 @@
import { Component, ChangeDetectionStrategy, AfterViewInit, OnInit, OnDestroy } from '@angular/core';
import { CrmCustomerService } from '@domain/crm';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { HistoryDTO } from '@swagger/crm';
import { Observable, Subject, combineLatest } from 'rxjs';
import { map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { CustomerSearchStore } from '../store';
import { CustomerSearchNavigation } from '../../navigations';
import { AsyncPipe, NgIf } from '@angular/common';
import { IconModule } from '@shared/components/icon';
import { SharedHistoryListModule } from '@shared/components/history';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { DomainOmsService } from '@domain/oms';
export interface CustomerHistoryViewMainState {
history?: HistoryDTO[];
fetching?: boolean;
}
@Component({
selector: 'page-customer-order-details-history-main-view',
templateUrl: 'order-details-history-main-view.component.html',
styleUrls: ['order-details-history-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [AsyncPipe, RouterLink, NgIf, IconModule, SharedHistoryListModule],
})
export class CustomerOrderDetailsHistoryMainViewComponent extends ComponentStore<CustomerHistoryViewMainState>
implements OnInit, OnDestroy {
private _onDestroy$ = new Subject<void>();
fetching$ = this.select((s) => s.fetching);
history$ = this.select((s) => s.history);
processId$ = this._store.processId$;
customerId$ = this._store.customerId$;
customer$ = this._store.customer$;
customerName$ = this.customer$.pipe(map((customer) => `${customer?.lastName}, ${customer?.firstName}`));
customerNumber$ = this.customer$.pipe(map((customer) => customer?.customerNumber));
orderId$ = this._activvatedRoute.params.pipe(map((params) => Number(params.orderId)));
order$ = this._store.order$;
orderItemId$ = this._activvatedRoute.params.pipe(map((params) => Number(params.orderItemId)));
orderItemSubsetId$ = combineLatest([this.order$, this.orderItemId$]).pipe(
map(([order, orderItemId]) => order?.items?.find((item) => item.id === orderItemId)?.data?.subsetItems?.[0]?.id)
);
detailsRoute$ = combineLatest([this.processId$, this.customerId$, this.orderId$, this.orderItemId$]).pipe(
map(([processId, customerId, orderId, orderItemId]) =>
this._navigation.orderDetialsRoute({ processId, customerId, orderId, orderItemId })
)
);
constructor(
private _activvatedRoute: ActivatedRoute,
private _store: CustomerSearchStore,
private _customerService: CrmCustomerService,
private _omsService: DomainOmsService,
private _navigation: CustomerSearchNavigation
) {
super({});
}
ngOnInit(): void {
this.orderId$.pipe(takeUntil(this._onDestroy$)).subscribe((orderId) => {
this._store.selectOrder(orderId);
});
this.orderItemSubsetId$.pipe(takeUntil(this._onDestroy$)).subscribe((orderItemSubsetId) => {
if (orderItemSubsetId) {
this.fetchHistory(orderItemSubsetId);
}
});
}
ngOnDestroy(): void {
this._onDestroy$.next();
this._onDestroy$.complete();
}
fetchHistory = this.effect((orderItemSubsetId$: Observable<number>) =>
orderItemSubsetId$.pipe(
tap(() => this.patchState({ fetching: true })),
switchMap((orderItemSubsetId) =>
this._omsService
.getHistory(orderItemSubsetId)
.pipe(tapResponse(this.handleFetchHistoryResponse, this.handleFetchHistoryError, this.handleFetchHistoryComplete))
)
)
);
handleFetchHistoryResponse = (history: HistoryDTO[]) => {
this.patchState({ history });
};
handleFetchHistoryError = (err: any) => {
console.error(err);
};
handleFetchHistoryComplete = () => {
this.patchState({ fetching: false });
};
}

View File

@@ -0,0 +1,26 @@
:host {
@apply grid grid-flow-row gap-2;
}
.page-customer-order-details-main-view__card {
@apply grid grid-flow-row;
}
.page-customer-order-details-main-view__card-header {
@apply grid grid-flow-col items-center justify-between bg-surface-2 text-surface-2-content p-4 font-bold rounded-t;
}
.page-customer-order-details-main-view__card-content {
@apply bg-surface text-surface-content p-4 rounded-b;
}
.col-data {
@apply grid grid-cols-[10rem_1fr] gap-2;
}
.col-label {
}
.col-value {
@apply font-bold text-left;
}

View File

@@ -0,0 +1,193 @@
<div class="page-customer-order-details-main-view__card">
<div class="page-customer-order-details-main-view__card-header">
<div class="flex flex-row">
<shared-icon class="mr-3" [icon]="accountTypeKey$ | async"></shared-icon>
<span>{{ accountTypeDescription$ | async }}</span>
</div>
<div class="text-right">
<a
*ngIf="ordersRoute$ | async; let ordersRoute"
[routerLink]="ordersRoute.path"
[queryParams]="ordersRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
>
<shared-icon icon="close" class="text-2xl"></shared-icon>
</a>
</div>
</div>
<div class="page-customer-order-details-main-view__card-content" *ngIf="order$ | async; let order">
<div class="grid grid-cols-2 items-end gap-2">
<div class="col-data order-date">
<div class="col-label">
Bestelldatum
</div>
<div class="col-value">{{ order?.orderDate | date: 'dd.MM.yyyy | HH:mm' }} Uhr</div>
</div>
<div class="font-bold text-right text-2xl">
{{ order?.orderNumber }}
</div>
<div class="col-data">
<div class="col-label">
Bestellkanal
</div>
<div class="col-value">
{{ order?.features?.orderSource }}
</div>
</div>
<div class="font-bold text-right">
{{ order?.processingStatus | orderProcessingStatus }}
</div>
<div class="col-data">
<div class="col-label">
Zielfiliale
</div>
<div class="col-value">
{{ orderTargetBranch$ | async | resolveBranch | branchName: 'name' }}
</div>
</div>
<div class="text-right">
<span class="font-bold">{{ order?.orderValue | currency: order?.orderValueCurrency:'code' }}</span> |
<span>{{ order?.items?.length }} Artikel</span>
</div>
</div>
</div>
</div>
<div class="page-customer-order-details-main-view__card" *ngIf="orderItem$ | async; let orderItem">
<div class="page-customer-order-details-main-view__card-header">
<div class="grid grid-flow-col gap-3 justify-start">
<shared-icon [icon]="orderItemOrderType$ | async"></shared-icon>
<span>{{ orderItemOrderType$ | async }}</span>
<span class="font-normal">{{ orderTargetBranch$ | async | resolveBranch | branchName: 'name' }}</span>
</div>
</div>
<div class="page-customer-order-details-main-view__card-content grid grid-cols-[6rem_1fr] gap-4">
<div>
<img class="rounded shadow mx-auto w-[5.9rem]" [src]="orderItem?.product?.ean | productImage" [alt]="orderItem?.product?.name" />
</div>
<div class="grid grid-flow-row gap-2">
<div class="grid grid-flow-col justify-between items-end">
<span>{{ orderItem.product?.contributors }}</span>
<a
*ngIf="orderDetailsHistoryRoute$ | async; let orderDetailsHistoryRoute"
[routerLink]="orderDetailsHistoryRoute.path"
[queryParams]="orderDetailsHistoryRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
class="text-brand font-bold text-xl"
>Historie</a
>
</div>
<div class="font-bold text-lg">
{{ orderItem?.product?.name }}
</div>
<div>
<span class="isa-label">
ans Lager
</span>
</div>
<div class="grid grid-flow-row gap-2" *ngIf="orderItemSubsetItem$ | async; let orderItemSubsetItem">
<div class="col-data">
<div class="col-label">
Menge
</div>
<div class="col-value">{{ orderItem?.quantity?.quantity }}x</div>
</div>
<div class="col-data">
<div class="col-label">
Format
</div>
<div class="col-value grid-flow-col grid gap-3 items-center justify-start">
<shared-icon [icon]="orderItem?.product?.format"></shared-icon>
<span>{{ orderItem?.product?.formatDetail }}</span>
</div>
</div>
<div class="col-data">
<div class="col-label">
ISBN/EAN
</div>
<div class="col-value">{{ orderItem?.product?.ean }}</div>
</div>
<div class="col-data">
<div class="col-label">
Preis
</div>
<div class="col-value">{{ orderItem?.unitPrice?.value?.value | currency: orderItem?.unitPrice?.value?.currency:'code' }}</div>
</div>
<div class="col-data">
<div class="col-label">
MwSt
</div>
<div class="col-value">{{ orderItem?.unitPrice?.vat?.inPercent }}%</div>
</div>
<hr />
<div class="col-data">
<div class="col-label">
Lieferant
</div>
<div class="col-value">
{{ orderItemSubsetItem?.supplier?.data?.name }}
</div>
</div>
<div class="col-data">
<div class="col-label">
Meldenummer
</div>
<div class="col-value">{{ orderItemSubsetItem?.ssc }} - {{ orderItemSubsetItem?.sscText }}</div>
</div>
<div class="col-data">
<div class="col-label">
Vsl. Lieferdatum
</div>
<div class="col-value">
{{ orderItemSubsetItem?.estimatedShippingDate | date: 'dd.MM.yyyy' }}
</div>
</div>
<div class="col-data">
<div class="col-label">
Zurücklegen bis
</div>
<div class="col-value">
{{ orderItemSubsetItem?.preferredPickUpDate | date: 'dd.MM.yyyy' }}
</div>
</div>
<hr />
<div class="col-data" *ngIf="orderItemSubsetItem?.compartmentCode">
<div class="col-label">
Abholfachnummer
</div>
<div class="col-value">
<span>{{ orderItemSubsetItem?.compartmentCode }}</span>
<span *ngIf="orderItemSubsetItem?.compartmentInfo">_{{ orderItemSubsetItem?.compartmentInfo }}</span>
</div>
</div>
<div class="col-data">
<div class="col-label">
Vormerker
</div>
<div class="col-value">-</div>
</div>
<hr />
<div class="col-data">
<div class="col-label">
Zahlungsweg
</div>
<div class="col-value">-</div>
</div>
<div class="col-data">
<div class="col-label">
Zahlungsart
</div>
<div class="col-value">-</div>
</div>
<div class="col-data">
<div class="col-label">
Anmerkung
</div>
<div class="col-value">
{{ orderItemSubsetItem?.specialComment }}
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,99 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { CustomerSearchStore } from '../store';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { debounceTime, map, takeUntil, tap } from 'rxjs/operators';
import { Subject, combineLatest } from 'rxjs';
import { AsyncPipe, CurrencyPipe, DatePipe, NgIf } from '@angular/common';
import { OrderProcessingStatusPipe } from '@shared/pipes/order';
import { BranchNamePipe, ResolveBranchPipe } from '@shared/pipes/branch';
import { IconComponent } from '@shared/components/icon';
import { ProductImagePipe } from '@cdn/product-image';
import { CustomerSearchNavigation } from '../../navigations';
@Component({
selector: 'page-customer-order-details-main-view',
templateUrl: 'order-details-main-view.component.html',
styleUrls: ['order-details-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-customer-order-details-main-view' },
standalone: true,
imports: [
NgIf,
AsyncPipe,
DatePipe,
OrderProcessingStatusPipe,
ProductImagePipe,
ResolveBranchPipe,
BranchNamePipe,
CurrencyPipe,
IconComponent,
RouterLink,
],
})
export class OrderDetailsMainViewComponent implements OnInit, OnDestroy {
orderId$ = this._activateRoute.params.pipe(map((params) => Number(params.orderId)));
order$ = this._store.order$;
orderTargetBranch$ = this.order$.pipe(map((order) => order?.targetBranch?.id));
customerId$ = this._activateRoute.params.pipe(map((params) => Number(params.customerId)));
customer$ = this._store.customer$;
accountType$ = this.customer$.pipe(map((customer) => customer?.features?.find((feature) => feature.group === 'd-customertype')));
accountTypeKey$ = this.accountType$.pipe(map((accountType) => accountType?.key));
accountTypeDescription$ = this.accountType$.pipe(map((accountType) => accountType?.description));
orderItemId$ = this._activateRoute.params.pipe(map((params) => Number(params.orderItemId)));
orderItem$ = combineLatest([this.order$, this.orderItemId$, this.customerId$]).pipe(
tap(([order, orderItemId, customerId]) => {
if (order?.items && customerId && !orderItemId) {
this._navigation.navigateToOrderDetails({
processId: this._store.processId,
customerId: customerId,
orderId: order?.id,
orderItemId: order?.items?.[0]?.id,
});
}
}),
map(([order, orderItemId]) => order?.items?.find((orderItem) => orderItem?.id === orderItemId)),
map((orderItem) => orderItem?.data)
);
orderItemOrderType$ = this.orderItem$.pipe(map((orderItem) => orderItem?.features?.orderType));
orderItemSubsetItem$ = this.orderItem$.pipe(map((orderItem) => orderItem?.subsetItems?.[0]?.data));
private _onDestroy = new Subject<void>();
ordersRoute$ = combineLatest([this.customerId$, this._store.processId$]).pipe(
map(([customerId, processId]) => this._navigation.ordersRoute({ processId, customerId }))
);
orderDetailsHistoryRoute$ = combineLatest([this.customerId$, this._store.processId$, this.orderId$, this.orderItemId$]).pipe(
map(([customerId, processId, orderId, orderItemId]) =>
this._navigation.orderDetailsHistoryRoute({ processId, customerId, orderId, orderItemId })
)
);
constructor(private _activateRoute: ActivatedRoute, private _store: CustomerSearchStore, private _navigation: CustomerSearchNavigation) {}
ngOnInit(): void {
this.orderId$.pipe(takeUntil(this._onDestroy)).subscribe((orderId) => {
this._store.selectOrder(orderId);
});
this.customerId$.pipe(takeUntil(this._onDestroy)).subscribe((customerId) => {
this._store.selectCustomer(customerId);
});
}
ngOnDestroy(): void {
this._onDestroy.next();
this._onDestroy.complete();
}
}

View File

@@ -0,0 +1,3 @@
a.order-item-active {
@apply border-[#0556B4] bg-[#D8DFE5];
}

View File

@@ -0,0 +1,27 @@
<div class="bg-surface text-surface-content rounded-t p-4 font-bold text-xl">
{{ customer$ | async | customerName }}
</div>
<div class="bg-surface-2 text-surface-2-content p-4 text-right">{{ (orderItems$ | async)?.length }} Artikel</div>
<div class="grid grid-flow-row gap-1">
<ng-container *ngFor="let orderItem of orderItems$ | async">
<a
*ngIf="getOrderDetailsRoute(orderItem.id); let route"
[routerLink]="route.path"
[queryParams]="route.urlTree.queryParams"
[queryParamsHandling]="'merge'"
routerLinkActive="order-item-active"
class="grid grid-cols-[auto_1fr] gap-4 p-4 border border-solid bg-surface text-surface-content rounded first:rounded-t-none"
>
<div>
<img [src]="orderItem?.product?.ean | productImage" [alt]="orderItem?.product?.name" class="rounded shadow mx-auto w-[3.75rem]" />
</div>
<div class="grid grid-flow-row gap-1">
<span>{{ orderItem?.product?.contributors }}</span>
<span class="font-bold truncate whitespace-nowrap">{{ orderItem?.product?.name }}</span>
<span>
<span class="isa-label">ans Lager</span>
</span>
</div>
</a>
</ng-container>
</div>

View File

@@ -0,0 +1,34 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CustomerSearchStore } from '../store';
import { map } from 'rxjs/operators';
import { CustomerNamePipe } from '@shared/pipes/customer';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { ProductImagePipe } from '@cdn/product-image';
import { CustomerSearchNavigation } from '../../navigations';
import { RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'page-customer-order-details-side-view',
templateUrl: 'order-details-side-view.component.html',
styleUrls: ['order-details-side-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-customer-order-details-side-view' },
standalone: true,
imports: [CustomerNamePipe, AsyncPipe, NgFor, ProductImagePipe, RouterLink, NgIf, RouterLinkActive],
})
export class OrderDetailsSideViewComponent {
customer$ = this._store.customer$;
orderItems$ = this._store.order$.pipe(map((order) => order?.items?.map((item) => item.data)));
constructor(private _store: CustomerSearchStore, private _navigation: CustomerSearchNavigation) {}
getOrderDetailsRoute(orderItemId: number) {
return this._navigation.orderDetialsRoute({
processId: this._store.processId,
customerId: this._store.customer.id,
orderId: this._store.order.id,
orderItemId,
});
}
}

View File

@@ -0,0 +1,8 @@
:host {
@apply block bg-surface text-surface-content rounded px-6 py-8;
}
.order-item {
@apply grid;
grid-template-columns: 4rem 7.5rem 1fr auto auto;
}

View File

@@ -0,0 +1,42 @@
<div class="flex flex-row justify-end -mt-4 -mr-2">
<a
*ngIf="detailsRoute$ | async; let detailsRoute"
[routerLink]="detailsRoute.path"
[queryParams]="detailsRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
>
<shared-icon icon="close" [size]="32"></shared-icon>
</a>
</div>
<h1 class="text-2xl text-center font-bold mb-4">Bestellungen</h1>
<p class="text-xl text-center">
Übersicht aller offenen und <br />
früheren Bestellungen
</p>
<div class="grid grid-flow-row -mx-6 -mb-8 mt-8 overflow-scroll max-h-[calc(100vh-390px)] scroll-bar">
<div *ngFor="let order of orders$ | async" class="order-item gap-6 items-center border-t-2 border-solid border-background px-6 py-3">
<div class="font-bold">
{{ order.orderDate | date: 'dd.MM.yy' }}
</div>
<div class="font-bold text-center">
{{ order.processingStatus | orderProcessingStatus }}
</div>
<div>
{{ order.features?.orderSource }}
<span *ngIf="order.features?.orderSource && (order | orderDestination)">|</span>
{{ order | orderDestination }}
</div>
<div>{{ order.orderValue | currency: order.currency:'code' }} | {{ order.itemsCount }} Artikel</div>
<div class="justify-self-end">
<a
class="grid justify-center items-center font-bold text-brand h-12"
*ngIf="getOrderDetailsRoute(order.id); let orderDetailsRoute"
[routerLink]="orderDetailsRoute.path"
[queryParams]="orderDetailsRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
>
Details
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,56 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { CustomerSearchStore } from '../store';
import { Subject, combineLatest } from 'rxjs';
import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
import { isEqual } from 'lodash';
import { AsyncPipe, CurrencyPipe, DatePipe, JsonPipe, NgFor, NgIf } from '@angular/common';
import { OrderDestinationPipe, OrderProcessingStatusPipe } from '@shared/pipes/order';
import { CustomerSearchNavigation } from '../../navigations';
import { RouterLink } from '@angular/router';
import { IconComponent } from '@shared/components/icon';
@Component({
selector: 'page-customer-orders-main-view',
templateUrl: 'orders-main-view.component.html',
styleUrls: ['orders-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-customer-orders-main-view' },
standalone: true,
imports: [AsyncPipe, NgFor, NgIf, DatePipe, OrderProcessingStatusPipe, OrderDestinationPipe, CurrencyPipe, RouterLink, IconComponent],
})
export class CustomerOrdersMainViewComponent implements OnInit, OnDestroy {
private _onDestroy = new Subject<void>();
orders$ = this._store.customerOrders$;
fetching$ = this._store.fetchingCustomerOrders$;
detailsRoute$ = combineLatest([this._store.processId$, this._store.customerId$]).pipe(
map(([processId, customerId]) => this._navigation.detailsRoute({ processId, customerId }))
);
constructor(private _store: CustomerSearchStore, private _navigation: CustomerSearchNavigation) {}
ngOnInit(): void {
this._store.customer$
.pipe(
takeUntil(this._onDestroy),
map((c) => c?.customerNumber),
distinctUntilChanged()
)
.subscribe((customerNumber) => {
if (customerNumber) {
this._store.fetchOrders();
}
});
}
ngOnDestroy(): void {
this._onDestroy.next();
this._onDestroy.complete();
}
getOrderDetailsRoute(orderId: number) {
return this._navigation.orderDetialsRoute({ customerId: this._store.customerId, processId: this._store.processId, orderId });
}
}

View File

@@ -0,0 +1,9 @@
import { NgModule } from '@angular/core';
import { CustomerOrdersMainViewComponent } from './orders-main-view.component';
@NgModule({
imports: [CustomerOrdersMainViewComponent],
exports: [CustomerOrdersMainViewComponent],
})
export class CustomerOrdersMainViewModule {}

View File

@@ -1,6 +1,12 @@
<div class="customer-result-list-header bg-surface-2 text-surface-2-content flex flex-row justify-between pb-4">
<div class="grid grid-flow-col gap-3 items-center justify-start grow-0">
<shared-searchbox class="w-[20.892rem]"> </shared-searchbox>
<shared-filter-input-group-main
class="w-[20.892rem]"
*ngIf="filter$ | async; let filter"
[inputGroup]="filter?.input | group: 'main'"
(search)="search(filter)"
[loading]="fetching$ | async"
></shared-filter-input-group-main>
<a
class="btn btn-light w-[5.813rem] h-14"
*ngIf="filterRoute$ | async; let route"
@@ -11,8 +17,7 @@
</a>
</div>
<span class="mr-5 self-end text-sm">
xxx Treffer
</span>
<span class="mr-5 self-end text-sm"> {{ hits$ | async }} Treffer </span>
</div>
<page-customer-result-list [processId]="processId$ | async"> </page-customer-result-list>
<page-customer-result-list [processId]="processId$ | async" [customers]="customers$ | async" (endReached)="paginate()">
</page-customer-result-list>

View File

@@ -4,6 +4,8 @@ import { CustomerSearchStore } from '../store/customer-search.store';
import { BehaviorSubject, Subscription, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { CustomerSearchNavigation, NavigationRoute } from '../../navigations';
import { cloneDeep } from 'lodash';
import { Filter } from '@shared/components/filter';
@Component({
selector: 'page-customer-results-main-view',
@@ -39,6 +41,14 @@ export class CustomerResultsMainViewComponent implements OnInit, OnDestroy {
routerEventsSubscription: Subscription;
filter$ = this._store.filter$.pipe(map((filter) => cloneDeep(filter)));
fetching$ = this._store.fetchingCustomerList$;
hits$ = this._store.customerListCount$;
customers$ = this._store.customerList$;
constructor(private _store: CustomerSearchStore, private _router: Router, private _navigation: CustomerSearchNavigation) {}
ngOnInit(): void {
@@ -52,4 +62,14 @@ export class CustomerResultsMainViewComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.routerEventsSubscription?.unsubscribe();
}
search(filter: Filter) {
this._store.setFilter(filter);
this._store.search();
}
paginate() {
console.log('paginate');
this._store.paginate();
}
}

View File

@@ -3,11 +3,11 @@ import { CommonModule } from '@angular/common';
import { CustomerResultsMainViewComponent } from './results-main-view.component';
import { CustomerResultListModule } from '../../components/customer-result-list/customer-result-list.module';
import { SearchboxModule } from '@shared/components/searchbox';
import { FilterModule } from '@shared/components/filter';
import { RouterModule } from '@angular/router';
@NgModule({
imports: [CommonModule, CustomerResultListModule, SearchboxModule, RouterModule],
imports: [CommonModule, CustomerResultListModule, FilterModule, RouterModule],
exports: [CustomerResultsMainViewComponent],
declarations: [CustomerResultsMainViewComponent],
})

View File

@@ -1,6 +1,12 @@
<div class="customer-result-list-header bg-surface-2 text-surface-2-content flex flex-col justify-between pb-4">
<div class="grid grid-flow-col gap-3 items-center justify-start grow-0">
<shared-searchbox class="w-[20.892rem]"> </shared-searchbox>
<shared-filter-input-group-main
class="w-[20.892rem]"
*ngIf="filter$ | async; let filter"
[inputGroup]="filter?.input | group: 'main'"
(search)="search(filter)"
[loading]="fetching$ | async"
></shared-filter-input-group-main>
<a
class="btn btn-light w-[5.813rem] h-14"
*ngIf="filterRoute$ | async; let route"
@@ -11,8 +17,7 @@
</a>
</div>
<span class="mr-5 self-end text-sm mt-4">
xxx Treffer
</span>
<span class="mr-5 self-end text-sm mt-4"> {{ hits$ | async }} Treffer </span>
</div>
<page-customer-result-list [processId]="processId$ | async" compact="true"> </page-customer-result-list>
<page-customer-result-list [processId]="processId$ | async" compact="true" [customers]="customers$ | async" (endReached)="paginate()">
</page-customer-result-list>

View File

@@ -4,6 +4,7 @@ import { CustomerSearchStore } from '../store/customer-search.store';
import { Subscription, BehaviorSubject, combineLatest } from 'rxjs';
import { CustomerSearchNavigation, NavigationRoute } from '../../navigations';
import { map } from 'rxjs/operators';
import { Filter } from '@shared/components/filter';
@Component({
selector: 'page-customer-results-side-view',
@@ -39,6 +40,14 @@ export class CustomerResultsSideViewComponent implements OnInit, OnDestroy {
routerEventsSubscription: Subscription;
filter$ = this._store.filter$;
fetching$ = this._store.fetchingCustomerList$;
hits$ = this._store.customerListCount$;
customers$ = this._store.customerList$;
constructor(private _store: CustomerSearchStore, private _router: Router, private _navigation: CustomerSearchNavigation) {}
ngOnInit(): void {
@@ -52,4 +61,13 @@ export class CustomerResultsSideViewComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.routerEventsSubscription?.unsubscribe();
}
search(filter: Filter) {
this._store.setFilter(filter);
this._store.search();
}
paginate() {
this._store.paginate();
}
}

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