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 || " "}} - </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 || " "}} + </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