diff --git a/backend/src/main/java/ch/puzzle/okr/Constants.java b/backend/src/main/java/ch/puzzle/okr/Constants.java index 38220a0e91..197d197e85 100644 --- a/backend/src/main/java/ch/puzzle/okr/Constants.java +++ b/backend/src/main/java/ch/puzzle/okr/Constants.java @@ -7,11 +7,14 @@ private Constants() { public static final String KEY_RESULT_TYPE_METRIC = "metric"; public static final String KEY_RESULT_TYPE_ORDINAL = "ordinal"; public static final String OBJECTIVE = "Objective"; + public static final String OBJECTIVE_LOWERCASE = "objective"; public static final String STATE_DRAFT = "Draft"; public static final String KEY_RESULT = "KeyResult"; public static final String CHECK_IN = "Check-in"; public static final String ACTION = "Action"; public static final String ALIGNMENT = "Alignment"; + public static final String ALIGNMENT_VIEW = "AlignmentView"; + public static final String ALIGNED_OBJECTIVE_ID = "alignedObjectiveId"; public static final String COMPLETED = "Completed"; public static final String ORGANISATION = "Organisation"; public static final String QUARTER = "Quarter"; diff --git a/backend/src/main/java/ch/puzzle/okr/ErrorKey.java b/backend/src/main/java/ch/puzzle/okr/ErrorKey.java index 415a76da20..50d20d7df7 100644 --- a/backend/src/main/java/ch/puzzle/okr/ErrorKey.java +++ b/backend/src/main/java/ch/puzzle/okr/ErrorKey.java @@ -4,5 +4,5 @@ public enum ErrorKey { ATTRIBUTE_NULL, ATTRIBUTE_CHANGED, ATTRIBUTE_SET_FORBIDDEN, ATTRIBUTE_NOT_SET, ATTRIBUTE_CANNOT_CHANGE, ATTRIBUTE_MUST_BE_DRAFT, KEY_RESULT_CONVERSION, ALREADY_EXISTS_SAME_NAME, CONVERT_TOKEN, DATA_HAS_BEEN_UPDATED, MODEL_NULL, MODEL_WITH_ID_NOT_FOUND, NOT_AUTHORIZED_TO_READ, NOT_AUTHORIZED_TO_WRITE, NOT_AUTHORIZED_TO_DELETE, - TOKEN_NULL + TOKEN_NULL, NOT_LINK_YOURSELF, NOT_LINK_IN_SAME_TEAM, ALIGNMENT_ALREADY_EXISTS, ALIGNMENT_DATA_FAIL } diff --git a/backend/src/main/java/ch/puzzle/okr/SecurityConfig.java b/backend/src/main/java/ch/puzzle/okr/SecurityConfig.java index 7bd87b4c2f..5b2cdf77c9 100644 --- a/backend/src/main/java/ch/puzzle/okr/SecurityConfig.java +++ b/backend/src/main/java/ch/puzzle/okr/SecurityConfig.java @@ -47,8 +47,9 @@ private HttpSecurity setHeaders(HttpSecurity http) throws Exception { + "script-src 'self' 'unsafe-inline';" + " style-src 'self' 'unsafe-inline';" + " object-src 'none';" + " base-uri 'self';" + " connect-src 'self' https://sso.puzzle.ch http://localhost:8544;" - + " font-src 'self';" + " frame-src 'self';" + " img-src 'self' data: ;" - + " manifest-src 'self';" + " media-src 'self';" + " worker-src 'none';")) + + " font-src 'self';" + " frame-src 'self';" + + " img-src 'self' data: blob:;" + " manifest-src 'self';" + + " media-src 'self';" + " worker-src 'none';")) .crossOriginEmbedderPolicy(coepCustomizer -> coepCustomizer .policy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP)) .crossOriginOpenerPolicy(coopCustomizer -> coopCustomizer diff --git a/backend/src/main/java/ch/puzzle/okr/controller/AlignmentController.java b/backend/src/main/java/ch/puzzle/okr/controller/AlignmentController.java index 85b6a77ec1..d4d307fac0 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/AlignmentController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/AlignmentController.java @@ -1,8 +1,7 @@ package ch.puzzle.okr.controller; -import ch.puzzle.okr.dto.alignment.AlignmentObjectiveDto; -import ch.puzzle.okr.mapper.AlignmentSelectionMapper; -import ch.puzzle.okr.service.business.AlignmentSelectionBusinessService; +import ch.puzzle.okr.dto.alignment.AlignmentLists; +import ch.puzzle.okr.service.business.AlignmentBusinessService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -20,26 +19,23 @@ @RestController @RequestMapping("api/v2/alignments") public class AlignmentController { - private final AlignmentSelectionMapper alignmentSelectionMapper; - private final AlignmentSelectionBusinessService alignmentSelectionBusinessService; + private final AlignmentBusinessService alignmentBusinessService; - public AlignmentController(AlignmentSelectionMapper alignmentSelectionMapper, - AlignmentSelectionBusinessService alignmentSelectionBusinessService) { - this.alignmentSelectionMapper = alignmentSelectionMapper; - this.alignmentSelectionBusinessService = alignmentSelectionBusinessService; + public AlignmentController(AlignmentBusinessService alignmentBusinessService) { + this.alignmentBusinessService = alignmentBusinessService; } - @Operation(summary = "Get all objectives and their key results to select the alignment", description = "Get a list of objectives with their key results to select the alignment") + @Operation(summary = "Get AlignmentLists from filter", description = "Get a list of AlignmentObjects with all AlignmentConnections, which match current quarter, team and objective filter") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Returned a list of objectives with their key results to select the alignment", content = { - @Content(mediaType = "application/json", schema = @Schema(implementation = AlignmentObjectiveDto.class)) }), - @ApiResponse(responseCode = "400", description = "Can't return list of objectives with their key results to select the alignment", content = @Content) }) - @GetMapping("/selections") - public ResponseEntity> getAlignmentSelections( - @RequestParam(required = false, defaultValue = "", name = "quarter") Long quarterFilter, - @RequestParam(required = false, defaultValue = "", name = "team") Long teamFilter) { + @ApiResponse(responseCode = "200", description = "Returned AlignmentLists, which match current filters", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AlignmentLists.class)) }), + @ApiResponse(responseCode = "400", description = "Can't generate AlignmentLists from current filters", content = @Content) }) + @GetMapping("/alignmentLists") + public ResponseEntity getAlignments( + @RequestParam(required = false, defaultValue = "", name = "teamFilter") List teamFilter, + @RequestParam(required = false, defaultValue = "", name = "quarterFilter") Long quarterFilter, + @RequestParam(required = false, defaultValue = "", name = "objectiveQuery") String objectiveQuery) { return ResponseEntity.status(HttpStatus.OK) - .body(alignmentSelectionMapper.toDto(alignmentSelectionBusinessService - .getAlignmentSelectionByQuarterIdAndTeamIdNot(quarterFilter, teamFilter))); + .body(alignmentBusinessService.getAlignmentListsByFilters(quarterFilter, teamFilter, objectiveQuery)); } } diff --git a/backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java b/backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java index 61e0f2cab8..672afebf37 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java @@ -1,5 +1,6 @@ package ch.puzzle.okr.controller; +import ch.puzzle.okr.dto.AlignmentDto; import ch.puzzle.okr.dto.ObjectiveDto; import ch.puzzle.okr.mapper.ObjectiveMapper; import ch.puzzle.okr.models.Objective; @@ -14,6 +15,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static org.springframework.http.HttpStatus.IM_USED; import static org.springframework.http.HttpStatus.OK; @@ -42,6 +45,19 @@ public ResponseEntity getObjective( .body(objectiveMapper.toDto(objectiveAuthorizationService.getEntityById(id))); } + @Operation(summary = "Get Alignment possibilities", description = "Get all possibilities to create an Alignment") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Returned all Alignment possibilities for an Objective", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AlignmentDto.class)) }), + @ApiResponse(responseCode = "401", description = "Not authorized to get Alignment possibilities", content = @Content), + @ApiResponse(responseCode = "404", description = "Did not find any possibilities to create an Alignment", content = @Content) }) + @GetMapping("/alignmentPossibilities/{quarterId}") + public ResponseEntity> getAlignmentPossibilities( + @Parameter(description = "The Quarter ID for getting Alignment possibilities.", required = true) @PathVariable Long quarterId) { + return ResponseEntity.status(HttpStatus.OK) + .body(objectiveAuthorizationService.getAlignmentPossibilities(quarterId)); + } + @Operation(summary = "Delete Objective by ID", description = "Delete Objective by ID") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Deleted Objective by ID"), @ApiResponse(responseCode = "401", description = "Not authorized to delete an Objective", content = @Content), diff --git a/backend/src/main/java/ch/puzzle/okr/dto/AlignmentDto.java b/backend/src/main/java/ch/puzzle/okr/dto/AlignmentDto.java new file mode 100644 index 0000000000..3ac7114429 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/AlignmentDto.java @@ -0,0 +1,6 @@ +package ch.puzzle.okr.dto; + +import java.util.List; + +public record AlignmentDto(Long teamId, String teamName, List alignmentObjects) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/dto/AlignmentObjectDto.java b/backend/src/main/java/ch/puzzle/okr/dto/AlignmentObjectDto.java new file mode 100644 index 0000000000..363278549b --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/AlignmentObjectDto.java @@ -0,0 +1,4 @@ +package ch.puzzle.okr.dto; + +public record AlignmentObjectDto(Long objectId, String objectTitle, String objectType) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveDto.java b/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveDto.java index bf230fdcc8..e4754b7bef 100644 --- a/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveDto.java +++ b/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveDto.java @@ -1,9 +1,11 @@ package ch.puzzle.okr.dto; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import ch.puzzle.okr.models.State; import java.time.LocalDateTime; public record ObjectiveDto(Long id, int version, String title, Long teamId, Long quarterId, String quarterLabel, - String description, State state, LocalDateTime createdOn, LocalDateTime modifiedOn, boolean writeable) { + String description, State state, LocalDateTime createdOn, LocalDateTime modifiedOn, boolean writeable, + AlignedEntityDto alignedEntity) { } diff --git a/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignedEntityDto.java b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignedEntityDto.java new file mode 100644 index 0000000000..9a4b94b86d --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignedEntityDto.java @@ -0,0 +1,4 @@ +package ch.puzzle.okr.dto.alignment; + +public record AlignedEntityDto(Long id, String type) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentConnectionDto.java b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentConnectionDto.java new file mode 100644 index 0000000000..a721058a0a --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentConnectionDto.java @@ -0,0 +1,5 @@ + +package ch.puzzle.okr.dto.alignment; + +public record AlignmentConnectionDto(Long alignedObjectiveId, Long targetObjectiveId, Long targetKeyResultId) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentLists.java b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentLists.java new file mode 100644 index 0000000000..4206fcde23 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentLists.java @@ -0,0 +1,7 @@ +package ch.puzzle.okr.dto.alignment; + +import java.util.List; + +public record AlignmentLists(List alignmentObjectDtoList, + List alignmentConnectionDtoList) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentObjectDto.java b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentObjectDto.java new file mode 100644 index 0000000000..b23c24f1d0 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentObjectDto.java @@ -0,0 +1,5 @@ +package ch.puzzle.okr.dto.alignment; + +public record AlignmentObjectDto(Long objectId, String objectTitle, String objectTeamName, String objectState, + String objectType) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/AlignmentSelectionMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/AlignmentSelectionMapper.java deleted file mode 100644 index 547181b95e..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/mapper/AlignmentSelectionMapper.java +++ /dev/null @@ -1,58 +0,0 @@ -package ch.puzzle.okr.mapper; - -import ch.puzzle.okr.dto.alignment.AlignmentKeyResultDto; -import ch.puzzle.okr.dto.alignment.AlignmentObjectiveDto; -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -@Component -public class AlignmentSelectionMapper { - - public List toDto(List alignments) { - List alignmentDtos = new ArrayList<>(); - alignments.forEach(alignment -> processObjectives(alignmentDtos, alignment)); - return alignmentDtos; - } - - private Optional getMatchingObjectiveDto(Long objectiveId, - List objectives) { - return objectives.stream().filter(objectiveDto -> Objects.equals(objectiveId, objectiveDto.id())).findFirst(); - } - - private void processObjectives(List objectiveDtos, AlignmentSelection alignment) { - Optional objectiveDto = getMatchingObjectiveDto( - alignment.getAlignmentSelectionId().getObjectiveId(), objectiveDtos); - if (objectiveDto.isPresent()) { - processKeyResults(objectiveDto.get(), alignment); - } else { - AlignmentObjectiveDto alignmentObjectiveDto = createObjectiveDto(alignment); - objectiveDtos.add(alignmentObjectiveDto); - processKeyResults(alignmentObjectiveDto, alignment); - } - } - - private void processKeyResults(AlignmentObjectiveDto objectiveDto, AlignmentSelection alignment) { - if (isValidId(alignment.getAlignmentSelectionId().getKeyResultId())) { - objectiveDto.keyResults().add(createKeyResultDto(alignment)); - } - } - - private AlignmentObjectiveDto createObjectiveDto(AlignmentSelection alignment) { - return new AlignmentObjectiveDto(alignment.getAlignmentSelectionId().getObjectiveId(), - alignment.getObjectiveTitle(), new ArrayList<>()); - } - - private AlignmentKeyResultDto createKeyResultDto(AlignmentSelection alignment) { - return new AlignmentKeyResultDto(alignment.getAlignmentSelectionId().getKeyResultId(), - alignment.getKeyResultTitle()); - } - - private boolean isValidId(Long id) { - return id != null && id > -1; - } -} diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java index a65574c408..cd340040f9 100644 --- a/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java +++ b/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java @@ -19,11 +19,13 @@ public ObjectiveMapper(TeamBusinessService teamBusinessService, QuarterBusinessS this.quarterBusinessService = quarterBusinessService; } + // TODO: Adjust Unit Tests of ObjectiveMapper after merge of multitenancy-main + public ObjectiveDto toDto(Objective objective) { return new ObjectiveDto(objective.getId(), objective.getVersion(), objective.getTitle(), objective.getTeam().getId(), objective.getQuarter().getId(), objective.getQuarter().getLabel(), objective.getDescription(), objective.getState(), objective.getCreatedOn(), objective.getModifiedOn(), - objective.isWriteable()); + objective.isWriteable(), objective.getAlignedEntity()); } public Objective toObjective(ObjectiveDto objectiveDto) { @@ -31,6 +33,7 @@ public Objective toObjective(ObjectiveDto objectiveDto) { .withTitle(objectiveDto.title()).withTeam(teamBusinessService.getTeamById(objectiveDto.teamId())) .withDescription(objectiveDto.description()).withModifiedOn(LocalDateTime.now()) .withState(objectiveDto.state()).withCreatedOn(objectiveDto.createdOn()) - .withQuarter(quarterBusinessService.getQuarterById(objectiveDto.quarterId())).build(); + .withQuarter(quarterBusinessService.getQuarterById(objectiveDto.quarterId())) + .withAlignedEntity(objectiveDto.alignedEntity()).build(); } } diff --git a/backend/src/main/java/ch/puzzle/okr/models/Objective.java b/backend/src/main/java/ch/puzzle/okr/models/Objective.java index 85a96b59dd..caff1e0395 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/Objective.java +++ b/backend/src/main/java/ch/puzzle/okr/models/Objective.java @@ -1,5 +1,6 @@ package ch.puzzle.okr.models; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -52,6 +53,7 @@ public class Objective implements WriteableInterface { private User modifiedBy; private transient boolean writeable; + private transient AlignedEntityDto alignedEntity; public Objective() { } @@ -68,6 +70,7 @@ private Objective(Builder builder) { setState(builder.state); setCreatedOn(builder.createdOn); setModifiedBy(builder.modifiedBy); + setAlignedEntity(builder.alignedEntity); } public Long getId() { @@ -160,12 +163,20 @@ public void setWriteable(boolean writeable) { this.writeable = writeable; } + public AlignedEntityDto getAlignedEntity() { + return alignedEntity; + } + + public void setAlignedEntity(AlignedEntityDto alignedEntity) { + this.alignedEntity = alignedEntity; + } + @Override public String toString() { return "Objective{" + "id=" + id + ", version=" + version + ", title='" + title + '\'' + ", createdBy=" + createdBy + ", team=" + team + ", quarter=" + quarter + ", description='" + description + '\'' + ", modifiedOn=" + modifiedOn + ", state=" + state + ", createdOn=" + createdOn + ", modifiedBy=" - + modifiedBy + ", writeable=" + writeable + '\'' + '}'; + + modifiedBy + ", writeable=" + writeable + ", alignedEntity=" + alignedEntity + '\'' + '}'; } @Override @@ -201,6 +212,7 @@ public static final class Builder { private State state; private LocalDateTime createdOn; private User modifiedBy; + private AlignedEntityDto alignedEntity; private Builder() { } @@ -264,6 +276,11 @@ public Builder withModifiedBy(User modifiedBy) { return this; } + public Builder withAlignedEntity(AlignedEntityDto alignedEntity) { + this.alignedEntity = alignedEntity; + return this; + } + public Objective build() { return new Objective(this); } diff --git a/backend/src/main/java/ch/puzzle/okr/models/alignment/Alignment.java b/backend/src/main/java/ch/puzzle/okr/models/alignment/Alignment.java index cf3a64f7fc..9cacab5dfb 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/alignment/Alignment.java +++ b/backend/src/main/java/ch/puzzle/okr/models/alignment/Alignment.java @@ -34,6 +34,10 @@ public Long getId() { return id; } + public void setId(Long id) { + this.id = id; + } + public int getVersion() { return version; } diff --git a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelection.java b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelection.java deleted file mode 100644 index 0246817184..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelection.java +++ /dev/null @@ -1,140 +0,0 @@ -package ch.puzzle.okr.models.alignment; - -import jakarta.persistence.EmbeddedId; -import jakarta.persistence.Entity; -import org.hibernate.annotations.Immutable; - -import java.util.Objects; - -@Entity -@Immutable -public class AlignmentSelection { - - @EmbeddedId - private AlignmentSelectionId alignmentSelectionId; - - private Long teamId; - private String teamName; - private String objectiveTitle; - private Long quarterId; - private String quarterLabel; - private String keyResultTitle; - - public AlignmentSelection() { - } - - private AlignmentSelection(Builder builder) { - alignmentSelectionId = builder.alignmentSelectionId; - teamId = builder.teamId; - teamName = builder.teamName; - objectiveTitle = builder.objectiveTitle; - quarterId = builder.quarterId; - quarterLabel = builder.quarterLabel; - keyResultTitle = builder.keyResultTitle; - } - - public AlignmentSelectionId getAlignmentSelectionId() { - return alignmentSelectionId; - } - - public Long getTeamId() { - return teamId; - } - - public String getObjectiveTitle() { - return objectiveTitle; - } - - public Long getQuarterId() { - return quarterId; - } - - public String getKeyResultTitle() { - return keyResultTitle; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - AlignmentSelection alignmentSelection = (AlignmentSelection) o; - return Objects.equals(alignmentSelectionId, alignmentSelection.alignmentSelectionId) - && Objects.equals(teamId, alignmentSelection.teamId) - && Objects.equals(objectiveTitle, alignmentSelection.objectiveTitle) - && Objects.equals(quarterId, alignmentSelection.quarterId) - && Objects.equals(keyResultTitle, alignmentSelection.keyResultTitle); - } - - @Override - public int hashCode() { - return Objects.hash(alignmentSelectionId, teamId, objectiveTitle, quarterId, keyResultTitle); - } - - @Override - public String toString() { - return "AlignmentSelection{" + "alignmentSelectionId=" + alignmentSelectionId + ", teamId='" + teamId - + ", teamName='" + teamName + '\'' + ", objectiveTitle='" + objectiveTitle + '\'' + ", quarterId=" - + quarterId + ", quarterLabel='" + quarterLabel + '\'' + ", keyResultTitle='" + keyResultTitle + '\'' - + '}'; - } - - public static final class Builder { - private AlignmentSelectionId alignmentSelectionId; - - private Long teamId; - private String teamName; - private String objectiveTitle; - private Long quarterId; - private String quarterLabel; - private String keyResultTitle; - - public Builder() { - // This builder can be empty, so that it can get called - } - - public static Builder builder() { - return new Builder(); - } - - public Builder withAlignmentSelectionId(AlignmentSelectionId alignmentSelectionId) { - this.alignmentSelectionId = alignmentSelectionId; - return this; - } - - public Builder withTeamId(Long teamId) { - this.teamId = teamId; - return this; - } - - public Builder withTeamName(String teamName) { - this.teamName = teamName; - return this; - } - - public Builder withObjectiveTitle(String objectiveTitle) { - this.objectiveTitle = objectiveTitle; - return this; - } - - public Builder withQuarterId(Long quarterId) { - this.quarterId = quarterId; - return this; - } - - public Builder withQuarterLabel(String quarterLabel) { - this.quarterLabel = quarterLabel; - return this; - } - - public Builder withKeyResultTitle(String keyResultTitle) { - this.keyResultTitle = keyResultTitle; - return this; - } - - public AlignmentSelection build() { - return new AlignmentSelection(this); - } - } -} diff --git a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelectionId.java b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelectionId.java deleted file mode 100644 index 75c52cf2b8..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelectionId.java +++ /dev/null @@ -1,83 +0,0 @@ -package ch.puzzle.okr.models.alignment; - -import jakarta.persistence.Embeddable; - -import java.io.Serializable; -import java.util.Objects; - -@Embeddable -public class AlignmentSelectionId implements Serializable { - - private Long objectiveId; - private Long keyResultId; - - public AlignmentSelectionId() { - } - - private AlignmentSelectionId(Long objectiveId, Long keyResultId) { - this.objectiveId = objectiveId; - this.keyResultId = keyResultId; - } - - private AlignmentSelectionId(Builder builder) { - this(builder.objectiveId, builder.keyResultId); - } - - public static AlignmentSelectionId of(Long objectiveId, Long keyResultId) { - return new AlignmentSelectionId(objectiveId, keyResultId); - } - - public Long getObjectiveId() { - return objectiveId; - } - - public Long getKeyResultId() { - return keyResultId; - } - - @Override - public String toString() { - return "AlignmentSelectionId{" + "objectiveId=" + objectiveId + ", keyResultId=" + keyResultId + '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - AlignmentSelectionId that = (AlignmentSelectionId) o; - return Objects.equals(objectiveId, that.objectiveId) && Objects.equals(keyResultId, that.keyResultId); - } - - @Override - public int hashCode() { - return Objects.hash(objectiveId, keyResultId); - } - - public static final class Builder { - private Long objectiveId; - private Long keyResultId; - - private Builder() { - } - - public static Builder builder() { - return new Builder(); - } - - public Builder withObjectiveId(Long objectiveId) { - this.objectiveId = objectiveId; - return this; - } - - public Builder withKeyResultId(Long keyResultId) { - this.keyResultId = keyResultId; - return this; - } - - public AlignmentSelectionId build() { - return new AlignmentSelectionId(this); - } - } -} diff --git a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java new file mode 100644 index 0000000000..6fd0ea825c --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java @@ -0,0 +1,236 @@ +package ch.puzzle.okr.models.alignment; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.Immutable; + +import java.util.Objects; + +@Entity +@Immutable +public class AlignmentView { + + @Id + private String uniqueId; + private Long id; + private String title; + private Long teamId; + private String teamName; + private Long quarterId; + private String state; + private String objectType; + private String connectionRole; + private Long counterpartId; + private String counterpartType; + + public AlignmentView() { + } + + private AlignmentView(Builder builder) { + setUniqueId(builder.uniqueId); + setId(builder.id); + setTitle(builder.title); + setTeamId(builder.teamId); + setTeamName(builder.teamName); + setQuarterId(builder.quarterId); + setState(builder.state); + setObjectType(builder.objectType); + setConnectionRole(builder.connectionRole); + setCounterpartId(builder.counterpartId); + setCounterpartType(builder.counterpartType); + } + + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Long getTeamId() { + return teamId; + } + + public void setTeamId(Long teamId) { + this.teamId = teamId; + } + + public String getTeamName() { + return teamName; + } + + public void setTeamName(String teamName) { + this.teamName = teamName; + } + + public Long getQuarterId() { + return quarterId; + } + + public void setQuarterId(Long quarterId) { + this.quarterId = quarterId; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getObjectType() { + return objectType; + } + + public void setObjectType(String objectType) { + this.objectType = objectType; + } + + public String getConnectionRole() { + return connectionRole; + } + + public void setConnectionRole(String connectionItem) { + this.connectionRole = connectionItem; + } + + public Long getCounterpartId() { + return counterpartId; + } + + public void setCounterpartId(Long refId) { + this.counterpartId = refId; + } + + public String getCounterpartType() { + return counterpartType; + } + + public void setCounterpartType(String refType) { + this.counterpartType = refType; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + AlignmentView that = (AlignmentView) o; + return Objects.equals(uniqueId, that.uniqueId) && Objects.equals(id, that.id) + && Objects.equals(title, that.title) && Objects.equals(teamId, that.teamId) + && Objects.equals(teamName, that.teamName) && Objects.equals(quarterId, that.quarterId) + && Objects.equals(state, that.state) && Objects.equals(objectType, that.objectType) + && Objects.equals(connectionRole, that.connectionRole) + && Objects.equals(counterpartId, that.counterpartId) + && Objects.equals(counterpartType, that.counterpartType); + } + + @Override + public int hashCode() { + return Objects.hash(uniqueId, id, title, teamId, teamName, quarterId, state, objectType, connectionRole, + counterpartId, counterpartType); + } + + @Override + public String toString() { + return "AlignmentView{" + "uniqueId='" + uniqueId + '\'' + ", id=" + id + ", title='" + title + '\'' + + ", teamId=" + teamId + ", teamName='" + teamName + '\'' + ", quarterId=" + quarterId + ", state='" + + state + '\'' + ", objectType='" + objectType + '\'' + ", connectionItem='" + connectionRole + '\'' + + ", refId=" + counterpartId + ", refType='" + counterpartType + '\'' + '}'; + } + + public static final class Builder { + private String uniqueId; + private Long id; + private String title; + private Long teamId; + private String teamName; + private Long quarterId; + private String state; + private String objectType; + private String connectionRole; + private Long counterpartId; + private String counterpartType; + + private Builder() { + } + + public static AlignmentView.Builder builder() { + return new AlignmentView.Builder(); + } + + public Builder withUniqueId(String val) { + uniqueId = val; + return this; + } + + public Builder withId(Long val) { + id = val; + return this; + } + + public Builder withTitle(String val) { + title = val; + return this; + } + + public Builder withTeamId(Long val) { + teamId = val; + return this; + } + + public Builder withTeamName(String val) { + teamName = val; + return this; + } + + public Builder withQuarterId(Long val) { + quarterId = val; + return this; + } + + public Builder withState(String val) { + state = val; + return this; + } + + public Builder withObjectType(String val) { + objectType = val; + return this; + } + + public Builder withConnectionRole(String val) { + connectionRole = val; + return this; + } + + public Builder withCounterpartId(Long val) { + counterpartId = val; + return this; + } + + public Builder withCounterpartType(String val) { + counterpartType = val; + return this; + } + + public AlignmentView build() { + return new AlignmentView(this); + } + } +} diff --git a/backend/src/main/java/ch/puzzle/okr/repository/AlignmentRepository.java b/backend/src/main/java/ch/puzzle/okr/repository/AlignmentRepository.java index d6ebcc70a7..16e8210932 100644 --- a/backend/src/main/java/ch/puzzle/okr/repository/AlignmentRepository.java +++ b/backend/src/main/java/ch/puzzle/okr/repository/AlignmentRepository.java @@ -11,7 +11,7 @@ public interface AlignmentRepository extends CrudRepository { - List findByAlignedObjectiveId(Long alignedObjectiveId); + Alignment findByAlignedObjectiveId(Long alignedObjectiveId); @Query(value = "from KeyResultAlignment where targetKeyResult.id = :keyResultId") List findByKeyResultAlignmentId(@Param("keyResultId") Long keyResultId); diff --git a/backend/src/main/java/ch/puzzle/okr/repository/AlignmentSelectionRepository.java b/backend/src/main/java/ch/puzzle/okr/repository/AlignmentSelectionRepository.java deleted file mode 100644 index 50896b44f3..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/repository/AlignmentSelectionRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package ch.puzzle.okr.repository; - -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.models.alignment.AlignmentSelectionId; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; - -public interface AlignmentSelectionRepository extends ReadOnlyRepository { - - @Query(value = "from AlignmentSelection where quarterId = :quarter_id and teamId != :ignoredTeamId") - List getAlignmentSelectionByQuarterIdAndTeamIdNot(@Param("quarter_id") Long quarterId, - @Param("ignoredTeamId") Long ignoredTeamId); -} diff --git a/backend/src/main/java/ch/puzzle/okr/repository/AlignmentViewRepository.java b/backend/src/main/java/ch/puzzle/okr/repository/AlignmentViewRepository.java new file mode 100644 index 0000000000..b1bd63c4f3 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/repository/AlignmentViewRepository.java @@ -0,0 +1,14 @@ +package ch.puzzle.okr.repository; + +import ch.puzzle.okr.models.alignment.AlignmentView; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface AlignmentViewRepository extends CrudRepository { + + @Query(value = "SELECT * FROM alignment_view where quarter_id = :quarterId ", nativeQuery = true) + List getAlignmentViewByQuarterId(@Param("quarterId") Long quarterId); +} diff --git a/backend/src/main/java/ch/puzzle/okr/repository/ObjectiveRepository.java b/backend/src/main/java/ch/puzzle/okr/repository/ObjectiveRepository.java index 009b34ac96..ccbc0aaa8d 100644 --- a/backend/src/main/java/ch/puzzle/okr/repository/ObjectiveRepository.java +++ b/backend/src/main/java/ch/puzzle/okr/repository/ObjectiveRepository.java @@ -14,4 +14,6 @@ public interface ObjectiveRepository extends CrudRepository { Integer countByTeamAndQuarter(Team team, Quarter quarter); List findObjectivesByTeamId(Long id); + + List findObjectivesByQuarterId(Long id); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java b/backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java index bb213bf2d8..cbeb5b484b 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java @@ -1,10 +1,13 @@ package ch.puzzle.okr.service.authorization; +import ch.puzzle.okr.dto.AlignmentDto; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.service.business.ObjectiveBusinessService; import org.springframework.stereotype.Service; +import java.util.List; + @Service public class ObjectiveAuthorizationService extends AuthorizationServiceBase { @@ -19,6 +22,10 @@ public Objective duplicateEntity(Long id, Objective objective) { return getBusinessService().duplicateObjective(id, objective, authorizationUser); } + public List getAlignmentPossibilities(Long quarterId) { + return getBusinessService().getAlignmentPossibilities(quarterId); + } + @Override protected void hasRoleReadById(Long id, AuthorizationUser authorizationUser) { getAuthorizationService().hasRoleReadByObjectiveId(id, authorizationUser); diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java new file mode 100644 index 0000000000..a9c51e229a --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -0,0 +1,346 @@ +package ch.puzzle.okr.service.business; + +import ch.puzzle.okr.ErrorKey; +import ch.puzzle.okr.dto.alignment.AlignmentConnectionDto; +import ch.puzzle.okr.dto.alignment.AlignmentLists; +import ch.puzzle.okr.dto.alignment.AlignmentObjectDto; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; +import ch.puzzle.okr.exception.OkrResponseStatusException; +import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.alignment.Alignment; +import ch.puzzle.okr.models.alignment.AlignmentView; +import ch.puzzle.okr.models.alignment.KeyResultAlignment; +import ch.puzzle.okr.models.alignment.ObjectiveAlignment; +import ch.puzzle.okr.models.keyresult.KeyResult; +import ch.puzzle.okr.service.persistence.AlignmentPersistenceService; +import ch.puzzle.okr.service.persistence.AlignmentViewPersistenceService; +import ch.puzzle.okr.service.persistence.KeyResultPersistenceService; +import ch.puzzle.okr.service.persistence.ObjectivePersistenceService; +import ch.puzzle.okr.service.validation.AlignmentValidationService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static ch.puzzle.okr.Constants.OBJECTIVE_LOWERCASE; + +@Service +public class AlignmentBusinessService { + + private final AlignmentPersistenceService alignmentPersistenceService; + private final AlignmentValidationService alignmentValidationService; + private final ObjectivePersistenceService objectivePersistenceService; + private final KeyResultPersistenceService keyResultPersistenceService; + private final AlignmentViewPersistenceService alignmentViewPersistenceService; + private final QuarterBusinessService quarterBusinessService; + + public AlignmentBusinessService(AlignmentPersistenceService alignmentPersistenceService, + AlignmentValidationService alignmentValidationService, + ObjectivePersistenceService objectivePersistenceService, + KeyResultPersistenceService keyResultPersistenceService, + AlignmentViewPersistenceService alignmentViewPersistenceService, + QuarterBusinessService quarterBusinessService) { + this.alignmentPersistenceService = alignmentPersistenceService; + this.alignmentValidationService = alignmentValidationService; + this.objectivePersistenceService = objectivePersistenceService; + this.keyResultPersistenceService = keyResultPersistenceService; + this.alignmentViewPersistenceService = alignmentViewPersistenceService; + this.quarterBusinessService = quarterBusinessService; + } + + protected record DividedAlignmentViewLists(List filterMatchingAlignments, + List nonMatchingAlignments) { + } + + public AlignedEntityDto getTargetIdByAlignedObjectiveId(Long alignedObjectiveId) { + alignmentValidationService.validateOnGet(alignedObjectiveId); + Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(alignedObjectiveId); + if (alignment instanceof KeyResultAlignment keyResultAlignment) { + return new AlignedEntityDto(keyResultAlignment.getAlignmentTarget().getId(), "keyResult"); + } else if (alignment instanceof ObjectiveAlignment objectiveAlignment) { + return new AlignedEntityDto(objectiveAlignment.getAlignmentTarget().getId(), "objective"); + } else { + return null; + } + } + + public void createEntity(Objective alignedObjective) { + validateOnCreateAndSaveAlignment(alignedObjective); + } + + private void validateOnCreateAndSaveAlignment(Objective alignedObjective) { + Alignment alignment = buildAlignmentModel(alignedObjective, 0); + alignmentValidationService.validateOnCreate(alignment); + alignmentPersistenceService.save(alignment); + } + + public void updateEntity(Long objectiveId, Objective objective) { + Alignment savedAlignment = alignmentPersistenceService.findByAlignedObjectiveId(objectiveId); + if (savedAlignment == null) { + validateOnCreateAndSaveAlignment(objective); + } else { + if (objective.getAlignedEntity() == null) { + validateOnDeleteAndDeleteById(savedAlignment.getId()); + } else { + Alignment alignment = buildAlignmentModel(objective, savedAlignment.getVersion()); + validateOnUpdateAndRecreateOrSaveAlignment(alignment, savedAlignment); + } + } + } + + private void validateOnUpdateAndRecreateOrSaveAlignment(Alignment alignment, Alignment savedAlignment) { + if (isAlignmentTypeChange(alignment, savedAlignment)) { + validateOnUpdateAndRecreateAlignment(savedAlignment.getId(), alignment); + } else { + validateOnUpdateAndSaveAlignment(savedAlignment.getId(), alignment); + } + } + + private void validateOnUpdateAndRecreateAlignment(Long id, Alignment alignment) { + alignment.setId(id); + alignmentValidationService.validateOnUpdate(id, alignment); + alignmentPersistenceService.recreateEntity(id, alignment); + } + + private void validateOnUpdateAndSaveAlignment(Long id, Alignment alignment) { + alignment.setId(id); + alignmentValidationService.validateOnUpdate(id, alignment); + alignmentPersistenceService.save(alignment); + } + + public Alignment buildAlignmentModel(Objective alignedObjective, int version) { + if (alignedObjective.getAlignedEntity().type().equals("objective")) { + Long entityId = alignedObjective.getAlignedEntity().id(); + + Objective targetObjective = objectivePersistenceService.findById(entityId); + return ObjectiveAlignment.Builder.builder() // + .withAlignedObjective(alignedObjective) // + .withTargetObjective(targetObjective) // + .withVersion(version).build(); + } else if (alignedObjective.getAlignedEntity().type().equals("keyResult")) { + Long entityId = alignedObjective.getAlignedEntity().id(); + + KeyResult targetKeyResult = keyResultPersistenceService.findById(entityId); + return KeyResultAlignment.Builder.builder() // + .withAlignedObjective(alignedObjective) // + .withTargetKeyResult(targetKeyResult) // + .withVersion(version).build(); + } else { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NOT_SET, + List.of("alignedEntity", alignedObjective.getAlignedEntity())); + } + } + + public boolean isAlignmentTypeChange(Alignment alignment, Alignment savedAlignment) { + return (alignment instanceof ObjectiveAlignment && savedAlignment instanceof KeyResultAlignment) + || (alignment instanceof KeyResultAlignment && savedAlignment instanceof ObjectiveAlignment); + } + + public void updateKeyResultIdOnIdChange(Long oldKeyResultId, KeyResult keyResult) { + alignmentPersistenceService.findByKeyResultAlignmentId(oldKeyResultId) + .forEach(alignment -> validateOnUpdateAndSaveAlignment(keyResult, alignment)); + } + + private void validateOnUpdateAndSaveAlignment(KeyResult keyResult, KeyResultAlignment alignment) { + alignment.setAlignmentTarget(keyResult); + alignmentValidationService.validateOnUpdate(alignment.getId(), alignment); + alignmentPersistenceService.save(alignment); + } + + public void deleteAlignmentByObjectiveId(Long objectiveId) { + ensureAlignmentIdIsNotNull(objectiveId); + alignmentPersistenceService.findByObjectiveAlignmentId(objectiveId) + .forEach(objectiveAlignment -> validateOnDeleteAndDeleteById(objectiveAlignment.getId())); + } + + private void ensureAlignmentIdIsNotNull(Long objectiveId) { + Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(objectiveId); + if (alignment != null) { + validateOnDeleteAndDeleteById(alignment.getId()); + } + } + + private void validateOnDeleteAndDeleteById(Long id) { + alignmentValidationService.validateOnDelete(id); + alignmentPersistenceService.deleteById(id); + } + + public void deleteAlignmentByKeyResultId(Long keyResultId) { + alignmentPersistenceService.findByKeyResultAlignmentId(keyResultId) + .forEach(keyResultAlignment -> validateOnDeleteAndDeleteById(keyResultAlignment.getId())); + } + + public AlignmentLists getAlignmentListsByFilters(Long quarterFilter, List teamFilter, + String objectiveFilter) { + quarterFilter = quarterFilter(quarterFilter); + teamFilter = Objects.requireNonNullElse(teamFilter, List.of()); + alignmentValidationService.validateOnAlignmentGet(quarterFilter, teamFilter); + + if (teamFilter.isEmpty()) { + return new AlignmentLists(List.of(), List.of()); + } + + List correctAlignmentViewList = correctAlignmentViewList(quarterFilter, teamFilter, + objectiveFilter); + sourceAndTargetListsEqualSameSize(correctAlignmentViewList, quarterFilter, teamFilter, objectiveFilter); + return generateAlignmentLists(correctAlignmentViewList); + } + + private Long quarterFilter(Long quarterFilter) { + if (Objects.isNull(quarterFilter)) { + return quarterBusinessService.getCurrentQuarter().getId(); + } + return quarterFilter; + } + + private List correctAlignmentViewList(Long quarterFilter, List teamFilter, + String objectiveFilter) { + List alignmentViewListByQuarter = alignmentViewPersistenceService + .getAlignmentViewListByQuarterId(quarterFilter); + + DividedAlignmentViewLists dividedAlignmentViewLists = filterAndDivideAlignmentViews(alignmentViewListByQuarter, + teamFilter, objectiveFilter); + return getAlignmentCounterpart(dividedAlignmentViewLists); + } + + protected void sourceAndTargetListsEqualSameSize(List finalList, Long quarterFilter, + List teamFilter, String objectiveFilter) { + List sourceList = finalList.stream() // + .filter(alignmentView -> Objects.equals(alignmentView.getConnectionRole(), "source")) // + .toList(); + + List targetList = finalList.stream() // + .filter(alignmentView -> Objects.equals(alignmentView.getConnectionRole(), "target")) // + .toList(); + + if (sourceList.size() != targetList.size()) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ALIGNMENT_DATA_FAIL, + List.of("alignmentData", quarterFilter, teamFilter, objectiveFilter)); + } + } + + protected AlignmentLists generateAlignmentLists(List alignmentViewList) { + List distictObjectDtoList = createDistinctAlignmentObjectDtoList(alignmentViewList); + List alignmentConnectionDtoList = createAlignmentConnectionDtoListFromConnections( + alignmentViewList); + + return new AlignmentLists(distictObjectDtoList, alignmentConnectionDtoList); + } + + private List createDistinctAlignmentObjectDtoList(List alignmentViewList) { + List alignmentObjectDtoList = new ArrayList<>(); + alignmentViewList.forEach(alignmentView -> alignmentObjectDtoList.add(new AlignmentObjectDto( // + alignmentView.getId(), // + alignmentView.getTitle(), // + alignmentView.getTeamName(), // + alignmentView.getState(), // + alignmentView.getObjectType()))); + + return alignmentObjectDtoList.stream() // + .distinct() // + .toList(); + } + + private List createAlignmentConnectionDtoListFromConnections( + List alignmentViewList) { + List alignmentConnectionDtoList = new ArrayList<>(); + alignmentViewList.forEach(alignmentView -> { + if (Objects.equals(alignmentView.getConnectionRole(), "source")) { + if (Objects.equals(alignmentView.getCounterpartType(), OBJECTIVE_LOWERCASE)) { + alignmentConnectionDtoList.add(new AlignmentConnectionDto( // + alignmentView.getId(), alignmentView.getCounterpartId(), null)); + } else { + alignmentConnectionDtoList.add(new AlignmentConnectionDto( // + alignmentView.getId(), null, alignmentView.getCounterpartId())); + } + } + }); + return alignmentConnectionDtoList; + } + + protected List getAlignmentCounterpart(DividedAlignmentViewLists alignmentViewLists) { + List nonMatchingAlignments = alignmentViewLists.nonMatchingAlignments(); + List filterMatchingAlignments = alignmentViewLists.filterMatchingAlignments(); + List correctAlignmentViewList = correctAlignmentViewList(filterMatchingAlignments, + nonMatchingAlignments); + return createFinalAlignmentViewList(filterMatchingAlignments, correctAlignmentViewList); + } + + private List correctAlignmentViewList(List filterMatchingAlignments, + List nonMatchingAlignments) { + List correctAlignmentViewList = new ArrayList<>(); + filterMatchingAlignments.forEach(alignment -> { + Optional matchingObject = findMatchingAlignmentInList(nonMatchingAlignments, alignment); + matchingObject.map(correctAlignmentViewList::add); + }); + return correctAlignmentViewList; + } + + private Optional findMatchingAlignmentInList(List alignmentList, + AlignmentView alignment) { + return alignmentList.stream().filter(view -> isMatching(alignment, view)).findFirst(); + } + + private boolean isMatching(AlignmentView firstAlignment, AlignmentView secondAlignment) { + return Objects.equals(secondAlignment.getId(), firstAlignment.getCounterpartId()) + && Objects.equals(secondAlignment.getObjectType(), firstAlignment.getCounterpartType()) + && Objects.equals(secondAlignment.getCounterpartId(), firstAlignment.getId()) + && Objects.equals(secondAlignment.getCounterpartType(), firstAlignment.getObjectType()); + } + + private List createFinalAlignmentViewList(List filterMatchingAlignments, + List correctAlignmentViewList) { + List finalAlignmentViewList = new ArrayList<>(filterMatchingAlignments); + if (!correctAlignmentViewList.isEmpty()) { + finalAlignmentViewList.addAll(correctAlignmentViewList); + } + return finalAlignmentViewList; + } + + protected DividedAlignmentViewLists filterAndDivideAlignmentViews(List alignmentViewList, + List teamFilter, String objectiveFilter) { + List filterMatchingAlignments = filterAlignmentListByTeamAndObjective(alignmentViewList, + teamFilter, objectiveFilter); + List nonMatchingAlignments = filterNonMatchingAlignments(alignmentViewList, + filterMatchingAlignments); + + return new DividedAlignmentViewLists(filterMatchingAlignments, nonMatchingAlignments); + } + + private List filterAlignmentListByTeamAndObjective(List alignmentViewList, + List teamFilter, String objectiveFilter) { + List filteredList = filterByTeam(alignmentViewList, teamFilter); + if (StringUtils.isNotBlank(objectiveFilter)) { + filteredList = filterByObjective(filteredList, objectiveFilter); + } + return filteredList; + } + + private List filterByTeam(List alignmentViewList, List teamFilter) { + return alignmentViewList.stream() // + .filter(alignmentView -> teamFilter.contains(alignmentView.getTeamId())) // + .toList(); + } + + private List filterByObjective(List filteredList, String objectiveFilter) { + return filteredList.stream() // + .filter(alignmentView -> isObjectiveAndMatchesFilter(alignmentView, objectiveFilter)) // + .toList(); + } + + private static boolean isObjectiveAndMatchesFilter(AlignmentView alignmentView, String objectiveFilter) { + return Objects.equals(alignmentView.getObjectType(), OBJECTIVE_LOWERCASE) + && alignmentView.getTitle().toLowerCase().contains(objectiveFilter.toLowerCase()); + } + + private List filterNonMatchingAlignments(List alignmentViewList, + List nonMatchingAlignments) { + return alignmentViewList.stream() // + .filter(alignmentView -> !nonMatchingAlignments.contains(alignmentView)) // + .toList(); + } +} diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessService.java deleted file mode 100644 index 8e6feefed1..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessService.java +++ /dev/null @@ -1,23 +0,0 @@ -package ch.puzzle.okr.service.business; - -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.service.persistence.AlignmentSelectionPersistenceService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class AlignmentSelectionBusinessService { - - private final AlignmentSelectionPersistenceService alignmentSelectionPersistenceService; - - public AlignmentSelectionBusinessService( - AlignmentSelectionPersistenceService alignmentSelectionPersistenceService) { - this.alignmentSelectionPersistenceService = alignmentSelectionPersistenceService; - } - - public List getAlignmentSelectionByQuarterIdAndTeamIdNot(Long quarterId, Long ignoredTeamId) { - return alignmentSelectionPersistenceService.getAlignmentSelectionByQuarterIdAndTeamIdNot(quarterId, - ignoredTeamId); - } -} diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java index 309a09d524..2835f440e7 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java @@ -24,14 +24,16 @@ public class KeyResultBusinessService implements BusinessServiceInterface acti action.resetId(); action.setKeyResult(recreatedEntity); }); + alignmentBusinessService.updateKeyResultIdOnIdChange(id, recreatedEntity); return recreatedEntity; } @@ -102,6 +105,7 @@ public void deleteEntityById(Long id) { .forEach(checkIn -> checkInBusinessService.deleteEntityById(checkIn.getId())); actionBusinessService.getActionsByKeyResultId(id) .forEach(action -> actionBusinessService.deleteEntityById(action.getId())); + alignmentBusinessService.deleteAlignmentByKeyResultId(id); keyResultPersistenceService.deleteById(id); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java index b532aba932..00940f98e1 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java @@ -1,6 +1,10 @@ package ch.puzzle.okr.service.business; +import ch.puzzle.okr.dto.AlignmentDto; +import ch.puzzle.okr.dto.AlignmentObjectDto; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.Team; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.models.keyresult.KeyResult; import ch.puzzle.okr.models.keyresult.KeyResultMetric; @@ -14,8 +18,7 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; -import java.util.List; -import java.util.Objects; +import java.util.*; import static ch.puzzle.okr.Constants.KEY_RESULT_TYPE_METRIC; import static ch.puzzle.okr.Constants.KEY_RESULT_TYPE_ORDINAL; @@ -26,31 +29,118 @@ public class ObjectiveBusinessService implements BusinessServiceInterface getAlignmentPossibilities(Long quarterId) { + validator.validateOnGet(quarterId); + + List objectivesByQuarter = objectivePersistenceService.findObjectiveByQuarterId(quarterId); + List teamList = getTeamsFromObjectives(objectivesByQuarter); + + return createAlignmentDtoForEveryTeam(teamList, objectivesByQuarter); + } + + private List getTeamsFromObjectives(List objectiveList) { + return objectiveList.stream() // + .map(Objective::getTeam) // + .distinct() // + .sorted(Comparator.comparing(Team::getName)) // + .toList(); + } + + private List createAlignmentDtoForEveryTeam(List teamList, + List objectivesByQuarter) { + List alignmentDtoList = new ArrayList<>(); + + teamList.forEach(team -> { + List filteredObjectiveList = objectivesByQuarter.stream() + .filter(objective -> objective.getTeam().equals(team)) + .sorted(Comparator.comparing(Objective::getTitle)).toList(); + + List alignmentObjectDtoList = generateAlignmentObjects(filteredObjectiveList); + AlignmentDto alignmentDto = new AlignmentDto(team.getId(), team.getName(), alignmentObjectDtoList); + alignmentDtoList.add(alignmentDto); + }); + + return alignmentDtoList; + } + + private List generateAlignmentObjects(List filteredObjectiveList) { + List alignmentObjectDtoList = new ArrayList<>(); + filteredObjectiveList.forEach(objective -> { + AlignmentObjectDto objectiveDto = new AlignmentObjectDto(objective.getId(), "O - " + objective.getTitle(), + "objective"); + alignmentObjectDtoList.add(objectiveDto); + + List keyResultList = keyResultBusinessService.getAllKeyResultsByObjective(objective.getId()) + .stream().sorted(Comparator.comparing(KeyResult::getTitle)).toList(); + + keyResultList.forEach(keyResult -> { + AlignmentObjectDto keyResultDto = new AlignmentObjectDto(keyResult.getId(), + "KR - " + keyResult.getTitle(), "keyResult"); + alignmentObjectDtoList.add(keyResultDto); + }); + }); + return alignmentObjectDtoList; } public List getEntitiesByTeamId(Long id) { validator.validateOnGet(id); - return objectivePersistenceService.findObjectiveByTeamId(id); + + List objectiveList = objectivePersistenceService.findObjectiveByTeamId(id); + objectiveList.forEach(objective -> { + AlignedEntityDto alignedEntity = alignmentBusinessService + .getTargetIdByAlignedObjectiveId(objective.getId()); + objective.setAlignedEntity(alignedEntity); + }); + + return objectiveList; } @Transactional public Objective updateEntity(Long id, Objective objective, AuthorizationUser authorizationUser) { Objective savedObjective = objectivePersistenceService.findById(id); + Objective updatedObjective = updateObjectiveWithSavedAttrs(objective, savedObjective, authorizationUser); + + validator.validateOnUpdate(id, updatedObjective); + savedObjective = objectivePersistenceService.save(updatedObjective); + handleAlignedEntity(id, savedObjective, updatedObjective); + return savedObjective; + } + + private void handleAlignedEntity(Long id, Objective savedObjective, Objective updatedObjective) { + AlignedEntityDto alignedEntity = alignmentBusinessService + .getTargetIdByAlignedObjectiveId(savedObjective.getId()); + if ((updatedObjective.getAlignedEntity() != null) + || updatedObjective.getAlignedEntity() == null && alignedEntity != null) { + savedObjective.setAlignedEntity(updatedObjective.getAlignedEntity()); + alignmentBusinessService.updateEntity(id, savedObjective); + } + } + + private Objective updateObjectiveWithSavedAttrs(Objective objective, Objective savedObjective, + AuthorizationUser authorizationUser) { objective.setCreatedBy(savedObjective.getCreatedBy()); objective.setCreatedOn(savedObjective.getCreatedOn()); objective.setModifiedBy(authorizationUser.user()); @@ -61,8 +151,7 @@ public Objective updateEntity(Long id, Objective objective, AuthorizationUser au not = " NOT "; } logger.debug("quarter has changed and is{}changeable, {}", not, objective); - validator.validateOnUpdate(id, objective); - return objectivePersistenceService.save(objective); + return objective; } public boolean isImUsed(Objective objective) { @@ -88,7 +177,11 @@ public Objective createEntity(Objective objective, AuthorizationUser authorizati objective.setCreatedBy(authorizationUser.user()); objective.setCreatedOn(LocalDateTime.now()); validator.validateOnCreate(objective); - return objectivePersistenceService.save(objective); + Objective savedObjective = objectivePersistenceService.save(objective); + if (objective.getAlignedEntity() != null) { + alignmentBusinessService.createEntity(savedObjective); + } + return savedObjective; } @Transactional @@ -96,29 +189,41 @@ public Objective duplicateObjective(Long id, Objective objective, AuthorizationU Objective duplicatedObjective = createEntity(objective, authorizationUser); List keyResultsOfDuplicatedObjective = keyResultBusinessService.getAllKeyResultsByObjective(id); for (KeyResult keyResult : keyResultsOfDuplicatedObjective) { - if (keyResult.getKeyResultType().equals(KEY_RESULT_TYPE_METRIC)) { - KeyResult keyResultMetric = KeyResultMetric.Builder.builder().withObjective(duplicatedObjective) - .withTitle(keyResult.getTitle()).withDescription(keyResult.getDescription()) - .withOwner(keyResult.getOwner()).withUnit(((KeyResultMetric) keyResult).getUnit()) - .withBaseline(0D).withStretchGoal(1D).build(); - keyResultBusinessService.createEntity(keyResultMetric, authorizationUser); - } else if (keyResult.getKeyResultType().equals(KEY_RESULT_TYPE_ORDINAL)) { - KeyResult keyResultOrdinal = KeyResultOrdinal.Builder.builder().withObjective(duplicatedObjective) - .withTitle(keyResult.getTitle()).withDescription(keyResult.getDescription()) - .withOwner(keyResult.getOwner()).withCommitZone("-").withTargetZone("-").withStretchZone("-") - .build(); - keyResultBusinessService.createEntity(keyResultOrdinal, authorizationUser); - } + createKeyResult(keyResult, duplicatedObjective, authorizationUser); } return duplicatedObjective; } + private void createKeyResult(KeyResult keyResult, Objective objective, AuthorizationUser authorizationUser) { + if (keyResult.getKeyResultType().equals(KEY_RESULT_TYPE_METRIC)) { + createMetricKeyResult(keyResult, objective, authorizationUser); + } else if (keyResult.getKeyResultType().equals(KEY_RESULT_TYPE_ORDINAL)) { + createOrdinalKeyResult(keyResult, objective, authorizationUser); + } + } + + private void createMetricKeyResult(KeyResult keyResult, Objective objective, AuthorizationUser authorizationUser) { + KeyResult keyResultMetric = KeyResultMetric.Builder.builder().withObjective(objective) + .withTitle(keyResult.getTitle()).withDescription(keyResult.getDescription()) + .withOwner(keyResult.getOwner()).withUnit(((KeyResultMetric) keyResult).getUnit()).withBaseline(0D) + .withStretchGoal(1D).build(); + keyResultBusinessService.createEntity(keyResultMetric, authorizationUser); + } + + private void createOrdinalKeyResult(KeyResult keyResult, Objective objective, AuthorizationUser authorizationUser) { + KeyResult keyResultOrdinal = KeyResultOrdinal.Builder.builder().withObjective(objective) + .withTitle(keyResult.getTitle()).withDescription(keyResult.getDescription()) + .withOwner(keyResult.getOwner()).withCommitZone("-").withTargetZone("-").withStretchZone("-").build(); + keyResultBusinessService.createEntity(keyResultOrdinal, authorizationUser); + } + @Transactional public void deleteEntityById(Long id) { validator.validateOnDelete(id); completedBusinessService.deleteCompletedByObjectiveId(id); keyResultBusinessService.getAllKeyResultsByObjective(id) .forEach(keyResult -> keyResultBusinessService.deleteEntityById(keyResult.getId())); + alignmentBusinessService.deleteAlignmentByObjectiveId(id); objectivePersistenceService.deleteById(id); } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java index 03bb2ad444..dc69d6b1fc 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java @@ -4,6 +4,9 @@ import ch.puzzle.okr.models.alignment.KeyResultAlignment; import ch.puzzle.okr.models.alignment.ObjectiveAlignment; import ch.puzzle.okr.repository.AlignmentRepository; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.List; @@ -12,6 +15,7 @@ @Service public class AlignmentPersistenceService extends PersistenceBase { + private static final Logger logger = LoggerFactory.getLogger(AlignmentPersistenceService.class); protected AlignmentPersistenceService(AlignmentRepository repository) { super(repository); @@ -22,7 +26,17 @@ public String getModelName() { return ALIGNMENT; } - public List findByAlignedObjectiveId(Long alignedObjectiveId) { + @Transactional + public Alignment recreateEntity(Long id, Alignment alignment) { + logger.debug("Delete and create new Alignment in order to prevent duplicates in case of changed Type"); + logger.debug("{}", alignment); + // delete entity in order to prevent duplicates in case of changed keyResultType + deleteById(id); + logger.debug("Reached delete entity with id {}", id); + return save(alignment); + } + + public Alignment findByAlignedObjectiveId(Long alignedObjectiveId) { return getRepository().findByAlignedObjectiveId(alignedObjectiveId); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceService.java deleted file mode 100644 index 3a439073b8..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceService.java +++ /dev/null @@ -1,21 +0,0 @@ -package ch.puzzle.okr.service.persistence; - -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.repository.AlignmentSelectionRepository; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class AlignmentSelectionPersistenceService { - - private final AlignmentSelectionRepository alignmentSelectionRepository; - - public AlignmentSelectionPersistenceService(AlignmentSelectionRepository alignmentSelectionRepository) { - this.alignmentSelectionRepository = alignmentSelectionRepository; - } - - public List getAlignmentSelectionByQuarterIdAndTeamIdNot(Long quarterId, Long ignoredTeamId) { - return alignmentSelectionRepository.getAlignmentSelectionByQuarterIdAndTeamIdNot(quarterId, ignoredTeamId); - } -} diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceService.java new file mode 100644 index 0000000000..74bb0d9038 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceService.java @@ -0,0 +1,26 @@ +package ch.puzzle.okr.service.persistence; + +import ch.puzzle.okr.models.alignment.AlignmentView; +import ch.puzzle.okr.repository.AlignmentViewRepository; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static ch.puzzle.okr.Constants.ALIGNMENT_VIEW; + +@Service +public class AlignmentViewPersistenceService extends PersistenceBase { + + protected AlignmentViewPersistenceService(AlignmentViewRepository repository) { + super(repository); + } + + @Override + public String getModelName() { + return ALIGNMENT_VIEW; + } + + public List getAlignmentViewListByQuarterId(Long quarterId) { + return getRepository().getAlignmentViewByQuarterId(quarterId); + } +} diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/KeyResultPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/KeyResultPersistenceService.java index 14e223bff5..d8a4da87a4 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/KeyResultPersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/KeyResultPersistenceService.java @@ -3,6 +3,8 @@ import ch.puzzle.okr.models.keyresult.KeyResult; import ch.puzzle.okr.repository.KeyResultRepository; import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.List; @@ -11,6 +13,7 @@ @Service public class KeyResultPersistenceService extends PersistenceBase { + private static final Logger logger = LoggerFactory.getLogger(KeyResultPersistenceService.class); protected KeyResultPersistenceService(KeyResultRepository repository) { super(repository); @@ -27,11 +30,11 @@ public List getKeyResultsByObjective(Long objectiveId) { @Transactional public KeyResult recreateEntity(Long id, KeyResult keyResult) { - System.out.println(keyResult.toString()); - System.out.println("*".repeat(30)); + logger.debug("Delete KeyResult in order to prevent duplicates in case of changed keyResultType"); + logger.debug("{}", keyResult); // delete entity in order to prevent duplicates in case of changed keyResultType deleteById(id); - System.out.printf("reached delete entity with %d", id); + logger.debug("Reached delete entity with id {}", id); return save(keyResult); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceService.java index 675d8ec249..e444a1c085 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceService.java @@ -49,6 +49,10 @@ public Objective findObjectiveById(Long objectiveId, AuthorizationUser authoriza return findByAnyId(objectiveId, authorizationUser, SELECT_OBJECTIVE_BY_ID, noResultException); } + public List findObjectiveByQuarterId(Long quarterId) { + return getRepository().findObjectivesByQuarterId(quarterId); + } + public List findObjectiveByTeamId(Long teamId) { return getRepository().findObjectivesByTeamId(teamId); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java new file mode 100644 index 0000000000..ef126e12fc --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java @@ -0,0 +1,126 @@ +package ch.puzzle.okr.service.validation; + +import ch.puzzle.okr.ErrorKey; +import ch.puzzle.okr.exception.OkrResponseStatusException; +import ch.puzzle.okr.models.Team; +import ch.puzzle.okr.models.alignment.Alignment; +import ch.puzzle.okr.models.alignment.KeyResultAlignment; +import ch.puzzle.okr.models.alignment.ObjectiveAlignment; +import ch.puzzle.okr.repository.AlignmentRepository; +import ch.puzzle.okr.service.persistence.AlignmentPersistenceService; +import ch.puzzle.okr.service.persistence.TeamPersistenceService; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Objects; + +import static ch.puzzle.okr.Constants.ALIGNED_OBJECTIVE_ID; + +@Service +public class AlignmentValidationService + extends ValidationBase { + + private final AlignmentPersistenceService alignmentPersistenceService; + private final TeamPersistenceService teamPersistenceService; + private final QuarterValidationService quarterValidationService; + private final TeamValidationService teamValidationService; + + public AlignmentValidationService(AlignmentPersistenceService alignmentPersistenceService, + TeamPersistenceService teamPersistenceService, QuarterValidationService quarterValidationService, + TeamValidationService teamValidationService) { + super(alignmentPersistenceService); + this.alignmentPersistenceService = alignmentPersistenceService; + this.teamPersistenceService = teamPersistenceService; + this.quarterValidationService = quarterValidationService; + this.teamValidationService = teamValidationService; + } + + @Override + public void validateOnCreate(Alignment model) { + throwExceptionWhenModelIsNull(model); + throwExceptionWhenIdIsNotNull(model.getId()); + throwExceptionWhenAlignmentObjectIsNull(model); + throwExceptionWhenAlignedIdIsSameAsTargetId(model); + throwExceptionWhenAlignmentIsInSameTeam(model); + throwExceptionWhenAlignmentWithAlignedObjectiveAlreadyExists(model); + validate(model); + } + + @Override + public void validateOnUpdate(Long id, Alignment model) { + throwExceptionWhenModelIsNull(model); + throwExceptionWhenIdIsNull(model.getId()); + throwExceptionWhenAlignmentObjectIsNull(model); + throwExceptionWhenAlignedIdIsSameAsTargetId(model); + throwExceptionWhenAlignmentIsInSameTeam(model); + validate(model); + } + + private void throwExceptionWhenAlignmentObjectIsNull(Alignment model) { + if (model.getAlignedObjective() == null) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, + List.of(ALIGNED_OBJECTIVE_ID)); + } else if (model instanceof ObjectiveAlignment objectiveAlignment) { + if (objectiveAlignment.getAlignmentTarget() == null) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, + List.of("targetObjectiveId", objectiveAlignment.getAlignedObjective().getId())); + } + } else if (model instanceof KeyResultAlignment keyResultAlignment + && (keyResultAlignment.getAlignmentTarget() == null)) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, + List.of("targetKeyResultId", keyResultAlignment.getAlignedObjective().getId())); + + } + } + + private void throwExceptionWhenAlignmentIsInSameTeam(Alignment model) { + Team alignedObjectiveTeam = teamPersistenceService.findById(model.getAlignedObjective().getTeam().getId()); + Team targetObjectTeam = null; + + if (model instanceof ObjectiveAlignment objectiveAlignment) { + targetObjectTeam = teamPersistenceService + .findById(objectiveAlignment.getAlignmentTarget().getTeam().getId()); + } else if (model instanceof KeyResultAlignment keyResultAlignment) { + targetObjectTeam = teamPersistenceService + .findById(keyResultAlignment.getAlignmentTarget().getObjective().getTeam().getId()); + } + + if (alignedObjectiveTeam.equals(targetObjectTeam)) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.NOT_LINK_IN_SAME_TEAM, + List.of("teamId", targetObjectTeam.getId())); + } + } + + private void throwExceptionWhenAlignedIdIsSameAsTargetId(Alignment model) { + if (model instanceof ObjectiveAlignment objectiveAlignment + && (Objects.equals(objectiveAlignment.getAlignedObjective().getId(), + objectiveAlignment.getAlignmentTarget().getId()))) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.NOT_LINK_YOURSELF, + List.of("targetObjectiveId", objectiveAlignment.getAlignmentTarget().getId())); + + } + } + + private void throwExceptionWhenAlignmentWithAlignedObjectiveAlreadyExists(Alignment model) { + if (this.alignmentPersistenceService.findByAlignedObjectiveId(model.getAlignedObjective().getId()) != null) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ALIGNMENT_ALREADY_EXISTS, + List.of(ALIGNED_OBJECTIVE_ID, model.getAlignedObjective().getId())); + } + } + + public void validateOnAlignmentGet(Long quarterId, List teamFilter) { + validateQuarter(quarterId); + teamFilter.forEach(this::validateTeam); + } + + public void validateTeam(Long id) { + teamValidationService.validateOnGet(id); + teamValidationService.doesEntityExist(id); + } + + public void validateQuarter(Long id) { + quarterValidationService.validateOnGet(id); + quarterValidationService.doesEntityExist(id); + } +} diff --git a/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql b/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql index bbaff68591..9aee34b4e2 100644 --- a/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql +++ b/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql @@ -33,7 +33,14 @@ values (19, 1, 'Lorem Ipsum sit amet diri guru humu saguri alam apmach helum di (22, 1, 'Lorem Ipsum sit amet diri guru humu saguri alam apmach helum di gau', '2023-10-02 13:07:56.000000', 'Wing Wang Tala Tala Ting Tang', 1, 7, 6, 'DRAFT', null, '2023-10-02 09:08:40.000000'), (21, 1, 'Lorem Ipsum sit amet diri guru humu saguri alam apmach helum di gau', '2023-10-02 13:07:09.000000', - 'Ting Tang Wala Wala Bing Bang', 1, 7, 6, 'DRAFT', null, '2023-10-02 09:07:39.000000'); + 'Ting Tang Wala Wala Bing Bang', 1, 7, 6, 'DRAFT', null, '2023-10-02 09:07:39.000000'), + (40,1,'', '2024-04-04 13:45:13.000000','Wir wollen eine gute Mitarbeiterzufriedenheit.', 1, 6, 5, 'ONGOING', null,'2024-04-04 13:44:52.000000'), + (41,1,'','2024-04-04 13:59:06.511620','Das Projekt generiert 10000 CHF Umsatz',1,6,5,'ONGOING',null,'2024-04-04 13:59:06.523496'), + (42,1,'','2024-04-04 13:59:40.835896','Die Lehrlinge sollen Freude haben',1,6,4,'ONGOING',null,'2024-04-04 13:59:40.848992'), + (43,1,'','2024-04-04 14:00:05.586152','Der Firmenumsatz steigt',1,6,5,'ONGOING',null,'2024-04-04 14:00:05.588509'), + (44,1,'','2024-04-04 14:00:28.221906','Die Members sollen gerne zur Arbeit kommen',1,6,6,'ONGOING',null,'2024-04-04 14:00:28.229058'), + (45,1,'','2024-04-04 14:00:47.659884','Unsere Designer äussern sich zufrieden',1,6,8,'ONGOING',null,'2024-04-04 14:00:47.664414'), + (46,1,'','2024-04-04 14:00:57.485887','Unsere Designer kommen gerne zur Arbeit',1,6,8,'ONGOING',null,'2024-04-04 14:00:57.494192'); insert into key_result (id, version, baseline, description, modified_on, stretch_goal, title, created_by_id, objective_id, owner_id, unit, key_result_type, created_on, commit_zone, target_zone, @@ -123,7 +130,9 @@ values (20, 1, 0, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lore', '2023-10-02 13:15:22.000000', null, 'Clap of thunder bilge aft log crows nest landlubber or just lubber overhaul', 1, 11, 1, '', 'ordinal', - '2023-10-02 09:16:07.000000', 'This is the commit zone', 'This is the target zone', 'This is the stretch zone'); + '2023-10-02 09:16:07.000000', 'This is the commit zone', 'This is the target zone', 'This is the stretch zone'), + (40,1,50,'',null,70,'60% sind in der Membersumfrage zufrienden',1,40,1,'PERCENT','metric','2024-04-04 14:06:21.689768',null,null,null), + (41,1,20000,'',null,80000,'Wir erreichen einen Umsatz von 70000 CHF',1,46,1,'CHF','metric','2024-04-04 14:06:42.100353',null,null,null); insert into check_in (id, version, change_info, created_on, initiatives, modified_on, value_metric, created_by_id, key_result_id, @@ -171,7 +180,8 @@ values (21, 1, null, 1, 30, 2, 'ordinal', 'FAIL'), (32, 1, 'Lorem ipsum dolor sit amet, richi rogsi brokilon', '2023-10-02 08:50:44.059000', ' sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat', '2023-10-02 22:00:00.000000', - 13, 1, 31, 3, 'metric', null); + 13, 1, 31, 3, 'metric', null), + (40,1,'','2024-04-04 14:10:33.377726','','2024-04-04 14:10:33.377739',30000,1,41,7,'metric',null); insert into quarter (id, label, start_date, end_date) values (8, 'GJ 23/24-Q3', '2024-01-01', '2024-03-31'); @@ -190,4 +200,18 @@ insert into completed (id, version, objective_id, comment) values (1, 1, 15, 'Not successful because there were many events this month'), (2, 1, 19, 'Was not successful because we were too slow'), (3, 1, 18, 'Sadly we had not enough members to complete this objective'), - (4, 1, 20, 'Objective could be completed fast and easy'); \ No newline at end of file + (4, 1, 20, 'Objective could be completed fast and easy'); + +insert into alignment(id, aligned_objective_id, alignment_type, target_key_result_id, target_objective_id, version) +values (1, 4, 'objective', null, 6, 0), + (2, 3, 'objective', null, 6, 0), + (3, 8, 'objective', null, 3, 0), + (4, 9, 'keyResult', 8, null, 0), + (5, 10, 'keyResult', 5, null, 0), + (6, 5, 'keyResult', 4, null, 0), + (7, 6, 'keyResult', 3, null, 0), +-- (8, 41, 'objective', null, 40, 0), + (9, 42, 'objective', null, 40, 0), + (10, 43, 'keyResult', 41, null, 0), + (11, 44, 'objective', null, 42, 0), + (12, 45, 'keyResult', 40, null, 0); diff --git a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql index 34aec66957..50741ef6b3 100644 --- a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql +++ b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql @@ -41,6 +41,9 @@ values (1, 'GJ 22/23-Q4', '2023-04-01', '2023-06-30'), (7, 'GJ 23/24-Q2', '2023-10-01', '2023-12-31'), (8, 'GJ 23/24-Q3', '2024-01-01', '2024-03-31'), (9, 'GJ 23/24-Q4', '2024-04-01', '2024-06-30'), + (10, 'GJ 24/25-Q1', '2024-07-01', '2024-09-30'), + (11, 'GJ 24/25-Q2', '2024-10-01', '2024-12-31'), + (199, 'Backlog', null, null); insert into team (id, version, name) @@ -71,7 +74,14 @@ values (4, 1, '', '2023-07-25 08:17:51.309958', 66, 'Build a company culture tha null, '2023-07-25 08:39:45.772126'), (8,1, '', '2023-07-25 08:39:28.175703', 40, 'consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua', - 1, 2, 6, 'ONGOING', null, '2023-07-25 08:39:28.175703'); + 1, 2, 6, 'ONGOING', null, '2023-07-25 08:39:28.175703'), + (40,1,'', '2024-04-04 13:45:13.000000',40,'Wir wollen eine gute Mitarbeiterzufriedenheit.', 1, 9, 5, 'ONGOING', null,'2024-04-04 13:44:52.000000'), + (41,1,'','2024-04-04 13:59:06.511620',40,'Das Projekt generiert 10000 CHF Umsatz',1,9,5,'ONGOING',null,'2024-04-04 13:59:06.523496'), + (42,1,'','2024-04-04 13:59:40.835896',40,'Die Lehrlinge sollen Freude haben',1,9,4,'ONGOING',null,'2024-04-04 13:59:40.848992'), + (43,1,'','2024-04-04 14:00:05.586152',40,'Der Firmenumsatz steigt',1,9,5,'ONGOING',null,'2024-04-04 14:00:05.588509'), + (44,1,'','2024-04-04 14:00:28.221906',40,'Die Members sollen gerne zur Arbeit kommen',1,9,6,'ONGOING',null,'2024-04-04 14:00:28.229058'), + (45,1,'','2024-04-04 14:00:47.659884',40,'Unsere Designer äussern sich zufrieden',1,9,8,'ONGOING',null,'2024-04-04 14:00:47.664414'), + (46,1,'','2024-04-04 14:00:57.485887',40,'Unsere Designer kommen gerne zur Arbeit',1,9,8,'ONGOING',null,'2024-04-04 14:00:57.494192'); insert into key_result (id, version, baseline, description, modified_on, stretch_goal, title, created_by_id, objective_id, owner_id, key_result_type, created_on, unit, commit_zone, target_zone, stretch_zone) @@ -90,7 +100,9 @@ values (10,1, 465, '', '2023-07-25 08:23:02.273028', 60, 'Im Durchschnitt soll (19,1, 50, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lore', '2023-07-25 08:42:56.407125', 1, 'nsetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At ', 1, 8, 1, 'metric', '2023-07-25 08:42:56.407125', 'PERCENT', null, null, null), (17,1, 525, 'asdf', '2023-07-25 08:41:52.844903', 20000000, 'vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lore', 1, 9, 1, 'metric', '2023-07-25 08:41:52.844903', 'PERCENT', null, null, null), (9,1, 100, '', '2023-07-25 08:48:45.825328', 80, 'Die Member des BBT reduzieren Ihre Lautstärke um 20%', 1, 5, 1, 'metric', '2023-07-25 08:48:45.825328', 'PERCENT', null, null, null), - (18,1, 0, 'consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lore', '2023-07-25 08:42:24.779721', 1, 'Lorem', 1, 8, 1, 'metric', '2023-07-25 08:42:24.779721', 'PERCENT', null, null, null); + (18,1, 0, 'consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lore', '2023-07-25 08:42:24.779721', 1, 'Lorem', 1, 8, 1, 'metric', '2023-07-25 08:42:24.779721', 'PERCENT', null, null, null), + (40,1,50,'',null,70,'60% sind in der Membersumfrage zufrienden',1,40,1,'metric','2024-04-04 14:06:21.689768','PERCENT',null,null,null), + (41,1,20000,'',null,80000,'Wir erreichen einen Umsatz von 70000 CHF',1,46,1,'metric','2024-04-04 14:06:42.100353','CHF',null,null,null); insert into check_in (id, version, change_info, created_on, initiatives, modified_on, value_metric, created_by_id, key_result_id, confidence, check_in_type, zone) values (1,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam', '2023-07-25 08:44:13.865976', '', '2023-07-24 22:00:00.000000', 77, 1, 8, 5, 'metric', null), @@ -111,11 +123,22 @@ values (1,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam (17,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-25 08:49:32.030171', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-24 22:00:00.000000', 66.7, 1, 16, 5, 'metric', null), (18,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-25 08:49:56.975649', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-24 22:00:00.000000', 99, 1, 15, 5, 'metric', null), (19,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-25 08:50:19.024254', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-24 22:00:00.000000', 35, 1, 19, 5, 'metric', null), - (20,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-25 08:50:44.059020', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-24 22:00:00.000000', 0.5, 1, 18, 5, 'metric', null); + (20,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-25 08:50:44.059020', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-24 22:00:00.000000', 0.5, 1, 18, 5, 'metric', null), + (40,1,'','2024-04-04 14:10:33.377726','','2024-04-04 14:10:33.377739',30000,1,41,7,'metric',null); -insert into alignment (id, version, aligned_objective_id, alignment_type, target_key_result_id, target_objective_id) values - (1,1, 4, 'objective', null, 3), - (2,1, 4, 'keyResult', 8, null); +insert into alignment (id, aligned_objective_id, alignment_type, target_key_result_id, target_objective_id, version) values + (1, 9, 'keyResult', 8, null, 1), + (2, 4, 'objective', null, 6, 0), + (3, 3, 'objective', null, 6, 0), + (4, 8, 'objective', null, 3, 0), + (5, 10, 'keyResult', 5, null, 0), + (6, 5, 'keyResult', 4, null, 0), + (7, 6, 'keyResult', 3, null, 0), + (8, 41, 'objective', null, 40, 0), + (9, 42, 'objective', null, 40, 0), + (10, 43, 'keyResult', 40, null, 0), + (11, 44, 'objective', null, 42, 0), + (12, 45, 'keyResult', 41, null, 0); insert into completed (id, version, objective_id, comment) values (1,1, 4, 'Das hat geklappt'), diff --git a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql index 51fbbfc187..acb9f76816 100644 --- a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql +++ b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql @@ -208,21 +208,6 @@ create table if not exists alignment foreign key (target_objective_id) references objective ); -DROP VIEW IF EXISTS ALIGNMENT_SELECTION; -CREATE VIEW ALIGNMENT_SELECTION AS -SELECT O.ID AS "OBJECTIVE_ID", - O.TITLE AS "OBJECTIVE_TITLE", - T.ID AS "TEAM_ID", - T.NAME AS "TEAM_NAME", - Q.ID AS "QUARTER_ID", - Q.LABEL AS "QUARTER_LABEL", - COALESCE(KR.ID, -1) AS "KEY_RESULT_ID", - KR.TITLE AS "KEY_RESULT_TITLE" -FROM OBJECTIVE O - LEFT JOIN TEAM T ON O.TEAM_ID = T.ID - LEFT JOIN QUARTER Q ON O.QUARTER_ID = Q.ID - LEFT JOIN KEY_RESULT KR ON O.ID = KR.OBJECTIVE_ID; - create table if not exists organisation ( id bigint not null, @@ -242,3 +227,56 @@ create table if not exists team_organisation constraint fk_team_organisation_team foreign key (team_id) references team ); + +DROP VIEW IF EXISTS ALIGNMENT_VIEW; +CREATE VIEW ALIGNMENT_VIEW AS +SELECT + CONCAT(OA.ID, COALESCE(A.TARGET_OBJECTIVE_ID, A.TARGET_KEY_RESULT_ID),'S','objective',A.ALIGNMENT_TYPE) AS UNIQUE_ID, + OA.ID AS ID, + OA.TITLE AS TITLE, + OTT.ID AS TEAM_ID, + OTT.NAME AS TEAM_NAME, + OA.QUARTER_ID AS QUARTER_ID, + OA.STATE AS STATE, + 'objective' AS OBJECT_TYPE, + 'source' AS CONNECTION_ROLE, + COALESCE(A.TARGET_OBJECTIVE_ID, A.TARGET_KEY_RESULT_ID) AS COUNTERPART_ID, + A.ALIGNMENT_TYPE AS COUNTERPART_TYPE +FROM ALIGNMENT A + LEFT JOIN OBJECTIVE OA ON OA.ID = A.ALIGNED_OBJECTIVE_ID + LEFT JOIN TEAM OTT ON OTT.ID = OA.TEAM_ID +UNION +SELECT + CONCAT(OT.ID, A.ALIGNED_OBJECTIVE_ID,'T','objective','objective') AS UNIQUE_ID, + OT.ID AS ID, + OT.TITLE AS TITLE, + OTT.ID AS TEAM_ID, + OTT.NAME AS TEAM_NAME, + OT.QUARTER_ID AS QUARTER_ID, + OT.STATE AS STATE, + 'objective' AS OBJECT_TYPE, + 'target' AS CONNECTION_ROLE, + A.ALIGNED_OBJECTIVE_ID AS COUNTERPART_ID, + 'objective' AS COUNTERPART_TYPE +FROM ALIGNMENT A + LEFT JOIN OBJECTIVE OT ON OT.ID = A.TARGET_OBJECTIVE_ID + LEFT JOIN TEAM OTT ON OTT.ID = OT.TEAM_ID +WHERE ALIGNMENT_TYPE = 'objective' +UNION +SELECT + CONCAT(KRT.ID, A.ALIGNED_OBJECTIVE_ID,'T','keyResult','keyResult') AS UNIQUE_ID, + KRT.ID AS ID, + KRT.TITLE AS TITLE, + OTT.ID AS TEAM_ID, + OTT.NAME AS TEAM_NAME, + O.QUARTER_ID AS QUARTER_ID, + NULL AS STATE, + 'keyResult' AS OBJECT_TYPE, + 'target' AS CONNECTION_ROLE, + A.ALIGNED_OBJECTIVE_ID AS COUNTERPART_ID, + 'objective' AS COUNTERPART_TYPE +FROM ALIGNMENT A + LEFT JOIN KEY_RESULT KRT ON KRT.ID = A.TARGET_KEY_RESULT_ID + LEFT JOIN OBJECTIVE O ON O.ID = KRT.OBJECTIVE_ID + LEFT JOIN TEAM OTT ON OTT.ID = O.TEAM_ID +WHERE ALIGNMENT_TYPE = 'keyResult'; \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V2_1_3__removeAlignmentSelection.sql b/backend/src/main/resources/db/migration/V2_1_3__removeAlignmentSelection.sql new file mode 100644 index 0000000000..751c03e89b --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_1_3__removeAlignmentSelection.sql @@ -0,0 +1 @@ +drop view if exists alignment_selection; \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V2_1_4__createAlignmentView.sql b/backend/src/main/resources/db/migration/V2_1_4__createAlignmentView.sql new file mode 100644 index 0000000000..2cced34040 --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_1_4__createAlignmentView.sql @@ -0,0 +1,56 @@ +DROP VIEW IF EXISTS alignment_view; +CREATE VIEW alignment_view AS +SELECT + concat(oa.id, coalesce(a.target_objective_id, a.target_key_result_id),'S','objective',a.alignment_type) as unique_id, + oa.id as id, + oa.title as title, + ott.id as team_id, + ott.name as team_name, + oa.quarter_id as quarter_id, + oa.state as state, + 'objective' as object_type, + 'source' as connection_role, + coalesce(a.target_objective_id, a.target_key_result_id) as counterpart_id, + a.alignment_type as counterpart_type +FROM alignment a + LEFT JOIN objective oa ON oa.id = a.aligned_objective_id + LEFT JOIN team ott ON ott.id = oa.team_id + +UNION + +SELECT + concat(ot.id, a.aligned_objective_id,'T','objective','objective') as unique_id, + ot.id as id, + ot.title as title, + ott.id as team_id, + ott.name as team_name, + ot.quarter_id as quarter_id, + ot.state as state, + 'objective' as object_type, + 'target' as connection_role, + a.aligned_objective_id as counterpart_id, + 'objective' as counterpart_type +FROM alignment a + LEFT JOIN objective ot ON ot.id = a.target_objective_id + LEFT JOIN team ott ON ott.id = ot.team_id +where alignment_type = 'objective' + +UNION + +SELECT + concat(krt.id, a.aligned_objective_id,'T','keyResult','keyResult') as unique_id, + krt.id as id, + krt.title as title, + ott.id as team_id, + ott.name as team_name, + o.quarter_id as quarter_id, + null as state, + 'keyResult' as object_type, + 'target' as connection_role, + a.aligned_objective_id as counterpart_id, + 'objective' as counterpart_type +FROM alignment a + LEFT JOIN key_result krt ON krt.id = a.target_key_result_id + LEFT JOIN objective o ON o.id = krt.objective_id + LEFT JOIN team ott ON ott.id = o.team_id +where alignment_type = 'keyResult'; \ No newline at end of file diff --git a/backend/src/test/java/ch/puzzle/okr/controller/AlignmentControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/AlignmentControllerIT.java index c0372930db..33dd701e76 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/AlignmentControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/AlignmentControllerIT.java @@ -1,9 +1,9 @@ package ch.puzzle.okr.controller; -import ch.puzzle.okr.mapper.AlignmentSelectionMapper; -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.models.alignment.AlignmentSelectionId; -import ch.puzzle.okr.service.business.AlignmentSelectionBusinessService; +import ch.puzzle.okr.dto.alignment.AlignmentConnectionDto; +import ch.puzzle.okr.dto.alignment.AlignmentLists; +import ch.puzzle.okr.dto.alignment.AlignmentObjectDto; +import ch.puzzle.okr.service.business.AlignmentBusinessService; import org.hamcrest.Matchers; import org.hamcrest.core.Is; import org.junit.jupiter.api.Test; @@ -13,17 +13,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import static org.mockito.ArgumentMatchers.any; +import static ch.puzzle.okr.TestConstants.TEAM_PUZZLE; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -34,64 +31,42 @@ class AlignmentControllerIT { @Autowired private MockMvc mvc; @MockBean - private AlignmentSelectionBusinessService alignmentSelectionBusinessService; - @SpyBean - private AlignmentSelectionMapper alignmentSelectionMapper; + private AlignmentBusinessService alignmentBusinessService; - static String alignmentObjectiveName = "Objective 5"; - static List alignmentSelectionPuzzle = List.of( - AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(1L, 20L)) - .withObjectiveTitle("Objective 1").withKeyResultTitle("KeyResult 20").build(), - AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(1L, 40L)) - .withObjectiveTitle("Objective 1").withKeyResultTitle("KeyResult 40").build()); - static List alignmentSelectionOKR = List.of( - AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(5L, 21L)) - .withObjectiveTitle(alignmentObjectiveName).withKeyResultTitle("KeyResult 21").build(), - AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(5L, 41L)) - .withObjectiveTitle(alignmentObjectiveName).withKeyResultTitle("KeyResult 41").build(), - AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(5L, 61L)) - .withObjectiveTitle(alignmentObjectiveName).withKeyResultTitle("KeyResult 61").build(), - AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(5L, 81L)) - .withObjectiveTitle(alignmentObjectiveName).withKeyResultTitle("KeyResult 81").build()); - static AlignmentSelection alignmentSelectionEmptyKeyResults = AlignmentSelection.Builder.builder() - .withAlignmentSelectionId(AlignmentSelectionId.of(8L, null)).withObjectiveTitle("Objective 8").build(); + private static final String OBJECTIVE = "objective"; + private static final String ONGOING = "ONGOING"; + static AlignmentObjectDto alignmentObjectDto1 = new AlignmentObjectDto(3L, "Title of first Objective", TEAM_PUZZLE, + ONGOING, OBJECTIVE); + static AlignmentObjectDto alignmentObjectDto2 = new AlignmentObjectDto(4L, "Title of second Objective", "BBT", + ONGOING, OBJECTIVE); + static AlignmentConnectionDto alignmentConnectionDto = new AlignmentConnectionDto(4L, 3L, null); - @Test - void shouldGetAllObjectivesWithKeyResults() throws Exception { - List alignmentSelections = new ArrayList<>(); - alignmentSelections.addAll(alignmentSelectionPuzzle); - alignmentSelections.addAll(alignmentSelectionOKR); - alignmentSelections.add(alignmentSelectionEmptyKeyResults); - BDDMockito.given(alignmentSelectionBusinessService.getAlignmentSelectionByQuarterIdAndTeamIdNot(2L, 4L)) - .willReturn(alignmentSelections); - - mvc.perform(get("/api/v2/alignments/selections?quarter=2&team=4").contentType(MediaType.APPLICATION_JSON)) - .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$", Matchers.hasSize(3))) - .andExpect(jsonPath("$[0].id", Is.is(1))).andExpect(jsonPath("$[0].keyResults[0].id", Is.is(20))) - .andExpect(jsonPath("$[0].keyResults[1].id", Is.is(40))).andExpect(jsonPath("$[1].id", Is.is(5))) - .andExpect(jsonPath("$[1].keyResults[0].id", Is.is(21))) - .andExpect(jsonPath("$[1].keyResults[1].id", Is.is(41))) - .andExpect(jsonPath("$[1].keyResults[2].id", Is.is(61))) - .andExpect(jsonPath("$[1].keyResults[3].id", Is.is(81))).andExpect(jsonPath("$[2].id", Is.is(8))) - .andExpect(jsonPath("$[2].keyResults.size()", Is.is(0))); - } + static AlignmentLists alignmentLists = new AlignmentLists(List.of(alignmentObjectDto1, alignmentObjectDto2), + List.of(alignmentConnectionDto)); @Test - void shouldGetAllObjectivesWithKeyResultsIfAllObjectivesFiltered() throws Exception { - BDDMockito.given(alignmentSelectionBusinessService.getAlignmentSelectionByQuarterIdAndTeamIdNot(any(), any())) - .willReturn(Collections.emptyList()); + void shouldReturnCorrectAlignmentData() throws Exception { + BDDMockito.given(alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 5L, 8L), "")) + .willReturn(alignmentLists); - mvc.perform(get("/api/v2/alignments/selections").contentType(MediaType.APPLICATION_JSON)) - .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$", Matchers.hasSize(0))); + mvc.perform(get("/api/v2/alignments/alignmentLists?quarterFilter=2&teamFilter=4,5,8") + .contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$.alignmentObjectDtoList", Matchers.hasSize(2))) + .andExpect(jsonPath("$.alignmentObjectDtoList[1].objectId", Is.is(4))) + .andExpect(jsonPath("$.alignmentConnectionDtoList[0].alignedObjectiveId", Is.is(4))) + .andExpect(jsonPath("$.alignmentConnectionDtoList[0].targetObjectiveId", Is.is(3))); } @Test - void shouldReturnObjectiveWithEmptyKeyResultListWhenNoKeyResultsInFilteredQuarter() throws Exception { - BDDMockito.given(alignmentSelectionBusinessService.getAlignmentSelectionByQuarterIdAndTeamIdNot(2L, 4L)) - .willReturn(List.of(alignmentSelectionEmptyKeyResults)); + void shouldReturnCorrectAlignmentDataWithObjectiveSearch() throws Exception { + BDDMockito.given(alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 5L, 8L), "secon")) + .willReturn(alignmentLists); - mvc.perform(get("/api/v2/alignments/selections?quarter=2&team=4").contentType(MediaType.APPLICATION_JSON)) - .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$", Matchers.hasSize(1))) - .andExpect(jsonPath("$[0].id", Is.is(8))).andExpect(jsonPath("$[0].keyResults.size()", Is.is(0))); + mvc.perform(get("/api/v2/alignments/alignmentLists?quarterFilter=2&teamFilter=4,5,8&objectiveQuery=secon") + .contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$.alignmentObjectDtoList", Matchers.hasSize(2))) + .andExpect(jsonPath("$.alignmentObjectDtoList[1].objectId", Is.is(4))) + .andExpect(jsonPath("$.alignmentConnectionDtoList[0].alignedObjectiveId", Is.is(4))) + .andExpect(jsonPath("$.alignmentConnectionDtoList[0].targetObjectiveId", Is.is(3))); } } diff --git a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java index f4fbc5d68a..f7c733f9a0 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java @@ -1,6 +1,9 @@ package ch.puzzle.okr.controller; +import ch.puzzle.okr.dto.AlignmentDto; +import ch.puzzle.okr.dto.AlignmentObjectDto; import ch.puzzle.okr.dto.ObjectiveDto; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import ch.puzzle.okr.mapper.ObjectiveMapper; import ch.puzzle.okr.models.*; import ch.puzzle.okr.service.authorization.AuthorizationService; @@ -23,14 +26,15 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.web.server.ResponseStatusException; -import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static ch.puzzle.okr.TestConstants.*; @WithMockUser(value = "spring") @ExtendWith(MockitoExtension.class) @@ -38,12 +42,13 @@ class ObjectiveControllerIT { private static final String OBJECTIVE_TITLE_1 = "Objective 1"; private static final String OBJECTIVE_TITLE_2 = "Objective 2"; + private static final String OBJECTIVE = "objective"; private static final String DESCRIPTION = "This is our description"; private static final String EVERYTHING_FINE_DESCRIPTION = "Everything Fine"; private static final String TITLE = "Hunting"; private static final String URL_BASE_OBJECTIVE = "/api/v2/objectives"; - private static final String URL_OBJECTIVE_5 = "/api/v2/objectives/5"; - private static final String URL_OBJECTIVE_10 = "/api/v2/objectives/10"; + private static final String URL_OBJECTIVE_5 = URL_BASE_OBJECTIVE + "/5"; + private static final String URL_OBJECTIVE_10 = URL_BASE_OBJECTIVE + "/10"; private static final String JSON = """ { "title": "FullObjective", "ownerId": 1, "ownerFirstname": "Bob", "ownerLastname": "Kaufmann", @@ -64,11 +69,14 @@ class ObjectiveControllerIT { "description": "This is our description", "progress": 33.3 } """; - private static final String RESPONSE_NEW_OBJECTIVE = """ - {"id":null,"version":1,"title":"Program Faster","teamId":1,"quarterId":1,"quarterLabel":"GJ 22/23-Q2","description":"Just be faster","state":"DRAFT","createdOn":null,"modifiedOn":null,"writeable":true}"""; + private static final String JSON_RESPONSE_NEW_OBJECTIVE = """ + {"id":null,"version":1,"title":"Program Faster","teamId":1,"quarterId":1,"quarterLabel":"GJ 22/23-Q2","description":"Just be faster","state":"DRAFT","createdOn":null,"modifiedOn":null,"writeable":true,"alignedEntity":null}"""; private static final String JSON_PATH_TITLE = "$.title"; + private static final AlignedEntityDto alignedEntityDtoObjective = new AlignedEntityDto(42L, OBJECTIVE); private static final Objective objective1 = Objective.Builder.builder().withId(5L).withTitle(OBJECTIVE_TITLE_1) .build(); + private static final Objective objectiveAlignment = Objective.Builder.builder().withId(9L) + .withTitle("Objective Alignment").withAlignedEntity(alignedEntityDtoObjective).build(); private static final Objective objective2 = Objective.Builder.builder().withId(7L).withTitle(OBJECTIVE_TITLE_2) .build(); private static final User user = User.Builder.builder().withId(1L).withFirstname("Bob").withLastname("Kaufmann") @@ -79,9 +87,17 @@ class ObjectiveControllerIT { .withCreatedBy(user).withTeam(team).withQuarter(quarter).withDescription(DESCRIPTION) .withModifiedOn(LocalDateTime.MAX).build(); private static final ObjectiveDto objective1Dto = new ObjectiveDto(5L, 1, OBJECTIVE_TITLE_1, 1L, 1L, "GJ 22/23-Q2", - DESCRIPTION, State.DRAFT, LocalDateTime.MAX, LocalDateTime.MAX, true); + DESCRIPTION, State.DRAFT, LocalDateTime.MAX, LocalDateTime.MAX, true, null); private static final ObjectiveDto objective2Dto = new ObjectiveDto(7L, 1, OBJECTIVE_TITLE_2, 1L, 1L, "GJ 22/23-Q2", - DESCRIPTION, State.DRAFT, LocalDateTime.MIN, LocalDateTime.MIN, true); + DESCRIPTION, State.DRAFT, LocalDateTime.MIN, LocalDateTime.MIN, true, new AlignedEntityDto(5L, OBJECTIVE)); + private static final ObjectiveDto objectiveAlignmentDto = new ObjectiveDto(9L, 1, "Objective Alignment", 1L, 1L, + "GJ 22/23-Q2", DESCRIPTION, State.DRAFT, LocalDateTime.MAX, LocalDateTime.MAX, true, + alignedEntityDtoObjective); + private static final AlignmentObjectDto alignmentObject1 = new AlignmentObjectDto(3L, "KR Title 1", "keyResult"); + private static final AlignmentObjectDto alignmentObject2 = new AlignmentObjectDto(1L, "Objective Title 1", + OBJECTIVE); + private static final AlignmentDto alignmentPossibilities = new AlignmentDto(1L, TEAM_PUZZLE, + List.of(alignmentObject1, alignmentObject2)); @Autowired private MockMvc mvc; @@ -96,6 +112,7 @@ class ObjectiveControllerIT { void setUp() { BDDMockito.given(objectiveMapper.toDto(objective1)).willReturn(objective1Dto); BDDMockito.given(objectiveMapper.toDto(objective2)).willReturn(objective2Dto); + BDDMockito.given(objectiveMapper.toDto(objectiveAlignment)).willReturn(objectiveAlignmentDto); } @Test @@ -107,6 +124,17 @@ void getObjectiveById() throws Exception { .andExpect(jsonPath(JSON_PATH_TITLE, Is.is(OBJECTIVE_TITLE_1))); } + @Test + void getObjectiveByIdWithAlignmentId() throws Exception { + BDDMockito.given(objectiveAuthorizationService.getEntityById(anyLong())).willReturn(objectiveAlignment); + + mvc.perform(get(URL_BASE_OBJECTIVE + "/9").contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$.id", Is.is(9))) + .andExpect(jsonPath(JSON_PATH_TITLE, Is.is("Objective Alignment"))) + .andExpect(jsonPath("$.alignedEntity.id", Is.is(42))) + .andExpect(jsonPath("$.alignedEntity.type", Is.is(OBJECTIVE))); + } + @Test void getObjectiveByIdFail() throws Exception { BDDMockito.given(objectiveAuthorizationService.getEntityById(anyLong())) @@ -116,10 +144,28 @@ void getObjectiveByIdFail() throws Exception { .andExpect(MockMvcResultMatchers.status().isNotFound()); } + @Test + void getAlignmentPossibilities() throws Exception { + BDDMockito.given(objectiveAuthorizationService.getAlignmentPossibilities(anyLong())) + .willReturn(List.of(alignmentPossibilities)); + + mvc.perform(get(URL_BASE_OBJECTIVE + "/alignmentPossibilities/5").contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$[0].teamId", Is.is(1))) + .andExpect(jsonPath("$[0].teamName", Is.is(TEAM_PUZZLE))) + .andExpect(jsonPath("$[0].alignmentObjects[0].objectId", Is.is(3))) + .andExpect(jsonPath("$[0].alignmentObjects[0].objectTitle", Is.is("KR Title 1"))) + .andExpect(jsonPath("$[0].alignmentObjects[0].objectType", Is.is("keyResult"))) + .andExpect(jsonPath("$[0].alignmentObjects[1].objectId", Is.is(1))) + .andExpect(jsonPath("$[0].alignmentObjects[1].objectTitle", Is.is("Objective Title 1"))) + .andExpect(jsonPath("$[0].alignmentObjects[1].objectType", Is.is(OBJECTIVE))); + + verify(objectiveAuthorizationService, times(1)).getAlignmentPossibilities(5L); + } + @Test void shouldReturnObjectiveWhenCreatingNewObjective() throws Exception { ObjectiveDto testObjective = new ObjectiveDto(null, 1, "Program Faster", 1L, 1L, "GJ 22/23-Q2", - "Just be faster", State.DRAFT, null, null, true); + "Just be faster", State.DRAFT, null, null, true, null); BDDMockito.given(objectiveMapper.toDto(any())).willReturn(testObjective); BDDMockito.given(objectiveAuthorizationService.createEntity(any())).willReturn(fullObjective); @@ -127,7 +173,7 @@ void shouldReturnObjectiveWhenCreatingNewObjective() throws Exception { mvc.perform(post(URL_BASE_OBJECTIVE).contentType(MediaType.APPLICATION_JSON) .with(SecurityMockMvcRequestPostProcessors.csrf()).content(CREATE_NEW_OBJECTIVE)) .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) - .andExpect(MockMvcResultMatchers.content().string(RESPONSE_NEW_OBJECTIVE)); + .andExpect(MockMvcResultMatchers.content().string(JSON_RESPONSE_NEW_OBJECTIVE)); verify(objectiveAuthorizationService, times(1)).createEntity(any()); } @@ -144,7 +190,7 @@ void shouldReturnResponseStatusExceptionWhenCreatingObjectiveWithNullValues() th @Test void shouldReturnUpdatedObjective() throws Exception { ObjectiveDto testObjective = new ObjectiveDto(1L, 1, TITLE, 1L, 1L, "GJ 22/23-Q2", EVERYTHING_FINE_DESCRIPTION, - State.NOTSUCCESSFUL, LocalDateTime.MIN, LocalDateTime.MAX, true); + State.NOTSUCCESSFUL, LocalDateTime.MIN, LocalDateTime.MAX, true, null); Objective objective = Objective.Builder.builder().withId(1L).withDescription(EVERYTHING_FINE_DESCRIPTION) .withTitle(TITLE).build(); @@ -162,7 +208,7 @@ void shouldReturnUpdatedObjective() throws Exception { @Test void shouldReturnImUsed() throws Exception { ObjectiveDto testObjectiveDto = new ObjectiveDto(1L, 1, TITLE, 1L, 1L, "GJ 22/23-Q2", - EVERYTHING_FINE_DESCRIPTION, State.SUCCESSFUL, LocalDateTime.MAX, LocalDateTime.MAX, true); + EVERYTHING_FINE_DESCRIPTION, State.SUCCESSFUL, LocalDateTime.MAX, LocalDateTime.MAX, true, null); Objective objectiveImUsed = Objective.Builder.builder().withId(1L).withDescription(EVERYTHING_FINE_DESCRIPTION) .withQuarter(Quarter.Builder.builder().withId(1L).withLabel("GJ 22/23-Q2").build()).withTitle(TITLE) .build(); @@ -207,7 +253,7 @@ void throwExceptionWhenObjectiveWithIdCantBeFoundWhileDeleting() throws Exceptio doThrow(new ResponseStatusException(HttpStatus.NOT_FOUND, "Objective not found")) .when(objectiveAuthorizationService).deleteEntityById(anyLong()); - mvc.perform(delete("/api/v2/objectives/1000").with(SecurityMockMvcRequestPostProcessors.csrf())) + mvc.perform(delete(URL_BASE_OBJECTIVE + "/1000").with(SecurityMockMvcRequestPostProcessors.csrf())) .andExpect(MockMvcResultMatchers.status().isNotFound()); } @@ -217,7 +263,7 @@ void shouldReturnIsCreatedWhenObjectiveWasDuplicated() throws Exception { BDDMockito.given(objectiveAuthorizationService.getAuthorizationService()).willReturn(authorizationService); BDDMockito.given(objectiveMapper.toDto(objective1)).willReturn(objective1Dto); - mvc.perform(post("/api/v2/objectives/{id}", objective1.getId()).contentType(MediaType.APPLICATION_JSON) + mvc.perform(post(URL_BASE_OBJECTIVE + "/{id}", objective1.getId()).contentType(MediaType.APPLICATION_JSON) .content(JSON).with(SecurityMockMvcRequestPostProcessors.csrf())) .andExpect(MockMvcResultMatchers.status().isCreated()) .andExpect(jsonPath("$.id", Is.is(objective1Dto.id().intValue()))) diff --git a/backend/src/test/java/ch/puzzle/okr/controller/OrganisationControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/OrganisationControllerIT.java index 055dcec6dc..d6b663fe39 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/OrganisationControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/OrganisationControllerIT.java @@ -24,6 +24,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static ch.puzzle.okr.TestConstants.*; @WithMockUser(value = "spring") @ExtendWith(MockitoExtension.class) @@ -31,7 +32,7 @@ class OrganisationControllerIT { /* Team test objects */ - private static final Team PUZZLE = Team.Builder.builder().withId(1L).withName("PUZZLE ITC").build(); + private static final Team PUZZLE = Team.Builder.builder().withId(1L).withName(TEAM_PUZZLE).build(); private static final Team BBT = Team.Builder.builder().withId(1L).withName("/BBT").build(); /* Organisation test objects */ diff --git a/backend/src/test/java/ch/puzzle/okr/mapper/AlignmentSelectionMapperTest.java b/backend/src/test/java/ch/puzzle/okr/mapper/AlignmentSelectionMapperTest.java deleted file mode 100644 index 26b799ff15..0000000000 --- a/backend/src/test/java/ch/puzzle/okr/mapper/AlignmentSelectionMapperTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package ch.puzzle.okr.mapper; - -import ch.puzzle.okr.dto.alignment.AlignmentObjectiveDto; -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.models.alignment.AlignmentSelectionId; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class AlignmentSelectionMapperTest { - private final AlignmentSelectionMapper alignmentSelectionMapper = new AlignmentSelectionMapper(); - - @Test - void toDtoShouldReturnEmptyListWhenNoObjectiveFound() { - List alignmentSelections = List.of(); - List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); - - assertTrue(alignmentObjectiveDtos.isEmpty()); - } - - @Test - void toDtoShouldReturnOneElementWhenObjectiveFound() { - List alignmentSelections = List.of(AlignmentSelection.Builder.builder() - .withAlignmentSelectionId(AlignmentSelectionId.Builder.builder().withObjectiveId(1L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").build()); - List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); - - assertEquals(1, alignmentObjectiveDtos.size()); - assertEquals(0, alignmentObjectiveDtos.get(0).keyResults().size()); - } - - @Test - void toDtoShouldReturnOneElementWhenObjectiveWithKeyResultFound() { - List alignmentSelections = List.of(AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(3L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1") - .withKeyResultTitle("Key Result 3").build()); - List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); - - assertEquals(1, alignmentObjectiveDtos.size()); - assertEquals(1, alignmentObjectiveDtos.get(0).keyResults().size()); - } - - @Test - void toDtoShouldReturnOneElementWhenObjectiveWithTwoKeyResultsFound() { - List alignmentSelections = List.of( - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(3L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1") - .withKeyResultTitle("Key Result 3").build(), - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(5L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1") - .withKeyResultTitle("Key Result 5").build()); - List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); - - assertEquals(1, alignmentObjectiveDtos.size()); - assertEquals(2, alignmentObjectiveDtos.get(0).keyResults().size()); - } - - @Test - void toDtoShouldReturnOneElementWhenTwoObjectivesWithKeyResultsFound() { - List alignmentSelections = List.of( - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(3L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1") - .withKeyResultTitle("Key Result 3").build(), - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(5L).withKeyResultId(6L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 5") - .withKeyResultTitle("Key Result 6").build(), - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(9L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1") - .withKeyResultTitle("Key Result 9").build()); - List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); - - assertEquals(2, alignmentObjectiveDtos.size()); - assertEquals(2, alignmentObjectiveDtos.get(0).keyResults().size()); - assertEquals(1, alignmentObjectiveDtos.get(1).keyResults().size()); - } -} diff --git a/backend/src/test/java/ch/puzzle/okr/mapper/OverviewMapperTest.java b/backend/src/test/java/ch/puzzle/okr/mapper/OverviewMapperTest.java index 5e4c51ad9b..e17fe68a1d 100644 --- a/backend/src/test/java/ch/puzzle/okr/mapper/OverviewMapperTest.java +++ b/backend/src/test/java/ch/puzzle/okr/mapper/OverviewMapperTest.java @@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static ch.puzzle.okr.TestConstants.*; @ExtendWith(MockitoExtension.class) class OverviewMapperTest { @@ -42,9 +43,8 @@ void toDtoShouldReturnEmptyListWhenNoTeamFound() { @Test void toDtoShouldReturnEmptyListWhenTeamFound() { - List overviews = List - .of(Overview.Builder.builder().withOverviewId(OverviewId.Builder.builder().withTeamId(2L).build()) - .withTeamName("Puzzle ITC").build()); + List overviews = List.of(Overview.Builder.builder() + .withOverviewId(OverviewId.Builder.builder().withTeamId(2L).build()).withTeamName(TEAM_PUZZLE).build()); List overviewDtos = overviewMapper.toDto(overviews); assertEquals(1, overviewDtos.size()); @@ -55,7 +55,7 @@ void toDtoShouldReturnEmptyListWhenTeamFound() { void toDtoShouldReturnOneElementWhenObjectiveFound() { List overviews = List.of(Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").build()); + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").build()); List overviewDtos = overviewMapper.toDto(overviews); assertEquals(1, overviewDtos.size()); @@ -68,7 +68,7 @@ void toDtoShouldReturnOneElementWhenObjectiveWithKeyResultFound() { List overviews = List.of(Overview.Builder.builder() .withOverviewId( OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L).withKeyResultId(3L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") .withKeyResultType(KEY_RESULT_TYPE_METRIC).build()); List overviewDtos = overviewMapper.toDto(overviews); @@ -82,7 +82,7 @@ void toDtoShouldReturnOneElementWhenObjectiveWithKeyResultAndCheckInsFound() { List overviews = List.of(Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L).withKeyResultId(3L) .withCheckInId(4L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") .withKeyResultType(KEY_RESULT_TYPE_METRIC).withCheckInValue(27.5).withConfidence(5).build()); List overviewDtos = overviewMapper.toDto(overviews); @@ -97,12 +97,12 @@ void toDtoShouldReturnOneElementWhenObjectiveWithTwoKeyResultAndCheckInFound() { Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L) .withKeyResultId(3L).withCheckInId(4L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") .withKeyResultType(KEY_RESULT_TYPE_ORDINAL).withCheckInZone("COMMIT").build(), Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L) .withKeyResultId(5L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 5") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 5") .withKeyResultType(KEY_RESULT_TYPE_METRIC).build()); List overviewDtos = overviewMapper.toDto(overviews); @@ -117,13 +117,13 @@ void toDtoShouldReturnOneElementWhenTwoObjectivesWithKeyResultAndCheckInFound() Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L) .withKeyResultId(3L).withCheckInId(4L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") .withKeyResultType(KEY_RESULT_TYPE_METRIC).withBaseline(20.0).withStretchGoal(37.0) .withUnit("TCHF").withCheckInValue(27.5).build(), Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(5L).withTeamId(2L) .withKeyResultId(6L).withCheckInId(7L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 5").withKeyResultTitle("Key Result 6") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 5").withKeyResultTitle("Key Result 6") .withKeyResultType(KEY_RESULT_TYPE_ORDINAL).withCommitZone("commit").withTargetZone("target") .withStretchZone("stretch").withCheckInZone("checkIn").build()); List overviewDtos = overviewMapper.toDto(overviews); @@ -155,7 +155,7 @@ void toDtoShouldReturnOneElementWhenTwoTeamsWithObjectivesAndKeyResultsFound() { Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L) .withKeyResultId(3L).withCheckInId(4L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") .withKeyResultType(KEY_RESULT_TYPE_ORDINAL).withCheckInZone("TARGET").build(), Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(5L).withTeamId(4L) @@ -181,7 +181,7 @@ void toDtoShouldThrowExceptionWhenKeyResultTypeNotSupported() { List overviews = List.of(Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L).withKeyResultId(3L) .withCheckInId(4L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") .withKeyResultType("unknown").withCheckInZone("TARGET").build()); OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java index 26f452d0a5..6aa9a1ad42 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java @@ -1,5 +1,7 @@ package ch.puzzle.okr.service.authorization; +import ch.puzzle.okr.dto.AlignmentDto; +import ch.puzzle.okr.dto.AlignmentObjectDto; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.service.business.ObjectiveBusinessService; @@ -11,11 +13,15 @@ import org.springframework.http.HttpStatus; import org.springframework.web.server.ResponseStatusException; +import java.util.List; + import static ch.puzzle.okr.TestHelper.defaultAuthorizationUser; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import static ch.puzzle.okr.TestConstants.*; @ExtendWith(MockitoExtension.class) class ObjectiveAuthorizationServiceTest { @@ -25,87 +31,122 @@ class ObjectiveAuthorizationServiceTest { ObjectiveBusinessService objectiveBusinessService; @Mock AuthorizationService authorizationService; - private final AuthorizationUser authorizationUser = defaultAuthorizationUser(); + private static final String JUNIT_TEST_REASON = "junit test reason"; + + private final AuthorizationUser authorizationUser = defaultAuthorizationUser(); private final Objective newObjective = Objective.Builder.builder().withId(5L).withTitle("Objective 1").build(); + private static final AlignmentObjectDto alignmentObject1 = new AlignmentObjectDto(3L, "KR Title 1", "keyResult"); + private static final AlignmentObjectDto alignmentObject2 = new AlignmentObjectDto(1L, "Objective Title 1", + "objective"); + private static final AlignmentDto alignmentPossibilities = new AlignmentDto(1L, TEAM_PUZZLE, + List.of(alignmentObject1, alignmentObject2)); @Test void createEntityShouldReturnObjectiveWhenAuthorized() { + // arrange when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); when(objectiveBusinessService.createEntity(newObjective, authorizationUser)).thenReturn(newObjective); + // act Objective objective = objectiveAuthorizationService.createEntity(newObjective); + + // assert assertEquals(newObjective, objective); } @Test void createEntityShouldThrowExceptionWhenNotAuthorized() { - String reason = "junit test reason"; + // arrange + String reason = JUNIT_TEST_REASON; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); doThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED, reason)).when(authorizationService) .hasRoleCreateOrUpdate(newObjective, authorizationUser); + // act ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> objectiveAuthorizationService.createEntity(newObjective)); + + // assert assertEquals(UNAUTHORIZED, exception.getStatusCode()); assertEquals(reason, exception.getReason()); } @Test void getEntityByIdShouldReturnObjectiveWhenAuthorized() { + // arrange Long id = 13L; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); when(objectiveBusinessService.getEntityById(id)).thenReturn(newObjective); + // act Objective objective = objectiveAuthorizationService.getEntityById(id); + + // assert assertEquals(newObjective, objective); } @Test void getEntityByIdShouldReturnObjectiveWritableWhenAuthorized() { + // arrange Long id = 13L; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); when(authorizationService.isWriteable(newObjective, authorizationUser)).thenReturn(true); when(objectiveBusinessService.getEntityById(id)).thenReturn(newObjective); + // act Objective objective = objectiveAuthorizationService.getEntityById(id); + + // assert assertTrue(objective.isWriteable()); } @Test void getEntityByIdShouldThrowExceptionWhenNotAuthorized() { + // arrange Long id = 13L; - String reason = "junit test reason"; + String reason = JUNIT_TEST_REASON; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); doThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED, reason)).when(authorizationService) .hasRoleReadByObjectiveId(id, authorizationUser); + // act ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> objectiveAuthorizationService.getEntityById(id)); + + // assert assertEquals(UNAUTHORIZED, exception.getStatusCode()); assertEquals(reason, exception.getReason()); } @Test void updateEntityShouldReturnUpdatedObjectiveWhenAuthorized() { + // arrange Long id = 13L; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); when(objectiveBusinessService.updateEntity(id, newObjective, authorizationUser)).thenReturn(newObjective); + // act Objective Objective = objectiveAuthorizationService.updateEntity(id, newObjective); + + // assert assertEquals(newObjective, Objective); } @Test void updateEntityShouldThrowExceptionWhenNotAuthorized() { + // arrange Long id = 13L; - String reason = "junit test reason"; + String reason = JUNIT_TEST_REASON; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); doThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED, reason)).when(authorizationService) .hasRoleCreateOrUpdate(newObjective, authorizationUser); + // act ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> objectiveAuthorizationService.updateEntity(id, newObjective)); + + // assert assertEquals(UNAUTHORIZED, exception.getStatusCode()); assertEquals(reason, exception.getReason()); } @@ -127,15 +168,49 @@ void deleteEntityByIdShouldPassThroughWhenAuthorized() { @Test void deleteEntityByIdShouldThrowExceptionWhenNotAuthorized() { + // arrange Long id = 13L; - String reason = "junit test reason"; + String reason = JUNIT_TEST_REASON; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); doThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED, reason)).when(authorizationService) .hasRoleDeleteByObjectiveId(id, authorizationUser); + // act ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> objectiveAuthorizationService.deleteEntityById(id)); + + // assert assertEquals(UNAUTHORIZED, exception.getStatusCode()); assertEquals(reason, exception.getReason()); } + + @Test + void getAlignmentPossibilitiesShouldReturnListWhenAuthorized() { + // arrange + when(objectiveBusinessService.getAlignmentPossibilities(anyLong())).thenReturn(List.of(alignmentPossibilities)); + + // act + List alignmentPossibilities = objectiveAuthorizationService.getAlignmentPossibilities(3L); + + // assert + assertEquals(TEAM_PUZZLE, alignmentPossibilities.get(0).teamName()); + assertEquals(1, alignmentPossibilities.get(0).teamId()); + assertEquals(3, alignmentPossibilities.get(0).alignmentObjects().get(0).objectId()); + assertEquals("KR Title 1", alignmentPossibilities.get(0).alignmentObjects().get(0).objectTitle()); + assertEquals("keyResult", alignmentPossibilities.get(0).alignmentObjects().get(0).objectType()); + assertEquals(1, alignmentPossibilities.get(0).alignmentObjects().get(1).objectId()); + assertEquals("objective", alignmentPossibilities.get(0).alignmentObjects().get(1).objectType()); + } + + @Test + void getAlignmentPossibilitiesShouldReturnEmptyListWhenNoAlignments() { + // arrange + when(objectiveBusinessService.getAlignmentPossibilities(anyLong())).thenReturn(List.of()); + + // act + List alignmentPossibilities = objectiveAuthorizationService.getAlignmentPossibilities(3L); + + // assert + assertEquals(0, alignmentPossibilities.size()); + } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java new file mode 100644 index 0000000000..e4b9bf7e79 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java @@ -0,0 +1,133 @@ +package ch.puzzle.okr.service.business; + +import ch.puzzle.okr.dto.alignment.AlignmentLists; +import ch.puzzle.okr.test.SpringIntegrationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@SpringIntegrationTest +class AlignmentBusinessServiceIT { + @Autowired + private AlignmentBusinessService alignmentBusinessService; + + private final String OBJECTIVE = "objective"; + + @Test + void shouldReturnCorrectAlignmentData() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(9L, List.of(5L, 6L), ""); + + assertEquals(6, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(4, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals("keyResult", alignmentLists.alignmentObjectDtoList().get(1).objectType()); + assertEquals(41L, alignmentLists.alignmentObjectDtoList().get(2).objectId()); + assertEquals(43L, alignmentLists.alignmentObjectDtoList().get(3).objectId()); + assertEquals(44L, alignmentLists.alignmentObjectDtoList().get(4).objectId()); + assertEquals(42L, alignmentLists.alignmentObjectDtoList().get(5).objectId()); + assertEquals(41L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertEquals(43L, alignmentLists.alignmentConnectionDtoList().get(1).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(1).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(1).targetObjectiveId()); + assertEquals(44L, alignmentLists.alignmentConnectionDtoList().get(2).alignedObjectiveId()); + assertEquals(42L, alignmentLists.alignmentConnectionDtoList().get(2).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(2).targetKeyResultId()); + assertEquals(42L, alignmentLists.alignmentConnectionDtoList().get(3).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(3).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(3).targetKeyResultId()); + } + + @Test + void shouldReturnCorrectAlignmentDataWhenLimitedTeamMatching() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(9L, List.of(6L), ""); + + assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(44L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(42L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(1).objectType()); + assertEquals(44L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(42L, alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + } + + @Test + void shouldReturnCorrectAlignmentDataWithObjectiveSearch() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(9L, List.of(4L, 5L, 6L, 8L), + "lehrling"); + + assertEquals(3, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(2, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(42L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(1).objectType()); + assertEquals(44L, alignmentLists.alignmentObjectDtoList().get(2).objectId()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(2).objectType()); + assertEquals(42L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertEquals(44L, alignmentLists.alignmentConnectionDtoList().get(1).alignedObjectiveId()); + assertEquals(42L, alignmentLists.alignmentConnectionDtoList().get(1).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(1).targetKeyResultId()); + } + + @Test + void shouldReturnCorrectAlignmentDataWithKeyResultWhenMatchingObjectiveSearch() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(9L, List.of(4L, 5L, 6L, 8L), + "firmenums"); + + assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(43L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals("keyResult", alignmentLists.alignmentObjectDtoList().get(1).objectType()); + assertEquals(43L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoAlignments() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(3L, List.of(5L, 6L), ""); + + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoMatchingObjectiveSearch() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(9L, List.of(4L, 5L, 6L, 8L), + "spass"); + + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + } + + @Test + void shouldReturnCorrectAlignmentDataWhenEmptyQuarterFilter() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(null, + List.of(4L, 5L, 6L, 8L), ""); + + assertEquals(8, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(5, alignmentLists.alignmentConnectionDtoList().size()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenEmptyTeamFilter() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, null, ""); + + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + } +} diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java new file mode 100644 index 0000000000..7e81b66335 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -0,0 +1,595 @@ +package ch.puzzle.okr.service.business; + +import ch.puzzle.okr.TestHelper; +import ch.puzzle.okr.dto.ErrorDto; +import ch.puzzle.okr.dto.alignment.AlignmentLists; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; +import ch.puzzle.okr.exception.OkrResponseStatusException; +import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.Quarter; +import ch.puzzle.okr.models.alignment.Alignment; +import ch.puzzle.okr.models.alignment.AlignmentView; +import ch.puzzle.okr.models.alignment.KeyResultAlignment; +import ch.puzzle.okr.models.alignment.ObjectiveAlignment; +import ch.puzzle.okr.models.keyresult.KeyResult; +import ch.puzzle.okr.models.keyresult.KeyResultMetric; +import ch.puzzle.okr.service.persistence.AlignmentPersistenceService; +import ch.puzzle.okr.service.persistence.AlignmentViewPersistenceService; +import ch.puzzle.okr.service.persistence.KeyResultPersistenceService; +import ch.puzzle.okr.service.persistence.ObjectivePersistenceService; +import ch.puzzle.okr.service.validation.AlignmentValidationService; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static ch.puzzle.okr.models.State.DRAFT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +@ExtendWith(MockitoExtension.class) +class AlignmentBusinessServiceTest { + @Mock + ObjectivePersistenceService objectivePersistenceService; + @Mock + KeyResultPersistenceService keyResultPersistenceService; + @Mock + AlignmentPersistenceService alignmentPersistenceService; + @Mock + AlignmentViewPersistenceService alignmentViewPersistenceService; + @Mock + QuarterBusinessService quarterBusinessService; + @Mock + AlignmentValidationService validator; + @InjectMocks + private AlignmentBusinessService alignmentBusinessService; + + Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withState(DRAFT).build(); + Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withState(DRAFT).build(); + Objective objective3 = Objective.Builder.builder().withId(10L).withTitle("Objective 3").withState(DRAFT).build(); + AlignedEntityDto alignedEntityDtoObjective = new AlignedEntityDto(8L, "objective"); + AlignedEntityDto alignedEntityDtoKeyResult = new AlignedEntityDto(5L, "keyResult"); + Objective objectiveAlignedObjective = Objective.Builder.builder().withId(42L).withTitle("Objective 42") + .withState(DRAFT).withAlignedEntity(alignedEntityDtoObjective).build(); + Objective keyResultAlignedObjective = Objective.Builder.builder().withId(45L).withTitle("Objective 45") + .withState(DRAFT).withAlignedEntity(alignedEntityDtoKeyResult).build(); + Objective wrongAlignedObjective = Objective.Builder.builder().withId(48L).withTitle("Objective 48").withState(DRAFT) + .withAlignedEntity(new AlignedEntityDto(0L, "Hello")).build(); + KeyResult metricKeyResult = KeyResultMetric.Builder.builder().withId(5L).withTitle("KR Title 1").build(); + ObjectiveAlignment objectiveALignment = ObjectiveAlignment.Builder.builder().withId(1L) + .withAlignedObjective(objective1).withTargetObjective(objective2).build(); + ObjectiveAlignment objectiveAlignment2 = ObjectiveAlignment.Builder.builder().withId(2L) + .withAlignedObjective(objective2).withTargetObjective(objective1).build(); + KeyResultAlignment keyResultAlignment = KeyResultAlignment.Builder.builder().withId(6L) + .withAlignedObjective(objective3).withTargetKeyResult(metricKeyResult).build(); + Quarter quarter = Quarter.Builder.builder().withId(2L).withLabel("GJ 23/24-Q1").build(); + + AlignmentView alignmentView1 = AlignmentView.Builder.builder().withUniqueId("45TkeyResultkeyResult").withId(4L) + .withTitle("Antwortzeit für Supportanfragen um 33% verkürzen.").withTeamId(5L).withTeamName("Puzzle ITC") + .withQuarterId(2L).withObjectType("keyResult").withConnectionRole("target").withCounterpartId(5L) + .withCounterpartType("objective").build(); + AlignmentView alignmentView2 = AlignmentView.Builder.builder().withUniqueId("54SobjectivekeyResult").withId(5L) + .withTitle("Wir wollen das leiseste Team bei Puzzle sein.").withTeamId(4L).withTeamName("/BBT") + .withQuarterId(2L).withObjectType("objective").withConnectionRole("source").withCounterpartId(4L) + .withCounterpartType("keyResult").build(); + AlignmentView alignmentView3 = AlignmentView.Builder.builder().withUniqueId("4041Tobjectiveobjective").withId(40L) + .withTitle("Wir wollen eine gute Mitarbeiterzufriedenheit.").withTeamId(6L).withTeamName("LoremIpsum") + .withQuarterId(2L).withObjectType("objective").withConnectionRole("target").withCounterpartId(41L) + .withCounterpartType("objective").build(); + AlignmentView alignmentView4 = AlignmentView.Builder.builder().withUniqueId("4140Sobjectiveobjective").withId(41L) + .withTitle("Das Projekt generiert 10000 CHF Umsatz").withTeamId(6L).withTeamName("LoremIpsum") + .withQuarterId(2L).withObjectType("objective").withConnectionRole("source").withCounterpartId(40L) + .withCounterpartType("objective").build(); + + @Test + void shouldGetTargetAlignmentIdObjective() { + // arrange + when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(objectiveALignment); + + // act + AlignedEntityDto alignedEntity = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); + + // assert + assertEquals(alignedEntityDtoObjective, alignedEntity); + } + + @Test + void shouldReturnNullWhenNoAlignmentFound() { + // arrange + when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(null); + + // act + AlignedEntityDto alignedEntity = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); + + // assert + verify(validator, times(1)).validateOnGet(5L); + assertNull(alignedEntity); + } + + @Test + void shouldGetTargetAlignmentIdKeyResult() { + // arrange + when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(keyResultAlignment); + + // act + AlignedEntityDto alignedEntity = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); + + // assert + assertEquals(alignedEntityDtoKeyResult, alignedEntity); + } + + @Test + void shouldCreateNewAlignment() { + // arrange + when(objectivePersistenceService.findById(8L)).thenReturn(objective1); + Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objectiveAlignedObjective) + .withTargetObjective(objective1).build(); + + // act + alignmentBusinessService.createEntity(objectiveAlignedObjective); + + // assert + verify(alignmentPersistenceService, times(1)).save(returnAlignment); + } + + @Test + void shouldUpdateEntityNewAlignment() { + // arrange + when(alignmentPersistenceService.findByAlignedObjectiveId(8L)).thenReturn(null); + when(objectivePersistenceService.findById(8L)).thenReturn(objective1); + Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objectiveAlignedObjective) + .withTargetObjective(objective1).build(); + + // act + alignmentBusinessService.updateEntity(8L, objectiveAlignedObjective); + + // assert + verify(alignmentPersistenceService, times(1)).save(returnAlignment); + } + + @Test + void shouldUpdateEntityDeleteAlignment() { + // arrange + when(alignmentPersistenceService.findByAlignedObjectiveId(8L)).thenReturn(objectiveAlignment2); + + // act + alignmentBusinessService.updateEntity(8L, objective3); + + // assert + verify(alignmentPersistenceService, times(1)).deleteById(2L); + } + + @Test + void shouldUpdateEntityChangeTargetId() { + // arrange + when(alignmentPersistenceService.findByAlignedObjectiveId(8L)).thenReturn(objectiveAlignment2); + when(objectivePersistenceService.findById(8L)).thenReturn(objective1); + Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withId(2L).withAlignedObjective(objectiveAlignedObjective) + .withTargetObjective(objective1).build(); + + // act + alignmentBusinessService.updateEntity(8L, objectiveAlignedObjective); + + // assert + verify(alignmentPersistenceService, times(1)).save(returnAlignment); + } + + @Test + void shouldUpdateEntityChangeObjectiveToKeyResult() { + // arrange + when(alignmentPersistenceService.findByAlignedObjectiveId(8L)).thenReturn(objectiveAlignment2); + when(keyResultPersistenceService.findById(5L)).thenReturn(metricKeyResult); + Alignment returnAlignment = KeyResultAlignment.Builder.builder().withId(2L).withAlignedObjective(keyResultAlignedObjective) + .withTargetKeyResult(metricKeyResult).build(); + + // act + alignmentBusinessService.updateEntity(8L, keyResultAlignedObjective); + + // assert + verify(alignmentPersistenceService, times(0)).save(returnAlignment); + verify(alignmentPersistenceService, times(1)).recreateEntity(2L, returnAlignment); + } + + @Test + void shouldBuildAlignmentCorrectObjective() { + // arrange + when(objectivePersistenceService.findById(8L)).thenReturn(objective1); + Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objectiveAlignedObjective) + .withTargetObjective(objective1).build(); + + // act + Alignment alignment = alignmentBusinessService.buildAlignmentModel(objectiveAlignedObjective, 0); + + // assert + assertEquals(returnAlignment, alignment); + assertInstanceOf(ObjectiveAlignment.class, alignment); + } + + @Test + void shouldBuildAlignmentCorrectKeyResult() { + // arrange + when(keyResultPersistenceService.findById(5L)).thenReturn(metricKeyResult); + Alignment returnAlignment = KeyResultAlignment.Builder.builder().withAlignedObjective(keyResultAlignedObjective) + .withTargetKeyResult(metricKeyResult).build(); + + // act + Alignment alignment = alignmentBusinessService.buildAlignmentModel(keyResultAlignedObjective, 0); + + // assert + assertEquals(returnAlignment, alignment); + assertInstanceOf(KeyResultAlignment.class, alignment); + } + + @Test + void shouldThrowErrorWhenAlignedEntityIsIncorrect() { + // arrange + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NOT_SET", + List.of("alignedEntity", new AlignedEntityDto(0L, "Hello").toString()))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> alignmentBusinessService.buildAlignmentModel(wrongAlignedObjective, 0)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void shouldReturnCorrectIsAlignmentTypeChange() { + assertTrue(alignmentBusinessService.isAlignmentTypeChange(keyResultAlignment, objectiveALignment)); + assertTrue(alignmentBusinessService.isAlignmentTypeChange(objectiveALignment, keyResultAlignment)); + assertFalse(alignmentBusinessService.isAlignmentTypeChange(objectiveALignment, objectiveALignment)); + assertFalse(alignmentBusinessService.isAlignmentTypeChange(keyResultAlignment, keyResultAlignment)); + } + + @Test + void shouldUpdateKeyResultIdOnChange() { + // arrange + when(alignmentPersistenceService.findByKeyResultAlignmentId(1L)).thenReturn(List.of(keyResultAlignment)); + + // act + alignmentBusinessService.updateKeyResultIdOnIdChange(1L, metricKeyResult); + keyResultAlignment.setAlignmentTarget(metricKeyResult); + + // assert + verify(alignmentPersistenceService, times(1)).save(keyResultAlignment); + } + + @Test + void shouldDeleteByObjectiveId() { + // arrange + when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(objectiveALignment); + when(alignmentPersistenceService.findByObjectiveAlignmentId(5L)).thenReturn(List.of(objectiveAlignment2)); + + // act + alignmentBusinessService.deleteAlignmentByObjectiveId(5L); + + // assert + verify(alignmentPersistenceService, times(1)).deleteById(objectiveALignment.getId()); + verify(alignmentPersistenceService, times(1)).deleteById(objectiveAlignment2.getId()); + } + + @Test + void shouldDeleteByKeyResultId() { + // arrange + when(alignmentPersistenceService.findByKeyResultAlignmentId(5L)).thenReturn(List.of(keyResultAlignment)); + + // act + alignmentBusinessService.deleteAlignmentByKeyResultId(5L); + + // assert + verify(alignmentPersistenceService, times(1)).deleteById(keyResultAlignment.getId()); + } + + @Test + void shouldReturnCorrectAlignmentData() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(5L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(4L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals(5L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(4L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + } + + @Test + void shouldReturnCorrectAlignmentDataWithMultipleTeamFilter() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 6L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(2, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(4, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(5L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals(5L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(4L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + assertEquals(41L, alignmentLists.alignmentConnectionDtoList().get(1).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(1).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(1).targetKeyResultId()); + } + + @Test + void shouldReturnCorrectAlignmentDataWhenTeamFilterHasLimitedMatch() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(5L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(4L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals(5L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(4L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoMatchingTeam() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(12L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoTeamFilterProvided() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, null, ""); + + verify(alignmentViewPersistenceService, times(0)).getAlignmentViewListByQuarterId(2L); + verify(validator, times(1)).validateOnAlignmentGet(2L, List.of()); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoQuarterFilterProvided() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(any())) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + when(quarterBusinessService.getCurrentQuarter()).thenReturn(quarter); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(null, List.of(4L, 6L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + verify(quarterBusinessService, times(1)).getCurrentQuarter(); + assertEquals(2, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(4, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(5L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals(5L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(4L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + assertEquals(41L, alignmentLists.alignmentConnectionDtoList().get(1).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(1).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(1).targetKeyResultId()); + } + + @Test + void shouldReturnCorrectAlignmentDataWithObjectiveSearch() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 5L, 6L), + "leise"); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(5L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(4L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals(5L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(4L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoMatchingObjectiveFromObjectiveSearch() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 5L, 6L), + "Supportanfragen"); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoMatchingObjectiveSearch() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 5L, 6L), + "wird nicht vorkommen"); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoAlignmentViews() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)).thenReturn(List.of()); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 5L, 6L), + ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + } + + @Test + void shouldReturnCorrectAlignmentListsWithComplexAlignments() { + AlignmentView alignmentView1 = AlignmentView.Builder.builder().withUniqueId("36TkeyResultkeyResult").withId(3L) + .withTitle("Steigern der URS um 25%").withTeamId(5L).withTeamName("Puzzle ITC").withQuarterId(2L) + .withObjectType("keyResult").withConnectionRole("target").withCounterpartId(6L) + .withCounterpartType("objective").build(); + AlignmentView alignmentView2 = AlignmentView.Builder.builder().withUniqueId("63SobjectivekeyResult").withId(6L) + .withTitle("Als BBT wollen wir den Arbeitsalltag der Members von Puzzle ITC erleichtern.") + .withTeamId(4L).withTeamName("/BBT").withQuarterId(2L).withObjectType("objective") + .withConnectionRole("source").withCounterpartId(3L).withCounterpartType("keyResult").build(); + AlignmentView alignmentView3 = AlignmentView.Builder.builder().withUniqueId("63Tobjectiveobjective").withId(6L) + .withTitle("Als BBT wollen wir den Arbeitsalltag der Members von Puzzle ITC erleichtern.") + .withTeamId(4L).withTeamName("/BBT").withQuarterId(2L).withObjectType("objective") + .withConnectionRole("target").withCounterpartId(3L).withCounterpartType("objective").build(); + AlignmentView alignmentView4 = AlignmentView.Builder.builder().withUniqueId("36Sobjectiveobjective").withId(3L) + .withTitle("Wir wollen die Kundenzufriedenheit steigern").withTeamId(4L).withTeamName("/BBT") + .withQuarterId(2L).withObjectType("objective").withConnectionRole("source").withCounterpartId(6L) + .withCounterpartType("objective").build(); + + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(5L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(3L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals("keyResult", alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(6L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(1).objectType()); + assertEquals(6L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(3L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + } + + @Test + void shouldCorrectFilterAlignmentViewListsWithAllCorrectData() { + AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService + .filterAndDivideAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), + List.of(4L, 6L, 5L), ""); + + assertEquals(4, dividedAlignmentViewLists.filterMatchingAlignments().size()); + assertEquals(0, dividedAlignmentViewLists.nonMatchingAlignments().size()); + assertEquals(4, dividedAlignmentViewLists.filterMatchingAlignments().get(0).getId()); + assertEquals(5, dividedAlignmentViewLists.filterMatchingAlignments().get(1).getId()); + assertEquals(40, dividedAlignmentViewLists.filterMatchingAlignments().get(2).getId()); + assertEquals(41, dividedAlignmentViewLists.filterMatchingAlignments().get(3).getId()); + } + + @Test + void shouldCorrectFilterAlignmentViewListsWithLimitedTeamFilter() { + AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService + .filterAndDivideAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), + List.of(6L), ""); + + assertEquals(2, dividedAlignmentViewLists.filterMatchingAlignments().size()); + assertEquals(2, dividedAlignmentViewLists.nonMatchingAlignments().size()); + assertEquals(40, dividedAlignmentViewLists.filterMatchingAlignments().get(0).getId()); + assertEquals(41, dividedAlignmentViewLists.filterMatchingAlignments().get(1).getId()); + assertEquals(4, dividedAlignmentViewLists.nonMatchingAlignments().get(0).getId()); + assertEquals(5, dividedAlignmentViewLists.nonMatchingAlignments().get(1).getId()); + } + + @Test + void shouldCorrectFilterAlignmentViewListsWithObjectiveSearch() { + AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService + .filterAndDivideAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), + List.of(4L, 6L, 5L), "leise"); + + assertEquals(1, dividedAlignmentViewLists.filterMatchingAlignments().size()); + assertEquals(3, dividedAlignmentViewLists.nonMatchingAlignments().size()); + assertEquals(5, dividedAlignmentViewLists.filterMatchingAlignments().get(0).getId()); + assertEquals(4, dividedAlignmentViewLists.nonMatchingAlignments().get(0).getId()); + assertEquals(40, dividedAlignmentViewLists.nonMatchingAlignments().get(1).getId()); + assertEquals(41, dividedAlignmentViewLists.nonMatchingAlignments().get(2).getId()); + } + + @Test + void shouldCorrectFilterWhenNoMatchingObjectiveSearch() { + AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService + .filterAndDivideAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), + List.of(4L, 6L, 5L), "verk"); + + assertEquals(0, dividedAlignmentViewLists.filterMatchingAlignments().size()); + assertEquals(4, dividedAlignmentViewLists.nonMatchingAlignments().size()); + assertEquals(4, dividedAlignmentViewLists.nonMatchingAlignments().get(0).getId()); + assertEquals(5, dividedAlignmentViewLists.nonMatchingAlignments().get(1).getId()); + assertEquals(40, dividedAlignmentViewLists.nonMatchingAlignments().get(2).getId()); + assertEquals(41, dividedAlignmentViewLists.nonMatchingAlignments().get(3).getId()); + } + + @Test + void shouldThrowErrorWhenPersistenceServiceReturnsIncorrectData() { + AlignmentView alignmentView5 = AlignmentView.Builder.builder().withUniqueId("23TkeyResultkeyResult").withId(20L) + .withTitle("Dies hat kein Gegenstück").withTeamId(5L).withTeamName("Puzzle ITC").withQuarterId(2L) + .withObjectType("keyResult").withConnectionRole("target").withCounterpartId(37L) + .withCounterpartType("objective").build(); + List finalList = List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4, + alignmentView5); + + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)).thenReturn(finalList); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(5L), "")); + + List expectedErrors = List + .of(new ErrorDto("ALIGNMENT_DATA_FAIL", List.of("alignmentData", "2", "[5]", ""))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + Assertions.assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void shouldNotThrowErrorWhenSameAmountOfSourceAndTarget() { + List finalList = List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4); + + assertDoesNotThrow( + () -> alignmentBusinessService.sourceAndTargetListsEqualSameSize(finalList, 2L, List.of(5L), "")); + } + + @Test + void shouldThrowErrorWhenNotSameAmountOfSourceAndTarget() { + List finalList = List.of(alignmentView1, alignmentView2, alignmentView3); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> alignmentBusinessService.sourceAndTargetListsEqualSameSize(finalList, 2L, List.of(5L), "")); + + List expectedErrors = List + .of(new ErrorDto("ALIGNMENT_DATA_FAIL", List.of("alignmentData", "2", "[5]", ""))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + Assertions.assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } +} diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessServiceTest.java deleted file mode 100644 index 961b1e12cb..0000000000 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessServiceTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package ch.puzzle.okr.service.business; - -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.models.alignment.AlignmentSelectionId; -import ch.puzzle.okr.service.persistence.AlignmentSelectionPersistenceService; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class AlignmentSelectionBusinessServiceTest { - - @InjectMocks - AlignmentSelectionBusinessService alignmentSelectionBusinessService; - @Mock - AlignmentSelectionPersistenceService alignmentSelectionPersistenceService; - - private static AlignmentSelection createAlignmentSelection() { - return AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(9L, 15L)) - .withTeamId(5L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 9").withQuarterId(2L) - .withQuarterLabel("GJ 23/24-Q1").withKeyResultTitle("Key Result 15").build(); - } - - @Test - void getAlignmentSelectionByQuarterIdAndTeamIdNotShouldReturnListOfAlignmentSelections() { - when(alignmentSelectionPersistenceService.getAlignmentSelectionByQuarterIdAndTeamIdNot(2L, 4L)) - .thenReturn(List.of(createAlignmentSelection())); - - List alignmentSelections = alignmentSelectionBusinessService - .getAlignmentSelectionByQuarterIdAndTeamIdNot(2L, 4L); - - assertEquals(1, alignmentSelections.size()); - verify(alignmentSelectionPersistenceService, times(1)).getAlignmentSelectionByQuarterIdAndTeamIdNot(2L, 4L); - } -} diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/KeyResultBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/KeyResultBusinessServiceTest.java index e272bbbb8a..51ca684c6e 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/KeyResultBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/KeyResultBusinessServiceTest.java @@ -42,6 +42,8 @@ class KeyResultBusinessServiceTest { KeyResultValidationService validator; @Mock ActionBusinessService actionBusinessService; + @Mock + AlignmentBusinessService alignmentBusinessService; @InjectMocks private KeyResultBusinessService keyResultBusinessService; List keyResults; @@ -154,6 +156,7 @@ void shouldEditMetricKeyResultWhenATypeChange() { verify(checkInBusinessService, times(1)).getCheckInsByKeyResultId(1L); verify(actionBusinessService, times(1)).deleteEntitiesByKeyResultId(1L); verify(actionBusinessService, times(1)).createEntities(actions); + verify(alignmentBusinessService, times(1)).updateKeyResultIdOnIdChange(1L, newKeyresult); assertEquals(1L, newKeyresult.getId()); assertEquals("Keyresult Metric update", newKeyresult.getTitle()); } @@ -172,6 +175,7 @@ void shouldEditOrdinalKeyResultWhenATypeChange() { verify(checkInBusinessService, times(1)).getCheckInsByKeyResultId(1L); verify(actionBusinessService, times(1)).deleteEntitiesByKeyResultId(1L); verify(actionBusinessService, times(1)).createEntities(actions); + verify(alignmentBusinessService, times(1)).updateKeyResultIdOnIdChange(1L, newKeyresult); assertEquals(1L, newKeyresult.getId()); assertEquals("Keyresult Ordinal update", newKeyresult.getTitle()); } @@ -324,6 +328,7 @@ void shouldDeleteKeyResultAndAssociatedCheckInsAndActions() { verify(checkInBusinessService, times(1)).deleteEntityById(1L); verify(actionBusinessService, times(2)).deleteEntityById(3L); + verify(alignmentBusinessService, times(1)).deleteAlignmentByKeyResultId(1L); verify(keyResultPersistenceService, times(1)).deleteById(1L); } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java index f7360742c3..36e1304af7 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java @@ -1,5 +1,9 @@ package ch.puzzle.okr.service.business; +import ch.puzzle.okr.dto.AlignmentDto; +import ch.puzzle.okr.dto.AlignmentObjectDto; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; +import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.*; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.models.keyresult.KeyResult; @@ -16,6 +20,7 @@ import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; import org.springframework.web.server.ResponseStatusException; import java.time.LocalDateTime; @@ -28,6 +33,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import static org.springframework.http.HttpStatus.NOT_FOUND; +import static ch.puzzle.okr.TestConstants.*; @ExtendWith(MockitoExtension.class) class ObjectiveBusinessServiceTest { @@ -40,62 +46,225 @@ class ObjectiveBusinessServiceTest { @Mock KeyResultBusinessService keyResultBusinessService; @Mock + AlignmentBusinessService alignmentBusinessService; + @Mock CompletedBusinessService completedBusinessService; @Mock ObjectiveValidationService validator = Mockito.mock(ObjectiveValidationService.class); - private final Team team1 = Team.Builder.builder().withId(1L).withName("Team1").build(); + private static final String TEAM_1 = "Team1"; + private static final String OBJECTIVE = "objective"; + private static final String FULL_OBJECTIVE_1 = "O - FullObjective1"; + private static final String FULL_OBJECTIVE_2 = "O - FullObjective2"; + + private final Team team1 = Team.Builder.builder().withId(1L).withName(TEAM_1).build(); private final Quarter quarter = Quarter.Builder.builder().withId(1L).withLabel("GJ 22/23-Q2").build(); private final User user = User.Builder.builder().withId(1L).withFirstname("Bob").withLastname("Kaufmann") .withUsername("bkaufmann").withEmail("kaufmann@puzzle.ch").build(); - private final Objective objective = Objective.Builder.builder().withId(5L).withTitle("Objective 1").build(); - private final Objective fullObjective = Objective.Builder.builder().withTitle("FullObjective1").withCreatedBy(user) - .withTeam(team1).withQuarter(quarter).withDescription("This is our description") + private final Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").build(); + private final Objective fullObjectiveCreate = Objective.Builder.builder().withTitle("FullObjective1") + .withCreatedBy(user).withTeam(team1).withQuarter(quarter).withDescription("This is our description") + .withModifiedOn(LocalDateTime.MAX).build(); + private final Objective fullObjective1 = Objective.Builder.builder().withId(1L).withTitle("FullObjective1") + .withCreatedBy(user).withTeam(team1).withQuarter(quarter).withDescription("This is our description") .withModifiedOn(LocalDateTime.MAX).build(); + private final Objective fullObjective2 = Objective.Builder.builder().withId(2L).withTitle("FullObjective2") + .withCreatedBy(user).withTeam(team1).withQuarter(quarter).withDescription("This is our description") + .withModifiedOn(LocalDateTime.MAX).build(); + private final Team team2 = Team.Builder.builder().withId(3L).withName(TEAM_PUZZLE).build(); + private final Objective fullObjective3 = Objective.Builder.builder().withId(3L).withTitle("FullObjective5") + .withCreatedBy(user).withTeam(team2).withQuarter(quarter).withDescription("This is our description") + .withModifiedOn(LocalDateTime.MAX).build(); + private final KeyResult ordinalKeyResult2 = KeyResultOrdinal.Builder.builder().withCommitZone("Baum") + .withStretchZone("Wald").withId(6L).withTitle("Keyresult Ordinal 6").withObjective(fullObjective3).build(); private final KeyResult ordinalKeyResult = KeyResultOrdinal.Builder.builder().withCommitZone("Baum") - .withStretchZone("Wald").withId(5L).withTitle("Keyresult Ordinal").withObjective(objective).build(); - private final List keyResultList = List.of(ordinalKeyResult, ordinalKeyResult, ordinalKeyResult); + .withStretchZone("Wald").withId(5L).withTitle("Keyresult Ordinal").withObjective(objective1).build(); + private final List keyResultList = List.of(ordinalKeyResult, ordinalKeyResult); + private final List objectiveList = List.of(fullObjective1, fullObjective2); + private final AlignmentObjectDto alignmentObjectDto1 = new AlignmentObjectDto(1L, FULL_OBJECTIVE_1, OBJECTIVE); + private final AlignmentObjectDto alignmentObjectDto2 = new AlignmentObjectDto(5L, "KR - Keyresult Ordinal", + "keyResult"); + private final AlignmentObjectDto alignmentObjectDto3 = new AlignmentObjectDto(5L, "KR - Keyresult Ordinal", + "keyResult"); + private final AlignmentObjectDto alignmentObjectDto4 = new AlignmentObjectDto(2L, FULL_OBJECTIVE_2, OBJECTIVE); + private final AlignmentObjectDto alignmentObjectDto5 = new AlignmentObjectDto(5L, "KR - Keyresult Ordinal", + "keyResult"); + private final AlignmentObjectDto alignmentObjectDto6 = new AlignmentObjectDto(5L, "KR - Keyresult Ordinal", + "keyResult"); + private final AlignmentDto alignmentDto = new AlignmentDto(1L, TEAM_1, List.of(alignmentObjectDto1, + alignmentObjectDto2, alignmentObjectDto3, alignmentObjectDto4, alignmentObjectDto5, alignmentObjectDto6)); + AlignedEntityDto alignedEntityDtoObjective = new AlignedEntityDto(53L, OBJECTIVE); @Test void getOneObjective() { - when(objectivePersistenceService.findById(5L)).thenReturn(objective); + // arrange + when(objectivePersistenceService.findById(5L)).thenReturn(objective1); + // act Objective realObjective = objectiveBusinessService.getEntityById(5L); + // assert + verify(alignmentBusinessService, times(1)).getTargetIdByAlignedObjectiveId(5L); assertEquals("Objective 1", realObjective.getTitle()); } @Test void getEntitiesByTeamId() { - when(objectivePersistenceService.findObjectiveByTeamId(anyLong())).thenReturn(List.of(objective)); + // arrange + when(objectivePersistenceService.findObjectiveByTeamId(anyLong())).thenReturn(List.of(objective1)); + // act List entities = objectiveBusinessService.getEntitiesByTeamId(5L); - assertThat(entities).hasSameElementsAs(List.of(objective)); + // assert + verify(alignmentBusinessService, times(1)).getTargetIdByAlignedObjectiveId(5L); + assertThat(entities).hasSameElementsAs(List.of(objective1)); } @Test void shouldNotFindTheObjective() { + // arrange when(objectivePersistenceService.findById(6L)) .thenThrow(new ResponseStatusException(NOT_FOUND, "Objective with id 6 not found")); + // act ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> objectiveBusinessService.getEntityById(6L)); + + // assert assertEquals(NOT_FOUND, exception.getStatusCode()); assertEquals("Objective with id 6 not found", exception.getReason()); } @Test void shouldSaveANewObjective() { + // arrange Objective objective = spy(Objective.Builder.builder().withTitle("Received Objective").withTeam(team1) .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) .withState(DRAFT).build()); - doNothing().when(objective).setCreatedOn(any()); + // act objectiveBusinessService.createEntity(objective, authorizationUser); + // assert verify(objectivePersistenceService, times(1)).save(objective); + verify(alignmentBusinessService, times(0)).createEntity(any()); + assertEquals(DRAFT, objective.getState()); + assertEquals(user, objective.getCreatedBy()); + assertNull(objective.getCreatedOn()); + } + + @Test + void shouldUpdateAnObjective() { + // arrange + Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) + .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) + .withState(DRAFT).build()); + when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); + when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn(null); + when(objectivePersistenceService.save(any())).thenReturn(objective); + doNothing().when(objective).setCreatedOn(any()); + + // act + Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, + authorizationUser); + + // assert + verify(objectivePersistenceService, times(1)).save(objective); + verify(alignmentBusinessService, times(0)).updateEntity(any(), any()); + assertEquals(objective.getTitle(), updatedObjective.getTitle()); + assertEquals(objective.getTeam(), updatedObjective.getTeam()); + assertNull(objective.getCreatedOn()); + } + + @Test + void shouldUpdateAnObjectiveWithAlignment() { + // arrange + Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) + .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) + .withState(DRAFT).withAlignedEntity(alignedEntityDtoObjective).build()); + when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); + when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())) + .thenReturn(new AlignedEntityDto(41L, OBJECTIVE)); + when(objectivePersistenceService.save(any())).thenReturn(objective); + doNothing().when(objective).setCreatedOn(any()); + + // act + Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, + authorizationUser); + objective.setAlignedEntity(alignedEntityDtoObjective); + + // assert + verify(objectivePersistenceService, times(1)).save(objective); + verify(alignmentBusinessService, times(1)).updateEntity(objective.getId(), objective); + assertEquals(objective.getTitle(), updatedObjective.getTitle()); + assertEquals(objective.getTeam(), updatedObjective.getTeam()); + assertNull(objective.getCreatedOn()); + } + + @Test + void shouldUpdateAnObjectiveWithANewAlignment() { + // arrange + Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) + .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) + .withState(DRAFT).withAlignedEntity(alignedEntityDtoObjective).build()); + when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); + when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn(null); + when(objectivePersistenceService.save(any())).thenReturn(objective); + doNothing().when(objective).setCreatedOn(any()); + + // act + Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, + authorizationUser); + objective.setAlignedEntity(alignedEntityDtoObjective); + + // assert + verify(objectivePersistenceService, times(1)).save(objective); + verify(alignmentBusinessService, times(1)).updateEntity(objective.getId(), objective); + assertEquals(objective.getTitle(), updatedObjective.getTitle()); + assertEquals(objective.getTeam(), updatedObjective.getTeam()); + assertNull(objective.getCreatedOn()); + } + + @Test + void shouldUpdateAnObjectiveWithAlignmentDelete() { + // arrange + Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) + .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) + .withState(DRAFT).build()); + when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); + when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())) + .thenReturn(new AlignedEntityDto(52L, "objective")); + when(objectivePersistenceService.save(any())).thenReturn(objective); + doNothing().when(objective).setCreatedOn(any()); + + // act + Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, + authorizationUser); + + // assert + verify(objectivePersistenceService, times(1)).save(objective); + verify(alignmentBusinessService, times(1)).updateEntity(objective.getId(), objective); + assertEquals(objective.getTitle(), updatedObjective.getTitle()); + assertEquals(objective.getTeam(), updatedObjective.getTeam()); + assertNull(objective.getCreatedOn()); + } + + @Test + void shouldSaveANewObjectiveWithAlignment() { + // arrange + Objective objective = spy(Objective.Builder.builder().withTitle("Received Objective").withTeam(team1) + .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) + .withState(DRAFT).withAlignedEntity(new AlignedEntityDto(42L, OBJECTIVE)).build()); + doNothing().when(objective).setCreatedOn(any()); + + // act + Objective savedObjective = objectiveBusinessService.createEntity(objective, authorizationUser); + + // assert + verify(objectivePersistenceService, times(1)).save(objective); + verify(alignmentBusinessService, times(1)).createEntity(savedObjective); assertEquals(DRAFT, objective.getState()); assertEquals(user, objective.getCreatedBy()); assertNull(objective.getCreatedOn()); @@ -103,11 +272,15 @@ void shouldSaveANewObjective() { @Test void shouldNotThrowResponseStatusExceptionWhenPuttingNullId() { + // arrange Objective objective1 = Objective.Builder.builder().withId(null).withTitle("Title") .withDescription("Description").withModifiedOn(LocalDateTime.now()).build(); - when(objectiveBusinessService.createEntity(objective1, authorizationUser)).thenReturn(fullObjective); + when(objectiveBusinessService.createEntity(objective1, authorizationUser)).thenReturn(fullObjectiveCreate); + // act Objective savedObjective = objectiveBusinessService.createEntity(objective1, authorizationUser); + + // assert assertNull(savedObjective.getId()); assertEquals("FullObjective1", savedObjective.getTitle()); assertEquals("Bob", savedObjective.getCreatedBy().getFirstname()); @@ -133,7 +306,7 @@ void updateEntityShouldHandleQuarterCorrectly(boolean hasKeyResultAnyCheckIns) { when(keyResultBusinessService.getAllKeyResultsByObjective(savedObjective.getId())).thenReturn(keyResultList); when(keyResultBusinessService.hasKeyResultAnyCheckIns(any())).thenReturn(hasKeyResultAnyCheckIns); when(objectivePersistenceService.save(changedObjective)).thenReturn(updatedObjective); - + when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn(null); boolean isImUsed = objectiveBusinessService.isImUsed(changedObjective); Objective updatedEntity = objectiveBusinessService.updateEntity(changedObjective.getId(), changedObjective, authorizationUser); @@ -143,31 +316,131 @@ void updateEntityShouldHandleQuarterCorrectly(boolean hasKeyResultAnyCheckIns) { updatedEntity.getQuarter()); assertEquals(changedObjective.getDescription(), updatedEntity.getDescription()); assertEquals(changedObjective.getTitle(), updatedEntity.getTitle()); + verify(alignmentBusinessService, times(0)).updateEntity(any(), any()); } @Test void shouldDeleteObjectiveAndAssociatedKeyResults() { + // arrange when(keyResultBusinessService.getAllKeyResultsByObjective(1L)).thenReturn(keyResultList); + // act objectiveBusinessService.deleteEntityById(1L); - verify(keyResultBusinessService, times(3)).deleteEntityById(5L); + // assert + verify(keyResultBusinessService, times(2)).deleteEntityById(5L); verify(objectiveBusinessService, times(1)).deleteEntityById(1L); + verify(alignmentBusinessService, times(1)).deleteAlignmentByObjectiveId(1L); } @Test void shouldDuplicateObjective() { + // arrange KeyResult keyResultOrdinal = KeyResultOrdinal.Builder.builder().withTitle("Ordinal").build(); KeyResult keyResultOrdinal2 = KeyResultOrdinal.Builder.builder().withTitle("Ordinal2").build(); KeyResult keyResultMetric = KeyResultMetric.Builder.builder().withTitle("Metric").withUnit(Unit.FTE).build(); KeyResult keyResultMetric2 = KeyResultMetric.Builder.builder().withTitle("Metric2").withUnit(Unit.CHF).build(); List keyResults = List.of(keyResultOrdinal, keyResultOrdinal2, keyResultMetric, keyResultMetric2); - - when(objectivePersistenceService.save(any())).thenReturn(objective); + when(objectivePersistenceService.save(any())).thenReturn(objective1); when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(keyResults); - objectiveBusinessService.duplicateObjective(objective.getId(), objective, authorizationUser); + // act + objectiveBusinessService.duplicateObjective(objective1.getId(), objective1, authorizationUser); + + // assert verify(keyResultBusinessService, times(4)).createEntity(any(), any()); verify(objectiveBusinessService, times(1)).createEntity(any(), any()); } + + @Test + void shouldReturnAlignmentPossibilities() { + // arrange + when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); + when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(keyResultList); + + // act + List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + + // assert + verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(1L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(2L); + assertEquals(alignmentsDtos, List.of(alignmentDto)); + } + + @Test + void shouldReturnAlignmentPossibilitiesWithMultipleTeams() { + // arrange + List objectiveList = List.of(fullObjective1, fullObjective2, fullObjective3); + when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); + when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(keyResultList); + AlignmentObjectDto alignmentObjectDto1 = new AlignmentObjectDto(3L, "O - FullObjective5", OBJECTIVE); + AlignmentObjectDto alignmentObjectDto2 = new AlignmentObjectDto(5L, "KR - Keyresult Ordinal", "keyResult"); + AlignmentObjectDto alignmentObjectDto3 = new AlignmentObjectDto(5L, "KR - Keyresult Ordinal", "keyResult"); + AlignmentDto alignmentDto = new AlignmentDto(3L, TEAM_PUZZLE, + List.of(alignmentObjectDto1, alignmentObjectDto2, alignmentObjectDto3)); + + // act + List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + + // assert + verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(1L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(2L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(3L); + assertEquals(alignmentsDtos, List.of(alignmentDto, this.alignmentDto)); + } + + @Test + void shouldReturnAlignmentPossibilitiesOnlyObjectives() { + // arrange + when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); + when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(List.of()); + + // act + List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + + // assert + verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(1L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(2L); + assertEquals(2, alignmentsDtos.get(0).alignmentObjects().size()); + assertEquals(1, alignmentsDtos.get(0).alignmentObjects().get(0).objectId()); + assertEquals(FULL_OBJECTIVE_1, alignmentsDtos.get(0).alignmentObjects().get(0).objectTitle()); + assertEquals(OBJECTIVE, alignmentsDtos.get(0).alignmentObjects().get(0).objectType()); + assertEquals(2, alignmentsDtos.get(0).alignmentObjects().get(1).objectId()); + assertEquals(FULL_OBJECTIVE_2, alignmentsDtos.get(0).alignmentObjects().get(1).objectTitle()); + assertEquals(OBJECTIVE, alignmentsDtos.get(0).alignmentObjects().get(1).objectType()); + } + + @Test + void shouldReturnEmptyAlignmentPossibilities() { + // arrange + List objectiveList = List.of(); + when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); + + // act + List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + + // assert + verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); + verify(keyResultBusinessService, times(0)).getAllKeyResultsByObjective(anyLong()); + assertEquals(0, alignmentsDtos.size()); + } + + @Test + void shouldThrowExceptionWhenQuarterIdIsNull() { + // arrange + Mockito.doThrow(new OkrResponseStatusException(HttpStatus.BAD_REQUEST, "ATTRIBUTE_NULL")).when(validator) + .validateOnGet(null); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> { + objectiveBusinessService.getAlignmentPossibilities(null); + }); + + // assert + assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); + assertEquals("ATTRIBUTE_NULL", exception.getReason()); + } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java index bbbe9749f3..34e2f56a20 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java @@ -7,6 +7,7 @@ import ch.puzzle.okr.models.alignment.Alignment; import ch.puzzle.okr.models.alignment.KeyResultAlignment; import ch.puzzle.okr.models.alignment.ObjectiveAlignment; +import ch.puzzle.okr.models.keyresult.KeyResult; import ch.puzzle.okr.models.keyresult.KeyResultMetric; import ch.puzzle.okr.test.SpringIntegrationTest; import org.junit.jupiter.api.AfterEach; @@ -16,8 +17,10 @@ import java.util.List; +import static ch.puzzle.okr.models.State.DRAFT; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; @SpringIntegrationTest @@ -25,6 +28,7 @@ class AlignmentPersistenceServiceIT { @Autowired private AlignmentPersistenceService alignmentPersistenceService; private Alignment createdAlignment; + private final String ALIGNMENT = "Alignment"; private static ObjectiveAlignment createObjectiveAlignment(Long id) { return ObjectiveAlignment.Builder.builder().withId(id) @@ -58,10 +62,13 @@ void tearDown() { @Test void saveAlignmentShouldSaveNewObjectiveAlignment() { + // arrange Alignment alignment = createObjectiveAlignment(null); + // act createdAlignment = alignmentPersistenceService.save(alignment); + // assert assertNotNull(createdAlignment.getId()); assertEquals(5L, createdAlignment.getAlignedObjective().getId()); assertEquals(4L, ((ObjectiveAlignment) createdAlignment).getAlignmentTarget().getId()); @@ -69,10 +76,13 @@ void saveAlignmentShouldSaveNewObjectiveAlignment() { @Test void saveAlignmentShouldSaveNewKeyResultAlignment() { + // arrange Alignment alignment = createKeyResultAlignment(null); + // act createdAlignment = alignmentPersistenceService.save(alignment); + // assert assertNotNull(createdAlignment.getId()); assertEquals(5L, createdAlignment.getAlignedObjective().getId()); assertEquals(8L, ((KeyResultAlignment) createdAlignment).getAlignmentTarget().getId()); @@ -80,56 +90,127 @@ void saveAlignmentShouldSaveNewKeyResultAlignment() { @Test void updateAlignmentShouldSaveKeyResultAlignment() { + // arrange createdAlignment = alignmentPersistenceService.save(createKeyResultAlignment(null)); Alignment updateAlignment = createKeyResultAlignment(createdAlignment.getId(), createdAlignment.getVersion()); updateAlignment.setAlignedObjective(Objective.Builder.builder().withId(8L).build()); + // act Alignment updatedAlignment = alignmentPersistenceService.save(updateAlignment); + // assert assertEquals(createdAlignment.getId(), updatedAlignment.getId()); assertEquals(createdAlignment.getVersion() + 1, updatedAlignment.getVersion()); } @Test void updateAlignmentShouldThrowExceptionWhenAlreadyUpdated() { + // arrange createdAlignment = alignmentPersistenceService.save(createKeyResultAlignment(null)); Alignment updateAlignment = createKeyResultAlignment(createdAlignment.getId(), 0); updateAlignment.setAlignedObjective(Objective.Builder.builder().withId(8L).build()); + List expectedErrors = List.of(new ErrorDto("DATA_HAS_BEEN_UPDATED", List.of(ALIGNMENT))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> alignmentPersistenceService.save(updateAlignment)); - List expectedErrors = List.of(new ErrorDto("DATA_HAS_BEEN_UPDATED", List.of("Alignment"))); - + // assert assertEquals(UNPROCESSABLE_ENTITY, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); } @Test - void findByAlignedObjectiveIdShouldReturnListOfAlignments() { - List alignments = alignmentPersistenceService.findByAlignedObjectiveId(4L); + void findByAlignedObjectiveIdShouldReturnAlignmentModel() { + // act + Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(4L); - assertEquals(2, alignments.size()); - alignments.forEach(this::assertAlignment); + // assert + assertNotNull(alignment); + assertEquals(4, alignment.getAlignedObjective().getId()); } @Test void findByKeyResultAlignmentIdShouldReturnListOfAlignments() { + // act List alignments = alignmentPersistenceService.findByKeyResultAlignmentId(8L); + // assert assertEquals(1, alignments.size()); assertAlignment(alignments.get(0)); } @Test void findByObjectiveAlignmentIdShouldReturnListOfAlignments() { + // act List alignments = alignmentPersistenceService.findByObjectiveAlignmentId(3L); + // assert assertEquals(1, alignments.size()); assertAlignment(alignments.get(0)); } + @Test + void recreateEntityShouldUpdateAlignmentNoTypeChange() { + // arrange + Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withState(DRAFT).build(); + Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withState(DRAFT).build(); + Objective objective3 = Objective.Builder.builder().withId(4L) + .withTitle("Build a company culture that kills the competition.").build(); + ObjectiveAlignment objectiveALignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective1) + .withTargetObjective(objective2).build(); + createdAlignment = alignmentPersistenceService.save(objectiveALignment); + ObjectiveAlignment createObjectiveAlignment = (ObjectiveAlignment) createdAlignment; + createObjectiveAlignment.setAlignmentTarget(objective3); + Long alignmentId = createObjectiveAlignment.getId(); + + // act + Alignment recreatedAlignment = alignmentPersistenceService.recreateEntity(createdAlignment.getId(), + createObjectiveAlignment); + createObjectiveAlignment = (ObjectiveAlignment) recreatedAlignment; + + // assert + assertNotNull(recreatedAlignment.getId()); + assertEquals(4L, createObjectiveAlignment.getAlignmentTarget().getId()); + assertEquals("Build a company culture that kills the competition.", + createObjectiveAlignment.getAlignmentTarget().getTitle()); + shouldDeleteOldAlignment(alignmentId); + + // delete re-created Alignment in tearDown() + createdAlignment = createObjectiveAlignment; + } + + @Test + void recreateEntityShouldUpdateAlignmentWithTypeChange() { + // arrange + Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withState(DRAFT).build(); + Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withState(DRAFT).build(); + KeyResult keyResult = KeyResultMetric.Builder.builder().withId(10L) + .withTitle("Im Durchschnitt soll die Lautstärke 60dB nicht überschreiten").build(); + ObjectiveAlignment objectiveALignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective1) + .withTargetObjective(objective2).build(); + createdAlignment = alignmentPersistenceService.save(objectiveALignment); + KeyResultAlignment keyResultAlignment = KeyResultAlignment.Builder.builder().withId(createdAlignment.getId()) + .withAlignedObjective(objective1).withTargetKeyResult(keyResult).build(); + Long alignmentId = createdAlignment.getId(); + + // act + Alignment recreatedAlignment = alignmentPersistenceService.recreateEntity(keyResultAlignment.getId(), + keyResultAlignment); + KeyResultAlignment returnedKeyResultAlignment = (KeyResultAlignment) recreatedAlignment; + + // assert + assertNotNull(recreatedAlignment.getId()); + assertEquals(createdAlignment.getAlignedObjective().getId(), recreatedAlignment.getAlignedObjective().getId()); + assertEquals("Im Durchschnitt soll die Lautstärke 60dB nicht überschreiten", + returnedKeyResultAlignment.getAlignmentTarget().getTitle()); + shouldDeleteOldAlignment(alignmentId); + + // delete re-created Alignment in tearDown() + createdAlignment = returnedKeyResultAlignment; + } + private void assertAlignment(Alignment alignment) { if (alignment instanceof ObjectiveAlignment objectiveAlignment) { assertAlignment(objectiveAlignment); @@ -140,15 +221,28 @@ private void assertAlignment(Alignment alignment) { } } + private void shouldDeleteOldAlignment(Long alignmentId) { + // Should delete the old Alignment + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> alignmentPersistenceService.findById(alignmentId)); + + List expectedErrors = List + .of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of(ALIGNMENT, alignmentId))); + + assertEquals(NOT_FOUND, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + private void assertAlignment(ObjectiveAlignment objectiveAlignment) { - assertEquals(1L, objectiveAlignment.getId()); + assertEquals(4L, objectiveAlignment.getId()); assertEquals(3L, objectiveAlignment.getAlignmentTarget().getId()); - assertEquals(4L, objectiveAlignment.getAlignedObjective().getId()); + assertEquals(8L, objectiveAlignment.getAlignedObjective().getId()); } private void assertAlignment(KeyResultAlignment keyResultAlignment) { - assertEquals(2L, keyResultAlignment.getId()); + assertEquals(1L, keyResultAlignment.getId()); assertEquals(8L, keyResultAlignment.getAlignmentTarget().getId()); - assertEquals(4L, keyResultAlignment.getAlignedObjective().getId()); + assertEquals(9L, keyResultAlignment.getAlignedObjective().getId()); } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceServiceIT.java deleted file mode 100644 index d01a662f88..0000000000 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceServiceIT.java +++ /dev/null @@ -1,48 +0,0 @@ -package ch.puzzle.okr.service.persistence; - -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.models.alignment.AlignmentSelectionId; -import ch.puzzle.okr.test.SpringIntegrationTest; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.List; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringIntegrationTest -class AlignmentSelectionPersistenceServiceIT { - @Autowired - private AlignmentSelectionPersistenceService alignmentSelectionPersistenceService; - - @Test - void getAlignmentSelectionByQuarterIdAndTeamIdNotShouldReturnAlignmentSelections() { - List alignmentSelections = alignmentSelectionPersistenceService - .getAlignmentSelectionByQuarterIdAndTeamIdNot(2L, 4L); - - assertEquals(12, alignmentSelections.size()); - alignmentSelections.forEach(alignmentSelection -> assertTrue( - matchAlignmentSelectionId(alignmentSelection.getAlignmentSelectionId()))); - } - - private boolean matchAlignmentSelectionId(AlignmentSelectionId alignmentSelectionId) { - return getExpectedAlignmentSelectionIds().anyMatch(id -> id.equals(alignmentSelectionId)); - } - - private static Stream getExpectedAlignmentSelectionIds() { - return Stream.of(AlignmentSelectionId.of(9L, 15L), // - AlignmentSelectionId.of(9L, 16L), // - AlignmentSelectionId.of(9L, 17L), // - AlignmentSelectionId.of(4L, 6L), // - AlignmentSelectionId.of(4L, 7L), // - AlignmentSelectionId.of(4L, 8L), // - AlignmentSelectionId.of(3L, 3L), // - AlignmentSelectionId.of(3L, 4L), // - AlignmentSelectionId.of(3L, 5L), // - AlignmentSelectionId.of(8L, 18L), // - AlignmentSelectionId.of(8L, 19L), // - AlignmentSelectionId.of(10L, -1L)); - } -} diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceServiceIT.java new file mode 100644 index 0000000000..9d36c2ede3 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceServiceIT.java @@ -0,0 +1,53 @@ +package ch.puzzle.okr.service.persistence; + +import ch.puzzle.okr.models.alignment.AlignmentView; +import ch.puzzle.okr.test.SpringIntegrationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringIntegrationTest +class AlignmentViewPersistenceServiceIT { + @Autowired + private AlignmentViewPersistenceService alignmentViewPersistenceService; + + private static final List expectedAlignmentViewIds = List.of(40L, 41L, 42L, 43L, 44L, 45L); + + private static final List expectedAlignmentViewTeamIds = List.of(4L, 5L, 6L, 8L); + + private static final List expectedAlignmentViewQuarterId = List.of(9L); + + @Test + void getAlignmentsByFiltersShouldReturnListOfAlignmentViews() { + List alignmentViewList = alignmentViewPersistenceService.getAlignmentViewListByQuarterId(9L); + + assertEquals(10, alignmentViewList.size()); + + assertThat(getAlignmentViewIds(alignmentViewList)).hasSameElementsAs(expectedAlignmentViewIds); + assertThat(getAlignmentViewTeamIds(alignmentViewList)).hasSameElementsAs(expectedAlignmentViewTeamIds); + assertThat(getAlignmentViewQuarterIds(alignmentViewList)).hasSameElementsAs(expectedAlignmentViewQuarterId); + } + + @Test + void getAlignmentsByFiltersShouldReturnEmptyListOfAlignmentViewsWhenQuarterNotExisting() { + List alignmentViewList = alignmentViewPersistenceService.getAlignmentViewListByQuarterId(311L); + + assertEquals(0, alignmentViewList.size()); + } + + private List getAlignmentViewIds(List alignmentViewIds) { + return alignmentViewIds.stream().map(AlignmentView::getId).toList(); + } + + private List getAlignmentViewTeamIds(List alignmentViewIds) { + return alignmentViewIds.stream().map(AlignmentView::getTeamId).toList(); + } + + private List getAlignmentViewQuarterIds(List alignmentViewIds) { + return alignmentViewIds.stream().map(AlignmentView::getQuarterId).toList(); + } +} diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/CheckInPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/CheckInPersistenceServiceIT.java index b985c482f5..ab366e16d5 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/CheckInPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/CheckInPersistenceServiceIT.java @@ -108,7 +108,7 @@ void updateKeyResultShouldThrowExceptionWhenAlreadyUpdated() { void getAllCheckInShouldReturnListOfAllCheckIns() { List checkIns = checkInPersistenceService.findAll(); - assertEquals(19, checkIns.size()); + assertEquals(20, checkIns.size()); } @Test diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java index 8bb744a9aa..343224c4b6 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java @@ -68,7 +68,7 @@ void tearDown() { void findAllShouldReturnListOfObjectives() { List objectives = objectivePersistenceService.findAll(); - assertEquals(7, objectives.size()); + assertEquals(14, objectives.size()); } @Test @@ -253,4 +253,19 @@ void countByTeamAndQuarterShouldReturnCountValue() { assertEquals(2, count); } + + @Test + void findObjectiveByQuarterId() { + List objectiveList = objectivePersistenceService.findObjectiveByQuarterId(2L); + + assertEquals(7, objectiveList.size()); + assertEquals("Wir wollen die Kundenzufriedenheit steigern", objectiveList.get(0).getTitle()); + } + + @Test + void findObjectiveByQuarterIdShouldReturnEmptyListWhenQuarterDoesNotExist() { + List objectives = objectivePersistenceService.findObjectiveByQuarterId(12345L); + + assertEquals(0, objectives.size()); + } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java new file mode 100644 index 0000000000..80c1803f2c --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java @@ -0,0 +1,456 @@ +package ch.puzzle.okr.service.validation; + +import ch.puzzle.okr.TestHelper; +import ch.puzzle.okr.dto.ErrorDto; +import ch.puzzle.okr.exception.OkrResponseStatusException; +import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.Team; +import ch.puzzle.okr.models.alignment.KeyResultAlignment; +import ch.puzzle.okr.models.alignment.ObjectiveAlignment; +import ch.puzzle.okr.models.keyresult.KeyResult; +import ch.puzzle.okr.models.keyresult.KeyResultMetric; +import ch.puzzle.okr.service.persistence.AlignmentPersistenceService; +import ch.puzzle.okr.service.persistence.TeamPersistenceService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static ch.puzzle.okr.models.State.DRAFT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static ch.puzzle.okr.TestConstants.*; + +@ExtendWith(MockitoExtension.class) +class AlignmentValidationServiceTest { + + @Mock + AlignmentPersistenceService alignmentPersistenceService; + @Mock + TeamPersistenceService teamPersistenceService; + @Mock + QuarterValidationService quarterValidationService; + @Mock + TeamValidationService teamValidationService; + @Spy + @InjectMocks + private AlignmentValidationService validator; + + Team team1 = Team.Builder.builder().withId(1L).withName(TEAM_PUZZLE).build(); + Team team2 = Team.Builder.builder().withId(2L).withName("BBT").build(); + Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withTeam(team1) + .withState(DRAFT).build(); + Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withTeam(team2) + .withState(DRAFT).build(); + Objective objective3 = Objective.Builder.builder().withId(10L).withTitle("Objective 3").withState(DRAFT).build(); + KeyResult metricKeyResult = KeyResultMetric.Builder.builder().withId(5L).withTitle("KR Title 1").build(); + ObjectiveAlignment objectiveALignment = ObjectiveAlignment.Builder.builder().withId(1L) + .withAlignedObjective(objective1).withTargetObjective(objective2).build(); + ObjectiveAlignment createAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective2) + .withTargetObjective(objective1).build(); + KeyResultAlignment keyResultAlignment = KeyResultAlignment.Builder.builder().withId(6L) + .withAlignedObjective(objective3).withTargetKeyResult(metricKeyResult).build(); + Long quarterId = 1L; + Long teamId = 1L; + List teamIds = List.of(1L, 2L, 3L, 4L); + + @BeforeEach + void setUp() { + Mockito.lenient().when(alignmentPersistenceService.getModelName()).thenReturn("Alignment"); + } + + @Test + void validateOnGetShouldBeSuccessfulWhenValidActionId() { + // act + validator.validateOnGet(1L); + + // assert + verify(validator, times(1)).validateOnGet(1L); + verify(validator, times(1)).throwExceptionWhenIdIsNull(1L); + } + + @Test + void validateOnGetShouldThrowExceptionIfIdIsNull() { + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnGet(null)); + + // assert + verify(validator, times(1)).throwExceptionWhenIdIsNull(null); + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("ATTRIBUTE_NULL", exception.getReason()); + assertEquals(List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("ID", "Alignment"))), exception.getErrors()); + } + + @Test + void validateOnCreateShouldBeSuccessfulWhenAlignmentIsValid() { + // arrange + when(alignmentPersistenceService.findByAlignedObjectiveId(anyLong())).thenReturn(null); + when(teamPersistenceService.findById(1L)).thenReturn(team1); + when(teamPersistenceService.findById(2L)).thenReturn(team2); + + // act + validator.validateOnCreate(createAlignment); + + // assert + verify(validator, times(1)).throwExceptionWhenModelIsNull(createAlignment); + verify(validator, times(1)).throwExceptionWhenIdIsNotNull(null); + verify(alignmentPersistenceService, times(1)).findByAlignedObjectiveId(8L); + verify(validator, times(1)).validate(createAlignment); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenModelIsNull() { + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(null)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("MODEL_NULL", exception.getReason()); + assertEquals(List.of(new ErrorDto("MODEL_NULL", List.of("Alignment"))), exception.getErrors()); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenIdIsNotNull() { + // arrange + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NOT_NULL", List.of("ID", "Alignment"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(keyResultAlignment)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenAlignedObjectiveIsNull() { + // arrange + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withTargetObjective(objective2) + .build(); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("alignedObjectiveId"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(objectiveAlignment)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenTargetObjectiveIsNull() { + // arrange + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective2) + .build(); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetObjectiveId", "8"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(objectiveAlignment)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenTargetKeyResultIsNull() { + // arrange + KeyResultAlignment wrongKeyResultAlignment = KeyResultAlignment.Builder.builder() + .withAlignedObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetKeyResultId", "8"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(wrongKeyResultAlignment)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenAlignedIdIsSameAsTargetId() { + // arrange + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective2) + .withTargetObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("NOT_LINK_YOURSELF", List.of("targetObjectiveId", "8"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(objectiveAlignment)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenAlignmentIsInSameTeamObjective() { + // arrange + when(teamPersistenceService.findById(2L)).thenReturn(team2); + Objective objective = objective1; + objective.setTeam(team2); + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective) + .withTargetObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "2"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(objectiveAlignment)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenAlignmentIsInSameTeamKeyResult() { + // arrange + when(teamPersistenceService.findById(1L)).thenReturn(team1); + KeyResult keyResult = KeyResultMetric.Builder.builder().withId(3L).withTitle("KeyResult 1").withObjective(objective1).build(); + KeyResultAlignment keyResultAlignment1 = KeyResultAlignment.Builder.builder().withAlignedObjective(objective1).withTargetKeyResult(keyResult).build(); + List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "1"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(keyResultAlignment1)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenAlignedObjectiveAlreadyExists() { + // arrange + when(alignmentPersistenceService.findByAlignedObjectiveId(anyLong())).thenReturn(objectiveALignment); + when(teamPersistenceService.findById(1L)).thenReturn(team1); + when(teamPersistenceService.findById(2L)).thenReturn(team2); + ObjectiveAlignment createAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective1).withTargetObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("ALIGNMENT_ALREADY_EXISTS", List.of("alignedObjectiveId", "5"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(createAlignment)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnUpdateShouldBeSuccessfulWhenAlignmentIsValid() { + // arrange + when(teamPersistenceService.findById(1L)).thenReturn(team1); + when(teamPersistenceService.findById(2L)).thenReturn(team2); + + // act + validator.validateOnUpdate(objectiveALignment.getId(), objectiveALignment); + + // assert + verify(validator, times(1)).throwExceptionWhenModelIsNull(objectiveALignment); + verify(validator, times(1)).throwExceptionWhenIdIsNull(objectiveALignment.getId()); + verify(validator, times(1)).validate(objectiveALignment); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenModelIsNull() { + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(1L, null)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("MODEL_NULL", exception.getReason()); + assertEquals(List.of(new ErrorDto("MODEL_NULL", List.of("Alignment"))), exception.getErrors()); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenIdIsNull() { + // arrange + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().build(); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(null, objectiveAlignment)); + + // assert + verify(validator, times(1)).throwExceptionWhenModelIsNull(objectiveAlignment); + verify(validator, times(1)).throwExceptionWhenIdIsNull(null); + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("ATTRIBUTE_NULL", exception.getReason()); + assertEquals(List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("ID", "Alignment"))), exception.getErrors()); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenAlignedObjectiveIsNull() { + // arrange + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withId(3L) + .withTargetObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("alignedObjectiveId"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(3L, objectiveAlignment)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenTargetObjectiveIsNull() { + // arrange + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withId(3L) + .withAlignedObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetObjectiveId", "8"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(3L, objectiveAlignment)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenTargetKeyResultIsNull() { + // arrange + KeyResultAlignment wrongKeyResultAlignment = KeyResultAlignment.Builder.builder().withId(3L) + .withAlignedObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetKeyResultId", "8"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(3L, wrongKeyResultAlignment)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenAlignedIdIsSameAsTargetId() { + // arrange + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withId(3L) + .withAlignedObjective(objective2).withTargetObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("NOT_LINK_YOURSELF", List.of("targetObjectiveId", "8"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(3L, objectiveAlignment)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenAlignmentIsInSameTeamObjective() { + // arrange + when(teamPersistenceService.findById(2L)).thenReturn(team2); + Objective objective = objective1; + objective.setTeam(team2); + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withId(3L) + .withAlignedObjective(objective).withTargetObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "2"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(2L, objectiveAlignment)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenAlignmentIsInSameTeamKeyResult() { + // arrange + when(teamPersistenceService.findById(1L)).thenReturn(team1); + KeyResult keyResult = KeyResultMetric.Builder.builder().withId(3L).withTitle("KeyResult 1").withObjective(objective1).build(); + KeyResultAlignment keyResultAlignment1 = KeyResultAlignment.Builder.builder().withId(2L).withAlignedObjective(objective1).withTargetKeyResult(keyResult).build(); + List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "1"))); + + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(2L, keyResultAlignment1)); + + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnDeleteShouldBeSuccessfulWhenValidAlignmentId() { + // act + validator.validateOnDelete(3L); + + // assert + verify(validator, times(1)).throwExceptionWhenIdIsNull(3L); + } + + @Test + void validateOnDeleteShouldThrowExceptionIfAlignmentIdIsNull() { + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnDelete(null)); + + // assert + verify(validator, times(1)).throwExceptionWhenIdIsNull(null); + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("ATTRIBUTE_NULL", exception.getReason()); + assertEquals(List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("ID", "Alignment"))), exception.getErrors()); + } + + @Test + void validateOnGetShouldCallQuarterValidator() { + validator.validateQuarter(quarterId); + verify(quarterValidationService, times(1)).validateOnGet(quarterId); + verify(quarterValidationService, times(1)).doesEntityExist(quarterId); + } + + @Test + void validateOnGetShouldCallTeamValidator() { + validator.validateTeam(teamId); + verify(teamValidationService, times(1)).validateOnGet(teamId); + verify(teamValidationService, times(1)).doesEntityExist(teamId); + } + + @Test + void validateOnGetShouldCallQuarterValidatorAndTeamValidator() { + validator.validateOnAlignmentGet(quarterId, teamIds); + verify(quarterValidationService, times(1)).validateOnGet(quarterId); + verify(quarterValidationService, times(1)).doesEntityExist(quarterId); + verify(teamValidationService, times(teamIds.size())).validateOnGet(anyLong()); + verify(teamValidationService, times(teamIds.size())).doesEntityExist(anyLong()); + } +} diff --git a/backend/src/test/resources/repositoriesAndPersistenceServices.csv b/backend/src/test/resources/repositoriesAndPersistenceServices.csv index 36f53411b3..671175a7f6 100644 --- a/backend/src/test/resources/repositoriesAndPersistenceServices.csv +++ b/backend/src/test/resources/repositoriesAndPersistenceServices.csv @@ -1,7 +1,6 @@ repository,persistenceService,validationService ActionRepository,ActionPersistenceService,ActionValidationService AlignmentRepository,AlignmentPersistenceService,"" -AlignmentSelectionRepository,AlignmentSelectionPersistenceService,"" CheckInRepository,CheckInPersistenceService,CheckInValidationService CompletedRepository,CompletedPersistenceService,CompletedValidationService KeyResultRepository,KeyResultPersistenceService,KeyResultValidationService diff --git a/frontend/cypress/e2e/checkIn.cy.ts b/frontend/cypress/e2e/checkIn.cy.ts index 937a6f51ca..e3df4724d0 100644 --- a/frontend/cypress/e2e/checkIn.cy.ts +++ b/frontend/cypress/e2e/checkIn.cy.ts @@ -259,8 +259,8 @@ describe('OKR Check-in e2e tests', () => { it(`Should display confirm dialog when creating checkin on draft objective`, () => { cy.getByTestId('add-objective').first().click(); - cy.fillOutObjective('draft objective title', 'safe-draft', '3'); - cy.visit('/?quarter=3'); + cy.fillOutObjective('draft objective title', 'safe-draft', '1'); + cy.visit('/?quarter=1'); cy.contains('draft objective title').first().parentsUntil('#objective-column').last().focus(); cy.tabForwardUntil('[data-testId="add-keyResult"]'); @@ -290,8 +290,8 @@ describe('OKR Check-in e2e tests', () => { it(`Should not display last value div if last checkin is not present`, () => { cy.getByTestId('add-objective').first().click(); - cy.fillOutObjective('new objective', 'safe', '3'); - cy.visit('/?quarter=3'); + cy.fillOutObjective('new objective', 'safe', '1'); + cy.visit('/?quarter=1'); cy.contains('new objective').first().parentsUntil('#objective-column').last().focus(); cy.tabForwardUntil('[data-testId="add-keyResult"]'); diff --git a/frontend/cypress/e2e/crud.cy.ts b/frontend/cypress/e2e/crud.cy.ts index bbe1dec6cc..d72430259f 100644 --- a/frontend/cypress/e2e/crud.cy.ts +++ b/frontend/cypress/e2e/crud.cy.ts @@ -12,8 +12,8 @@ describe('CRUD operations', () => { ].forEach(([objectiveTitle, buttonTestId, icon]) => { it(`Create objective, no keyresults`, () => { cy.getByTestId('add-objective').first().click(); - cy.fillOutObjective(objectiveTitle, buttonTestId, '3'); - cy.visit('/?quarter=3'); + cy.fillOutObjective(objectiveTitle, buttonTestId, '1'); + cy.visit('/?quarter=1'); const objective = cy.contains(objectiveTitle).first().parentsUntil('#objective-column').last(); objective.getByTestId('objective-state').should('have.attr', 'src', `assets/icons/${icon}`); }); @@ -25,7 +25,7 @@ describe('CRUD operations', () => { ].forEach(([objectiveTitle, buttonTestId, icon]) => { it(`Create objective, no keyresults`, () => { cy.getByTestId('add-objective').first().click(); - cy.fillOutObjective(objectiveTitle, buttonTestId, '3', '', true); + cy.fillOutObjective(objectiveTitle, buttonTestId, '1', '', true); cy.contains('Key Result erfassen'); }); }); @@ -42,8 +42,8 @@ describe('CRUD operations', () => { it(`Create objective, cancel`, () => { const objectiveTitle = 'this is a canceled objective'; cy.getByTestId('add-objective').first().click(); - cy.fillOutObjective(objectiveTitle, 'cancel', '3'); - cy.visit('/?quarter=3'); + cy.fillOutObjective(objectiveTitle, 'cancel', '1'); + cy.visit('/?quarter=1'); cy.contains(objectiveTitle).should('not.exist'); }); diff --git a/frontend/cypress/e2e/diagram.cy.ts b/frontend/cypress/e2e/diagram.cy.ts new file mode 100644 index 0000000000..76e53bb336 --- /dev/null +++ b/frontend/cypress/e2e/diagram.cy.ts @@ -0,0 +1,79 @@ +import * as users from '../fixtures/users.json'; + +describe('OKR diagram e2e tests', () => { + describe('tests via click', () => { + beforeEach(() => { + cy.loginAsUser(users.gl); + cy.visit('/?quarter=10'); + cy.getByTestId('add-objective').first().click(); + cy.fillOutObjective('An Objective for Testing', 'safe-draft', '10'); + }); + + it('Can switch to diagram with the tab-switch', () => { + cy.get('h1:contains(Puzzle ITC)').should('have.length', 1); + cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); + cy.contains('Overview'); + cy.contains('Network'); + cy.contains('An Objective for Testing'); + + cy.getByTestId('diagramTab').first().click(); + + cy.contains('Kein Alignment vorhanden'); + cy.get('h1:visible:contains(Puzzle ITC)').should('have.length', 0); + cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); + cy.getByTestId('objective').should('not.be.visible'); + + cy.getByTestId('overviewTab').first().click(); + + cy.get('h1:contains(Puzzle ITC)').should('have.length', 1); + cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); + cy.contains('An Objective for Testing'); + cy.getByTestId('objective').should('be.visible'); + }); + + it('Can switch to diagram and the filter stay the same', () => { + cy.get('h1:contains(Puzzle ITC)').should('have.length', 1); + cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); + cy.contains('Overview'); + cy.contains('Network'); + cy.contains('An Objective for Testing'); + cy.getByTestId('quarterFilter').should('contain', 'GJ 24/25-Q1'); + cy.get('mat-chip:visible:contains("Puzzle ITC")') + .should('have.css', 'background-color') + .and('eq', 'rgb(30, 90, 150)'); + cy.get('mat-chip:visible:contains("/BBT")') + .should('have.css', 'background-color') + .and('eq', 'rgb(255, 255, 255)'); + + cy.get('mat-chip:visible:contains("/BBT")').click(); + cy.get('mat-chip:visible:contains("Puzzle ITC")').click(); + + cy.getByTestId('diagramTab').first().click(); + + cy.contains('Kein Alignment vorhanden'); + cy.get('h1:contains(Puzzle ITC)').should('have.length', 0); + cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); + cy.getByTestId('quarterFilter').should('contain', 'GJ 24/25-Q1'); + cy.getByTestId('objective').should('have.length', 0); + cy.get('mat-chip:visible:contains("/BBT")').should('have.css', 'background-color').and('eq', 'rgb(30, 90, 150)'); + cy.get('mat-chip:visible:contains("Puzzle ITC")') + .should('have.css', 'background-color') + .and('eq', 'rgb(255, 255, 255)'); + cy.get('mat-chip:visible:contains("Puzzle ITC")').click(); + + cy.getByTestId('quarterFilter').first().focus(); + cy.focused().realPress('ArrowDown'); + + cy.getByTestId('quarterFilter').should('contain', 'GJ 23/24-Q4'); + cy.get('canvas').should('have.length', 3); + + cy.getByTestId('overviewTab').first().click(); + + cy.get('mat-chip:visible:contains("/BBT")').should('have.css', 'background-color').and('eq', 'rgb(30, 90, 150)'); + cy.get('mat-chip:visible:contains("Puzzle ITC")') + .should('have.css', 'background-color') + .and('eq', 'rgb(30, 90, 150)'); + cy.getByTestId('quarterFilter').should('contain', 'GJ 23/24-Q4'); + }); + }); +}); diff --git a/frontend/cypress/e2e/duplicated-scoring.cy.ts b/frontend/cypress/e2e/duplicated-scoring.cy.ts index d1152ced59..5cc5565794 100644 --- a/frontend/cypress/e2e/duplicated-scoring.cy.ts +++ b/frontend/cypress/e2e/duplicated-scoring.cy.ts @@ -22,8 +22,8 @@ describe('e2e test for scoring adjustment on objective duplicate', () => { cy.get('.objective').first().getByTestId('three-dot-menu').click(); cy.get('.mat-mdc-menu-content').contains('Objective duplizieren').click(); - cy.fillOutObjective('A duplicated Objective for this tool', 'safe', '3'); - cy.visit('/?quarter=3'); + cy.fillOutObjective('A duplicated Objective for this tool', 'safe', '1'); + cy.visit('/?quarter=1'); let scoringBlock1 = cy .getByTestId('objective') diff --git a/frontend/cypress/e2e/objective-alignment.cy.ts b/frontend/cypress/e2e/objective-alignment.cy.ts new file mode 100644 index 0000000000..22770ba7ff --- /dev/null +++ b/frontend/cypress/e2e/objective-alignment.cy.ts @@ -0,0 +1,201 @@ +import * as users from '../fixtures/users.json'; + +describe('OKR Objective Alignment e2e tests', () => { + beforeEach(() => { + cy.loginAsUser(users.gl); + cy.visit('/?quarter=2'); + }); + + it(`Create Objective with an Alignment`, () => { + cy.getByTestId('add-objective').first().click(); + + cy.getByTestId('title').first().clear().type('Objective with new alignment'); + cy.getByTestId('alignmentInput').first().should('have.attr', 'placeholder', 'Bezug wählen'); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); + + cy.getByTestId('safe').click(); + + cy.contains('Objective with new alignment'); + cy.getByTestId('objective') + .filter(':contains("Objective with new alignment")') + .last() + .getByTestId('three-dot-menu') + .click() + .wait(500) + .get('.objective-menu-option') + .contains('Objective bearbeiten') + .click(); + + cy.getByTestId('alignmentInput') + .first() + .should('have.value', 'O - Als BBT wollen wir den Arbeitsalltag der Members von Puzzle ITC erleichtern.'); + }); + + it(`Update alignment of Objective`, () => { + cy.getByTestId('add-objective').first().click(); + + cy.getByTestId('title').first().clear().type('We change alignment of this Objective'); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); + cy.getByTestId('safe').click(); + + cy.contains('We change alignment of this Objective'); + cy.getByTestId('objective') + .filter(':contains("We change alignment of this Objective")') + .last() + .getByTestId('three-dot-menu') + .click() + .wait(500) + .get('.objective-menu-option') + .contains('Objective bearbeiten') + .click(); + + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('Delete'); + cy.realPress('ArrowDown'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); + cy.getByTestId('safe').click(); + + cy.getByTestId('objective') + .filter(':contains("We change alignment of this Objective")') + .last() + .getByTestId('three-dot-menu') + .click() + .wait(500) + .get('.objective-menu-option') + .contains('Objective bearbeiten') + .click(); + + cy.getByTestId('alignmentInput') + .first() + .should('have.value', 'KR - Das BBT hilft den Membern 20% mehr beim Töggelen'); + }); + + it(`Delete alignment of Objective`, () => { + cy.getByTestId('add-objective').first().click(); + + cy.getByTestId('title').first().clear().type('We delete the alignment'); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); + cy.getByTestId('safe').click(); + + cy.contains('We delete the alignment'); + cy.getByTestId('objective') + .filter(':contains("We delete the alignment")') + .last() + .getByTestId('three-dot-menu') + .click() + .wait(500) + .get('.objective-menu-option') + .contains('Objective bearbeiten') + .click(); + + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('Delete'); + cy.tabForward(); + cy.getByTestId('safe').click(); + + cy.getByTestId('objective') + .filter(':contains("We delete the alignment")') + .last() + .getByTestId('three-dot-menu') + .click() + .wait(500) + .get('.objective-menu-option') + .contains('Objective bearbeiten') + .click(); + + cy.getByTestId('alignmentInput').first().should('have.attr', 'placeholder', 'Bezug wählen'); + }); + + it(`Alignment Possibilities change when quarter change`, () => { + cy.visit('/?quarter=1'); + + cy.get('mat-chip:visible:contains("Alle")').click(); + cy.get('mat-chip:visible:contains("Alle")').click(); + cy.get('mat-chip:visible:contains("/BBT")').click(); + + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('We can link later on this'); + cy.getByTestId('safe').click(); + + cy.get('mat-chip:visible:contains("Alle")').click(); + + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('There is my other alignment'); + cy.getByTestId('alignmentInput').first().should('have.attr', 'placeholder', 'Bezug wählen'); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); + + cy.getByTestId('alignmentInput') + .first() + .invoke('val') + .then((val) => { + const selectValue = val; + cy.getByTestId('quarterSelect').select('GJ 23/24-Q1'); + cy.getByTestId('title').first().clear().type('There is our other alignment'); + + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); + + cy.getByTestId('alignmentInput').first().should('not.have.value', selectValue); + + cy.getByTestId('cancel').click(); + + cy.visit('/?quarter=2'); + + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('Quarter change objective'); + + cy.get('select#quarter').select('GJ 22/23-Q4'); + cy.getByTestId('title').first().clear().type('A new title'); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); + + cy.getByTestId('alignmentInput').first().should('have.value', selectValue); + }); + }); + + it(`Correct placeholder`, () => { + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('This is an objective which we dont save'); + cy.getByTestId('alignmentInput').first().should('have.attr', 'placeholder', 'Bezug wählen'); + + cy.getByTestId('quarterSelect').select('GJ 23/24-Q3'); + cy.getByTestId('title').first().clear().type('We changed the quarter'); + + cy.getByTestId('alignmentInput').first().should('have.attr', 'placeholder', 'Kein Alignment vorhanden'); + }); + + it(`Correct filtering`, () => { + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('Die urs steigt'); + cy.getByTestId('safe').click(); + + cy.scrollTo('top'); + cy.get('mat-chip:visible:contains("Puzzle ITC")').click(); + cy.get('mat-chip:visible:contains("/BBT")').click(); + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('Ein alignment objective'); + + cy.getByTestId('alignmentInput').clear().type('urs'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); + + cy.getByTestId('alignmentInput').first().should('have.value', 'O - Die urs steigt'); + + cy.getByTestId('alignmentInput').clear().type('urs'); + cy.realPress('ArrowDown'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); + cy.getByTestId('alignmentInput').first().should('have.value', 'KR - Steigern der URS um 25%'); + }); +}); diff --git a/frontend/cypress/e2e/tab.cy.ts b/frontend/cypress/e2e/tab.cy.ts index f20f3f4889..bca8d6831c 100644 --- a/frontend/cypress/e2e/tab.cy.ts +++ b/frontend/cypress/e2e/tab.cy.ts @@ -213,6 +213,7 @@ describe('Tab workflow tests', () => { editInputFields('Edited by Cypress too'); cy.tabForward(); cy.tabForward(); + cy.tabForward(); cy.realPress('Enter'); cy.contains('Edited by Cypress'); }); @@ -231,6 +232,7 @@ describe('Tab workflow tests', () => { cy.focused().contains('GJ'); cy.realPress('ArrowDown'); cy.tabForward(); + cy.tabForward(); cy.focused().contains('Speichern'); cy.realPress('Enter'); cy.wait(500); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 62f5abeef5..023d96e1d8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,6 +23,7 @@ "@ngx-translate/http-loader": "^8.0.0", "angular-oauth2-oidc": "^17.0.0", "bootstrap": "^5.3.2", + "cytoscape": "^3.28.1", "moment": "^2.30.1", "ngx-toastr": "^18.0.0", "rxjs": "^7.8.1", @@ -35,6 +36,7 @@ "@angular/compiler-cli": "^17.0.6", "@cypress/schematic": "^2.5.1", "@cypress/skip-test": "^2.6.1", + "@types/cytoscape": "^3.21.0", "@types/jest": "^29.5.11", "cypress": "^13.6.3", "cypress-real-events": "^1.11.0", @@ -5726,6 +5728,12 @@ "@types/node": "*" } }, + "node_modules/@types/cytoscape": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.0.tgz", + "integrity": "sha512-RN5SPiyVDpUP+LoOlxxlOYAMzkE7iuv3gA1jt3Hx2qTwArpZVPPdO+SI0hUj49OAn4QABR7JK9Gi0hibzGE0Aw==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.56.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", @@ -8289,6 +8297,18 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/cytoscape": { + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.28.1.tgz", + "integrity": "sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==", + "dependencies": { + "heap": "^0.2.6", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -9753,6 +9773,11 @@ "node": ">= 0.4" } }, + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==" + }, "node_modules/hosted-git-info": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", @@ -12870,8 +12895,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.debounce": { "version": "4.0.8", diff --git a/frontend/package.json b/frontend/package.json index 378772ff9f..8e0c3b5641 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "@ngx-translate/http-loader": "^8.0.0", "angular-oauth2-oidc": "^17.0.0", "bootstrap": "^5.3.2", + "cytoscape": "^3.28.1", "moment": "^2.30.1", "ngx-toastr": "^18.0.0", "rxjs": "^7.8.1", @@ -48,6 +49,7 @@ "@angular/compiler-cli": "^17.0.6", "@cypress/schematic": "^2.5.1", "@cypress/skip-test": "^2.6.1", + "@types/cytoscape": "^3.21.0", "@types/jest": "^29.5.11", "cypress": "^13.6.3", "cypress-real-events": "^1.11.0", diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 59dab8cddb..3986b7b0d6 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -72,6 +72,7 @@ import { CdkDrag, CdkDragHandle, CdkDropList } from '@angular/cdk/drag-drop'; import { TeamManagementComponent } from './shared/dialog/team-management/team-management.component'; import { KeyresultDialogComponent } from './shared/dialog/keyresult-dialog/keyresult-dialog.component'; import { CustomizationService } from './shared/services/customization.service'; +import { DiagramComponent } from './diagram/diagram.component'; function initOauthFactory(configService: ConfigService, oauthService: OAuthService) { return async () => { @@ -134,6 +135,7 @@ export const MY_FORMATS = { ActionPlanComponent, TeamManagementComponent, KeyresultDialogComponent, + DiagramComponent, ], imports: [ CommonModule, diff --git a/frontend/src/app/diagram/diagram.component.html b/frontend/src/app/diagram/diagram.component.html new file mode 100644 index 0000000000..9e34e3ff55 --- /dev/null +++ b/frontend/src/app/diagram/diagram.component.html @@ -0,0 +1,9 @@ +
+ +
+

Kein Alignment vorhanden

+ +
diff --git a/frontend/src/app/diagram/diagram.component.scss b/frontend/src/app/diagram/diagram.component.scss new file mode 100644 index 0000000000..e19ddd7398 --- /dev/null +++ b/frontend/src/app/diagram/diagram.component.scss @@ -0,0 +1,9 @@ +#cy { + margin-top: 30px; + width: calc(100vw - 60px); + height: calc(100vh - 360px); +} + +.puzzle-logo { + filter: invert(38%) sepia(31%) saturate(216%) hue-rotate(167deg) brightness(96%) contrast(85%); +} diff --git a/frontend/src/app/diagram/diagram.component.spec.ts b/frontend/src/app/diagram/diagram.component.spec.ts new file mode 100644 index 0000000000..dda3c6e3aa --- /dev/null +++ b/frontend/src/app/diagram/diagram.component.spec.ts @@ -0,0 +1,201 @@ +import { DiagramComponent } from './diagram.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { AlignmentLists } from '../shared/types/model/AlignmentLists'; +import { alignmentLists, alignmentListsKeyResult, keyResult, keyResultMetric } from '../shared/testData'; +import * as functions from './svgGeneration'; +import { getDraftIcon, getNotSuccessfulIcon, getOnGoingIcon, getSuccessfulIcon } from './svgGeneration'; +import { of } from 'rxjs'; +import { KeyresultService } from '../shared/services/keyresult.service'; +import { ParseUnitValuePipe } from '../shared/pipes/parse-unit-value/parse-unit-value.pipe'; + +const keyResultServiceMock = { + getFullKeyResult: jest.fn(), +}; + +describe('DiagramComponent', () => { + let component: DiagramComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DiagramComponent], + imports: [HttpClientTestingModule], + providers: [{ provide: KeyresultService, useValue: keyResultServiceMock }, ParseUnitValuePipe], + }); + fixture = TestBed.createComponent(DiagramComponent); + URL.createObjectURL = jest.fn(); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call cleanUpDiagram when ngOnDestroy gets called', () => { + jest.spyOn(component, 'cleanUpDiagram'); + + component.ngOnDestroy(); + expect(component.cleanUpDiagram).toHaveBeenCalled(); + }); + + it('should call generateElements if alignmentData is present', () => { + jest.spyOn(component, 'generateNodes'); + + component.prepareDiagramData(alignmentLists); + expect(component.generateNodes).toHaveBeenCalled(); + expect(component.alignmentDataCache?.alignmentObjectDtoList.length).not.toEqual(0); + }); + + it('should not call generateElements if alignmentData is empty', () => { + jest.spyOn(component, 'generateNodes'); + + let alignmentLists: AlignmentLists = { + alignmentObjectDtoList: [], + alignmentConnectionDtoList: [], + }; + + component.prepareDiagramData(alignmentLists); + expect(component.generateNodes).not.toHaveBeenCalled(); + }); + + it('should call prepareDiagramData when Subject receives new data', () => { + jest.spyOn(component, 'cleanUpDiagram'); + jest.spyOn(component, 'prepareDiagramData'); + + component.ngAfterViewInit(); + component.alignmentData$.next(alignmentLists); + + expect(component.cleanUpDiagram).toHaveBeenCalled(); + expect(component.prepareDiagramData).toHaveBeenCalledWith(alignmentLists); + }); + + it('should generate correct diagramData for Objectives', () => { + jest.spyOn(component, 'generateConnections'); + jest.spyOn(component, 'generateDiagram'); + jest.spyOn(component, 'generateObjectiveSVG').mockReturnValue('Test.svg'); + + let edge = { + data: { + source: 'Ob1', + target: 'Ob2', + }, + }; + let element1 = { + data: { + id: 'Ob1', + }, + style: { + 'background-image': 'Test.svg', + }, + }; + let element2 = { + data: { + id: 'Ob2', + }, + style: { + 'background-image': 'Test.svg', + }, + }; + + let diagramElements: any[] = [element1, element2]; + let edges: any[] = [edge]; + + component.generateNodes(alignmentLists); + + expect(component.generateConnections).toHaveBeenCalled(); + expect(component.diagramData).toEqual(diagramElements.concat(edges)); + }); + + it('should generate correct diagramData for KeyResult Metric', () => { + jest.spyOn(component, 'generateConnections'); + jest.spyOn(component, 'generateDiagram'); + jest.spyOn(component, 'generateObjectiveSVG').mockReturnValue('TestObjective.svg'); + jest.spyOn(component, 'generateKeyResultSVG').mockReturnValue('TestKeyResult.svg'); + jest.spyOn(keyResultServiceMock, 'getFullKeyResult').mockReturnValue(of(keyResultMetric)); + + let diagramData: any[] = getReturnedAlignmentDataKeyResult(); + + component.generateNodes(alignmentListsKeyResult); + + expect(component.generateConnections).toHaveBeenCalled(); + expect(component.diagramData).toEqual(diagramData); + }); + + it('should generate correct diagramData for KeyResult Ordinal', () => { + jest.spyOn(component, 'generateConnections'); + jest.spyOn(component, 'generateDiagram'); + jest.spyOn(component, 'generateObjectiveSVG').mockReturnValue('TestObjective.svg'); + jest.spyOn(component, 'generateKeyResultSVG').mockReturnValue('TestKeyResult.svg'); + jest.spyOn(keyResultServiceMock, 'getFullKeyResult').mockReturnValue(of(keyResult)); + + let diagramData: any[] = getReturnedAlignmentDataKeyResult(); + + component.generateNodes(alignmentListsKeyResult); + + expect(component.generateConnections).toHaveBeenCalled(); + expect(component.diagramData).toEqual(diagramData); + }); + + it('should generate correct SVGs for Objective', () => { + jest.spyOn(functions, 'generateObjectiveSVG'); + + component.generateObjectiveSVG('Title 1', 'Team name 1', 'ONGOING'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 1', 'Team name 1', getOnGoingIcon); + + component.generateObjectiveSVG('Title 2', 'Team name 2', 'SUCCESSFUL'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 2', 'Team name 2', getSuccessfulIcon); + + component.generateObjectiveSVG('Title 3', 'Team name 3', 'NOTSUCCESSFUL'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 3', 'Team name 3', getNotSuccessfulIcon); + + component.generateObjectiveSVG('Title 4', 'Team name 4', 'DRAFT'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 4', 'Team name 4', getDraftIcon); + }); + + it('should generate correct SVGs for KeyResult', () => { + jest.spyOn(functions, 'generateObjectiveSVG'); + + component.generateObjectiveSVG('Title 1', 'Team name 1', 'ONGOING'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 1', 'Team name 1', getOnGoingIcon); + + component.generateObjectiveSVG('Title 2', 'Team name 2', 'SUCCESSFUL'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 2', 'Team name 2', getSuccessfulIcon); + + component.generateObjectiveSVG('Title 3', 'Team name 3', 'NOTSUCCESSFUL'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 3', 'Team name 3', getNotSuccessfulIcon); + + component.generateObjectiveSVG('Title 4', 'Team name 4', 'DRAFT'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 4', 'Team name 4', getDraftIcon); + }); +}); + +function getReturnedAlignmentDataKeyResult(): any[] { + let edge = { + data: { + source: 'Ob3', + target: 'KR102', + }, + }; + let element1 = { + data: { + id: 'Ob3', + }, + style: { + 'background-image': 'TestObjective.svg', + }, + }; + let element2 = { + data: { + id: 'KR102', + }, + style: { + 'background-image': 'TestKeyResult.svg', + }, + }; + + let diagramElements: any[] = [element1, element2]; + let edges: any[] = [edge]; + + return diagramElements.concat(edges); +} diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts new file mode 100644 index 0000000000..6d35f71bad --- /dev/null +++ b/frontend/src/app/diagram/diagram.component.ts @@ -0,0 +1,272 @@ +import { AfterViewInit, Component, Input, OnDestroy } from '@angular/core'; +import { map, Observable, of, Subject, zip } from 'rxjs'; +import { AlignmentLists } from '../shared/types/model/AlignmentLists'; +import cytoscape from 'cytoscape'; +import { + generateKeyResultSVG, + generateNeutralKeyResultSVG, + generateObjectiveSVG, + getDraftIcon, + getNotSuccessfulIcon, + getOnGoingIcon, + getSuccessfulIcon, +} from './svgGeneration'; +import { KeyresultService } from '../shared/services/keyresult.service'; +import { KeyResult } from '../shared/types/model/KeyResult'; +import { KeyResultMetric } from '../shared/types/model/KeyResultMetric'; +import { calculateCurrentPercentage } from '../shared/common'; +import { KeyResultOrdinal } from '../shared/types/model/KeyResultOrdinal'; +import { Router } from '@angular/router'; +import { AlignmentObject } from '../shared/types/model/AlignmentObject'; +import { AlignmentConnection } from '../shared/types/model/AlignmentConnection'; +import { Zone } from '../shared/types/enums/Zone'; +import { ObjectiveState } from '../shared/types/enums/ObjectiveState'; +import { RefreshDataService } from '../shared/services/refresh-data.service'; + +@Component({ + selector: 'app-diagram', + templateUrl: './diagram.component.html', + styleUrl: './diagram.component.scss', +}) +export class DiagramComponent implements AfterViewInit, OnDestroy { + @Input() + public alignmentData$: Subject = new Subject(); + cy!: cytoscape.Core; + diagramData: any[] = []; + alignmentDataCache: AlignmentLists | null = null; + reloadRequired: boolean | null | undefined = false; + + constructor( + private keyResultService: KeyresultService, + private refreshDataService: RefreshDataService, + private router: Router, + ) {} + + ngAfterViewInit(): void { + this.refreshDataService.reloadAlignmentSubject.subscribe((value: boolean | null | undefined): void => { + this.reloadRequired = value; + }); + + this.alignmentData$.subscribe((alignmentData: AlignmentLists): void => { + if (this.reloadRequired == true || JSON.stringify(this.alignmentDataCache) !== JSON.stringify(alignmentData)) { + this.reloadRequired = undefined; + this.alignmentDataCache = alignmentData; + this.diagramData = []; + this.cleanUpDiagram(); + this.prepareDiagramData(alignmentData); + } + }); + } + + ngOnDestroy(): void { + this.cleanUpDiagram(); + this.alignmentData$.unsubscribe(); + this.refreshDataService.reloadAlignmentSubject.unsubscribe(); + } + + generateDiagram(): void { + this.cy = cytoscape({ + container: document.getElementById('cy'), + elements: this.diagramData, + + zoom: 1, + zoomingEnabled: true, + userZoomingEnabled: true, + wheelSensitivity: 0.3, + + style: [ + { + selector: '[id^="Ob"]', + style: { + height: 160, + width: 160, + }, + }, + { + selector: '[id^="KR"]', + style: { + height: 120, + width: 120, + }, + }, + { + selector: 'edge', + style: { + width: 1, + 'line-color': '#000000', + 'target-arrow-color': '#000000', + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', + }, + }, + ], + + layout: { + name: 'cose', + }, + }); + + this.cy.on('tap', 'node', (evt: cytoscape.EventObject) => { + let node = evt.target; + node.style({ + 'border-width': 0, + }); + + let type: string = node.id().charAt(0) == 'O' ? 'objective' : 'keyresult'; + this.router.navigate([type.toLowerCase(), node.id().substring(2)]); + }); + + this.cy.on('mouseover', 'node', (evt: cytoscape.EventObject) => { + let node = evt.target; + node.style({ + 'border-color': '#1E5A96', + 'border-width': 2, + }); + }); + + this.cy.on('mouseout', 'node', (evt: cytoscape.EventObject) => { + evt.target.style({ + 'border-width': 0, + }); + }); + } + + prepareDiagramData(alignmentData: AlignmentLists): void { + if (alignmentData.alignmentObjectDtoList.length != 0) { + this.generateNodes(alignmentData); + } + } + + generateNodes(alignmentData: AlignmentLists): void { + let observableArray: any[] = []; + let diagramElements: any[] = []; + alignmentData.alignmentObjectDtoList.forEach((alignmentObject: AlignmentObject) => { + if (alignmentObject.objectType == 'objective') { + let node = { + data: { + id: 'Ob' + alignmentObject.objectId, + }, + style: { + 'background-image': this.generateObjectiveSVG( + alignmentObject.objectTitle, + alignmentObject.objectTeamName, + alignmentObject.objectState!, + ), + }, + }; + diagramElements.push(node); + observableArray.push(of(node)); + } else { + let observable: Observable = this.keyResultService.getFullKeyResult(alignmentObject.objectId).pipe( + map((keyResult: KeyResult) => { + let keyResultState: string | undefined; + + if (keyResult.keyResultType == 'metric') { + let metricKeyResult: KeyResultMetric = keyResult as KeyResultMetric; + let percentage: number = calculateCurrentPercentage(metricKeyResult); + keyResultState = this.generateMetricKeyResultState(percentage); + } else { + let ordinalKeyResult: KeyResultOrdinal = keyResult as KeyResultOrdinal; + keyResultState = ordinalKeyResult.lastCheckIn?.value.toString(); + } + let element = this.generateKeyResultElement(alignmentObject, keyResultState); + diagramElements.push(element); + }), + ); + observableArray.push(observable); + } + }); + + zip(observableArray).subscribe(async () => { + await this.generateConnections(alignmentData, diagramElements); + }); + } + + generateMetricKeyResultState(percentage: number): string | undefined { + let keyResultState: string | undefined; + if (percentage < 30) { + keyResultState = 'FAIL'; + } else if (percentage < 70) { + keyResultState = 'COMMIT'; + } else if (percentage < 100) { + keyResultState = 'TARGET'; + } else if (percentage >= 100) { + keyResultState = 'STRETCH'; + } else { + keyResultState = undefined; + } + return keyResultState; + } + + generateKeyResultElement(alignmentObject: AlignmentObject, keyResultState: string | undefined) { + return { + data: { + id: 'KR' + alignmentObject.objectId, + }, + style: { + 'background-image': this.generateKeyResultSVG( + alignmentObject.objectTitle, + alignmentObject.objectTeamName, + keyResultState, + ), + }, + }; + } + + async generateConnections(alignmentData: AlignmentLists, diagramElements: any[]) { + let edges: any[] = []; + alignmentData.alignmentConnectionDtoList.forEach((alignmentConnection: AlignmentConnection) => { + let edge = { + data: { + source: 'Ob' + alignmentConnection.alignedObjectiveId, + target: + alignmentConnection.targetKeyResultId == null + ? 'Ob' + alignmentConnection.targetObjectiveId + : 'KR' + alignmentConnection.targetKeyResultId, + }, + }; + edges.push(edge); + }); + this.diagramData = diagramElements.concat(edges); + + // Sometimes the DOM Element #cy is not ready when cytoscape tries to generate the diagram + // To avoid this, we use here a setTimeout() + setTimeout(() => this.generateDiagram(), 0); + } + + generateObjectiveSVG(title: string, teamName: string, state: string): string { + switch (state) { + case ObjectiveState.ONGOING: + return generateObjectiveSVG(title, teamName, getOnGoingIcon); + case ObjectiveState.SUCCESSFUL: + return generateObjectiveSVG(title, teamName, getSuccessfulIcon); + case ObjectiveState.NOTSUCCESSFUL: + return generateObjectiveSVG(title, teamName, getNotSuccessfulIcon); + default: + return generateObjectiveSVG(title, teamName, getDraftIcon); + } + } + + generateKeyResultSVG(title: string, teamName: string, state: string | undefined): string { + switch (state) { + case Zone.FAIL: + return generateKeyResultSVG(title, teamName, '#BA3838', 'white'); + case Zone.COMMIT: + return generateKeyResultSVG(title, teamName, '#FFD600', 'black'); + case Zone.TARGET: + return generateKeyResultSVG(title, teamName, '#1E8A29', 'black'); + case Zone.STRETCH: + return generateKeyResultSVG(title, teamName, '#1E5A96', 'white'); + default: + return generateNeutralKeyResultSVG(title, teamName); + } + } + + cleanUpDiagram() { + if (this.cy) { + this.cy.edges().remove(); + this.cy.nodes().remove(); + this.cy.removeAllListeners(); + } + } +} diff --git a/frontend/src/app/diagram/svgGeneration.ts b/frontend/src/app/diagram/svgGeneration.ts new file mode 100644 index 0000000000..e08baf0ed2 --- /dev/null +++ b/frontend/src/app/diagram/svgGeneration.ts @@ -0,0 +1,204 @@ +export function generateObjectiveSVG(title: string, teamName: string, iconFunction: any) { + let svg = ` + + + + + + + + + + + + + ${iconFunction} + + + +
+

+ ${title} +

+ +
+
+ + +
+

${teamName}

+
+
+
+`; + + let blob: Blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }); + return URL.createObjectURL(blob); +} + +export function generateKeyResultSVG(title: string, teamName: string, backgroundColor: any, fontColor: any) { + let svg = ` + + + + + + + + + + + + +
+

+ ${title} +

+ +
+
+ + +
+

${teamName}

+
+
+
+ `; + + let blob: Blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }); + return URL.createObjectURL(blob); +} + +export function generateNeutralKeyResultSVG(title: string, teamName: string) { + let svg = ` + + + + + + + + + + + + +
+

+ ${title} +

+ +
+
+ + +
+

${teamName}

+
+
+
+ `; + + let blob: Blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }); + return URL.createObjectURL(blob); +} + +export function getDraftIcon() { + return ` + + + + + + + + `; +} + +export function getOnGoingIcon() { + return ` + + + + + + + + `; +} + +export function getSuccessfulIcon() { + return ` + + + + + + + + `; +} + +export function getNotSuccessfulIcon() { + return ` + + + + + + + + `; +} diff --git a/frontend/src/app/keyresult-detail/keyresult-detail.component.ts b/frontend/src/app/keyresult-detail/keyresult-detail.component.ts index 5326c6d430..79d03c2f54 100644 --- a/frontend/src/app/keyresult-detail/keyresult-detail.component.ts +++ b/frontend/src/app/keyresult-detail/keyresult-detail.component.ts @@ -118,6 +118,8 @@ export class KeyresultDetailComponent implements OnInit { this.refreshDataService.markDataRefresh(); } else if (result?.closeState === CloseState.DELETED) { this.router.navigate(['']).then(() => this.refreshDataService.markDataRefresh()); + } else if (result == '') { + return; } else { this.loadKeyResult(this.keyResult$.getValue().id); } @@ -180,9 +182,11 @@ export class KeyresultDetailComponent implements OnInit { keyResult: this.keyResult$.getValue(), }, }); - dialogRef.afterClosed().subscribe(() => { - this.loadKeyResult(this.keyResult$.getValue().id); - this.refreshDataService.markDataRefresh(); + dialogRef.afterClosed().subscribe((result) => { + if (result != '' && result != undefined) { + this.loadKeyResult(this.keyResult$.getValue().id); + this.refreshDataService.markDataRefresh(true); + } }); } diff --git a/frontend/src/app/objective-detail/objective-detail.component.ts b/frontend/src/app/objective-detail/objective-detail.component.ts index 40253b3588..747bd70ea0 100644 --- a/frontend/src/app/objective-detail/objective-detail.component.ts +++ b/frontend/src/app/objective-detail/objective-detail.component.ts @@ -65,6 +65,7 @@ export class ObjectiveDetailComponent { .subscribe((result) => { if (result?.openNew) { this.openAddKeyResultDialog(); + return; } this.refreshDataService.markDataRefresh(); }); @@ -84,8 +85,10 @@ export class ObjectiveDetailComponent { }) .afterClosed() .subscribe((result) => { - if (result.delete) { + if (result && result.delete) { this.router.navigate(['']); + } else if (result == '' || result == undefined) { + return; } else { this.loadObjective(this.objective$.value.id); } diff --git a/frontend/src/app/overview/overview.component.html b/frontend/src/app/overview/overview.component.html index a8e1e40d7f..5c46bf9c35 100644 --- a/frontend/src/app/overview/overview.component.html +++ b/frontend/src/app/overview/overview.component.html @@ -3,14 +3,50 @@
- - -
-

Kein Team ausgewählt

- +
+
+
+ Overview +
+
+ Network +
+
+
+
+ +
+
+ + +
+

Kein Team ausgewählt

+ +
+
+ +
+ +
diff --git a/frontend/src/app/overview/overview.component.scss b/frontend/src/app/overview/overview.component.scss index 0f59a39431..f327dbff74 100644 --- a/frontend/src/app/overview/overview.component.scss +++ b/frontend/src/app/overview/overview.component.scss @@ -13,3 +13,50 @@ .puzzle-logo { filter: invert(38%) sepia(31%) saturate(216%) hue-rotate(167deg) brightness(96%) contrast(85%); } + +.active { + border-left: #909090 1px solid; + border-top: #909090 1px solid; + border-right: #909090 1px solid; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + background: white; + color: black; +} + +.non-active { + color: #9c9c9c; + border-bottom: #909090 1px solid; +} + +.tab-title { + display: flex; + justify-content: center; + align-items: center; + height: 39px; + margin-bottom: 16px; +} + +.buffer { + border-bottom: #909090 1px solid; +} + +.tabfocus { + outline: none; + &:focus-visible { + border-radius: 5px; + border: 2px solid #1a4e83; + } +} + +.overview { + width: 87px; +} + +.diagram { + width: 100px; +} + +.hidden { + display: none; +} diff --git a/frontend/src/app/overview/overview.component.spec.ts b/frontend/src/app/overview/overview.component.spec.ts index 4599cb1e60..6f9e20117e 100644 --- a/frontend/src/app/overview/overview.component.spec.ts +++ b/frontend/src/app/overview/overview.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { OverviewComponent } from './overview.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { overViewEntity1 } from '../shared/testData'; +import { alignmentLists, overViewEntity1 } from '../shared/testData'; import { BehaviorSubject, of, Subject } from 'rxjs'; import { OverviewService } from '../shared/services/overview.service'; import { AppRoutingModule } from '../app-routing.module'; @@ -16,11 +16,16 @@ import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { AlignmentService } from '../shared/services/alignment.service'; const overviewService = { getOverview: jest.fn(), }; +const alignmentService = { + getAlignmentByFilter: jest.fn(), +}; + const authGuardMock = () => { return Promise.resolve(true); }; @@ -53,6 +58,10 @@ describe('OverviewComponent', () => { provide: OverviewService, useValue: overviewService, }, + { + provide: AlignmentService, + useValue: alignmentService, + }, { provide: authGuard, useValue: authGuardMock, @@ -132,6 +141,23 @@ describe('OverviewComponent', () => { expect(component.loadOverview).toHaveBeenLastCalledWith(); }); + it('should call overviewService on overview', async () => { + jest.spyOn(overviewService, 'getOverview'); + component.isOverview = true; + + component.loadOverview(3, [5, 6], ''); + expect(overviewService.getOverview).toHaveBeenCalled(); + }); + + it('should call alignmentService on diagram', async () => { + jest.spyOn(alignmentService, 'getAlignmentByFilter').mockReturnValue(of(alignmentLists)); + component.isOverview = false; + fixture.detectChanges(); + + component.loadOverview(3, [5, 6], ''); + expect(alignmentService.getAlignmentByFilter).toHaveBeenCalled(); + }); + function markFiltersAsReady() { refreshDataServiceMock.quarterFilterReady.next(null); refreshDataServiceMock.teamFilterReady.next(null); diff --git a/frontend/src/app/overview/overview.component.ts b/frontend/src/app/overview/overview.component.ts index 674346df80..43116692ce 100644 --- a/frontend/src/app/overview/overview.component.ts +++ b/frontend/src/app/overview/overview.component.ts @@ -5,6 +5,8 @@ import { OverviewService } from '../shared/services/overview.service'; import { ActivatedRoute } from '@angular/router'; import { RefreshDataService } from '../shared/services/refresh-data.service'; import { getQueryString, getValueFromQuery, isMobileDevice, trackByFn } from '../shared/common'; +import { AlignmentService } from '../shared/services/alignment.service'; +import { AlignmentLists } from '../shared/types/model/AlignmentLists'; @Component({ selector: 'app-overview', @@ -14,13 +16,16 @@ import { getQueryString, getValueFromQuery, isMobileDevice, trackByFn } from '.. }) export class OverviewComponent implements OnInit, OnDestroy { overviewEntities$: Subject = new Subject(); + alignmentLists$: Subject = new Subject(); protected readonly trackByFn = trackByFn; private destroyed$: ReplaySubject = new ReplaySubject(1); hasAdminAccess: ReplaySubject = new ReplaySubject(1); overviewPadding: Subject = new Subject(); + isOverview: boolean = true; constructor( private overviewService: OverviewService, + private alignmentService: AlignmentService, private refreshDataService: RefreshDataService, private activatedRoute: ActivatedRoute, private changeDetector: ChangeDetectorRef, @@ -62,7 +67,15 @@ export class OverviewComponent implements OnInit, OnDestroy { this.loadOverview(quarterId, teamIds, objectiveQueryString); } - loadOverview(quarterId?: number, teamIds?: number[], objectiveQuery?: string) { + loadOverview(quarterId?: number, teamIds?: number[], objectiveQuery?: string): void { + if (this.isOverview) { + this.loadOverviewData(quarterId, teamIds, objectiveQuery); + } else { + this.loadAlignmentData(quarterId, teamIds, objectiveQuery); + } + } + + loadOverviewData(quarterId?: number, teamIds?: number[], objectiveQuery?: string): void { this.overviewService .getOverview(quarterId, teamIds, objectiveQuery) .pipe( @@ -77,8 +90,32 @@ export class OverviewComponent implements OnInit, OnDestroy { }); } + loadAlignmentData(quarterId?: number, teamIds?: number[], objectiveQuery?: string): void { + this.alignmentService + .getAlignmentByFilter(quarterId, teamIds, objectiveQuery) + .pipe( + catchError(() => { + this.loadOverview(); + return EMPTY; + }), + ) + .subscribe((alignmentLists: AlignmentLists) => { + this.alignmentLists$.next(alignmentLists); + }); + } + ngOnDestroy(): void { this.destroyed$.next(true); this.destroyed$.complete(); } + + switchPage(input: string) { + if (input == 'diagram' && this.isOverview) { + this.isOverview = false; + this.loadOverviewWithParams(); + } else if (input == 'overview' && !this.isOverview) { + this.isOverview = true; + this.loadOverviewWithParams(); + } + } } diff --git a/frontend/src/app/shared/dialog/check-in-history-dialog/check-in-history-dialog.component.ts b/frontend/src/app/shared/dialog/check-in-history-dialog/check-in-history-dialog.component.ts index 8add8dad5d..a8880f8908 100644 --- a/frontend/src/app/shared/dialog/check-in-history-dialog/check-in-history-dialog.component.ts +++ b/frontend/src/app/shared/dialog/check-in-history-dialog/check-in-history-dialog.component.ts @@ -57,9 +57,11 @@ export class CheckInHistoryDialogComponent implements OnInit { maxHeight: dialogConfig.maxHeight, maxWidth: dialogConfig.maxWidth, }); - dialogRef.afterClosed().subscribe(() => { + dialogRef.afterClosed().subscribe((result) => { this.loadCheckInHistory(); - this.refreshDataService.markDataRefresh(); + if (result != '' && result != undefined) { + this.refreshDataService.markDataRefresh(true); + } }); } diff --git a/frontend/src/app/shared/dialog/checkin/check-in-form/check-in-form.component.ts b/frontend/src/app/shared/dialog/checkin/check-in-form/check-in-form.component.ts index c6955bd251..c164ab74eb 100644 --- a/frontend/src/app/shared/dialog/checkin/check-in-form/check-in-form.component.ts +++ b/frontend/src/app/shared/dialog/checkin/check-in-form/check-in-form.component.ts @@ -98,7 +98,9 @@ export class CheckInFormComponent implements OnInit { this.checkInService.saveCheckIn(checkIn).subscribe(() => { this.actionService.updateActions(this.dialogForm.value.actionList!).subscribe(() => { - this.dialogRef.close(); + this.dialogRef.close({ + checkIn: checkIn, + }); }); }); } diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index 28e461b3bf..3dccca69da 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -1,7 +1,7 @@
@@ -10,11 +10,11 @@
@@ -53,9 +53,10 @@
- -

Key Results im Anschluss erfassen

-
+ +
+ + + + @for (group of filteredAlignmentOptions$ | async; track group) { + + @for (alignmentObject of group.alignmentObjects; track alignmentObject) { + {{ alignmentObject.objectTitle }} + } + + } + +
+ +

Key Results im Anschluss erfassen

+
diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss index 28e560f6d2..d5d63bee0a 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss @@ -10,8 +10,11 @@ .add-keyresult { // Select is aligned with label and it's not easy to align checkbox to select - margin-top: -6px; - margin-left: 1.2rem; + margin-left: -10px; +} + +.add-keyresult-text { + margin: 0; } @media only screen and (min-width: 820px) { @@ -22,21 +25,24 @@ } } -@media only screen and (max-width: 770px) { - .add-keyresult { - margin-left: -10px; - margin-top: 1rem; - } +.dialog-content { + max-height: 35vh; +} - .add-keyresult-text { - margin: 0; - } +.select-width { + width: 92%; } -.add-keyresult-text { - margin-left: -10px; +.mat-mdc-option { + margin-bottom: 5px; } -.dialog-content { - max-height: 35vh; +.alignment-input { + border: solid 1px #909090; + padding: 0.44rem 0.625rem 0.375rem 0.625rem !important; + cursor: pointer; + height: 1.18rem; + box-sizing: content-box; + overflow: hidden; + text-overflow: ellipsis; } diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index bae1fc2c25..0f98cd7a76 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -10,7 +10,18 @@ import { MatSelectModule } from '@angular/material/select'; import { ReactiveFormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ObjectiveService } from '../../services/objective.service'; -import { objective, quarter, quarterList, teamMin1 } from '../../testData'; +import { + alignmentPossibility1, + alignmentPossibility2, + alignmentPossibilityObject1, + alignmentPossibilityObject2, + alignmentPossibilityObject3, + objective, + objectiveWithAlignment, + quarter, + quarterList, + teamMin1, +} from '../../testData'; import { Observable, of } from 'rxjs'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { HarnessLoader } from '@angular/cdk/testing'; @@ -27,20 +38,24 @@ import { DialogHeaderComponent } from '../../custom/dialog-header/dialog-header. import { TranslateTestingModule } from 'ngx-translate-testing'; import * as de from '../../../../assets/i18n/de.json'; import { ActivatedRoute } from '@angular/router'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { AlignmentPossibility } from '../../types/model/AlignmentPossibility'; +import { ElementRef } from '@angular/core'; let objectiveService = { getFullObjective: jest.fn(), createObjective: jest.fn(), updateObjective: jest.fn(), deleteObjective: jest.fn(), + getAlignmentPossibilities: jest.fn(), }; const quarterService = { getAllQuarters(): Observable { return of([ + { id: 199, startDate: null, endDate: null, label: 'Backlog' }, { id: 1, startDate: quarter.startDate, endDate: quarter.endDate, label: quarter.label }, { id: 2, startDate: quarter.startDate, endDate: quarter.endDate, label: quarter.label }, - { id: 199, startDate: null, endDate: null, label: 'Backlog' }, ]); }, }; @@ -89,6 +104,7 @@ describe('ObjectiveDialogComponent', () => { MatSelectModule, ReactiveFormsModule, MatInputModule, + MatAutocompleteModule, NoopAnimationsModule, MatCheckboxModule, RouterTestingModule, @@ -105,6 +121,7 @@ describe('ObjectiveDialogComponent', () => { { provide: TeamService, useValue: teamService }, ], }); + jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of([])); fixture = TestBed.createComponent(ObjectiveFormComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -116,6 +133,8 @@ describe('ObjectiveDialogComponent', () => { }); it.each([['DRAFT'], ['ONGOING']])('onSubmit create', async (state: string) => { + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + //Prepare data let title: string = 'title'; let description: string = 'description'; @@ -126,7 +145,7 @@ describe('ObjectiveDialogComponent', () => { team = teams[0].id; }); quarterService.getAllQuarters().subscribe((quarters) => { - quarter = quarters[1].id; + quarter = quarters[2].id; }); // Get input elements and set values @@ -170,6 +189,7 @@ describe('ObjectiveDialogComponent', () => { teamId: 2, title: title, writeable: true, + alignedEntity: null, }, teamId: 1, }); @@ -182,7 +202,7 @@ describe('ObjectiveDialogComponent', () => { description: 'Test description', quarter: 0, team: 0, - relation: 0, + alignment: null, createKeyResults: false, }); @@ -198,6 +218,64 @@ describe('ObjectiveDialogComponent', () => { title: 'Test title', quarterId: 0, teamId: 0, + version: undefined, + alignedEntity: null, + }); + }); + + it('should create objective with alignment objective', () => { + matDataMock.objective.objectiveId = undefined; + component.objectiveForm.setValue({ + title: 'Test title with alignment', + description: 'Test description', + quarter: 0, + team: 0, + alignment: alignmentPossibilityObject2, + createKeyResults: false, + }); + + objectiveService.createObjective.mockReturnValue(of({ ...objective, state: 'DRAFT' })); + component.onSubmit('DRAFT'); + + fixture.detectChanges(); + + expect(objectiveService.createObjective).toHaveBeenCalledWith({ + description: 'Test description', + id: undefined, + state: 'DRAFT', + title: 'Test title with alignment', + quarterId: 0, + teamId: 0, + version: undefined, + alignedEntity: { id: 2, type: 'objective' }, + }); + }); + + it('should create objective with alignment keyResult', () => { + matDataMock.objective.objectiveId = undefined; + component.objectiveForm.setValue({ + title: 'Test title with alignment', + description: 'Test description', + quarter: 0, + team: 0, + alignment: alignmentPossibilityObject3, + createKeyResults: false, + }); + + objectiveService.createObjective.mockReturnValue(of({ ...objective, state: 'DRAFT' })); + component.onSubmit('DRAFT'); + + fixture.detectChanges(); + + expect(objectiveService.createObjective).toHaveBeenCalledWith({ + description: 'Test description', + id: undefined, + state: 'DRAFT', + title: 'Test title with alignment', + quarterId: 0, + teamId: 0, + version: undefined, + alignedEntity: { id: 1, type: 'keyResult' }, }); }); @@ -208,7 +286,7 @@ describe('ObjectiveDialogComponent', () => { description: 'Test description', quarter: 1, team: 1, - relation: 0, + alignment: null, createKeyResults: false, }); @@ -224,6 +302,38 @@ describe('ObjectiveDialogComponent', () => { title: 'Test title', quarterId: 1, teamId: 1, + version: undefined, + alignedEntity: null, + }); + }); + + it('should update objective with alignment', () => { + objectiveService.updateObjective.mockReset(); + matDataMock.objective.objectiveId = 1; + component.state = 'DRAFT'; + component.objectiveForm.setValue({ + title: 'Test title with alignment', + description: 'Test description', + quarter: 1, + team: 1, + alignment: alignmentPossibilityObject3, + createKeyResults: false, + }); + + objectiveService.updateObjective.mockReturnValue(of({ ...objective, state: 'ONGOING' })); + fixture.detectChanges(); + + component.onSubmit('DRAFT'); + + expect(objectiveService.updateObjective).toHaveBeenCalledWith({ + description: 'Test description', + id: 1, + state: 'DRAFT', + title: 'Test title with alignment', + quarterId: 1, + teamId: 1, + version: undefined, + alignedEntity: { id: 1, type: 'keyResult' }, }); }); @@ -254,10 +364,25 @@ describe('ObjectiveDialogComponent', () => { expect(rawFormValue.quarter).toBe(objective.quarterId); }); + it('should load default values into form onInit with defined objectiveId with an alignment', async () => { + matDataMock.objective.objectiveId = 1; + const routerHarness = await RouterTestingHarness.create(); + await routerHarness.navigateByUrl('/?quarter=2'); + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + objectiveService.getFullObjective.mockReturnValue(of(objectiveWithAlignment)); + component.ngOnInit(); + const rawFormValue = component.objectiveForm.getRawValue(); + expect(rawFormValue.title).toBe(objectiveWithAlignment.title); + expect(rawFormValue.description).toBe(objectiveWithAlignment.description); + expect(rawFormValue.team).toBe(objectiveWithAlignment.teamId); + expect(rawFormValue.quarter).toBe(objectiveWithAlignment.quarterId); + expect(rawFormValue.alignment).toBe(alignmentPossibilityObject2); + }); + it('should return correct value if allowed to save to backlog', async () => { component.quarters = quarterList; - const isBacklogQuarterSpy = jest.spyOn(component, 'isBacklogQuarter'); - isBacklogQuarterSpy.mockReturnValue(false); + const isBacklogQuarterSpy = jest.spyOn(component, 'isNotBacklogQuarter'); + isBacklogQuarterSpy.mockReturnValue(true); component.data.action = 'duplicate'; fixture.detectChanges(); @@ -271,6 +396,7 @@ describe('ObjectiveDialogComponent', () => { expect(component.allowedToSaveBacklog()).toBeTruthy(); component.state = 'ONGOING'; + isBacklogQuarterSpy.mockReturnValue(false); fixture.detectChanges(); expect(component.allowedToSaveBacklog()).toBeFalsy(); @@ -335,7 +461,7 @@ describe('ObjectiveDialogComponent', () => { }); }); - describe('Backlog quarter', () => { + describe('AlignmentPossibilities', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ @@ -346,6 +472,7 @@ describe('ObjectiveDialogComponent', () => { MatSelectModule, ReactiveFormsModule, MatInputModule, + MatAutocompleteModule, NoopAnimationsModule, MatCheckboxModule, RouterTestingModule, @@ -360,9 +487,9 @@ describe('ObjectiveDialogComponent', () => { { provide: ObjectiveService, useValue: objectiveService }, { provide: QuarterService, useValue: quarterService }, { provide: TeamService, useValue: teamService }, - { provide: ActivatedRoute, useValue: mockActivatedRoute }, ], }); + fixture = TestBed.createComponent(ObjectiveFormComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -373,7 +500,192 @@ describe('ObjectiveDialogComponent', () => { expect(component).toBeTruthy(); }); - it('should set correct default value if objective is released in backlog', async () => { + it('should load correct alignment possibilities', async () => { + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + component.generateAlignmentPossibilities(3, null, null); + + expect(component.alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.objectiveForm.getRawValue().alignment).toEqual(null); + }); + + it('should not include current team in alignment possibilities', async () => { + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + component.generateAlignmentPossibilities(3, null, 1); + + expect(component.alignmentPossibilities).toStrictEqual([alignmentPossibility2]); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility2]); + expect(component.objectiveForm.getRawValue().alignment).toEqual(null); + }); + + it('should return team and objective with same text in alignment possibilities', async () => { + component.alignmentInput.nativeElement.value = 'puzzle'; + component.alignmentPossibilities = [alignmentPossibility1, alignmentPossibility2]; + component.filter(); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + }); + + it('should load existing objective alignment to objectiveForm', async () => { + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + component.generateAlignmentPossibilities(3, objectiveWithAlignment, null); + + expect(component.alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.objectiveForm.getRawValue().alignment).toEqual(alignmentPossibilityObject2); + }); + + it('should load existing keyResult alignment to objectiveForm', async () => { + objectiveWithAlignment.alignedEntity = { id: 1, type: 'keyResult' }; + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + component.generateAlignmentPossibilities(3, objectiveWithAlignment, null); + + expect(component.alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.objectiveForm.getRawValue().alignment).toEqual(alignmentPossibilityObject3); + }); + + it('should filter correct alignment possibilities', async () => { + // Search for one title + component.alignmentInput.nativeElement.value = 'palm'; + component.alignmentPossibilities = [alignmentPossibility1, alignmentPossibility2]; + component.filter(); + let modifiedAlignmentPossibility: AlignmentPossibility = { + teamId: 1, + teamName: 'Puzzle ITC', + alignmentObjects: [alignmentPossibilityObject3], + }; + expect(component.filteredAlignmentOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); + + // Search for team name + component.alignmentInput.nativeElement.value = 'Puzzle IT'; + component.alignmentPossibilities = [alignmentPossibility1, alignmentPossibility2]; + component.filter(); + modifiedAlignmentPossibility = { + teamId: 1, + teamName: 'Puzzle ITC', + alignmentObjects: [alignmentPossibilityObject2, alignmentPossibilityObject3], + }; + expect(component.filteredAlignmentOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); + + // Search for two objects + component.alignmentInput.nativeElement.value = 'buy'; + component.alignmentPossibilities = [alignmentPossibility1, alignmentPossibility2]; + component.filter(); + let modifiedAlignmentPossibilities = [ + { + teamId: 1, + teamName: 'Puzzle ITC', + alignmentObjects: [alignmentPossibilityObject3], + }, + { + teamId: 2, + teamName: 'We are cube', + alignmentObjects: [alignmentPossibilityObject1], + }, + ]; + expect(component.filteredAlignmentOptions$.getValue()).toEqual(modifiedAlignmentPossibilities); + + // No match + component.alignmentInput.nativeElement.value = 'findus'; + component.alignmentPossibilities = [alignmentPossibility1, alignmentPossibility2]; + component.filter(); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([]); + }); + + it('should not include alignment object when already containing in team', async () => { + component.alignmentInput.nativeElement.value = 'puzzle'; + component.alignmentPossibilities = [alignmentPossibility1]; + component.filter(); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1]); + }); + + it('should find correct alignment object', () => { + // objective + let alignmentObject = component.findAlignmentPossibilityObject( + [alignmentPossibility1, alignmentPossibility2], + 1, + 'objective', + ); + expect(alignmentObject!.objectId).toEqual(1); + expect(alignmentObject!.objectTitle).toEqual('We want to increase the income puzzle buy'); + + // keyResult + alignmentObject = component.findAlignmentPossibilityObject( + [alignmentPossibility1, alignmentPossibility2], + 1, + 'keyResult', + ); + expect(alignmentObject!.objectId).toEqual(1); + expect(alignmentObject!.objectTitle).toEqual('We buy 3 palms puzzle'); + + // no match + alignmentObject = component.findAlignmentPossibilityObject( + [alignmentPossibility1, alignmentPossibility2], + 133, + 'keyResult', + ); + expect(alignmentObject).toEqual(null); + }); + + it('should display kein alignment vorhanden when no alignment possibility', () => { + component.filteredAlignmentOptions$.next([alignmentPossibility1, alignmentPossibility2]); + fixture.detectChanges(); + expect(component.alignmentInput.nativeElement.getAttribute('placeholder')).toEqual('Bezug wählen'); + + component.filteredAlignmentOptions$.next([]); + fixture.detectChanges(); + expect(component.alignmentInput.nativeElement.getAttribute('placeholder')).toEqual('Kein Alignment vorhanden'); + }); + + it('should update alignments on quarter change', () => { + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + component.updateAlignments(); + expect(component.alignmentInput.nativeElement.value).toEqual(''); + expect(component.objectiveForm.getRawValue().alignment).toEqual(null); + expect(objectiveService.getAlignmentPossibilities).toHaveBeenCalled(); + }); + + it('should return correct displayedValue', () => { + component.alignmentInput.nativeElement.value = 'O - Objective 1'; + expect(component.displayedValue()).toEqual('O - Objective 1'); + + component.alignmentInput = new ElementRef(document.createElement('input')); + expect(component.displayedValue()).toEqual(''); + }); + }); + + describe('Backlog quarter', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + MatDialogModule, + MatIconModule, + MatFormFieldModule, + MatSelectModule, + ReactiveFormsModule, + MatInputModule, + MatAutocompleteModule, + NoopAnimationsModule, + MatCheckboxModule, + RouterTestingModule, + TranslateTestingModule.withTranslations({ + de: de, + }), + ], + declarations: [ObjectiveFormComponent, DialogHeaderComponent], + providers: [ + { provide: MatDialogRef, useValue: dialogMock }, + { provide: MAT_DIALOG_DATA, useValue: matDataMock }, + { provide: ObjectiveService, useValue: objectiveService }, + { provide: QuarterService, useValue: quarterService }, + { provide: TeamService, useValue: teamService }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + ], + }); + jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of([])); + fixture = TestBed.createComponent(ObjectiveFormComponent); + component = fixture.componentInstance; component.data = { objective: { objectiveId: 1, @@ -381,8 +693,16 @@ describe('ObjectiveDialogComponent', () => { }, action: 'releaseBacklog', }; + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); - const isBacklogQuarterSpy = jest.spyOn(component, 'isBacklogQuarter'); + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set correct default value if objective is released in backlog', async () => { + const isBacklogQuarterSpy = jest.spyOn(component, 'isNotBacklogQuarter'); isBacklogQuarterSpy.mockReturnValue(false); const routerHarness = await RouterTestingHarness.create(); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 2b967cabd8..c881ef4372 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -1,10 +1,10 @@ -import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Quarter } from '../../types/model/Quarter'; import { TeamService } from '../../services/team.service'; import { Team } from '../../types/model/Team'; import { QuarterService } from '../../services/quarter.service'; -import { forkJoin, Observable, of, Subject } from 'rxjs'; +import { BehaviorSubject, forkJoin, Observable, of } from 'rxjs'; import { ObjectiveService } from '../../services/objective.service'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { State } from '../../types/enums/State'; @@ -15,6 +15,8 @@ import { formInputCheck, getQuarterLabel, getValueFromQuery, hasFormFieldErrors, import { ActivatedRoute } from '@angular/router'; import { CONFIRM_DIALOG_WIDTH, GJ_REGEX_PATTERN } from '../../constantLibary'; import { TranslateService } from '@ngx-translate/core'; +import { AlignmentPossibility } from '../../types/model/AlignmentPossibility'; +import { AlignmentPossibilityObject } from '../../types/model/AlignmentPossibilityObject'; @Component({ selector: 'app-objective-form', @@ -23,18 +25,22 @@ import { TranslateService } from '@ngx-translate/core'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ObjectiveFormComponent implements OnInit { + @ViewChild('alignmentInput') alignmentInput!: ElementRef; + objectiveForm = new FormGroup({ title: new FormControl('', [Validators.required, Validators.minLength(2), Validators.maxLength(250)]), description: new FormControl('', [Validators.maxLength(4096)]), quarter: new FormControl(0, [Validators.required]), team: new FormControl({ value: 0, disabled: true }, [Validators.required]), - relation: new FormControl({ value: 0, disabled: true }), + alignment: new FormControl(null), createKeyResults: new FormControl(false), }); quarters$: Observable = of([]); quarters: Quarter[] = []; teams$: Observable = of([]); - currentTeam: Subject = new Subject(); + alignmentPossibilities: AlignmentPossibility[] = []; + filteredAlignmentOptions$: BehaviorSubject = new BehaviorSubject([]); + currentTeam$: BehaviorSubject = new BehaviorSubject(null); state: string | null = null; version!: number; protected readonly formInputCheck = formInputCheck; @@ -62,6 +68,15 @@ export class ObjectiveFormComponent implements OnInit { onSubmit(submitType: any): void { const value = this.objectiveForm.getRawValue(); const state = this.data.objective.objectiveId == null ? submitType : this.state; + + let alignment: AlignmentPossibilityObject | null = value.alignment; + let alignedEntity: { id: number; type: string } | null = alignment + ? { + id: alignment.objectId, + type: alignment.objectType, + } + : null; + let objectiveDTO: Objective = { id: this.data.objective.objectiveId, version: this.version, @@ -70,6 +85,7 @@ export class ObjectiveFormComponent implements OnInit { title: value.title, teamId: value.team, state: state, + alignedEntity: alignedEntity, } as unknown as Objective; const submitFunction = this.getSubmitFunction(objectiveDTO.id, objectiveDTO); @@ -89,18 +105,19 @@ export class ObjectiveFormComponent implements OnInit { forkJoin([objective$, this.quarters$]).subscribe(([objective, quarters]) => { this.quarters = quarters; const teamId = isCreating ? objective.teamId : this.data.objective.teamId; - let quarterId = getValueFromQuery(this.route.snapshot.queryParams['quarter'], quarters[1].id)[0]; + let quarterId = getValueFromQuery(this.route.snapshot.queryParams['quarter'], quarters[2].id)[0]; let currentQuarter: Quarter | undefined = this.quarters.find((quarter) => quarter.id == quarterId); - if (currentQuarter && !this.isBacklogQuarter(currentQuarter.label) && this.data.action == 'releaseBacklog') { - quarterId = quarters[1].id; + if (currentQuarter && !this.isNotBacklogQuarter(currentQuarter.label) && this.data.action == 'releaseBacklog') { + quarterId = quarters[2].id; } this.state = objective.state; this.version = objective.version; this.teams$.subscribe((value) => { - this.currentTeam.next(value.filter((team) => team.id == teamId)[0]); + this.currentTeam$.next(value.filter((team: Team) => team.id == teamId)[0]); }); + this.generateAlignmentPossibilities(quarterId, objective, teamId!); this.objectiveForm.patchValue({ title: objective.title, @@ -190,6 +207,7 @@ export class ObjectiveFormComponent implements OnInit { state: 'DRAFT' as State, teamId: 0, quarterId: 0, + alignedEntity: null, } as Objective; } @@ -198,12 +216,12 @@ export class ObjectiveFormComponent implements OnInit { (quarter) => quarter.id == this.objectiveForm.value.quarter, ); if (currentQuarter) { - let isBacklogCurrent: boolean = !this.isBacklogQuarter(currentQuarter.label); + let isBacklogCurrent: boolean = this.isNotBacklogQuarter(currentQuarter.label); if (this.data.action == 'duplicate') return true; if (this.data.objective.objectiveId) { - return isBacklogCurrent ? this.state == 'DRAFT' : true; + return !isBacklogCurrent ? this.state == 'DRAFT' : true; } else { - return !isBacklogCurrent; + return isBacklogCurrent; } } else { return true; @@ -226,9 +244,144 @@ export class ObjectiveFormComponent implements OnInit { } } - isBacklogQuarter(label: string) { + isNotBacklogQuarter(label: string) { return GJ_REGEX_PATTERN.test(label); } + generateAlignmentPossibilities(quarterId: number, objective: Objective | null, teamId: number | null) { + this.objectiveService + .getAlignmentPossibilities(quarterId) + .subscribe((alignmentPossibilities: AlignmentPossibility[]) => { + if (teamId) { + alignmentPossibilities = alignmentPossibilities.filter((item: AlignmentPossibility) => item.teamId != teamId); + } + + if (objective) { + let alignedEntity: { id: number; type: string } | null = objective.alignedEntity; + if (alignedEntity) { + let alignmentPossibilityObject: AlignmentPossibilityObject | null = this.findAlignmentPossibilityObject( + alignmentPossibilities, + alignedEntity.id, + alignedEntity.type, + ); + this.objectiveForm.patchValue({ + alignment: alignmentPossibilityObject, + }); + } + } + + this.filteredAlignmentOptions$.next(alignmentPossibilities.slice()); + this.alignmentPossibilities = alignmentPossibilities; + }); + } + + findAlignmentPossibilityObject( + alignmentPossibilities: AlignmentPossibility[], + objectId: number, + objectType: string, + ): AlignmentPossibilityObject | null { + for (let possibility of alignmentPossibilities) { + let foundObject: AlignmentPossibilityObject | undefined = possibility.alignmentObjects.find( + (alignmentObject: AlignmentPossibilityObject) => + alignmentObject.objectId === objectId && alignmentObject.objectType === objectType, + ); + if (foundObject) { + return foundObject; + } + } + return null; + } + + updateAlignments() { + this.alignmentInput.nativeElement.value = ''; + this.filteredAlignmentOptions$.next([]); + this.objectiveForm.patchValue({ + alignment: null, + }); + this.generateAlignmentPossibilities(this.objectiveForm.value.quarter!, null, this.currentTeam$.getValue()!.id); + } + + filter() { + let filterValue: string = this.alignmentInput.nativeElement.value.toLowerCase(); + let matchingTeams: AlignmentPossibility[] = this.alignmentPossibilities.filter( + (possibility: AlignmentPossibility) => possibility.teamName.toLowerCase().includes(filterValue), + ); + + let filteredObjects: AlignmentPossibilityObject[] = + this.getMatchingAlignmentPossibilityObjectsByInputFilter(filterValue); + let matchingPossibilities: AlignmentPossibility[] = + this.getAlignmentPossibilityFromAlignmentObject(filteredObjects); + matchingPossibilities = [...new Set(matchingPossibilities)]; + + let alignmentOptionList: AlignmentPossibility[] = this.removeNotMatchingObjectsFromAlignmentObject( + matchingPossibilities, + filteredObjects, + ); + alignmentOptionList = this.removeAlignmentObjectWhenAlreadyContainingInMatchingTeam( + alignmentOptionList, + matchingTeams, + ); + + let concatAlignmentOptionList: AlignmentPossibility[] = + filterValue == '' ? matchingTeams : matchingTeams.concat(alignmentOptionList); + this.filteredAlignmentOptions$.next([...new Set(concatAlignmentOptionList)]); + } + + getMatchingAlignmentPossibilityObjectsByInputFilter(filterValue: string): AlignmentPossibilityObject[] { + return this.alignmentPossibilities.flatMap((alignmentPossibility: AlignmentPossibility) => + alignmentPossibility.alignmentObjects.filter((alignmentPossibilityObject: AlignmentPossibilityObject) => + alignmentPossibilityObject.objectTitle.toLowerCase().includes(filterValue), + ), + ); + } + + getAlignmentPossibilityFromAlignmentObject(filteredObjects: AlignmentPossibilityObject[]): AlignmentPossibility[] { + return this.alignmentPossibilities.filter((possibility: AlignmentPossibility) => + filteredObjects.some((alignmentPossibilityObject: AlignmentPossibilityObject) => + possibility.alignmentObjects.includes(alignmentPossibilityObject), + ), + ); + } + + removeNotMatchingObjectsFromAlignmentObject( + matchingPossibilities: AlignmentPossibility[], + filteredObjects: AlignmentPossibilityObject[], + ): AlignmentPossibility[] { + return matchingPossibilities.map((possibility: AlignmentPossibility) => ({ + ...possibility, + alignmentObjects: possibility.alignmentObjects.filter((alignmentPossibilityObject: AlignmentPossibilityObject) => + filteredObjects.includes(alignmentPossibilityObject), + ), + })); + } + + removeAlignmentObjectWhenAlreadyContainingInMatchingTeam( + alignmentOptionList: AlignmentPossibility[], + matchingTeams: AlignmentPossibility[], + ): AlignmentPossibility[] { + return alignmentOptionList.filter( + (alignmentOption) => + !matchingTeams.some((alignmentPossibility) => alignmentPossibility.teamId === alignmentOption.teamId), + ); + } + + displayWith(value: any) { + if (value) { + return value.objectTitle; + } + } + + displayedValue(): string { + if (this.alignmentInput) { + return this.alignmentInput.nativeElement.value; + } else { + return ''; + } + } + + scrollLeft() { + this.alignmentInput.nativeElement.scrollLeft = 0; + } + protected readonly getQuarterLabel = getQuarterLabel; } diff --git a/frontend/src/app/shared/services/alignment.service.spec.ts b/frontend/src/app/shared/services/alignment.service.spec.ts new file mode 100644 index 0000000000..c3d7f558bf --- /dev/null +++ b/frontend/src/app/shared/services/alignment.service.spec.ts @@ -0,0 +1,65 @@ +import { TestBed } from '@angular/core/testing'; + +import { AlignmentService } from './alignment.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { HttpClient } from '@angular/common/http'; +import { of } from 'rxjs'; +import { alignmentLists } from '../testData'; + +const httpClient = { + get: jest.fn(), +}; + +describe('AlignmentService', () => { + let service: AlignmentService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [{ provide: HttpClient, useValue: httpClient }], + }).compileComponents(); + service = TestBed.inject(AlignmentService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should set params of filter correctly without objectiveQuery', (done) => { + jest.spyOn(httpClient, 'get').mockReturnValue(of(alignmentLists)); + service.getAlignmentByFilter(2, [4, 5], '').subscribe((alignmentLists) => { + alignmentLists.alignmentObjectDtoList.forEach((object) => { + expect(object.objectType).toEqual('objective'); + expect(object.objectTeamName).toEqual('Example Team'); + }); + alignmentLists.alignmentConnectionDtoList.forEach((connection) => { + expect(connection.targetKeyResultId).toEqual(null); + expect(connection.alignedObjectiveId).toEqual(1); + expect(connection.targetObjectiveId).toEqual(2); + }); + done(); + }); + expect(httpClient.get).toHaveBeenCalledWith('/api/v2/alignments/alignmentLists', { + params: { quarterFilter: 2, teamFilter: [4, 5] }, + }); + }); + + it('should set params of filter correctly with objectiveQuery', (done) => { + jest.spyOn(httpClient, 'get').mockReturnValue(of(alignmentLists)); + service.getAlignmentByFilter(2, [4, 5], 'objective').subscribe((alignmentLists) => { + alignmentLists.alignmentObjectDtoList.forEach((object) => { + expect(object.objectType).toEqual('objective'); + expect(object.objectTeamName).toEqual('Example Team'); + }); + alignmentLists.alignmentConnectionDtoList.forEach((connection) => { + expect(connection.targetKeyResultId).toEqual(null); + expect(connection.alignedObjectiveId).toEqual(1); + expect(connection.targetObjectiveId).toEqual(2); + }); + done(); + }); + expect(httpClient.get).toHaveBeenCalledWith('/api/v2/alignments/alignmentLists', { + params: { objectiveQuery: 'objective', quarterFilter: 2, teamFilter: [4, 5] }, + }); + }); +}); diff --git a/frontend/src/app/shared/services/alignment.service.ts b/frontend/src/app/shared/services/alignment.service.ts new file mode 100644 index 0000000000..36964b8457 --- /dev/null +++ b/frontend/src/app/shared/services/alignment.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { AlignmentLists } from '../types/model/AlignmentLists'; +import { optionalValue } from '../common'; + +@Injectable({ + providedIn: 'root', +}) +export class AlignmentService { + constructor(private httpClient: HttpClient) {} + + getAlignmentByFilter(quarterId?: number, teamIds?: number[], objectiveQuery?: string): Observable { + const params = optionalValue({ + teamFilter: teamIds, + quarterFilter: quarterId, + objectiveQuery: objectiveQuery, + }); + + return this.httpClient.get(`/api/v2/alignments/alignmentLists`, { params: params }); + } +} diff --git a/frontend/src/app/shared/services/objective.service.ts b/frontend/src/app/shared/services/objective.service.ts index 04988bf6cd..388abce455 100644 --- a/frontend/src/app/shared/services/objective.service.ts +++ b/frontend/src/app/shared/services/objective.service.ts @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { Objective } from '../types/model/Objective'; import { Observable } from 'rxjs'; import { Completed } from '../types/model/Completed'; +import { AlignmentPossibility } from '../types/model/AlignmentPossibility'; @Injectable({ providedIn: 'root', @@ -14,6 +15,10 @@ export class ObjectiveService { return this.httpClient.get('/api/v2/objectives/' + id); } + getAlignmentPossibilities(quarterId: number): Observable { + return this.httpClient.get('/api/v2/objectives/alignmentPossibilities/' + quarterId); + } + createObjective(objectiveDTO: Objective): Observable { return this.httpClient.post('/api/v2/objectives', objectiveDTO); } diff --git a/frontend/src/app/shared/services/refresh-data.service.ts b/frontend/src/app/shared/services/refresh-data.service.ts index d9f994e4c9..637eb78ba8 100644 --- a/frontend/src/app/shared/services/refresh-data.service.ts +++ b/frontend/src/app/shared/services/refresh-data.service.ts @@ -7,13 +7,15 @@ import { DEFAULT_HEADER_HEIGHT_PX } from '../constantLibary'; }) export class RefreshDataService { public reloadOverviewSubject: Subject = new Subject(); + public reloadAlignmentSubject: Subject = new Subject(); public quarterFilterReady: Subject = new Subject(); public teamFilterReady: Subject = new Subject(); public okrBannerHeightSubject: BehaviorSubject = new BehaviorSubject(DEFAULT_HEADER_HEIGHT_PX); - markDataRefresh() { + markDataRefresh(reload?: boolean | null) { this.reloadOverviewSubject.next(); + this.reloadAlignmentSubject.next(reload); } } diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index 930e424e55..52e16248d0 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -19,6 +19,11 @@ import { Action } from './types/model/Action'; import { OrganisationState } from './types/enums/OrganisationState'; import { Organisation } from './types/model/Organisation'; import { Dashboard } from './types/model/Dashboard'; +import { AlignmentObject } from './types/model/AlignmentObject'; +import { AlignmentConnection } from './types/model/AlignmentConnection'; +import { AlignmentLists } from './types/model/AlignmentLists'; +import { AlignmentPossibilityObject } from './types/model/AlignmentPossibilityObject'; +import { AlignmentPossibility } from './types/model/AlignmentPossibility'; export const organisationActive = { id: 1, @@ -135,7 +140,7 @@ export const quarterBacklog: Quarter = { endDate: null, } as Quarter; -export const quarterList: Quarter[] = [quarter1, quarter2, quarterBacklog]; +export const quarterList: Quarter[] = [quarterBacklog, quarter1, quarter2]; export const checkInMetric: CheckInMin = { id: 815, @@ -364,6 +369,20 @@ export const objective: Objective = { quarterLabel: 'GJ 22/23-Q2', state: State.SUCCESSFUL, writeable: true, + alignedEntity: null, +}; + +export const objectiveWithAlignment: Objective = { + id: 5, + version: 1, + title: 'title', + description: 'description', + teamId: 2, + quarterId: 2, + quarterLabel: 'GJ 22/23-Q2', + state: State.SUCCESSFUL, + writeable: true, + alignedEntity: { id: 2, type: 'objective' }, }; export const objectiveWriteableFalse: Objective = { @@ -376,6 +395,7 @@ export const objectiveWriteableFalse: Objective = { quarterLabel: 'GJ 22/23-Q2', state: State.NOTSUCCESSFUL, writeable: false, + alignedEntity: null, }; export const firstCheckIn: CheckInMin = { @@ -615,3 +635,87 @@ export const keyResultActions: KeyResultMetric = { actionList: [action1, action2], writeable: true, }; + +export const alignmentPossibilityObject1: AlignmentPossibilityObject = { + objectId: 1, + objectTitle: 'We want to increase the income puzzle buy', + objectType: 'objective', +}; + +export const alignmentPossibilityObject2: AlignmentPossibilityObject = { + objectId: 2, + objectTitle: 'Our office has more plants for', + objectType: 'objective', +}; + +export const alignmentPossibilityObject3: AlignmentPossibilityObject = { + objectId: 1, + objectTitle: 'We buy 3 palms puzzle', + objectType: 'keyResult', +}; + +export const alignmentPossibility1: AlignmentPossibility = { + teamId: 1, + teamName: 'Puzzle ITC', + alignmentObjects: [alignmentPossibilityObject2, alignmentPossibilityObject3], +}; + +export const alignmentPossibility2: AlignmentPossibility = { + teamId: 2, + teamName: 'We are cube', + alignmentObjects: [alignmentPossibilityObject1], +}; + +export const alignmentObject1: AlignmentObject = { + objectId: 1, + objectTitle: 'Title 1', + objectTeamName: 'Example Team', + objectState: 'ONGOING', + objectType: 'objective', +}; + +export const alignmentObject2: AlignmentObject = { + objectId: 2, + objectTitle: 'Title 2', + objectTeamName: 'Example Team', + objectState: 'DRAFT', + objectType: 'objective', +}; + +export const alignmentObject3: AlignmentObject = { + objectId: 3, + objectTitle: 'Title 3', + objectTeamName: 'Example Team', + objectState: 'DRAFT', + objectType: 'objective', +}; + +export const alignmentObjectKeyResult: AlignmentObject = { + objectId: 102, + objectTitle: 'Title 1', + objectTeamName: 'Example Team', + objectState: null, + objectType: 'keyResult', +}; + +export const alignmentConnection: AlignmentConnection = { + alignedObjectiveId: 1, + targetObjectiveId: 2, + targetKeyResultId: null, +}; + +export const alignmentConnectionKeyResult: AlignmentConnection = { + alignedObjectiveId: 3, + targetObjectiveId: null, + targetKeyResultId: 102, +}; + +export const alignmentLists: AlignmentLists = { + alignmentObjectDtoList: [alignmentObject1, alignmentObject2], + alignmentConnectionDtoList: [alignmentConnection], +}; + +export const alignmentListsKeyResult: AlignmentLists = { + alignmentObjectDtoList: [alignmentObject3, alignmentObjectKeyResult], + alignmentConnectionDtoList: [alignmentConnectionKeyResult], +}; diff --git a/frontend/src/app/shared/types/enums/ObjectiveState.ts b/frontend/src/app/shared/types/enums/ObjectiveState.ts new file mode 100644 index 0000000000..342fa5a49d --- /dev/null +++ b/frontend/src/app/shared/types/enums/ObjectiveState.ts @@ -0,0 +1,6 @@ +export enum ObjectiveState { + DRAFT = 'DRAFT', + ONGOING = 'ONGOING', + SUCCESSFUL = 'SUCCESSFUL', + NOTSUCCESSFUL = 'NOTSUCCESSFUL', +} diff --git a/frontend/src/app/shared/types/model/AlignmentConnection.ts b/frontend/src/app/shared/types/model/AlignmentConnection.ts new file mode 100644 index 0000000000..1e6d5a6305 --- /dev/null +++ b/frontend/src/app/shared/types/model/AlignmentConnection.ts @@ -0,0 +1,5 @@ +export interface AlignmentConnection { + alignedObjectiveId: number; + targetObjectiveId: number | null; + targetKeyResultId: number | null; +} diff --git a/frontend/src/app/shared/types/model/AlignmentLists.ts b/frontend/src/app/shared/types/model/AlignmentLists.ts new file mode 100644 index 0000000000..c5d41d7ef9 --- /dev/null +++ b/frontend/src/app/shared/types/model/AlignmentLists.ts @@ -0,0 +1,7 @@ +import { AlignmentObject } from './AlignmentObject'; +import { AlignmentConnection } from './AlignmentConnection'; + +export interface AlignmentLists { + alignmentObjectDtoList: AlignmentObject[]; + alignmentConnectionDtoList: AlignmentConnection[]; +} diff --git a/frontend/src/app/shared/types/model/AlignmentObject.ts b/frontend/src/app/shared/types/model/AlignmentObject.ts new file mode 100644 index 0000000000..320f5222d7 --- /dev/null +++ b/frontend/src/app/shared/types/model/AlignmentObject.ts @@ -0,0 +1,7 @@ +export interface AlignmentObject { + objectId: number; + objectTitle: string; + objectTeamName: string; + objectState: string | null; + objectType: string; +} diff --git a/frontend/src/app/shared/types/model/AlignmentPossibility.ts b/frontend/src/app/shared/types/model/AlignmentPossibility.ts new file mode 100644 index 0000000000..e275888502 --- /dev/null +++ b/frontend/src/app/shared/types/model/AlignmentPossibility.ts @@ -0,0 +1,7 @@ +import { AlignmentPossibilityObject } from './AlignmentPossibilityObject'; + +export interface AlignmentPossibility { + teamId: number; + teamName: string; + alignmentObjects: AlignmentPossibilityObject[]; +} diff --git a/frontend/src/app/shared/types/model/AlignmentPossibilityObject.ts b/frontend/src/app/shared/types/model/AlignmentPossibilityObject.ts new file mode 100644 index 0000000000..86c7491742 --- /dev/null +++ b/frontend/src/app/shared/types/model/AlignmentPossibilityObject.ts @@ -0,0 +1,5 @@ +export interface AlignmentPossibilityObject { + objectId: number; + objectTitle: string; + objectType: string; +} diff --git a/frontend/src/app/shared/types/model/Objective.ts b/frontend/src/app/shared/types/model/Objective.ts index 4126c61fd5..c83e313057 100644 --- a/frontend/src/app/shared/types/model/Objective.ts +++ b/frontend/src/app/shared/types/model/Objective.ts @@ -14,4 +14,8 @@ export interface Objective { modifiedOn?: Date; createdBy?: User; writeable: boolean; + alignedEntity: { + id: number; + type: string; + } | null; } diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index 669da7212f..3721d7dd9f 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -53,7 +53,11 @@ "NOT_AUTHORIZED_TO_WRITE": "Du bist nicht autorisiert, um dieses {0} zu bearbeiten.", "NOT_AUTHORIZED_TO_DELETE": "Du bist nicht autorisiert, um dieses {0} zu löschen.", "TOKEN_NULL": "Das erhaltene Token ist null.", - "ILLEGAL_CHANGE_OBJECTIVE_QUARTER": "Element kann nicht in ein anderes Quartal verlegt werden." + "ILLEGAL_CHANGE_OBJECTIVE_QUARTER": "Element kann nicht in ein anderes Quartal verlegt werden.", + "NOT_LINK_YOURSELF": "Das Objective kann nicht auf sich selbst zeigen.", + "NOT_LINK_IN_SAME_TEAM": "Das Objective kann nicht auf ein Objekt im selben Team zeigen.", + "ALIGNMENT_ALREADY_EXISTS": "Es existiert bereits ein Alignment ausgehend vom Objective mit der ID {1}.", + "ALIGNMENT_DATA_FAIL": "Es gab ein Problem bei der Zusammenstellung der Daten für das Diagramm mit den Filter Quartal: {1}, Team: {2} und ObjectiveQuery: {3}." }, "SUCCESS": { "TEAM": {