Merge feature/dashboard

This commit is contained in:
Jakob Fahr 2020-07-05 15:10:01 +02:00
parent f5dae215a4
commit 4ca662c819
No known key found for this signature in database
GPG Key ID: 8873416D8E4CEF6B
15 changed files with 2204 additions and 491 deletions

1767
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -1,16 +1,17 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component';
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';
const routes: Routes = [ const routes: Routes = [
{ path: 'tasks', component: TaskTableComponent }, { path: 'tasks', component: TaskTableComponent },
{ path: 'userstories', component: UserstoryTableComponent }, { path: 'userstories', component: UserstoryTableComponent },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'sprints', component: SprintTableComponent }, { path: 'sprints', component: SprintTableComponent },
// { path: 'dashboard', component: DashboardComponent },
{ path: '', redirectTo: '/tasks', pathMatch: 'full' }, { path: '', redirectTo: '/tasks', pathMatch: 'full' },
]; ];

View File

@ -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: [
@ -26,7 +27,8 @@ import { SprintTableComponent } from './sprint-table/sprint-table.component';
UserstoryTableComponent, UserstoryTableComponent,
SprintFormComponent, SprintFormComponent,
SprintTableComponent, SprintTableComponent,
// DashboardComponent, DashboardComponent,
UserstoryInnerTableComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -1,3 +1,7 @@
/* .text-2em { .text-large {
font-size: 2rem; font-size: 1.2rem;
} */ }
.text-very-large {
font-size: 2.4rem;
}

View File

@ -1,60 +1,98 @@
{% comment %} <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>
<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>
<ng-container *ngIf="selectedSprint !== undefined">
<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>
<div class="row"> <div class="row">
<div class="p-3 xl-col-6 lg-col-6 md-col-12 sm-col-12 xs-col-12"> <div class="p-3 col-12 col-xl-6 col-lg-6 col-md-12 col-sm-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<span class="text-large">
Userstories Userstories
</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div *ngIf="selectedSprint !== undefined && usedStatus.length === 0">
Zum Sprint "{{selectedSprint.title}}" sind aktuell keine Userstories vorhanden.
</div>
<canvas id="done-stories-chart"></canvas> <canvas id="done-stories-chart"></canvas>
</div> </div>
</div> </div>
</div>
<div class="p-3 xl-col-6 lg-col-6 md-col-12 sm-col-12 xs-col-12">
<div class="card-deck"> <div class="card-deck">
<div <div
class="card" class="card text-center"
[class.border-danger]="getSprintUrgency() == 0" *ngFor="let status of usedStatus"
[class.border-warning]="getSprintUrgency() == 1"
[class.border-success]="getSprintUrgency() == 2"
*ngIf="getRemainingDaysInSprint() !== undefined"
> >
<div class="card-header"> <div class="card-header">
Days remaining <span class="text-large">
{{status.title}}
</span>
</div> </div>
<div class="card-body text-center"> <div class="card-body">
<span class="text-2em"> <span class="text-very-large">
{{getRemainingDaysInSprint()}} <b>{{getNumberOfUserstoriesByStatus(status)}}</b>
</span> </span>
</div> </div>
</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 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> </div>
</div> {% endcomment %} </div>

View File

