Es existiert bereits ein Onlinekonto

This commit is contained in:
Lorenz Hilpert
2022-10-04 17:48:15 +02:00
parent cd25d6da38
commit 650026b0c0
17 changed files with 301 additions and 66 deletions

View File

@@ -4,7 +4,7 @@ import { AbstractControl, AsyncValidatorFn, FormControl, FormGroup } from '@angu
import { ActivatedRoute, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { CrmCustomerService } from '@domain/crm';
import { AddressDTO, CustomerDTO, ShippingAddressDTO } from '@swagger/crm';
import { AddressDTO, CustomerDTO, PayerDTO, ShippingAddressDTO } from '@swagger/crm';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { UiValidators } from '@ui/validators';
import { isNull } from 'lodash';
@@ -138,6 +138,7 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
patchFormData(key: keyof CustomerCreateFormData, value: any) {
this._formData.next({ ...this.formData, [key]: value });
this.cdr.markForCheck();
}
emailExistsValidator: AsyncValidatorFn = (control) => {
@@ -240,12 +241,35 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
customer.dateOfBirth = data.birthDate;
}
if (data.billingAddress?.deviatingAddress) {
const billingAddress = this.mapToBillingAddress(data.billingAddress);
if (this.validateShippingAddress) {
try {
billingAddress.address = await this.validateAddressData(billingAddress.address);
} catch (error) {
this.form.enable();
setTimeout(() => {
this.addressFormBlock.setAddressValidationError(error.error.invalidProperties);
}, 10);
return;
}
}
customer.payers = [
{
payer: billingAddress,
isDefault: new Date().toJSON(),
},
];
}
if (data.deviatingDeliveryAddress?.deviatingAddress) {
const shippingAddress = this.mapToShippingAddress(data.deviatingDeliveryAddress);
if (this.validateShippingAddress) {
try {
debugger;
shippingAddress.address = await this.validateAddressData(shippingAddress.address);
} catch (error) {
this.form.enable();
@@ -284,6 +308,22 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
};
}
mapToBillingAddress({ name, address, email, organisation, phoneNumbers }: DeviatingAddressFormBlockData): PayerDTO {
return {
gender: name?.gender,
title: name?.title,
firstName: name?.firstName,
lastName: name?.lastName,
address,
communicationDetails: {
email: email ? email : null,
mobile: phoneNumbers?.mobile ? phoneNumbers.mobile : null,
phone: phoneNumbers?.phone ? phoneNumbers.phone : null,
},
organisation,
};
}
ngOnDestroy(): void {
this.onDestroy$.next();
this.onDestroy$.complete();

View File

@@ -173,7 +173,7 @@ export class CreateP4MCustomerComponent extends AbstractCreateCustomer implement
}
}
mapCustomerInfoDtoToCustomerDto(customerInfoDto: CustomerInfoDTO): CustomerDTO {
static MapCustomerInfoDtoToCustomerDto(customerInfoDto: CustomerInfoDTO): CustomerDTO {
return {
address: customerInfoDto.address,
agentComment: customerInfoDto.agentComment,
@@ -210,7 +210,7 @@ export class CreateP4MCustomerComponent extends AbstractCreateCustomer implement
if (customerDto) {
customer = { ...customerDto, ...customer };
} else if (customerInfoDto) {
customer = { ...this.mapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
}
const p4mFeature = customer.features?.find((attr) => attr.key === 'p4mUser');

View File

@@ -1,5 +1,9 @@
<form *ngIf="formData$ | async; let data" (keydown.enter)="$event.preventDefault()">
<h1 class="title flex flex-row items-center justify-center">Kundenkartendaten erfasen</h1>
<p class="description">Bitte erfassen Sie die Kundenkarte</p>
<app-customer-type-selector [processId]="processId$ | async" [p4mUser]="true" customerType="webshop" [readonly]="true">
</app-customer-type-selector>
<app-p4m-number-form-block
#p4mBlock
@@ -21,35 +25,55 @@
>
</app-newsletter-form-block>
<app-deviating-address-form-block
#dbaBlock
[defaults]="{ address: { country: 'DEU' } }"
[tabIndexStart]="newsletterBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('billingAddress', $event)"
[data]="data.billingAddress"
(dataChanges)="patchFormData('billingAddress', $event)"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="shippingAddressRequiredMarks"
[addressValidatorFns]="shippingAddressValidators"
>
Abweichende Rechnugsaddresse anlegen
</app-deviating-address-form-block>
<app-name-form-block [data]="data.name" readonly> </app-name-form-block>
<app-deviating-address-form-block
#ddaBlock
[defaults]="{ address: { country: 'DEU' } }"
[tabIndexStart]="dbaBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="shippingAddressRequiredMarks"
[addressValidatorFns]="shippingAddressValidators"
>
Abweichende Lieferadresse anlegen
</app-deviating-address-form-block>
<app-email-form-block [data]="data.email" readonly> </app-email-form-block>
<app-organisation-form-block appearence="compact" [data]="data.organisation" readonly></app-organisation-form-block>
<app-address-form-block [data]="data.address" readonly> </app-address-form-block>
<div class="mt-8">
<h4 class="-mb-6">Rechnugsadresse</h4>
<ui-form-control class="-mb-5" [showHint]="false">
<input type="text" [value]="(billingAddress | address:true) ?? 'Keine Adresse vorhanden'" [uiInput] [readonly]="true" />
</ui-form-control>
<app-deviating-address-form-block
#dbaBlock
[defaults]="{ address: { country: 'DEU' } }"
[tabIndexStart]="newsletterBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('billingAddress', $event)"
[data]="data.billingAddress"
(dataChanges)="patchFormData('billingAddress', $event)"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidatorFns"
>
Abweichende Rechnugsaddresse anlegen
</app-deviating-address-form-block>
</div>
<div class="mt-8">
<h4 class="-mb-6">Lieferadresse</h4>
<ui-form-control class="-mb-5" [showHint]="false">
<input type="text" [value]="(shippingAddress | address:true) ?? 'Keine Adresse vorhanden'" [uiInput] [readonly]="true" />
</ui-form-control>
<app-deviating-address-form-block
#ddaBlock
[defaults]="{ address: { country: 'DEU' } }"
[tabIndexStart]="dbaBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidatorFns"
>
Abweichende Lieferadresse anlegen
</app-deviating-address-form-block>
</div>
<app-birth-date-form-block
#bdBlock

View File

@@ -1,6 +1,10 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CustomerDTO } from '@swagger/crm';
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { ValidatorFn, Validators } from '@angular/forms';
import { Result } from '@domain/defs';
import { CustomerDTO, CustomerInfoDTO, KeyValueDTOOfStringAndString, PayerDTO } from '@swagger/crm';
import { NameFormBlockData } from '../../form-blocks/name/name-form-block-data';
import { AbstractCreateCustomer } from '../abstract-create-customer';
import { CreateP4MCustomerComponent } from '../create-p4m-customer';
@Component({
selector: 'page-update-p4m-webshop-customer',
@@ -8,12 +12,136 @@ import { AbstractCreateCustomer } from '../abstract-create-customer';
styleUrls: ['../create-customer.scss', 'update-p4m-webshop-customer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UpdateP4MWebshopCustomerComponent extends AbstractCreateCustomer {
export class UpdateP4MWebshopCustomerComponent extends AbstractCreateCustomer implements OnInit {
validateAddress = true;
validateShippingAddress = true;
saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
throw new Error('Method not implemented.');
agbValidatorFns = [Validators.requiredTrue];
birthDateValidatorFns = [Validators.required];
nameRequiredMarks = ['gender', 'firstName', 'lastName'];
nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
firstName: [Validators.required],
lastName: [Validators.required],
gender: [Validators.required],
title: [],
};
addressRequiredMarks = ['street', 'streetNumber', 'zipCode', 'city', 'country'];
addressValidatorFns: Record<string, ValidatorFn[]> = {
street: [Validators.required],
streetNumber: [Validators.required],
zipCode: [Validators.required],
city: [Validators.required],
country: [Validators.required],
};
get billingAddress(): PayerDTO | undefined {
const payers = this.formData?._meta?.customerDto?.payers;
if (!payers || payers.length === 0) {
return undefined;
}
// the default payer is the payer with the latest isDefault(Date) value
const defaultPayer = payers.reduce((prev, curr) => (new Date(prev.isDefault) > new Date(curr.isDefault) ? prev : curr));
return defaultPayer.payer.data;
}
get shippingAddress() {
const shippingAddresses = this.formData?._meta?.customerDto?.shippingAddresses;
if (!shippingAddresses || shippingAddresses.length === 0) {
return undefined;
}
// the default shipping address is the shipping address with the latest isDefault(Date) value
const defaultShippingAddress = shippingAddresses.reduce((prev, curr) =>
new Date(prev.data.isDefault) > new Date(curr.data.isDefault) ? prev : curr
);
return defaultShippingAddress.data;
}
ngOnInit() {
super.ngOnInit();
}
getInterests(): KeyValueDTOOfStringAndString[] {
const interests: KeyValueDTOOfStringAndString[] = [];
for (const key in this.formData.interests) {
if (this.formData.interests[key]) {
interests.push({ key, group: 'KUBI_INTERESSEN' });
}
}
return interests;
}
getNewsletter(): KeyValueDTOOfStringAndString | undefined {
if (this.formData.newsletter) {
return { key: 'kubi_newsletter', group: 'KUBI_NEWSLETTER' };
}
}
async saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
console.log('saveCustomer', customer);
let res: Result<CustomerDTO>;
const { customerDto, customerInfoDto } = this.formData?._meta ?? {};
if (customerDto) {
customer = { ...customerDto, shippingAddresses: [], payers: [], ...customer };
if (customerDto.shippingAddresses?.length) {
customer.shippingAddresses.unshift(...customerDto.shippingAddresses);
}
if (customerDto.payers?.length) {
customer.payers.unshift(...customerDto.payers);
}
} else if (customerInfoDto) {
customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
}
const p4mFeature = customer.features?.find((attr) => attr.key === 'p4mUser');
if (p4mFeature) {
p4mFeature.value = this.formData.p4m;
} else {
customer.features.push({
key: 'p4mUser',
value: this.formData.p4m,
});
}
const interests = this.getInterests();
if (interests.length > 0) {
customer.features?.push(...interests);
// TODO: Klärung wie Interessen zukünftig gespeichert werden
// await this._loyaltyCardService
// .LoyaltyCardSaveInteressen({
// customerId: res.result.id,
// interessen: this.getInterests(),
// })
// .toPromise();
}
const newsletter = this.getNewsletter();
if (newsletter) {
customer.features.push(newsletter);
} else {
customer.features = customer.features.filter((feature) => feature.key !== 'kubi_newsletter' && feature.group !== 'KUBI_NEWSLETTER');
}
res = await this.customerService.createOnlineCustomer(customer).toPromise();
return res.result;
}
}

View File

@@ -16,10 +16,15 @@ import {
EmailFormBlockModule,
PhoneNumbersFormBlockModule,
} from '../../form-blocks';
import { UiFormControlModule } from '@ui/form-control';
import { UiInputModule } from '@ui/input';
import { CustomerPipesModule } from '../../pipes';
import { CustomerTypeSelectorModule } from '../../shared/customer-type-selector';
@NgModule({
imports: [
CommonModule,
CustomerTypeSelectorModule,
AddressFormBlockModule,
BirthDateFormBlockModule,
InterestsFormBlockModule,
@@ -31,6 +36,9 @@ import {
AcceptAGBFormBlockModule,
EmailFormBlockModule,
PhoneNumbersFormBlockModule,
UiFormControlModule,
UiInputModule,
CustomerPipesModule,
],
exports: [UpdateP4MWebshopCustomerComponent],
declarations: [UpdateP4MWebshopCustomerComponent],

View File

@@ -15,6 +15,7 @@ import { PhoneNumbersFormBlockComponent, PhoneNumbersFormBlockData } from '../ph
templateUrl: 'deviating-address-form-block.component.html',
styleUrls: ['deviating-address-form-block.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
exportAs: 'appDeviatingAddressFormBlock',
})
export class DeviatingAddressFormBlockComponent extends FormBlockGroup<DeviatingAddressFormBlockData> implements AfterViewInit {
@Input()
@@ -36,13 +37,7 @@ export class DeviatingAddressFormBlockComponent extends FormBlockGroup<Deviating
private readonly _phoneNumbersFormBlock: AddressFormBlockComponent;
get tabIndexEnd() {
let tabIndex = this.tabIndexStart;
if (this.control.value.deviatingDeliveryAddress) {
return (
this._phoneNumbersFormBlock?.tabIndexEnd ?? this._emailFormBlock?.tabIndexEnd ?? this._addressFormBlock?.tabIndexEnd ?? tabIndex
);
}
return tabIndex;
return this.tabIndexStart + 10;
}
@Input() defaults: Partial<DeviatingAddressFormBlockData>;

View File

@@ -14,9 +14,6 @@ export class OrganisationFormBlockComponent extends FormBlockGroup<OrganisationF
appearence: 'default' | 'compact' = 'default';
get tabIndexEnd() {
if (this.appearence === 'compact') {
return this.tabIndexStart;
}
return this.tabIndexStart + 2;
}

View File

@@ -18,11 +18,13 @@ export interface AddressData {
export class AddressPipe implements PipeTransform {
private result: string;
private data: AddressData;
private useComma = false;
constructor(private cdr: ChangeDetectorRef) {}
transform(data: AddressData): string {
transform(data: AddressData, useComma?: boolean): string {
this.data = data;
this.useComma = useComma ?? this.useComma;
this.getResult();
return this.result;
@@ -33,13 +35,13 @@ export class AddressPipe implements PipeTransform {
let parts = [
this.data?.organisation?.name || '',
this.data?.organisation?.department || '',
[this.data?.lastName, this.data?.firstName].filter((f) => !!f).join(', '),
[this.data?.lastName, this.data?.firstName].filter((f) => !!f).join(this.useComma ? ' ' : ', '),
`${this.data?.address?.street || ''} ${this.data?.address?.streetNumber || ''}`,
`${this.data?.address?.zipCode || ''} ${this.data?.address?.city || ''}`,
]
.filter((value) => value != '')
.filter((value) => value.trim());
result = parts.join(' | ');
result = parts.join(this.useComma ? ', ' : ' | ');
if (result !== this.result) {
this.result = result;

View File

@@ -1,4 +1,9 @@
<ui-checkbox *ngIf="customerType !== 'b2b'" [ngModel]="p4mUser" (ngModelChange)="setValue({ p4mUser: !p4mUser })" [disabled]="p4mReadonly">
<ui-checkbox
*ngIf="customerType !== 'b2b'"
[ngModel]="p4mUser"
(ngModelChange)="setValue({ p4mUser: !p4mUser })"
[disabled]="p4mReadonly || readonly"
>
Kundenkarte
</ui-checkbox>
<ng-container *ngFor="let option of filteredOptions$ | async">
@@ -6,6 +11,7 @@
*ngIf="option?.enabled !== false"
[ngModel]="option.value === customerType"
(ngModelChange)="setValue({ customerType: $event ? option.value : undefined })"
[disabled]="readonly"
>
{{ option.label }}
</ui-checkbox>

View File

@@ -42,6 +42,9 @@ export class CustomerTypeSelectorComponent extends ComponentStore<CustomerTypeSe
implements OnInit, OnDestroy, ControlValueAccessor {
private _onDestroy$ = new Subject<void>();
@Input()
readonly: boolean;
@Input()
get value() {
if (this.p4mUser) {

View File

@@ -1,15 +1,18 @@
<div class="input-wrapper" [class.empty]="!ngControl?.value" [class.focused]="uiControl?.focused | async">
<div class="input-wrapper" [class.empty]="!ngControl?.value" [class.focused]="!isReadonly && (uiControl?.focused | async)">
<!-- suffix and prefix order changed due to flex-row-reverse -->
<span class="suffix">{{ suffix }}</span>
<ng-content select="input, ui-select, ui-datepicker, button, ui-searchbox, textarea"></ng-content>
<span class="prefix">{{ prefix }}</span>
<label *ngIf="label" [for]="uiControl?.id">{{ label }}{{ requiredMark }}</label>
</div>
<span class="hint" *ngIf="ngControl?.touched && ngControl?.errors">
<span class="hint" *ngIf="showHint && ngControl?.touched && ngControl?.errors">
{{ ngControl?.errors | uiFormControlFirstError: label }}
</span>
<span class="hint readonly-hint" *ngIf="showHint && isReadonly">Nicht Änderbar</span>
<button
*ngIf="clearable && ngControl?.value && !ngControl?.disabled && !(uiControl?.type === 'radio' || uiControl?.type === 'checkbox')"
*ngIf="
!isReadonly && clearable && ngControl?.value && !ngControl?.disabled && !(uiControl?.type === 'radio' || uiControl?.type === 'checkbox')
"
class="ui-form-control-clear clear"
type="button"
(click)="clear()"

View File

@@ -8,6 +8,10 @@ button.clear {
.hint {
@apply text-brand text-x-small font-bold;
&.readonly-hint {
@apply text-inactive-branch font-normal;
}
}
.status {
@@ -142,3 +146,10 @@ button.clear {
@apply gap-0;
}
}
::ng-deep .ui-form-control--readonly {
input,
ui-select {
@apply opacity-40;
}
}

View File

@@ -32,6 +32,11 @@ export class UiFormControlComponent implements AfterContentInit, OnDestroy {
return this.uiControl?.type;
}
@HostBinding('class.ui-form-control--readonly')
get isReadonly() {
return this.uiControl?.readonly;
}
@Input()
label: string;
@@ -50,6 +55,9 @@ export class UiFormControlComponent implements AfterContentInit, OnDestroy {
@Input()
suffix: string;
@Input()
showHint = true;
@Output()
cleared = new EventEmitter();
@@ -68,8 +76,6 @@ export class UiFormControlComponent implements AfterContentInit, OnDestroy {
this.cdr.markForCheck();
})
);
} else {
console.error('UiFormControlDirective is missing in Template');
}
}

View File

@@ -1,3 +1,4 @@
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
import { Directive, EventEmitter, HostBinding, Input } from '@angular/core';
@Directive()
@@ -10,6 +11,17 @@ export abstract class UiFormControlDirective<T> {
@HostBinding('attr.id')
id: string = (Date.now() + Math.random()).toString(32);
private _readonly = false;
@Input()
@HostBinding('readonly')
get readonly(): boolean {
return this._readonly;
}
set readonly(value: BooleanInput) {
this._readonly = coerceBooleanProperty(value);
}
focused = new EventEmitter<boolean>();
abstract type: string;

View File

@@ -17,6 +17,11 @@ export class UiFormFieldComponent implements AfterContentInit {
@ContentChild(UiLabelComponent, { read: UiLabelComponent }) label: UiLabelComponent;
@HostBinding('class.ui-form-field--readonly')
isReadonly() {
return this.input?.readonly;
}
constructor() {}
ngAfterContentInit(): void {

View File

@@ -1,3 +1,4 @@
import { BooleanInput } from '@angular/cdk/coercion';
import { Directive, ElementRef, HostBinding, Input, Optional, Self } from '@angular/core';
import { FormControl, FormControlDirective } from '@angular/forms';
@@ -26,6 +27,10 @@ export class UiInputDirective {
}
}
@Input()
@HostBinding('readonly')
readonly: boolean;
constructor(
@Optional() @Self() private readonly _controlDirective: FormControlDirective,
private _elementRef: ElementRef<UiInputDirective>

View File

@@ -32,16 +32,6 @@ import { UiSelectOptionComponent } from './ui-select-option.component';
export class UiSelectComponent extends UiFormControlDirective<any> implements ControlValueAccessor, AfterContentInit {
readonly type = 'ui-select';
private _readonly = false;
@Input()
get readonly(): boolean {
return this._readonly;
}
set readonly(value: BooleanInput) {
this._readonly = coerceBooleanProperty(value);
}
@Input()
value: any;