ANlage und bearbeiten von Adressen

This commit is contained in:
Lorenz Hilpert
2023-07-10 11:42:28 +02:00
parent 7b12857a35
commit 201ea2ee9c
41 changed files with 1684 additions and 38 deletions

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

@@ -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,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

@@ -1,3 +1,3 @@
<shared-breadcrumb [key]="processId$ | async" [tags]="['customer']" class="mb-9"></shared-breadcrumb>
<shared-breadcrumb [key]="processId$ | async" [tags]="['customer']" class="mb-3"></shared-breadcrumb>
<router-outlet></router-outlet>

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,74 @@
<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,91 @@
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 } from '@angular/common';
import { AddressSelectionModalService } from '@shared/modals/address-selection-modal';
import { CustomerSearchStore } from '../store';
import { CustomerSearchNavigation } from '../../navigations';
@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: [AsyncPipe, 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));
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,91 @@
<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,121 @@
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 } from 'rxjs';
@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: [AsyncPipe, NgIf, NgForOf, ReactiveFormsModule, SelectModule, FormControlComponent, CheckboxComponent],
})
export class AddShippingAddressMainViewComponent implements OnInit, OnDestroy {
private _onDestroy = new Subject<void>();
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,13 +1,11 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
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, map, switchMap, take, takeUntil } from 'rxjs/operators';
import { filter, first, switchMap, takeUntil } from 'rxjs/operators';
import { CustomerSearchNavigation } from '../navigations';
import { Filter } from '@shared/components/filter';
import { cloneDeep, isEqual } from 'lodash';
@Component({
selector: 'page-customer-search',
@@ -84,8 +82,8 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
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'].includes(this._breadcrumb)) {
console.log('_breadcrumb', this._breadcrumb);
if (['details', 'history', 'edit', 'add-billing-address'].includes(this._breadcrumb)) {
await this._router.navigate([], { queryParams: route.queryParams });
} else {
await this._router.navigate(route.path, { queryParams: route.queryParams });
@@ -152,15 +150,15 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
}
async checkBreadcrumbs(force = false) {
let breadcumb: string;
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;
}
this._breadcrumb = breadcumb;
this._breadcrumb = breadcrumb;
await this.checkMainBreadcrumb();
await this.checkSearchBreadcrumb();
await this.checkDetailsBreadcrumb();
@@ -319,7 +317,7 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
const navigation = this._navigation.editRoute({
processId: this._store.processId,
customerId: this._store.customerId,
isB2b: this._store.customer?.features?.['b2b'] ?? false,
isB2b: this._store.isBusinessKonto,
});
const breadcrumb: Breadcrumb = {

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,153 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } 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, 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';
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(
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);
}
});
}
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,150 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } 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 { 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';
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);
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(
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);
}
});
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
}
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

@@ -3,7 +3,7 @@
}
.data-label {
@apply w-[10.75rem];
@apply w-52;
}
.data-value {

View File

@@ -8,11 +8,14 @@
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">Kundendetails</h1>
<h1 class="text-[1.625rem] font-bold">
{{ (isBusinessKonto$ | async) ? 'Firmendetails' : 'Kundendetails' }}
</h1>
<p>Sind Ihre Kundendaten korrekt?</p>
</div>
</div>
@@ -22,6 +25,7 @@
*ngIf="editRoute$ | async; let editRoute"
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
class="btn btn-label font-bold text-brand"
type="button"
>Bearbeiten</a
@@ -41,7 +45,26 @@
<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>
<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>
@@ -62,6 +85,7 @@
<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>
@@ -86,6 +110,7 @@
<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>
@@ -94,12 +119,18 @@
<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>
<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>
</ng-template>

View File

@@ -40,6 +40,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);
@@ -66,9 +68,13 @@ 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);
isBusinessKonto$ = this._store.isBusinessKonto$;
constructor(private _store: CustomerSearchStore, private _navigation: CustomerSearchNavigation) {}
}

View File

