From f3854f5c7081ceeb82a0a92798738f6ed0bc2483 Mon Sep 17 00:00:00 2001 From: Igor Kedzierawski Date: Sun, 21 Apr 2024 02:24:02 +0200 Subject: [PATCH 1/4] improved placeholder data --- .../cornbackend/config/PlaceholderData.java | 84 +++++++++++++------ 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/config/PlaceholderData.java b/corn-backend/src/main/java/dev/corn/cornbackend/config/PlaceholderData.java index f4385ee0..5fbb47bd 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/config/PlaceholderData.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/config/PlaceholderData.java @@ -21,7 +21,6 @@ import dev.corn.cornbackend.entities.user.data.UserResponse; import dev.corn.cornbackend.entities.user.interfaces.UserRepository; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityTransaction; import jakarta.persistence.Query; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -31,11 +30,13 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Random; +import java.util.stream.IntStream; import java.util.stream.Stream; @Configuration @@ -85,9 +86,9 @@ public void run(String... args) { User projectOwner = drawRandom(users); Arrays.stream(PROJECT_NAMES) - .forEach(name -> { - projectService.addNewProject(name, projectOwner); - }); + .forEach(name -> { + projectService.addNewProject(name, projectOwner); + }); Project[] projects = projectRepository.findAllByOwnerOrderByName(projectOwner, Pageable.ofSize(PROJECT_NAMES.length)) .stream().toList().toArray(new Project[0]); Project project = projects[0]; @@ -97,7 +98,7 @@ public void run(String... args) { .forEach(user -> Arrays.stream(projects).forEach(currentProject -> projectMemberService.addMemberToProject(user.getUsername(), currentProject.getProjectId(), projectOwner))); - List members = projectMemberRepository.findAllByProject(project, Pageable.ofSize(users.size())) + List members = projectMemberRepository.findAllByProject(project, Pageable.ofSize(users.size())) .stream().toList(); LocalDate prevDate = LocalDate.now(); @@ -113,29 +114,34 @@ public void run(String... args) { List allSprints = sprintRepository.findAll(); - List backlogItems = Arrays.stream(SAMPLE_BACKLOG_ITEMS) - .map(item -> new BacklogItem(0, - item[0], item[1], - drawRandom(statusesPool), - LocalDate.now().plusDays(random.nextInt(14)), - Collections.emptyList(), - drawRandom(members), - drawRandom(allSprints), - project, - drawRandom(typesPool) - )).map(backlogItemRepository::save).toList(); + List backlogItems = new ArrayList<>(); + + for (Sprint sprint : allSprints) { + backlogItems.addAll(IntStream.range(0, 8 + random.nextInt(8)) + .mapToObj(i -> drawRandom(SAMPLE_BACKLOG_ITEMS)) + .map(item -> new BacklogItem(0, + item[0], item[1], + drawRandom(statusesPool), + null, + Collections.emptyList(), + drawRandom(members), + sprint, + project, + drawRandom(typesPool) + )).map(backlogItemRepository::save).toList()); + } for (int i = 0; i < SAMPLE_BACKLOG_ITEMS.length / 4; i++) { for (int j = 0; j < random.nextInt(4); j++) { long backlogItemId = drawRandom(backlogItems).getBacklogItemId(); User commenter = drawRandom(users); - for(int k = 0; k < 5; k++) { + for (int k = 0; k < 5; k++) { backlogItemCommentService.addNewComment(new BacklogItemCommentRequest( drawRandom(SAMPLE_COMMENTS), backlogItemId ), commenter); } backlogItemCommentService.addNewComment(new BacklogItemCommentRequest( - LONG_STRING, backlogItemId),users.get(4)); + LONG_STRING, backlogItemId), users.get(4)); } } @@ -151,9 +157,31 @@ public void run(String... args) { drawRandom(typesPool))) .forEach(backlogItemRepository::save); + int dayShift = 24; + + backlogItemRepository.saveAll(backlogItems.stream() + .filter(item -> item.getSprint().isStartAfter(LocalDate.now().plusDays(dayShift))) + .peek(item -> item.setStatus(ItemStatus.TODO)) + .toList() + ); + backlogItemRepository.saveAll(backlogItems.stream() + .filter(item -> item.getSprint().isEndBefore(LocalDate.now().plusDays(dayShift))) + .peek(item -> { + if (random.nextDouble() < 0.53) { + item.setStatus(ItemStatus.DONE); + LocalDate start = item.getSprint().getStartDate(); + LocalDate end = item.getSprint().getEndDate(); + int daysBetween = (int) ChronoUnit.DAYS.between(start, end); + item.setTaskFinishDate(start.plusDays(1+random.nextInt(daysBetween-2)).minusDays(dayShift)); + } else { + item.setStatus(random.nextDouble() < 0.5 ? ItemStatus.IN_PROGRESS : ItemStatus.TODO); + } + }).toList() + ); + Query query = entityManager.createNativeQuery("UPDATE sprint SET " + - "end_date = end_date - INTERVAL '17 days', " + - "start_date = start_date - INTERVAL '17 days';"); + "end_date = end_date - INTERVAL '"+dayShift+" days', " + + "start_date = start_date - INTERVAL '"+dayShift+" days';"); query.executeUpdate(); } @@ -163,6 +191,10 @@ private T drawRandom(List list) { return tmp.get(0); } + private T drawRandom(T[] array) { + return array[random.nextInt(array.length)]; + } + private final String[][] SAMPLE_BACKLOG_ITEMS = { {"Develop Feature X", "Implement and test the new feature X to enhance user experience."}, {"Fix Bug in Login Module", "Investigate and resolve the login module bug reported by users."}, @@ -304,14 +336,14 @@ private T drawRandom(List list) { {"Upgrade CAPTCHA Security", "Enhance CAPTCHA security to prevent spam and abuse."}, {"Implement Continuous Integration", "Introduce continuous integration for automated code testing and deployment."}, }; - + private static final String LONG_STRING = """ - aaa very long string aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - hahahaha you though I was joking this string is reeeeaalllllyyyy loooooooooooooooooooooooooooooong and guess what - it ain't stopping dfsdkfbsfdkjsbdfkljdbflkjdsbflksdjbfskldjfblksdjbfklsjdbflkjsdbflkjsdbflksdbfjklsdfb - ok I'm done\n\n\n\n\n\n\nhaha just jk ok now I'm done - """; + aaa very long string aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + hahahaha you though I was joking this string is reeeeaalllllyyyy loooooooooooooooooooooooooooooong and guess what + it ain't stopping dfsdkfbsfdkjsbdfkljdbflkjdsbflksdjbfskldjfblksdjbfklsjdbflkjsdbflkjsdbflksdbfjklsdfb + ok I'm done\n\n\n\n\n\n\nhaha just jk ok now I'm done + """; private final String[] PROJECT_NAMES = { "BlueSky Initiative", From 210e950e5f85988d86e01006eaf44f43388f120a Mon Sep 17 00:00:00 2001 From: Igor Kedzierawski Date: Sun, 21 Apr 2024 20:47:13 +0200 Subject: [PATCH 2/4] changed field type to date --- .../api/v1/backlog/item/data/backlog-item-response.interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corn-frontend/src/app/core/services/api/v1/backlog/item/data/backlog-item-response.interface.ts b/corn-frontend/src/app/core/services/api/v1/backlog/item/data/backlog-item-response.interface.ts index ef813500..981b46da 100644 --- a/corn-frontend/src/app/core/services/api/v1/backlog/item/data/backlog-item-response.interface.ts +++ b/corn-frontend/src/app/core/services/api/v1/backlog/item/data/backlog-item-response.interface.ts @@ -7,7 +7,7 @@ export interface BacklogItemResponse { status: string; assignee: User; itemType: string; - taskFinishDate: string; + taskFinishDate: Date; projectId: number; sprintId: number; } \ No newline at end of file From e7936a3e24c5024308562d708a223fae6db08f18 Mon Sep 17 00:00:00 2001 From: Igor Kedzierawski Date: Sun, 21 Apr 2024 20:52:18 +0200 Subject: [PATCH 3/4] implemented reports view --- corn-frontend/package-lock.json | 20 ++ corn-frontend/package.json | 4 +- corn-frontend/src/app/app.routes.ts | 5 + .../src/app/core/enum/BoardsPaths.ts | 1 + .../boards/reports/bucket.interface.ts | 4 + .../boards/reports/simple_sprint.interface.ts | 8 + .../app/pages/boards/boards.component.html | 2 +- .../src/app/pages/boards/boards.component.ts | 4 + .../boards/reports/reports.component.html | 20 ++ .../pages/boards/reports/reports.component.ts | 175 ++++++++++++++++++ 10 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 corn-frontend/src/app/core/interfaces/boards/reports/bucket.interface.ts create mode 100644 corn-frontend/src/app/core/interfaces/boards/reports/simple_sprint.interface.ts create mode 100644 corn-frontend/src/app/pages/boards/reports/reports.component.html create mode 100644 corn-frontend/src/app/pages/boards/reports/reports.component.ts diff --git a/corn-frontend/package-lock.json b/corn-frontend/package-lock.json index 415aee83..874cc17c 100644 --- a/corn-frontend/package-lock.json +++ b/corn-frontend/package-lock.json @@ -19,6 +19,8 @@ "@angular/platform-browser-dynamic": "^17.1.0", "@angular/router": "^17.1.0", "@angular/service-worker": "^17.1.0", + "@canvasjs/angular-charts": "^1.2.0", + "@canvasjs/charts": "^3.8.2", "@ng-icons/akar-icons": "^26.3.0", "@ng-icons/bootstrap-icons": "^26.5.0", "@ng-icons/core": "^26.3.0", @@ -2438,6 +2440,24 @@ "node": ">=6.9.0" } }, + "node_modules/@canvasjs/angular-charts": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@canvasjs/angular-charts/-/angular-charts-1.2.0.tgz", + "integrity": "sha512-9wumUHLIvmfv6h9sQ1n9Rm4jQgg33OFkW2S/0P347mwf7v/N8K0MFPELpExM4Vhy9mj54/DX92QQvRIi9gZ/Lw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@canvasjs/charts": "^3.7.5" + } + }, + "node_modules/@canvasjs/charts": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@canvasjs/charts/-/charts-3.8.2.tgz", + "integrity": "sha512-ON3TQ24dTD57K/JaD8t/ro0/9/ENrSnz8UK7KUgfzcogTR8JsyLX3J2gZyv2Qr/czTykugQx2J/O53zg9atZog==" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", diff --git a/corn-frontend/package.json b/corn-frontend/package.json index 5410c7a0..3946599c 100644 --- a/corn-frontend/package.json +++ b/corn-frontend/package.json @@ -22,6 +22,8 @@ "@angular/platform-browser-dynamic": "^17.1.0", "@angular/router": "^17.1.0", "@angular/service-worker": "^17.1.0", + "@canvasjs/angular-charts": "^1.2.0", + "@canvasjs/charts": "^3.8.2", "@ng-icons/akar-icons": "^26.3.0", "@ng-icons/bootstrap-icons": "^26.5.0", "@ng-icons/core": "^26.3.0", @@ -53,4 +55,4 @@ "tailwindcss": "^3.4.1", "typescript": "~5.3.2" } -} \ No newline at end of file +} diff --git a/corn-frontend/src/app/app.routes.ts b/corn-frontend/src/app/app.routes.ts index ee82bac5..2eb440c6 100644 --- a/corn-frontend/src/app/app.routes.ts +++ b/corn-frontend/src/app/app.routes.ts @@ -36,6 +36,11 @@ export const routes: Routes = [ loadComponent: () => import("@pages/boards/board/board.component") .then(c => c.BoardComponent) }, + { + path: BoardsPaths.REPORTS, + loadComponent: () => import("@pages/boards/reports/reports.component") + .then(c => c.ReportsComponent) + }, { path: RouterPaths.SETTINGS_PATH, loadComponent: () => import("@pages/boards/project-settings/project-settings.component") diff --git a/corn-frontend/src/app/core/enum/BoardsPaths.ts b/corn-frontend/src/app/core/enum/BoardsPaths.ts index d49433e6..8d8f8701 100644 --- a/corn-frontend/src/app/core/enum/BoardsPaths.ts +++ b/corn-frontend/src/app/core/enum/BoardsPaths.ts @@ -2,4 +2,5 @@ export enum BoardsPaths { BACKLOG="backlog", TIMELINE="timeline", BOARD="board", + REPORTS = "reports", } diff --git a/corn-frontend/src/app/core/interfaces/boards/reports/bucket.interface.ts b/corn-frontend/src/app/core/interfaces/boards/reports/bucket.interface.ts new file mode 100644 index 00000000..c8547f35 --- /dev/null +++ b/corn-frontend/src/app/core/interfaces/boards/reports/bucket.interface.ts @@ -0,0 +1,4 @@ +export interface Bucket { + remainingTasks: number; + date: Date; +} \ No newline at end of file diff --git a/corn-frontend/src/app/core/interfaces/boards/reports/simple_sprint.interface.ts b/corn-frontend/src/app/core/interfaces/boards/reports/simple_sprint.interface.ts new file mode 100644 index 00000000..ca4be818 --- /dev/null +++ b/corn-frontend/src/app/core/interfaces/boards/reports/simple_sprint.interface.ts @@ -0,0 +1,8 @@ +export interface SimpleSprint { + sprintId: number; + projectId: number; + sprintName: string; + sprintDescription: string; + startDate: Date; + endDate: Date; +} \ No newline at end of file diff --git a/corn-frontend/src/app/pages/boards/boards.component.html b/corn-frontend/src/app/pages/boards/boards.component.html index 625e0945..0da32528 100644 --- a/corn-frontend/src/app/pages/boards/boards.component.html +++ b/corn-frontend/src/app/pages/boards/boards.component.html @@ -22,7 +22,7 @@ [iconName]="'akarClipboard'" [label]="'Board'"> - diff --git a/corn-frontend/src/app/pages/boards/boards.component.ts b/corn-frontend/src/app/pages/boards/boards.component.ts index bc3c63a9..5e1bca34 100644 --- a/corn-frontend/src/app/pages/boards/boards.component.ts +++ b/corn-frontend/src/app/pages/boards/boards.component.ts @@ -79,6 +79,10 @@ export class BoardsComponent implements OnInit { this.router.navigate([`/${ RouterPaths.BOARDS_PATH }/${ BoardsPaths.BOARD }`]); } + navigateToReports(): void { + this.router.navigate([`/${ RouterPaths.BOARDS_PATH }/${ BoardsPaths.REPORTS }`]); + } + navigateToProjectSettings(): void { this.router.navigate([RouterPaths.BOARDS_DIRECT_PATH, RouterPaths.SETTINGS_PATH]) } diff --git a/corn-frontend/src/app/pages/boards/reports/reports.component.html b/corn-frontend/src/app/pages/boards/reports/reports.component.html new file mode 100644 index 00000000..dd219c07 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/reports/reports.component.html @@ -0,0 +1,20 @@ +
+ @if (currentSprint === null) { + Brak sprintów w projekcie + } @else if(currentSprint === undefined) { + Ładowanie sprintów... + } @else { +
+ +

{{ currentSprint.sprintName }}

+ +
+ } +
+
+
+
diff --git a/corn-frontend/src/app/pages/boards/reports/reports.component.ts b/corn-frontend/src/app/pages/boards/reports/reports.component.ts new file mode 100644 index 00000000..b472e77e --- /dev/null +++ b/corn-frontend/src/app/pages/boards/reports/reports.component.ts @@ -0,0 +1,175 @@ +import { AfterViewInit, Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatIconModule } from '@angular/material/icon'; +import { SprintApi } from '@core/services/api/v1/sprint/sprint-api.service'; +import { StorageService } from '@core/services/storage.service'; +import { SprintResponse } from '@core/services/api/v1/sprint/data/sprint-response.interface'; +import { ProjectMemberApi } from '@core/services/api/v1/project/member/project-member-api.service'; +import { BacklogItemApi } from '@core/services/api/v1/backlog/item/backlog-item-api.service'; +import { BacklogItemStatus } from '@core/enum/BacklogItemStatus'; +import { StorageKey } from '@core/enum/storage-key.enum'; +import { firstValueFrom } from 'rxjs'; +import { SimpleSprint } from '@core/interfaces/boards/reports/simple_sprint.interface'; +import { Pageable } from '@core/services/api/utils/pageable.interface'; +import { Bucket } from '@core/interfaces/boards/reports/bucket.interface'; +import { CanvasJS, CanvasJSAngularChartsModule } from '@canvasjs/angular-charts'; + +@Component({ + selector: 'app-reports', + standalone: true, + imports: [ + CommonModule, + MatInputModule, + MatMenuModule, + MatButtonModule, + MatIconModule, + CanvasJSAngularChartsModule, + ], + templateUrl: './reports.component.html', +}) +export class ReportsComponent implements AfterViewInit { + + protected currentSprint: SimpleSprint | null | undefined = undefined; + protected previousSprint: SimpleSprint | null = null; + protected nextSprint: SimpleSprint | null = null; + + protected chartOptions = { + animationEnabled: true, + theme: "dark1", + title: { + text: "Sprint Burndown" + }, + axisX: { + valueFormatString: "D MMM" + }, + axisY: { + title: "No. Tasks" + }, + toolTip: { + shared: true + }, + data: [{ + name: "Actual", + type: "line", + showInLegend: true, + xValueFormatString: "DD MMM YYYY", + lineThickness: 5, + color: "yellow", + dataPoints: [], + }, { + name: "Ideal", + type: "line", + showInLegend: true, + lineThickness: 5, + color: "silver", + dataPoints: [], + }] + } + + protected chart: any; + + constructor( + protected readonly sprintApi: SprintApi, + protected readonly projectMemberApi: ProjectMemberApi, + protected readonly backlogItemApi: BacklogItemApi, + protected readonly storage: StorageService, + ) { } + + async ngAfterViewInit(): Promise { + this.chart = new CanvasJS.Chart("chartContainer", this.chartOptions); + await this.loadAndDisplayCurrentSprint(); + } + + private async loadAndDisplayCurrentSprint(): Promise { + const projectId: number = this.storage.getValueFromStorage(StorageKey.PROJECT_ID); + const sprints = await firstValueFrom(this.sprintApi.getCurrentAndFutureSprints(projectId)); + if (sprints.length == 0) { + this.currentSprint = null; + } else { + this.currentSprint = undefined; + this.loadSprintInfo(this.toSimpleSprint(sprints[0])); + } + } + + private async loadSprintInfo(sprint: SimpleSprint): Promise { + const [nextSprint, previousSprint] = (await Promise.all([ + firstValueFrom(this.sprintApi.getSprintsAfterSprint(sprint.sprintId, Pageable.of(0, 1, "startDate", "ASC"))), + firstValueFrom(this.sprintApi.getSprintsBeforeSprint(sprint.sprintId, Pageable.of(0, 1, "startDate", "DESC"))), + ])).map(page => page.numberOfElements > 0 ? this.toSimpleSprint(page.content[0]) : null); + this.currentSprint = sprint; + this.nextSprint = (nextSprint && nextSprint.startDate <= new Date()) ? nextSprint : null; + this.previousSprint = previousSprint; + + this.updateBuckets(sprint); + } + + private async updateBuckets(sprint: SimpleSprint): Promise { + const itemsResponse = await firstValueFrom(this.backlogItemApi.getAllBySprintId(sprint.sprintId)); + const allTasksInSprintCount = itemsResponse.length; + + const doneItems = itemsResponse + .filter(item => item.status === BacklogItemStatus.DONE && item.taskFinishDate) + .map(item => { + item.taskFinishDate && (item.taskFinishDate = new Date(item.taskFinishDate)); + return item; + }) + + const [startDate, endDate] = [sprint.startDate, sprint.endDate]; + const [startTime, endTime] = [startDate.getTime(), endDate.getTime()]; + + const dayLengthMs = 1000 * 60 * 60 * 24; + const sprintTimespan = Math.floor((endTime - startTime) / dayLengthMs) + + const buckets = Array.from({ length: sprintTimespan + 1 }).map((_, i) => ({ + remainingTasks: allTasksInSprintCount, + date: new Date(startTime + i * dayLengthMs), + })); + + doneItems.forEach(item => { + buckets.filter(bucket => bucket.date >= item.taskFinishDate) + .forEach(bucket => bucket.remainingTasks--); + }); + + this.updateDatapoints(buckets); + } + + private updateDatapoints(buckets: Bucket[]) { + const actualPoints = buckets.map(bucket => { + return ({ + x: bucket.date, + y: bucket.remainingTasks, + }); + }); + const ideal = [{ ...actualPoints[0] }, { ...actualPoints.at(-1) }]; + ideal[1]!.y = 0; + + this.chart.options.data[0].dataPoints = actualPoints; + this.chart.options.data[1].dataPoints = ideal; + this.chart.render(); + } + + protected async switchDisplayedSprint(forward: boolean): Promise { + if (forward && this.nextSprint) { + this.currentSprint = undefined; + await this.loadSprintInfo(this.nextSprint); + } else if (!forward && this.previousSprint) { + this.currentSprint = undefined; + await this.loadSprintInfo(this.previousSprint); + } + } + + private toSimpleSprint(sprintResponse: SprintResponse): SimpleSprint { + return { + sprintId: sprintResponse.sprintId, + projectId: sprintResponse.projectId, + sprintName: sprintResponse.sprintName, + sprintDescription: sprintResponse.sprintDescription, + startDate: new Date(sprintResponse.startDate), + endDate: new Date(sprintResponse.endDate), + }; + } + +} From b6ea9435320aafa8f02de53e2dcce05f7b874070 Mon Sep 17 00:00:00 2001 From: Igor Kedzierawski Date: Sun, 21 Apr 2024 21:34:36 +0200 Subject: [PATCH 4/4] centrified sprint labels --- corn-frontend/src/app/pages/boards/board/board.component.html | 2 +- .../src/app/pages/boards/reports/reports.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/corn-frontend/src/app/pages/boards/board/board.component.html b/corn-frontend/src/app/pages/boards/board/board.component.html index 546f3164..7f4b44c4 100644 --- a/corn-frontend/src/app/pages/boards/board/board.component.html +++ b/corn-frontend/src/app/pages/boards/board/board.component.html @@ -5,7 +5,7 @@ Ładowanie sprintów... } @else {
-
+
diff --git a/corn-frontend/src/app/pages/boards/reports/reports.component.html b/corn-frontend/src/app/pages/boards/reports/reports.component.html index dd219c07..9c2a491b 100644 --- a/corn-frontend/src/app/pages/boards/reports/reports.component.html +++ b/corn-frontend/src/app/pages/boards/reports/reports.component.html @@ -4,7 +4,7 @@ } @else if(currentSprint === undefined) { Ładowanie sprintów... } @else { -
+