Implement Schedule History
All checks were successful
Java CI with Maven / test (push) Successful in 37s
Java CI with Maven / build-and-push-frontend (push) Successful in 2m13s
Java CI with Maven / build-and-push-backend (push) Successful in 1m21s

This commit is contained in:
Sebastian 2023-12-20 20:53:37 +01:00
parent 84123f6664
commit 0d64d9c72a
14 changed files with 350 additions and 15 deletions

View File

@ -17,15 +17,10 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.parameters.P; import org.springframework.security.core.parameters.P;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.time.Duration; import java.time.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.time.temporal.TemporalAdjusters;
import java.util.Comparator; import java.util.*;
import java.util.List;
import java.util.Map;
@CrossOrigin(origins = "*", maxAge = 3600) @CrossOrigin(origins = "*", maxAge = 3600)
@RestController @RestController
@ -70,4 +65,11 @@ public class StatisticController {
}); });
return ResponseEntity.ok(activityInfos); return ResponseEntity.ok(activityInfos);
} }
@GetMapping("/history/schedules/{date}")
public ResponseEntity<?> getPastSchedules(@PathVariable String date) {
LocalDate dateArg = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
List<AbstractSchedule> abstractSchedules = taskScheduleService.findSchedulesByDate(SecurityContextHolder.getContext().getAuthentication().getName(), dateArg);
return ResponseEntity.ok(abstractSchedules.stream().map(AbstractSchedule::toScheduleInfo).toList());
}
} }

View File

@ -0,0 +1,43 @@
package core.api.models.timemanager.history;
import com.fasterxml.jackson.annotation.JsonProperty;
import core.api.models.timemanager.taskSchedule.scheduleInfos.ScheduleInfo;
import java.util.List;
public class PastScheduleInfo {
@JsonProperty
private List<ScheduleInfo> mondaySchedules;
@JsonProperty
private List<ScheduleInfo> tuesdaySchedules;
@JsonProperty
private List<ScheduleInfo> wednesdaySchedules;
@JsonProperty
private List<ScheduleInfo> thursdaySchedules;
@JsonProperty
private List<ScheduleInfo> fridaySchedules;
@JsonProperty
private List<ScheduleInfo> saturdaySchedules;
@JsonProperty
private List<ScheduleInfo> sundaySchedules;
public PastScheduleInfo(List<ScheduleInfo> mondaySchedules, List<ScheduleInfo> tuesdaySchedules,
List<ScheduleInfo> wednesdaySchedules, List<ScheduleInfo> thursdaySchedules,
List<ScheduleInfo> fridaySchedules, List<ScheduleInfo> saturdaySchedules,
List<ScheduleInfo> sundaySchedules) {
this.mondaySchedules = mondaySchedules;
this.tuesdaySchedules = tuesdaySchedules;
this.wednesdaySchedules = wednesdaySchedules;
this.thursdaySchedules = thursdaySchedules;
this.fridaySchedules = fridaySchedules;
this.saturdaySchedules = saturdaySchedules;
this.sundaySchedules = sundaySchedules;
}
}

View File

@ -19,5 +19,4 @@ public interface ScheduleRepository extends CrudRepository<AbstractSchedule, Lon
@Query(value = "SELECT s FROM AbstractSchedule s WHERE s.task.taskgroup.user.username = ?1 AND s.startTime is NOT NULL and s.stopTime is NULL") @Query(value = "SELECT s FROM AbstractSchedule s WHERE s.task.taskgroup.user.username = ?1 AND s.startTime is NOT NULL and s.stopTime is NULL")
Optional<AbstractSchedule> getActiveScheduleOfUser(String username); Optional<AbstractSchedule> getActiveScheduleOfUser(String username);
} }

View File

