File

src/app/test-controller/components/test-controller/test-controller.component.ts

Implements

OnInit OnDestroy

Metadata

styleUrls ./test-controller.component.css
templateUrl ./test-controller.component.html

Index

Properties
Methods
HostListeners

Constructor

constructor(appVersion: string, isProductionMode: boolean, mds: MainDataService, tcs: TestControllerService, bs: BackendService, reviewDialog: MatDialog, snackBar: MatSnackBar, router: Router, route: ActivatedRoute, cts: CustomtextService, cmd: CommandService, tls: TestLoaderService)
Parameters :
Name Type Optional
appVersion string No
isProductionMode boolean No
mds MainDataService No
tcs TestControllerService No
bs BackendService No
reviewDialog MatDialog No
snackBar MatSnackBar No
router Router No
route ActivatedRoute No
cts CustomtextService No
cmd CommandService No
tls TestLoaderService No

HostListeners

window:unload
Arguments : '$event'
window:unload()

Methods

handleCommand
handleCommand(commandName: string, params: string[])
Parameters :
Name Type Optional
commandName string No
params string[] No
Returns : void
Private handleMaxTimer
handleMaxTimer(maxTimerData: MaxTimerData)
Parameters :
Name Type Optional
maxTimerData MaxTimerData No
Returns : void
ngOnDestroy
ngOnDestroy()
Returns : void
ngOnInit
ngOnInit()
Returns : void
Private refreshUnitMenu
refreshUnitMenu()
Returns : void
Private setUnitScreenHeader
setUnitScreenHeader()
Returns : void
showReviewDialog
showReviewDialog()
Returns : void
Private startAppFocusLogging
startAppFocusLogging()
Returns : void
Private startConnectionStatusLogging
startConnectionStatusLogging()
Returns : void
toggleSidebar
toggleSidebar()
Returns : void
unloadHandler
unloadHandler()
Decorators :
@HostListener('window:unload', ['$event'])
Returns : void

Properties

Public appVersion
Type : string
Decorators :
@Inject('APP_VERSION')
Public cmd
Type : CommandService
debugPane
Default value : false
Public isProductionMode
Type : boolean
Decorators :
@Inject('IS_PRODUCTION_MODE')
Private logTestControllerStatusChange
Default value : () => {...}
Public mds
Type : MainDataService
navButtons
Type : ElementRef
Decorators :
@ViewChild('navButtons')
sidebarOpen
Default value : false
Private subscriptions
Type : literal type
Default value : { errorReporting: null, testStatus: null, routing: null, appWindowHasFocus: null, appFocus: null, command: null, maxTimer: null, connectionStatus: null }
Public tcs
Type : TestControllerService
Private timerRunning
Default value : false
timerValue
Type : MaxTimerData
Default value : null
unitNavigationList
Type : UnitNaviButtonData[]
Default value : []
unitNavigationTarget
Default value : UnitNavigationTarget
unitScreenHeader
Type : string
Default value : ''
import { ActivatedRoute, Router } from '@angular/router';
import {
  Component, ElementRef, HostListener, Inject, OnDestroy, OnInit, ViewChild
} from '@angular/core';
import {
  Subscription
} from 'rxjs';
import {
  debounceTime, distinctUntilChanged, filter, map
} from 'rxjs/operators';
import { CustomtextService } from 'iqb-components';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import {
  AppFocusState,
  Command, MaxTimerDataType,
  ReviewDialogData,
  StateReportEntry,
  TestControllerState,
  TestStateKey, UnitNaviButtonData,
  UnitNavigationTarget,
  WindowFocusState
} from '../../interfaces/test-controller.interfaces';
import { BackendService } from '../../services/backend.service';
import { MainDataService } from '../../../maindata.service';
import { TestControllerService } from '../../services/test-controller.service';
import { ReviewDialogComponent } from '../review-dialog/review-dialog.component';
import { CommandService } from '../../services/command.service';
import { TestLoaderService } from '../../services/test-loader.service';
import { MaxTimerData } from '../../classes/test-controller.classes';
import { ApiError } from '../../../app.interfaces';

