Merged PR 1831: Kundendaten // B2B - nach "Bearbeiten" speichern nicht möglich

fix(customer): Updated Validations for B2B Customer
Conditional Validator - If Organisation Name is set First and Lastname is not required, If no first and last name organisation name is required
Ref: #4996
This commit is contained in:
Lorenz Hilpert
2025-04-11 10:21:18 +00:00
committed by Nino Righi
parent a93251f082
commit cdcd41a884
6 changed files with 319 additions and 41 deletions

View File

@@ -12,22 +12,45 @@
<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
[label]="formGroup.controls.organisation.errors?.required ? 'Firma *' : '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" />
<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" />
<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 *ngFor="let gender of genderSettings.genders" [value]="gender.value">{{ gender.label }}</shared-select-option>
<shared-select-option *ngFor="let gender of genderSettings.genders" [value]="gender.value">{{
gender.label
}}</shared-select-option>
</shared-select>
</shared-form-control>
@@ -42,50 +65,107 @@
</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
[label]="formGroup.controls.organisation.errors?.required ? 'Nachname *' : '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
[label]="formGroup.controls.organisation.errors?.required ? 'Vorname *' : '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" />
<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" />
<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" />
<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" />
<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" />
<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" />
<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">
<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 formControlName="isDefault">Diese Lieferadresse als Standard Adresse festlegen</shared-checkbox>
<shared-checkbox formControlName="isDefault"
>Diese Lieferadresse als Standard Adresse festlegen</shared-checkbox
>
</div>
<div class="mt-6 text-center col-span-2">
<button
@@ -98,3 +178,4 @@
</button>
</div>
</form>

View File

@@ -15,6 +15,7 @@ import { RouterLink } from '@angular/router';
import { IconComponent } from '@shared/components/icon';
import { zipCodeValidator } from '../../validators/zip-code-validator';
import { GenderSettingsService } from '@shared/services/gender';
import { validateCompanyOrPersonalInfoRequired } from '../../validators/gender-b2b-validator';
@Component({
selector: 'page-add-shipping-address-main-view',
@@ -70,10 +71,30 @@ export class AddShippingAddressMainViewComponent implements OnInit, OnDestroy {
ngOnInit() {
this._store.customer$.pipe(takeUntil(this._onDestroy)).subscribe(() => {
// Dynamic validation rules based on account type
// For business accounts (isBusinessKonto), we apply different validation rules
// than for personal accounts
if (this._store.isBusinessKonto) {
this.formGroup.controls.organisation.setValidators([Validators.required]);
// For business accounts:
// - Clear individual validators from personal info fields
// - Add a form-level validator that requires either company OR personal info
// - Make address fields optional to support different business address formats
this.formGroup.controls.organisation.setValidators([]);
this.formGroup.controls.gender.setValidators([]);
this.formGroup.controls.firstName.setValidators([]);
this.formGroup.controls.lastName.setValidators([]);
this.formGroup.setValidators([validateCompanyOrPersonalInfoRequired]);
this.formGroup.controls.street.setValidators([]);
this.formGroup.controls.streetNumber.setValidators([]);
} else {
// For personal accounts:
// - Organization info is optional
// - First and last name are required
// - No special form-level validators needed
this.formGroup.controls.organisation.clearValidators();
this.formGroup.controls.firstName.setValidators([Validators.required]);
this.formGroup.controls.lastName.setValidators([Validators.required]);
this.formGroup.setValidators([]);
}
});
}
@@ -133,7 +154,10 @@ export class AddShippingAddressMainViewComponent implements OnInit, OnDestroy {
formData.isDefault,
);
this._navigation.navigateToDetails({ processId: this._store.processId, customerId: this._store.customerId });
this._navigation.navigateToDetails({
processId: this._store.processId,
customerId: this._store.customerId,
});
} catch (error) {
this.formGroup.enable();
}

View File

@@ -5,9 +5,10 @@ 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 { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { UiDateInputDirective } from '@ui/input';
import { validateCompanyOrPersonalInfoRequired } from '../../validators/gender-b2b-validator';
@Component({
selector: 'page-customer-data-edit-b2b',
@@ -26,8 +27,9 @@ import { UiDateInputDirective } from '@ui/input';
],
})
export class CustomerDataEditB2BComponent extends CustomerDataEditComponent {
afterInitForm = (control) => {
afterInitForm = (control: FormGroup) => {
control.get('lastName').setValidators([]);
control.get('firstName').setValidators([]);
control.setValidators([validateCompanyOrPersonalInfoRequired]);
};
}

View File

@@ -12,22 +12,49 @@
<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
[label]="formGroup.controls.organisation.errors?.required ? 'Firma *' : 'Firma'"
class="col-span-2"
[hasRequiredMark]="
formGroup.controls.organisation.errors?.required ||
formGroup.errors?.eitherCompanyOrPersonalRequired
"
>
<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" />
<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" />
<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 *ngFor="let gender of genderSettings.genders" [value]="gender.value">{{ gender.label }}</shared-select-option>
<shared-select-option *ngFor="let gender of genderSettings.genders" [value]="gender.value">{{
gender.label
}}</shared-select-option>
</shared-select>
</shared-form-control>
@@ -42,50 +69,115 @@
</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
[label]="formGroup.controls.organisation.errors?.required ? 'Nachname *' : 'Nachname'"
[hasRequiredMark]="
formGroup.controls.lastName.errors?.required ||
formGroup.errors?.eitherCompanyOrPersonalRequired
"
>
<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
[label]="formGroup.controls.organisation.errors?.required ? 'Vorname *' : 'Vorname'"
[hasRequiredMark]="
formGroup.controls.firstName.errors?.required ||
formGroup.errors?.eitherCompanyOrPersonalRequired
"
>
<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" />
<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" />
<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" />
<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" />
<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" />
<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" />
<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">
<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 formControlName="isDefault">Diese Lieferadresse als Standard Adresse festlegen</shared-checkbox>
<shared-checkbox formControlName="isDefault"
>Diese Lieferadresse als Standard Adresse festlegen</shared-checkbox
>
</div>
<div class="mt-6 text-center col-span-2">
<button
@@ -98,3 +190,4 @@
</button>
</div>
</form>

View File

@@ -1,4 +1,11 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, inject, ChangeDetectorRef } from '@angular/core';
import {
Component,
ChangeDetectionStrategy,
OnInit,
OnDestroy,
inject,
ChangeDetectorRef,
} from '@angular/core';
import { CheckboxComponent } from '@shared/components/checkbox';
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { SelectModule } from '@shared/components/select';
@@ -16,6 +23,7 @@ import { ActivatedRoute, RouterLink } from '@angular/router';
import { ComponentStore } from '@ngrx/component-store';
import { zipCodeValidator } from '../../validators/zip-code-validator';
import { GenderSettingsService } from '@shared/services/gender';
import { validateCompanyOrPersonalInfoRequired } from '../../validators/gender-b2b-validator';
export interface EditShippingAddressMainViewState {
shippingAddress?: ShippingAddressDTO;
@@ -102,9 +110,15 @@ export class EditShippingAddressMainViewComponent
this._store.customer$.pipe(takeUntil(this._onDestroy)).subscribe(() => {
if (this._store.isBusinessKonto) {
this.formGroup.controls.organisation.setValidators([Validators.required]);
this.formGroup.controls.organisation.setValidators([]);
this.formGroup.controls.firstName.setValidators([]);
this.formGroup.controls.lastName.setValidators([]);
this.formGroup.setValidators([validateCompanyOrPersonalInfoRequired]);
} else {
this.formGroup.controls.organisation.clearValidators();
this.formGroup.controls.firstName.setValidators([Validators.required]);
this.formGroup.controls.lastName.setValidators([Validators.required]);
this.formGroup.setValidators([]);
}
});
}
@@ -185,7 +199,10 @@ export class EditShippingAddressMainViewComponent
formData.isDefault,
);
this._navigation.navigateToDetails({ processId: this._store.processId, customerId: this._store.customerId });
this._navigation.navigateToDetails({
processId: this._store.processId,
customerId: this._store.customerId,
});
} catch (error) {
this.formGroup.enable();
}