@ -1,9 +1,11 @@
package core.services; package core.services;
import core.api.models.timemanager.history.PastScheduleInfo;
import core.api.models.timemanager.taskSchedule.scheduleInfos.AdvancedScheduleFieldInfo; import core.api.models.timemanager.taskSchedule.scheduleInfos.AdvancedScheduleFieldInfo;
import core.api.models.timemanager.taskSchedule.scheduleInfos.AdvancedScheduleInfo; import core.api.models.timemanager.taskSchedule.scheduleInfos.AdvancedScheduleInfo;
import core.api.models.timemanager.taskSchedule.scheduleInfos.BasicScheduleFieldInfo; import core.api.models.timemanager.taskSchedule.scheduleInfos.BasicScheduleFieldInfo;
import core.api.models.timemanager.taskSchedule.ForgottenScheduleInfo; import core.api.models.timemanager.taskSchedule.ForgottenScheduleInfo;
import core.api.models.timemanager.taskSchedule.scheduleInfos.ScheduleInfo;
import core.entities.timemanager.AbstractSchedule; import core.entities.timemanager.AbstractSchedule;
import core.entities.timemanager.AdvancedTaskSchedule; import core.entities.timemanager.AdvancedTaskSchedule;
import core.entities.timemanager.BasicTaskSchedule; import core.entities.timemanager.BasicTaskSchedule;
@ -14,13 +16,12 @@ import core.repositories.timemanager.TaskRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.DayOfWeek;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.time.temporal.ChronoUnit;
import java.util.LinkedList; import java.util.*;
import java.util.List;
import java.util.Optional;
@Service @Service
public class TaskScheduleService { public class TaskScheduleService {
@ -216,4 +217,24 @@ public class TaskScheduleService {
public void deleteSchedules(List<AbstractSchedule> taskSchedules) { public void deleteSchedules(List<AbstractSchedule> taskSchedules) {
scheduleRepository.deleteAll(taskSchedules); scheduleRepository.deleteAll(taskSchedules);
} }
public List<AbstractSchedule> findSchedulesByDate(String username, LocalDate date) {
List<AbstractSchedule> allSchedules = scheduleRepository.findAllByUsername(username);
return findScheduleByDate(allSchedules, date);
}
private List<AbstractSchedule> findScheduleByDate(List<AbstractSchedule> schedules, LocalDate date) {
List<AbstractSchedule> filteredSchedules = new ArrayList<>();
for(AbstractSchedule schedule : schedules) {
if(schedule.isCompleted()) {
LocalDate startDate = LocalDate.from(schedule.getStartTime());
LocalDate endDate = LocalDate.from(schedule.getStopTime());
if(startDate.isEqual(date) || endDate.isEqual(date)) {
filteredSchedules.add(schedule);
}
}
}
return filteredSchedules;
}
} }

View File

@ -18,6 +18,7 @@ import { HttpClient, HttpHeaders, HttpParams,
import { CustomHttpParameterCodec } from '../encoder'; import { CustomHttpParameterCodec } from '../encoder';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ScheduleInfo } from '../model/models';
import { ScheduleStatus } from '../model/models'; import { ScheduleStatus } from '../model/models';
import { SimpleStatusResponse } from '../model/models'; import { SimpleStatusResponse } from '../model/models';
import { TaskgroupActivityInfo } from '../model/models'; import { TaskgroupActivityInfo } from '../model/models';
@ -87,6 +88,65 @@ export class HistoryService {
return httpParams; return httpParams;
} }
/**
* List past schedules
* Get schedules of the past
* @param date date
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public historySchedulesDateGet(date: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<Array<ScheduleInfo>>;
public historySchedulesDateGet(date: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<Array<ScheduleInfo>>>;
public historySchedulesDateGet(date: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<Array<ScheduleInfo>>>;
public historySchedulesDateGet(date: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<any> {
if (date === null || date === undefined) {
throw new Error('Required parameter date was null or undefined when calling historySchedulesDateGet.');
}
let localVarHeaders = this.defaultHeaders;
let localVarCredential: string | undefined;
// authentication (API_TOKEN) required
localVarCredential = this.configuration.lookupCredential('API_TOKEN');
if (localVarCredential) {
localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential);
}
let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (localVarHttpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (localVarHttpHeaderAcceptSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
}
let localVarHttpContext: HttpContext | undefined = options && options.context;
if (localVarHttpContext === undefined) {
localVarHttpContext = new HttpContext();
}
let responseType_: 'text' | 'json' = 'json';
if(localVarHttpHeaderAcceptSelected && localVarHttpHeaderAcceptSelected.startsWith('text')) {
responseType_ = 'text';
}
return this.httpClient.get<Array<ScheduleInfo>>(`${this.configuration.basePath}/history/schedules/${encodeURIComponent(String(date))}`,
{
context: localVarHttpContext,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
observe: observe,
reportProgress: reportProgress
}
);
}
/** /**
* get number of active minutes * get number of active minutes
* get number of worked minutes today * get number of worked minutes today

View File

@ -0,0 +1,24 @@
/**
* API Title
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { ScheduleInfo } from './scheduleInfo';
export interface PastScheduleInfo {
MONDAY: Array<ScheduleInfo>;
TUESDAY: Array<ScheduleInfo>;
WEDNESDAY: Array<ScheduleInfo>;
THURSDAY: Array<ScheduleInfo>;
FRIDAY: Array<ScheduleInfo>;
SATURDAY: Array<ScheduleInfo>;
SUNDAY: Array<ScheduleInfo>;
}

View File

@ -16,6 +16,7 @@ import {
ForgottenTaskStartDialogComponent ForgottenTaskStartDialogComponent
} from "./dashboard/forgotten-task-start-dialog/forgotten-task-start-dialog.component"; } from "./dashboard/forgotten-task-start-dialog/forgotten-task-start-dialog.component";
import {TaskgroupActivityComponent} from "./statistics/taskgroup-activity/taskgroup-activity.component"; import {TaskgroupActivityComponent} from "./statistics/taskgroup-activity/taskgroup-activity.component";
import {ScheduleHistoryComponent} from "./statistics/schedule-history/schedule-history.component";
const routes: Routes = [ const routes: Routes = [
{path: '', component: MainComponent}, {path: '', component: MainComponent},
@ -32,7 +33,8 @@ const routes: Routes = [
{path: 'active', component: ActiveTaskOverviewComponent}, {path: 'active', component: ActiveTaskOverviewComponent},
{path: 'scheduler', component: DraggableSchedulerComponent}, {path: 'scheduler', component: DraggableSchedulerComponent},
{path: 'forgotten', component: ForgottenTaskStartDialogComponent}, {path: 'forgotten', component: ForgottenTaskStartDialogComponent},
{path: 'statistics/taskgroup-activity', component: TaskgroupActivityComponent} {path: 'statistics/taskgroup-activity', component: TaskgroupActivityComponent},
{path: 'statistics/schedule-history', component: ScheduleHistoryComponent},
]; ];
@NgModule({ @NgModule({

View File

@ -14,6 +14,7 @@
<mat-menu #statisticsMenu=matMenu> <mat-menu #statisticsMenu=matMenu>
<button mat-menu-item routerLink="statistics/taskgroup-activity">Activity</button> <button mat-menu-item routerLink="statistics/taskgroup-activity">Activity</button>
<button mat-menu-item routerLink="statistics/schedule-history">History</button>
</mat-menu> </mat-menu>
<span class="example-spacer"></span> <span class="example-spacer"></span>

View File

@ -85,6 +85,7 @@ import {NgxSliderModule} from "ngx-slider-v2";
import {NgApexchartsModule} from "ng-apexcharts"; import {NgApexchartsModule} from "ng-apexcharts";
import { SimpleActivityDiagramComponent } from './statistics/taskgroup-activity/simple-activity-diagram/simple-activity-diagram.component'; import { SimpleActivityDiagramComponent } from './statistics/taskgroup-activity/simple-activity-diagram/simple-activity-diagram.component';
import { HeatmapActivityComponent } from './statistics/taskgroup-activity/heatmap-activity/heatmap-activity.component'; import { HeatmapActivityComponent } from './statistics/taskgroup-activity/heatmap-activity/heatmap-activity.component';
import { ScheduleHistoryComponent } from './statistics/schedule-history/schedule-history.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
@ -128,6 +129,7 @@ import { HeatmapActivityComponent } from './statistics/taskgroup-activity/heatma
TaskgroupActivityComponent, TaskgroupActivityComponent,
SimpleActivityDiagramComponent, SimpleActivityDiagramComponent,
HeatmapActivityComponent, HeatmapActivityComponent,
ScheduleHistoryComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -0,0 +1,25 @@
.container {
margin: 20px auto;
width: 70%;
}
.spacer {
margin-bottom: 2.5%;
}
@media screen and (max-width: 600px) {
.container {
width: 100%;
margin: 20px 10px;
}
}
.long-form {
width: 100%;
margin-bottom: 10px;
}
table {
width: 100%;
}

View File

@ -0,0 +1,46 @@
<div class="container">
<app-navigation-link-list #navLinkList [navigationLinks]="defaultNavigationLinkPath"></app-navigation-link-list>
<mat-form-field class="long-form">
<mat-label>Choose a date</mat-label>
<input matInput [matDatepicker]="picker" [(ngModel)]="selectedDate">
<mat-hint>MM/DD/YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<table mat-table [dataSource]="schedules" class="mat-elevation-z8">
<!--- Note that these columns can be defined in any order.
The actual rendered columns are set as a property on the row definition" -->
<!-- Position Column -->
<ng-container matColumnDef="taskgroup">
<th mat-header-cell *matHeaderCellDef> Taskgroup </th>
<td mat-cell *matCellDef="let element"> {{computeTaskgroupPath(element.taskgroupPath)}} </td>
</ng-container>
<ng-container matColumnDef="task">
<th mat-header-cell *matHeaderCellDef> Task </th>
<td mat-cell *matCellDef="let element"> {{element.task.taskName}} </td>
</ng-container>
<ng-container matColumnDef="start">
<th mat-header-cell *matHeaderCellDef> Starttime </th>
<td mat-cell *matCellDef="let element"> {{formatDate(element.startTime)}} </td>
</ng-container>
<ng-container matColumnDef="end">
<th mat-header-cell *matHeaderCellDef> Endtime </th>
<td mat-cell *matCellDef="let element"> {{formatDate(element.finishedTime)}} </td>
</ng-container>
<ng-container matColumnDef="duration">
<th mat-header-cell *matHeaderCellDef> Duration </th>
<td mat-cell *matCellDef="let element"> {{calcDuration(element.startTime, element.finishedTime)}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>

View File

@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ScheduleHistoryComponent } from './schedule-history.component';
describe('ScheduleHistoryComponent', () => {
let component: ScheduleHistoryComponent;
let fixture: ComponentFixture<ScheduleHistoryComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ScheduleHistoryComponent]
});
fixture = TestBed.createComponent(ScheduleHistoryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,62 @@
import {Component, OnInit} from '@angular/core';
import {HistoryService, ScheduleInfo, TaskgroupEntityInfo} from "../../../api";
import * as moment from "moment";
import {NavigationLink} from "../../navigation-link-list/navigation-link-list.component";
@Component({
selector: 'app-schedule-history',
templateUrl: './schedule-history.component.html',
styleUrls: ['./schedule-history.component.css']
})
export class ScheduleHistoryComponent implements OnInit{
defaultNavigationLinkPath: NavigationLink[] = [
{
linkText: 'Dashboard',
routerLink: ['/']
},
{
linkText: 'Statistics',
routerLink: []
},
{
linkText: 'Taskgroup Activity',
routerLink: ['/statistics/taskgroup-activity']
}
];
selectedDate: Date = new Date();
schedules: ScheduleInfo[] = []
displayedColumns: string[] = ['taskgroup', 'task', 'start', 'end', 'duration'];
ngOnInit() {
this.historyService.historySchedulesDateGet(moment(this.selectedDate).format("YYYY-MM-DD")).subscribe({
next: resp => {
this.schedules = resp;
}
})
}
constructor(private historyService: HistoryService) {
}
computeTaskgroupPath(taskgroupPath: TaskgroupEntityInfo[]) {
let result = "";
taskgroupPath.forEach(taskgroup => {
result += taskgroup.taskgroupName + "/";
})
return result;
}
formatDate(date: string) {
return moment(date).format('dd, DD. MMM YYYY');
}
calcDuration(startDate: string, endDate: string) {
const start = moment(new Date(startDate));
const end = moment(new Date(endDate));
const duration = moment.duration(end.diff(start));
return duration.asMinutes() + " Minutes";
}
}

View File

@ -1961,6 +1961,31 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/SimpleStatusResponse' $ref: '#/components/schemas/SimpleStatusResponse'
/history/schedules/{date}:
get:
security:
- API_TOKEN: []
tags:
- history
description: Get schedules of the past
summary: List past schedules
parameters:
- name: date
in: path
description: date
required: true
schema:
type: string
format: date
responses:
200:
description: Operatio successfull
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ScheduleInfo'
components: components:
@ -2637,4 +2662,6 @@ components:
activeMinutes: activeMinutes:
type: number type: number
description: Number of minutes the task was active description: Number of minutes the task was active
example: 122 example: 122