src/app/test-controller/components/test-controller/test-controller.component.ts
styleUrls | ./test-controller.component.css |
templateUrl | ./test-controller.component.html |
Properties |
|
Methods |
|
HostListeners |
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 :
|
window:unload |
Arguments : '$event'
|
window:unload()
|
handleCommand | |||||||||
handleCommand(commandName: string, params: string[])
|
|||||||||
Parameters :
Returns :
void
|
Private handleMaxTimer | ||||||
handleMaxTimer(maxTimerData: MaxTimerData)
|
||||||
Parameters :
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
|
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
|
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;
}