diff --git a/backend/src/main/java/ch/puzzle/okr/Constants.java b/backend/src/main/java/ch/puzzle/okr/Constants.java index dcc7bb8a03..759b8170f9 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 QUARTER = "Quarter"; public static final String TEAM = "Team"; diff --git a/backend/src/main/java/ch/puzzle/okr/ErrorKey.java b/backend/src/main/java/ch/puzzle/okr/ErrorKey.java index f5a67168eb..b432701a95 100644 --- a/backend/src/main/java/ch/puzzle/okr/ErrorKey.java +++ b/backend/src/main/java/ch/puzzle/okr/ErrorKey.java @@ -4,5 +4,6 @@ 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, TRIED_TO_DELETE_LAST_ADMIN, TRIED_TO_REMOVE_LAST_OKR_CHAMPION + TOKEN_NULL, NOT_LINK_YOURSELF, NOT_LINK_IN_SAME_TEAM, ALIGNMENT_ALREADY_EXISTS, TRIED_TO_DELETE_LAST_ADMIN, + TRIED_TO_REMOVE_LAST_OKR_CHAMPION, ALIGNMENT_DATA_FAIL } 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/mapper/keyresult/KeyResultMetricMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/keyresult/KeyResultMetricMapper.java index e285304f06..0be03ca39b 100644 --- a/backend/src/main/java/ch/puzzle/okr/mapper/keyresult/KeyResultMetricMapper.java +++ b/backend/src/main/java/ch/puzzle/okr/mapper/keyresult/KeyResultMetricMapper.java @@ -3,8 +3,7 @@ import ch.puzzle.okr.dto.keyresult.*; import ch.puzzle.okr.mapper.ActionMapper; import ch.puzzle.okr.models.Action; -import ch.puzzle.okr.models.checkin.CheckIn; -import ch.puzzle.okr.models.checkin.CheckInMetric; +import ch.puzzle.okr.models.checkin.*; import ch.puzzle.okr.models.keyresult.KeyResult; import ch.puzzle.okr.models.keyresult.KeyResultMetric; import ch.puzzle.okr.service.business.CheckInBusinessService; @@ -85,7 +84,7 @@ public KeyResult toKeyResultMetric(KeyResultMetricDto keyResultMetricDto) { public KeyResultLastCheckInMetricDto getLastCheckInDto(Long keyResultId) { CheckIn lastCheckIn = checkInBusinessService.getLastCheckInByKeyResultId(keyResultId); - if (lastCheckIn == null) { + if (!(lastCheckIn instanceof CheckInMetric)) { return null; } return new KeyResultLastCheckInMetricDto(lastCheckIn.getId(), lastCheckIn.getVersion(), diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/keyresult/KeyResultOrdinalMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/keyresult/KeyResultOrdinalMapper.java index e62c93b482..772fa2a3bf 100644 --- a/backend/src/main/java/ch/puzzle/okr/mapper/keyresult/KeyResultOrdinalMapper.java +++ b/backend/src/main/java/ch/puzzle/okr/mapper/keyresult/KeyResultOrdinalMapper.java @@ -86,7 +86,7 @@ public KeyResult toKeyResultOrdinal(KeyResultOrdinalDto keyResultOrdinalDto) { public KeyResultLastCheckInOrdinalDto getLastCheckInDto(Long keyResultId) { CheckIn lastCheckIn = checkInBusinessService.getLastCheckInByKeyResultId(keyResultId); - if (lastCheckIn == null) { + if (!(lastCheckIn instanceof CheckInOrdinal)) { return null; } return new KeyResultLastCheckInOrdinalDto(lastCheckIn.getId(), lastCheckIn.getVersion(), 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 22dd21de0c..e1c48938ad 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 3d2fcf802e..b2596a7c15 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 2dc987df98..07824a3515 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,38 +29,124 @@ 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()); objective.setModifiedOn(LocalDateTime.now()); setQuarterIfIsImUsed(objective, savedObjective); - validator.validateOnUpdate(id, objective); - return objectivePersistenceService.save(objective); + return objective; } private void setQuarterIfIsImUsed(Objective objective, Objective savedObjective) { @@ -97,7 +186,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; } /** @@ -164,6 +257,7 @@ public void deleteEntityById(Long 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 095a5800ef..2c51f8e3a8 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 9e7a2bcd34..e1c7c0c8d2 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 @@ -60,6 +60,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-demo/afterMigrate__0_initialData.sql b/backend/src/main/resources/db/data-migration-demo/afterMigrate__0_initialData.sql index 0217f691c9..b8f412afd9 100644 --- a/backend/src/main/resources/db/data-migration-demo/afterMigrate__0_initialData.sql +++ b/backend/src/main/resources/db/data-migration-demo/afterMigrate__0_initialData.sql @@ -67,27 +67,27 @@ VALUES (1, 1, 1, 4, TRUE), insert into objective (id, description, modified_on, title, created_by_id, quarter_id, team_id, state, modified_by_id, created_on, version) -values (4, '', '2023-07-25 08:17:51.309958', 'Build a company culture that kills the competition.', 1, 7, 5, 'ONGOING', +values (4, '', '2023-07-25 08:17:51.309958', 'Build a company culture that kills the competition.', 1, 2, 5, 'ONGOING', 1, '2023-07-25 08:17:51.309958', 1), (3, 'Die Konkurenz nimmt stark zu, um weiterhin einen angenehmen Markt bearbeiten zu können, wollen wir die Kundenzufriedenheit steigern. ', - '2023-07-25 08:13:48.768262', 'Wir wollen die Kundenzufriedenheit steigern', 1, 7, 5, 'ONGOING', 1, + '2023-07-25 08:13:48.768262', 'Wir wollen die Kundenzufriedenheit steigern', 1, 2, 5, 'ONGOING', 1, '2023-07-25 08:13:48.768262', 1), (6, '', '2023-07-25 08:26:46.982010', - 'Als BBT wollen wir den Arbeitsalltag der Members von Puzzle ITC erleichtern.', 1, 7, 4, 'ONGOING', 1, + 'Als BBT wollen wir den Arbeitsalltag der Members von Puzzle ITC erleichtern.', 1, 2, 4, 'ONGOING', 1, '2023-07-25 08:26:46.982010', 1), (5, 'Damit wir nicht alle anderen Entwickler stören wollen wir so leise wie möglich arbeiten', - '2023-07-25 08:20:36.894258', 'Wir wollen das leiseste Team bei Puzzle sein', 1, 7, 4, 'NOTSUCCESSFUL', 1, + '2023-07-25 08:20:36.894258', 'Wir wollen das leiseste Team bei Puzzle sein', 1, 2, 4, 'NOTSUCCESSFUL', 1, '2023-07-25 08:20:36.894258', 1), (9, '', '2023-07-25 08:39:45.752126', '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.', - 1, 7, 6, 'NOTSUCCESSFUL', 1, '2023-07-25 08:39:45.752126', 1), + 1, 2, 6, 'NOTSUCCESSFUL', 1, '2023-07-25 08:39:45.752126', 1), (10, '', '2023-07-25 08:39:45.772126', - 'should not appear on staging, no sea takimata sanctus est Lorem ipsum dolor sit amet.', 1, 7, 6, 'SUCCESSFUL', + 'should not appear on staging, no sea takimata sanctus est Lorem ipsum dolor sit amet.', 1, 2, 6, 'SUCCESSFUL', 1, '2023-07-25 08:39:45.772126', 1), (8, '', '2023-07-25 08:39:28.175703', 'consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua', - 1, 7, 6, 'NOTSUCCESSFUL', 1, '2023-07-25 08:39:28.175703', 1); + 1, 2, 6, 'NOTSUCCESSFUL', 1, '2023-07-25 08:39:28.175703', 1); insert into completed (id, version, objective_id, comment) values (1, 1, 5, 'Not successful because there were many events this month'), 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 24cb7b36cf..ca24af460f 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 @@ -96,7 +96,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, 2, 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,2,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,2,4,'ONGOING',null,'2024-04-04 13:59:40.848992'), + (43,1,'','2024-04-04 14:00:05.586152',40,'Der Firmenumsatz steigt',1,2,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,2,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,2,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,2,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) @@ -115,7 +122,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), @@ -136,11 +145,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 cd6cfe0ed7..9320cd094c 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, @@ -261,3 +246,56 @@ ALTER TABLE IF EXISTS person_team ADD CONSTRAINT FK_person_team_person FOREIGN KEY (person_id) REFERENCES person; CREATE SEQUENCE IF NOT EXISTS sequence_person_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..1ce925b852 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,10 @@ 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 ch.puzzle.okr.test.TestConstants; import org.hamcrest.Matchers; import org.hamcrest.core.Is; import org.junit.jupiter.api.Test; @@ -13,17 +14,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.test.TestConstants.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -34,64 +32,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 c2d0fde2a8..fdbf6c2e68 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java @@ -1,10 +1,14 @@ 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; import ch.puzzle.okr.service.authorization.ObjectiveAuthorizationService; +import ch.puzzle.okr.test.TestConstants; import org.hamcrest.core.Is; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -23,9 +27,10 @@ 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 ch.puzzle.okr.test.TestConstants.TEAM_PUZZLE; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.*; @@ -38,12 +43,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 +70,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 +88,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 +113,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 +125,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 +145,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 +174,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 +191,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 +209,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 +254,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 +264,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 new file mode 100644 index 0000000000..e69de29bb2 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 84c1db2ff8..0000000000 --- a/backend/src/test/java/ch/puzzle/okr/mapper/AlignmentSelectionMapperTest.java +++ /dev/null @@ -1,92 +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 ch.puzzle.okr.test.TestConstants.TEAM_PUZZLE; -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(TEAM_PUZZLE).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(TEAM_PUZZLE).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(TEAM_PUZZLE).withObjectiveTitle("Objective 1") - .withKeyResultTitle("Key Result 3").build(), - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(5L).build()) - .withTeamId(2L).withTeamName(TEAM_PUZZLE).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(TEAM_PUZZLE).withObjectiveTitle("Objective 1") - .withKeyResultTitle("Key Result 3").build(), - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(5L).withKeyResultId(6L).build()) - .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 5") - .withKeyResultTitle("Key Result 6").build(), - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(9L).build()) - .withTeamId(2L).withTeamName(TEAM_PUZZLE).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/ObjectiveMapperTest.java b/backend/src/test/java/ch/puzzle/okr/mapper/ObjectiveMapperTest.java index 35ce1d1cc7..b4b30f8414 100644 --- a/backend/src/test/java/ch/puzzle/okr/mapper/ObjectiveMapperTest.java +++ b/backend/src/test/java/ch/puzzle/okr/mapper/ObjectiveMapperTest.java @@ -113,8 +113,8 @@ void toObjectiveShouldMapDtoToObjective() { STATE, // CREATE_DATE_TIME, // MODIFIED_DATE_TIME, // - IS_WRITEABLE // - ); + IS_WRITEABLE, // + null); // mock (LocalDateTime.now()) + act + assert try (MockedStatic mockedStatic = Mockito.mockStatic(LocalDateTime.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 04b72412f9..c8965a0668 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; @@ -12,8 +14,12 @@ import org.springframework.http.HttpStatus; import org.springframework.web.server.ResponseStatusException; +import java.util.List; + +import static ch.puzzle.okr.test.TestConstants.TEAM_PUZZLE; import static ch.puzzle.okr.test.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; @@ -26,87 +32,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.updateOrAddAuthorizationUser()).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.updateOrAddAuthorizationUser()).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.updateOrAddAuthorizationUser()).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.updateOrAddAuthorizationUser()).thenReturn(authorizationUser); when(authorizationService.hasRoleWriteForTeam(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.updateOrAddAuthorizationUser()).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.updateOrAddAuthorizationUser()).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.updateOrAddAuthorizationUser()).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()); } @@ -128,18 +169,52 @@ void deleteEntityByIdShouldPassThroughWhenAuthorized() { @Test void deleteEntityByIdShouldThrowExceptionWhenNotAuthorized() { + // arrange Long id = 13L; - String reason = "junit test reason"; + String reason = JUNIT_TEST_REASON; when(authorizationService.updateOrAddAuthorizationUser()).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()); + } + @DisplayName("duplicateEntity() should throw exception when not authorized") @Test void duplicateEntityShouldThrowExceptionWhenNotAuthorized() { 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..f0127d6ef5 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java @@ -0,0 +1,141 @@ +package ch.puzzle.okr.service.business; + +import ch.puzzle.okr.dto.alignment.AlignmentLists; +import ch.puzzle.okr.multitenancy.TenantContext; +import ch.puzzle.okr.test.SpringIntegrationTest; +import ch.puzzle.okr.test.TestHelper; +import org.junit.jupiter.api.BeforeEach; +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"; + + @BeforeEach + void setUp() { + TenantContext.setCurrentTenant(TestHelper.SCHEMA_PITC); + } + + @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..79f652b4cb --- /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.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 ch.puzzle.okr.test.TestHelper; +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 29b57b5aed..0000000000 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessServiceTest.java +++ /dev/null @@ -1,43 +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 ch.puzzle.okr.test.TestConstants.TEAM_PUZZLE; -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(TEAM_PUZZLE).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 d23d3b61d8..da526ecee5 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 ce4db63f9c..f49b2b4473 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; @@ -7,6 +11,7 @@ import ch.puzzle.okr.models.keyresult.KeyResultOrdinal; import ch.puzzle.okr.service.persistence.ObjectivePersistenceService; import ch.puzzle.okr.service.validation.ObjectiveValidationService; +import ch.puzzle.okr.test.TestConstants; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -16,11 +21,13 @@ 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; import java.util.List; +import static ch.puzzle.okr.test.TestConstants.TEAM_PUZZLE; import static ch.puzzle.okr.test.TestHelper.defaultAuthorizationUser; import static ch.puzzle.okr.models.State.DRAFT; import static org.assertj.core.api.Assertions.assertThat; @@ -40,62 +47,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") .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 +273,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 +307,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,16 +317,21 @@ 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 @@ -196,4 +375,96 @@ void shouldDuplicateObjective() { // called for creating the new KeyResults verify(keyResultBusinessService, times(2)).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 de38eed3a5..58d3021d4c 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.multitenancy.TenantContext; import ch.puzzle.okr.test.SpringIntegrationTest; @@ -18,8 +19,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 @@ -27,6 +30,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) @@ -66,10 +70,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()); @@ -77,10 +84,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()); @@ -88,56 +98,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); @@ -148,15 +229,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 60ab85c7d9..0000000000 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceServiceIT.java +++ /dev/null @@ -1,62 +0,0 @@ -package ch.puzzle.okr.service.persistence; - -import ch.puzzle.okr.test.TestHelper; -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.models.alignment.AlignmentSelectionId; -import ch.puzzle.okr.multitenancy.TenantContext; -import ch.puzzle.okr.test.SpringIntegrationTest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -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; - - @BeforeEach - void setUp() { - TenantContext.setCurrentTenant(TestHelper.SCHEMA_PITC); - } - - @AfterEach - void tearDown() { - TenantContext.setCurrentTenant(null); - } - - @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..5cc608d253 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceServiceIT.java @@ -0,0 +1,61 @@ +package ch.puzzle.okr.service.persistence; + +import ch.puzzle.okr.models.alignment.AlignmentView; +import ch.puzzle.okr.multitenancy.TenantContext; +import ch.puzzle.okr.test.SpringIntegrationTest; +import ch.puzzle.okr.test.TestHelper; +import org.junit.jupiter.api.BeforeEach; +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); + + @BeforeEach + void setUp() { + TenantContext.setCurrentTenant(TestHelper.SCHEMA_PITC); + } + + @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/ObjectivePersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java index 9708e9d779..0044a70bb8 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 @@ -277,4 +277,18 @@ private void assertObjective(Long expectedId, String expectedTitle, Objective cu assertEquals(expectedTitle, currentObjective.getTitle()); } + @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..91fc7c71f9 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java @@ -0,0 +1,463 @@ +package ch.puzzle.okr.service.validation; + +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 ch.puzzle.okr.test.TestHelper; +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 ch.puzzle.okr.test.TestConstants.TEAM_PUZZLE; +import static ch.puzzle.okr.test.TestHelper.*; +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 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(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 642a999219..a05c5fd405 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 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/cypress/e2e/crud.cy.ts b/frontend/cypress/e2e/crud.cy.ts new file mode 100644 index 0000000000..e69de29bb2 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/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/package-lock.json b/frontend/package-lock.json index 855d2e88ed..2b364b847e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,6 +23,7 @@ "@ngx-translate/http-loader": "^16.0.0", "angular-oauth2-oidc": "^17.0.2", "bootstrap": "^5.3.3", + "cytoscape": "^3.28.1", "moment": "^2.30.1", "ngx-toastr": "^19.0.0", "rxjs": "^7.8.1", @@ -34,6 +35,7 @@ "@angular/compiler-cli": "^18.2.8", "@cypress/schematic": "^2.5.2", "@cypress/skip-test": "^2.6.1", + "@types/cytoscape": "^3.21.0", "@types/jest": "^29.5.13", "@types/uuid": "^10.0.0", "browserslist": "^4.24.2", @@ -5481,6 +5483,13 @@ "@types/node": "*" } }, + "node_modules/@types/cytoscape": { + "version": "3.21.8", + "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.8.tgz", + "integrity": "sha512-6Bo9ZDrv0vfwe8Sg/ERc5VL0yU0gYvP4dgZi0fAXYkKHfyHaNqWRMcwYm3mu4sLsXbB8ZuXE75sR7qnaOL5JgQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -8481,6 +8490,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/cytoscape": { + "version": "3.30.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.3.tgz", + "integrity": "sha512-HncJ9gGJbVtw7YXtIs3+6YAFSSiKsom0amWc33Z7QbylbY2JGMrA0yz4EwrdTScZxnwclXeEZHzO5pxoy0ZE4g==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index accbcefc7f..8955f952a2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,7 @@ "@ngx-translate/http-loader": "^16.0.0", "angular-oauth2-oidc": "^17.0.2", "bootstrap": "^5.3.3", + "cytoscape": "^3.28.1", "moment": "^2.30.1", "ngx-toastr": "^19.0.0", "rxjs": "^7.8.1", @@ -48,6 +49,7 @@ "@angular/compiler-cli": "^18.2.8", "@cypress/schematic": "^2.5.2", "@cypress/skip-test": "^2.6.1", + "@types/cytoscape": "^3.21.0", "@types/jest": "^29.5.13", "@types/uuid": "^10.0.0", "browserslist": "^4.24.2", diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index cd2e70a8d3..d6b9a00a56 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -69,6 +69,7 @@ import { ApplicationTopBarComponent } from './components/application-top-bar/app import { A11yModule } from '@angular/cdk/a11y'; import { CustomizationService } from './services/customization.service'; import { MetricCheckInDirective } from './components/checkin/check-in-form-metric/metric-check-in-directive'; +import { DiagramComponent } from './diagram/diagram.component'; function initOauthFactory(configService: ConfigService, oauthService: OAuthService) { return async () => { @@ -125,6 +126,7 @@ export const MY_FORMATS = { CheckInFormOrdinalComponent, CheckInFormComponent, MetricCheckInDirective, + DiagramComponent, ], bootstrap: [AppComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], diff --git a/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.ts b/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.ts index 19b1866b44..5351b423ff 100644 --- a/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.ts +++ b/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.ts @@ -42,7 +42,7 @@ export class CheckInHistoryDialogComponent implements OnInit { checkIn: checkIn, }, }); - dialogRef.afterClosed().subscribe(() => { + dialogRef.afterClosed().subscribe((result) => { this.loadCheckInHistory(); this.refreshDataService.reloadKeyResultSubject.next(); this.refreshDataService.markDataRefresh(); diff --git a/frontend/src/app/components/checkin/check-in-form-metric/check-in-form-metric.component.spec.ts b/frontend/src/app/components/checkin/check-in-form-metric/check-in-form-metric.component.spec.ts index 6287808fcb..9f8435cda1 100644 --- a/frontend/src/app/components/checkin/check-in-form-metric/check-in-form-metric.component.spec.ts +++ b/frontend/src/app/components/checkin/check-in-form-metric/check-in-form-metric.component.spec.ts @@ -10,6 +10,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { Unit } from '../../../shared/types/enums/Unit'; import { TranslateTestingModule } from 'ngx-translate-testing'; +// @ts-ignore import * as de from '../../../../assets/i18n/de.json'; describe('CheckInFormComponent', () => { diff --git a/frontend/src/app/components/checkin/check-in-form/check-in-form.component.ts b/frontend/src/app/components/checkin/check-in-form/check-in-form.component.ts index 8156ac14ad..ae388f1105 100644 --- a/frontend/src/app/components/checkin/check-in-form/check-in-form.component.ts +++ b/frontend/src/app/components/checkin/check-in-form/check-in-form.component.ts @@ -87,7 +87,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/components/objective-detail/objective-detail.component.ts b/frontend/src/app/components/objective-detail/objective-detail.component.ts index c5dfa7e138..87ff958f2d 100644 --- a/frontend/src/app/components/objective-detail/objective-detail.component.ts +++ b/frontend/src/app/components/objective-detail/objective-detail.component.ts @@ -58,6 +58,7 @@ export class ObjectiveDetailComponent { .subscribe((result) => { if (result?.openNew) { this.openAddKeyResultDialog(); + return; } this.refreshDataService.markDataRefresh(); }); @@ -76,8 +77,10 @@ export class ObjectiveDetailComponent { .afterClosed() .subscribe((result) => { this.refreshDataService.markDataRefresh(); - if (result.delete) { + if (result && result.delete) { this.backToOverview(); + } else if (result == '' || result == undefined) { + return; } else { this.loadObjective(this.objective$.value.id); } diff --git a/frontend/src/app/components/overview/overview.component.html b/frontend/src/app/components/overview/overview.component.html index 7103ade284..87bedf0c8a 100644 --- a/frontend/src/app/components/overview/overview.component.html +++ b/frontend/src/app/components/overview/overview.component.html @@ -1,13 +1,54 @@
- - -
-

Kein Team ausgewählt

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

Kein Team ausgewählt

+ +
+
+ +
+ +
diff --git a/frontend/src/app/components/overview/overview.component.scss b/frontend/src/app/components/overview/overview.component.scss index 8fa50ca098..9fac80c61f 100644 --- a/frontend/src/app/components/overview/overview.component.scss +++ b/frontend/src/app/components/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/components/overview/overview.component.spec.ts b/frontend/src/app/components/overview/overview.component.spec.ts index 44a584d46a..da91c32396 100644 --- a/frontend/src/app/components/overview/overview.component.spec.ts +++ b/frontend/src/app/components/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 '../../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 '../../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/components/overview/overview.component.ts b/frontend/src/app/components/overview/overview.component.ts index b5c2b9158d..902dca1186 100644 --- a/frontend/src/app/components/overview/overview.component.ts +++ b/frontend/src/app/components/overview/overview.component.ts @@ -13,9 +13,11 @@ import { } from 'rxjs'; import { OverviewService } from '../../services/overview.service'; import { ActivatedRoute } from '@angular/router'; -import { RefreshDataService } from '../../services/refresh-data.service'; import { getQueryString, getValueFromQuery, isMobileDevice, trackByFn } from '../../shared/common'; import { ConfigService } from '../../services/config.service'; +import { AlignmentService } from '../../services/alignment.service'; +import { RefreshDataService } from '../../services/refresh-data.service'; +import { AlignmentLists } from '../../shared/types/model/AlignmentLists'; @Component({ selector: 'app-overview', @@ -25,14 +27,17 @@ import { ConfigService } from '../../services/config.service'; }) export class OverviewComponent implements OnInit, OnDestroy { overviewEntities$: Subject = new Subject(); + alignmentLists$: Subject = new Subject(); protected readonly trackByFn = trackByFn; private destroyed$: ReplaySubject = new ReplaySubject(1); overviewPadding: Subject = new Subject(); + isOverview: boolean = true; backgroundLogoSrc$ = new BehaviorSubject('assets/images/empty.svg'); constructor( private overviewService: OverviewService, + private alignmentService: AlignmentService, private refreshDataService: RefreshDataService, private activatedRoute: ActivatedRoute, private changeDetector: ChangeDetectorRef, @@ -82,7 +87,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( @@ -96,8 +109,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/components/quarter-filter/quarter-filter.component.spec.ts b/frontend/src/app/components/quarter-filter/quarter-filter.component.spec.ts index bcd2e4bdd5..412c44bc53 100644 --- a/frontend/src/app/components/quarter-filter/quarter-filter.component.spec.ts +++ b/frontend/src/app/components/quarter-filter/quarter-filter.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, tick } from '@angular/core/testing'; import { QuarterFilterComponent } from './quarter-filter.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { OverviewService } from '../../services/overview.service'; @@ -7,6 +7,7 @@ import { Quarter } from '../../shared/types/model/Quarter'; import { QuarterService } from '../../services/quarter.service'; import { RouterTestingHarness, RouterTestingModule } from '@angular/router/testing'; import { FormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 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..ccfd0ca4e3 --- /dev/null +++ b/frontend/src/app/diagram/diagram.component.spec.ts @@ -0,0 +1,200 @@ +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 '../services/keyresult.service'; + +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 }], + }); + 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..c05dfcaf83 --- /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 { 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 { KeyresultService } from '../services/keyresult.service'; +import { RefreshDataService } from '../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 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/services/alignment.service.spec.ts b/frontend/src/app/services/alignment.service.spec.ts new file mode 100644 index 0000000000..d00824a57e --- /dev/null +++ b/frontend/src/app/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 '../shared/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/services/alignment.service.ts b/frontend/src/app/services/alignment.service.ts new file mode 100644 index 0000000000..a8d43c36d7 --- /dev/null +++ b/frontend/src/app/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 '../shared/types/model/AlignmentLists'; +import { optionalValue } from '../shared/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/services/objective.service.ts b/frontend/src/app/services/objective.service.ts index e0d49f4460..6b940482f9 100644 --- a/frontend/src/app/services/objective.service.ts +++ b/frontend/src/app/services/objective.service.ts @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { Objective } from '../shared/types/model/Objective'; import { Observable } from 'rxjs'; import { Completed } from '../shared/types/model/Completed'; +import { AlignmentPossibility } from '../shared/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/services/refresh-data.service.ts b/frontend/src/app/services/refresh-data.service.ts index 3c1a1fb321..a354dc6adf 100644 --- a/frontend/src/app/services/refresh-data.service.ts +++ b/frontend/src/app/services/refresh-data.service.ts @@ -8,13 +8,15 @@ import { DEFAULT_HEADER_HEIGHT_PX } from '../shared/constantLibary'; export class RefreshDataService { public reloadOverviewSubject: Subject = new Subject(); public reloadKeyResultSubject: 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/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index f6a1b30d0e..6cd3b0c600 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,4 +1,4 @@ - +
@@ -41,6 +41,7 @@ class="custom-select bg-white" formControlName="quarter" id="quarter" + (change)="updateAlignments()" [attr.data-testId]="'quarterSelect'" > @@ -51,21 +52,49 @@
- -
-
- -

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.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index 8e683e9a4e..00c2d7d846 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 @@ -9,7 +9,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 { marketingTeamWriteable, objective, quarter, quarterList } from '../../testData'; +import { + alignmentPossibility1, + alignmentPossibility2, + alignmentPossibilityObject1, + alignmentPossibilityObject2, + alignmentPossibilityObject3, + marketingTeamWriteable, + objective, + objectiveWithAlignment, + quarter, + quarterList, +} from '../../testData'; import { Observable, of } from 'rxjs'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { HarnessLoader } from '@angular/cdk/testing'; @@ -25,17 +36,21 @@ import { RouterTestingHarness } from '@angular/router/testing'; import { TranslateTestingModule } from 'ngx-translate-testing'; // @ts-ignore import * as de from '../../../../assets/i18n/de.json'; -import { ActivatedRoute, provideRouter } from '@angular/router'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { DialogTemplateCoreComponent } from '../../custom/dialog-template-core/dialog-template-core.component'; import { MatDividerModule } from '@angular/material/divider'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { AlignmentPossibility } from '../../types/model/AlignmentPossibility'; +import { ElementRef } from '@angular/core'; +import { ActivatedRoute, provideRouter } from '@angular/router'; let objectiveService = { getFullObjective: jest.fn(), createObjective: jest.fn(), updateObjective: jest.fn(), deleteObjective: jest.fn(), + getAlignmentPossibilities: jest.fn(), }; interface MatDialogDataInterface { @@ -98,6 +113,7 @@ describe('ObjectiveDialogComponent', () => { MatSelectModule, ReactiveFormsModule, MatInputModule, + MatAutocompleteModule, NoopAnimationsModule, MatCheckboxModule, TranslateTestingModule.withTranslations({ @@ -117,6 +133,7 @@ describe('ObjectiveDialogComponent', () => { { provide: TeamService, useValue: teamService }, ], }); + jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of([])); fixture = TestBed.createComponent(ObjectiveFormComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -130,6 +147,8 @@ describe('ObjectiveDialogComponent', () => { it.each([['DRAFT'], ['ONGOING']])( 'onSubmit create', fakeAsync((state: string) => { + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + //Prepare data let title: string = 'title'; let description: string = 'description'; @@ -140,7 +159,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 @@ -185,6 +204,7 @@ describe('ObjectiveDialogComponent', () => { teamId: 2, title: title, writeable: true, + alignedEntity: null, }, teamId: 1, }); @@ -198,7 +218,7 @@ describe('ObjectiveDialogComponent', () => { description: 'Test description', quarter: 0, team: 0, - relation: 0, + alignment: null, createKeyResults: false, }); @@ -214,6 +234,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' }, }); }); @@ -224,7 +302,7 @@ describe('ObjectiveDialogComponent', () => { description: 'Test description', quarter: 1, team: 1, - relation: 0, + alignment: null, createKeyResults: false, }); @@ -240,6 +318,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' }, }); }); @@ -270,10 +380,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(); @@ -287,6 +412,7 @@ describe('ObjectiveDialogComponent', () => { expect(component.allowedToSaveBacklog()).toBeTruthy(); component.state = 'ONGOING'; + isBacklogQuarterSpy.mockReturnValue(false); fixture.detectChanges(); expect(component.allowedToSaveBacklog()).toBeFalsy(); @@ -346,7 +472,7 @@ describe('ObjectiveDialogComponent', () => { }); }); - describe('Backlog quarter', () => { + describe('AlignmentPossibilities', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ @@ -356,14 +482,14 @@ describe('ObjectiveDialogComponent', () => { MatSelectModule, ReactiveFormsModule, MatInputModule, + MatAutocompleteModule, NoopAnimationsModule, MatCheckboxModule, TranslateTestingModule.withTranslations({ de: de, }), - MatDividerModule, ], - declarations: [ObjectiveFormComponent, DialogTemplateCoreComponent], + declarations: [ObjectiveFormComponent], providers: [ provideRouter([]), provideHttpClient(), @@ -373,9 +499,10 @@ describe('ObjectiveDialogComponent', () => { { 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; fixture.detectChanges(); @@ -386,7 +513,194 @@ 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: [ + MatDialogModule, + MatIconModule, + MatFormFieldModule, + MatSelectModule, + ReactiveFormsModule, + MatInputModule, + MatAutocompleteModule, + NoopAnimationsModule, + MatCheckboxModule, + TranslateTestingModule.withTranslations({ + de: de, + }), + MatDividerModule, + ], + declarations: [ObjectiveFormComponent, DialogTemplateCoreComponent], + providers: [ + provideRouter([]), + provideHttpClient(), + provideHttpClientTesting(), + { 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, @@ -394,8 +708,16 @@ describe('ObjectiveDialogComponent', () => { }, action: 'releaseBacklog', }; + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); - const isBacklogQuarterSpy = jest.spyOn(component, 'isBacklogQuarter'); + it.skip('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 8bb93a22bd..dd5175cc5b 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, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ElementRef, Inject, OnDestroy, 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, takeUntil } from 'rxjs'; +import { BehaviorSubject, forkJoin, Observable, of, Subject, takeUntil } from 'rxjs'; import { ObjectiveService } from '../../../services/objective.service'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { State } from '../../types/enums/State'; @@ -16,6 +16,8 @@ import { ActivatedRoute } from '@angular/router'; import { GJ_REGEX_PATTERN } from '../../constantLibary'; import { TranslateService } from '@ngx-translate/core'; import { DialogService } from '../../../services/dialog.service'; +import { AlignmentPossibility } from '../../types/model/AlignmentPossibility'; +import { AlignmentPossibilityObject } from '../../types/model/AlignmentPossibilityObject'; @Component({ selector: 'app-objective-form', @@ -24,19 +26,22 @@ import { DialogService } from '../../../services/dialog.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ObjectiveFormComponent implements OnInit, OnDestroy { + @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([]); currentQuarter$: 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; @@ -64,6 +69,15 @@ export class ObjectiveFormComponent implements OnInit, OnDestroy { 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, @@ -72,6 +86,7 @@ export class ObjectiveFormComponent implements OnInit, OnDestroy { title: value.title, teamId: value.team, state: state, + alignedEntity: alignedEntity, } as unknown as Objective; const submitFunction = this.getSubmitFunction(objectiveDTO.id, objectiveDTO); @@ -94,15 +109,16 @@ export class ObjectiveFormComponent implements OnInit, OnDestroy { const newEditQuarter = isCreating ? currentQuarter.id : objective.quarterId; let quarterId = getValueFromQuery(this.route.snapshot.queryParams['quarter'], newEditQuarter)[0]; - 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.id == teamId)[0]); }); + this.generateAlignmentPossibilities(quarterId, objective, teamId!); this.objectiveForm.patchValue({ title: objective.title, @@ -178,6 +194,7 @@ export class ObjectiveFormComponent implements OnInit, OnDestroy { state: 'DRAFT' as State, teamId: 0, quarterId: 0, + alignedEntity: null, } as Objective; } @@ -186,12 +203,12 @@ export class ObjectiveFormComponent implements OnInit, OnDestroy { (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; @@ -214,7 +231,7 @@ export class ObjectiveFormComponent implements OnInit, OnDestroy { } } - isBacklogQuarter(label: string) { + isNotBacklogQuarter(label: string) { return GJ_REGEX_PATTERN.test(label); } @@ -237,4 +254,138 @@ export class ObjectiveFormComponent implements OnInit, OnDestroy { return ''; } + 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; + } } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 19f96e5572..bea4f5d1cd 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -24,6 +24,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { DialogTemplateCoreComponent } from './custom/dialog-template-core/dialog-template-core.component'; import { MatDividerModule } from '@angular/material/divider'; import { UnitTransformationPipe } from './pipes/unit-transformation/unit-transformation.pipe'; +import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; @NgModule({ declarations: [ @@ -58,6 +59,8 @@ import { UnitTransformationPipe } from './pipes/unit-transformation/unit-transfo RouterOutlet, MatProgressSpinnerModule, MatDividerModule, + MatAutocompleteTrigger, + MatAutocomplete, ], exports: [ ExampleDialogComponent, diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index 48ba98a0de..902a0916a8 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -15,6 +15,11 @@ import { KeyResultMetric } from './types/model/KeyResultMetric'; import { Unit } from './types/enums/Unit'; import { Team } from './types/model/Team'; import { Action } from './types/model/Action'; +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 teamFormObject = { name: 'newTeamName', @@ -98,7 +103,7 @@ export const quarter2: Quarter = new Quarter(2, 'GJ 22/23-Q3', new Date('2023-01 export const quarterBacklog: Quarter = new Quarter(999, 'GJ 23/24-Q1', null, null); -export const quarterList: Quarter[] = [quarter1, quarter2, quarterBacklog]; +export const quarterList: Quarter[] = [quarterBacklog, quarter1, quarter2]; export const checkInMetric: CheckInMin = { id: 815, @@ -319,6 +324,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 = { @@ -331,6 +350,7 @@ export const objectiveWriteableFalse: Objective = { quarterLabel: 'GJ 22/23-Q2', state: State.NOTSUCCESSFUL, writeable: false, + alignedEntity: null, }; export const firstCheckIn: CheckInMin = { @@ -565,3 +585,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/app/team-management/show-edit-role/show-edit-role.component.spec.ts b/frontend/src/app/team-management/show-edit-role/show-edit-role.component.spec.ts index 066a1fa945..3b54cb83b2 100644 --- a/frontend/src/app/team-management/show-edit-role/show-edit-role.component.spec.ts +++ b/frontend/src/app/team-management/show-edit-role/show-edit-role.component.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testin import { ShowEditRoleComponent } from './show-edit-role.component'; import { testUser } from '../../shared/testData'; import { TranslateTestingModule } from 'ngx-translate-testing'; +// @ts-ignore import * as de from '../../../assets/i18n/de.json'; describe('ShowEditRoleComponent', () => { diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index be9c8d61a4..626fa7f256 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -91,8 +91,12 @@ "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.", + "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}.", "TRIED_TO_DELETE_LAST_ADMIN": "Der letzte Administrator eines Teams kann nicht entfernt werden", - "TRIED_TO_REMOVE_LAST_OKR_CHAMPION": "Der letzte OKR Champion kann nicht entfernt werden" + "TRIED_TO_REMOVE_LAST_OKR_CHAMPION": "Der letzte OKR Champion kann nicht entfernt werden", + "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": {