@ -1,159 +1,140 @@
// import { Component, OnInit } from '@angular/core'; import {Component, OnChanges} from '@angular/core';
// // import Chart from 'chart.js'; import {forkJoin} from 'rxjs';
// import { import Chart from 'chart.js';
// BackendService, import {BackendService, ScrumSprint, ScrumStatus, ScrumUserstory} from '../services/backend.service';
// ScrumStatus,
// ScrumUser,
// ScrumUserstory,
// ScrumSprint,
// } 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( return this.status.filter(s => this.selectedSprintUserstories.find(us => us.statusid === s.id) !== undefined);
// (s) => this.userstories.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);
}
// constructor(private backendService: BackendService) { public get currentSprint(): ScrumSprint {
// // backendService.getUserstories().subscribe(response => { const now = Date.now();
// // if (response.status > 399) { return this.sprints.find(s => Date.parse(s.startDate) < now && Date.parse(s.endDate) > now);
// // alert('Fehler'); }
// // } else {
// // this.userstories.push(...response.body);
// // }
// // });
// // backendService.getAllStatus().subscribe(response => {
// // if (response.status > 399) {
// // alert('Fehler');
// // } else {
// // this.status.push(...response.body);
// // }
// // });
// // backendService.getSprints().subscribe(response => {
// // if (response.status > 399) {
// // alert('Fehler');
// // } else {
// // this.sprints.push(...response.body);
// // }
// // });
// this.status = [ private _selectedSprint: ScrumSprint;
// { id: 0, title: 'In progress', description: '' }, public get selectedSprint(): ScrumSprint {
// { id: 1, title: 'Done', description: '' }, if (this._selectedSprint === undefined) {
// ]; if (this.currentSprint === undefined) {
// this.userstories = [ return this.sprints[0];
// { statusid: 0, title: '' }, }
// { statusid: 0, title: '' }, return this.currentSprint;
// { statusid: 0, title: '' }, }
// { statusid: 1, title: '' }, return this._selectedSprint;
// { 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),
// },
// ];
// }
// ngOnInit(): void { public set selectedSprint(value) {
// // @ts-ignore this._selectedSprint = value;
// const context = document this.createChart();
// .getElementById('done-stories-chart') }
// .getContext('2d');
// const chart = new Chart(context, {
// type: 'pie',
// data: {
// labels: this.usedStatus.map((s) => s.title),
// datasets: [
// {
// label: 'Done stories',
// data: this.usedStatus.map((s) =>
// this.getNumberOfUserstoriesByStatus(s)
// ),
// backgroundColor: this.getBackgroundColors(),
// },
// ],
// },
// });
// }
// private getBackgroundColors(): string[] { public status: ScrumStatus[] = [];
// const baseColors = [ public userstories: ScrumUserstory[] = [];
// 'rgb(255, 153, 102)', public sprints: ScrumSprint[] = [];
// 'rgb(255, 102, 102)',
// 'rgb(153, 204, 255)', public chart: Chart;
// 'rgb(102, 153, 102)',
// 'rgb(204, 204, 153)', constructor(private backendService: BackendService) {
// 'rgb(153, 102, 204)', // download userstories, status and sprints and update
// 'rgb(204, 102, 102)', // the chart whenever a new response is there
// 'rgb(255, 204, 153)', forkJoin([backendService.getUserstories(), backendService.getAllStatus(), backendService.getSprints()])
// 'rgb(153, 102, 255)', .subscribe(results => {
// 'rgb(204, 204, 204)', const [userstoryResponse, statusResponse, sprintResponse] = results;
// 'rgb(102, 255, 204)', if (userstoryResponse.status > 399 || statusResponse.status > 399 || sprintResponse.status > 399) {
// 'rgb(102, 153, 255)', alert('Fehler');
// 'rgb(153, 102, 153)', } else {
// 'rgb(204, 204, 255)', this.userstories.push(...userstoryResponse.body);
// ]; this.status.push(...statusResponse.body);
// const colors = []; this.sprints.push(...sprintResponse.body);
// while (colors.length < this.usedStatus.length) { this.createChart();
// colors.push(...baseColors); }
// } });
// return colors; }
// }
/**
* 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()}`;
}
private createChart() {
// @ts-ignore
const context = document.getElementById('done-stories-chart').getContext('2d');
if (this.usedStatus.length === 0) {
this.chart.destroy();
}
else {
this.chart = new Chart(context, {
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();
}
}
// public getNumberOfUserstoriesByStatus(status: ScrumStatus): number { // public getNumberOfUserstoriesByStatus(status: ScrumStatus): number {
// return this.userstories.filter((us) => us.statusid === status.id).length; // return this.userstories.filter((us) => us.statusid === status.id).length;
// } // }
// public getRemainingDaysInSprint(): number { public getNumberOfUserstoriesByStatus(status: ScrumStatus): number {
// const now = new Date(); return this.selectedSprintUserstories.filter(us => us.statusid === status.id).length;
// const currentSprint = this.sprints.find( }
// (s) => s.endDate > now && s.startDate < now
// );
// if (currentSprint === undefined) {
// return undefined;
// }
// const daysDelta = Math.floor(
// (currentSprint.endDate.getTime() - now.getTime()) / 86400000
// );
// return daysDelta;
// }
// public getSprintUrgency(): number { public getRemainingDaysInSprint(): number {
// const now = new Date(); if (this.selectedSprint === undefined) {
// const currentSprint = this.sprints.find( return undefined;
// (s) => s.endDate > now && s.startDate < now }
// ); return Math.floor((Date.parse(this.selectedSprint.endDate) - Date.now()) / 86400000);
// if (currentSprint === undefined) { }
// return undefined;
// } /**
// const deltaFromNow = currentSprint.endDate.getTime() - now.getTime(); * Returns the "urgency" of the current sprint (ie what percentage of the sprint is remaining)
// const deltaFromStart = * as an integer from 0 (most urgent) to 2 (least urgent).
// currentSprint.endDate.getTime() - currentSprint.startDate.getTime(); */
// return Math.floor((3 * deltaFromNow) / deltaFromStart); 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);
}
}

View File

