issue-77 #105

Merged
sebastian merged 17 commits from issue-77 into master 2024-03-16 10:12:35 +01:00
14 changed files with 224 additions and 74 deletions
Showing only changes of commit aafb5886db - Show all commits

View File

@ -6,19 +6,9 @@ import core.entities.timemanager.Task;
import java.time.DayOfWeek; import java.time.DayOfWeek;
public class TaskRepeatWeekDayInfo { public class TaskRepeatWeekDayInfo {
private DayOfWeek dayOfWeek;
private int offset; private int offset;
private long taskID; private long taskID;
public DayOfWeek getDayOfWeek() {
return dayOfWeek;
}
public void setDayOfWeek(DayOfWeek dayOfWeek) {
this.dayOfWeek = dayOfWeek;
}
public int getOffset() { public int getOffset() {
return offset; return offset;
} }

View File

@ -3,18 +3,17 @@ package core.api.models.timemanager.tasks.repeatinginfo;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Size;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
public class TaskRepeatWeekInfo { public class TaskRepeatWeekInfo {
@Length(min = 1, max = 7) @Size(min = 1, max = 7)
private List<TaskRepeatWeekDayInfo> weekDayInfos; private List<TaskRepeatWeekDayInfo> weekDayInfos;
private DeadlineStrategy deadlineStrategy; private DeadlineStrategy deadlineStrategy;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private LocalDate endDate; private LocalDate endDate;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private LocalDate startDate;
public List<TaskRepeatWeekDayInfo> getWeekDayInfos() { public List<TaskRepeatWeekDayInfo> getWeekDayInfos() {
return weekDayInfos; return weekDayInfos;

View File

@ -36,6 +36,9 @@ public class TaskSeriesService {
Optional<Task> task = taskRepository.findById(taskRepeatDayInfo.getTaskID()); Optional<Task> task = taskRepository.findById(taskRepeatDayInfo.getTaskID());
if(task.isEmpty()) return ServiceExitCode.MISSING_ENTITY; if(task.isEmpty()) return ServiceExitCode.MISSING_ENTITY;
TaskSerieItem rootItem = taskSerie.addTask(task.get());
task.get().setTaskSerieItem(rootItem);
LocalDate currentTaskDate = task.get().getStartDate().plusDays(taskRepeatDayInfo.getOffset()); LocalDate currentTaskDate = task.get().getStartDate().plusDays(taskRepeatDayInfo.getOffset());
while(currentTaskDate.isBefore(taskRepeatInfo.getEndDate())) { while(currentTaskDate.isBefore(taskRepeatInfo.getEndDate())) {
Task clonedTask = Task.cloneTask(task.get()); Task clonedTask = Task.cloneTask(task.get());
@ -44,12 +47,14 @@ public class TaskSeriesService {
TaskSerieItem taskSerieItem = taskSerie.addTask(clonedTask); TaskSerieItem taskSerieItem = taskSerie.addTask(clonedTask);
clonedTask.setTaskSerieItem(taskSerieItem); clonedTask.setTaskSerieItem(taskSerieItem);
createdTasks.add(clonedTask); createdTasks.add(clonedTask);
currentTaskDate = currentTaskDate.plusDays(taskRepeatDayInfo.getOffset());
} }
} }
taskSerie.getTasks().sort(Comparator.comparing(o -> o.getTask().getStartDate())); taskSerie.getTasks().sort(Comparator.comparing(o -> o.getTask().getStartDate()));
for(int i=0; i<taskSerie.getTasks().size(); i++) { for(int i=0; i<taskSerie.getTasks().size(); i++) {
taskSerie.getTasks().get(i).setSeriesIndex(i); taskSerie.getTasks().get(i).setSeriesIndex(i+1);
if(taskRepeatInfo.getDeadlineStrategy() == DeadlineStrategy.DEADLINE_EQUAL_START) { if(taskRepeatInfo.getDeadlineStrategy() == DeadlineStrategy.DEADLINE_EQUAL_START) {
taskSerie.getTasks().get(i).getTask().setDeadline(taskSerie.getTasks().get(i).getTask().getStartDate()); taskSerie.getTasks().get(i).getTask().setDeadline(taskSerie.getTasks().get(i).getTask().getStartDate());
} else { } else {

View File

@ -36,10 +36,6 @@ public class TaskService {
} }
public ServiceResult<Task> createTask(Taskgroup taskgroup, TaskFieldInfo taskFieldInfo) { public ServiceResult<Task> createTask(Taskgroup taskgroup, TaskFieldInfo taskFieldInfo) {
if(existTaskByName(taskgroup.getTasks(), taskFieldInfo.getTaskName())) {
return new ServiceResult<>(ServiceExitCode.ENTITY_ALREADY_EXIST);
}
//Check for invalid date (deadline before start //Check for invalid date (deadline before start
if(taskFieldInfo.getStartDate() != null && taskFieldInfo.getDeadline() != null && if(taskFieldInfo.getStartDate() != null && taskFieldInfo.getDeadline() != null &&
taskFieldInfo.getDeadline().isBefore(taskFieldInfo.getStartDate())) { taskFieldInfo.getDeadline().isBefore(taskFieldInfo.getStartDate())) {

View File

@ -49,6 +49,8 @@ model/taskEntityInfo.ts
model/taskFieldInfo.ts model/taskFieldInfo.ts
model/taskOverviewInfo.ts model/taskOverviewInfo.ts
model/taskRepeatDayInfo.ts model/taskRepeatDayInfo.ts
model/taskRepeatWeekDayInfo.ts
model/taskRepeatWeekInfo.ts
model/taskScheduleStopResponse.ts model/taskScheduleStopResponse.ts
model/taskShortInfo.ts model/taskShortInfo.ts
model/taskTaskgroupInfo.ts model/taskTaskgroupInfo.ts

View File

@ -20,6 +20,7 @@ import { Observable } from 'rxjs';
import { SimpleStatusResponse } from '../model/models'; import { SimpleStatusResponse } from '../model/models';
import { TaskRepeatDayInfo } from '../model/models'; import { TaskRepeatDayInfo } from '../model/models';
import { TaskRepeatWeekInfo } from '../model/models';
import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; import { BASE_PATH, COLLECTION_FORMATS } from '../variables';
import { Configuration } from '../configuration'; import { Configuration } from '../configuration';
@ -156,4 +157,70 @@ export class TaskseriesService {
); );
} }
/**
* daily repeating task creation
* Creates a daily repeating task
* @param taskRepeatWeekInfo
* @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 tasksTaskseriesWeeklyPost(taskRepeatWeekInfo?: TaskRepeatWeekInfo, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<SimpleStatusResponse>;
public tasksTaskseriesWeeklyPost(taskRepeatWeekInfo?: TaskRepeatWeekInfo, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<SimpleStatusResponse>>;
public tasksTaskseriesWeeklyPost(taskRepeatWeekInfo?: TaskRepeatWeekInfo, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<SimpleStatusResponse>>;
public tasksTaskseriesWeeklyPost(taskRepeatWeekInfo?: TaskRepeatWeekInfo, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<any> {
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();
}
// to determine the Content-Type header
const consumes: string[] = [
'application/json'
];
const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes);
if (httpContentTypeSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected);
}
let responseType_: 'text' | 'json' = 'json';
if(localVarHttpHeaderAcceptSelected && localVarHttpHeaderAcceptSelected.startsWith('text')) {
responseType_ = 'text';
}
return this.httpClient.post<SimpleStatusResponse>(`${this.configuration.basePath}/tasks/taskseries/weekly`,
taskRepeatWeekInfo,
{
context: localVarHttpContext,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
observe: observe,
reportProgress: reportProgress
}
);
}
} }

View File

@ -30,6 +30,8 @@ export * from './taskEntityInfo';
export * from './taskFieldInfo'; export * from './taskFieldInfo';
export * from './taskOverviewInfo'; export * from './taskOverviewInfo';
export * from './taskRepeatDayInfo'; export * from './taskRepeatDayInfo';
export * from './taskRepeatWeekDayInfo';
export * from './taskRepeatWeekInfo';
export * from './taskScheduleStopResponse'; export * from './taskScheduleStopResponse';
export * from './taskShortInfo'; export * from './taskShortInfo';
export * from './taskTaskgroupInfo'; export * from './taskTaskgroupInfo';

View File

@ -93,6 +93,7 @@ import { ConnectionSettingsComponent } from './user-settings/connection-settings
import { NtfySettingsComponent } from './user-settings/connection-settings/ntfy-settings/ntfy-settings.component'; import { NtfySettingsComponent } from './user-settings/connection-settings/ntfy-settings/ntfy-settings.component';
import { TaskSeriesCreatorComponent } from './tasks/task-series-creator/task-series-creator.component'; import { TaskSeriesCreatorComponent } from './tasks/task-series-creator/task-series-creator.component';
import {MatStepperModule} from "@angular/material/stepper"; import {MatStepperModule} from "@angular/material/stepper";
import { TaskWeeklySeriesCreatorComponent } from './tasks/task-weekly-series-creator/task-weekly-series-creator.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
@ -141,6 +142,7 @@ import {MatStepperModule} from "@angular/material/stepper";
ConnectionSettingsComponent, ConnectionSettingsComponent,
NtfySettingsComponent, NtfySettingsComponent,
TaskSeriesCreatorComponent, TaskSeriesCreatorComponent,
TaskWeeklySeriesCreatorComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -20,7 +20,7 @@ td, th {
margin: 0 auto; margin: 0 auto;
} }
.mat-column-status, .mat-column-eta { .mat-column-status, .mat-column-eta, .mat-column-select {
width: 32px; width: 32px;
text-align: left; text-align: left;
} }

View File

@ -8,6 +8,22 @@
<div class="mat-elevation-z8"> <div class="mat-elevation-z8">
<table mat-table [dataSource]="datasource" matSort> <table mat-table [dataSource]="datasource" matSort>
<!-- Checkbox Column -->
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? toggleAllRows() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
<td mat-cell *matCellDef="let task"> <td mat-cell *matCellDef="let task">
@ -45,7 +61,9 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="delete"> <ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef mat-sort-header></th> <th mat-header-cell *matHeaderCellDef mat-sort-header>
<button mat-icon-button color="primary" (click)="repeatSelectedTasks()"><mat-icon>event_repeat</mat-icon></button>
</th>
<td mat-cell *matCellDef="let task"> <td mat-cell *matCellDef="let task">
<button mat-icon-button color="warn" (click)="deleteTask(task)"><mat-icon>delete</mat-icon></button> <button mat-icon-button color="warn" (click)="deleteTask(task)"><mat-icon>delete</mat-icon></button>
</td> </td>

View File

@ -10,6 +10,8 @@ import {MatSnackBar} from "@angular/material/snack-bar";
import {ClearTaskDialogComponent, ClearTaskDialogData} from "../clear-task-dialog/clear-task-dialog.component"; import {ClearTaskDialogComponent, ClearTaskDialogData} from "../clear-task-dialog/clear-task-dialog.component";
import * as moment from "moment/moment"; import * as moment from "moment/moment";
import {TaskStatus, TaskStatusService} from "../task-status.service"; import {TaskStatus, TaskStatusService} from "../task-status.service";
import {SelectionModel} from "@angular/cdk/collections";
import {TaskWeeklySeriesCreatorComponent} from "../task-weekly-series-creator/task-weekly-series-creator.component";
@Component({ @Component({
selector: 'app-task-dashboard', selector: 'app-task-dashboard',
@ -19,15 +21,7 @@ import {TaskStatus, TaskStatusService} from "../task-status.service";
export class TaskDashboardComponent implements OnChanges{ export class TaskDashboardComponent implements OnChanges{
ngOnChanges(): void { ngOnChanges(): void {
if(this.taskgroupID != undefined) { if(this.taskgroupID != undefined) {
this.taskService.tasksTaskgroupIDStatusGet(this.taskgroupID!, "all").subscribe({ this.fetchTasks()
next: resp => {
this.datasource = new MatTableDataSource<TaskEntityInfo>(resp);
this.datasource.paginator = this.paginator!;
this.datasource.sort = this.sort!;
resp.forEach(task => console.log(task))
}
})
} }
} }
@ -35,9 +29,29 @@ export class TaskDashboardComponent implements OnChanges{
@ViewChild(MatPaginator) paginator: MatPaginator | undefined @ViewChild(MatPaginator) paginator: MatPaginator | undefined
@ViewChild(MatSort) sort: MatSort | undefined @ViewChild(MatSort) sort: MatSort | undefined
displayedColumns: string[] = ['status', 'name', 'eta', 'start', 'deadline', 'finished', 'edit', 'delete']; displayedColumns: string[] = ['select', 'status', 'name', 'eta', 'start', 'deadline', 'finished', 'edit', 'delete'];
datasource: MatTableDataSource<TaskEntityInfo> = new MatTableDataSource<TaskEntityInfo>(); datasource: MatTableDataSource<TaskEntityInfo> = new MatTableDataSource<TaskEntityInfo>();
selection = new SelectionModel<TaskEntityInfo>(true, []);
/** Whether the number of selected elements matches the total number of rows. */
isAllSelected() {
const numSelected = this.selection.selected.length;
const numRows = this.datasource.data.length;
return numSelected === numRows;
}
/** Selects all rows if they are not all selected; otherwise clear selection. */
toggleAllRows() {
if (this.isAllSelected()) {
this.selection.clear();
return;
}
this.selection.select(...this.datasource.data);
}
constructor(private taskService: TaskService, constructor(private taskService: TaskService,
private dialog: MatDialog, private dialog: MatDialog,
private snackbar: MatSnackBar, private snackbar: MatSnackBar,
@ -107,4 +121,30 @@ export class TaskDashboardComponent implements OnChanges{
} }
protected readonly TaskStatus = TaskStatus; protected readonly TaskStatus = TaskStatus;
repeatSelectedTasks() {
const selectedTasks = this.selection.selected;
const dialogRef = this.dialog.open(TaskWeeklySeriesCreatorComponent, {
data: selectedTasks,
minWidth: "400px"
});
dialogRef.afterClosed().subscribe(res => {
if(res) {
this.fetchTasks()
}
})
}
fetchTasks() {
this.taskService.tasksTaskgroupIDStatusGet(this.taskgroupID!, "all").subscribe({
next: resp => {
this.datasource = new MatTableDataSource<TaskEntityInfo>(resp);
this.datasource.paginator = this.paginator!;
this.datasource.sort = this.sort!;
resp.forEach(task => console.log(task))
}
})
}
} }

View File

@ -1,22 +1,7 @@
<h1 mat-dialog-title>Create Task-Series</h1> <h1 mat-dialog-title>Create Task-Series</h1>
<mat-stepper linear="linear" #stepper orientation="vertical"> <mat-stepper linear="linear" #stepper orientation="vertical">
<mat-step [stepControl]="firstFormGroup"> <mat-step [stepControl]="dailyFormGroup">
<form [formGroup]="firstFormGroup"> <form [formGroup]="dailyFormGroup">
<ng-template matStepLabel>Select Task Repeating Strategy</ng-template>
<mat-form-field appearance="outline" class="long-form">
<mat-label>Repeating Strategy</mat-label>
<mat-select formControlName="repeatingStrategyCtrl">
<mat-option value="DAILY">Daily</mat-option>
<mat-option value="WEEKLY">Weekly</mat-option>
</mat-select>
</mat-form-field>
<div>
<button mat-raised-button color="primary" matStepperNext (click)="onSelectRepeatingStrategy()">Next</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="repeatingStrategy ? dailyFormGroup : weeklyFormGroup">
<form [formGroup]="dailyFormGroup" *ngIf="repeatingStrategy=='DAILY'">
<ng-template matStepLabel>Define Repeating Information</ng-template> <ng-template matStepLabel>Define Repeating Information</ng-template>
<mat-form-field appearance="outline" class="long-form"> <mat-form-field appearance="outline" class="long-form">
<mat-label>Offset</mat-label> <mat-label>Offset</mat-label>

View File

@ -13,25 +13,15 @@ import * as moment from "moment";
}) })
export class TaskSeriesCreatorComponent { export class TaskSeriesCreatorComponent {
firstFormGroup = this._formBuilder.group({
repeatingStrategyCtrl: ['', Validators.required]
})
dailyFormGroup = this._formBuilder.group({ dailyFormGroup = this._formBuilder.group({
offsetCtrl: ['', Validators.required], offsetCtrl: ['', Validators.required],
deadlineStrategyCtrl: ['', Validators.required] deadlineStrategyCtrl: ['', Validators.required]
}) })
weeklyFormGroup = this._formBuilder.group({
})
endDateFormGroup = this._formBuilder.group({ endDateFormGroup = this._formBuilder.group({
endDateCtrl: ['', Validators.required] endDateCtrl: ['', Validators.required]
}) })
repeatingStrategy: "DAILY"|"WEEKLY"|undefined = undefined
constructor(private _formBuilder: FormBuilder, constructor(private _formBuilder: FormBuilder,
private taskSeriesService: TaskseriesService, private taskSeriesService: TaskseriesService,
@Inject(MAT_DIALOG_DATA) public task: TaskEntityInfo, @Inject(MAT_DIALOG_DATA) public task: TaskEntityInfo,
@ -39,23 +29,7 @@ export class TaskSeriesCreatorComponent {
} }
onSelectRepeatingStrategy() {
const selectedStrategy = this.firstFormGroup.get("repeatingStrategyCtrl")!.value!
if(selectedStrategy === "DAILY") {
this.repeatingStrategy = "DAILY";
} else {
this.repeatingStrategy = "WEEKLY";
}
console.log(this.repeatingStrategy)
}
save() { save() {
if(this.repeatingStrategy === 'DAILY') {
this.saveDailyRepeating()
}
}
saveDailyRepeating() {
this.taskSeriesService.tasksTaskIDTaskseriesDailyPost(this.task.taskID,{ this.taskSeriesService.tasksTaskIDTaskseriesDailyPost(this.task.taskID,{
offset: Number(this.dailyFormGroup.get('offsetCtrl')!.value!), offset: Number(this.dailyFormGroup.get('offsetCtrl')!.value!),
deadlineStrategy: this.convertDeadlineStrategyCtrlToDeadlineEnum(), deadlineStrategy: this.convertDeadlineStrategyCtrlToDeadlineEnum(),

View File

@ -2119,6 +2119,38 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/SimpleStatusResponse' $ref: '#/components/schemas/SimpleStatusResponse'
/tasks/taskseries/weekly:
post:
security:
- API_TOKEN: []
tags:
- taskseries
description: Creates a daily repeating task
summary: daily repeating task creation
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TaskRepeatWeekInfo'
responses:
200:
description: Operation successfull
content:
application/json:
schema:
$ref: '#/components/schemas/SimpleStatusResponse'
403:
description: No permission
content:
application/json:
schema:
$ref: '#/components/schemas/SimpleStatusResponse'
404:
description: Task not found
content:
application/json:
schema:
$ref: '#/components/schemas/SimpleStatusResponse'
components: components:
@ -2846,4 +2878,42 @@ components:
endingDate: endingDate:
type: string type: string
format: date format: date
description: Date until the tasks repeat description: Date until the tasks repeat
TaskRepeatWeekDayInfo:
required:
- offset
- taskID
additionalProperties: false
properties:
offset:
type: number
description: number repeating days
example: 7
minimum: 1
taskID:
type: number
description: internal identifier of task
example: 1
TaskRepeatWeekInfo:
required:
- weekDayInfos
- deadlineStrategy
- endDate
additionalProperties: false
properties:
deadlineStrategy:
type: string
enum:
- DEADLINE_EQUAL_START
- DEADLINE_FIT_START
endDate:
type: string
format: date
description: Date until the tasks repeat
weekDayInfos:
type: array
items:
$ref: '#/components/schemas/TaskRepeatWeekDayInfo'
maxLength: 7
minLength: 1