From ba89bdd097fd06f1633580d9aa997c0bd8eb264c Mon Sep 17 00:00:00 2001
From: Albert <Albert.Bruns@mgm-tp.com>
Date: Thu, 3 Apr 2025 11:31:29 +0200
Subject: [PATCH] poc

---
 .../admin/keycloak-shared/src/lib/form.util.ts  |  3 +++
 .../src/lib/keycloak-formservice.ts             | 12 ++----------
 .../user-form-roles.component.html              |  6 +++---
 .../user-form-roles.component.ts                | 12 +++++++++++-
 .../user-form-save-button.component.ts          |  6 +++---
 .../src/lib/user-form/user-form.component.html  |  4 ++--
 .../src/lib/user-form/user-form.component.ts    |  5 +++--
 .../user/src/lib/user-form/user.formservice.ts  | 17 +++++++++++++++++
 .../checkbox-editor.component.html              |  1 +
 .../checkbox-editor.component.ts                |  4 +++-
 .../formcontrol-editor.abstract.component.ts    | 13 ++++++++++++-
 .../src/lib/form/checkbox/checkbox.component.ts |  9 ++++++++-
 12 files changed, 68 insertions(+), 24 deletions(-)

diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/form.util.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/form.util.ts
index d57cf61935..0aa7983369 100644
--- a/alfa-client/libs/admin/keycloak-shared/src/lib/form.util.ts
+++ b/alfa-client/libs/admin/keycloak-shared/src/lib/form.util.ts
@@ -5,6 +5,9 @@ export function patchForm(valueToPatch: any, formGroup: FormGroup): void {
   formGroup.patchValue(valueToPatch);
 
   patchNonStringValues(valueToPatch, formGroup);
+
+  console.log(formGroup);
+  console.log(valueToPatch);
 }
 
 function patchNonStringValues(valueToPatch: any, formGroup: FormGroup): void {
diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts
index 6cd713c43d..2949096da2 100644
--- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts
+++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts
@@ -31,7 +31,7 @@ import {
 import { inject, Injectable } from '@angular/core';
 import { AbstractControl, FormArray, FormBuilder, FormGroup } from '@angular/forms';
 import { ActivatedRoute, UrlSegment } from '@angular/router';
-import { catchError, first, Observable, of, tap } from 'rxjs';
+import { catchError, delay, first, Observable, of, startWith, tap } from 'rxjs';
 import { ValidationMessageCode } from '../../../../tech-shared/src/lib/validation/tech.validation.messages';
 import * as FormUtil from './form.util';
 import { ErrorRepresentation, KeycloakErrorMessage, KeycloakFieldName, KeycloakHttpErrorResponse } from './keycloak-error.model';
@@ -78,17 +78,9 @@ export abstract class KeycloakFormService<T> {
   }
 
   public submit(): Observable<StateResource<T>> {
-    console.info('value before update and validity: %o', this.form.value);
-    this._showValidationErrorForAllInvalidControls(this.form);
-    console.info('value after update and validity: %o', this.form.value);
-    setTimeout(() => {
-      console.info('form after timeout valid = %s, value = %o', this.form.valid, this.form.value);
-    }, 0);
     if (this.form.invalid) {
-      console.info('form INVALID');
       return this._processInvalidForm();
     }
-    console.info('form VALID');
     return this._doSubmit().pipe(
       catchError((keycloakError: KeycloakHttpErrorResponse) => this._processResponseValidationErrors(keycloakError)),
     );
@@ -96,7 +88,7 @@ export abstract class KeycloakFormService<T> {
 
   _processInvalidForm(): Observable<StateResource<T>> {
     this._showValidationErrorForAllInvalidControls(this.form);
-    return of(createEmptyStateResource<T>());
+    return of(createEmptyStateResource<T>()).pipe(delay(200), startWith(createEmptyStateResource<T>(true)));
   }
 
   _processResponseValidationErrors(keycloakError: KeycloakHttpErrorResponse): Observable<StateResource<T>> {
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html
index d3d39a71ce..f89c785b92 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html
@@ -35,7 +35,7 @@
     <div [formGroupName]="UserFormService.ALFA_GROUP" class="flex flex-1 flex-col gap-2">
       <h3 class="text-md block font-medium text-text">Alfa</h3>
       <div class="flex items-center gap-2">
-        <ods-checkbox-editor [formControlName]="UserFormService.LOESCHEN" label="Löschen" inputId="delete" />
+        <ods-checkbox-editor [formControlName]="UserFormService.LOESCHEN" label="Löschen" inputId="delete" (inputChange)="disableUncheckedAlfaCheckboxes(UserFormService.LOESCHEN,$event)"/>
         <button
           data-test-id="loschen-role-info-button"
           tooltip='Diese Rolle hat dieselben Rechte wie die Rolle "User". Zusätzlich kann "Löschen" Löschanträge aus Alfa bestätigen. '
@@ -44,7 +44,7 @@
         </button>
       </div>
       <div class="flex items-center gap-2">
-        <ods-checkbox-editor [formControlName]="UserFormService.USER" label="User" inputId="user" />
+        <ods-checkbox-editor [formControlName]="UserFormService.USER" label="User" inputId="user" (inputChange)="disableUncheckedAlfaCheckboxes(UserFormService.USER,$event)"/>
         <button
           data-test-id="user-role-info-button"
           tooltip="Diese Rolle kann alle Vorgänge sehen und bearbeiten, wenn diese seiner Organisationseinheit zugewiesen sind."
@@ -53,7 +53,7 @@
         </button>
       </div>
       <div class="flex items-center gap-2">
-        <ods-checkbox-editor [formControlName]="UserFormService.POSTSTELLE" label="Poststelle" inputId="post_office" />
+        <ods-checkbox-editor [formControlName]="UserFormService.POSTSTELLE" label="Poststelle" inputId="post_office" (inputChange)="disableUncheckedAlfaCheckboxes(UserFormService.POSTSTELLE,$event)"/>
         <button data-test-id="poststelle-role-info-button" tooltip="Diese Rolle kann alle neu eingegangenen Vorgänge sehen.">
           <ods-info-icon />
         </button>
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts
index 7a8c901dab..27480606f9 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts
@@ -1,6 +1,6 @@
 import { generateValidationErrorId, InvalidParam } from '@alfa-client/tech-shared';
 import { AsyncPipe } from '@angular/common';
-import { Component, Input, OnInit } from '@angular/core';
+import { Component, inject, Input, OnInit } from '@angular/core';
 import { AbstractControl, FormControlStatus, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
 import { CheckboxEditorComponent, ValidationErrorComponent } from '@ods/component';
 import { InfoIconComponent, TooltipDirective } from '@ods/system';
@@ -22,6 +22,8 @@ import { UserFormService } from '../user.formservice';
   templateUrl: './user-form-roles.component.html',
 })
 export class UserFormRolesComponent implements OnInit {
+  public readonly formService = inject(UserFormService);
+
   @Input() formGroupParent: UntypedFormGroup;
 
   public invalidParams$: Observable<InvalidParam[]> = of([]);
@@ -37,4 +39,12 @@ export class UserFormRolesComponent implements OnInit {
       tap((invalidParams: InvalidParam[]) => (this.isValid = isEmpty(invalidParams))),
     );
   }
+
+  disableUncheckedAlfaCheckboxes(formControlName: string, value: boolean) {
+    if (value) {
+      this.formService.disableOtherCheckboxes(formControlName);
+    } else {
+      this.formService.enableAllAlfaCheckboxes();
+    }
+  }
 }
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.ts
index 941904eb8d..e1d18c6e2c 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.ts
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.ts
@@ -1,7 +1,7 @@
 import { User } from '@admin-client/user-shared';
 import { StateResource } from '@alfa-client/tech-shared';
 import { AsyncPipe } from '@angular/common';
-import { Component, inject } from '@angular/core';
+import { Component, inject, Input } from '@angular/core';
 import { ButtonWithSpinnerComponent } from '@ods/component';
 import { Observable } from 'rxjs';
 import { UserFormService } from '../user.formservice';
@@ -15,9 +15,9 @@ import { UserFormService } from '../user.formservice';
 export class UserFormSaveButtonComponent {
   public readonly formService = inject(UserFormService);
 
-  public submitStateResource$: Observable<StateResource<User>>;
+  @Input() submitStateResource$: Observable<StateResource<User>>;
 
   public submit(): void {
-    this.submitStateResource$ = this.formService.submit();
+    this.formService.form.get(UserFormService.ALFA_GROUP).updateValueAndValidity();
   }
 }
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.html
index 0d9c504aa0..367fabce15 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.html
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.html
@@ -23,7 +23,7 @@
     unter der Lizenz sind dem Lizenztext zu entnehmen.
 
 -->
-<form [formGroup]="formService.form" (ngSubmit)="onSubmit()">
+<form [formGroup]="formService.form" (ngSubmit)="submit()">
   <ods-spinner [stateResource]="userStateResource$ | async">
     <div class="max-w-[960px]" data-test-id="user-content">
       <admin-user-form-headline [isPatch]="isPatch" />
@@ -34,7 +34,7 @@
         [formGroupOrganisationsEinheiten]="formService.getOrganisationsEinheitenGroup()"
       />
       <div class="mb-6 flex justify-between">
-        <admin-user-form-save-button />
+        <admin-user-form-save-button [submitStateResource$]="submitStateResource$"/>
         @if (isPatch) {
           <admin-delete-open-dialog-button data-test-id="delete-button-container" />
         }
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.ts
index eee2cb8625..9874532168 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.ts
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.ts
@@ -62,6 +62,7 @@ export class UserFormComponent implements OnInit {
   public userStateResource$: Observable<StateResource<User>>;
   public isPatch: boolean;
   public userName: string;
+  public submitStateResource$: Observable<StateResource<User>>;
 
   ngOnInit(): void {
     this.userStateResource$ = this.formService.get().pipe(
@@ -72,7 +73,7 @@ export class UserFormComponent implements OnInit {
     );
   }
 
-  onSubmit(): void {
-    this.formService.submit();
+  public submit(): void {
+    this.submitStateResource$ = this.formService.submit();
   }
 }
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts
index d97dee1e2b..717247d028 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts
@@ -164,6 +164,8 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest
     const alfaGroup: UntypedFormGroup = this.getRoleGroup(UserFormService.ALFA_GROUP);
     this._handleAlfaGroupChange(alfaGroup);
     this._alfaGroupChanges = alfaGroup.valueChanges.subscribe(() => {
+      console.log('Checkboxes in ALFA_GROUP changed');
+      alfaGroup.parent.setErrors(null, { emitEvent: false });
       this._handleAlfaGroupChange(alfaGroup);
     });
   }
@@ -187,6 +189,21 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest
     }
   }
 
+  disableOtherCheckboxes(formControlName: string) {
+    const alfaGroup: UntypedFormGroup = this.getRoleGroup(UserFormService.ALFA_GROUP);
+    for (const key of Object.keys(alfaGroup.controls)) {
+      const control: AbstractControl = alfaGroup.controls[key];
+      if (formControlName !== key) {
+        if (!control.value) control.disable({ emitEvent: false });
+      }
+    }
+  }
+
+  enableAllAlfaCheckboxes() {
+    const alfaGroup: UntypedFormGroup = this.getRoleGroup(UserFormService.ALFA_GROUP);
+    this._enableAllCheckboxes(alfaGroup);
+  }
+
   _enableAllCheckboxes(group: UntypedFormGroup): void {
     for (const control of Object.values<AbstractControl>(group.controls)) {
       control.enable({ emitEvent: false });
diff --git a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html
index 1cc8b07470..cf21692584 100644
--- a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html
+++ b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html
@@ -30,6 +30,7 @@
   [disabled]="control.disabled"
   [hasError]="hasError"
   [ariaDescribedBy]="validationErrorId"
+  (inputChange)="inputChange.emit($event)"
 >
   <ods-validation-error
     error
diff --git a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts
index bbb9f693fc..0a098c993e 100644
--- a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts
+++ b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts
@@ -22,7 +22,7 @@
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
 import { ConvertForDataTestPipe, generateValidationErrorId } from '@alfa-client/tech-shared';
-import { Component, Input } from '@angular/core';
+import { Component, EventEmitter, Input, Output } from '@angular/core';
 import { ReactiveFormsModule } from '@angular/forms';
 import { CheckboxComponent } from '@ods/system';
 import { FormControlEditorAbstractComponent } from '../formcontrol-editor.abstract.component';
@@ -38,6 +38,8 @@ export class CheckboxEditorComponent extends FormControlEditorAbstractComponent
   @Input() inputId: string;
   @Input() label: string;
 
+  @Output() inputChange = new EventEmitter<boolean>();
+
   public readonly validationErrorId: string = generateValidationErrorId();
 
   get hasError(): boolean {
diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts
index 1338796de9..c83b8b16d3 100644
--- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts
+++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts
@@ -23,7 +23,7 @@
  */
 import { InvalidParam } from '@alfa-client/tech-shared';
 import { Component, OnDestroy, OnInit, Optional, Self } from '@angular/core';
-import { ControlValueAccessor, NgControl, UntypedFormControl } from '@angular/forms';
+import { AbstractControl, ControlValueAccessor, NgControl, UntypedFormControl } from '@angular/forms';
 import { Subscription } from 'rxjs';
 
 @Component({
@@ -94,6 +94,17 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue
   removeErrors(): void {
     this.fieldControl.setErrors(null);
     this._updateInvalidParams();
+    this.clearAllParentErrors(this.control.control);
+  }
+
+  clearAllParentErrors(control: AbstractControl | null): void {
+    if (!control) return;
+
+    const parent = control.parent;
+    if (parent) {
+      parent.setErrors(null);
+      this.clearAllParentErrors(parent);
+    }
   }
 
   _updateInvalidParams(): void {
diff --git a/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts b/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts
index 3b3750bd31..767a1e9c06 100644
--- a/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts
+++ b/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts
@@ -23,7 +23,7 @@
  */
 import { ConvertForDataTestPipe, EMPTY_STRING } from '@alfa-client/tech-shared';
 import { CommonModule } from '@angular/common';
-import { Component, Input } from '@angular/core';
+import { Component, EventEmitter, Input, Output } from '@angular/core';
 import { FormControl, ReactiveFormsModule } from '@angular/forms';
 
 @Component({
@@ -46,6 +46,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms';
           [attr.disabled]="disabled ? true : null"
           [attr.data-test-id]="(label | convertForDataTest) + '-checkbox-editor'"
           [attr.aria-describedby]="ariaDescribedBy"
+          (change)="inputChangeHandler($event)"
         />
         <label class="leading-5 text-text" [attr.for]="inputId">{{ label }}</label>
         <svg
@@ -71,4 +72,10 @@ export class CheckboxComponent {
   @Input() disabled: boolean = false;
   @Input() hasError: boolean = false;
   @Input() ariaDescribedBy: string = EMPTY_STRING;
+
+  @Output() inputChange = new EventEmitter<boolean | undefined>();
+
+  inputChangeHandler(event: Event) {
+    this.inputChange.emit((event.target as HTMLInputElement).checked);
+  }
 }
-- 
GitLab