@Component({
  templateUrl: './test-controller.component.html',
  styleUrls: ['./test-controller.component.css']
})
export class TestControllerComponent implements OnInit, OnDestroy {
  private subscriptions: { [key: string]: Subscription | null } = {
    errorReporting: null,
    testStatus: null,
    routing: null,
    appWindowHasFocus: null,
    appFocus: null,
    command: null,
    maxTimer: null,
    connectionStatus: null
  };

  private timerRunning = false;

  timerValue: MaxTimerData = null;
  unitNavigationTarget = UnitNavigationTarget;
  unitNavigationList: UnitNaviButtonData[] = [];
  debugPane = false;
  sidebarOpen = false;
  unitScreenHeader: string = '';

  @ViewChild('navButtons') navButtons: ElementRef;

  constructor(
    @Inject('APP_VERSION') public appVersion: string,
    @Inject('IS_PRODUCTION_MODE') public isProductionMode: boolean,
    public mds: MainDataService,
    public tcs: TestControllerService,
    private bs: BackendService,
    private reviewDialog: MatDialog,
    private snackBar: MatSnackBar,
    private router: Router,
    private route: ActivatedRoute,
    private cts: CustomtextService,
    public cmd: CommandService,
    private tls: TestLoaderService
  ) {
  }

  ngOnInit(): void {
    setTimeout(() => {
      this.mds.progressVisualEnabled = false;

      this.subscriptions.errorReporting = this.mds.appError$
        .pipe(filter(e => !!e))
        .subscribe(() => this.tcs.errorOut());

      this.subscriptions.testStatus = this.tcs.testStatus$
        .pipe(distinctUntilChanged())
        .subscribe(status => this.logTestControllerStatusChange(status));

      this.subscriptions.appWindowHasFocus = this.mds.appWindowHasFocus$
        .subscribe(hasFocus => {
          this.tcs.windowFocusState$.next(hasFocus ? WindowFocusState.HOST : WindowFocusState.UNKNOWN);
        });

      this.subscriptions.command = this.cmd.command$
        .pipe(
          distinctUntilChanged((command1: Command, command2: Command): boolean => (command1.id === command2.id))
        )
        .subscribe((command: Command) => {
          this.handleCommand(command.keyword, command.arguments);
        });

      this.subscriptions.routing = this.route.params
        .subscribe(params => {
          this.tcs.testId = params.t;
          this.tls.loadTest()
            .then(() => {
              this.startAppFocusLogging();
              this.startConnectionStatusLogging();
              this.setUnitScreenHeader();
            })
            .catch((error: string | Error | ApiError) => {
              console.log('error', error);
              if (typeof error === 'string') {
                // interceptor already pushed mds.appError$
                return;
              }
              if (error instanceof Error) {
                this.mds.appError$.next({
                  label: 'Kritischer Fehler',
                  description: error.message,
                  category: 'PROBLEM'
                });
              }
              if (error instanceof ApiError) {
                this.mds.appError$.next({
                  label: error.code === 423 ? 'Test ist gesperrt' : 'Problem beim Laden des Tests',
                  description: error.info,
                  category: 'PROBLEM'
                });
              }
            });
        });

      this.subscriptions.maxTimer = this.tcs.maxTimeTimer$
        .subscribe(maxTimerEvent => this.handleMaxTimer(maxTimerEvent));

      this.subscriptions.currentUnit = this.tcs.currentUnitSequenceId$
        .subscribe(() => {
          this.refreshUnitMenu();
          this.setUnitScreenHeader();
        });
    });
  }

