From ed6e88dd2d9f768185c06d0db253a3db168d7d04 Mon Sep 17 00:00:00 2001
From: paf <paf@titelfrei.de>
Date: Fri, 26 Mar 2021 10:52:29 +0100
Subject: [PATCH] adds the "always affect all sessions" "mode", which is
 available if only booklet species is present

---
 .../group-monitor.component.html              |  74 ++--
 .../group-monitor.component.spec.ts           |  13 +-
 .../group-monitor/group-monitor.component.ts  |  36 +-
 .../group-monitor/group-monitor.interfaces.ts |   6 +
 src/app/group-monitor/group-monitor.module.ts |   2 +
 .../group-monitor.service.spec.ts             |   4 +
 .../group-monitor/group-monitor.service.ts    |  92 +++--
 .../test-view/test-view.component.html        | 378 +++++++++---------
 8 files changed, 348 insertions(+), 257 deletions(-)

diff --git a/src/app/group-monitor/group-monitor.component.html b/src/app/group-monitor/group-monitor.component.html
index efb19f41..d1ee5253 100644
--- a/src/app/group-monitor/group-monitor.component.html
+++ b/src/app/group-monitor/group-monitor.component.html
@@ -86,13 +86,18 @@
       <h2>{{'Test-Steuerung' | customtext:'gm_controls' | async}}</h2>
 
       <div class="selection-info" *ngIf="gms.sessionsStats$ | async as sessionsStats">
-        <div *ngIf="sessionsStats.differentBookletSpecies > 1">
+        <div *ngIf="sessionsStats.differentBookletSpecies > 1; else: singleBookletSpecies">
           {{
-          'Die verwendeten Booklets sind zu unterschiedlich um gemeinsam benutzt zu werden.'
+          'Die verwendeten Booklets sind zu unterschiedlich, um gemeinsam benutzt zu werden.'
             | customtext:'gm_multiple_booklet_species_warning'
             | async
           }}
         </div>
+        <ng-template #singleBookletSpecies>
+          <mat-slide-toggle color="accent" (change)="toggleAlwaysCheckAll($event)" [disabled]="!gms.checkingOptions.manualCheckingOnly">
+            Einzelauswahl der Sitzungen
+          </mat-slide-toggle>
+        </ng-template>
       </div>
 
       <div class="selection-info" *ngIf="gms.checkedStats$ | async as checkedStats">
@@ -162,6 +167,7 @@
         <div *ngFor="let warning of warnings | keyvalue" class="alert-warning">
           <mat-icon>warning</mat-icon>{{warning.value.text}}
         </div>
+        <pre>{{displayOptions | json}}</pre>
         <pre>{{permissions | json}}</pre>
         <pre>{{selectedElement | json}}</pre>
       </div>
@@ -203,39 +209,39 @@
 
         <div class="test-view-table-wrapper">
           <table class="test-view-table" matSort (matSortChange)="setTableSorting($event)">
-
             <thead>
-            <tr class="mat-sort-container">
-              <td mat-sort-header="_checked">
-                <mat-checkbox
-                    (click)="$event.stopPropagation()"
-                    (change)="gms.toggleCheckAll($event)"
-                    [checked]="false"
-                    (contextmenu)="gms.invertChecked($event)"
-                ></mat-checkbox> <!-- TODO cheked!! -->
-              </td>
-              <td mat-sort-header="_superState">
-                <mat-icon>person</mat-icon>
-              </td>
-              <td mat-sort-header="groupLabel" *ngIf="displayOptions.groupColumn === 'show'">
-                {{'Gruppe' | customtext:'gm_col_group' | async}}
-              </td>
-              <td mat-sort-header="personLabel">
-                {{'Teilnehmer' | customtext:'gm_col_person' | async}}
-              </td>
-              <td mat-sort-header="bookletName" *ngIf="displayOptions.bookletColumn === 'show'">
-                {{'Testheft' | customtext:'gm_col_booklet' | async}}
-              </td>
-              <td mat-sort-header="_currentBlock" *ngIf="displayOptions.blockColumn === 'show'">
-                {{'Block' | customtext:'gm_col_testlet' | async}}
-              </td>
-              <td mat-sort-header="timestamp">
-                {{'Aktivität' | customtext:'gm_col_activity' | async}}
-              </td>
-              <td mat-sort-header="_currentUnit" *ngIf="displayOptions.unitColumn === 'show'">
-                {{'Aufgabe' | customtext:'gm_col_unit' | async}}
-              </td>
-            </tr>
+              <tr class="mat-sort-container">
+                <td mat-sort-header="_checked" *ngIf="displayOptions.manualChecking">
+                  <mat-checkbox
+                      *ngIf="gms.checkedStats$ | async as checkedStats"
+                      (click)="$event.stopPropagation()"
+                      (change)="toggleCheckAll($event)"
+                      [checked]="checkedStats.all"
+                      (contextmenu)="invertChecked($event)"
+                  ></mat-checkbox> <!-- TODO cheked!! -->
+                </td>
+                <td mat-sort-header="_superState">
+                  <mat-icon>person</mat-icon>
+                </td>
+                <td mat-sort-header="groupLabel" *ngIf="displayOptions.groupColumn === 'show'">
+                  {{'Gruppe' | customtext:'gm_col_group' | async}}
+                </td>
+                <td mat-sort-header="personLabel">
+                  {{'Teilnehmer' | customtext:'gm_col_person' | async}}
+                </td>
+                <td mat-sort-header="bookletName" *ngIf="displayOptions.bookletColumn === 'show'">
+                  {{'Testheft' | customtext:'gm_col_booklet' | async}}
+                </td>
+                <td mat-sort-header="_currentBlock" *ngIf="displayOptions.blockColumn === 'show'">
+                  {{'Block' | customtext:'gm_col_testlet' | async}}
+                </td>
+                <td mat-sort-header="timestamp">
+                  {{'Aktivität' | customtext:'gm_col_activity' | async}}
+                </td>
+                <td mat-sort-header="_currentUnit" *ngIf="displayOptions.unitColumn === 'show'">
+                  {{'Aufgabe' | customtext:'gm_col_unit' | async}}
+                </td>
+              </tr>
             </thead>
 
             <ng-container *ngFor="let session of gms.sessions$ | async; trackBy: trackSession">
