Refine dashboard
This commit is contained in:
parent
a27de02468
commit
301407c0c4
1764
package-lock.json
generated
1764
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,6 +23,7 @@
|
|||||||
"@ng-bootstrap/ng-bootstrap": "^6.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^6.0.0",
|
||||||
"bootstrap": "^4.4.0",
|
"bootstrap": "^4.4.0",
|
||||||
"chart.js": "^2.9.3",
|
"chart.js": "^2.9.3",
|
||||||
|
"component": "^1.1.0",
|
||||||
"rxjs": "~6.5.4",
|
"rxjs": "~6.5.4",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
"zone.js": "~0.10.2"
|
"zone.js": "~0.10.2"
|
||||||
|
@ -14,7 +14,8 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { UserstoryTableComponent } from './userstory-table/userstory-table.component';
|
import { UserstoryTableComponent } from './userstory-table/userstory-table.component';
|
||||||
import { TaskTableComponent } from './task-table/task-table.component';
|
import { TaskTableComponent } from './task-table/task-table.component';
|
||||||
import { SprintTableComponent } from './sprint-table/sprint-table.component';
|
import { SprintTableComponent } from './sprint-table/sprint-table.component';
|
||||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
import { UserstoryInnerTableComponent } from './userstory-inner-table/userstory-inner-table.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -27,6 +28,7 @@ import { DashboardComponent } from './dashboard/dashboard.component';
|
|||||||
SprintFormComponent,
|
SprintFormComponent,
|
||||||
SprintTableComponent,
|
SprintTableComponent,
|
||||||
DashboardComponent,
|
DashboardComponent,
|
||||||
|
UserstoryInnerTableComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
.text-2em {
|
.text-large {
|
||||||
font-size: 2rem;
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-very-large {
|
||||||
|
font-size: 2.4rem;
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,98 @@
|
|||||||
<div class="container-fluid">
|
<div class="mx-5 my-3">
|
||||||
|
|
||||||
<div class="row px-3 py-2">
|
<div class="row px-3 py-2">
|
||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row px-3 py-2">
|
<div class="row px-3 py-2">
|
||||||
<h2>Current sprint</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<ng-container *ngIf="selectedSprint === undefined">
|
||||||
|
<h3 class="mr-3 text-primary">Alle Userstories</h3>
|
||||||
|
<a [routerLink]="['sprints']">Lege einen Sprint an, um Userstories zu organisieren.</a>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<div class="p-3 xl-col-6 lg-col-6 md-col-12 sm-col-12 xs-col-12">
|
<ng-container *ngIf="selectedSprint !== undefined">
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
<h3 class="mr-3 text-primary" *ngIf="selectedSprint === currentSprint">Aktueller Sprint:</h3>
|
||||||
|
<h3 class="mr-3 text-primary" *ngIf="selectedSprint !== currentSprint">Sprint:</h3>
|
||||||
|
<h3 class="mr-3 custom-text-secondary">{{selectedSprint.title}}</h3>
|
||||||
|
<h3 class="mr-3">{{toDateString(selectedSprint.startDate)}} - {{toDateString(selectedSprint.endDate)}}</h3>
|
||||||
|
|
||||||
|
<label class="mr-3">
|
||||||
|
<select class="select custom-select custom-text-secondary" [(ngModel)]="selectedSprint">
|
||||||
|
<option class="bg-secondary text-dark" [ngValue]="currentSprint">
|
||||||
|
{{currentSprint.title}} (aktuell)
|
||||||
|
</option>
|
||||||
|
<option value="" disabled="disabled">─────────────────────────</option>
|
||||||
|
<option class="text-dark" *ngFor="let sprint of sprints" [ngValue]="sprint">
|
||||||
|
<ng-container *ngIf="sprint === currentSprint">
|
||||||
|
{{sprint.title}} (aktuell)
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="sprint !== currentSprint">
|
||||||
|
{{sprint.title}} ({{toDateString(sprint.startDate)}} - {{toDateString(sprint.endDate)}})
|
||||||
|
</ng-container>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<span class="mr-5"></span>
|
||||||
|
<h3
|
||||||
|
*ngIf="selectedSprint === currentSprint"
|
||||||
|
class="mr-3 custom-text-secondary"
|
||||||
|
[class.text-success]="getSprintUrgency() === 2"
|
||||||
|
[class.text-warning]="getSprintUrgency() === 1"
|
||||||
|
[class.text-danger]="getSprintUrgency() === 0"
|
||||||
|
>
|
||||||
|
Verbleibende Tage: {{getRemainingDaysInSprint()}}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="p-3 col-12 col-xl-6 col-lg-6 col-md-12 col-sm-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="text-large">
|
||||||
Userstories
|
Userstories
|
||||||
</div>
|
</span>
|
||||||
<div class="card-body">
|
|
||||||
<canvas id="done-stories-chart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="card-body">
|
||||||
|
<div *ngIf="selectedSprint !== undefined && usedStatus.length === 0">
|
||||||
<div class="p-3 xl-col-6 lg-col-6 md-col-12 sm-col-12 xs-col-12">
|
Zum Sprint "{{selectedSprint.title}}" sind aktuell keine Userstories vorhanden.
|
||||||
<div class="card-deck">
|
</div>
|
||||||
|
<canvas id="done-stories-chart"></canvas>
|
||||||
<div
|
</div>
|
||||||
class="card"
|
</div>
|
||||||
[class.border-danger]="getSprintUrgency() == 0"
|
<div class="card-deck">
|
||||||
[class.border-warning]="getSprintUrgency() == 1"
|
<div
|
||||||
[class.border-success]="getSprintUrgency() == 2"
|
class="card text-center"
|
||||||
*ngIf="getRemainingDaysInSprint() !== undefined"
|
*ngFor="let status of usedStatus"
|
||||||
>
|
>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Days remaining
|
<span class="text-large">
|
||||||
|
{{status.title}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<span class="text-very-large">
|
||||||
|
<b>{{getNumberOfUserstoriesByStatus(status)}}</b>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body text-center">
|
|
||||||
<span class="text-2em">
|
|
||||||
{{getRemainingDaysInSprint()}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" *ngFor="let status of usedStatus">
|
|
||||||
<div class="card-header">
|
|
||||||
Userstories: {{status.title}}
|
|
||||||
</div>
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<span class="text-2em">
|
|
||||||
{{getNumberOfUserstoriesByStatus(status)}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 col-12 col-xl-6 col-lg-6 col-md-12 col-sm-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<app-userstory-inner-table [items]="selectedSprintUserstories"></app-userstory-inner-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,79 +1,111 @@
|
|||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnChanges} from '@angular/core';
|
||||||
|
import {forkJoin} from 'rxjs';
|
||||||
import Chart from 'chart.js';
|
import Chart from 'chart.js';
|
||||||
import {BackendService, ScrumStatus, ScrumUser, ScrumUserstory, ScrumSprint} from '../services/backend.service';
|
import {BackendService, ScrumSprint, ScrumStatus, ScrumUserstory} from '../services/backend.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
templateUrl: 'dashboard.component.html',
|
templateUrl: 'dashboard.component.html',
|
||||||
styleUrls: ['./dashboard.component.css']
|
styleUrls: ['./dashboard.component.css']
|
||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit {
|
export class DashboardComponent {
|
||||||
/**
|
/**
|
||||||
* Returns the status that are used by at least one userstory.
|
* Returns the status that are used by at least one userstory.
|
||||||
*/
|
*/
|
||||||
public get usedStatus(): ScrumStatus[] {
|
public get usedStatus(): ScrumStatus[] {
|
||||||
return this.status.filter(s => this.userstories.find(us => us.statusid === s.id) !== undefined);
|
return this.status.filter(s => this.selectedSprintUserstories.find(us => us.statusid === s.id) !== undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
private status: ScrumStatus[];
|
public get selectedSprintUserstories(): ScrumUserstory[] {
|
||||||
private userstories: ScrumUserstory[];
|
if (this.selectedSprint === undefined) {
|
||||||
private sprints: ScrumSprint[];
|
return this.userstories;
|
||||||
|
}
|
||||||
|
return this.userstories.filter(us => us.sprintid === this.selectedSprint.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get currentSprint(): ScrumSprint {
|
||||||
|
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) {
|
||||||
|
if (this.currentSprint === undefined) {
|
||||||
|
return this.sprints[0];
|
||||||
|
}
|
||||||
|
return this.currentSprint;
|
||||||
|
}
|
||||||
|
return this._selectedSprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set selectedSprint(value) {
|
||||||
|
this._selectedSprint = value;
|
||||||
|
this.createChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
public status: ScrumStatus[] = [];
|
||||||
|
public userstories: ScrumUserstory[] = [];
|
||||||
|
public sprints: ScrumSprint[] = [];
|
||||||
|
|
||||||
|
public chart: Chart;
|
||||||
|
|
||||||
constructor(private backendService: BackendService) {
|
constructor(private backendService: BackendService) {
|
||||||
// backendService.getUserstories().subscribe(response => {
|
// download userstories, status and sprints and update
|
||||||
// if (response.status > 399) {
|
// the chart whenever a new response is there
|
||||||
// alert('Fehler');
|
forkJoin([backendService.getUserstories(), backendService.getAllStatus(), backendService.getSprints()])
|
||||||
// } else {
|
.subscribe(results => {
|
||||||
// this.userstories.push(...response.body);
|
const [userstoryResponse, statusResponse, sprintResponse] = results;
|
||||||
// }
|
if (userstoryResponse.status > 399 || statusResponse.status > 399 || sprintResponse.status > 399) {
|
||||||
// });
|
alert('Fehler');
|
||||||
// backendService.getAllStatus().subscribe(response => {
|
} else {
|
||||||
// if (response.status > 399) {
|
this.userstories.push(...userstoryResponse.body);
|
||||||
// alert('Fehler');
|
this.status.push(...statusResponse.body);
|
||||||
// } else {
|
this.sprints.push(...sprintResponse.body);
|
||||||
// this.status.push(...response.body);
|
this.createChart();
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// backendService.getSprints().subscribe(response => {
|
|
||||||
// if (response.status > 399) {
|
|
||||||
// alert('Fehler');
|
|
||||||
// } else {
|
|
||||||
// this.sprints.push(...response.body);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
this.status = [
|
|
||||||
{id: 0, title: "In progress", description:""},
|
|
||||||
{id: 1, title: "Done", description:""},
|
|
||||||
];
|
|
||||||
this.userstories = [
|
|
||||||
{statusid: 0, title:""},
|
|
||||||
{statusid: 0, title:""},
|
|
||||||
{statusid: 0, title:""},
|
|
||||||
{statusid: 1, title:""},
|
|
||||||
{statusid: 1, title:""},
|
|
||||||
];
|
|
||||||
this.sprints = [
|
|
||||||
{description:"", title:"", project: 0, startDate: new Date(2020, 5, 22), endDate: new Date(2020, 5, 28)},
|
|
||||||
{description:"", title:"", project: 0, startDate: new Date(2020, 5, 29), endDate: new Date(2020, 6, 5)},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the date in the following format: 1.7.2020
|
||||||
|
*/
|
||||||
|
public toDateString(isoFormatString) {
|
||||||
|
const date = new Date(isoFormatString);
|
||||||
|
return `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()}`;
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
private createChart() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const context = document.getElementById('done-stories-chart').getContext('2d');
|
const context = document.getElementById('done-stories-chart').getContext('2d');
|
||||||
const chart = new Chart(context, {
|
|
||||||
type: 'pie',
|
if (this.usedStatus.length === 0) {
|
||||||
data: {
|
this.chart.destroy();
|
||||||
labels: this.usedStatus.map(s => s.title),
|
}
|
||||||
datasets: [{
|
|
||||||
label: 'Done stories',
|
else {
|
||||||
data: this.usedStatus.map(s => this.getNumberOfUserstoriesByStatus(s)),
|
this.chart = new Chart(context, {
|
||||||
backgroundColor: this.getBackgroundColors(),
|
type: 'pie',
|
||||||
}]
|
data: {
|
||||||
}
|
labels: this.usedStatus.map(s => s.title),
|
||||||
});
|
datasets: [{
|
||||||
|
data: this.usedStatus.map(s => this.getNumberOfUserstoriesByStatus(s)),
|
||||||
|
backgroundColor: this.getBackgroundColors(),
|
||||||
|
options: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
labels: {
|
||||||
|
fontColor: 'rgba(255, 255, 255, 0.8)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.chart.update();
|
||||||
|
this.chart.render();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBackgroundColors(): string[] {
|
private getBackgroundColors(): string[] {
|
||||||
@ -101,27 +133,28 @@ export class DashboardComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getNumberOfUserstoriesByStatus(status: ScrumStatus): number {
|
public getNumberOfUserstoriesByStatus(status: ScrumStatus): number {
|
||||||
return this.userstories.filter(us => us.statusid === status.id).length;
|
return this.selectedSprintUserstories.filter(us => us.statusid === status.id).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRemainingDaysInSprint(): number {
|
public getRemainingDaysInSprint(): number {
|
||||||
const now = new Date();
|
if (this.selectedSprint === undefined) {
|
||||||
const currentSprint = this.sprints.find(s => s.endDate > now && s.startDate < now);
|
|
||||||
if (currentSprint === undefined) {
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const daysDelta = Math.floor((currentSprint.endDate.getTime() - now.getTime()) / 86400000);
|
return Math.floor((Date.parse(this.selectedSprint.endDate) - Date.now()) / 86400000);
|
||||||
return daysDelta;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
public getSprintUrgency(): number {
|
||||||
const now = new Date();
|
const now = Date.now();
|
||||||
const currentSprint = this.sprints.find(s => s.endDate > now && s.startDate < now);
|
const sprint = this.selectedSprint;
|
||||||
if (currentSprint === undefined) {
|
if (sprint === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const deltaFromNow = currentSprint.endDate.getTime() - now.getTime();
|
const deltaFromNow = Date.parse(sprint.endDate) - now;
|
||||||
const deltaFromStart = currentSprint.endDate.getTime() - currentSprint.startDate.getTime();
|
const deltaFromStart = Date.parse(sprint.endDate) - Date.parse(sprint.startDate);
|
||||||
return Math.floor(3 * deltaFromNow / deltaFromStart);
|
return Math.floor(3 * deltaFromNow / deltaFromStart);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,8 +233,8 @@ export interface ScrumSprint{
|
|||||||
id?: number;
|
id?: number;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
startDate: Date;
|
startDate: string;
|
||||||
endDate: Date;
|
endDate: string;
|
||||||
project: number;
|
project: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
|||||||
import { getNumberForPriority } from '../services/sorting.service';
|
import { getNumberForPriority } from '../services/sorting.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-userstory-table',
|
selector: 'app-task-table',
|
||||||
templateUrl: './task-table.component.html',
|
templateUrl: './task-table.component.html',
|
||||||
styleUrls: ['./task-table.component.css'],
|
styleUrls: ['./task-table.component.css'],
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
<button class="btn btn-secondary my-3" (click)="openUserstoryForm()">Neue Userstory</button>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th (click)="sortById()" class="sortable">
|
||||||
|
<span>ID</span>
|
||||||
|
<span *ngIf="sortBy === 'id'" class="pl-3">
|
||||||
|
<span *ngIf="sortDescending"><i class="fa fa-sort-up"></i></span>
|
||||||
|
<span *ngIf="sortDescending === false"><i class="fa fa-sort-down"></i></span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th (click)="sortByTitle()" class="sortable">
|
||||||
|
<span>Titel</span>
|
||||||
|
<span *ngIf="sortBy === 'title'" class="pl-3">
|
||||||
|
<span *ngIf="sortDescending"><i class="fa fa-sort-up"></i></span>
|
||||||
|
<span *ngIf="sortDescending === false"><i class="fa fa-sort-down"></i></span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th (click)="sortByTasks()" class="sortable">
|
||||||
|
<span>Tasks</span>
|
||||||
|
<span *ngIf="sortBy === 'tasks'" class="pl-3">
|
||||||
|
<span *ngIf="sortDescending"><i class="fa fa-sort-up"></i></span>
|
||||||
|
<span *ngIf="sortDescending === false"><i class="fa fa-sort-down"></i></span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th (click)="sortByStatus()" class="sortable">
|
||||||
|
<span>Status</span>
|
||||||
|
<span *ngIf="sortBy === 'statusid'" class="pl-3">
|
||||||
|
<span *ngIf="sortDescending"><i class="fa fa-sort-up"></i></span>
|
||||||
|
<span *ngIf="sortDescending === false"><i class="fa fa-sort-down"></i></span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th (click)="sortByPrio()" class="sortable">
|
||||||
|
<span>Priorität</span>
|
||||||
|
<label class="pl-3" (click)="$event.stopPropagation()">
|
||||||
|
<select [(ngModel)]="filterPriority">
|
||||||
|
<option [ngValue]="null" selected></option>
|
||||||
|
<option *ngFor="let p of getAllPriorities()" [ngValue]="p">{{p}}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<span *ngIf="sortBy === 'priority'" class="pl-3">
|
||||||
|
<span *ngIf="sortDescending"><i class="fa fa-sort-up"></i></span>
|
||||||
|
<span *ngIf="sortDescending === false"><i class="fa fa-sort-down"></i></span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th (click)="sortByCategory()" class="sortable">
|
||||||
|
<span>Category</span>
|
||||||
|
<span *ngIf="sortBy === 'categoryid'" class="pl-3">
|
||||||
|
<span *ngIf="sortDescending"><i class="fa fa-sort-up"></i></span>
|
||||||
|
<span *ngIf="sortDescending === false"><i class="fa fa-sort-down"></i></span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let userstory of filteredItems" [class.table-info]="userstory.id === highlightId">
|
||||||
|
<td>{{userstory.id}}</td>
|
||||||
|
<td>{{userstory.title}}</td>
|
||||||
|
<td>
|
||||||
|
<a [routerLink]="['/tasks', {userstoryId: userstory.id}]">
|
||||||
|
{{getNumberOfTasks(userstory)}} Tasks
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a [routerLink]="['/status', {id: userstory.statusid}]">
|
||||||
|
{{getStatusTitleById(userstory.statusid)}}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{userstory.priority}}</td>
|
||||||
|
<td>
|
||||||
|
<a [routerLink]="['/categories', {id: userstory.categoryid}]">
|
||||||
|
{{getCategoryTitleById(userstory.categoryid)}}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" rel="tooltip" (click)="openUserstoryForm(userstory)" class="btn btn-success btn-sm btn-icon">
|
||||||
|
<i class="fa fa-pencil-alt"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" rel="tooltip" (click)="deleteUserstory(userstory)" class="btn btn-danger btn-sm btn-icon">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
143
src/app/userstory-inner-table/userstory-inner-table.component.ts
Normal file
143
src/app/userstory-inner-table/userstory-inner-table.component.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import {
|
||||||
|
BackendService,
|
||||||
|
ScrumTask,
|
||||||
|
ScrumUserstory,
|
||||||
|
ScrumStatus,
|
||||||
|
ScrumCategory,
|
||||||
|
} from '../services/backend.service';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TableComponentBase } from '../services/table-component.base';
|
||||||
|
import { getNumberForPriority } from '../services/sorting.service';
|
||||||
|
import { UserstoryFormComponent } from '../userstory-form/userstory-form.component';
|
||||||
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-userstory-inner-table',
|
||||||
|
templateUrl: './userstory-inner-table.component.html',
|
||||||
|
styleUrls: ['./userstory-inner-table.component.css']
|
||||||
|
})
|
||||||
|
export class UserstoryInnerTableComponent extends TableComponentBase<ScrumUserstory> {
|
||||||
|
public tasks: ScrumTask[] = [];
|
||||||
|
public filterPriority: string | null = null;
|
||||||
|
public highlightId: number;
|
||||||
|
public status: ScrumStatus[] = [];
|
||||||
|
public categories: ScrumCategory[] = [];
|
||||||
|
|
||||||
|
@Input() public items: ScrumUserstory[] = [];
|
||||||
|
|
||||||
|
public get filteredItems() {
|
||||||
|
return this.items.filter(
|
||||||
|
(task) =>
|
||||||
|
this.filterPriority === null || task.priority === this.filterPriority
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private backendService: BackendService,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.applyFilterParameters(this.route.snapshot.paramMap);
|
||||||
|
this.route.paramMap.subscribe((map) => this.applyFilterParameters(map));
|
||||||
|
|
||||||
|
backendService.getTasks().subscribe((response) => {
|
||||||
|
if (response.status > 399) {
|
||||||
|
alert('Fehler');
|
||||||
|
} else {
|
||||||
|
this.tasks.push(...response.body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
backendService.getAllStatus().subscribe((response) => {
|
||||||
|
if (response.status > 399) {
|
||||||
|
alert('Fehler');
|
||||||
|
} else {
|
||||||
|
this.status.push(...response.body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
backendService.getCategories().subscribe((response) => {
|
||||||
|
if (response.status > 399) {
|
||||||
|
alert('Fehler');
|
||||||
|
} else {
|
||||||
|
this.categories.push(...response.body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyFilterParameters(params: ParamMap) {
|
||||||
|
if (params.has('id')) {
|
||||||
|
this.highlightId = parseInt(params.get('id'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public deleteUserstory(userstory: ScrumUserstory) {
|
||||||
|
this.backendService.deleteUserstory(userstory).subscribe((response) => {
|
||||||
|
if (response.status > 399) {
|
||||||
|
alert('Fehler');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const index = this.items.indexOf(userstory);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.items.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public openUserstoryForm(editUserstory?: ScrumUserstory) {
|
||||||
|
const modalRef = this.modalService.open(UserstoryFormComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
keyboard: true,
|
||||||
|
});
|
||||||
|
if (editUserstory === null) {
|
||||||
|
modalRef.result.then((result) => {
|
||||||
|
this.items.push(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
modalRef.componentInstance.userstory = editUserstory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNumberOfTasks(userstory: ScrumUserstory) {
|
||||||
|
return this.tasks.filter((t) => t.userstoryid === userstory.id).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sortById() {
|
||||||
|
this.doNumericSort('id', (us) => us.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sortByTitle() {
|
||||||
|
this.doStringSort('title', (us) => us.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sortByPrio() {
|
||||||
|
this.doNumericSort('priority', (us) => getNumberForPriority(us.priority));
|
||||||
|
}
|
||||||
|
|
||||||
|
public sortByTasks() {
|
||||||
|
this.doNumericSort('tasks', (us) => this.getNumberOfTasks(us));
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByStatus() {
|
||||||
|
this.doNumericSort('statusid', (us) => us.statusid);
|
||||||
|
}
|
||||||
|
sortByCategory() {
|
||||||
|
this.doNumericSort('categoryid', (us) => us.categoryid);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatusTitleById(id) {
|
||||||
|
var status = this.status.find((x) => x.id === id);
|
||||||
|
if (!status) {
|
||||||
|
return 'N/A';
|
||||||
|
}
|
||||||
|
return status.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategoryTitleById(id) {
|
||||||
|
var category = this.categories.find((x) => x.id === id);
|
||||||
|
if (!category) {
|
||||||
|
return 'N/A';
|
||||||
|
}
|
||||||
|
return category.title;
|
||||||
|
}
|
||||||
|
}
|
@ -1,95 +1,4 @@
|
|||||||
<div class="mx-5 my-3">
|
<div class="mx-5 my-3">
|
||||||
|
|
||||||
<h3 class="my-1">Userstories</h3>
|
<h3 class="my-1">Userstories</h3>
|
||||||
|
<app-userstory-inner-table [items]="items"></app-userstory-inner-table>
|
||||||
<button class="btn btn-secondary my-3" (click)="openUserstoryForm()">Neue Userstory</button>
|
|
||||||
|
|
||||||
<table class="table">
|
|
||||||
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th (click)="sortById()" class="sortable">
|
|
||||||
<span>ID</span>
|
|
||||||
<span *ngIf="sortBy === 'id'" class="pl-3">
|
|
||||||
<span *ngIf="sortDescending"><i class="fa fa-sort-up"></i></span>
|
|
||||||
<span *ngIf="sortDescending === false"><i class="fa fa-sort-down"></i></span>
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
<th (click)="sortByTitle()" class="sortable">
|
|
||||||
<span>Titel</span>
|
|
||||||
<span *ngIf="sortBy === 'title'" class="pl-3">
|
|
||||||
<span *ngIf="sortDescending"><i class="fa fa-sort-up"></i></span>
|
|
||||||
<span *ngIf="sortDescending === false"><i class="fa fa-sort-down"></i></span>
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
<th (click)="sortByTasks()" class="sortable">
|
|
||||||
<span>Tasks</span>
|
|
||||||
<span *ngIf="sortBy === 'tasks'" class="pl-3">
|
|
||||||
<span *ngIf="sortDescending"><i class="fa fa-sort-up"></i></span>
|
|
||||||
<span *ngIf="sortDescending === false"><i class="fa fa-sort-down"></i></span>
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
<th (click)="sortByStatus()" class="sortable">
|
|
||||||
<span>Status</span>
|
|
||||||
<span *ngIf="sortBy === 'statusid'" class="pl-3">
|
|
||||||
<span *ngIf="sortDescending"><i class="fa fa-sort-up"></i></span>
|
|
||||||
<span *ngIf="sortDescending === false"><i class="fa fa-sort-down"></i></span>
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
<th (click)="sortByPrio()" class="sortable">
|
|
||||||
<span>Priorität</span>
|
|
||||||
<label class="pl-3" (click)="$event.stopPropagation()">
|
|
||||||
<select [(ngModel)]="filterPriority">
|
|
||||||
<option [ngValue]="null" selected></option>
|
|
||||||
<option *ngFor="let p of getAllPriorities()" [ngValue]="p">{{p}}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<span *ngIf="sortBy === 'priority'" class="pl-3">
|
|
||||||
<span *ngIf="sortDescending"><i class="fa fa-sort-up"></i></span>
|
|
||||||
<span *ngIf="sortDescending === false"><i class="fa fa-sort-down"></i></span>
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
<th (click)="sortByCategory()" class="sortable">
|
|
||||||
<span>Category</span>
|
|
||||||
<span *ngIf="sortBy === 'categoryid'" class="pl-3">
|
|
||||||
<span *ngIf="sortDescending"><i class="fa fa-sort-up"></i></span>
|
|
||||||
<span *ngIf="sortDescending === false"><i class="fa fa-sort-down"></i></span>
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let userstory of filteredItems" [class.table-info]="userstory.id === highlightId">
|
|
||||||
<td>{{userstory.id}}</td>
|
|
||||||
<td>{{userstory.title}}</td>
|
|
||||||
<td>
|
|
||||||
<a [routerLink]="['/tasks', {userstoryId: userstory.id}]">
|
|
||||||
{{getNumberOfTasks(userstory)}} Tasks
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a [routerLink]="['/status', {id: userstory.statusid}]">
|
|
||||||
{{getStatusTitleById(userstory.statusid)}}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>{{userstory.priority}}</td>
|
|
||||||
<td>
|
|
||||||
<a [routerLink]="['/categories', {id: userstory.categoryid}]">
|
|
||||||
{{getCategoryTitleById(userstory.categoryid)}}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button type="button" rel="tooltip" (click)="openUserstoryForm(userstory)" class="btn btn-success btn-sm btn-icon">
|
|
||||||
<i class="fa fa-pencil-alt"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" rel="tooltip" (click)="deleteUserstory(userstory)" class="btn btn-danger btn-sm btn-icon">
|
|
||||||
<i class="fa fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,49 +1,18 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
BackendService,
|
BackendService,
|
||||||
ScrumTask,
|
|
||||||
ScrumUserstory,
|
ScrumUserstory,
|
||||||
ScrumStatus,
|
|
||||||
ScrumCategory,
|
|
||||||
} from '../services/backend.service';
|
} from '../services/backend.service';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import { TableComponentBase } from '../services/table-component.base';
|
|
||||||
import { getNumberForPriority } from '../services/sorting.service';
|
|
||||||
import { UserstoryFormComponent } from '../userstory-form/userstory-form.component';
|
|
||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-userstory-table',
|
selector: 'app-userstory-table',
|
||||||
templateUrl: './userstory-table.component.html',
|
templateUrl: './userstory-table.component.html',
|
||||||
styleUrls: ['./userstory-table.component.css'],
|
styleUrls: ['./userstory-table.component.css'],
|
||||||
})
|
})
|
||||||
export class UserstoryTableComponent extends TableComponentBase<
|
export class UserstoryTableComponent {
|
||||||
ScrumUserstory
|
public items: ScrumUserstory[] = [];
|
||||||
> {
|
|
||||||
public tasks: ScrumTask[] = [];
|
|
||||||
public filterPriority: string | null = null;
|
|
||||||
public highlightId: number;
|
|
||||||
public status: ScrumStatus[] = [];
|
|
||||||
public categories: ScrumCategory[] = [];
|
|
||||||
|
|
||||||
public get filteredItems() {
|
|
||||||
return this.items.filter(
|
|
||||||
(task) =>
|
|
||||||
this.filterPriority === null || task.priority === this.filterPriority
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private backendService: BackendService,
|
|
||||||
private modalService: NgbModal,
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.applyFilterParameters(this.route.snapshot.paramMap);
|
|
||||||
this.route.paramMap.subscribe((map) => this.applyFilterParameters(map));
|
|
||||||
|
|
||||||
|
constructor(private backendService: BackendService) {
|
||||||
backendService.getUserstories().subscribe((response) => {
|
backendService.getUserstories().subscribe((response) => {
|
||||||
if (response.status > 399) {
|
if (response.status > 399) {
|
||||||
alert('Fehler');
|
alert('Fehler');
|
||||||
@ -51,100 +20,5 @@ export class UserstoryTableComponent extends TableComponentBase<
|
|||||||
this.items.push(...response.body);
|
this.items.push(...response.body);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
backendService.getTasks().subscribe((response) => {
|
|
||||||
if (response.status > 399) {
|
|
||||||
alert('Fehler');
|
|
||||||
} else {
|
|
||||||
this.tasks.push(...response.body);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
backendService.getAllStatus().subscribe((response) => {
|
|
||||||
if (response.status > 399) {
|
|
||||||
alert('Fehler');
|
|
||||||
} else {
|
|
||||||
this.status.push(...response.body);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
backendService.getCategories().subscribe((response) => {
|
|
||||||
if (response.status > 399) {
|
|
||||||
alert('Fehler');
|
|
||||||
} else {
|
|
||||||
this.categories.push(...response.body);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyFilterParameters(params: ParamMap) {
|
|
||||||
if (params.has('id')) {
|
|
||||||
this.highlightId = parseInt(params.get('id'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public deleteUserstory(userstory: ScrumUserstory) {
|
|
||||||
this.backendService.deleteUserstory(userstory).subscribe((response) => {
|
|
||||||
if (response.status > 399) {
|
|
||||||
alert('Fehler');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const index = this.items.indexOf(userstory);
|
|
||||||
if (index !== -1) {
|
|
||||||
this.items.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public openUserstoryForm(editUserstory?: ScrumUserstory) {
|
|
||||||
const modalRef = this.modalService.open(UserstoryFormComponent, {
|
|
||||||
backdrop: 'static',
|
|
||||||
keyboard: true,
|
|
||||||
});
|
|
||||||
if (editUserstory === null) {
|
|
||||||
modalRef.result.then((result) => {
|
|
||||||
this.items.push(result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
modalRef.componentInstance.userstory = editUserstory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNumberOfTasks(userstory: ScrumUserstory) {
|
|
||||||
return this.tasks.filter((t) => t.userstoryid === userstory.id).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sortById() {
|
|
||||||
this.doNumericSort('id', (us) => us.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sortByTitle() {
|
|
||||||
this.doStringSort('title', (us) => us.title);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sortByPrio() {
|
|
||||||
this.doNumericSort('priority', (us) => getNumberForPriority(us.priority));
|
|
||||||
}
|
|
||||||
|
|
||||||
public sortByTasks() {
|
|
||||||
this.doNumericSort('tasks', (us) => this.getNumberOfTasks(us));
|
|
||||||
}
|
|
||||||
|
|
||||||
sortByStatus() {
|
|
||||||
this.doNumericSort('statusid', (us) => us.statusid);
|
|
||||||
}
|
|
||||||
sortByCategory() {
|
|
||||||
this.doNumericSort('categoryid', (us) => us.categoryid);
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatusTitleById(id) {
|
|
||||||
var status = this.status.find((x) => x.id === id);
|
|
||||||
if (!status) {
|
|
||||||
return 'N/A';
|
|
||||||
}
|
|
||||||
return status.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCategoryTitleById(id) {
|
|
||||||
var category = this.categories.find((x) => x.id === id);
|
|
||||||
if (!category) {
|
|
||||||
return 'N/A';
|
|
||||||
}
|
|
||||||
return category.title;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,9 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
.custom-text-secondary {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white-content .custom-text-secondary {
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user