  private logTestControllerStatusChange = (testControllerState: TestControllerState): void => {
    if (this.tcs.testMode.saveResponses) {
      this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{
        key: TestStateKey.CONTROLLER, timeStamp: Date.now(), content: testControllerState
      }]);
    }
  };

  private startAppFocusLogging() {
    if (!this.tcs.testMode.saveResponses) {
      return;
    }
    if (this.subscriptions.appFocus !== null) {
      this.subscriptions.appFocus.unsubscribe();
    }
    this.subscriptions.appFocus = this.tcs.windowFocusState$.pipe(
      debounceTime(500)
    ).subscribe((newState: WindowFocusState) => {
      if (this.tcs.testStatus$.getValue() === TestControllerState.ERROR) {
        return;
      }
      if (newState === WindowFocusState.UNKNOWN) {
        this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{
          key: TestStateKey.FOCUS, timeStamp: Date.now(), content: AppFocusState.HAS_NOT
        }]);
      } else {
        this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{
          key: TestStateKey.FOCUS, timeStamp: Date.now(), content: AppFocusState.HAS
        }]);
      }
    });
  }

  private startConnectionStatusLogging() {
    this.subscriptions.connectionStatus = this.cmd.connectionStatus$
      .pipe(
        map(status => status === 'ws-online'),
        distinctUntilChanged()
      )
      .subscribe(isWsConnected => {
        if (this.tcs.testMode.saveResponses) {
          this.bs.updateTestState(this.tcs.testId, [{
            key: TestStateKey.CONNECTION,
            content: isWsConnected ? 'WEBSOCKET' : 'POLLING',
            timeStamp: Date.now()
          }]);
        }
      });
  }

  showReviewDialog(): void {
    if (this.tcs.rootTestlet === null) {
      this.snackBar.open('Kein Testheft verfügbar.', '', { duration: 3000 });
    } else {
      const authData = MainDataService.getAuthData();
      const dialogRef = this.reviewDialog.open(ReviewDialogComponent, {
        width: '700px',
        data: <ReviewDialogData>{
          loginname: authData.displayName,
          bookletname: this.tcs.rootTestlet.title,
          unitTitle: this.tcs.currentUnitTitle,
          unitDbKey: this.tcs.currentUnitDbKey
        }
      });

      dialogRef.afterClosed().subscribe(result => {
        if (typeof result !== 'undefined') {
          if (result !== false) {
            const targetSelection = result.target;
            if (targetSelection === 'u') {
              this.bs.saveUnitReview(
                this.tcs.testId,
                this.tcs.currentUnitDbKey,
                result.priority,
                dialogRef.componentInstance.getCategories(),
                result.sender ? `${result.sender}: ${result.entry}` : result.entry
              ).subscribe(ok => {
                if (!ok) {
                  this.snackBar.open('Konnte Kommentar nicht speichern', '', { duration: 3000 });
                } else {
                  this.snackBar.open('Kommentar gespeichert', '', { duration: 1000 });
                }
              });
            } else {
              this.bs.saveTestReview(
                this.tcs.testId,
                result.priority,
                dialogRef.componentInstance.getCategories(),
                result.sender ? `${result.sender}: ${result.entry}` : result.entry
              ).subscribe(ok => {
                if (!ok) {
                  this.snackBar.open('Konnte Kommentar nicht speichern', '', { duration: 3000 });
                } else {
                  this.snackBar.open('Kommentar gespeichert', '', { duration: 1000 });
                }
              });
            }
          }
        }
      });
    }
  }

  handleCommand(commandName: string, params: string[]): void {
    switch (commandName.toLowerCase()) {
      case 'debug':
        this.debugPane = params.length === 0 || params[0].toLowerCase() !== 'off';
        if (this.debugPane) {
          console.log('select (focus) app window to see the debugPane');
        }
        break;
      case 'pause':
        this.tcs.resumeTargetUnitSequenceId = this.tcs.currentUnitSequenceId;
        this.tcs.pause();
        break;
      case 'resume':
        // eslint-disable-next-line no-case-declarations
        const navTarget =
          (this.tcs.resumeTargetUnitSequenceId > 0) ?
            this.tcs.resumeTargetUnitSequenceId.toString() :
            UnitNavigationTarget.FIRST;
        this.tcs.testStatus$.next(TestControllerState.RUNNING);
        this.tcs.setUnitNavigationRequest(navTarget, true);
        break;
      case 'terminate':
        this.tcs.terminateTest('BOOKLETLOCKEDbyOPERATOR', true, params.indexOf('lock') > -1);
        break;
      case 'goto':
        this.tcs.testStatus$.next(TestControllerState.RUNNING);
        // eslint-disable-next-line no-case-declarations
        let gotoTarget: string;
        if ((params.length === 2) && (params[0] === 'id')) {
          gotoTarget = (this.tcs.allUnitIds.indexOf(params[1]) + 1).toString(10);
        } else if (params.length === 1) {
          gotoTarget = params[0];
        }
        if (gotoTarget && gotoTarget !== '0') {
          this.tcs.resumeTargetUnitSequenceId = 0;
          this.tcs.interruptMaxTimer();
          this.tcs.setUnitNavigationRequest(gotoTarget, true);
        }
        break;
      default:
    }
  }

  private handleMaxTimer(maxTimerData: MaxTimerData): void {
    switch (maxTimerData.type) {
      case MaxTimerDataType.STARTED:
        this.snackBar.open(this.cts.getCustomText('booklet_msgTimerStarted') +
          maxTimerData.timeLeftMinString, '', { duration: 3000 });
        this.timerValue = maxTimerData;
        break;
      case MaxTimerDataType.ENDED:
        this.snackBar.open(this.cts.getCustomText('booklet_msgTimeOver'), '', { duration: 3000 });
        this.tcs.rootTestlet.setTimeLeft(maxTimerData.testletId, 0);
        this.tcs.maxTimeTimers[maxTimerData.testletId] = 0;
        if (this.tcs.testMode.saveResponses) {
          this.bs.updateTestState(
            this.tcs.testId,
            [<StateReportEntry>{
              key: TestStateKey.TESTLETS_TIMELEFT,
              timeStamp: Date.now(),
              content: JSON.stringify(this.tcs.maxTimeTimers)
            }]
          );
        }
        this.timerRunning = false;
        this.timerValue = null;
        if (this.tcs.testMode.forceTimeRestrictions) {
          const nextUnlockedUSId = this.tcs.rootTestlet.getNextUnlockedUnitSequenceId(this.tcs.currentUnitSequenceId);
          this.tcs.setUnitNavigationRequest(nextUnlockedUSId.toString(10));
        }
        break;
      case MaxTimerDataType.CANCELLED:
        this.snackBar.open(this.cts.getCustomText('booklet_msgTimerCancelled'), '', { duration: 3000 });
        this.tcs.rootTestlet.setTimeLeft(maxTimerData.testletId, 0);
        this.tcs.maxTimeTimers[maxTimerData.testletId] = 0;
        if (this.tcs.testMode.saveResponses) {
          this.bs.updateTestState(
            this.tcs.testId,
            [<StateReportEntry>{
              key: TestStateKey.TESTLETS_TIMELEFT,
              timeStamp: Date.now(),
              content: JSON.stringify(this.tcs.maxTimeTimers)
            }]
          );
        }
        this.timerValue = null;
        break;
      case MaxTimerDataType.INTERRUPTED:
        this.tcs.rootTestlet.setTimeLeft(maxTimerData.testletId, this.tcs.maxTimeTimers[maxTimerData.testletId]);
        this.timerValue = null;
        break;
      case MaxTimerDataType.STEP:
        this.timerValue = maxTimerData;
        if ((maxTimerData.timeLeftSeconds % 15) === 0) {
          this.tcs.maxTimeTimers[maxTimerData.testletId] = Math.round(maxTimerData.timeLeftSeconds / 60);
          if (this.tcs.testMode.saveResponses) {
            this.bs.updateTestState(
              this.tcs.testId,
              [<StateReportEntry>{
                key: TestStateKey.TESTLETS_TIMELEFT,
                timeStamp: Date.now(),
                content: JSON.stringify(this.tcs.maxTimeTimers)
              }]
            );
          }
        }
        if ((maxTimerData.timeLeftSeconds / 60) === 5) {
          this.snackBar.open(this.cts.getCustomText('booklet_msgSoonTimeOver5Minutes'), '', { duration: 3000 });
        } else if ((maxTimerData.timeLeftSeconds / 60) === 1) {
          this.snackBar.open(this.cts.getCustomText('booklet_msgSoonTimeOver1Minute'), '', { duration: 3000 });
        }
        break;
      default:
    }
  }

  private refreshUnitMenu(): void {
    this.sidebarOpen = false;
    this.unitNavigationList = [];
    if (!this.tcs.rootTestlet) {
      return;
    }
    const unitCount = this.tcs.rootTestlet.getMaxSequenceId() - 1;
    for (let sequenceId = 1; sequenceId <= unitCount; sequenceId++) {
      const unitData = this.tcs.rootTestlet.getUnitAt(sequenceId);
      this.unitNavigationList.push({
        sequenceId,
        shortLabel: unitData.unitDef.naviButtonLabel,
        longLabel: unitData.unitDef.title,
        testletLabel: unitData.testletLabel,
        disabled: unitData.unitDef.locked,
        isCurrent: sequenceId === this.tcs.currentUnitSequenceId
      });
    }
    if (this.navButtons) {
      setTimeout(() => {
        this.navButtons.nativeElement.querySelector('.current-unit')
          .scrollIntoView({ inline: 'center' });
      }, 50);
    }
  }

  toggleSidebar(): void {
    this.sidebarOpen = !this.sidebarOpen;
  }

  private setUnitScreenHeader(): void {
    if (!this.tcs.rootTestlet || !this.tcs.currentUnitSequenceId) {
      this.unitScreenHeader = '';
      return;
    }
    switch (this.tcs.bookletConfig.unit_screenheader) {
      case 'WITH_UNIT_TITLE':
        this.unitScreenHeader = this.tcs.rootTestlet.getUnitAt(this.tcs.currentUnitSequenceId).unitDef.title;
        break;
      case 'WITH_BOOKLET_TITLE':
        this.unitScreenHeader = this.tcs.rootTestlet.title;
        break;
      case 'WITH_BLOCK_TITLE':
        this.unitScreenHeader = this.tcs.rootTestlet.getUnitAt(this.tcs.currentUnitSequenceId).testletLabel;
        break;
      default:
        this.unitScreenHeader = '';
    }
  }

  ngOnDestroy(): void {
    Object.keys(this.subscriptions)
      .filter(subscriptionKey => this.subscriptions[subscriptionKey])
      .forEach(subscriptionKey => {
        this.subscriptions[subscriptionKey].unsubscribe();
        this.subscriptions[subscriptionKey] = null;
      });
    this.tls.reset();

    this.mds.progressVisualEnabled = true;
  }

  @HostListener('window:unload', ['$event'])
  unloadHandler(): void {
    if (this.cmd.connectionStatus$.getValue() !== 'ws-online') {
      this.bs.notifyDyingTest(this.tcs.testId);
    }
  }
}
<div class="debug-pane" *ngIf="debugPane" cdkDrag>
  <div><b>STATUS:</b> {{tcs.testStatus$ | async}}</div>
  <div><b>TIMER:</b> {{timerValue?.timeLeftString}}<b> {{timerValue?.testletId}} {{timerValue?.type}}</b></div>
  <div><b>MODE:</b> {{tcs.testMode.modeId}}</div>
  <div><b>FOCUS:</b> {{tcs.windowFocusState$ | async}}</div>
  <div><b>BS:</b> {{cmd.connectionStatus$ | async}}</div>
  <div><b>CONFIG:</b> <pre>{{tcs.bookletConfig | json}}</pre></div>
  <div><b>TESTMODE:</b> <pre>{{tcs.testMode | json}}</pre></div>