diff --git a/src/app/group-monitor/group-monitor.component.spec.ts b/src/app/group-monitor/group-monitor.component.spec.ts
index d04d0c7b..c41cd637 100644
--- a/src/app/group-monitor/group-monitor.component.spec.ts
+++ b/src/app/group-monitor/group-monitor.component.spec.ts
@@ -15,6 +15,7 @@ import { CustomtextPipe } from 'iqb-components';
 import { Pipe } from '@angular/core';
 import { GroupMonitorComponent } from './group-monitor.component';
 import {
+  CheckingOptions,
   GroupData, TestSession,
   TestSessionData, TestSessionSetStats
 } from './group-monitor.interfaces';
@@ -47,20 +48,18 @@ class MockBackendService {
 }
 
 class MockGroupMonitorService {
-  sessionsStats$ = new BehaviorSubject<TestSessionSetStats>(unitTestSessionsStats);
+  checkingOptions: CheckingOptions = {
+    manualCheckingOnly: false,
+    autoCheckAll: true
+  };
 
+  sessionsStats$ = new BehaviorSubject<TestSessionSetStats>(unitTestSessionsStats);
   checkedStats$ = new BehaviorSubject<TestSessionSetStats>(unitTestCheckedStats);
-
-  // checkedStats = unitTestCheckedStats;
-
   sessions$ = new BehaviorSubject<TestSession[]>(unitTestExampleSessions);
-
   sessions = unitTestExampleSessions;
-
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   connect = (_: string) => {};
   disconnect = () => {};
-
   isChecked = () => false;
 }
 
diff --git a/src/app/group-monitor/group-monitor.component.ts b/src/app/group-monitor/group-monitor.component.ts
index 1b28199c..87cce86e 100644
--- a/src/app/group-monitor/group-monitor.component.ts
+++ b/src/app/group-monitor/group-monitor.component.ts
@@ -8,6 +8,8 @@ import { Observable, Subscription } from 'rxjs';
 
 import { MatDialog } from '@angular/material/dialog';
 import { ConfirmDialogComponent, ConfirmDialogData } from 'iqb-components';
