Merged branch comments/dashbaord

This commit is contained in:
Nicolai Ort 2020-07-14 18:49:06 +02:00
commit b51bd226ea
2 changed files with 339 additions and 306 deletions

View File

@ -1,119 +1,124 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="content"> <div class="content">
<h3>Dashboard</h3> <h3>Dashboard</h3>
<div> <div>
<div class="row px-3 py-2"> <div class="row px-3 py-2">
<ng-container *ngIf="selectedSprint === undefined">
<h3 class="mr-3 text-primary">Alle Userstories</h3> <!-- Show a message if no sprints exist yet, with a link to the page where a new one can be created -->
<a [routerLink]="['sprints']" <ng-container *ngIf="selectedSprint === undefined">
>Lege einen Sprint an, um Userstories zu organisieren.</a <h3 class="mr-3 text-primary">Alle Userstories</h3>
> <a [routerLink]="['sprints']"
</ng-container> >Lege einen Sprint an, um Userstories zu organisieren.</a
>
<ng-container *ngIf="selectedSprint !== undefined"> </ng-container>
<h3
class="mr-3 text-primary" <ng-container *ngIf="selectedSprint !== undefined">
*ngIf="selectedSprint === currentSprint" <h3
> class="mr-3 text-primary"
Aktueller Sprint: *ngIf="selectedSprint === currentSprint"
</h3> >
<h3 Aktueller Sprint:
class="mr-3 text-primary" </h3>
*ngIf="selectedSprint !== currentSprint" <h3
> class="mr-3 text-primary"
Sprint: *ngIf="selectedSprint !== currentSprint"
</h3> >
<h3 class="mr-3 custom-text-secondary">{{ selectedSprint.title }}</h3> Sprint:
<h3 class="mr-3"> </h3>
{{ toDateString(selectedSprint.startDate) }} - <h3 class="mr-3 custom-text-secondary">{{ selectedSprint.title }}</h3>
{{ toDateString(selectedSprint.endDate) }} <h3 class="mr-3">
</h3> {{ toDateString(selectedSprint.startDate) }} -
{{ toDateString(selectedSprint.endDate) }}
<label class="mr-3"> </h3>
<select
class="select custom-select custom-text-secondary" <label class="mr-3">
[(ngModel)]="selectedSprint" <select
> class="select custom-select custom-text-secondary"
<option class="bg-secondary text-dark" [ngValue]="currentSprint"> [(ngModel)]="selectedSprint"
{{ currentSprint.title }} (aktuell) >
</option> <option class="bg-secondary text-dark" [ngValue]="currentSprint">
<option value="" disabled="disabled" {{ currentSprint.title }} (aktuell)
>─────────────────────────</option </option>
> <option value="" disabled="disabled"
<option >─────────────────────────</option
class="text-dark" >
*ngFor="let sprint of sprints" <option
[ngValue]="sprint" class="text-dark"
> *ngFor="let sprint of sprints"
<ng-container *ngIf="sprint === currentSprint"> [ngValue]="sprint"
{{ sprint.title }} (aktuell) >
</ng-container> <ng-container *ngIf="sprint === currentSprint">
<ng-container *ngIf="sprint !== currentSprint"> {{ sprint.title }} (aktuell)
{{ sprint.title }} ({{ toDateString(sprint.startDate) }} - </ng-container>
{{ toDateString(sprint.endDate) }}) <ng-container *ngIf="sprint !== currentSprint">
</ng-container> {{ sprint.title }} ({{ toDateString(sprint.startDate) }} -
</option> {{ toDateString(sprint.endDate) }})
</select> </ng-container>
</label> </option>
</select>
<span class="mr-5"></span> </label>
<h3
*ngIf="selectedSprint === currentSprint" <!-- If the active sprint is selected: Show the number of remaining days + urgency -->
class="mr-3 custom-text-secondary" <span class="mr-5"></span>
[class.text-success]="getSprintUrgency() === 2" <h3
[class.text-warning]="getSprintUrgency() === 1" *ngIf="selectedSprint === currentSprint"
[class.text-danger]="getSprintUrgency() === 0" class="mr-3 custom-text-secondary"
> [class.text-success]="getSprintUrgency() === 2"
Verbleibende Tage: {{ getRemainingDaysInSprint() }} [class.text-warning]="getSprintUrgency() === 1"
</h3> [class.text-danger]="getSprintUrgency() === 0"
</ng-container> >
</div> Verbleibende Tage: {{ getRemainingDaysInSprint() }}
</h3>
<div class="row"> </ng-container>
<div class="p-3 col-12 col-xl-6 col-lg-6 col-md-12 col-sm-12"> </div>
<div class="card">
<div class="card-header"> <div class="row">
<span class="text-large">
Userstories <!-- Info on the userstories in the selected sprint -->
</span> <div class="p-3 col-12 col-xl-6 col-lg-6 col-md-12 col-sm-12">
</div> <div class="card">
<div class="card-body"> <div class="card-header">
<div <span class="text-large">
*ngIf="selectedSprint !== undefined && usedStatus.length === 0" Userstories
> </span>
Zum Sprint "{{ selectedSprint.title }}" sind aktuell keine </div>
Userstories vorhanden. <div class="card-body">
</div> <div
<canvas id="done-stories-chart"></canvas> *ngIf="selectedSprint !== undefined && usedStatus.length === 0"
</div> >
</div> Zum Sprint "{{ selectedSprint.title }}" sind aktuell keine
<div class="card-deck"> Userstories vorhanden.
<div class="card text-center" *ngFor="let status of usedStatus"> </div>
<div class="card-header"> <canvas id="done-stories-chart"></canvas>
<span class="text-large"> </div>
{{ status.title }} </div>
</span> <div class="card-deck">
</div> <div class="card text-center" *ngFor="let status of usedStatus">
<div class="card-body"> <div class="card-header">
<span class="text-very-large"> <span class="text-large">
<b>{{ getNumberOfUserstoriesByStatus(status) }}</b> {{ status.title }}
</span> </span>
</div> </div>
</div> <div class="card-body">
</div> <span class="text-very-large">
</div> <b>{{ getNumberOfUserstoriesByStatus(status) }}</b>
</span>
<div class="p-3 col-12 col-xl-6 col-lg-6 col-md-12 col-sm-12"> </div>
<div class="card"> </div>
<div class="card-body"> </div>
<app-userstory-inner-table </div>
[items]="selectedSprintUserstories"
></app-userstory-inner-table> <div class="p-3 col-12 col-xl-6 col-lg-6 col-md-12 col-sm-12">
</div> <div class="card">
</div> <div class="card-body">
</div> <app-userstory-inner-table
</div> [items]="selectedSprintUserstories"
</div> ></app-userstory-inner-table>
</div> </div>
</div> </div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,187 +1,215 @@
import { Component, OnChanges } from '@angular/core'; import { Component } from '@angular/core';
import { forkJoin } from 'rxjs'; import { forkJoin } from 'rxjs';
import Chart from 'chart.js'; import Chart from 'chart.js';
import { import { BackendService, ScrumSprint, ScrumStatus, ScrumUserstory } from '../../services/backend.service';
BackendService,
ScrumSprint, @Component({
ScrumStatus, selector: 'app-dashboard',
ScrumUserstory, templateUrl: 'dashboard.component.html',
} from '../../services/backend.service'; styleUrls: ['./dashboard.component.css'],
})
@Component({ export class DashboardComponent {
selector: 'app-dashboard', /**
templateUrl: 'dashboard.component.html', * Returns the status that are used by at least one userstory.
styleUrls: ['./dashboard.component.css'], */
}) public get usedStatus(): ScrumStatus[] {
export class DashboardComponent { return this.status.filter(
/** (s) =>
* Returns the status that are used by at least one userstory. this.selectedSprintUserstories.find((us) => us.statusid === s.id) !==
*/ undefined
public get usedStatus(): ScrumStatus[] { );
return this.status.filter( }
(s) =>
this.selectedSprintUserstories.find((us) => us.statusid === s.id) !== /**
undefined * Returns all userstories in the selected sprint.
); */
} public get selectedSprintUserstories(): ScrumUserstory[] {
if (this.selectedSprint === undefined) {
public get selectedSprintUserstories(): ScrumUserstory[] { return this.userstories;
if (this.selectedSprint === undefined) { }
return this.userstories; return this.userstories.filter(
} (us) => us.sprintid === this.selectedSprint.id
return this.userstories.filter( );
(us) => us.sprintid === this.selectedSprint.id }
);
} /**
* Returns the currently active sprint.
public get currentSprint(): ScrumSprint { * If multiple sprints are active at the current time, any one of them may be returned.
const now = Date.now(); */
return this.sprints.find( public get currentSprint(): ScrumSprint {
(s) => Date.parse(s.startDate) < now && Date.parse(s.endDate) > now const now = Date.now();
); return this.sprints.find(
} (s) => Date.parse(s.startDate) < now && Date.parse(s.endDate) > now
);
private _selectedSprint: ScrumSprint; }
public get selectedSprint(): ScrumSprint {
if (this._selectedSprint === undefined) { private _selectedSprint: ScrumSprint;
if (this.currentSprint === undefined) {
return this.sprints[0]; /**
} * Returns the sprint selected by the user.
return this.currentSprint; */
} public get selectedSprint(): ScrumSprint {
return this._selectedSprint; if (this._selectedSprint === undefined) {
} if (this.currentSprint === undefined) {
return this.sprints[0];
public set selectedSprint(value) { }
this._selectedSprint = value; return this.currentSprint;
this.createChart(); }
} return this._selectedSprint;
}
public status: ScrumStatus[] = [];
public userstories: ScrumUserstory[] = []; /**
public sprints: ScrumSprint[] = []; * Sets the sprint selected by the user.
*/
public chart: Chart; public set selectedSprint(value) {
this._selectedSprint = value;
constructor(private backendService: BackendService) { this.createChart();
// download userstories, status and sprints and update }
// the chart whenever a new response is there
forkJoin([ /** All status. */
backendService.getUserstories(), public status: ScrumStatus[] = [];
backendService.getAllStatus(),
backendService.getSprints(), /** All userstories. */
]).subscribe((results) => { public userstories: ScrumUserstory[] = [];
const [userstoryResponse, statusResponse, sprintResponse] = results;
if ( /** All sprints. */
userstoryResponse.status > 399 || public sprints: ScrumSprint[] = [];
statusResponse.status > 399 ||
sprintResponse.status > 399 /**
) { * The pie chart showing the userstories in the selected sprint.
alert('Fehler'); */
} else { public chart: Chart;
this.userstories.push(...userstoryResponse.body);
this.status.push(...statusResponse.body); constructor(private backendService: BackendService) {
this.sprints.push(...sprintResponse.body); // download userstories, status and sprints and update
this.createChart(); // the chart whenever a new response is there
} forkJoin([
}); backendService.getUserstories(),
} backendService.getAllStatus(),
backendService.getSprints(),
/** ]).subscribe((results) => {
* Returns the date in the following format: 1.7.2020 const [userstoryResponse, statusResponse, sprintResponse] = results;
*/ if (
public toDateString(isoFormatString) { userstoryResponse.status > 399 ||
const date = new Date(isoFormatString); statusResponse.status > 399 ||
return `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()}`; sprintResponse.status > 399
} ) {
alert('Fehler');
private createChart() { } else {
// @ts-ignore this.userstories.push(...userstoryResponse.body);
const context = document.getElementById('done-stories-chart').getContext('2d'); this.status.push(...statusResponse.body);
this.sprints.push(...sprintResponse.body);
if (this.usedStatus.length === 0) { this.createChart();
this.chart.destroy(); }
} else { });
this.chart = new Chart(context, { }
type: 'pie',
data: { /**
labels: this.usedStatus.map((s) => s.title), * Returns the date in the following format: 1.7.2020
datasets: [ * @param dateValue an integer representing the number of milliseconds since the
{ * ECMAScript epoch -or- a string in a format recognized by the Date.parse() method.
data: this.usedStatus.map((s) => */
this.getNumberOfUserstoriesByStatus(s) public toDateString(dateValue) {
), const date = new Date(dateValue);
backgroundColor: this.getBackgroundColors(), return `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()}`;
options: { }
legend: {
display: true, private createChart() {
position: 'right', // @ts-ignore
labels: { const context = document.getElementById('done-stories-chart').getContext('2d');
fontColor: 'rgba(255, 255, 255, 0.8)',
}, if (this.usedStatus.length === 0) {
}, this.chart.destroy();
}, } else {
}, this.chart = new Chart(context, {
], type: 'pie',
}, data: {
}); labels: this.usedStatus.map((s) => s.title),
this.chart.update(); datasets: [
this.chart.render(); {
} data: this.usedStatus.map((s) =>
} this.getNumberOfUserstoriesByStatus(s)
),
private getBackgroundColors(): string[] { backgroundColor: this.getBackgroundColors(),
const baseColors = [ options: {
'rgb(255, 153, 102)', legend: {
'rgb(255, 102, 102)', display: true,
'rgb(153, 204, 255)', position: 'right',
'rgb(102, 153, 102)', labels: {
'rgb(204, 204, 153)', fontColor: 'rgba(255, 255, 255, 0.8)',
'rgb(153, 102, 204)', },
'rgb(204, 102, 102)', },
'rgb(255, 204, 153)', },
'rgb(153, 102, 255)', },
'rgb(204, 204, 204)', ],
'rgb(102, 255, 204)', },
'rgb(102, 153, 255)', });
'rgb(153, 102, 153)', this.chart.update();
'rgb(204, 204, 255)', this.chart.render();
]; }
const colors = []; }
while (colors.length < this.usedStatus.length) {
colors.push(...baseColors); /**
} * Returns a sufficiently large array of background colors to be used in the chart.
return colors; */
} private getBackgroundColors(): string[] {
const baseColors = [
public getNumberOfUserstoriesByStatus(status: ScrumStatus): number { 'rgb(255, 153, 102)',
return this.selectedSprintUserstories.filter( 'rgb(255, 102, 102)',
(us) => us.statusid === status.id 'rgb(153, 204, 255)',
).length; 'rgb(102, 153, 102)',
} 'rgb(204, 204, 153)',
'rgb(153, 102, 204)',
public getRemainingDaysInSprint(): number { 'rgb(204, 102, 102)',
if (this.selectedSprint === undefined) { 'rgb(255, 204, 153)',
return undefined; 'rgb(153, 102, 255)',
} 'rgb(204, 204, 204)',
return Math.floor( 'rgb(102, 255, 204)',
(Date.parse(this.selectedSprint.endDate) - Date.now()) / 86400000 'rgb(102, 153, 255)',
); 'rgb(153, 102, 153)',
} 'rgb(204, 204, 255)',
];
/** const colors = [];
* Returns the "urgency" of the current sprint (ie what percentage of the sprint is remaining) while (colors.length < this.usedStatus.length) {
* as an integer from 0 (most urgent) to 2 (least urgent). colors.push(...baseColors);
*/ }
public getSprintUrgency(): number { return colors;
const now = Date.now(); }
const sprint = this.selectedSprint;
if (sprint === undefined) { /**
return undefined; * Returns the number of userstories in the selected sprint that have the given status.
} */
const deltaFromNow = Date.parse(sprint.endDate) - now; public getNumberOfUserstoriesByStatus(status: ScrumStatus): number {
const deltaFromStart = return this.selectedSprintUserstories.filter(
Date.parse(sprint.endDate) - Date.parse(sprint.startDate); (us) => us.statusid === status.id
return Math.floor((3 * deltaFromNow) / deltaFromStart); ).length;
} }
}
/**
* Returns the days remaining until the end date of the selected sprint.
*/
public getRemainingDaysInSprint(): number {
if (this.selectedSprint === undefined) {
return undefined;
}
return Math.floor(
(Date.parse(this.selectedSprint.endDate) - Date.now()) / 86400000
);
}
/**
* Returns the "urgency" of the current sprint (ie what percentage of the sprint is remaining)
* as an integer from 0 (most urgent) to 2 (least urgent).
*/
public getSprintUrgency(): number {
const now = Date.now();
const sprint = this.selectedSprint;
if (sprint === undefined) {
return undefined;
}
const deltaFromNow = Date.parse(sprint.endDate) - now;
const deltaFromStart =
Date.parse(sprint.endDate) - Date.parse(sprint.startDate);
return Math.floor((3 * deltaFromNow) / deltaFromStart);
}
}