@@ -4,9 +4,11 @@ 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';
@NgModule({
imports: [CommonModule, CountryPipe, RouterModule],
imports: [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,74 @@
<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,148 @@
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 } from 'rxjs';
import { ActivatedRoute } from '@angular/router';
import { UiModalService } from '@ui/modal';
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, NgForOf, ReactiveFormsModule, SelectModule, FormControlComponent, CheckboxComponent],
})
export class EditBillingAddressMainViewComponent extends ComponentStore<EditBillingAddressMainViewState> implements OnInit, OnDestroy {
private _onDestroy$ = new Subject<void>();
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

@@ -1,5 +1,11 @@
<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" 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

@@ -1,5 +1,11 @@
<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" 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,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,91 @@
<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,121 @@
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 } from 'rxjs';
@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: [AsyncPipe, NgIf, NgForOf, ReactiveFormsModule, SelectModule, FormControlComponent, CheckboxComponent],
})
export class EditShippingAddressMainViewComponent implements OnInit, OnDestroy {
private _onDestroy = new Subject<void>();
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

@@ -38,6 +38,42 @@ export class CustomerSearchStore extends ComponentStore<CustomerSearchState> imp
customer$ = this.select(S.selectCustomer);
get isBestellungOhneKonto() {
return this.get(S.selectIsBestellungOhneKonto);
}
isBestellungOhneKonto$ = this.select(S.selectIsBestellungOhneKonto);
get isOnlinekonto() {
return this.get(S.selectIsOnlinekonto);
}
isOnlinekonto$ = this.select(S.selectIsOnlinekonto);
get isOnlineKontoMitKundenkarte() {
return this.get(S.selectIsOnlinekontoMitKundenkundenkarte);
}
isOnlineKontoMitKundenkarte$ = this.select(S.selectIsOnlinekontoMitKundenkundenkarte);
get isKundenkarte() {
return this.get(S.selectIsKundenkarte);
}
isKundenkarte$ = this.select(S.selectIsKundenkarte);
get isBusinessKonto() {
return this.get(S.selectIsBusinessKonto);
}
isBusinessKonto$ = this.select(S.selectIsBusinessKonto);
get isMitarbeiter() {
return this.get(S.selectIsMitarbeiter);
}
isMitarbeiter$ = this.select(S.selectIsMitarbeiter);
get queryParams() {
return this.get(S.selectQueryParams);
}
@@ -109,7 +145,7 @@ export class CustomerSearchStore extends ComponentStore<CustomerSearchState> imp
}),
switchMap((customerId) =>
this._customerService
.getCustomer(customerId)
.getCustomer(customerId, 2)
.pipe(tapResponse(this.handleSelectCustomerResponse, this.handleSelectCustomerError, this.handleSelectCustomerComplete))
)
)

View File