+import { MatSlideToggleChange } from '@angular/material/slide-toggle';
+import { MatCheckboxChange } from '@angular/material/checkbox';
 import { BackendService } from './backend.service';
 import {
   GroupData,
@@ -45,7 +47,8 @@ export class GroupMonitorComponent implements OnInit, OnDestroy {
     bookletColumn: 'show',
     blockColumn: 'show',
     unitColumn: 'hide',
-    highlightSpecies: false
+    highlightSpecies: false,
+    manualChecking: false
   };
 
   permissions: {
@@ -89,6 +92,10 @@ export class GroupMonitorComponent implements OnInit, OnDestroy {
 
   private onSessionsUpdate(stats: TestSessionSetStats): void {
     this.displayOptions.highlightSpecies = (stats.differentBookletSpecies > 1);
+
+    if (!this.gms.checkingOptions.manualCheckingOnly) {
+      this.displayOptions.manualChecking = true;
+    }
   }
 
   private onCheckedChange(stats: TestSessionSetStats): void {
@@ -203,7 +210,12 @@ export class GroupMonitorComponent implements OnInit, OnDestroy {
     } else {
       this.gms.uncheckSession(session);
     }
-    this.gms.onCheckedChanged();
+  }
+
+  invertChecked(event: Event): boolean {
+    event.preventDefault();
+    this.gms.invertChecked();
+    return false;
   }
 
   private addWarning(key, text): void {
@@ -215,4 +227,24 @@ export class GroupMonitorComponent implements OnInit, OnDestroy {
       timeout: window.setTimeout(() => delete this.warnings[key], 30000)
     };
   }
+
+  toggleAlwaysCheckAll(event: MatSlideToggleChange): void {
+    if (this.gms.checkingOptions.manualCheckingOnly && !event.checked) {
+      this.gms.checkAll();
+      this.displayOptions.manualChecking = false;
+      this.gms.checkingOptions.autoCheckAll = true;
+    } else {
+      this.gms.checkNone();
+      this.displayOptions.manualChecking = true;
+      this.gms.checkingOptions.autoCheckAll = false;
+    }
+  }
+
+  toggleCheckAll(event: MatCheckboxChange): void {
+    if (event.checked) {
+      this.gms.checkAll();
+    } else {
+      this.gms.checkNone();
+    }
+  }
 }
diff --git a/src/app/group-monitor/group-monitor.interfaces.ts b/src/app/group-monitor/group-monitor.interfaces.ts
index bdc2b74e..97783ede 100644
--- a/src/app/group-monitor/group-monitor.interfaces.ts
+++ b/src/app/group-monitor/group-monitor.interfaces.ts
@@ -107,6 +107,12 @@ export interface TestViewDisplayOptions {
   groupColumn: 'show' | 'hide';
   bookletColumn: 'show' | 'hide';
   highlightSpecies: boolean;
+  manualChecking: boolean;
+}
+
+export interface CheckingOptions {
+  manualCheckingOnly: boolean;
+  autoCheckAll: boolean;
 }
 
 export function isUnit(testletOrUnit: Testlet|Unit): testletOrUnit is Unit {
diff --git a/src/app/group-monitor/group-monitor.module.ts b/src/app/group-monitor/group-monitor.module.ts
index 1a58add1..ffb45772 100644
--- a/src/app/group-monitor/group-monitor.module.ts
+++ b/src/app/group-monitor/group-monitor.module.ts
@@ -16,6 +16,7 @@ import { MatChipsModule } from '@angular/material/chips';
 import { CdkTableModule } from '@angular/cdk/table';
 
 import { IqbComponentsModule } from 'iqb-components';
+import { MatSlideToggleModule } from '@angular/material/slide-toggle';
 import { GroupMonitorRoutingModule } from './group-monitor-routing.module';
 import { GroupMonitorComponent } from './group-monitor.component';
 import { BackendService } from './backend.service';
@@ -46,6 +47,7 @@ import { GroupMonitorService } from './group-monitor.service';
     FormsModule,
     MatSidenavModule,
     MatCheckboxModule,
+    MatSlideToggleModule,
     IqbComponentsModule
   ],
   providers: [
diff --git a/src/app/group-monitor/group-monitor.service.spec.ts b/src/app/group-monitor/group-monitor.service.spec.ts
index e126bc0e..6f3e353d 100644
--- a/src/app/group-monitor/group-monitor.service.spec.ts
+++ b/src/app/group-monitor/group-monitor.service.spec.ts
@@ -112,6 +112,8 @@ describe('GroupMonitorService', () => {
     it('should sort by checked', () => {
       const exampleSession1 = unitTestExampleSessions[1];
       // sortSession does not only return sorted array, but sorts original array in-play as js function sort does
+      service.checkingOptions.autoCheckAll = false; // TODO if replaceCheckedSessions was not private, this
+      service.checkNone(); // could be written more straigtforward
       service.checkSession(exampleSession1);
       const sorted = service.sortSessions({ active: '_checked', direction: 'asc' }, unitTestExampleSessions);
       expect(sorted[0].id).toEqual(exampleSession1.id);
@@ -119,6 +121,8 @@ describe('GroupMonitorService', () => {
 
     it('should sort by checked reverse', () => {
       const exampleSession1 = unitTestExampleSessions[1];
+      service.checkingOptions.autoCheckAll = false;
+      service.checkNone();
       service.checkSession(exampleSession1);
       const sorted = service.sortSessions({ active: '_checked', direction: 'desc' }, unitTestExampleSessions);
       expect(sorted[2].id).toEqual(exampleSession1.id);
diff --git a/src/app/group-monitor/group-monitor.service.ts b/src/app/group-monitor/group-monitor.service.ts
index 211d86d3..786ce12d 100644
--- a/src/app/group-monitor/group-monitor.service.ts
+++ b/src/app/group-monitor/group-monitor.service.ts
@@ -4,32 +4,37 @@ import {
 } from 'rxjs';
 import { Sort } from '@angular/material/sort';
 import { map, switchMap, tap } from 'rxjs/operators';
-import { MatCheckboxChange } from '@angular/material/checkbox';
 import { BackendService } from './backend.service';
 import { BookletService } from './booklet.service';
 import { TestSessionService } from './test-session.service';
 import {
   isBooklet,
-  Selection,
+  Selection, CheckingOptions,
   TestSession,
   TestSessionFilter, TestSessionSetStats,
   TestSessionsSuperStates
 } from './group-monitor.interfaces';
 
 /**
- * fragen:
+ * func:
+ * - checkAll
+ * - automatisch den nächsten wählen (?)
+ * - problem beim markieren
+ * tidy:
  * - was geben die commands zurück?
  * - wie wird alles reseted?
- * # is*alloweed sollte on checkedChanges ermittelt werden
- * # sollte _checked ein observable sein? (hint: ja)
+ * test
+ * polish:
  * - naming
- * - checkAll
+ * - tests
  */
 
 @Injectable()
 export class GroupMonitorService {
   sortBy$: Subject<Sort>;
   filters$: Subject<TestSessionFilter[]>;
+  checkingOptions: CheckingOptions;
+
   private groupName: string;
 
   get sessions$(): Observable<TestSession[]> {
@@ -88,6 +93,10 @@ export class GroupMonitorService {
     this.groupName = groupName;
     this.sortBy$ = new BehaviorSubject<Sort>({ direction: 'asc', active: 'personLabel' });
     this.filters$ = new BehaviorSubject<TestSessionFilter[]>([]);
+    this.checkingOptions = {
+      manualCheckingOnly: true,
+      autoCheckAll: true
+    };
 
     this._checkedStats$ = new BehaviorSubject<TestSessionSetStats>(GroupMonitorService.getEmptyStats());
     this._sessionsStats$ = new BehaviorSubject<TestSessionSetStats>(GroupMonitorService.getEmptyStats());
@@ -106,7 +115,7 @@ export class GroupMonitorService {
       .pipe(
         // eslint-disable-next-line max-len
         map(([sortBy, filters, sessions]) => this.sortSessions(sortBy, GroupMonitorService.filterSessions(sessions, filters))),
-        tap(sessions => this.updateEverything(sessions))
+        tap(sessions => this.synchronizeChecked(sessions))
       )
       .subscribe(this._sessions$);
   }
@@ -170,16 +179,26 @@ export class GroupMonitorService {
     };
   }
 
-  private updateEverything(sessions: TestSession[]): void { // TODo naming
+  private synchronizeChecked(sessions: TestSession[]): void {
+    const sessionsStats = this.getSessionSetStats(sessions);
+
+    this.checkingOptions.manualCheckingOnly = (sessionsStats.differentBookletSpecies < 2);
+
+    if (!this.checkingOptions.manualCheckingOnly) {
+      this.checkingOptions.autoCheckAll = false;
+    }
+
     const newCheckedSessions: { [sessionFullId: number]: TestSession } = {};
     sessions
       .forEach(session => {
-        if (typeof this._checked[session.id] !== 'undefined') {
+        if (this.checkingOptions.autoCheckAll || (typeof this._checked[session.id] !== 'undefined')) {
           newCheckedSessions[session.id] = session;
         }
       });
     this._checked = newCheckedSessions;
-    this._sessionsStats$.next(this.getSessionSetStats(this.sessions));
+
+    this._checkedStats$.next(this.getSessionSetStats(Object.values(this._checked)));
+    this._sessionsStats$.next(sessionsStats);
   }
 
   sortSessions(sort: Sort, sessions: TestSession[]): TestSession[] {
@@ -270,7 +289,14 @@ export class GroupMonitorService {
     return this.bs.unlock(this.groupName, sessionIds);
   }
 
+  isChecked(session: TestSession): boolean {
+    return (typeof this._checked[session.id] !== 'undefined');
+  }
+
   checkSessionsBySelection(selected: Selection): void {
+    if (this.checkingOptions.autoCheckAll) {
+      return;
+    }
     let toCheck: TestSession[] = [];
     if (selected.element) {
       if (!selected.spreading) {
@@ -285,38 +311,46 @@ export class GroupMonitorService {
     this.replaceCheckedSessions(toCheck);
   }
 
-  toggleCheckAll(event: MatCheckboxChange): void {
-    if (event.checked) {
-      this.replaceCheckedSessions(
-        this._sessions$.getValue()
-          .filter(session => session.data.testId && session.data.testId > -1)
-      );
-    } else {
-      this.replaceCheckedSessions([]);
+  invertChecked(): void {
+    if (this.checkingOptions.autoCheckAll) {
+      return;
     }
-  }
-
-  invertChecked(event: Event): boolean { // TODO move back to component
-    event.preventDefault();
     const unChecked = this._sessions$.getValue()
       .filter(session => session.data.testId && session.data.testId > -1)
       .filter(session => !this.isChecked(session));
     this.replaceCheckedSessions(unChecked);
-    return false;
-  }
-
-  isChecked(session: TestSession): boolean {
-    return (typeof this._checked[session.id] !== 'undefined');
   }
 
   checkSession(session: TestSession): void {
+    if (this.checkingOptions.autoCheckAll) {
+      return;
+    }
     this._checked[session.id] = session;
+    this.onCheckedChanged();
   }
 
   uncheckSession(session: TestSession): void {
+    if (this.checkingOptions.autoCheckAll) {
+      return;
+    }
     if (this.isChecked(session)) {
       delete this._checked[session.id];
     }
+    this.onCheckedChanged();
+  }
+
+  checkAll(): void {
+    if (this.checkingOptions.autoCheckAll) {
+      return;
+    }
+    this.replaceCheckedSessions(this._sessions$.getValue());
+  }
+
+  checkNone(): void {
+    if (this.checkingOptions.autoCheckAll) {
+      return;
+    }
+    this.replaceCheckedSessions([]);
   }
 
   private replaceCheckedSessions(sessionsToCheck: TestSession[]): void {
@@ -327,11 +361,11 @@ export class GroupMonitorService {
     this.onCheckedChanged();
   }
 
-  onCheckedChanged(): void {
+  private onCheckedChanged(): void {
     this._checkedStats$.next(this.getSessionSetStats(this.checked));
   }
 
-  getSessionSetStats(sessionSet: TestSession[]): TestSessionSetStats {
+  getSessionSetStats(sessionSet: TestSession[]): TestSessionSetStats { // TODO only private for test
     const booklets = new Set();
     const bookletSpecies = new Set();
     let paused = 0;
diff --git a/src/app/group-monitor/test-view/test-view.component.html b/src/app/group-monitor/test-view/test-view.component.html
index a213106b..61bfbe15 100644
--- a/src/app/group-monitor/test-view/test-view.component.html
+++ b/src/app/group-monitor/test-view/test-view.component.html
@@ -1,29 +1,29 @@
-<td class="selected">
-    <mat-checkbox
-            *ngIf="testSession.data.testId >= 0"
-            (change)="check($event)"
-            (contextmenu)="invertSelection()"
-            [checked]="checked"
-    >
-    </mat-checkbox>
+<td class="selected" *ngIf="displayOptions.manualChecking">
+  <mat-checkbox
+    *ngIf="testSession.data.testId >= 0"
+    (change)="check($event)"
+    (contextmenu)="invertSelection()"
+    [checked]="checked"
+  >
+  </mat-checkbox>
 </td>
 
 <td class="super-state" (click)="deselect($event)" (contextmenu)="deselectForce($event)">
-    <div class="vertical-align-middle" *ngIf="superStateIcons[testSession.state] as iconData">
-        <mat-icon class="unit-badge {{iconData?.class}}" matTooltip="{{iconData.tooltip}}">
-            {{iconData.icon}}
-        </mat-icon>
-    </div>
+  <div class="vertical-align-middle" *ngIf="superStateIcons[testSession.state] as iconData">
+    <mat-icon class="unit-badge {{iconData?.class}}" matTooltip="{{iconData.tooltip}}">
+      {{iconData.icon}}
+    </mat-icon>
+  </div>
 </td>
 
 <td class="group" *ngIf="displayOptions.groupColumn === 'show'" (click)="deselect($event)" (contextmenu)="deselectForce($event)">
-    <div class="vertical-align-middle">{{testSession.data.groupLabel}}</div>
+  <div class="vertical-align-middle">{{testSession.data.groupLabel}}</div>
 </td>
 
 <td class="user" (click)="deselect($event)" (contextmenu)="deselectForce($event)">
-    <div class="vertical-align-middle">
-        <h1>{{testSession.data.personLabel}}</h1>
-    </div>
+  <div class="vertical-align-middle">
+    <h1>{{testSession.data.personLabel}}</h1>
+  </div>
 </td>
 
 <td class="booklet"
@@ -31,208 +31,216 @@
     (click)="deselect($event)"
     (contextmenu)="deselectForce($event)"
 >
-    <ng-container *ngIf="!testSession.booklet.error; else: noBooklet">
-        <div class="vertical-align-middle">{{testSession.booklet.metadata.label}}</div>
-    </ng-container>
-    <ng-template #noBooklet>
-        <div class="vertical-align-middle">{{testSession.data.bookletName}}</div>
-    </ng-template>
+  <ng-container *ngIf="!testSession.booklet.error; else: noBooklet">
+    <div class="vertical-align-middle">{{testSession.booklet.metadata.label}}</div>
+  </ng-container>
+  <ng-template #noBooklet>
+    <div class="vertical-align-middle">{{testSession.data.bookletName}}</div>
+  </ng-template>
 </td>
 
 <td class="block" (click)="deselect($event)" (contextmenu)="deselectForce($event)" *ngIf="displayOptions.blockColumn === 'show'">
-    <div *ngIf="testSession.current as current;" class="vertical-align-middle">
-        {{current.parent.label || (current.parentIndexGlobal ? blockName(current.parentIndexGlobal) : '')}}
-        <mat-icon class="unit-badge"
-                  *ngIf="testSession.timeLeft && (testSession.timeLeft[current.parent.id] !== undefined)"
-                  matBadge="{{testSession.timeLeft[current.parent.id].toString()}}"
-                  matBadgeColor="accent"
-                  matTooltip="Verbleibende Zeit"
-        >schedule
-        </mat-icon>
-    </div>
+  <div *ngIf="testSession.current as current;" class="vertical-align-middle">
+    {{current.parent.label || (current.parentIndexGlobal ? blockName(current.parentIndexGlobal) : '')}}
+    <mat-icon
+      class="unit-badge"
+      *ngIf="testSession.timeLeft && (testSession.timeLeft[current.parent.id] !== undefined)"
+      matBadge="{{testSession.timeLeft[current.parent.id].toString()}}"
+      matBadgeColor="accent"
+      matTooltip="Verbleibende Zeit"
+    >schedule
+    </mat-icon>
+  </div>
 </td>
 
 <td class="test" (click)="deselect($event)" (contextmenu)="deselectForce($event)">
-    <ng-container *ngIf="!testSession.booklet['error']; else: noBookletReason">
-
-        <div *ngIf="testSession.booklet.units as testlet"
-             class="units-container"
-             [class]="{
-                locked: hasState(testSession.data.testState, 'status', 'locked'),
-                paused: hasState(testSession.data.testState, 'CONTROLLER', 'PAUSED'),
-                error: hasState(testSession.data.testState, 'CONTROLLER', 'ERROR'),
-                pending: !hasState(testSession.data.testState, 'CONTROLLER')
-             }"
-             [ngSwitch]="displayOptions.view"
-             (mouseleave)="mark()"
-             (click)="deselect($event)"
-        >
-            <div class="units full" *ngSwitchCase="'full'" >
-                <ng-container *ngTemplateOutlet="testletFull; context: {$implicit: testlet}"></ng-container>
-            </div>
-
-            <div class="units medium" *ngSwitchCase="'medium'" >
-                <ng-container *ngTemplateOutlet="bookletMedium; context: {$implicit: testlet}"></ng-container>
-            </div>
-
-            <div class="units small" *ngSwitchCase="'small'" >
-                <ng-container *ngTemplateOutlet="bookletSmall; context: {$implicit: testlet}"></ng-container>
-            </div>
-        </div>
-    </ng-container>
-
-    <ng-template #noBookletReason>
-        <span *ngIf="testSession.booklet.error == 'missing-id'">Kein Testheft zugeordnet</span>
-        <span *ngIf="testSession.booklet.error == 'missing-file'" class="warning">Kein Zugriff auf Testheft-Datei!</span>
-        <span *ngIf="testSession.booklet.error == 'xml'" class="warning">Konnte Testheft-Datei nicht lesen!</span>
-        <span *ngIf="testSession.booklet.error == 'general'" class="warning">Fehler beim Zugriff aus Testheft-Datei!</span>
-    </ng-template>
-</td>
+  <ng-container *ngIf="!testSession.booklet['error']; else: noBookletReason">
+
+    <div
+      *ngIf="testSession.booklet.units as testlet"
+      class="units-container"
+      [class]="{
+        locked: hasState(testSession.data.testState, 'status', 'locked'),
+        paused: hasState(testSession.data.testState, 'CONTROLLER', 'PAUSED'),
+        error: hasState(testSession.data.testState, 'CONTROLLER', 'ERROR'),
+        pending: !hasState(testSession.data.testState, 'CONTROLLER')
+      }"
+      [ngSwitch]="displayOptions.view"
+      (mouseleave)="mark()"
+      (click)="deselect($event)"
+    >
+      <div class="units full" *ngSwitchCase="'full'" >
+        <ng-container *ngTemplateOutlet="testletFull; context: {$implicit: testlet}"></ng-container>
+      </div>
 
+      <div class="units medium" *ngSwitchCase="'medium'" >
+        <ng-container *ngTemplateOutlet="bookletMedium; context: {$implicit: testlet}"></ng-container>
+      </div>
 
-<ng-template #testletFull let-testlet>
+      <div class="units small" *ngSwitchCase="'small'" >
+        <ng-container *ngTemplateOutlet="bookletSmall; context: {$implicit: testlet}"></ng-container>
+      </div>
+    </div>
+  </ng-container>
+
+  <ng-template #noBookletReason>
+    <span *ngIf="testSession.booklet.error == 'missing-id'">Kein Testheft zugeordnet</span>
+    <span *ngIf="testSession.booklet.error == 'missing-file'" class="warning">Kein Zugriff auf Testheft-Datei!</span>
+    <span *ngIf="testSession.booklet.error == 'xml'" class="warning">Konnte Testheft-Datei nicht lesen!</span>
+    <span *ngIf="testSession.booklet.error == 'general'" class="warning">Fehler beim Zugriff aus Testheft-Datei!</span>
+  </ng-template>
+</td>
 
-    <span *ngIf="testlet.restrictions && testlet.restrictions.codeToEnter as codeToEnter"
-          class="unit restriction"
-          matTooltip="Freigabewort: {{codeToEnter.code.toUpperCase()}}"
-          matTooltipPosition="above"
-        >
-        <mat-icon>{{testSession.clearedCodes && (testSession.clearedCodes.indexOf(testlet.id) > -1) ? 'lock_open' : 'lock'}}</mat-icon>
-    </span>
 
-    <ng-container *ngFor="let testletOrUnit of testlet.children; trackBy: trackUnits" [ngSwitch]="getTestletType(testletOrUnit)">
+<ng-template #testletFull let-testlet>
 
-        <span *ngSwitchCase="'unit'"
-              [class]="{
-                unit: true,
-                current: testSession.data.unitName === testletOrUnit.id
-              }"
-              matTooltip="{{testletOrUnit.label}}"
-              matTooltipPosition="above"
-            >{{testletOrUnit.labelShort || "&nbsp;"}}
-        </span>
+  <span
+    *ngIf="testlet.restrictions && testlet.restrictions.codeToEnter as codeToEnter"
+    class="unit restriction"
+    matTooltip="Freigabewort: {{codeToEnter.code.toUpperCase()}}"
+    matTooltipPosition="above"
+  >
+    <mat-icon>{{testSession.clearedCodes && (testSession.clearedCodes.indexOf(testlet.id) > -1) ? 'lock_open' : 'lock'}}</mat-icon>
+  </span>
 
-        <span *ngSwitchCase="'testlet'"
-              [class]="{
-                testlet: true,
-                selected: isSelected(testletOrUnit) && checked,
-                marked: isMarked(testletOrUnit)
-              }"
-              (mouseenter)="mark(testletOrUnit)"
-              (click)="select($event, testletOrUnit)"
-              matTooltip="{{testletOrUnit.label}}"
-            >
-            <ng-container *ngTemplateOutlet="testletFull; context: {$implicit: testletOrUnit}"></ng-container>
-        </span>
+  <ng-container *ngFor="let testletOrUnit of testlet.children; trackBy: trackUnits" [ngSwitch]="getTestletType(testletOrUnit)">
 
-    </ng-container>
+    <span
+      *ngSwitchCase="'unit'"
+      [class]="{
+        unit: true,
+        current: testSession.data.unitName === testletOrUnit.id
+      }"
+      matTooltip="{{testletOrUnit.label}}"
+      matTooltipPosition="above"
+    >
+      {{testletOrUnit.labelShort || "&nbsp;"}}
+    </span>
 
+    <span *ngSwitchCase="'testlet'"
+      [class]="{
+        testlet: true,
+        selected: isSelected(testletOrUnit) && checked,
+        marked: isMarked(testletOrUnit)
+      }"
+      (mouseenter)="mark(testletOrUnit)"
+      (click)="select($event, testletOrUnit)"
+      matTooltip="{{testletOrUnit.label}}"
+    >
+      <ng-container *ngTemplateOutlet="testletFull; context: {$implicit: testletOrUnit}"></ng-container>
+    </span>
+  </ng-container>
 </ng-template>
 
 
 <ng-template #bookletMedium let-testlet>
-    <ng-container *ngTemplateOutlet="testletTemplateMedium; context: {testlet: testlet}">
-    </ng-container>
+  <ng-container *ngTemplateOutlet="testletTemplateMedium; context: {testlet: testlet}">
+  </ng-container>
 </ng-template>
 
 
 <ng-template #testletTemplateMedium let-testlet="testlet">
 
-    <ng-container *ngFor="let testletOrUnit of testlet.children; let i = index; trackBy: trackUnits" [ngSwitch]="getTestletType(testletOrUnit)">
+  <ng-container *ngFor="let testletOrUnit of testlet.children; let i = index; trackBy: trackUnits" [ngSwitch]="getTestletType(testletOrUnit)">
 
-        <span *ngSwitchCase="'unit'"
-              [class]="(testSession.data.unitName === testletOrUnit.id) ? 'unit current': 'unit'"
-              matTooltip="{{testletOrUnit.label}}"
-              matTooltipPosition="above"
-        >·
-        </span>
+    <span *ngSwitchCase="'unit'"
+      [class]="(testSession.data.unitName === testletOrUnit.id) ? 'unit current': 'unit'"
+      matTooltip="{{testletOrUnit.label}}"
+      matTooltipPosition="above"
+    >·
+    </span>
+
+    <span *ngSwitchCase="'testlet'" class="testlet" matTooltip="{{testletOrUnit.label}}">
+
+      <span
+        *ngIf="testletOrUnit.restrictions && testletOrUnit.restrictions.codeToEnter as codeToEnter"
+        class="unit restriction"
+        matTooltip="Freigabewort: {{codeToEnter.code.toUpperCase()}}"
+        matTooltipPosition="above"
+      >
+        <mat-icon>
+          {{testSession.clearedCodes && (testSession.clearedCodes.indexOf(testletOrUnit.id) > -1) ? 'lock_open' : 'lock'}}
+        </mat-icon>
+      </span>
 
-        <span *ngSwitchCase="'testlet'" class="testlet" matTooltip="{{testletOrUnit.label}}">
-
-            <span *ngIf="testletOrUnit.restrictions && testletOrUnit.restrictions.codeToEnter as codeToEnter"
-                  class="unit restriction"
-                  matTooltip="Freigabewort: {{codeToEnter.code.toUpperCase()}}"
-                  matTooltipPosition="above"
-            >
-                <mat-icon>{{testSession.clearedCodes && (testSession.clearedCodes.indexOf(testletOrUnit.id) > -1) ? 'lock_open' : 'lock'}}</mat-icon>
-            </span>
-
-            <ng-container *ngIf="testSession.current; else: unFeaturedTestlet">
-                <span
-                        *ngIf="testSession.current.ancestor.id === testletOrUnit.id; else: unFeaturedTestlet"
-                        [class]="{
-                            unit: true,
-                            aggregated: true,
-                            current: true,
-                            selected: isSelected(testletOrUnit) && checked,
-                            marked: isMarked(testletOrUnit)
-                        }"
-                        matTooltip="{{testSession.current.unit.label}}"
-                        matTooltipPosition="above"
-                        (mouseenter)="mark(testletOrUnit)"
-                        (click)="select($event, testletOrUnit)"
-                >{{testSession.current.indexAncestor + 1}} / {{testSession.current.unitCountAncestor}}
-                </span>
-            </ng-container>
-
-            <ng-template #unFeaturedTestlet>
-                <span
-                        [class]="{
-                            unit: true,
-                            aggregated: true,
-                            selected: isSelected(testletOrUnit) && checked,
-                            marked: isMarked(testletOrUnit)
-                        }"
-                        (mouseenter)="mark(testletOrUnit)"
-                        (click)="select($event, testletOrUnit)"
-                >{{testletOrUnit.descendantCount}}</span>
-            </ng-template>
+      <ng-container *ngIf="testSession.current; else: unFeaturedTestlet">
+        <span
+          *ngIf="testSession.current.ancestor.id === testletOrUnit.id; else: unFeaturedTestlet"
+          [class]="{
+            unit: true,
+            aggregated: true,
+            current: true,
+            selected: isSelected(testletOrUnit) && checked,
+            marked: isMarked(testletOrUnit)
+          }"
+          matTooltip="{{testSession.current.unit.label}}"
+          matTooltipPosition="above"
+          (mouseenter)="mark(testletOrUnit)"
+          (click)="select($event, testletOrUnit)"
+        >
+          {{testSession.current.indexAncestor + 1}} / {{testSession.current.unitCountAncestor}}
         </span>
-    </ng-container>
+      </ng-container>
+
+      <ng-template #unFeaturedTestlet>
+        <span
+          [class]="{
+            unit: true,
+            aggregated: true,
+            selected: isSelected(testletOrUnit) && checked,
+            marked: isMarked(testletOrUnit)
+          }"
+          (mouseenter)="mark(testletOrUnit)"
+          (click)="select($event, testletOrUnit)"
+        >{{testletOrUnit.descendantCount}}</span>
+      </ng-template>
+  </span>
+  </ng-container>
 </ng-template>
 
 
 <ng-template #bookletSmall let-testlet>
+  <span
+    class="testlet" *ngIf="testSession.current; else: unFeaturedTestlet"
+    matTooltip="{{testSession.current.parent?.label}}"
+  >
     <span
-            class="testlet" *ngIf="testSession.current; else: unFeaturedTestlet"
-            matTooltip="{{testSession.current.parent?.label}}"
-        >
-        <span
-                class="unit current aggregated"
-                matTooltip="{{testSession.current.unit.label}}"
-                matTooltipPosition="above"
-            >
-
-            {{testSession.current.indexGlobal + 1}} / {{testSession.current.unitCountGlobal}}
-        </span>
+      class="unit current aggregated"
+      matTooltip="{{testSession.current.unit.label}}"
+      matTooltipPosition="above"
+    >
+      {{testSession.current.indexGlobal + 1}} / {{testSession.current.unitCountGlobal}}
     </span>
+  </span>
 
-    <ng-template #unFeaturedTestlet>
-        <span class="testlet" >
-            <span class="unit aggregated">{{testlet.descendantCount}}</span>
-        </span>
-    </ng-template>
+  <ng-template #unFeaturedTestlet>
+    <span class="testlet" >
+      <span class="unit aggregated">{{testlet.descendantCount}}</span>
+    </span>
+  </ng-template>
 </ng-template>
 
 <td class="current-unit" (click)="deselect($event)" (contextmenu)="deselectForce($event)" *ngIf="displayOptions.unitColumn === 'show'">
-    <div *ngIf="testSession.current as current;" class="vertical-align-middle">
-        <h2 matTooltip="{{current.unit.label}}">{{current.unit.labelShort || current.unit.id}}</h2>
-        <mat-icon class="unit-badge"
-              *ngIf="hasState(testSession.data.unitState, 'PRESENTATION_PROGRESS', 'complete')"
-              matTooltip="Vollständig betrachtet / angehört"
-            >remove_red_eye
-        </mat-icon>
-        <mat-icon class="unit-badge"
-              *ngIf="hasState(testSession.data.unitState, 'RESPONSE_PROGRESS', 'complete')"
-              matTooltip="Fertig beantwortet"
-            >done_all
-        </mat-icon>
-        <mat-icon class="unit-badge"
-              *ngIf="hasState(testSession.data.unitState, 'CURRENT_PAGE_NR')"
-              matBadge="{{this.stateString(testSession.data.unitState, ['CURRENT_PAGE_NR', 'PAGES_COUNT'], '/')}}"
-              matBadgeColor="accent"
-              matTooltip="{{this.stateString(testSession.data.unitState, ['CURRENT_PAGE_ID'])}}"
-            >description
-        </mat-icon>
-    </div>
+  <div *ngIf="testSession.current as current;" class="vertical-align-middle">
+    <h2 matTooltip="{{current.unit.label}}">{{current.unit.labelShort || current.unit.id}}</h2>
+    <mat-icon
+      class="unit-badge"
+      *ngIf="hasState(testSession.data.unitState, 'PRESENTATION_PROGRESS', 'complete')"
+      matTooltip="Vollständig betrachtet / angehört"
+    >remove_red_eye
+    </mat-icon>
+    <mat-icon
+      class="unit-badge"
+      *ngIf="hasState(testSession.data.unitState, 'RESPONSE_PROGRESS', 'complete')"
+      matTooltip="Fertig beantwortet"
+    >done_all
+    </mat-icon>
+    <mat-icon class="unit-badge"
+      *ngIf="hasState(testSession.data.unitState, 'CURRENT_PAGE_NR')"
+      matBadge="{{this.stateString(testSession.data.unitState, ['CURRENT_PAGE_NR', 'PAGES_COUNT'], '/')}}"
+      matBadgeColor="accent"
+      matTooltip="{{this.stateString(testSession.data.unitState, ['CURRENT_PAGE_ID'])}}"
+    >description
+    </mat-icon>
+  </div>
 </td>
-- 
GitLab