</div>

<div id="header" fxLayout="row" fxLayoutAlign="end center" *ngIf="tcs.bookletConfig.unit_screenheader !== 'OFF'">
  <h1>{{unitScreenHeader}}</h1>

  <span fxFlex></span>

  <p class="timer" *ngIf="tcs.testMode.showTimeLeft || (tcs.bookletConfig.unit_show_time_left === 'ON')">
    {{timerValue?.timeLeftString}}
  </p>

  <mat-button-toggle-group
    *ngIf="(tcs.bookletConfig.unit_navibuttons !== 'OFF') && ((tcs.testStatus$ | async) === tcs.testStatusEnum.RUNNING)"
    fxLayout="row wrap"
    fxLayoutAlign="start center"
    class="main-buttons"
  >
    <mat-button-toggle
      #prevButton="matButtonToggle"
      [disabled]="tcs.currentUnitSequenceId <= 1"
      (click)="tcs.setUnitNavigationRequest(unitNavigationTarget.PREVIOUS); prevButton.checked = false;"
      matTooltip="Zurück"
      fxFlex="none"
    >
      <i class="material-icons">chevron_left</i>
    </mat-button-toggle>

    <span #navButtons *ngIf="tcs.bookletConfig.unit_navibuttons !== 'ARROWS_ONLY'" class="nav-buttons">
      <mat-button-toggle
        *ngFor="let u of unitNavigationList; let index = index"
        fxLayoutAlign="start center"
        [checked]="u.isCurrent"
        [class]="{'current-unit': u.isCurrent}"
        (click)="tcs.setUnitNavigationRequest(u.sequenceId.toString())"
        [disabled]="u.disabled"
        [matTooltip]="u.longLabel + (!tcs.isUnitContentLoaded(u.sequenceId) ? ' (wird noch geladen!)' : '')"
      >
        {{ u.shortLabel }}
      </mat-button-toggle>
    </span>

    <mat-button-toggle
      #nextButton="matButtonToggle"
      [disabled]="tcs.currentUnitSequenceId >= tcs.allUnitIds.length"
      (click)="tcs.setUnitNavigationRequest(unitNavigationTarget.NEXT); nextButton.checked = false;"
      matTooltip="Weiter"
      fxFlex="none"
    >
      <i class="material-icons">chevron_right</i>
    </mat-button-toggle>
  </mat-button-toggle-group>

  <div class="plus-buttons" fxFlex="none" fxLayout="row">
    <button
      mat-button
      (click)="showReviewDialog()"
      *ngIf="tcs.testMode.canReview"
      matTooltip="Kommentar senden"
      fxFlex="none"
    >
      <mat-icon>rate_review</mat-icon>
    </button>
    <button
      mat-button
      (click)="toggleSidebar()"
      *ngIf="(tcs.bookletConfig.unit_menu !== 'OFF') || tcs.testMode.showUnitMenu"
      matTooltip="Zur Aufgabenliste"
      fxFlex="none"
    >
      <mat-icon>menu</mat-icon>
    </button>
  </div>