View File

@@ -26,6 +26,8 @@ export function requireGenderWhenNameIsSet(control: UntypedFormGroup): Validatio
const genderControl = control.get('gender');
const nameControl = control.get('lastName');
genderControl?.setErrors(null);
if (nameControl && !Validators.required(nameControl)) {
const errors = Validators.min(1)(genderControl) ? { required: true } : null;
@@ -36,3 +38,62 @@ export function requireGenderWhenNameIsSet(control: UntypedFormGroup): Validatio
}
return null;
}
/**
* Angular form validator that implements mutual exclusivity between organisation name and personal details.
* Either:
* 1. Organisation name must be provided (company case), OR
* 2. Both first name and last name must be provided (individual case)
*
* @param control - The form group containing 'firstName', 'lastName', and nested 'organisation.name' controls
* @returns Validation errors if the mutual exclusivity rule is violated, null otherwise
*/
export function validateCompanyOrPersonalInfoRequired(
control: UntypedFormGroup,
): ValidationErrors | null {
const firstNameControl = control.get('firstName');
const lastNameControl = control.get('lastName');
const organisationControl = control.get('organisation');
// Safely access the nested company name, with null checks
const companyNameControl = organisationControl?.get('name') ?? organisationControl;
if (!firstNameControl || !lastNameControl || !companyNameControl) {
// If any control is missing, we can't validate
return null;
}
const hasCompanyName = !!companyNameControl.value;
const hasFirstName = !!firstNameControl.value;
const hasLastName = !!lastNameControl.value;
const hasPersonalInfo = hasFirstName || hasLastName;
// Clear previous validation errors
firstNameControl.setErrors(null);
lastNameControl.setErrors(null);
companyNameControl.setErrors(null);
// Case 1: Neither company name nor personal info provided - require one of them
if (!hasCompanyName && !hasPersonalInfo) {
const errors = { required: true };
companyNameControl.setErrors(errors);
firstNameControl.setErrors(errors);
lastNameControl.setErrors(errors);
return { eitherCompanyOrPersonalRequired: true };
}
// Case 2: First name provided but last name missing
if (hasFirstName && !hasLastName) {
lastNameControl.setErrors({ required: true });
return { lastNameRequired: true };
}
// Case 3: Last name provided but first name missing
if (!hasFirstName && hasLastName) {
firstNameControl.setErrors({ required: true });
return { firstNameRequired: true };
}
// Valid: Either company name or complete personal info is provided
return null;
}