@@ -19,6 +19,54 @@ export function selectCustomerId(s: CustomerSearchState) {
return selectCustomer(s)?.id;
}
export function selecthasB2bFeature(s: CustomerSearchState) {
return !!selectCustomer(s)?.features?.some((c) => c.key === 'b2b');
}
export function selectHasGuestFeature(s: CustomerSearchState) {
return !!selectCustomer(s)?.features?.some((c) => c.key === 'guest');
}
export function selectHasWebshopFeature(s: CustomerSearchState) {
return !!selectCustomer(s)?.features?.some((c) => c.key === 'webshop');
}
export function selectHasP4mUserFeature(s: CustomerSearchState) {
return !!selectCustomer(s)?.features?.some((c) => c.key === 'p4mUser');
}
export function selectHasStaffFeature(s: CustomerSearchState) {
return !!selectCustomer(s)?.features?.some((c) => c.key === 'staff');
}
export function selectHasStoreFeature(s: CustomerSearchState) {
return !!selectCustomer(s)?.features?.some((c) => c.key === 'store');
}
export function selectIsBestellungOhneKonto(s: CustomerSearchState) {
return selectHasWebshopFeature(s) && selectHasGuestFeature(s);
}
export function selectIsOnlinekonto(s: CustomerSearchState) {
return selectHasWebshopFeature(s) && !selectHasGuestFeature(s);
}
export function selectIsOnlinekontoMitKundenkundenkarte(s: CustomerSearchState) {
return selectHasWebshopFeature(s) && selectHasP4mUserFeature(s);
}
export function selectIsKundenkarte(s: CustomerSearchState) {
return selectHasP4mUserFeature(s) && selectHasStoreFeature(s);
}
export function selectIsBusinessKonto(s: CustomerSearchState) {
return selecthasB2bFeature(s) && !selectHasStaffFeature(s);
}
export function selectIsMitarbeiter(s: CustomerSearchState) {
return selectHasStaffFeature(s);
}
export function selectFilter(s: CustomerSearchState) {
if (!s.defaultFilter) return undefined;

View File

@@ -45,6 +45,76 @@ export class CustomerSearchNavigation {
return this._router.navigate(route.path, { queryParams: route.queryParams });
}
addBillingAddressRoute(params: { processId: NumberInput; customerId: NumberInput }): NavigationRoute {
const path = [
'/kunde',
coerceNumberProperty(params.processId),
'customer',
coerceNumberProperty(params.customerId),
'billingaddress',
'add',
];
const urlTree = this._router.createUrlTree(path, { queryParams: {} });
return {
path,
urlTree,
};
}
navigateToAddBillingAddress(params: { processId: NumberInput; customerId: NumberInput }): Promise<boolean> {
const route = this.addBillingAddressRoute(params);
return this._router.navigate(route.path, { queryParams: route.queryParams });
}
editBillingAddressRoute(params: { processId: NumberInput; customerId: NumberInput; payerId: NumberInput }): NavigationRoute {
const path = [
'/kunde',
coerceNumberProperty(params.processId),
'customer',
coerceNumberProperty(params.customerId),
'billingaddress',
coerceNumberProperty(params.payerId),
'edit',
];
const urlTree = this._router.createUrlTree(path, { queryParams: {} });
return {
path,
urlTree,
};
}
navigateToEditBillingAddress(params: { processId: NumberInput; customerId: NumberInput; payerId: NumberInput }): Promise<boolean> {
const route = this.editBillingAddressRoute(params);
return this._router.navigate(route.path, { queryParams: route.queryParams });
}
addShippingAddressRoute(params: { processId: NumberInput; customerId: NumberInput }): NavigationRoute {
const path = [
'/kunde',
coerceNumberProperty(params.processId),
'customer',
coerceNumberProperty(params.customerId),
'shippingaddress',
'add',
];
const urlTree = this._router.createUrlTree(path, { queryParams: {} });
return {
path,
urlTree,
};
}
navigateToAddShippingAddress(params: { processId: NumberInput; customerId: NumberInput }): Promise<boolean> {
const route = this.addShippingAddressRoute(params);
return this._router.navigate(route.path, { queryParams: route.queryParams });
}
historyRoute(params: { processId: NumberInput; customerId: NumberInput }): NavigationRoute {
const path = ['/kunde', coerceNumberProperty(params.processId), 'customer', coerceNumberProperty(params.customerId), 'history'];

View File

@@ -17,6 +17,10 @@ import { UpdateP4MWebshopCustomerComponent } from './create-customer/update-p4m-
import { CreateCustomerComponent } from './create-customer/create-customer.component';
import { CustomerDataEditB2BComponent } from './customer-search/edit-main-view/customer-data-edit-b2b.component';
import { CustomerDataEditB2CComponent } from './customer-search/edit-main-view/customer-data-edit-b2c.component';
import { AddBillingAddressMainViewComponent } from './customer-search/add-billing-address-main-view/add-billing-address-main-view.component';
import { AddShippingAddressMainViewComponent } from './customer-search/add-shipping-address-main-view/add-shipping-address-main-view.component';
import { EditBillingAddressMainViewComponent } from './customer-search/edit-billing-address-main-view/edit-billing-address-main-view.component';
import { EditShippingAddressMainViewComponent } from './customer-search/edit-shipping-address-main-view/edit-shipping-address-main-view.component';
export const routes: Routes = [
{
@@ -42,14 +46,34 @@ export const routes: Routes = [
path: '',
component: CustomerSearchComponent,
children: [
{ path: '', component: CustomerFilterMainViewComponent, data: { side: 'main', breadcumb: 'main' } },
{ path: 'search', component: CustomerResultsMainViewComponent, data: { breadcumb: 'search' } },
{ path: 'filter', component: CustomerFilterMainViewComponent, data: { side: 'results', breadcumb: 'search' } },
{ path: ':customerId', component: CustomerDetailsViewMainComponent, data: { side: 'results', breadcumb: 'details' } },
{ path: ':customerId/history', component: CustomerHistoryMainViewComponent, data: { side: 'results', breadcumb: 'history' } },
{ path: ':customerId/edit/b2b', component: CustomerDataEditB2BComponent, data: { side: 'results', breadcumb: 'edit' } },
{ path: ':customerId/edit', component: CustomerDataEditB2CComponent, data: { side: 'results', breadcumb: 'edit' } },
// { path: ':customerId/orders', component: CustomerSearchComponent, data: { side: 'orderItems' } },
{ path: '', component: CustomerFilterMainViewComponent, data: { side: 'main', breadcrumb: 'main' } },
{ path: 'search', component: CustomerResultsMainViewComponent, data: { breadcrumb: 'search' } },
{ path: 'filter', component: CustomerFilterMainViewComponent, data: { side: 'results', breadcrumb: 'search' } },
{ path: ':customerId', component: CustomerDetailsViewMainComponent, data: { side: 'results', breadcrumb: 'details' } },
{ path: ':customerId/history', component: CustomerHistoryMainViewComponent, data: { side: 'results', breadcrumb: 'history' } },
{ path: ':customerId/edit/b2b', component: CustomerDataEditB2BComponent, data: { side: 'results', breadcrumb: 'edit' } },
{ path: ':customerId/edit', component: CustomerDataEditB2CComponent, data: { side: 'results', breadcrumb: 'edit' } },
{
path: ':customerId/billingaddress/add',
component: AddBillingAddressMainViewComponent,
data: { side: 'results', breadcrumb: 'add-billing-address' },
},
{
path: ':customerId/billingaddress/:payerId/edit',
component: EditBillingAddressMainViewComponent,
data: { side: 'results', breadcrumb: 'edit-billing-address' },
},
{
path: ':customerId/shippingaddress/add',
component: AddShippingAddressMainViewComponent,
data: { side: 'results', breadcrumb: 'add-shipping-address' },
},
{
path: ':customerId/shippingaddress/:shippingAddressId/edit',
component: EditShippingAddressMainViewComponent,
data: { side: 'results', breadcrumb: 'edit-shipping-address' },
},
// { path: ':customerId/orders', component: CustomerSearchComponent, data: { side: 'orderItems' } }, EditBillingAddressMainViewComponent
],
},
],

View File

@@ -0,0 +1,15 @@
<div class="wrapper">
<h2>Bitte überprüfen Sie die eingegebenen Adressdaten</h2>
<p>Vorschläge:</p>
<ul class="content">
<li *ngFor="let item of ref?.data">
<span>{{ item.street }} {{ item.streetNumber }}, {{ item.zipCode }} {{ item.city }}</span>
<button (click)="ref.close(item)">Übernehmen</button>
</li>
</ul>
<div class="center">
<button class="select-btn" (click)="ref.close('continue')">Eingegebene Adresse übernehmen</button>
</div>
</div>

View File

@@ -0,0 +1,36 @@
:host {
@apply bg-white;
}
h2,
p {
@apply text-center text-p2;
}
h2 {
@apply font-bold text-xl;
}
ul {
@apply list-none text-p2 -mx-4 p-0 mt-px-35;
li {
@apply flex flex-row items-center justify-between border-glitter border-t-4 border-b-0 border-solid border-r-0 border-l-0 py-px-15 px-px-25;
&:last-child {
@apply border-b-4;
}
button {
@apply border-none outline-none bg-transparent text-brand text-p1 text-right font-bold;
}
}
}
.center {
@apply text-center;
}
.select-btn {
@apply border-none outline-none bg-brand text-white font-bold text-p1 px-px-25 py-px-15 rounded-full my-8;
}

View File

@@ -0,0 +1,13 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { AddressDTO } from '@swagger/crm';
import { UiModalRef } from '@ui/modal';
@Component({
selector: 'page-address-selection-modal',
templateUrl: 'address-selection-modal.component.html',
styleUrls: ['address-selection-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddressSelectionModalComponent {
constructor(public ref: UiModalRef<AddressDTO[] | string>) {}
}

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AddressSelectionModalComponent } from './address-selection-modal.component';
import { UiIconModule } from '@ui/icon';
@NgModule({
imports: [CommonModule, UiIconModule],
exports: [AddressSelectionModalComponent],
declarations: [AddressSelectionModalComponent],
})
export class AddressSelectionModalModule {}

View File

@@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import { CrmCustomerService } from '@domain/crm';
import { AddressDTO } from '@swagger/crm';
import { UiModalService } from '@ui/modal';
import { map } from 'rxjs/operators';
import { AddressSelectionModalComponent } from './address-selection-modal.component';
@Injectable({ providedIn: 'root' })
export class AddressSelectionModalService {
constructor(private customerService: CrmCustomerService, private modal: UiModalService) {}
async validateAddress(address: AddressDTO): Promise<AddressDTO> {
if (address.street && address.streetNumber && address.zipCode && address.city && address.country) {
try {
let addresses = await this.customerService
.validateAddress(address)
.pipe(
map((response) =>
response.result?.map((ad) => ({
...address,
...ad,
}))
)
)
.toPromise();
if (addresses?.length > 0) {
const modalResult = await this.modal.open({ content: AddressSelectionModalComponent, data: addresses }).afterClosed$.toPromise();
if (modalResult?.data) {
if (modalResult.data === 'continue') {
return address;
} else {
return modalResult.data;
}
}
}
} catch (error) {
throw error;
}
}
return address;
}
}

View File

@@ -0,0 +1,3 @@
export * from './lib/address-selection-modal.component';
export * from './lib/address-selection-modal.module';
export * from './lib/address-selection-modal.service';