</div>

<mat-sidenav-container
  [class]="{'tc-body': true, 'with-header': tcs.bookletConfig.unit_screenheader !== 'OFF'}"
  hasBackdrop="true"
>
  <mat-sidenav
    [(opened)]="sidebarOpen"
    (click)="toggleSidebar()"
    mode="over"
    class="sidebar"
    fixedInViewport="true"
    fixedTopGap="55"
    position="end"
  >
    <unit-menu></unit-menu>
  </mat-sidenav>
  <mat-sidenav-content>
    <router-outlet></router-outlet>
  </mat-sidenav-content>
</mat-sidenav-container>

./test-controller.component.css

.timer {
  color: lightgray;
  margin: 0 10px;
}

.tc-body {
  overflow-x: auto;
  position: absolute;
  width: 100%;
  top: 0;
  bottom: 0;
}

.tc-body.with-header {
  top: 65px;
}

h1 {
  font-size: 1.5em;
  color: white;
  margin-right: 1em;
  word-wrap: break-word;
  overflow: hidden;
  text-overflow: clip;
  width: 80%;
  line-height: 30px;
  max-height: 60px;
}

#header {
  position: absolute;
  width: 100%;
  z-index: 444;
  color: white;;
  height: 65px;
  padding-left: 116px;
  pointer-events: none;
}

#header mat-button-toggle-group {
  background: white;
}

#header .nav-buttons {
  overflow-x: auto;
  display: flex;
  border-left: solid 1px rgba(0, 0, 0, 0.12);
  border-right: solid 1px rgba(0, 0, 0, 0.12);
}

#header .main-buttons,
#header .plus-buttons {
  pointer-events: auto;
}

#header .plus-buttons {
  min-width: 16px;
}

#header .plus-buttons button ~ button {
  min-width: 48px;
  padding-left: 0;
}

.debug-pane {
  color: orange;
  position: absolute;
  background: black;
  z-index: 10000;
  padding: 2px
}

.debug-pane b {
  color: white;
  font-weight: normal;
}

.sidebar {
  min-width: 200px;
  max-width: 25%;
}

.with-header .sidebar {
  top: 65px !important;
}

.mat-drawer-container {
  background: transparent !important;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""