@ -246,6 +246,7 @@ export interface ScrumUserstory {
projectid?: number; projectid?: number;
} }
<<<<<<< HEAD
export interface ScrumSprint { export interface ScrumSprint {
id?: number; id?: number;
title: string; title: string;
@ -253,6 +254,15 @@ export interface ScrumSprint {
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
project?: number; project?: number;
=======
export interface ScrumSprint{
id?: number;
title: string;
description?: string;
startDate: string;
endDate: string;
project: number;
>>>>>>> feature/dashboard
} }
export interface ScrumCategory { export interface ScrumCategory {

View File

@ -14,7 +14,7 @@ import { getNumberForPriority } from '../services/sorting.service';
import { NONE_TYPE } from '@angular/compiler'; import { NONE_TYPE } from '@angular/compiler';
@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'],
}) })

View File

@ -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>

View 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;
}
}

View File

@ -1,106 +1,4 @@
<div class="container-fluid"> <div class="mx-5 my-3">
<h3 class="my-1">Userstories</h3>
<h3>Userstories</h3> <app-userstory-inner-table [items]="items"></app-userstory-inner-table>
<button class="btn btn-secondary" (click)="openUserstoryForm()">Neue Userstory</button>
<table class="table">
<thead>
<tr>
<th (click)="sortById()" class="sortable">
<span>ID</span>
<span>
<span *ngIf="sortBy != 'id'"><i class="fa fa-sort fa-lg"></i></span>
<span *ngIf="sortDescending && sortBy === 'id'"><i class="fa fa-sort-up fa-lg"></i></span>
<span *ngIf="sortDescending === false && sortBy === 'id'"><i class="fa fa-sort-down fa-lg"></i></span>
</span>
</th>
<th (click)="sortByTitle()" class="sortable">
<span>Titel</span>
<span>
<span *ngIf="sortBy != 'title'"><i class="fa fa-sort fa-lg"></i></span>
<span *ngIf="sortDescending && sortBy === 'title'"><i class="fa fa-sort-up fa-lg"></i></span>
<span *ngIf="sortDescending === false && sortBy === 'title'"><i class="fa fa-sort-down fa-lg"></i></span>
</span>
</th>
<th (click)="sortByTasks()" class="sortable">
<span>Tasks</span>
<span>
<span *ngIf="sortBy != 'tasks'"><i class="fa fa-sort fa-lg"></i></span>
<span *ngIf="sortDescending && sortBy === 'tasks'"><i class="fa fa-sort-up fa-lg"></i></span>
<span *ngIf="sortDescending === false && sortBy === 'tasks'"><i class="fa fa-sort-down fa-lg"></i></span>
</span>
</th>
<th (click)="sortByStatus()" class="sortable">
<span>Status</span>
<span>
<span *ngIf="sortBy != 'statusid'"><i class="fa fa-sort fa-lg"></i></span>
<span *ngIf="sortDescending && sortBy === 'statusid'"><i class="fa fa-sort-up fa-lg"></i></span>
<span *ngIf="sortDescending === false && sortBy === 'statusid'"><i class="fa fa-sort-down fa-lg"></i></span>
</span>
</th>
<th class="sortable">
<div class="d-inline-block">
<div class="d-inline-block">
<span (click)="sortByPrio()">Priorität: </span>
<div ngbDropdown class="d-inline-block">
<span id="dropdownBasic1" ngbDropdownToggle>{{filterPriority || "All"}}</span>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<option ngbDropdownItem (click)="filterPriority=null">All</option>
<option ngbDropdownItem *ngFor="let p of getAllPriorities()" (click)="filterPriority=p">{{p}}</option>
</div>
</div>
<span (click)="sortByPrio()">
<span *ngIf="sortBy != 'priority'"><i class="fa fa-sort fa-lg"></i></span>
<span *ngIf="sortDescending && sortBy === 'priority'"><i class="fa fa-sort-up fa-lg"></i></span>
<span *ngIf="sortDescending === false && sortBy === 'priority'"><i class="fa fa-sort-down fa-lg"></i></span>
</span>
</div>
</div>
</th>
<th (click)="sortByCategory()" class="sortable">
<span>Category</span>
<span>
<span *ngIf="sortBy != 'categoryid'"><i class="fa fa-sort fa-lg"></i></span>
<span *ngIf="sortDescending && sortBy === 'categoryid'"><i class="fa fa-sort-up fa-lg"></i></span>
<span *ngIf="sortDescending === false && sortBy === 'categoryid'"><i class="fa fa-sort-down fa-lg"></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>

View File

@ -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,101 +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,
size: 'lg'
});
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;
} }
} }

9
src/styles.css Normal file
View File

@ -0,0 +1,9 @@
/* 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;
}