From 69dbda6c5e1d43e9b15ded7fe6f3096de009dc35 Mon Sep 17 00:00:00 2001 From: Martin Mechtel <mechtelm@user.hu-berlin.de> Date: Mon, 8 Oct 2018 15:43:25 +0200 Subject: [PATCH] release 0.7 --- package-lock.json | 10 +- src/app/admin/admin.module.ts | 4 +- src/app/admin/backend.service.ts | 141 ++++++++++++-- src/app/admin/monitor/monitor.component.css | 20 +- src/app/admin/monitor/monitor.component.html | 81 ++++++-- src/app/admin/monitor/monitor.component.ts | 123 ++++++++++-- src/app/admin/results/results.component.css | 23 +-- src/app/admin/results/results.component.html | 120 ++++++------ src/app/admin/results/results.component.ts | 189 +++++++++++++++---- src/environments/environment.build.ts | 2 +- src/environments/environment.ts | 2 +- 11 files changed, 545 insertions(+), 170 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a01e0da..efc0b6e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3815,7 +3815,7 @@ }, "file-saver": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-1.3.8.tgz", + "resolved": "http://registry.npmjs.org/file-saver/-/file-saver-1.3.8.tgz", "integrity": "sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg==" }, "file-uri-to-path": { @@ -4066,12 +4066,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4091,7 +4093,8 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", @@ -4239,6 +4242,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index 05d3e654..1351c1c2 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -1,3 +1,4 @@ +import { FlexLayoutModule } from '@angular/flex-layout'; import { LoginDialogComponent } from './login-dialog/login-dialog.component'; import { BackendService } from './backend.service'; import { IqbFilesModule } from '../iqb-files'; @@ -48,7 +49,8 @@ import {MatGridListModule} from '@angular/material/grid-list'; MatSnackBarModule, MatGridListModule, IqbCommonModule, - BrowserAnimationsModule + BrowserAnimationsModule, + FlexLayoutModule ], exports: [ AdminComponent diff --git a/src/app/admin/backend.service.ts b/src/app/admin/backend.service.ts index 0b2903e7..3d35b089 100644 --- a/src/app/admin/backend.service.ts +++ b/src/app/admin/backend.service.ts @@ -127,41 +127,87 @@ export class BackendService { ); } -/*******************************/ + /*******************************/ + getBookletsStarted(adminToken: string, workspaceId: number, groups: string[]): Observable<BookletsStarted[]>{ + const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }; + return this.http + .post<BookletsStarted[]>(this._serverUrl + 'getBookletsStarted.php', {at: adminToken, ws: workspaceId, g: groups}, httpOptions); + } + + /*******************************/ + lockBooklets(adminToken: string, workspaceId: number, groups: string[]): Observable<boolean>{ + const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }; + return this.http + .post<boolean>(this._serverUrl + 'lockBooklets.php', {at: adminToken, ws: workspaceId, g: groups}, httpOptions); + } - showStats(adminToken: string, workspaceId: number, responseOnly: boolean): Observable<GroupResponse[]>{ + unlockBooklets(adminToken: string, workspaceId: number, groups: string[]): Observable<boolean>{ const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; return this.http - .post<GroupResponse[]>(this._serverUrl + 'getTestStats.php', {at: adminToken, ws: workspaceId, rso: responseOnly}, httpOptions); + .post<boolean>(this._serverUrl + 'unlockBooklets.php', {at: adminToken, ws: workspaceId, g: groups}, httpOptions); } - downloadCSVResponses(adminToken: string, workspaceId: number, groups: GroupResponse[]): Observable<string>{ - const customHeaders = new HttpHeaders().set('Content-Type', 'application/json'); - const httpOptions = {headers: customHeaders, responseType: 'text' as 'json'}; + getMonitorData(adminToken: string, workspaceId: number): Observable<MonitorData[]>{ + const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }; + return this.http + .post<MonitorData[]>(this._serverUrl + 'getMonitorData.php', {at: adminToken, ws: workspaceId}, httpOptions); + } + getResultData(adminToken: string, workspaceId: number): Observable<ResultData[]>{ + const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }; return this.http - .post<string>(this._serverUrl + 'getCSVResponses.php', {at: adminToken, ws: workspaceId, groups: groups }, httpOptions); + .post<ResultData[]>(this._serverUrl + 'getResultData.php', {at: adminToken, ws: workspaceId}, httpOptions); } - downloadCSVResponses2(adminToken: string, workspaceId: number, groups: GroupResponse[]): void{ - let url = this._serverUrl; - url += 'getCSVResponses.php?'; - url += 'at='; - url += adminToken; - url += '&ws='; - url += workspaceId; - groups.forEach(group => { - url += '&groups[]='; - url += group; - }); - window.open(url); + getResponses(adminToken: string, workspaceId: number, groups: string[]): Observable<UnitResponse[]>{ + const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }; + return this.http + .post<UnitResponse[]>(this._serverUrl + 'getResponses.php', {at: adminToken, ws: workspaceId, g: groups}, httpOptions); } + getLogs(adminToken: string, workspaceId: number, groups: string[]): Observable<LogData[]>{ + const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }; + return this.http + .post<LogData[]>(this._serverUrl + 'getLogs.php', {at: adminToken, ws: workspaceId, g: groups}, httpOptions); + } + getReviews(adminToken: string, workspaceId: number, groups: string[]): Observable<ReviewData[]>{ + const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }; + return this.http + .post<ReviewData[]>(this._serverUrl + 'getReviews.php', {at: adminToken, ws: workspaceId, g: groups}, httpOptions); + } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . private handleError(errorObj: HttpErrorResponse): Observable<ServerError> { @@ -221,10 +267,65 @@ export interface CheckWorkspaceResponseData { } - export interface GroupResponse { name: string; testsTotal: number; testsStarted: number; responsesGiven: number; } + +export interface BookletsStarted { + groupname: string; + loginname: string; + code: string; + bookletname: string; + locked: boolean; +} + +export interface UnitResponse { + groupname: string; + loginname: string; + code: string; + bookletname: string; + unitname: string; + responses: string; +} + +export interface MonitorData { + groupname: string; + loginsPrepared: number; + personsPrepared: number; + bookletsPrepared: number; + bookletsStarted: number; + bookletsLocked: number; +} + +export interface ResultData { + groupname: string; + bookletsStarted: number; + num_units_min: number; + num_units_max: number; + num_units_mean: number; +} + +export interface LogData { + groupname: string; + loginname: string; + code: string; + bookletname: string; + unitname: string; + logtime: Date; + logentry: string; +} + +export interface ReviewData { + groupname: string; + loginname: string; + code: string; + bookletname: string; + unitname: string; + priority: number; + categories: string; + reviewtime: Date; + entry: string; +} diff --git a/src/app/admin/monitor/monitor.component.css b/src/app/admin/monitor/monitor.component.css index 6dba0d90..0e471019 100644 --- a/src/app/admin/monitor/monitor.component.css +++ b/src/app/admin/monitor/monitor.component.css @@ -1,12 +1,24 @@ -.mat-card { +.columnhost { + width: 100%; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: flex-start; + justify-content: left; +} +/* .mat-card { max-width: 50%; margin-top: 20px; } .title { margin-top: 2550px; -} +} */ -table { +/* table { width: 100%; -} \ No newline at end of file +} */ + +.cellstyle1 { + padding: 5px; +} diff --git a/src/app/admin/monitor/monitor.component.html b/src/app/admin/monitor/monitor.component.html index d2d21f9d..d823a3a7 100644 --- a/src/app/admin/monitor/monitor.component.html +++ b/src/app/admin/monitor/monitor.component.html @@ -1,29 +1,70 @@ -<table mat-table [dataSource]="groupStats" matSort class="mat-elevation-z8"> +<div class="columnhost" fxLayout="column"> + <div class="spinner-container" *ngIf="dataLoading"> + <mat-spinner></mat-spinner> + </div> + <div fxLayout="row"> + <button mat-raised-button (click)="downloadCSV()" [disabled]="!tableselectionCheckbox.hasValue()" + matTooltip="Download markierte Gruppen als CSV für Excel" matTooltipPosition="above"> + <mat-icon>cloud_download</mat-icon> + </button> + <button mat-raised-button (click)="lock()" [disabled]="!tableselectionCheckbox.hasValue()" + matTooltip="Sperre gestartete Testhefte für markierte Gruppen" matTooltipPosition="above"> + <mat-icon>lock</mat-icon> + </button> - <ng-container matColumnDef="name"> - <th mat-header-cell *matHeaderCellDef mat-sort-header>Group Name</th> - <td mat-cell *matCellDef="let element">{{element.name}}</td> - </ng-container> + <button mat-raised-button (click)="unlock()" [disabled]="!tableselectionCheckbox.hasValue()" + matTooltip="Gesperrte Testhefte für markierte Gruppen freigeben" matTooltipPosition="above"> + <mat-icon>lock_open</mat-icon> + </button> + </div> - <ng-container matColumnDef="testsTotal"> - <th mat-header-cell *matHeaderCellDef mat-sort-header>Number of tests from XML</th> - <td mat-cell *matCellDef="let element">{{element.testsTotal}} </td> - </ng-container> + <mat-table [dataSource]="monitorDataSource" matSort> + <ng-container matColumnDef="selectCheckbox"> + <mat-header-cell *matHeaderCellDef fxFlex="70px"> + <mat-checkbox (change)="$event ? masterToggle() : null" + [checked]="tableselectionCheckbox.hasValue() && isAllSelected()" + [indeterminate]="tableselectionCheckbox.hasValue() && !isAllSelected()"> + </mat-checkbox> + </mat-header-cell> + <mat-cell *matCellDef="let row" fxFlex="70px"> + <mat-checkbox (click)="$event.stopPropagation()" + (change)="$event ? tableselectionCheckbox.toggle(row) : null" + [checked]="tableselectionCheckbox.isSelected(row)"> + </mat-checkbox> + </mat-cell> + </ng-container> - <ng-container matColumnDef="testsStarted"> - <th mat-header-cell *matHeaderCellDef mat-sort-header> Booklets started / Number of test takers</th> - <td mat-cell *matCellDef="let element"> {{element.testsStarted}} </td> - </ng-container> + <ng-container matColumnDef="groupname"> + <mat-header-cell *matHeaderCellDef mat-sort-header fxFlex="300px">Login-Gruppe</mat-header-cell> + <mat-cell *matCellDef="let element" fxFlex="300px">{{element.groupname}}</mat-cell> + </ng-container> - <ng-container matColumnDef="responsesGiven"> - <th mat-header-cell *matHeaderCellDef mat-sort-header> Responses / Total responses planned </th> - <td mat-cell *matCellDef="let element"> {{element.responsesGiven}} </td> - </ng-container> + <ng-container matColumnDef="loginsPrepared"> + <mat-header-cell *matHeaderCellDef mat-sort-header fxLayoutAlign="center center">Logins</mat-header-cell> + <mat-cell *matCellDef="let element" fxLayoutAlign="center center">{{element.loginsPrepared}} </mat-cell> + </ng-container> - <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> - <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> -</table> + <ng-container matColumnDef="personsPrepared"> + <mat-header-cell *matHeaderCellDef mat-sort-header fxLayoutAlign="center center">Personen</mat-header-cell> + <mat-cell *matCellDef="let element" fxLayoutAlign="center center"> {{element.personsPrepared}} </mat-cell> + </ng-container> + <ng-container matColumnDef="bookletsPrepared"> + <mat-header-cell *matHeaderCellDef mat-sort-header fxLayoutAlign="center center">Testhefte insges.</mat-header-cell> + <mat-cell *matCellDef="let element" fxLayoutAlign="center center"> {{element.bookletsPrepared}} </mat-cell> + </ng-container> + <ng-container matColumnDef="bookletsStarted"> + <mat-header-cell *matHeaderCellDef mat-sort-header fxLayoutAlign="center center">Testhefte gestartet</mat-header-cell> + <mat-cell *matCellDef="let element" fxLayoutAlign="center center"> {{element.bookletsStarted}} </mat-cell> + </ng-container> + <ng-container matColumnDef="bookletsLocked"> + <mat-header-cell *matHeaderCellDef mat-sort-header fxLayoutAlign="center center">gesperrte Testhefte</mat-header-cell> + <mat-cell *matCellDef="let element" fxLayoutAlign="center center"> {{ element.bookletsLocked }} </mat-cell> + </ng-container> + <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> + <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row> + </mat-table> +</div> diff --git a/src/app/admin/monitor/monitor.component.ts b/src/app/admin/monitor/monitor.component.ts index 4361c9f2..ef4630b3 100644 --- a/src/app/admin/monitor/monitor.component.ts +++ b/src/app/admin/monitor/monitor.component.ts @@ -1,7 +1,9 @@ -import { BackendService, GroupResponse } from './../backend.service'; +import { BackendService, MonitorData, BookletsStarted } from './../backend.service'; import { MainDatastoreService } from './../maindatastore.service'; import { Component, OnInit, ViewChild } from '@angular/core'; import { MatSnackBar, MatSort, MatTableDataSource } from '@angular/material'; +import { SelectionModel } from '@angular/cdk/collections'; +import { saveAs } from 'file-saver'; @Component({ @@ -9,10 +11,13 @@ import { MatSnackBar, MatSort, MatTableDataSource } from '@angular/material'; styleUrls: ['./monitor.component.css'] }) export class MonitorComponent implements OnInit { - displayedColumns: string[] = ['name', 'testsTotal', 'testsStarted', 'responsesGiven']; - private groupStats = new MatTableDataSource<GroupResponse>([]); - private isAdmin = false; + displayedColumns: string[] = ['selectCheckbox', 'groupname', 'loginsPrepared', + 'personsPrepared', 'bookletsPrepared', 'bookletsStarted', 'bookletsLocked']; + private monitorDataSource = new MatTableDataSource<MonitorData>([]); + private isAdmin = false; + private tableselectionCheckbox = new SelectionModel<MonitorData>(true, []); + private dataLoading = false; @ViewChild(MatSort) sort: MatSort; constructor( @@ -25,18 +30,114 @@ export class MonitorComponent implements OnInit { } ngOnInit() { - this.mds.adminToken$.subscribe(at => this.updateStats()); - this.mds.workspaceId$.subscribe(ws => this.updateStats()); + this.mds.adminToken$.subscribe(at => this.updateTable()); + this.mds.workspaceId$.subscribe(ws => this.updateTable()); } - updateStats() { - this.bs.showStats(this.mds.adminToken$.getValue(), this.mds.workspaceId$.getValue(), false).subscribe( - (responseData: GroupResponse[]) => { - this.groupStats = new MatTableDataSource<GroupResponse>(responseData); - this.groupStats.sort = this.sort; + updateTable() { + this.dataLoading = true; + this.tableselectionCheckbox.clear; + this.bs.getMonitorData(this.mds.adminToken$.getValue(), this.mds.workspaceId$.getValue()).subscribe( + (monitorData: MonitorData[]) => { + this.dataLoading = false; + this.monitorDataSource = new MatTableDataSource<MonitorData>(monitorData); + this.monitorDataSource.sort = this.sort; } ) } + isAllSelected() { + const numSelected = this.tableselectionCheckbox.selected.length; + const numRows = this.monitorDataSource.data.length; + return numSelected === numRows; + } + + masterToggle() { + this.isAllSelected() ? + this.tableselectionCheckbox.clear() : + this.monitorDataSource.data.forEach(row => this.tableselectionCheckbox.select(row)); + } + + downloadCSV() { + if (this.tableselectionCheckbox.selected.length > 0) { + this.dataLoading = true; + const selectedGroups: string[] = []; + this.tableselectionCheckbox.selected.forEach(element => { + selectedGroups.push(element.groupname); + }); + this.bs.getBookletsStarted( + this.mds.adminToken$.getValue(), + this.mds.workspaceId$.getValue(), + selectedGroups).subscribe(bData => { + + const bookletList = bData as BookletsStarted[]; + if (bookletList.length > 0) { + const columnDelimiter = ';'; + const lineDelimiter = '\n'; + + let myCsvData = 'groupname' + columnDelimiter + 'loginname' + columnDelimiter + 'code' + columnDelimiter + + 'bookletname' + columnDelimiter + 'locked' + lineDelimiter; + bookletList.forEach((b: BookletsStarted) => { + myCsvData += '"' + b.groupname + '"' + columnDelimiter + '"' + b.loginname + '"' + columnDelimiter + '"' + b.code + '"' + columnDelimiter + + '"' + b.bookletname + '"' + columnDelimiter + '"' + (b.locked ? 'X' : '-') + '"' + lineDelimiter; + }); + var blob = new Blob([myCsvData], {type: "text/csv;charset=utf-8"}); + saveAs(blob, "iqb-testcenter-bookletsStarted.csv"); + } else { + this.snackBar.open('Keine Daten verfügbar.', 'Fehler', {duration: 3000}); + } + + this.tableselectionCheckbox.clear(); + this.dataLoading = false; + } + ); + } + } + + lock() { + if (this.tableselectionCheckbox.selected.length > 0) { + this.dataLoading = true; + const selectedGroups: string[] = []; + this.tableselectionCheckbox.selected.forEach(element => { + selectedGroups.push(element.groupname); + }); + this.bs.lockBooklets( + this.mds.adminToken$.getValue(), + this.mds.workspaceId$.getValue(), + selectedGroups).subscribe(success => { + const ok = success as boolean; + if (ok) { + this.snackBar.open('Testhefte wurden gesperrt.', 'Sperrung', {duration: 1000}); + } else { + this.snackBar.open('Testhefte konnten nicht gesperrt werden.', 'Fehler', {duration: 3000}); + } + }); + this.dataLoading = false; + this.updateTable(); + } + } + + unlock() { + if (this.tableselectionCheckbox.selected.length > 0) { + this.dataLoading = true; + const selectedGroups: string[] = []; + this.tableselectionCheckbox.selected.forEach(element => { + selectedGroups.push(element.groupname); + }); + this.bs.unlockBooklets( + this.mds.adminToken$.getValue(), + this.mds.workspaceId$.getValue(), + selectedGroups).subscribe(success => { + const ok = success as boolean; + if (ok) { + this.snackBar.open('Testhefte wurden freigegeben.', 'Sperrung', {duration: 1000}); + } else { + this.snackBar.open('Testhefte konnten nicht freigegeben werden.', 'Fehler', {duration: 3000}); + } + }); + this.dataLoading = false; + this.updateTable(); + } + } } diff --git a/src/app/admin/results/results.component.css b/src/app/admin/results/results.component.css index 3a8a1b11..3c577b39 100644 --- a/src/app/admin/results/results.component.css +++ b/src/app/admin/results/results.component.css @@ -1,17 +1,12 @@ -.mat-card { - max-width: 50%; - margin-top: 20px; -} - -.title { - margin-top: 2550px; -} - -table { +.columnhost { width: 100%; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: flex-start; + justify-content: left; } -button { - margin-top: 10px; - padding: 10px; -} \ No newline at end of file +.mat-icon { + margin-right: 5px; +} diff --git a/src/app/admin/results/results.component.html b/src/app/admin/results/results.component.html index 1686cf97..b07954ed 100644 --- a/src/app/admin/results/results.component.html +++ b/src/app/admin/results/results.component.html @@ -1,56 +1,64 @@ - -<table mat-table [dataSource]="groupStats" matSort class="mat-elevation-z8"> - - <ng-container matColumnDef="selectCheckbox"> - <mat-header-cell *matHeaderCellDef fxFlex="70px"> - <mat-checkbox (change)="$event ? masterToggle() : null" - [checked]="tableselectionCheckbox.hasValue() && isAllSelected()" - [indeterminate]="tableselectionCheckbox.hasValue() && !isAllSelected()"> - </mat-checkbox> - </mat-header-cell> - <mat-cell *matCellDef="let row" fxFlex="70px"> - <mat-checkbox (click)="$event.stopPropagation()" - (change)="$event ? tableselectionCheckbox.toggle(row) : null" - [checked]="tableselectionCheckbox.isSelected(row)"> - </mat-checkbox> - </mat-cell> - </ng-container> - - <ng-container matColumnDef="name"> - <th mat-header-cell *matHeaderCellDef mat-sort-header>Group Name</th> - <td mat-cell *matCellDef="let element">{{element.name}}</td> - </ng-container> - - <ng-container matColumnDef="testsTotal"> - <th mat-header-cell *matHeaderCellDef mat-sort-header>Number of tests from XML</th> - <td mat-cell *matCellDef="let element">{{element.testsTotal}} </td> - </ng-container> - - <ng-container matColumnDef="testsStarted"> - <th mat-header-cell *matHeaderCellDef mat-sort-header> Booklets started / Number of test takers</th> - <td mat-cell *matCellDef="let element"> {{element.testsStarted}} </td> - </ng-container> - - <ng-container matColumnDef="responsesGiven"> - <th mat-header-cell *matHeaderCellDef mat-sort-header> Responses / Total responses planned </th> - <td mat-cell *matCellDef="let element"> {{element.responsesGiven}} </td> - </ng-container> - - <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> - <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> -</table> -<!-- <p>number of selected rows: {{ tableselectionCheckbox.selected.length }}</p> --> - -<button mat-button [disabled]="tableselectionCheckbox.selected.length === 0" (click)= "downloadOnClick()">Save Data to CSV</button> - - - - - - - - - - - - +<div class="columnhost" fxLayout="column"> + <div class="spinner-container" *ngIf="dataLoading"> + <mat-spinner></mat-spinner> + </div> + <div fxLayout="row" fxLayoutGap="10px"> + <button mat-raised-button (click)="downloadResponsesCSV()" [disabled]="!tableselectionCheckbox.hasValue()" + matTooltip="Download markierte Gruppen als CSV für Excel" matTooltipPosition="above"> + <mat-icon>cloud_download</mat-icon>Antworten + </button> + <button mat-raised-button (click)="downloadLogsCSV()" [disabled]="!tableselectionCheckbox.hasValue()" + matTooltip="Download markierte Gruppen als CSV für Excel" matTooltipPosition="above"> + <mat-icon>cloud_download</mat-icon>Logs + </button> + <button mat-raised-button (click)="downloadReviewsCSV()" [disabled]="!tableselectionCheckbox.hasValue()" + matTooltip="Download markierte Gruppen als CSV für Excel" matTooltipPosition="above"> + <mat-icon>cloud_download</mat-icon>Kommentare + </button> + </div> + + <mat-table [dataSource]="resultDataSource" matSort> + <ng-container matColumnDef="selectCheckbox"> + <mat-header-cell *matHeaderCellDef fxFlex="70px"> + <mat-checkbox (change)="$event ? masterToggle() : null" + [checked]="tableselectionCheckbox.hasValue() && isAllSelected()" + [indeterminate]="tableselectionCheckbox.hasValue() && !isAllSelected()"> + </mat-checkbox> + </mat-header-cell> + <mat-cell *matCellDef="let row" fxFlex="70px"> + <mat-checkbox (click)="$event.stopPropagation()" + (change)="$event ? tableselectionCheckbox.toggle(row) : null" + [checked]="tableselectionCheckbox.isSelected(row)"> + </mat-checkbox> + </mat-cell> + </ng-container> + + <ng-container matColumnDef="groupname"> + <mat-header-cell *matHeaderCellDef mat-sort-header fxFlex="300px">Login-Gruppe</mat-header-cell> + <mat-cell *matCellDef="let element" fxFlex="300px">{{element.groupname}}</mat-cell> + </ng-container> + + <ng-container matColumnDef="bookletsStarted"> + <mat-header-cell *matHeaderCellDef mat-sort-header fxLayoutAlign="center center">Testhefte gestartet</mat-header-cell> + <mat-cell *matCellDef="let element" fxLayoutAlign="center center"> {{element.bookletsStarted}} </mat-cell> + </ng-container> + + <ng-container matColumnDef="num_units_min"> + <mat-header-cell *matHeaderCellDef mat-sort-header fxLayoutAlign="center center">Aufgaben min</mat-header-cell> + <mat-cell *matCellDef="let element" fxLayoutAlign="center center">{{element.num_units_min}} </mat-cell> + </ng-container> + + <ng-container matColumnDef="num_units_max"> + <mat-header-cell *matHeaderCellDef mat-sort-header fxLayoutAlign="center center">Aufgaben max</mat-header-cell> + <mat-cell *matCellDef="let element" fxLayoutAlign="center center">{{element.num_units_max}} </mat-cell> + </ng-container> + + <ng-container matColumnDef="num_units_mean"> + <mat-header-cell *matHeaderCellDef mat-sort-header fxLayoutAlign="center center">Aufgaben Mittelwert</mat-header-cell> + <mat-cell *matCellDef="let element" fxLayoutAlign="center center">{{element.num_units_mean | number:'1.1-1'}} </mat-cell> + </ng-container> + + <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> + <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row> + </mat-table> +</div> diff --git a/src/app/admin/results/results.component.ts b/src/app/admin/results/results.component.ts index 42f3f5f3..48b4b072 100644 --- a/src/app/admin/results/results.component.ts +++ b/src/app/admin/results/results.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit, ViewChild } from '@angular/core'; -import { BackendService, GroupResponse } from './../backend.service'; +import { BackendService, UnitResponse, ResultData, ReviewData, LogData } from './../backend.service'; import { MainDatastoreService } from './../maindatastore.service'; import { MatSnackBar, MatSort, MatTableDataSource } from '@angular/material'; import { SelectionModel } from '@angular/cdk/collections'; -// import { saveAs } from 'file-saver'; +import { saveAs } from 'file-saver'; @Component({ @@ -11,11 +11,12 @@ import { SelectionModel } from '@angular/cdk/collections'; styleUrls: ['./results.component.css'] }) export class ResultsComponent implements OnInit { - displayedColumns: string[] = ['selectCheckbox', 'name', 'testsTotal', 'testsStarted', 'responsesGiven']; - private groupStats = new MatTableDataSource<GroupResponse>([]); + displayedColumns: string[] = ['selectCheckbox', 'groupname', 'bookletsStarted', 'num_units_min', 'num_units_max', 'num_units_mean']; + private resultDataSource = new MatTableDataSource<ResultData>([]); private isAdmin = false; - private downloadButton = Object; - private tableselectionCheckbox = new SelectionModel <GroupResponse>(true, []); + // prepared for selection if needed sometime + private tableselectionCheckbox = new SelectionModel<ResultData>(true, []); + private dataLoading = false; @ViewChild(MatSort) sort: MatSort; @@ -29,57 +30,167 @@ export class ResultsComponent implements OnInit { } ngOnInit() { - this.mds.adminToken$.subscribe(at => this.updateStats()); - this.mds.workspaceId$.subscribe(ws => this.updateStats()); + this.mds.adminToken$.subscribe(at => this.updateTable()); + this.mds.workspaceId$.subscribe(ws => this.updateTable()); // console.log(saveAs); } - updateStats() { - this.bs.showStats(this.mds.adminToken$.getValue(), this.mds.workspaceId$.getValue(), true).subscribe( - (responseData: GroupResponse[]) => { - this.groupStats = new MatTableDataSource<GroupResponse>(responseData); - this.groupStats.sort = this.sort; + updateTable() { + this.dataLoading = true; + this.tableselectionCheckbox.clear(); + this.bs.getResultData(this.mds.adminToken$.getValue(), this.mds.workspaceId$.getValue()).subscribe( + (resultData: ResultData[]) => { + this.dataLoading = false; + this.resultDataSource = new MatTableDataSource<ResultData>(resultData); + this.resultDataSource.sort = this.sort; } ) } isAllSelected() { const numSelected = this.tableselectionCheckbox.selected.length; - const numRows = this.groupStats.data.length; + const numRows = this.resultDataSource.data.length; return numSelected === numRows; } masterToggle() { this.isAllSelected() ? this.tableselectionCheckbox.clear() : - this.groupStats.data.forEach(row => this.tableselectionCheckbox.select(row)); + this.resultDataSource.data.forEach(row => this.tableselectionCheckbox.select(row)); } - downloadOnClick() { - // let observableDlResponse = this.bs.downloadCSVResponses(this.mds.adminToken$.getValue(), this.mds.workspaceId$.getValue(), this.tableselectionCheckbox.selected); - - - // observableDlResponse.subscribe( - // (responseData: string) => { - // let savedFileContent = responseData; - - // // this.groupStats = new MatTableDataSource<GroupResponse>(responseData); - // // this.groupStats.sort = this.sort; - - // // - // const saveFile = new File([savedFileContent], 'responses.csv', { type: 'text/plain;charset=utf-8' }); - // saveAs(saveFile); - // } - // ) - - // create an array with groupIds that you get with tableselectionCheckbox - let processedSelection = []; - this.tableselectionCheckbox.selected.forEach(element => { - processedSelection.push(element.name); - }); - - this.bs.downloadCSVResponses2(this.mds.adminToken$.getValue(), this.mds.workspaceId$.getValue(), processedSelection); + // 444444444444444444444444444444444444444444444444444444444444444444444444444444444444444 + downloadResponsesCSV() { + if (this.tableselectionCheckbox.selected.length > 0) { + this.dataLoading = true; + const selectedGroups: string[] = []; + this.tableselectionCheckbox.selected.forEach(element => { + selectedGroups.push(element.groupname); + }); + this.bs.getResponses( + this.mds.adminToken$.getValue(), + this.mds.workspaceId$.getValue(), + selectedGroups).subscribe( + (responseData: UnitResponse[]) => { + if (responseData.length > 0) { + const columnDelimiter = ';'; + const lineDelimiter = '\n'; + let myCsvData = 'groupname' + columnDelimiter + 'loginname' + columnDelimiter + 'code' + columnDelimiter + + 'bookletname' + columnDelimiter + 'unitname' + columnDelimiter + 'responses' + lineDelimiter; + responseData.forEach((resp: UnitResponse) => { + if ((resp.responses !== null) && (resp.responses.length > 0)) { + myCsvData += '"' + resp.groupname + '"' + columnDelimiter + '"' + resp.loginname + '"' + columnDelimiter + '"' + resp.code + '"' + columnDelimiter + + '"' + resp.bookletname + '"' + columnDelimiter + '"' + resp.unitname + '"' + columnDelimiter + resp.responses.replace(/\\"/g, '""') + lineDelimiter; + } + }); + var blob = new Blob([myCsvData], {type: "text/csv;charset=utf-8"}); + saveAs(blob, "iqb-testcenter-responses.csv"); + } else { + this.snackBar.open('Keine Daten verfügbar.', 'Fehler', {duration: 3000}); + } + this.tableselectionCheckbox.clear(); + this.dataLoading = false; + }) + } + } + // 444444444444444444444444444444444444444444444444444444444444444444444444444444444444444 + downloadReviewsCSV() { + if (this.tableselectionCheckbox.selected.length > 0) { + this.dataLoading = true; + const selectedGroups: string[] = []; + this.tableselectionCheckbox.selected.forEach(element => { + selectedGroups.push(element.groupname); + }); + this.bs.getReviews( + this.mds.adminToken$.getValue(), + this.mds.workspaceId$.getValue(), + selectedGroups).subscribe( + (responseData: ReviewData[]) => { + if (responseData.length > 0) { + // collect categories + const allCategories: string[] = []; + responseData.forEach((resp: ReviewData) => { + resp.categories.split(' ').forEach(s => { + const s_trimmed = s.trim(); + if (s_trimmed.length > 0) { + if (!allCategories.includes(s_trimmed)) { + allCategories.push(s_trimmed); + } + } + }) + }); + + const columnDelimiter = ';'; + const lineDelimiter = '\n'; + let myCsvData = 'groupname' + columnDelimiter + 'loginname' + columnDelimiter + 'code' + columnDelimiter + + 'bookletname' + columnDelimiter + 'unitname' + columnDelimiter + + 'priority' + columnDelimiter; + allCategories.forEach(s => { + myCsvData += 'category: ' + s + columnDelimiter; + }); + myCsvData += 'reviewtime' + columnDelimiter + 'entry' + lineDelimiter; + + responseData.forEach((resp: ReviewData) => { + if ((resp.entry !== null) && (resp.entry.length > 0)) { + myCsvData += '"' + resp.groupname + '"' + columnDelimiter + '"' + resp.loginname + '"' + columnDelimiter + '"' + resp.code + '"' + columnDelimiter + + '"' + resp.bookletname + '"' + columnDelimiter + '"' + resp.unitname + '"' + columnDelimiter + '"' + resp.priority + '"' + columnDelimiter; + const resp_categories = resp.categories.split(' '); + allCategories.forEach(s => { + if (resp_categories.includes(s)) { + myCsvData += '"X"' + columnDelimiter; + } else { + myCsvData += columnDelimiter; + } + }); + myCsvData += '"' + resp.reviewtime + '"' + columnDelimiter + '"' + resp.entry + '"' + lineDelimiter; + } + }); + var blob = new Blob([myCsvData], {type: "text/csv;charset=utf-8"}); + saveAs(blob, "iqb-testcenter-reviews.csv"); + } else { + this.snackBar.open('Keine Daten verfügbar.', 'Fehler', {duration: 3000}); + } + this.tableselectionCheckbox.clear(); + this.dataLoading = false; + }) + } } + // 444444444444444444444444444444444444444444444444444444444444444444444444444444444444444 + downloadLogsCSV() { + if (this.tableselectionCheckbox.selected.length > 0) { + this.dataLoading = true; + const selectedGroups: string[] = []; + this.tableselectionCheckbox.selected.forEach(element => { + selectedGroups.push(element.groupname); + }); + this.bs.getLogs( + this.mds.adminToken$.getValue(), + this.mds.workspaceId$.getValue(), + selectedGroups).subscribe( + (responseData: LogData[]) => { + if (responseData.length > 0) { + const columnDelimiter = ';'; + const lineDelimiter = '\n'; + let myCsvData = 'groupname' + columnDelimiter + 'loginname' + columnDelimiter + 'code' + columnDelimiter + + 'bookletname' + columnDelimiter + 'unitname' + columnDelimiter + + 'logtime' + columnDelimiter + 'logentry' + lineDelimiter; + responseData.forEach((resp: LogData) => { + if ((resp.logentry !== null) && (resp.logentry.length > 0)) { + myCsvData += '"' + resp.groupname + '"' + columnDelimiter + '"' + resp.loginname + '"' + columnDelimiter + '"' + resp.code + '"' + columnDelimiter + + '"' + resp.bookletname + '"' + columnDelimiter + '"' + resp.unitname + '"' + columnDelimiter + '"' + + resp.logtime + '"' + columnDelimiter + resp.logentry.replace(/\\"/g, '""') + lineDelimiter; + } + }); + var blob = new Blob([myCsvData], {type: "text/csv;charset=utf-8"}); + saveAs(blob, "iqb-testcenter-logs.csv"); + } else { + this.snackBar.open('Keine Daten verfügbar.', 'Fehler', {duration: 3000}); + } + this.tableselectionCheckbox.clear(); + this.dataLoading = false; + }) + } + } } diff --git a/src/environments/environment.build.ts b/src/environments/environment.build.ts index fd6970bd..7c7fd335 100644 --- a/src/environments/environment.build.ts +++ b/src/environments/environment.build.ts @@ -5,5 +5,5 @@ export const environment = { testcenterUrl: '/', appName: 'IQB-Testcenter Verwaltung', appPublisher: 'IQB - Institut zur Qualitätsentwicklung im Bildungswesen', - appVersion: '0.5 - 27.9.2018' + appVersion: '0.7 - 8.10.2018' }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 9a83fe20..6b893b1a 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -5,7 +5,7 @@ export const environment = { production: false, - testcenterUrl: 'https://ocba.iqb.hu-berlin.de/', + testcenterUrl: 'https://www.iqb-testcenter.de/', appName: 'IQB-Testcenter Verwaltung', appPublisher: 'IQB - Institut zur Qualitätsentwicklung im Bildungswesen', appVersion: '0.5 (dev)' -- GitLab