From 05d092c4887f446de2a0c027d2b6df04aca310a7 Mon Sep 17 00:00:00 2001 From: Laurent Fasani Date: Thu, 3 Sep 2026 15:54:29 +0200 Subject: [PATCH 1/3] [98] Change the way the impacted tasks are calculated All impacted tasks have their effort kept unchanged whatever the calculation option. That means that a constraining task boundary can be changed during update. The dependency link is stronger than the internal constraint. Issue: https://github.com/ObeoNetwork/pepper/issues/98 --- CHANGELOG.adoc | 1 + README.adoc | 2 +- backend/pepper-domain-services/README.md | 51 +- backend/pepper-domain-services/pom.xml | 16 +- .../services/NonWorkingDaysService.java | 72 +- .../services/TaskComputationService.java | 6 +- .../pepper/domain/services/TaskHelper.java | 99 +++ .../domain/services/TemporalHelper.java | 42 + .../WorkpackageComputationService.java | 6 +- .../ComputeDynamicallyChangeUpdateStep.java | 49 ++ .../update/CreateDependencyUpdateStep.java | 64 ++ .../services/update/DependencyUpdateStep.java | 137 ++++ .../services/update/EffortUpdateStep.java | 84 ++ .../services/update/ParentUpdateStep.java | 63 ++ .../update/TaskBoundaryUpdateStep.java | 137 ++++ .../services/update/TaskUpdateService.java | 121 +++ .../services/update/TaskUpdateStep.java | 41 + .../services/NonWorkingDaysServiceTests.java | 136 +++- .../update/TaskUpdateStepEqualityTests.java | 80 ++ .../peppermm/impl/AbstractTaskImpl.java | 240 +++--- ...pperMMProjectTemplateInitializerEmpty.java | 14 +- ...perMMProjectTemplateInitializerSample.java | 13 +- .../project/PepperMMSampleBuilder.java | 10 +- .../project/PepperMMSamplesProvider.java | 15 +- .../representations/PepperMMJavaService.java | 760 ++---------------- .../AbstractTaskPropertiesConfigurer.java | 80 +- .../DependencyLinkPropertiesConfigurer.java | 31 +- .../WorkpackagePropertiesConfigurer.java | 42 +- .../gantt/ViewGanttDescriptionBuilder.java | 2 +- .../PepperMMSampleBuilderTests.java | 6 +- .../view/EditDependenciesServiceTests.java | 3 +- .../view/PepperMMJavaServiceTests.java | 98 +-- 32 files changed, 1473 insertions(+), 1048 deletions(-) create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskHelper.java create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/TemporalHelper.java create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/update/ComputeDynamicallyChangeUpdateStep.java create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/update/CreateDependencyUpdateStep.java create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/update/DependencyUpdateStep.java create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/update/EffortUpdateStep.java create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/update/ParentUpdateStep.java create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskBoundaryUpdateStep.java create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateService.java create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateStep.java create mode 100644 backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateStepEqualityTests.java diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 76e4a35..a2ce880 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -17,6 +17,7 @@ - https://github.com/ObeoNetwork/pepper/issues/89[#89] Make duration (AbstractTask and Workpackage) computed from non working days. For now, it corresponds to week-end and french public holidays 2026. - https://github.com/ObeoNetwork/pepper/issues/94[#94] Rename the dependency link duration to dependency link delay and display the delay in days - https://github.com/ObeoNetwork/pepper/issues/60[#60] Take Resource (including Unavailability period) into account for effort commutation +- https://github.com/ObeoNetwork/pepper/issues/98[#98] Change the way the impacted tasks are calculated. Impacted tasks are always moved (effort is kept) === Bug fixes diff --git a/README.adoc b/README.adoc index 7b19a17..dcf2f29 100644 --- a/README.adoc +++ b/README.adoc @@ -17,7 +17,7 @@ Follow the links below for more informations : * https://blog.obeosoft.com/pepper-a-case-of-open-innovation-with-sirius-web[Blog post presenting Pepper] -= Starting with Pepper +== Starting with Pepper This repository is a mono repo containing both frontend and backend components. diff --git a/backend/pepper-domain-services/README.md b/backend/pepper-domain-services/README.md index 33f94ef..eb6a3cf 100644 --- a/backend/pepper-domain-services/README.md +++ b/backend/pepper-domain-services/README.md @@ -3,6 +3,23 @@ Pepper only supports a single time zone. The server and the browser must use the same time zone. + +## Upadate philosophy + +There are two approaches to solve the update. + +### Approach 1: Full update + +Except if there is a cycle that is detected, the update should always succeed. + +The philophy is "I know what I am doing, so I let the algorithm change task boundaries I specifically set in the model" + +### Approach 2: Validation + +"I don't want the update to change task boundaries I specifically set in the model" + +If this occurs, then conflicting changes are not applied until the user cancel or accept the changes. + ## Considering task constraints A task is constrained by two `TimeConstraint` values among: @@ -12,19 +29,16 @@ A task is constrained by two `TimeConstraint` values among: - `EFFORT` If a constrained boundary (`START` or `END`) is also constrained by a dependency, the dependency constraint is considered stronger than the task constraint. -In that case, the effort is no longer considered constraining. +A boundary constrained by a dependency can not be changed by a user. -A boundary constrained by a dependency can not be changed directly. - -For example: A START-EFFORT task has its end date constrained by a dependency. If the start date is moved, the end date remains unchanged and the effort is updated accordingly. -## Task bounds computation +## Task boundaries computation -An AbstractTask has its bounds defined as Instant. -When modifying the task, either from Gantt, details view or by the algorithm, the AbstractTask bounds are rounded to the closest half-day. +An AbstractTask has its boundaries defined as Instant. +When modifying the task, either from Gantt, details view or by the algorithm, the AbstractTask boundaries are rounded to the closest half-day. Non-working days (in week and configured fixed non-working days) do not consume any effort. -The workpackage has its bounds defined as LocalDate +The workpackage has its boundaries defined as LocalDate. Both workpackage startDate and endDate are included. ### Task with assigned persons @@ -36,16 +50,19 @@ If no person is assigned, one working day consume an effort of one day. ## Gantt interactions -### Moving a task - -- If the task is constrained by dependencies nothing is done. -- Otherwise, the constraining boundary or boundaries are updated according to the task calculation option: - -- `START-END`: both boundaries are updated. The effort may change if the number of included non-working days changes. - -- `START-EFFORT` and `END-EFFORT`: the constraining boundary is updated. The constrained boundary may move by more than the drag delta if the number of included non-working days changes, because the effort is preserved. - ### Changing one task boundary -- If the boundary is constraining, this boundary is updated and the other constraint is preserved. -- If the opposite boundary is constrained by a dependency, the moved boundary is updated and the effort is updated as well. +- If the changed boundary is constraining, this moved boundary is updated and the other constraint is preserved. +- If the changed boundary is not constraining or the opposite boundary is constrained by a dependency, then the moved boundary is updated along with the effort. - Otherwise, the change is interpreted as a user intent to update the effort by the move delta. -- [FUTURE ENHANCEMENT] A global option could forbid changing a non-constraining boundary directly. In that mode, moving such a task boundary would not be allowed. + +For example: A START-EFFORT task has its end date constrained by a dependency. If the start date is moved, the end date remains unchanged and the effort is updated accordingly. + +In any case, the impacted tasks are updating keeping the effort unchanged + -- [FUTURE ENHANCEMENT] Clicking on an alternative key while updating the task, the impacted task would be updated according to their constraint. + +### Moving a task + +- If the task is constrained by dependencies nothing is done. +- Otherwise, the moved task and all the impacted tasks are updating keeping the effort unchanged. diff --git a/backend/pepper-domain-services/pom.xml b/backend/pepper-domain-services/pom.xml index 35f204a..a9d2629 100644 --- a/backend/pepper-domain-services/pom.xml +++ b/backend/pepper-domain-services/pom.xml @@ -11,8 +11,8 @@ Contributors: Obeo - initial API and implementation --> - 4.0.0 @@ -34,6 +34,18 @@ org.eclipse.core org.eclipse.core.runtime + + org.eclipse.emf + org.eclipse.emf.ecore + + + org.eclipse.sirius + sirius-components-core + + + org.eclipse.sirius + sirius-components-interpreter + pepper pepper-mm diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/NonWorkingDaysService.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/NonWorkingDaysService.java index 11e937a..fffb830 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/NonWorkingDaysService.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/NonWorkingDaysService.java @@ -160,6 +160,7 @@ public Duration getDuration(LocalDate startDate, LocalDate endDate, List /** * Returns the end time reached after the specified number of working hours from {@code startTime}. Non-working days in week and configured fixed non-working days do not consume any effort. * When persons is provided, days that correspond of unavailability period of all the persons are also excluded. + * With zero effort, returns the supplied instant if the minute before it is on a working day; otherwise advances to the next valid end time in half-day steps. * * @param startTime * the non-null start of the interval @@ -174,20 +175,19 @@ public Instant getNextEndTime(Instant startTime, int effortInHours, List Duration remainingDuration = Duration.ofHours(effortInHours); Instant currentEndTime = startTime; - while (!remainingDuration.isZero()) { + while (!remainingDuration.isZero() + || !this.isWorkingDay(currentEndTime.minus(1, ChronoUnit.MINUTES).atZone(ZoneOffset.UTC).toLocalDate(), persons)) { LocalDate currentDate = currentEndTime.atZone(ZoneOffset.UTC).toLocalDate(); Instant nextHalfDayStart = currentEndTime.truncatedTo(ChronoUnit.HALF_DAYS).plus(1, ChronoUnit.HALF_DAYS); int nbWorkingPersons = this.getNbWorkingPersons(currentDate, persons); - if (nbWorkingPersons == 0) { - currentEndTime = nextHalfDayStart; - } else { + if (nbWorkingPersons > 0) { Duration availableDuration = Duration.ofHours(Duration.between(currentEndTime, nextHalfDayStart).toHours() * nbWorkingPersons); Duration consumedDuration = remainingDuration.compareTo(availableDuration) < 0 ? remainingDuration : availableDuration; remainingDuration = remainingDuration.minus(consumedDuration); - currentEndTime = nextHalfDayStart; } + currentEndTime = nextHalfDayStart; } return currentEndTime; } @@ -203,18 +203,42 @@ public Instant getNextEndTime(Instant startTime, int effortInHours, List * @return the supplied instant or the next valid end time */ public Instant getNextEndTime(Instant instant, List persons) { - if (instant == null) { + return this.getNextEndTime(instant, 0, persons); + } + + /** + * Returns the next valid start time reached after the specified number of working hours from {@code startTime}. Non-working days in week and configured fixed non-working days do not consume any + * effort. When persons is provided, days that correspond of unavailability period of all the persons are also excluded. + * With zero effort, returns the supplied instant if it is on a working day; otherwise advances to the next valid start time in half-day steps. + * + * @param startTime + * the non-null start of the interval + * @param effortInHours + * the number of working hours to add + * @return the resulting start time, or {@code null} when {@code startTime} is null + */ + public Instant getNextStartTime(Instant startTime, int effortInHours, List persons) { + if (startTime == null) { return null; } - Instant nextEndTime = instant; - if (!this.isWorkingDay(nextEndTime.minus(1, ChronoUnit.MINUTES).atZone(ZoneOffset.UTC).toLocalDate(), persons)) { - nextEndTime = instant.plus(6, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HALF_DAYS); - while (!this.isWorkingDay(nextEndTime.atZone(ZoneOffset.UTC).toLocalDate(), persons)) { - nextEndTime = nextEndTime.plus(1, ChronoUnit.HALF_DAYS); + + Duration remainingDuration = Duration.ofHours(effortInHours); + Instant currentStartTime = startTime; + while (!remainingDuration.isZero() + || !this.isWorkingDay(currentStartTime.atZone(ZoneOffset.UTC).toLocalDate(), persons)) { + LocalDate currentDate = currentStartTime.atZone(ZoneOffset.UTC).toLocalDate(); + Instant nextHalfDayStart = currentStartTime.truncatedTo(ChronoUnit.HALF_DAYS).plus(1, ChronoUnit.HALF_DAYS); + int nbWorkingPersons = this.getNbWorkingPersons(currentDate, persons); + if (nbWorkingPersons > 0) { + Duration availableDuration = Duration.ofHours(Duration.between(currentStartTime, nextHalfDayStart).toHours() * nbWorkingPersons); + Duration consumedDuration = remainingDuration.compareTo(availableDuration) < 0 + ? remainingDuration + : availableDuration; + remainingDuration = remainingDuration.minus(consumedDuration); } - nextEndTime = nextEndTime.plus(1, ChronoUnit.HALF_DAYS); + currentStartTime = nextHalfDayStart; } - return nextEndTime; + return currentStartTime; } /** @@ -226,23 +250,14 @@ public Instant getNextEndTime(Instant instant, List persons) { * @return the supplied instant or the previous valid start time */ public Instant getPreviousStartTime(Instant instant, List persons) { - if (instant == null) { - return null; - } - Instant previousStartTime = instant; - if (!this.isWorkingDay(instant.atZone(ZoneOffset.UTC).toLocalDate(), persons)) { - previousStartTime = instant.truncatedTo(ChronoUnit.HALF_DAYS); - while (!this.isWorkingDay(previousStartTime.atZone(ZoneOffset.UTC).toLocalDate(), persons)) { - previousStartTime = previousStartTime.minus(1, ChronoUnit.HALF_DAYS); - } - } - return previousStartTime; + return this.getPreviousStartTime(instant, 0, persons); } /** * Returns the start time reached after moving backward by the specified number of working hours from {@code endTime}. Non-working days in week and configured fixed non-working days do not consume * any effort. * When persons is provided, days that correspond of unavailability period of all the persons are also excluded. + * With zero effort, returns the supplied instant if it is on a working day; otherwise moves backward to the previous valid start time in half-day steps. * * @param endTime * the non-null end of the interval @@ -258,20 +273,19 @@ public Instant getPreviousStartTime(Instant endTime, int effortInHours, List 0) { Duration availableDuration = Duration.ofHours(Duration.between(previousHalfDayStart, currentStartTime).toHours() * nbWorkingPersons); Duration consumedDuration = remainingDuration.compareTo(availableDuration) < 0 ? remainingDuration : availableDuration; remainingDuration = remainingDuration.minus(consumedDuration); - currentStartTime = previousHalfDayStart; } + currentStartTime = previousHalfDayStart; } return currentStartTime; } diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskComputationService.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskComputationService.java index a7b8358..99d2de1 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskComputationService.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskComputationService.java @@ -39,6 +39,8 @@ public class TaskComputationService { private final NonWorkingDaysService nonWorkingDaysService = new NonWorkingDaysService(); + private final TaskHelper taskHelper = new TaskHelper(); + private final ZoneId localZone = ZoneId.systemDefault(); /** @@ -52,7 +54,7 @@ public void updateStartTime(AbstractTask abstractTask, Instant newStartTime) { Instant currentEndTime = this.roundToNearestHalfDay(abstractTask.getEndTime()); int currentEffort = abstractTask.getEffort(); - if (calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT) && previousStartTime != null) { + if (calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT) && !taskHelper.isBoundaryConstrainedByDependency(((DependencyRelatedObject) abstractTask).getDependencies(), StartOrEnd.END) && previousStartTime != null) { Instant newEndTime = nonWorkingDaysService.getNextEndTime(previousStartTime, currentEffort, abstractTask.getAssignedPersons()).minus(1, ChronoUnit.MINUTES); abstractTask.setEndTime(this.convertAccordingToTimeZone(newEndTime)); } else { @@ -76,7 +78,7 @@ public void updateEndTime(AbstractTask abstractTask, Instant newEndTime) { Instant currentStartTime = this.roundToNearestHalfDay(abstractTask.getStartTime()); int currentEffort = abstractTask.getEffort(); - if (calculationOption.equals(TaskTimeBoundariesConstraint.END_EFFORT) && nextEndTime != null) { + if (calculationOption.equals(TaskTimeBoundariesConstraint.END_EFFORT) && !taskHelper.isBoundaryConstrainedByDependency(((DependencyRelatedObject) abstractTask).getDependencies(), StartOrEnd.START) && nextEndTime != null) { Instant newStartTime = nonWorkingDaysService.getPreviousStartTime(nextEndTime, currentEffort, abstractTask.getAssignedPersons()); //.plus(1, ChronoUnit.MINUTES); abstractTask.setStartTime(this.convertAccordingToTimeZone(newStartTime)); } else { diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskHelper.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskHelper.java new file mode 100644 index 0000000..b890a01 --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskHelper.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package pepper.domain.services; + +import java.time.temporal.Temporal; +import java.util.List; +import java.util.Optional; + +import org.eclipse.emf.ecore.EObject; + +import pepper.peppermm.AbstractTask; +import pepper.peppermm.DependencyLink; +import pepper.peppermm.DependencyRelatedObject; +import pepper.peppermm.NamedElement; +import pepper.peppermm.StartOrEnd; +import pepper.peppermm.TaskTimeBoundariesConstraint; +import pepper.peppermm.Workpackage; + +/** + * Helper related to tasks (AbstractTask and Workpackage). + * + * @author lfasani + */ +public class TaskHelper { + public Temporal getStartTemporal(DependencyRelatedObject dependencyRelatedObject) { + Temporal startTemporal = null; + if (dependencyRelatedObject instanceof AbstractTask abstractTask) { + startTemporal = abstractTask.getStartTime(); + } else if (dependencyRelatedObject instanceof Workpackage workpackage) { + startTemporal = workpackage.getStartDate(); + } + return startTemporal; + } + + public String getName(Object task) { + return Optional.of(task) + .filter(NamedElement.class::isInstance) + .map(NamedElement.class::cast) + .map(NamedElement::getName) + .orElse("(no name)"); + } + + public boolean isComputedDynamically(DependencyRelatedObject targetTask) { + boolean isComputedDynamically = false; + if (targetTask instanceof AbstractTask abstractTask && abstractTask.isComputeStartEndDynamically()) { + isComputedDynamically = abstractTask.isComputeStartEndDynamically(); + } else if (targetTask instanceof Workpackage workpackage && !workpackage.getOwnedTasks().isEmpty()) { + isComputedDynamically = true; + } + return isComputedDynamically; + } + + public Optional getParent(EObject eObject, Class clazz) { + Optional objectOpt = Optional.empty(); + EObject parent = eObject.eContainer(); + while (parent != null) { + if (clazz.isInstance(parent)) { + objectOpt = Optional.of(clazz.cast(parent)); + break; + } + parent = parent.eContainer(); + } + + return objectOpt; + } + + public TaskTimeBoundariesConstraint getCalculationOption(DependencyRelatedObject task) { + TaskTimeBoundariesConstraint calculationOption = null; + if (task instanceof AbstractTask abstractTask) { + calculationOption = abstractTask.getCalculationOption(); + } else if (task instanceof Workpackage workpackage) { + calculationOption = workpackage.getCalculationOption(); + } + return calculationOption; + } + + public void setCalculationOption(DependencyRelatedObject task, TaskTimeBoundariesConstraint calculationOption) { + if (task instanceof AbstractTask abstractTask) { + abstractTask.setCalculationOption(calculationOption); + } else if (task instanceof Workpackage workpackage) { + workpackage.setCalculationOption(calculationOption); + } + } + + public boolean isBoundaryConstrainedByDependency(List dependencies, StartOrEnd boundary) { + return dependencies.stream() + .anyMatch(dep -> dep.getTargetKind() == boundary); + } +} diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/TemporalHelper.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/TemporalHelper.java new file mode 100644 index 0000000..2e1c72a --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/TemporalHelper.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package pepper.domain.services; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; + +/** + * Helper related to Instant and Date. + * + * @author lfasani + */ +public class TemporalHelper { + public int roundToNearestHalfDayInHours(String nbDaysString) { + double doubleValue = Double.parseDouble(nbDaysString.replace(',', '.')); + + Duration inputDuration = Duration.ofHours((int) (doubleValue * 24)); + Duration duration = inputDuration.isNegative() + ? inputDuration.minusHours(6).truncatedTo(ChronoUnit.HALF_DAYS) + : inputDuration.plusMinutes(6).truncatedTo(ChronoUnit.HALF_DAYS); + + return Math.toIntExact(duration.toHours()); + } + + public Instant roundToNearestHalfDay(Instant instant) { + return Optional.ofNullable(instant) + .map(inst -> inst.plus(Duration.ofHours(6)).truncatedTo(ChronoUnit.HALF_DAYS)) + .orElse(null); + } +} diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/WorkpackageComputationService.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/WorkpackageComputationService.java index a800c04..45ea139 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/WorkpackageComputationService.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/WorkpackageComputationService.java @@ -33,6 +33,8 @@ public class WorkpackageComputationService { private final NonWorkingDaysService nonWorkingDaysService = new NonWorkingDaysService(); + private final TaskHelper taskHelper = new TaskHelper(); + public void updateStartDate(Workpackage workpackage, LocalDate newStartDate) { LocalDate previousNewStartDate = nonWorkingDaysService.getPreviousStartDate(newStartDate, workpackage.getAssignedPersons()); TaskTimeBoundariesConstraint calculationOption = workpackage.getCalculationOption(); @@ -40,7 +42,7 @@ public void updateStartDate(Workpackage workpackage, LocalDate newStartDate) { LocalDate currentEndDate = workpackage.getEndDate(); int currentEffort = workpackage.getEffort(); - if (calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT) && previousNewStartDate != null) { + if (calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT) && !taskHelper.isBoundaryConstrainedByDependency(workpackage.getDependencies(), StartOrEnd.END) && previousNewStartDate != null) { LocalDate newEndDate = nonWorkingDaysService.getNextEndDate(previousNewStartDate, currentEffort, workpackage.getAssignedPersons()); workpackage.setEndDate(newEndDate); } else { @@ -67,7 +69,7 @@ public void updateEndDate(Workpackage workpackage, LocalDate newEndDate) { LocalDate currentStartDate = workpackage.getStartDate(); int currentEffort = workpackage.getEffort(); - if (calculationOption.equals(TaskTimeBoundariesConstraint.END_EFFORT) && nextNewEndDate != null) { + if (calculationOption.equals(TaskTimeBoundariesConstraint.END_EFFORT) && !taskHelper.isBoundaryConstrainedByDependency(workpackage.getDependencies(), StartOrEnd.START) && nextNewEndDate != null) { LocalDate newStartDate = nonWorkingDaysService.getPreviousStartDate(nextNewEndDate, currentEffort, workpackage.getAssignedPersons()); workpackage.setStartDate(newStartDate); } else { diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/ComputeDynamicallyChangeUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/ComputeDynamicallyChangeUpdateStep.java new file mode 100644 index 0000000..f4639d8 --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/ComputeDynamicallyChangeUpdateStep.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +package pepper.domain.services.update; + +import pepper.domain.services.TaskHelper; +import pepper.peppermm.AbstractTask; + +/** + * This class represents an update step for the change of the effort of a task. + * @author lfasani + */ +public final class ComputeDynamicallyChangeUpdateStep extends TaskUpdateStep { + private static final TaskHelper TASK_HELPER = new TaskHelper(); + + private final AbstractTask abstractTask; + + private final Boolean newValue; + + public ComputeDynamicallyChangeUpdateStep(AbstractTask abstractTask, Boolean newValue) { + this.abstractTask = abstractTask; + this.newValue = newValue; + } + + @Override + public Object getImpactedTask() { + return abstractTask; + } + + @Override + public String getName() { + return TASK_HELPER.getName(abstractTask); + } + + @Override + public void update() { + abstractTask.setComputeStartEndDynamically(newValue != null && newValue); + } +} diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/CreateDependencyUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/CreateDependencyUpdateStep.java new file mode 100644 index 0000000..45a7507 --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/CreateDependencyUpdateStep.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +package pepper.domain.services.update; + +import pepper.domain.services.TaskHelper; +import pepper.peppermm.DependencyLink; +import pepper.peppermm.DependencyRelatedObject; +import pepper.peppermm.PepperFactory; +import pepper.peppermm.StartOrEnd; + +/** + * This class represents the creation of a {@link DependencyLink}. + * @author lfasani + */ +public final class CreateDependencyUpdateStep extends TaskUpdateStep { + private static final TaskHelper TASK_HELPER = new TaskHelper(); + + private final DependencyRelatedObject source; + + private final DependencyRelatedObject target; + + private final StartOrEnd sourceBoundary; + + private final StartOrEnd endBoundary; + + public CreateDependencyUpdateStep(DependencyRelatedObject source, DependencyRelatedObject target, StartOrEnd sourceBoundary, StartOrEnd endBoundary) { + this.source = source; + this.target = target; + this.sourceBoundary = sourceBoundary; + this.endBoundary = endBoundary; + } + + @Override + public Object getImpactedTask() { + return source; + } + + @Override + public String getName() { + return TASK_HELPER.getName(source); + } + + @Override + public void update() { + DependencyLink dependencyLink = PepperFactory.eINSTANCE.createDependencyLink(); + dependencyLink.setSourceKind(sourceBoundary); + dependencyLink.setTargetKind(endBoundary); + + dependencyLink.setSource(source); + target.getDependencies().add(dependencyLink); + } + +} diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/DependencyUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/DependencyUpdateStep.java new file mode 100644 index 0000000..9fd7455 --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/DependencyUpdateStep.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +package pepper.domain.services.update; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.Comparator; +import java.util.List; + +import pepper.domain.services.NonWorkingDaysService; +import pepper.domain.services.TaskComputationService; +import pepper.domain.services.TaskHelper; +import pepper.domain.services.TemporalHelper; +import pepper.domain.services.WorkpackageComputationService; +import pepper.peppermm.AbstractTask; +import pepper.peppermm.DependencyLink; +import pepper.peppermm.DependencyRelatedObject; +import pepper.peppermm.StartOrEnd; +import pepper.peppermm.TaskTimeBoundariesConstraint; +import pepper.peppermm.Workpackage; + +/** + * This class represents the update of a task due to a {@link pepper.peppermm.DependencyLink}. + * @author lfasani + */ +public final class DependencyUpdateStep extends TaskUpdateStep { + private static final TaskHelper TASK_HELPER = new TaskHelper(); + + private static final TaskComputationService TASK_COMPUTATION_SERVICE = new TaskComputationService(); + + private static final WorkpackageComputationService WORKPACKAGE_COMPUTATION_SERVICE = new WorkpackageComputationService(); + + private final NonWorkingDaysService nonWorkingDaysService = new NonWorkingDaysService(); + + private final TemporalHelper temporalHelper = new TemporalHelper(); + + private final DependencyRelatedObject targetTask; + + public DependencyUpdateStep(DependencyRelatedObject targetTask) { + this.targetTask = targetTask; + } + + @Override + public Object getImpactedTask() { + return targetTask; + } + + @Override + public String getName() { + return TASK_HELPER.getName(targetTask); + } + + @Override + public void update() { + List dependencies = targetTask.getDependencies(); + boolean startTimeControlledByDependency = TASK_HELPER.isBoundaryConstrainedByDependency(dependencies, StartOrEnd.START); + boolean endTimeControlledByDependency = TASK_HELPER.isBoundaryConstrainedByDependency(dependencies, StartOrEnd.END); + + TaskTimeBoundariesConstraint initialCalculationOption = TASK_HELPER.getCalculationOption(targetTask); + if (startTimeControlledByDependency) { //Whatever endTimeControlledByDependency + TASK_HELPER.setCalculationOption(targetTask, TaskTimeBoundariesConstraint.START_EFFORT); + + if (targetTask instanceof AbstractTask abstractTask) { + Instant nextTimeFromDependency = this.getNextTimeFromDependency(dependencies, StartOrEnd.START); + TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, nextTimeFromDependency); + } else if (targetTask instanceof Workpackage workpackage) { + LocalDate nextDateFromDependency = this.getNextDateFromDependency(dependencies, StartOrEnd.START); + WORKPACKAGE_COMPUTATION_SERVICE.updateStartDate(workpackage, nextDateFromDependency); + } + } + if (endTimeControlledByDependency) { + TASK_HELPER.setCalculationOption(targetTask, TaskTimeBoundariesConstraint.END_EFFORT); + + if (targetTask instanceof AbstractTask abstractTask) { + Instant nextTimeFromDependency = this.getNextTimeFromDependency(dependencies, StartOrEnd.END); + TASK_COMPUTATION_SERVICE.updateEndTime(abstractTask, nextTimeFromDependency); + } else if (targetTask instanceof Workpackage workpackage) { + LocalDate nextDateFromDependency = this.getNextDateFromDependency(dependencies, StartOrEnd.END); + WORKPACKAGE_COMPUTATION_SERVICE.updateEndDate(workpackage, nextDateFromDependency); + } + } + TASK_HELPER.setCalculationOption(targetTask, initialCalculationOption); + + } + + @SuppressWarnings("checkstyle:ReturnCount") + Instant getNextTimeFromDependency(List dependencies, StartOrEnd targetBoundary) { + return dependencies.stream() + .filter(dep -> dep.getTargetKind() == targetBoundary) + .filter(dependencyLink -> dependencyLink.getSource() instanceof AbstractTask) + .map(dependencyLink -> { + Instant roundedStartTime = temporalHelper.roundToNearestHalfDay(((AbstractTask) dependencyLink.getSource()).getStartTime()); + Instant roundedEndTime = temporalHelper.roundToNearestHalfDay(((AbstractTask) dependencyLink.getSource()).getEndTime()); + if (targetBoundary == StartOrEnd.START) { + if (dependencyLink.getSourceKind() == StartOrEnd.START) { + return nonWorkingDaysService.getNextStartTime(roundedStartTime, dependencyLink.getDelay(), List.of()); + } else { + return nonWorkingDaysService.getNextStartTime(roundedEndTime, dependencyLink.getDelay(), List.of()); + } + } else { + if (dependencyLink.getSourceKind() == StartOrEnd.START) { + return nonWorkingDaysService.getNextEndTime(roundedStartTime, dependencyLink.getDelay(), List.of()); + } else { + return nonWorkingDaysService.getNextEndTime(roundedEndTime, dependencyLink.getDelay(), List.of()); + } + } + }) + .max(Comparator.naturalOrder()) + .orElse(null); + } + + LocalDate getNextDateFromDependency(List dependencies, StartOrEnd targetBoundary) { + return dependencies.stream() + .filter(dep -> dep.getTargetKind() == targetBoundary) + .filter(dependencyLink -> dependencyLink.getSource() instanceof Workpackage) + .map(dependencyLink -> { + if (dependencyLink.getSourceKind() == StartOrEnd.START) { + return nonWorkingDaysService.getNextEndDate(((Workpackage) dependencyLink.getSource()).getStartDate().plusDays(1), dependencyLink.getDelay() + 1, List.of()); + } else { + return nonWorkingDaysService.getNextEndDate(((Workpackage) dependencyLink.getSource()).getEndDate().plusDays(1), dependencyLink.getDelay() + 1, List.of()); + } + }) + .max(Comparator.naturalOrder()) + .orElse(null); + } +} diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/EffortUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/EffortUpdateStep.java new file mode 100644 index 0000000..1ea6f6f --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/EffortUpdateStep.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +package pepper.domain.services.update; + +import pepper.domain.services.TaskComputationService; +import pepper.domain.services.TaskHelper; +import pepper.domain.services.TemporalHelper; +import pepper.domain.services.WorkpackageComputationService; +import pepper.peppermm.AbstractTask; +import pepper.peppermm.DependencyRelatedObject; +import pepper.peppermm.Workpackage; + +/** + * This class represents an update step for the change of the effort of a task. + * @author lfasani + */ +public final class EffortUpdateStep extends TaskUpdateStep { + private static final TaskHelper TASK_HELPER = new TaskHelper(); + private static final TemporalHelper TEMPORAL_HELPER = new TemporalHelper(); + + private static final TaskComputationService TASK_COMPUTATION_SERVICE = new TaskComputationService(); + private static final WorkpackageComputationService WORKPACKAGE_COMPUTATION_SERVICE = new WorkpackageComputationService(); + + private final DependencyRelatedObject task; + + private final String newEffort; + + public EffortUpdateStep(DependencyRelatedObject task, String newEffort) { + this.task = task; + this.newEffort = newEffort; + } + + @Override + public Object getImpactedTask() { + return task; + } + + @Override + public String getName() { + return TASK_HELPER.getName(task); + } + + @Override + public void update() { + if (task instanceof AbstractTask abstractTask) { + if (newEffort == null || newEffort.isBlank()) { + TASK_COMPUTATION_SERVICE.updateEffort(abstractTask, 0); + } else { + try { + int valueInHours = TEMPORAL_HELPER.roundToNearestHalfDayInHours(newEffort); + if (valueInHours >= 0) { + TASK_COMPUTATION_SERVICE.updateEffort(abstractTask, valueInHours); + } + } catch (NumberFormatException e) { + // Ignore + } + } + } else if (task instanceof Workpackage workpackage) { + if (newEffort == null || newEffort.isBlank()) { + WORKPACKAGE_COMPUTATION_SERVICE.updateEffort(workpackage, 0); + } else { + try { + int valueInHours = (int) Math.round(Double.parseDouble(newEffort)); + if (valueInHours >= 0) { + WORKPACKAGE_COMPUTATION_SERVICE.updateEffort(workpackage, valueInHours); + } + } catch (NumberFormatException e) { + // Ignore + } + } + } + } +} diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/ParentUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/ParentUpdateStep.java new file mode 100644 index 0000000..4870fe1 --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/ParentUpdateStep.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +package pepper.domain.services.update; + +import java.time.Instant; +import java.util.Comparator; +import java.util.List; + +import pepper.domain.services.TaskHelper; +import pepper.peppermm.AbstractTask; +import pepper.peppermm.Task; + +/** + * This class represents the update of a dynamic task which is a parent of updated tasks. + * @author lfasani + */ +public final class ParentUpdateStep extends TaskUpdateStep { + private static final TaskHelper TASK_HELPER = new TaskHelper(); + + private final AbstractTask abstractTask; + + public ParentUpdateStep(AbstractTask abstractTask) { + this.abstractTask = abstractTask; + } + + @Override + public Object getImpactedTask() { + return abstractTask; + } + + @Override + public String getName() { + return TASK_HELPER.getName(abstractTask); + } + + @Override + public void update() { + List subTasks = abstractTask.getSubTasks(); + Instant startTime = subTasks.stream() + .map(AbstractTask::getStartTime) + .min(Comparator.naturalOrder()) + .orElse(null); + abstractTask.setStartTime(startTime); + + Instant endTime = subTasks.stream() + .map(AbstractTask::getEndTime) + .max(Comparator.naturalOrder()) + .orElse(null); + abstractTask.setEndTime(endTime); + } + +} diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskBoundaryUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskBoundaryUpdateStep.java new file mode 100644 index 0000000..4f94134 --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskBoundaryUpdateStep.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +package pepper.domain.services.update; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.List; +import java.util.Optional; + +import pepper.domain.services.TaskComputationService; +import pepper.domain.services.TaskHelper; +import pepper.domain.services.WorkpackageComputationService; +import pepper.peppermm.AbstractTask; +import pepper.peppermm.DependencyLink; +import pepper.peppermm.DependencyRelatedObject; +import pepper.peppermm.StartOrEnd; +import pepper.peppermm.TaskTimeBoundariesConstraint; +import pepper.peppermm.Workpackage; + +/** + * This class represents an update step for the change of boundary of a task. + */ +public final class TaskBoundaryUpdateStep extends TaskUpdateStep { + private static final TaskHelper TASK_HELPER = new TaskHelper(); + private static final TaskComputationService TASK_COMPUTATION_SERVICE = new TaskComputationService(); + private static final WorkpackageComputationService WORKPACKAGE_COMPUTATION_SERVICE = new WorkpackageComputationService(); + + private final DependencyRelatedObject task; + + private final Temporal start; + + private final Temporal end; + + public TaskBoundaryUpdateStep(DependencyRelatedObject task, Temporal start, Temporal end) { + this.task = task; + this.start = start; + this.end = end; + } + + @Override + public Object getImpactedTask() { + return task; + } + + @Override + public String getName() { + return TASK_HELPER.getName(task); + } + + @SuppressWarnings("checkstyle:MissingSwitchDefault") + @Override + public void update() { + if (task instanceof AbstractTask abstractTask && start instanceof Instant startTime && end instanceof Instant endTime) { + Instant newStartTime = TASK_COMPUTATION_SERVICE.roundToNearestHalfDay(startTime); + Instant newEndTime = TASK_COMPUTATION_SERVICE.roundToNearestHalfDay(endTime); + long differenceStart = Optional.ofNullable(abstractTask.getStartTime()) + .map(currentStartTime -> newStartTime.getEpochSecond() - TASK_COMPUTATION_SERVICE.roundToNearestHalfDay(currentStartTime).getEpochSecond()) + .orElse((long) -1); + long differenceEnd = Optional.ofNullable(abstractTask.getEndTime()) + .map(currentEndTime -> newEndTime.getEpochSecond() - TASK_COMPUTATION_SERVICE.roundToNearestHalfDay(currentEndTime).getEpochSecond()) + .orElse((long) -1); + boolean taskShifted = differenceStart != 0 && differenceEnd != 0; + List dependencies = task.getDependencies(); + // Nothing is done when moving a task constrained by dependencies + if (dependencies.isEmpty() || !taskShifted) { + boolean startTimeControlledByDependency = TASK_HELPER.isBoundaryConstrainedByDependency(dependencies, StartOrEnd.START); + boolean endTimeControlledByDependency = TASK_HELPER.isBoundaryConstrainedByDependency(dependencies, StartOrEnd.END); + + if (taskShifted) { + TaskTimeBoundariesConstraint calculationOption = abstractTask.getCalculationOption(); + switch (calculationOption) { + case START_EFFORT -> TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, newStartTime); + case END_EFFORT -> TASK_COMPUTATION_SERVICE.updateEndTime(abstractTask, newEndTime); + case START_END -> { + TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, newStartTime); + TASK_COMPUTATION_SERVICE.updateEndTime(abstractTask, newEndTime); + } + } + } else { + if (differenceStart != 0 && !startTimeControlledByDependency) { + TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, newStartTime); + } + + if (differenceEnd != 0 && !endTimeControlledByDependency) { + TASK_COMPUTATION_SERVICE.updateEndTime(abstractTask, newEndTime); + } + } + + } + } else if (task instanceof Workpackage workpackage && start instanceof LocalDate startDate && end instanceof LocalDate endDate) { + long differenceEnd = Optional.ofNullable(workpackage.getEndDate()).map(currentEndDate -> ChronoUnit.DAYS.between(endDate, currentEndDate)) + .orElse((long) -1); + long differenceStart = Optional.ofNullable(workpackage.getStartDate()).map(currentStartDate -> ChronoUnit.DAYS.between(startDate, currentStartDate)) + .orElse((long) -1); + boolean taskShifted = differenceStart != 0 && differenceEnd != 0; + List dependencies = workpackage.getDependencies(); + boolean startDateControlledByDependency = TASK_HELPER.isBoundaryConstrainedByDependency(dependencies, StartOrEnd.START); + boolean endDateControlledByDependency = TASK_HELPER.isBoundaryConstrainedByDependency(dependencies, StartOrEnd.END); + + if (taskShifted) { + // Nothing is done when moving a task constrained by dependencies + if (dependencies.isEmpty()) { + TaskTimeBoundariesConstraint calculationOption = workpackage.getCalculationOption(); + switch (calculationOption) { + case START_EFFORT -> WORKPACKAGE_COMPUTATION_SERVICE.updateStartDate(workpackage, startDate); + case END_EFFORT -> WORKPACKAGE_COMPUTATION_SERVICE.updateEndDate(workpackage, endDate); + case START_END -> { + WORKPACKAGE_COMPUTATION_SERVICE.updateStartDate(workpackage, startDate); + WORKPACKAGE_COMPUTATION_SERVICE.updateEndDate(workpackage, endDate); + } + } + } + } else { + if (differenceStart != 0 && !startDateControlledByDependency) { + WORKPACKAGE_COMPUTATION_SERVICE.updateStartDate(workpackage, startDate); + } + + if (differenceEnd != 0 && !endDateControlledByDependency) { + WORKPACKAGE_COMPUTATION_SERVICE.updateEndDate(workpackage, endDate); + } + } + } + } +} diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateService.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateService.java new file mode 100644 index 0000000..6d2d27d --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateService.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +package pepper.domain.services.update; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.sirius.components.core.api.IFeedbackMessageService; +import org.eclipse.sirius.components.interpreter.SimpleCrossReferenceProvider; +import org.eclipse.sirius.components.representations.Message; +import org.eclipse.sirius.components.representations.MessageLevel; +import org.springframework.stereotype.Service; + +import pepper.domain.services.TaskComputationService; +import pepper.domain.services.TaskHelper; +import pepper.domain.services.WorkpackageComputationService; +import pepper.peppermm.AbstractTask; +import pepper.peppermm.DependencyLink; +import pepper.peppermm.DependencyRelatedObject; + +/** + * Service to manage the update of a task and the impacted tasks. + * + * @author lfasani + */ +@Service +public class TaskUpdateService { + + private final TaskHelper taskHelper = new TaskHelper(); + + private final SimpleCrossReferenceProvider simpleCrossReferenceProvider = new SimpleCrossReferenceProvider(); + + private final IFeedbackMessageService feedbackMessageService; + + private final TaskComputationService taskComputationService = new TaskComputationService(); + + private final WorkpackageComputationService workpackageComputationService = new WorkpackageComputationService(); + + public TaskUpdateService(IFeedbackMessageService feedbackMessageService) { + this.feedbackMessageService = Objects.requireNonNull(feedbackMessageService); + } + + public void updateWithImpacts(EObject task, TaskUpdateStep taskUpdateStep) { + this.updateWithImpacts(task, List.of(taskUpdateStep)); + } + + public void updateWithImpacts(EObject task, List taskUpdateSteps) { + List tasksToUpdate = new ArrayList<>(taskUpdateSteps); + List currentBranchOfTasksToUpdate = new ArrayList<>(tasksToUpdate); + try { + this.computeTaskToUpdate(task, tasksToUpdate, currentBranchOfTasksToUpdate); + this.doUpdate(tasksToUpdate); + } catch (IllegalStateException e) { + // logged in IFeedbackMessageService + } + } + + public void doUpdate(List tasksToUpdate) { + tasksToUpdate.forEach(TaskUpdateStep::update); + } + + /** + * Aggregates the tasksToUpdate with tasks that are dependencies of currentTask. + */ + public void computeTaskToUpdate(EObject currentTask, List tasksToUpdate, List currentBranchOfTasksToUpdate) throws IllegalStateException { + for (var inverseReference : simpleCrossReferenceProvider.getInverseReferences(currentTask)) { + if (inverseReference.getEObject() instanceof DependencyLink dependencyLink) { + if (dependencyLink.eContainer() instanceof DependencyRelatedObject targetTask && currentTask instanceof DependencyRelatedObject sourceTask) { + if (taskHelper.isComputedDynamically(targetTask)) { + this.fail(String.format("Having a dependency targeting a dynamically computed task \"%s\" is not possible.", taskHelper.getName(targetTask))); + } + this.computeTaskToUpdate(tasksToUpdate, currentBranchOfTasksToUpdate, targetTask, new DependencyUpdateStep(targetTask)); + } + } + } + + if (currentTask.eContainer() instanceof AbstractTask abstractTask) { + if (abstractTask.isComputeStartEndDynamically()) { + this.computeTaskToUpdate(tasksToUpdate, currentBranchOfTasksToUpdate, abstractTask, new ParentUpdateStep(abstractTask)); + } + } + } + + private void computeTaskToUpdate(List tasksToUpdate, List currentBranchOfTasksToUpdate, EObject targetTask, TaskUpdateStep newUpdateStep) { + List newBranchOfTasksToUpdate = new ArrayList<>(currentBranchOfTasksToUpdate); + newBranchOfTasksToUpdate.add(newUpdateStep); + + if (newBranchOfTasksToUpdate.stream().distinct().count() != newBranchOfTasksToUpdate.size()) { + String updatePath = newBranchOfTasksToUpdate.stream() + .map(TaskUpdateStep::getName) + .collect(Collectors.joining(" -> ")); + this.fail("Creating a cyclic dependency is not possible: " + updatePath); + } + if (!tasksToUpdate.contains(newUpdateStep)) { + tasksToUpdate.add(newUpdateStep); + } + + this.computeTaskToUpdate(targetTask, tasksToUpdate, newBranchOfTasksToUpdate); + } + + void fail(String message) { + this.feedbackMessageService.addFeedbackMessage(new Message(message, MessageLevel.ERROR)); + throw new IllegalStateException(); + } + + +} diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateStep.java new file mode 100644 index 0000000..8b4c7bd --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateStep.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +package pepper.domain.services.update; + +/** + * Represent a step in the update of a task. + * @author lfasani + */ +public abstract class TaskUpdateStep { + /** + * Returns the object affected by this update step. + * + * @return the impacted task + */ + public abstract Object getImpactedTask(); + + public abstract String getName(); + + public abstract void update(); + + @Override + public final boolean equals(Object object) { + return object instanceof TaskUpdateStep that && this.getImpactedTask() == that.getImpactedTask(); + } + + @Override + public final int hashCode() { + return System.identityHashCode(this.getImpactedTask()); + } +} diff --git a/backend/pepper-domain-services/src/test/java/pepper/domain/services/NonWorkingDaysServiceTests.java b/backend/pepper-domain-services/src/test/java/pepper/domain/services/NonWorkingDaysServiceTests.java index 73ac8cb..4d23336 100644 --- a/backend/pepper-domain-services/src/test/java/pepper/domain/services/NonWorkingDaysServiceTests.java +++ b/backend/pepper-domain-services/src/test/java/pepper/domain/services/NonWorkingDaysServiceTests.java @@ -21,6 +21,8 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import pepper.peppermm.PepperFactory; import pepper.peppermm.Person; @@ -91,7 +93,7 @@ public void getEffortBetweenConsecutiveWorkingDatesIsOneDay() { @Test public void getEffortBetweenDatesExcludesNonWorkingDays() { var service = new NonWorkingDaysService(); - // 10 and 11 are in a week-end and 14 is off + // 10 and 11 are in a weekend and 14 is off LocalDate startDate = FRIDAY_2026_07_10; LocalDate endDate = LocalDate.of(2026, 7, 16); @@ -138,6 +140,47 @@ public void getNextEndTimeMovesPastANonWorkingDay() { assertThat(service.getNextEndTime(instant, List.of())).isEqualTo(Instant.parse("2026-07-15T12:00:00Z")); } + @ParameterizedTest + @CsvSource({ + "2026-07-31T13:00:00Z, 2026-07-31T13:00:00Z", + "2026-08-01T13:00:00Z, 2026-08-03T12:00:00Z", + "2026-07-14T09:00:00Z, 2026-07-15T12:00:00Z", + "2026-08-01T00:00:00Z, 2026-08-01T00:00:00Z", + "2026-08-01T00:00:30Z, 2026-08-01T00:00:30Z", + "2026-08-01T00:01:00Z, 2026-08-03T12:00:00Z", + "2026-08-03T00:00:00Z, 2026-08-03T12:00:00Z", + "2026-07-14T00:00:00Z, 2026-07-14T00:00:00Z", + "2026-07-15T00:00:00Z, 2026-07-15T12:00:00Z" + }) + public void getNextEndTimeWithZeroEffortRespectsWorkingEndBoundaries(Instant instant, Instant expected) { + this.assertNextEndTimeWithZeroEffort(instant, List.of(), expected); + } + + @Test + public void getNextEndTimeWithZeroEffortRespectsAssignedPersons() { + Person unavailablePerson = this.getPerson1(); + Person availablePerson = PepperFactory.eINSTANCE.createPerson(); + + this.assertNextEndTimeWithZeroEffort(TUESDAY_2026_07_07_12_00, List.of(unavailablePerson), WEDNESDAY_2026_07_08_12_00); + this.assertNextEndTimeWithZeroEffort(TUESDAY_2026_07_07_12_00, List.of(unavailablePerson, availablePerson), TUESDAY_2026_07_07_12_00); + this.assertNextEndTimeWithZeroEffort(TUESDAY_2026_07_07_00_00, List.of(unavailablePerson), TUESDAY_2026_07_07_00_00); + this.assertNextEndTimeWithZeroEffort(WEDNESDAY_2026_07_08_00_00, List.of(unavailablePerson), WEDNESDAY_2026_07_08_12_00); + } + + @Test + public void getNextEndTimeWithZeroEffortHandlesNullInputs() { + this.assertNextEndTimeWithZeroEffort(null, List.of(), null); + this.assertNextEndTimeWithZeroEffort(null, null, null); + this.assertNextEndTimeWithZeroEffort(Instant.parse("2026-08-01T13:00:00Z"), null, Instant.parse("2026-08-03T12:00:00Z")); + } + + private void assertNextEndTimeWithZeroEffort(Instant instant, List persons, Instant expected) { + var service = new NonWorkingDaysService(); + + assertThat(service.getNextEndTime(instant, 0, persons)).isEqualTo(expected); + assertThat(service.getNextEndTime(instant, persons)).isEqualTo(expected); + } + @Test public void getPreviousStartTimeKeepsAnInstantOnAWorkingDay() { var service = new NonWorkingDaysService(); @@ -146,6 +189,55 @@ public void getPreviousStartTimeKeepsAnInstantOnAWorkingDay() { assertThat(service.getPreviousStartTime(instant, List.of())).isEqualTo(instant); } + @Test + public void getNextStartTime() { + var service = new NonWorkingDaysService(); + assertThat(service.getNextStartTime(TUESDAY_2026_07_07_00_00, 24, List.of())).isEqualTo(WEDNESDAY_2026_07_08_00_00); + assertThat(service.getNextStartTime(TUESDAY_2026_07_07_00_00, 0, List.of())).isEqualTo(TUESDAY_2026_07_07_00_00); + } + + @ParameterizedTest + @CsvSource({ + "2026-07-31T13:00:00Z, 0, 2026-07-31T13:00:00Z", // 31 FRIDAY + "2026-07-14T00:00:00Z, 0, 2026-07-15T00:00:00Z", // 14 OFF + "2026-08-01T00:00:00Z, 0, 2026-08-03T00:00:00Z", // 01 SATURDAY + "2026-08-02T18:00:00Z, 0, 2026-08-03T00:00:00Z", // 02 SUNDAY + "2026-08-03T00:00:00Z, 0, 2026-08-03T00:00:00Z", // 03 MONDAY + "2026-07-31T12:00:00Z, 12, 2026-08-03T00:00:00Z", + "2026-07-31T13:00:00Z, 23, 2026-08-03T12:00:00Z", + "2026-08-01T12:00:00Z, 24, 2026-08-04T00:00:00Z", + "2026-07-13T00:00:00Z, 24, 2026-07-15T00:00:00Z", + "2026-07-13T12:00:00Z, 24, 2026-07-15T12:00:00Z", + "2026-07-14T00:00:00Z, 24, 2026-07-16T00:00:00Z" + }) + public void getNextStartTimeRespectsWorkingStartBoundaries(Instant instant, int effort, Instant expected) { + var service = new NonWorkingDaysService(); + + assertThat(service.getNextStartTime(instant, effort, List.of())).isEqualTo(expected); + } + + @Test + public void getNextStartTimeRespectsAssignedPersons() { + var service = new NonWorkingDaysService(); + Person unavailablePerson = this.getPerson1(); + Person availablePerson = PepperFactory.eINSTANCE.createPerson(); + + assertThat(service.getNextStartTime(TUESDAY_2026_07_07_12_00, 0, List.of(unavailablePerson))).isEqualTo(WEDNESDAY_2026_07_08_00_00); + assertThat(service.getNextStartTime(TUESDAY_2026_07_07_12_00, 0, List.of(unavailablePerson, availablePerson))).isEqualTo(TUESDAY_2026_07_07_12_00); + assertThat(service.getNextStartTime(MONDAY_2026_07_06_12_00, 12, List.of(unavailablePerson))).isEqualTo(WEDNESDAY_2026_07_08_00_00); + assertThat(service.getNextStartTime(MONDAY_2026_07_06_12_00, 24, List.of(unavailablePerson, availablePerson))).isEqualTo(TUESDAY_2026_07_07_00_00); + assertThat(service.getNextStartTime(MONDAY_2026_07_06_12_00, 36, List.of(unavailablePerson, availablePerson))).isEqualTo(TUESDAY_2026_07_07_12_00); + } + + @Test + public void getNextStartTimeHandlesNullInputs() { + var service = new NonWorkingDaysService(); + + assertThat(service.getNextStartTime(null, 0, List.of())).isNull(); + assertThat(service.getNextStartTime(null, 24, List.of())).isNull(); + assertThat(service.getNextStartTime(TUESDAY_2026_07_07_00_00, 24, null)).isEqualTo(WEDNESDAY_2026_07_08_00_00); + } + @Test public void getPreviousStartTimeMovesBeforeAWeekend() { var service = new NonWorkingDaysService(); @@ -162,6 +254,47 @@ public void getPreviousStartTimeMovesBeforeANonWorkingDay() { assertThat(service.getPreviousStartTime(instant, List.of())).isEqualTo(Instant.parse("2026-07-13T12:00:00Z")); } + @ParameterizedTest + @CsvSource({ + "2026-07-31T13:00:00Z, 2026-07-31T13:00:00Z", // 31 FRIDAY + "2026-08-02T18:00:00Z, 2026-07-31T12:00:00Z", // 02 SUNDAY + "2026-08-01T12:00:00Z, 2026-07-31T12:00:00Z", + "2026-07-14T09:00:00Z, 2026-07-13T12:00:00Z", + "2026-08-01T00:00:00Z, 2026-07-31T12:00:00Z", + "2026-08-01T00:00:30Z, 2026-07-31T12:00:00Z", + "2026-08-03T00:00:00Z, 2026-08-03T00:00:00Z", + "2026-07-14T00:00:00Z, 2026-07-13T12:00:00Z", + "2026-07-15T00:00:00Z, 2026-07-15T00:00:00Z" + }) + public void getPreviousStartTimeWithZeroEffortRespectsWorkingStartBoundaries(Instant instant, Instant expected) { + this.assertPreviousStartTimeWithZeroEffort(instant, List.of(), expected); + } + + @Test + public void getPreviousStartTimeWithZeroEffortRespectsAssignedPersons() { + Person unavailablePerson = this.getPerson1(); + Person availablePerson = PepperFactory.eINSTANCE.createPerson(); + + this.assertPreviousStartTimeWithZeroEffort(TUESDAY_2026_07_07_12_00, List.of(unavailablePerson), MONDAY_2026_07_06_12_00); + this.assertPreviousStartTimeWithZeroEffort(TUESDAY_2026_07_07_12_00, List.of(unavailablePerson, availablePerson), TUESDAY_2026_07_07_12_00); + this.assertPreviousStartTimeWithZeroEffort(TUESDAY_2026_07_07_00_00, List.of(unavailablePerson), MONDAY_2026_07_06_12_00); + this.assertPreviousStartTimeWithZeroEffort(WEDNESDAY_2026_07_08_00_00, List.of(unavailablePerson), WEDNESDAY_2026_07_08_00_00); + } + + @Test + public void getPreviousStartTimeWithZeroEffortHandlesNullInputs() { + this.assertPreviousStartTimeWithZeroEffort(null, List.of(), null); + this.assertPreviousStartTimeWithZeroEffort(null, null, null); + this.assertPreviousStartTimeWithZeroEffort(Instant.parse("2026-08-02T18:00:00Z"), null, Instant.parse("2026-07-31T12:00:00Z")); + } + + private void assertPreviousStartTimeWithZeroEffort(Instant instant, List persons, Instant expected) { + var service = new NonWorkingDaysService(); + + assertThat(service.getPreviousStartTime(instant, 0, persons)).isEqualTo(expected); + assertThat(service.getPreviousStartTime(instant, persons)).isEqualTo(expected); + } + @Test public void getPreviousStartDateKeepsAWorkingDate() { var service = new NonWorkingDaysService(); @@ -336,6 +469,7 @@ public void getNextEndDateWithEffortWithAssignedPersons() { Person person2 = PepperFactory.eINSTANCE.createPerson(); assertThat(service.getNextEndDate(MONDAY_2026_07_06, 1, List.of(person1))).isEqualTo(MONDAY_2026_07_06); + assertThat(service.getNextEndDate(TUESDAY_2026_07_07, 1, List.of(person1))).isEqualTo(WEDNESDAY_2026_07_08); assertThat(service.getNextEndDate(MONDAY_2026_07_06, 2, List.of(person1))).isEqualTo(WEDNESDAY_2026_07_08); assertThat(service.getNextEndDate(MONDAY_2026_07_06, 1, List.of(person1, person2))).isEqualTo(MONDAY_2026_07_06); assertThat(service.getNextEndDate(MONDAY_2026_07_06, 2, List.of(person1, person2))).isEqualTo(MONDAY_2026_07_06); diff --git a/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateStepEqualityTests.java b/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateStepEqualityTests.java new file mode 100644 index 0000000..9f6fb82 --- /dev/null +++ b/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateStepEqualityTests.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +package pepper.domain.services.update; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import pepper.peppermm.PepperFactory; +import pepper.peppermm.StartOrEnd; +import pepper.peppermm.Task; + +/** + * Tests equality between update steps that represent the same task. + * @author lfasani + */ +public class TaskUpdateStepEqualityTests { + + private static final Task IMPACTED_TASK = PepperFactory.eINSTANCE.createTask(); + + private static final List STEPS_FOR_SAME_TASK = List.of( + new ComputeDynamicallyChangeUpdateStep(IMPACTED_TASK, true), + new CreateDependencyUpdateStep(IMPACTED_TASK, PepperFactory.eINSTANCE.createTask(), StartOrEnd.START, StartOrEnd.END), + new DependencyUpdateStep(IMPACTED_TASK), + new EffortUpdateStep(IMPACTED_TASK, "8"), + new ParentUpdateStep(IMPACTED_TASK), + new TaskBoundaryUpdateStep(IMPACTED_TASK, Instant.EPOCH, Instant.EPOCH)); + + private static Stream stepsForSameTask() { + return STEPS_FOR_SAME_TASK.stream(); + } + + @ParameterizedTest + @MethodSource("stepsForSameTask") + public void updateStepsWithTheSameImpactedTaskAreEqual(TaskUpdateStep step) { + assertThat(STEPS_FOR_SAME_TASK) + .allSatisfy(otherStep -> { + assertThat(step).isEqualTo(otherStep).hasSameHashCodeAs(otherStep); + assertThat(otherStep).isEqualTo(step); + }); + } + + @ParameterizedTest + @MethodSource("stepsForSameTask") + public void updateStepsWithDistinctImpactedTaskInstancesAreNotEqual(TaskUpdateStep step) { + Task distinctTaskWithTheSameName = PepperFactory.eINSTANCE.createTask(); + IMPACTED_TASK.setName("Task"); + distinctTaskWithTheSameName.setName("Task"); + TaskUpdateStep distinctStep = new EffortUpdateStep(distinctTaskWithTheSameName, "8"); + + assertThat(step).isNotEqualTo(distinctStep); + assertThat(distinctStep).isNotEqualTo(step); + } + + @Test + public void setDeduplicatesUpdateStepsForTheSameImpactedTask() { + Set steps = new HashSet<>(STEPS_FOR_SAME_TASK); + + assertThat(steps).hasSize(1); + } +} diff --git a/backend/pepper-mm/src/main/java/pepper/peppermm/impl/AbstractTaskImpl.java b/backend/pepper-mm/src/main/java/pepper/peppermm/impl/AbstractTaskImpl.java index 5b86f09..4b40314 100644 --- a/backend/pepper-mm/src/main/java/pepper/peppermm/impl/AbstractTaskImpl.java +++ b/backend/pepper-mm/src/main/java/pepper/peppermm/impl/AbstractTaskImpl.java @@ -13,8 +13,8 @@ package pepper.peppermm.impl; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.Collection; +import java.util.Objects; import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.common.notify.NotificationChain; @@ -22,18 +22,15 @@ import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.InternalEObject; import org.eclipse.emf.ecore.impl.ENotificationImpl; -import org.eclipse.emf.ecore.impl.MinimalEObjectImpl; import org.eclipse.emf.ecore.util.EObjectContainmentEList; import org.eclipse.emf.ecore.util.EObjectResolvingEList; import org.eclipse.emf.ecore.util.InternalEList; import pepper.peppermm.AbstractTask; import pepper.peppermm.PepperPackage; -import pepper.peppermm.Person; import pepper.peppermm.Task; import pepper.peppermm.TaskTag; import pepper.peppermm.TaskTimeBoundariesConstraint; -import pepper.peppermm.Team; /** * An implementation of the model object 'Abstract Task'. @@ -66,63 +63,93 @@ public abstract class AbstractTaskImpl extends AssignableObjectImpl implements A protected static final Instant START_TIME_EDEFAULT = null; /** - * The cached value of the '{@link #getStartTime() Start Time}' attribute. + * The default value of the '{@link #getEndTime() End Time}' attribute. * - * @see #getStartTime() + * @see #getEndTime() * @generated * @ordered */ - protected Instant startTime = START_TIME_EDEFAULT; + protected static final Instant END_TIME_EDEFAULT = null; /** - * The default value of the '{@link #getEndTime() End Time}' attribute. + * The default value of the '{@link #getProgress() Progress}' attribute. * - * @see #getEndTime() + * @see #getProgress() * @generated * @ordered */ - protected static final Instant END_TIME_EDEFAULT = null; + protected static final int PROGRESS_EDEFAULT = 0; /** - * The cached value of the '{@link #getEndTime() End Time}' attribute. + * The default value of the '{@link #isComputeStartEndDynamically() Compute Start End Dynamically}' attribute. + * + * @see #isComputeStartEndDynamically() + * @generated + * @ordered + */ + protected static final boolean COMPUTE_START_END_DYNAMICALLY_EDEFAULT = false; + + /** + * The default value of the '{@link #getCalculationOption() Calculation Option}' attribute. + * + * @see #getCalculationOption() + * @generated + * @ordered + */ + protected static final TaskTimeBoundariesConstraint CALCULATION_OPTION_EDEFAULT = TaskTimeBoundariesConstraint.START_END; + + /** + * The default value of the '{@link #getDuration() Duration}' attribute. * - * @see #getEndTime() + * @see #getDuration() * @generated * @ordered */ - protected Instant endTime = END_TIME_EDEFAULT; + protected static final int DURATION_EDEFAULT = 0; /** - * The default value of the '{@link #getProgress() Progress}' attribute. + * The default value of the '{@link #getEffort() Effort}' attribute. + * + * + * @see #getEffort() + * @generated + * @ordered + */ + protected static final int EFFORT_EDEFAULT = 0; + + /** + * The cached value of the '{@link #getStartTime() Start Time}' attribute. * - * @see #getProgress() + * @see #getStartTime() * @generated * @ordered */ - protected static final int PROGRESS_EDEFAULT = 0; + protected Instant startTime = START_TIME_EDEFAULT; /** - * The cached value of the '{@link #getProgress() Progress}' attribute. + * The cached value of the '{@link #getEndTime() End Time}' attribute. * - * @see #getProgress() + * @see #getEndTime() * @generated * @ordered */ - protected int progress = PROGRESS_EDEFAULT; + protected Instant endTime = END_TIME_EDEFAULT; /** - * The default value of the '{@link #isComputeStartEndDynamically() Compute Start End Dynamically}' attribute. - * - * @see #isComputeStartEndDynamically() + * The cached value of the '{@link #getProgress() Progress}' attribute. + * + * @see #getProgress() * @generated * @ordered */ - protected static final boolean COMPUTE_START_END_DYNAMICALLY_EDEFAULT = false; + protected int progress = PROGRESS_EDEFAULT; /** * The cached value of the '{@link #isComputeStartEndDynamically() Compute Start End Dynamically}' attribute. @@ -146,43 +173,23 @@ public abstract class AbstractTaskImpl extends AssignableObjectImpl implements A /** * The cached value of the '{@link #getSubTasks() Sub Tasks}' containment reference list. - * + * * @see #getSubTasks() * @generated * @ordered */ protected EList subTasks; - /** - * The default value of the '{@link #getCalculationOption() Calculation Option}' attribute. - * - * @see #getCalculationOption() - * @generated - * @ordered - */ - protected static final TaskTimeBoundariesConstraint CALCULATION_OPTION_EDEFAULT = TaskTimeBoundariesConstraint.START_END; - /** * The cached value of the '{@link #getCalculationOption() Calculation Option}' attribute. - * + * * @see #getCalculationOption() * @generated * @ordered */ protected TaskTimeBoundariesConstraint calculationOption = CALCULATION_OPTION_EDEFAULT; - /** - * The default value of the '{@link #getDuration() Duration}' attribute. - * - * @see #getDuration() - * @generated - * @ordered - */ - protected static final int DURATION_EDEFAULT = 0; - /** * The cached value of the '{@link #getDuration() Duration}' attribute. * - * - * @see #getEffort() - * @generated - * @ordered - */ - protected static final int EFFORT_EDEFAULT = 0; - /** * The cached value of the '{@link #getEffort() Effort}' attribute. * @@ -248,8 +245,8 @@ public Instant getStartTime() { public void setStartTime(Instant newStartTime) { Instant oldStartTime = startTime; startTime = newStartTime; - if (eNotificationRequired()) - eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__START_TIME, oldStartTime, startTime)); + if (this.eNotificationRequired()) + this.eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__START_TIME, oldStartTime, startTime)); } /** @@ -270,8 +267,8 @@ public Instant getEndTime() { public void setEndTime(Instant newEndTime) { Instant oldEndTime = endTime; endTime = newEndTime; - if (eNotificationRequired()) - eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__END_TIME, oldEndTime, endTime)); + if (this.eNotificationRequired()) + this.eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__END_TIME, oldEndTime, endTime)); } /** @@ -293,18 +290,18 @@ public void setProgress(int newProgress) { if (newProgress >= 0 && newProgress <= 100) { int oldProgress = progress; progress = newProgress; - if (eNotificationRequired()) - eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__PROGRESS, oldProgress, progress)); + if (this.eNotificationRequired()) + this.eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__PROGRESS, oldProgress, progress)); } } /** * - * @generated + * @generated NOT */ @Override public boolean isComputeStartEndDynamically() { - return computeStartEndDynamically; + return computeStartEndDynamically && this instanceof Task task && !task.getSubTasks().isEmpty(); } /** @@ -315,8 +312,8 @@ public boolean isComputeStartEndDynamically() { public void setComputeStartEndDynamically(boolean newComputeStartEndDynamically) { boolean oldComputeStartEndDynamically = computeStartEndDynamically; computeStartEndDynamically = newComputeStartEndDynamically; - if (eNotificationRequired()) - eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__COMPUTE_START_END_DYNAMICALLY, oldComputeStartEndDynamically, computeStartEndDynamically)); + if (this.eNotificationRequired()) + this.eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__COMPUTE_START_END_DYNAMICALLY, oldComputeStartEndDynamically, computeStartEndDynamically)); } /** @@ -360,8 +357,8 @@ public TaskTimeBoundariesConstraint getCalculationOption() { public void setCalculationOption(TaskTimeBoundariesConstraint newCalculationOption) { TaskTimeBoundariesConstraint oldCalculationOption = calculationOption; calculationOption = newCalculationOption == null ? CALCULATION_OPTION_EDEFAULT : newCalculationOption; - if (eNotificationRequired()) - eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__CALCULATION_OPTION, oldCalculationOption, calculationOption)); + if (this.eNotificationRequired()) + this.eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__CALCULATION_OPTION, oldCalculationOption, calculationOption)); } /** @@ -382,8 +379,8 @@ public int getDuration() { public void setDuration(int newDuration) { int oldDuration = duration; duration = newDuration; - if (eNotificationRequired()) - eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__DURATION, oldDuration, duration)); + if (this.eNotificationRequired()) + this.eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__DURATION, oldDuration, duration)); } /** @@ -405,8 +402,8 @@ public int getEffort() { public void setEffort(int newEffort) { int oldEffort = effort; effort = newEffort; - if (eNotificationRequired()) - eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__EFFORT, oldEffort, effort)); + if (this.eNotificationRequired()) + this.eNotify(new ENotificationImpl(this, Notification.SET, PepperPackage.ABSTRACT_TASK__EFFORT, oldEffort, effort)); } /** @@ -417,7 +414,7 @@ public void setEffort(int newEffort) { public NotificationChain eInverseRemove(InternalEObject otherEnd, int featureID, NotificationChain msgs) { switch (featureID) { case PepperPackage.ABSTRACT_TASK__SUB_TASKS: - return ((InternalEList)getSubTasks()).basicRemove(otherEnd, msgs); + return ((InternalEList) this.getSubTasks()).basicRemove(otherEnd, msgs); } return super.eInverseRemove(otherEnd, featureID, msgs); } @@ -430,23 +427,23 @@ public NotificationChain eInverseRemove(InternalEObject otherEnd, int featureID, public Object eGet(int featureID, boolean resolve, boolean coreType) { switch (featureID) { case PepperPackage.ABSTRACT_TASK__START_TIME: - return getStartTime(); + return this.getStartTime(); case PepperPackage.ABSTRACT_TASK__END_TIME: - return getEndTime(); + return this.getEndTime(); case PepperPackage.ABSTRACT_TASK__PROGRESS: - return getProgress(); + return this.getProgress(); case PepperPackage.ABSTRACT_TASK__COMPUTE_START_END_DYNAMICALLY: - return isComputeStartEndDynamically(); + return this.isComputeStartEndDynamically(); case PepperPackage.ABSTRACT_TASK__TAGS: - return getTags(); + return this.getTags(); case PepperPackage.ABSTRACT_TASK__SUB_TASKS: - return getSubTasks(); + return this.getSubTasks(); case PepperPackage.ABSTRACT_TASK__CALCULATION_OPTION: - return getCalculationOption(); + return this.getCalculationOption(); case PepperPackage.ABSTRACT_TASK__DURATION: - return getDuration(); + return this.getDuration(); case PepperPackage.ABSTRACT_TASK__EFFORT: - return getEffort(); + return this.getEffort(); } return super.eGet(featureID, resolve, coreType); } @@ -460,33 +457,33 @@ public Object eGet(int featureID, boolean resolve, boolean coreType) { public void eSet(int featureID, Object newValue) { switch (featureID) { case PepperPackage.ABSTRACT_TASK__START_TIME: - setStartTime((Instant)newValue); + this.setStartTime((Instant)newValue); return; case PepperPackage.ABSTRACT_TASK__END_TIME: - setEndTime((Instant)newValue); + this.setEndTime((Instant)newValue); return; case PepperPackage.ABSTRACT_TASK__PROGRESS: - setProgress((Integer)newValue); + this.setProgress((Integer)newValue); return; case PepperPackage.ABSTRACT_TASK__COMPUTE_START_END_DYNAMICALLY: - setComputeStartEndDynamically((Boolean)newValue); + this.setComputeStartEndDynamically((Boolean)newValue); return; case PepperPackage.ABSTRACT_TASK__TAGS: - getTags().clear(); - getTags().addAll((Collection)newValue); + this.getTags().clear(); + this.getTags().addAll((Collection)newValue); return; case PepperPackage.ABSTRACT_TASK__SUB_TASKS: - getSubTasks().clear(); - getSubTasks().addAll((Collection)newValue); + this.getSubTasks().clear(); + this.getSubTasks().addAll((Collection)newValue); return; case PepperPackage.ABSTRACT_TASK__CALCULATION_OPTION: - setCalculationOption((TaskTimeBoundariesConstraint)newValue); + this.setCalculationOption((TaskTimeBoundariesConstraint)newValue); return; case PepperPackage.ABSTRACT_TASK__DURATION: - setDuration((Integer)newValue); + this.setDuration((Integer)newValue); return; case PepperPackage.ABSTRACT_TASK__EFFORT: - setEffort((Integer)newValue); + this.setEffort((Integer)newValue); return; } super.eSet(featureID, newValue); @@ -500,31 +497,31 @@ public void eSet(int featureID, Object newValue) { public void eUnset(int featureID) { switch (featureID) { case PepperPackage.ABSTRACT_TASK__START_TIME: - setStartTime(START_TIME_EDEFAULT); + this.setStartTime(START_TIME_EDEFAULT); return; case PepperPackage.ABSTRACT_TASK__END_TIME: - setEndTime(END_TIME_EDEFAULT); + this.setEndTime(END_TIME_EDEFAULT); return; case PepperPackage.ABSTRACT_TASK__PROGRESS: - setProgress(PROGRESS_EDEFAULT); + this.setProgress(PROGRESS_EDEFAULT); return; case PepperPackage.ABSTRACT_TASK__COMPUTE_START_END_DYNAMICALLY: - setComputeStartEndDynamically(COMPUTE_START_END_DYNAMICALLY_EDEFAULT); + this.setComputeStartEndDynamically(COMPUTE_START_END_DYNAMICALLY_EDEFAULT); return; case PepperPackage.ABSTRACT_TASK__TAGS: - getTags().clear(); + this.getTags().clear(); return; case PepperPackage.ABSTRACT_TASK__SUB_TASKS: - getSubTasks().clear(); + this.getSubTasks().clear(); return; case PepperPackage.ABSTRACT_TASK__CALCULATION_OPTION: - setCalculationOption(CALCULATION_OPTION_EDEFAULT); + this.setCalculationOption(CALCULATION_OPTION_EDEFAULT); return; case PepperPackage.ABSTRACT_TASK__DURATION: - setDuration(DURATION_EDEFAULT); + this.setDuration(DURATION_EDEFAULT); return; case PepperPackage.ABSTRACT_TASK__EFFORT: - setEffort(EFFORT_EDEFAULT); + this.setEffort(EFFORT_EDEFAULT); return; } super.eUnset(featureID); @@ -538,9 +535,9 @@ public void eUnset(int featureID) { public boolean eIsSet(int featureID) { switch (featureID) { case PepperPackage.ABSTRACT_TASK__START_TIME: - return START_TIME_EDEFAULT == null ? startTime != null : !START_TIME_EDEFAULT.equals(startTime); + return !Objects.equals(START_TIME_EDEFAULT, startTime); case PepperPackage.ABSTRACT_TASK__END_TIME: - return END_TIME_EDEFAULT == null ? endTime != null : !END_TIME_EDEFAULT.equals(endTime); + return !Objects.equals(END_TIME_EDEFAULT, endTime); case PepperPackage.ABSTRACT_TASK__PROGRESS: return progress != PROGRESS_EDEFAULT; case PepperPackage.ABSTRACT_TASK__COMPUTE_START_END_DYNAMICALLY: @@ -565,25 +562,24 @@ public boolean eIsSet(int featureID) { */ @Override public String toString() { - if (eIsProxy()) return super.toString(); - - StringBuilder result = new StringBuilder(super.toString()); - result.append(" (startTime: "); - result.append(startTime); - result.append(", endTime: "); - result.append(endTime); - result.append(", progress: "); - result.append(progress); - result.append(", computeStartEndDynamically: "); - result.append(computeStartEndDynamically); - result.append(", calculationOption: "); - result.append(calculationOption); - result.append(", duration: "); - result.append(duration); - result.append(", effort: "); - result.append(effort); - result.append(')'); - return result.toString(); + if (this.eIsProxy()) return super.toString(); + + String result = super.toString() + " (startTime: " + + startTime + + ", endTime: " + + endTime + + ", progress: " + + progress + + ", computeStartEndDynamically: " + + computeStartEndDynamically + + ", calculationOption: " + + calculationOption + + ", duration: " + + duration + + ", effort: " + + effort + + ')'; + return result; } } // AbstractTaskImpl diff --git a/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMProjectTemplateInitializerEmpty.java b/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMProjectTemplateInitializerEmpty.java index dd55fdd..7295826 100644 --- a/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMProjectTemplateInitializerEmpty.java +++ b/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMProjectTemplateInitializerEmpty.java @@ -25,9 +25,6 @@ import org.eclipse.sirius.web.application.project.services.api.ISemanticDataInitializer; import org.springframework.stereotype.Service; -import pepper.domain.services.TaskComputationService; -import pepper.domain.services.WorkpackageComputationService; - /** * Provides Pepper meta model specific project empty template. * @@ -38,15 +35,8 @@ public class PepperMMProjectTemplateInitializerEmpty implements ISemanticDataIni private final IEditingContextPersistenceService editingContextPersistenceService; - private final TaskComputationService taskComputationService; - - private final WorkpackageComputationService workpackageComputationService; - - public PepperMMProjectTemplateInitializerEmpty(IEditingContextPersistenceService editingContextPersistenceService, TaskComputationService taskComputationService, - WorkpackageComputationService workpackageComputationService) { + public PepperMMProjectTemplateInitializerEmpty(IEditingContextPersistenceService editingContextPersistenceService) { this.editingContextPersistenceService = editingContextPersistenceService; - this.taskComputationService = taskComputationService; - this.workpackageComputationService = workpackageComputationService; } @Override @@ -63,7 +53,7 @@ public void handle(ICause cause, IEditingContext editingContext, String projectT resource.eAdapters().add(resourceMetadataAdapter); emfEditingContext.getDomain().getResourceSet().getResources().add(resource); - resource.getContents().add(new PepperMMSampleBuilder(this.taskComputationService, this.workpackageComputationService).getEmptySampleContent()); + resource.getContents().add(new PepperMMSampleBuilder().getEmptySampleContent()); this.editingContextPersistenceService.persist(cause, editingContext); } diff --git a/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMProjectTemplateInitializerSample.java b/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMProjectTemplateInitializerSample.java index f9a073e..fab56b3 100644 --- a/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMProjectTemplateInitializerSample.java +++ b/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMProjectTemplateInitializerSample.java @@ -36,8 +36,6 @@ import org.eclipse.sirius.web.application.project.services.api.ISemanticDataInitializer; import org.springframework.stereotype.Service; -import pepper.domain.services.TaskComputationService; -import pepper.domain.services.WorkpackageComputationService; import pepper.peppermm.Organization; import pepper.peppermm.Workpackage; @@ -55,21 +53,14 @@ public class PepperMMProjectTemplateInitializerSample implements ISemanticDataIn private final IRepresentationDescriptionSearchService representationDescriptionSearchService; private final IRepresentationMetadataPersistenceService representationMetadataPersistenceService; - private final TaskComputationService taskComputationService; - - private final WorkpackageComputationService workpackageComputationService; - public PepperMMProjectTemplateInitializerSample(IEditingContextPersistenceService editingContextPersistenceService, IRepresentationPersistenceService representationPersistenceService, IGanttCreationService ganttCreationService, IRepresentationDescriptionSearchService representationDescriptionSearchService, - IRepresentationMetadataPersistenceService representationMetadataPersistenceService, TaskComputationService taskComputationService, - WorkpackageComputationService workpackageComputationService) { + IRepresentationMetadataPersistenceService representationMetadataPersistenceService) { this.editingContextPersistenceService = editingContextPersistenceService; this.representationPersistenceService = representationPersistenceService; this.ganttCreationService = ganttCreationService; this.representationDescriptionSearchService = representationDescriptionSearchService; this.representationMetadataPersistenceService = representationMetadataPersistenceService; - this.taskComputationService = taskComputationService; - this.workpackageComputationService = workpackageComputationService; } @Override @@ -86,7 +77,7 @@ public void handle(ICause cause, IEditingContext editingContext, String projectT resource.eAdapters().add(resourceMetadataAdapter); emfEditingContext.getDomain().getResourceSet().getResources().add(resource); - resource.getContents().add(new PepperMMSampleBuilder(this.taskComputationService, this.workpackageComputationService).getSampleContent()); + resource.getContents().add(new PepperMMSampleBuilder().getSampleContent()); this.editingContextPersistenceService.persist(cause, editingContext); diff --git a/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMSampleBuilder.java b/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMSampleBuilder.java index 42acbea..c85d936 100644 --- a/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMSampleBuilder.java +++ b/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMSampleBuilder.java @@ -78,13 +78,11 @@ public class PepperMMSampleBuilder { private static final String MAIN_WORKPACKAGE = "Main workpackage"; - private final TaskComputationService taskComputationService; + private final TaskComputationService taskComputationService = new TaskComputationService(); - private final WorkpackageComputationService workpackageComputationService; + private final WorkpackageComputationService workpackageComputationService = new WorkpackageComputationService(); - public PepperMMSampleBuilder(TaskComputationService taskComputationService, WorkpackageComputationService workpackageComputationService) { - this.taskComputationService = taskComputationService; - this.workpackageComputationService = workpackageComputationService; + public PepperMMSampleBuilder() { } public Organization getSampleContent() { @@ -168,7 +166,7 @@ private Project createDevProject(Person paul, Person peter) { taskComputationService.updateEndTime(development, Instant.parse(DATE_2023_12_16T23_59_00Z)); development.setComputeStartEndDynamically(true); - Task codeDev = createCodeDev(peter); + Task codeDev = this.createCodeDev(peter); Task frontDev = PepperFactory.eINSTANCE.createTask(); frontDev.setName("Front"); taskComputationService.updateStartTime(frontDev, Instant.parse(DATE_2023_12_13T00_00_00Z)); diff --git a/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMSamplesProvider.java b/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMSamplesProvider.java index d996260..0b460ae 100644 --- a/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMSamplesProvider.java +++ b/backend/pepper-starter/src/main/java/pepper/starter/services/project/PepperMMSamplesProvider.java @@ -23,9 +23,6 @@ import org.eclipse.sirius.components.emf.services.JSONResourceFactory; import org.springframework.stereotype.Service; -import pepper.domain.services.TaskComputationService; -import pepper.domain.services.WorkpackageComputationService; - /** * Used to provide an example of PepperMM. * @@ -36,14 +33,8 @@ public class PepperMMSamplesProvider { private final List migrationParticipants; - private final TaskComputationService taskComputationService; - - private final WorkpackageComputationService workpackageComputationService; - - public PepperMMSamplesProvider(List migrationParticipants, TaskComputationService taskComputationService, WorkpackageComputationService workpackageComputationService) { + public PepperMMSamplesProvider(List migrationParticipants) { this.migrationParticipants = Objects.requireNonNull(migrationParticipants); - this.taskComputationService = Objects.requireNonNull(taskComputationService); - this.workpackageComputationService = workpackageComputationService; } public UUID addPepperMMSample(ResourceSet resourceSet, String resourceName) { @@ -58,7 +49,7 @@ public UUID addPepperMMSample(ResourceSet resourceSet, String resourceName) { resource.eAdapters().add(resourceMetadataAdapter); resourceSet.getResources().add(resource); - resource.getContents().add(new PepperMMSampleBuilder(this.taskComputationService, this.workpackageComputationService).getSampleContent()); + resource.getContents().add(new PepperMMSampleBuilder().getSampleContent()); return documentId; } @@ -75,7 +66,7 @@ public UUID addEmptyPepperMM(ResourceSet resourceSet, String resourceName) { resource.eAdapters().add(resourceMetadataAdapter); resourceSet.getResources().add(resource); - resource.getContents().add(new PepperMMSampleBuilder(this.taskComputationService, this.workpackageComputationService).getEmptySampleContent()); + resource.getContents().add(new PepperMMSampleBuilder().getEmptySampleContent()); return documentId; } diff --git a/backend/pepper-starter/src/main/java/pepper/starter/services/representations/PepperMMJavaService.java b/backend/pepper-starter/src/main/java/pepper/starter/services/representations/PepperMMJavaService.java index 2f34ef1..40d7de2 100644 --- a/backend/pepper-starter/src/main/java/pepper/starter/services/representations/PepperMMJavaService.java +++ b/backend/pepper-starter/src/main/java/pepper/starter/services/representations/PepperMMJavaService.java @@ -14,18 +14,17 @@ import java.time.Instant; import java.time.LocalDate; -import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.StreamSupport; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.ecore.EObject; -import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.sirius.components.core.api.IFeedbackMessageService; import org.eclipse.sirius.components.interpreter.SimpleCrossReferenceProvider; @@ -34,6 +33,10 @@ import pepper.domain.services.TaskComputationService; import pepper.domain.services.WorkpackageComputationService; +import pepper.domain.services.update.CreateDependencyUpdateStep; +import pepper.domain.services.update.DependencyUpdateStep; +import pepper.domain.services.update.TaskBoundaryUpdateStep; +import pepper.domain.services.update.TaskUpdateService; import pepper.peppermm.AbstractTask; import pepper.peppermm.DependencyLink; import pepper.peppermm.DependencyRelatedObject; @@ -41,7 +44,6 @@ import pepper.peppermm.Project; import pepper.peppermm.StartOrEnd; import pepper.peppermm.Task; -import pepper.peppermm.TaskTimeBoundariesConstraint; import pepper.peppermm.Workpackage; /** @@ -57,105 +59,41 @@ public class PepperMMJavaService { private final IFeedbackMessageService feedbackMessageService; - private final TaskComputationService taskComputationService; + private final TaskComputationService taskComputationService = new TaskComputationService(); - private final WorkpackageComputationService workpackageComputationService; + private final WorkpackageComputationService workpackageComputationService = new WorkpackageComputationService(); - private final ZoneId zone = ZoneId.systemDefault(); + private final TaskUpdateService taskUpdateService; - public PepperMMJavaService(IFeedbackMessageService feedbackMessageService, TaskComputationService taskComputationService, WorkpackageComputationService workpackageComputationService) { + public PepperMMJavaService(IFeedbackMessageService feedbackMessageService, TaskUpdateService taskUpdateService) { this.feedbackMessageService = Objects.requireNonNull(feedbackMessageService); - this.taskComputationService = taskComputationService; - this.workpackageComputationService = workpackageComputationService; + this.taskUpdateService = taskUpdateService; } - private static Instant getTaskStartTime(Task task) { - if (task.isComputeStartEndDynamically()) { - return task.getSubTasks().stream() - .map(PepperMMJavaService::getTaskStartTime) - .min(Instant::compareTo) - .orElse(task.getStartTime()); - } - return task.getStartTime(); - } - - private static Instant getTaskEndTime(Task task) { - if (task.isComputeStartEndDynamically()) { - return task.getSubTasks().stream() - .map(PepperMMJavaService::getTaskEndTime) - .max(Instant::compareTo) - .orElse(task.getEndTime()); - } - return task.getEndTime(); - } - - private static Instant getlaterInstant(DependencyLink dep) { - Instant laterInstant = null; - Task source = (Task) dep.getSource(); - if (dep.getSourceKind() == StartOrEnd.END) { - laterInstant = getTaskEndTime(source).plus(dep.getDelay(), ChronoUnit.HOURS); - } else if (dep.getSourceKind() == StartOrEnd.START) { - laterInstant = getTaskStartTime(source).plus(dep.getDelay(), ChronoUnit.HOURS); - } - return laterInstant; - } +// private static Instant getTaskStartTime(Task task) { +// if (task.isComputeStartEndDynamically()) { +// return task.getSubTasks().stream() +// .map(PepperMMJavaService::getTaskStartTime) +// .min(Instant::compareTo) +// .orElse(task.getStartTime()); +// } +// return task.getStartTime(); +// } - @SuppressWarnings({ "checkstyle:NestedIfDepth", "checkstyle:MethodLength", "checkstyle:MissingSwitchDefault" }) public void editTask(EObject eObject, String name, String description, Instant startTime, Instant endTime, Integer progress, boolean keepEffort) { + if (eObject instanceof Task task) { + taskUpdateService.updateWithImpacts(task, new TaskBoundaryUpdateStep(task, startTime, endTime)); + if (name != null) { task.setName(name); } - if (description != null) { - task.setDescription(description); - } - if (endTime != null && startTime != null) { - Instant newStartTime = taskComputationService.roundToNearestHalfDay(startTime); - Instant newEndTime = taskComputationService.roundToNearestHalfDay(endTime); - long differenceStart = newStartTime.getEpochSecond() - taskComputationService.roundToNearestHalfDay(task.getStartTime()).getEpochSecond(); - long differenceEnd = newEndTime.getEpochSecond() - taskComputationService.roundToNearestHalfDay(task.getEndTime()).getEpochSecond(); - boolean taskShifted = differenceStart != 0 && differenceEnd != 0; - List dependencies = task.getDependencies(); - // Nothing is done when moving a task constrained by dependencies - if (dependencies.isEmpty() || !taskShifted) { - boolean startTimeControlledByDependency = - dependencies.stream() - .anyMatch(dep -> dep.getTargetKind() == StartOrEnd.START); - - boolean endTimeControlledByDependency = - dependencies.stream() - .anyMatch(dep -> dep.getTargetKind() == StartOrEnd.END); - - if (taskShifted) { - if (dependencies.isEmpty()) { - TaskTimeBoundariesConstraint calculationOption = task.getCalculationOption(); - switch (calculationOption) { - case START_EFFORT -> taskComputationService.updateStartTime(task, newStartTime); - case END_EFFORT -> taskComputationService.updateEndTime(task, newEndTime); - case START_END -> { - taskComputationService.updateStartTime(task, newStartTime); - taskComputationService.updateEndTime(task, newEndTime); - } - } - this.followMoveDependency(task); - } - } else { - if (differenceStart != 0 && !startTimeControlledByDependency) { - taskComputationService.updateStartTime(task, newStartTime); - this.followMoveDependency(task); - } - - if (differenceEnd != 0 && !endTimeControlledByDependency) { - taskComputationService.updateEndTime(task, newEndTime); - this.followMoveDependency(task); - } - } - - } - } if (progress != null) { task.setProgress(progress); } + if (description != null) { + task.setDescription(description); + } } } @@ -209,59 +147,41 @@ public void createTask(EObject context) { /** * Delete a given {@link DependencyRelatedObject} and all {@link DependencyLink} related to it. Then update all dependency placement of its dependent objects. * - * @param context - * the object to delete + * @param taskToDelete + * the task to delete */ - public void deleteDependencyRelatedObject(EObject context) { - if (context instanceof DependencyRelatedObject source) { - List targetDependencies = new ArrayList<>(); - List sourceDependencies = source.getDependencies(); - - Collection sourceInverseReferences = simpleCrossReferenceProvider.getInverseReferences(source); - for (EStructuralFeature.Setting sourceInverseReference : sourceInverseReferences) { - if (sourceInverseReference.getEObject() instanceof DependencyLink dependencyLink) { - if (dependencyLink.eContainer() instanceof DependencyRelatedObject object) { - targetDependencies.add(object); - } - EcoreUtil.delete(dependencyLink, true); - } - } - if (source instanceof Task task) { - for (Task subTask : task.getSubTasks()) { - targetDependencies.addAll(this.getAllDependencyTargetTask(new LinkedHashSet<>(), subTask)); - } - } - - EcoreUtil.delete(source, true); - - for (DependencyLink sourceDependencyLink : sourceDependencies) { - this.followMoveDependency(sourceDependencyLink.getSource()); - } - for (DependencyRelatedObject targetDependency : targetDependencies) { - List dependencyLinksOfTargetDependencies = targetDependency.getDependencies(); - for (DependencyLink dependencyLinksOfTargetDependency : dependencyLinksOfTargetDependencies) { - this.followMoveDependency(dependencyLinksOfTargetDependency.getSource()); - } - } - } - } + public void deleteDependencyRelatedObject(DependencyRelatedObject taskToDelete) { + List otherTasksToDelete = StreamSupport.stream(Spliterators.spliteratorUnknownSize(taskToDelete.eAllContents(), Spliterator.ORDERED), false) + .filter(DependencyRelatedObject.class::isInstance) + .map(DependencyRelatedObject.class::cast) + .toList(); + List allTasksToDelete = new ArrayList<>(otherTasksToDelete); + allTasksToDelete.add(taskToDelete); + + List dependencyLinkOfNotDeletedImpactedObjects = allTasksToDelete.stream() + .flatMap(dependencyRelatedObject -> { + return simpleCrossReferenceProvider.getInverseReferences(dependencyRelatedObject).stream() + .map(setting -> { + if (setting.getEObject() instanceof DependencyLink dependencyLink) { + return dependencyLink; + } + return null; + }); + }) + .filter(Objects::nonNull) + .filter(dependencyLink -> !otherTasksToDelete.contains(dependencyLink.eContainer())) + .toList(); - private LinkedHashSet getAllDependencyTargetTask(LinkedHashSet targetTasks, Task task) { + List otherImpactedTasks = dependencyLinkOfNotDeletedImpactedObjects.stream() + .map(EObject::eContainer) + .filter(DependencyRelatedObject.class::isInstance) + .map(DependencyRelatedObject.class::cast) + .toList(); - Collection sourceInverseReferences = simpleCrossReferenceProvider.getInverseReferences(task); - for (EStructuralFeature.Setting sourceInverseReference : sourceInverseReferences) { - if (sourceInverseReference.getEObject() instanceof DependencyLink dependencyLink) { - if (dependencyLink.eContainer() instanceof Task object) { - targetTasks.add(object); - } - EcoreUtil.delete(dependencyLink, true); - } - } + dependencyLinkOfNotDeletedImpactedObjects.forEach(EcoreUtil::delete); + EcoreUtil.delete(taskToDelete, true); - for (Task subTask : task.getSubTasks()) { - this.getAllDependencyTargetTask(targetTasks, subTask); - } - return targetTasks; + otherImpactedTasks.forEach(dependencyRelatedObject -> taskUpdateService.updateWithImpacts(dependencyRelatedObject, new DependencyUpdateStep(dependencyRelatedObject))); } /** @@ -274,517 +194,31 @@ private LinkedHashSet getAllDependencyTargetTask(LinkedHashSet targe * @param source * the dependency source */ - public void deleteDependencyLink(EObject target, EObject source) { - if (target instanceof DependencyRelatedObject targetObject) { - if (source instanceof DependencyRelatedObject sourceObject) { - targetObject.getDependencies().removeIf(dep -> dep.getSource().equals(sourceObject)); - } - - for (DependencyLink targetDependencyLink : targetObject.getDependencies()) { - this.followMoveDependency(targetDependencyLink.getSource()); - } - } - } - - public void createDependencyLink(EObject source, EObject target, org.eclipse.sirius.components.gantt.StartOrEnd sourceStartOrEnd, org.eclipse.sirius.components.gantt.StartOrEnd targetStartOrEnd) { - DependencyLink dependencyLink = PepperFactory.eINSTANCE.createDependencyLink(); - if (sourceStartOrEnd.equals(org.eclipse.sirius.components.gantt.StartOrEnd.END)) { - dependencyLink.setSourceKind(StartOrEnd.END); - } else { - dependencyLink.setSourceKind(StartOrEnd.START); - } - if (targetStartOrEnd.equals(org.eclipse.sirius.components.gantt.StartOrEnd.START)) { - dependencyLink.setTargetKind(StartOrEnd.START); - } else { - dependencyLink.setTargetKind(StartOrEnd.END); - } - if (source instanceof DependencyRelatedObject sourceObject) { - dependencyLink.setSource(sourceObject); - if (target instanceof DependencyRelatedObject targetObject) { - //Ensure no dependency already exists between source and target to prevent duplicates or cycles - if (!this.isDuplicateOrCycle(sourceObject, targetObject)) { - //Ensure the target task is not computed dynamically - if (targetObject instanceof Task targetTask && targetTask.isComputeStartEndDynamically()) { - this.feedbackMessageService.addFeedbackMessage(new Message("Creating a dependency targeting a dynamically computed task is not possible.", MessageLevel.ERROR)); - } else { - targetObject.getDependencies().add(dependencyLink); - this.followMoveDependency(sourceObject); - } - } - } - } - } - - /** - * Checks if the source task already depends on one of the target task's ancestor tasks. - * - * @param sourceObject - * the {@link DependencyRelatedObject} source - * @param parent - * the parent {@link Task} of the target - * @return {@code true} if the source already depends on the specified parent or one of its ancestors; {@code false} otherwise - */ - private boolean isParentCycle(DependencyRelatedObject sourceObject, Task parent) { - for (DependencyLink dep : sourceObject.getDependencies()) { - if (dep.getSource().equals(parent)) { - return true; - } - } - boolean isParentCycle = false; - if (parent.eContainer() instanceof Task grandParent) { - isParentCycle = this.isParentCycle(sourceObject, grandParent); - } - return isParentCycle; - } - - private boolean isCycle(DependencyRelatedObject sourceObject, DependencyRelatedObject targetObject) { - boolean isCycle = false; - for (DependencyLink dep : sourceObject.getDependencies()) { - if (dep.getSource().equals(targetObject)) { - isCycle = true; - } else if (!isCycle) { - isCycle = this.isCycle(dep.getSource(), targetObject); - } - } - return isCycle; - } - - /** - * Validates a dependency creation request. - * - * @param sourceObject - * the dependency source - * @param targetObject - * the dependency target - * @return {@code true} if the dependency is invalid because it would create a cycle or duplicate a dependency; {@code false} otherwise - */ - private boolean isDuplicateOrCycle(DependencyRelatedObject sourceObject, DependencyRelatedObject targetObject) { - //to prevent cycles - boolean isParentChildDependency = sourceObject.equals(targetObject.eContainer()) || targetObject.equals(sourceObject.eContainer()); - boolean isParentCycle = false; - if (targetObject.eContainer() instanceof Task parent) { - isParentCycle = this.isParentCycle(sourceObject, parent); - } - - boolean isCycle = this.isCycle(sourceObject, targetObject); - - if (isParentChildDependency) { - this.feedbackMessageService.addFeedbackMessage(new Message("Creating a dependency between a parent task and one of its children is not possible.", MessageLevel.ERROR)); - } - if (isParentCycle) { - this.feedbackMessageService.addFeedbackMessage( - new Message("Creating a dependency when the source task already depends on one of the target task's parent tasks is not possible", MessageLevel.ERROR)); - } - if (isCycle) { - this.feedbackMessageService.addFeedbackMessage(new Message("Creating a cyclic dependency is not possible.", MessageLevel.ERROR)); - } - - if (isCycle || isParentCycle || isParentChildDependency) { - return true; - } else { - //to prevent duplicates - boolean isDuplicate = false; - for (DependencyLink dep : targetObject.getDependencies()) { - if (dep.getSource().equals(sourceObject)) { - isDuplicate = true; - break; - } - } - if (isDuplicate) { - this.feedbackMessageService.addFeedbackMessage(new Message("Creating a duplicated dependency is not possible.", MessageLevel.ERROR)); - } - return isDuplicate; - } - } - - /** - * Finds all {@link DependencyRelatedObject} instances that depend on the given {@link DependencyRelatedObject} and update them according to their dependency relationships. - * - * @param sourceObject - * the object that has been moved - */ - public void followMoveDependency(DependencyRelatedObject sourceObject) { - List targetTasks = new ArrayList<>(); - List targetWorkpackages = new ArrayList<>(); - //get all tasks pointed by sourceTask - for (var inverseReference : simpleCrossReferenceProvider.getInverseReferences(sourceObject)) { - if (inverseReference.getEObject() instanceof DependencyLink dep) { - for (var inverseReferenceDependencyLink : simpleCrossReferenceProvider.getInverseReferences(dep)) { - var target = inverseReferenceDependencyLink.getEObject(); - if (target instanceof Task targetTask && sourceObject instanceof Task) { - targetTasks.add(targetTask); - } else if (target instanceof Workpackage targetWorkpackage && sourceObject instanceof Workpackage) { - targetWorkpackages.add(targetWorkpackage); - } - } - } - } - if (sourceObject instanceof Task sourceTask) { - this.followTaskMoveDependency(targetTasks, sourceTask); - this.followMoveDependenciesParent(sourceTask); - } - if (sourceObject instanceof Workpackage sourceWorkpackage) { - this.followWorkpackageMoveDependency(targetWorkpackages, sourceWorkpackage); - } - } - - /** - * Updates the dates of all {@link Workpackage} that depend on the given source {@link Workpackage}. - * - * @param targetWorkpackages - * the dependent workpackages - * @param sourceWorkpackage - * the workpackage that has been moved - */ - private void followWorkpackageMoveDependency(List targetWorkpackages, Workpackage sourceWorkpackage) { - List dependencies = new ArrayList<>(); - for (Workpackage workpackage : targetWorkpackages) { - //Get the strongest dependency links - DependencyLink winnerStart = null; - DependencyLink winnerEnd = null; - LocalDate laterStart = null; - LocalDate laterEnd = null; - for (DependencyLink dep : workpackage.getDependencies()) { - if (dep.getTargetKind().equals(StartOrEnd.END)) { - LocalDate newLocalDate = this.getlaterLocalDate(dep); - if (laterEnd == null || laterEnd.isBefore(newLocalDate)) { - laterEnd = newLocalDate; - winnerEnd = dep; - } - } - if (dep.getTargetKind().equals(StartOrEnd.START)) { - LocalDate newLocalDate = this.getlaterLocalDate(dep); - if (laterStart == null || laterStart.isBefore(newLocalDate)) { - laterStart = newLocalDate; - winnerStart = dep; - } - } - } - for (DependencyLink dep : workpackage.getDependencies()) { - if ((dep.equals(winnerStart) && winnerEnd == null) || (dep.equals(winnerEnd) && winnerStart == null)) { - Workpackage bestSourceWorkpackage = (Workpackage) dep.getSource(); - this.setWorkpackageNewDates(workpackage, dep); - if (bestSourceWorkpackage == sourceWorkpackage) { - dependencies.add(workpackage); - } - } else if (dep.equals(winnerEnd)) { - Workpackage bestSourceWorkpackage = (Workpackage) dep.getSource(); - this.setWorkpackageNewEndDate(workpackage, dep); - if (bestSourceWorkpackage == sourceWorkpackage) { - dependencies.add(workpackage); - } - } else if (dep.equals(winnerStart)) { - Workpackage bestSourceWorkpackage = (Workpackage) dep.getSource(); - this.setWorkpackageNewStartDate(workpackage, dep); - if (bestSourceWorkpackage == sourceWorkpackage) { - dependencies.add(workpackage); - } - } - } - if (winnerEnd != null && winnerStart != null) { - if (workpackage.getStartDate().isAfter(workpackage.getEndDate())) { - workpackageComputationService.updateEffort(workpackage, 1); - workpackageComputationService.updateEndDate(workpackage, workpackage.getStartDate().plusDays(1)); - this.feedbackMessageService.addFeedbackMessage( - new Message("Task dependencies overlap : End date has been changed to avoid to have end date before start date.", MessageLevel.WARNING)); - } - } - } - for (Workpackage workpackage : dependencies) { - this.followMoveDependency(workpackage); - } - } - - /** - * Updates the dates of all {@link Task} that depend on the given source {@link Task}. - * - * @param targetTasks - * the dependent workpackages - * @param sourceTask - * the workpackage that has been moved - */ - private void followTaskMoveDependency(List targetTasks, Task sourceTask) { - List dependencies = new ArrayList<>(); - for (Task task : targetTasks) { - //Get the strongest dependency links - DependencyLink winnerStart = null; - DependencyLink winnerEnd = null; - Instant laterInstantStart = null; - Instant laterInstantEnd = null; - for (DependencyLink dep : task.getDependencies()) { - if (dep.getTargetKind().equals(StartOrEnd.END)) { - Instant newInstant = getlaterInstant(dep); - if (laterInstantEnd == null || laterInstantEnd.isBefore(newInstant)) { - laterInstantEnd = newInstant; - winnerEnd = dep; - } - } - if (dep.getTargetKind().equals(StartOrEnd.START)) { - Instant newInstant = getlaterInstant(dep); - if (laterInstantStart == null || laterInstantStart.isBefore(newInstant)) { - laterInstantStart = newInstant; - winnerStart = dep; - } - } - } - for (DependencyLink dep : task.getDependencies()) { - //if the task is only pointed to one extremity - if ((dep.equals(winnerStart) && winnerEnd == null) || (dep.equals(winnerEnd) && winnerStart == null)) { - Task bestSourceTask = (Task) dep.getSource(); - this.setTaskNewDates(task, dep); - if (bestSourceTask == sourceTask) { - dependencies.add(task); - } - } else if (dep.equals(winnerEnd)) { - Task bestSourceTask = (Task) dep.getSource(); - this.setTaskNewEndDate(task, dep); - if (bestSourceTask == sourceTask) { - dependencies.add(task); - } - } else if (dep.equals(winnerStart)) { - Task bestSourceTask = (Task) dep.getSource(); - this.setTaskNewStartDate(task, dep); - if (bestSourceTask == sourceTask) { - dependencies.add(task); - } - } - } - if (winnerEnd != null && winnerStart != null) { - if (task.getEndTime().isBefore(task.getStartTime())) { - Instant newEndTime = task.getStartTime().plus(12, ChronoUnit.HOURS); - this.setTaskEffort(task, task.getStartTime(), newEndTime); - taskComputationService.updateEndTime(task, newEndTime.minus(1, ChronoUnit.MINUTES)); - this.feedbackMessageService.addFeedbackMessage(new Message("Task dependencies overlap.", MessageLevel.ERROR)); - } - } - } - for (Task task : dependencies) { - this.followMoveDependency(task); - } + public void deleteDependencyLink(DependencyRelatedObject source, DependencyRelatedObject target) { + target.getDependencies().removeIf(dep -> dep.getSource().equals(source)); + taskUpdateService.updateWithImpacts(target, new DependencyUpdateStep(target)); } - private boolean isMilestone(Task task) { - return task.getStartTime().equals(task.getEndTime()); - } + public void createDependencyLink(DependencyRelatedObject source, DependencyRelatedObject target, org.eclipse.sirius.components.gantt.StartOrEnd sourceStartOrEnd, + org.eclipse.sirius.components.gantt.StartOrEnd targetStartOrEnd) { - private int startAdjustmentMinutes(Task sourceTask) { - if (this.isMilestone(sourceTask)) { - return 0; + if (target instanceof Task targetTask && targetTask.isComputeStartEndDynamically()) { + this.feedbackMessageService.addFeedbackMessage(new Message("Creating a dependency targeting a dynamically computed task is not possible.", MessageLevel.ERROR)); } else { - return 1; - } - } - - private int endAdjustmentMinutes(Task sourceTask, Task targetTask) { - int adjustment = 0; - if (this.isMilestone(sourceTask)) { - adjustment--; - } - if (this.isMilestone(targetTask)) { - adjustment++; - } - return adjustment; - } - - /** - * Propagates dependency updates through the hierarchy of dynamically computed parent {@link Task}s. - *

- * The parent task hierarchy is traversed recursively and {@link #followMoveDependency(DependencyRelatedObject)} is invoked on each parent task. - * - * @param task - * the task from which dependency updates are propagated - */ - public void followMoveDependenciesParent(Task task) { - if (task.eContainer() instanceof Task parentTask) { - if (parentTask.isComputeStartEndDynamically()) { - this.followMoveDependency(parentTask); - this.followMoveDependenciesParent(parentTask); + StartOrEnd startBoundary = StartOrEnd.START; + if (sourceStartOrEnd.equals(org.eclipse.sirius.components.gantt.StartOrEnd.END)) { + startBoundary = StartOrEnd.END; } - } - } - - /** - * Recalculates and updates the start and end dates of the specified target {@link Task} according to the given {@link DependencyLink}. - *

- * The task effort is preserved during the calculation. Only the start and end instants are shifted to satisfy the dependency constraints. - * - * @param task - * the target {@link Task} whose start and end dates must be updated according to the dependency - * @param dep - * the {@link DependencyLink} defining the relationship between the source and the target tasks, including the dependency type and delay - */ - private void setTaskNewDates(Task task, DependencyLink dep) { - Task bestSourceTask = (Task) dep.getSource(); - Instant sourceStart = getTaskStartTime(bestSourceTask); - Instant sourceEnd = getTaskEndTime(bestSourceTask); - Instant oldTaskStart = task.getStartTime(); - Instant oldTaskEnd = task.getEndTime(); - int delay = dep.getDelay(); - StartOrEnd sourceStartOrEnd = dep.getSourceKind(); - StartOrEnd targetStartOrEnd = dep.getTargetKind(); - if (sourceStartOrEnd == StartOrEnd.END && targetStartOrEnd == StartOrEnd.START) { - Instant newTaskStart = sourceEnd.plus(delay, ChronoUnit.HOURS) - .plus(this.startAdjustmentMinutes(bestSourceTask), ChronoUnit.MINUTES); - taskComputationService.updateStartTime(task, newTaskStart); - } else if (sourceStartOrEnd == StartOrEnd.START && targetStartOrEnd == StartOrEnd.START) { - Instant newTaskStart = sourceStart.plus(delay, ChronoUnit.HOURS); - taskComputationService.updateStartTime(task, newTaskStart); - } else if (sourceStartOrEnd == StartOrEnd.END && targetStartOrEnd == StartOrEnd.END) { - Instant newTaskEnd = sourceEnd.plus(delay, ChronoUnit.HOURS) - .plus(this.endAdjustmentMinutes(bestSourceTask, task), ChronoUnit.MINUTES); - taskComputationService.updateEndTime(task, newTaskEnd); - } else if (sourceStartOrEnd == StartOrEnd.START && targetStartOrEnd == StartOrEnd.END) { - Instant newTaskEnd = sourceStart.plus(delay, ChronoUnit.HOURS).minus(1, ChronoUnit.MINUTES); - if (this.isMilestone(task)) { - newTaskEnd = newTaskEnd.plus(1, ChronoUnit.MINUTES); + StartOrEnd targetBoundary = StartOrEnd.START; + if (targetStartOrEnd.equals(org.eclipse.sirius.components.gantt.StartOrEnd.END)) { + targetBoundary = StartOrEnd.END; } - taskComputationService.updateEndTime(task, newTaskEnd); - } - } - /** - * Given an XXX-END {@link DependencyLink}, set the new end date of a given {@link Task} - */ - private void setTaskNewEndDate(Task task, DependencyLink dep) { - Task bestSourceTask = (Task) dep.getSource(); - Instant sourceStart = getTaskStartTime(bestSourceTask); - Instant sourceEnd = getTaskEndTime(bestSourceTask); - int delay = dep.getDelay(); - StartOrEnd sourceStartOrEnd = dep.getSourceKind(); - Instant newTaskEnd = task.getEndTime(); - if (sourceStartOrEnd == StartOrEnd.END) { - newTaskEnd = sourceEnd.plus(delay, ChronoUnit.HOURS) - .plus(this.endAdjustmentMinutes(bestSourceTask, task), ChronoUnit.MINUTES); - } else if (sourceStartOrEnd == StartOrEnd.START) { - newTaskEnd = sourceStart.plus(delay, ChronoUnit.HOURS).minus(1, ChronoUnit.MINUTES); - if (this.isMilestone(task)) { - newTaskEnd = newTaskEnd.plus(1, ChronoUnit.MINUTES); - } - } - this.setTaskEffort(task, task.getStartTime(), newTaskEnd); - taskComputationService.updateEndTime(task, newTaskEnd); - } + taskUpdateService.updateWithImpacts(target, List.of( + new CreateDependencyUpdateStep(source, target, startBoundary, targetBoundary), + new DependencyUpdateStep(target))); - /** - * Given an XXX-Start {@link DependencyLink}, set the new start date of a given {@link Task} - */ - private void setTaskNewStartDate(Task task, DependencyLink dep) { - Task bestSourceTask = (Task) dep.getSource(); - Instant sourceStart = getTaskStartTime(bestSourceTask); - Instant sourceEnd = getTaskEndTime(bestSourceTask); - int delay = dep.getDelay(); - StartOrEnd sourceStartOrEnd = dep.getSourceKind(); - Instant newTaskStart = task.getStartTime(); - if (sourceStartOrEnd == StartOrEnd.END) { - newTaskStart = sourceEnd.plus(delay, ChronoUnit.HOURS) - .plus(this.startAdjustmentMinutes(bestSourceTask), ChronoUnit.MINUTES); - } else if (sourceStartOrEnd == StartOrEnd.START) { - newTaskStart = sourceStart.plus(delay, ChronoUnit.HOURS); } - this.setTaskEffort(task, task.getStartTime(), newTaskStart); - taskComputationService.updateStartTime(task, newTaskStart); - } - - /** - * Updates a {@link Workpackage} dates based on a dependency relationship. - * - * @param workpackage - * the {@link Workpackage} to update - * @param dependencyLink - * the {@link DependencyLink} defining how the new dates are calculated - */ - private void setWorkpackageNewDates(Workpackage workpackage, DependencyLink dependencyLink) { - Workpackage bestSourceworkpackage = (Workpackage) dependencyLink.getSource(); - LocalDate sourceStart = bestSourceworkpackage.getStartDate(); - LocalDate sourceEnd = bestSourceworkpackage.getEndDate(); - LocalDate oldWorkpackageStart = workpackage.getStartDate(); - LocalDate oldWorkpackageEnd = workpackage.getEndDate(); - long effort = ChronoUnit.DAYS.between(oldWorkpackageStart, oldWorkpackageEnd); - StartOrEnd sourceStartOrEnd = dependencyLink.getSourceKind(); - StartOrEnd targetStartOrEnd = dependencyLink.getTargetKind(); - int delay = dependencyLink.getDelay(); - if (targetStartOrEnd.equals(StartOrEnd.START)) { - delay += 1; - } - if (sourceStartOrEnd.equals(StartOrEnd.START)) { - delay -= 1; - } - if (sourceStartOrEnd == StartOrEnd.END && targetStartOrEnd == StartOrEnd.START) { - LocalDate newWorkpackageStart = sourceEnd.plusDays(delay); - LocalDate newWorkpackageEnd = newWorkpackageStart.plusDays(effort); - workpackageComputationService.updateStartDate(workpackage, newWorkpackageStart); - } else if (sourceStartOrEnd == StartOrEnd.START && targetStartOrEnd == StartOrEnd.START) { - LocalDate newWorkpackageStart = sourceStart.plusDays(delay); - LocalDate newWorkpackageEnd = newWorkpackageStart.plusDays(effort); - workpackageComputationService.updateStartDate(workpackage, newWorkpackageStart); - } else if (sourceStartOrEnd == StartOrEnd.END && targetStartOrEnd == StartOrEnd.END) { - LocalDate newWorkpackageEnd = sourceEnd.plusDays(delay); - LocalDate newWorkpackageStart = newWorkpackageEnd.minusDays(effort); - workpackageComputationService.updateEndDate(workpackage, newWorkpackageEnd); - } else if (sourceStartOrEnd == StartOrEnd.START && targetStartOrEnd == StartOrEnd.END) { - LocalDate newWorkpackageEnd = sourceStart.plusDays(delay); - LocalDate newWorkpackageStart = newWorkpackageEnd.minusDays(effort); - workpackageComputationService.updateEndDate(workpackage, newWorkpackageEnd); - } - } - - /** - * Given an XXX-END {@link DependencyLink}, set the new end date of a given {@link Workpackage} - */ - private void setWorkpackageNewEndDate(Workpackage workpackage, DependencyLink dependencyLink) { - Workpackage bestSourceworkpackage = (Workpackage) dependencyLink.getSource(); - LocalDate sourceStart = bestSourceworkpackage.getStartDate(); - LocalDate sourceEnd = bestSourceworkpackage.getEndDate(); - LocalDate newWorkpackageEnd = workpackage.getEndDate(); - StartOrEnd sourceStartOrEnd = dependencyLink.getSourceKind(); - int delay = dependencyLink.getDelay(); - if (sourceStartOrEnd.equals(StartOrEnd.START)) { - delay -= 1; - } - if (sourceStartOrEnd == StartOrEnd.END) { - newWorkpackageEnd = sourceEnd.plusDays(delay); - - } else if (sourceStartOrEnd == StartOrEnd.START) { - newWorkpackageEnd = sourceStart.plusDays(delay); - } - workpackageComputationService.updateEffort(workpackage, (int) ChronoUnit.DAYS.between(workpackage.getStartDate(), newWorkpackageEnd)); - workpackageComputationService.updateEndDate(workpackage, newWorkpackageEnd); - } - - /** - * Given an XXX-END {@link DependencyLink}, set the new end date of a given {@link Workpackage} - */ - private void setWorkpackageNewStartDate(Workpackage workpackage, DependencyLink dependencyLink) { - Workpackage bestSourceworkpackage = (Workpackage) dependencyLink.getSource(); - LocalDate sourceStart = bestSourceworkpackage.getStartDate(); - LocalDate sourceEnd = bestSourceworkpackage.getEndDate(); - LocalDate newWorkpackageStart = workpackage.getEndDate(); - StartOrEnd sourceStartOrEnd = dependencyLink.getSourceKind(); - int delay = dependencyLink.getDelay() - 1; - if (sourceStartOrEnd == StartOrEnd.END) { - newWorkpackageStart = sourceEnd.plusDays(delay); - - } else if (sourceStartOrEnd == StartOrEnd.START) { - newWorkpackageStart = sourceStart.plusDays(delay); - } - workpackageComputationService.updateEffort(workpackage, (int) ChronoUnit.DAYS.between(newWorkpackageStart, workpackage.getEndDate())); - workpackageComputationService.updateStartDate(workpackage, newWorkpackageStart); - } - - private LocalDate getlaterLocalDate(DependencyLink dep) { - LocalDate laterLocalDate = null; - Workpackage source = (Workpackage) dep.getSource(); - if (dep.getSourceKind() == StartOrEnd.END) { - laterLocalDate = source.getEndDate().plusDays(dep.getDelay()); - } else if (dep.getSourceKind() == StartOrEnd.START) { - laterLocalDate = source.getStartDate().plusDays(dep.getDelay()); - } - return laterLocalDate; - } - - public void editDependencyLinkDelay(DependencyLink depLink, int newDelay) { - depLink.setDelay(newDelay); - this.followMoveDependency(depLink.getSource()); } public void moveTaskIntoTarget(Task sourceTask, EObject target, int indexInTarget) { @@ -849,55 +283,19 @@ public void deleteWorkpackage(EObject context) { } } - @SuppressWarnings({ "checkstyle:NestedIfDepth", "checkstyle:MissingSwitchDefault" }) public void editWorkpackage(EObject eObject, String name, String description, LocalDate startDate, LocalDate endDate, Integer progress, boolean keepEffort) { if (eObject instanceof Workpackage workpackage) { + taskUpdateService.updateWithImpacts(workpackage, new TaskBoundaryUpdateStep(workpackage, startDate, endDate)); + if (name != null) { workpackage.setName(name); } - if (description != null) { - workpackage.setDescription(description); - } - if (endDate != null && startDate != null) { - long differenceEnd = ChronoUnit.DAYS.between(endDate, workpackage.getEndDate()); - long differenceStart = ChronoUnit.DAYS.between(startDate, workpackage.getStartDate()); - List dependencies = workpackage.getDependencies(); - boolean startDateControlledByDependency = - dependencies.stream() - .anyMatch(dep -> dep.getTargetKind() == StartOrEnd.START); - - boolean endDateControlledByDependency = - dependencies.stream() - .anyMatch(dep -> dep.getTargetKind() == StartOrEnd.END); - - if (differenceStart != 0 && differenceEnd != 0) { - if (dependencies.isEmpty()) { - TaskTimeBoundariesConstraint calculationOption = workpackage.getCalculationOption(); - switch (calculationOption) { - case START_EFFORT -> workpackageComputationService.updateStartDate(workpackage, startDate); - case END_EFFORT -> workpackageComputationService.updateEndDate(workpackage, endDate); - case START_END -> { - workpackageComputationService.updateStartDate(workpackage, startDate); - workpackageComputationService.updateEndDate(workpackage, endDate); - } - } - this.followMoveDependency(workpackage); - } - } else { - if (differenceStart != 0 && !startDateControlledByDependency) { - workpackageComputationService.updateStartDate(workpackage, startDate); - this.followMoveDependency(workpackage); - } - - if (differenceEnd != 0 && !endDateControlledByDependency) { - workpackageComputationService.updateEndDate(workpackage, endDate); - this.followMoveDependency(workpackage); - } - } - } if (progress != null) { workpackage.setProgress(progress); } + if (description != null) { + workpackage.setDescription(description); + } } } diff --git a/backend/pepper-starter/src/main/java/pepper/starter/services/representations/details/AbstractTaskPropertiesConfigurer.java b/backend/pepper-starter/src/main/java/pepper/starter/services/representations/details/AbstractTaskPropertiesConfigurer.java index 85b77fe..540f371 100644 --- a/backend/pepper-starter/src/main/java/pepper/starter/services/representations/details/AbstractTaskPropertiesConfigurer.java +++ b/backend/pepper-starter/src/main/java/pepper/starter/services/representations/details/AbstractTaskPropertiesConfigurer.java @@ -14,11 +14,9 @@ package pepper.starter.services.representations.details; import java.time.DateTimeException; -import java.time.Duration; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -39,7 +37,6 @@ import org.eclipse.emf.edit.provider.ItemProviderAdapter; import org.eclipse.sirius.components.collaborative.forms.services.api.IPropertiesDescriptionRegistry; import org.eclipse.sirius.components.collaborative.forms.services.api.IPropertiesDescriptionRegistryConfigurer; -import org.eclipse.sirius.components.core.api.IFeedbackMessageService; import org.eclipse.sirius.components.core.api.IIdentityService; import org.eclipse.sirius.components.core.api.ILabelService; import org.eclipse.sirius.components.forms.DateTimeType; @@ -62,8 +59,10 @@ import org.eclipse.sirius.components.widget.reference.ReferenceWidgetDescription; import org.springframework.stereotype.Service; -import pepper.domain.services.TaskComputationService; -import pepper.domain.services.WorkpackageComputationService; +import pepper.domain.services.update.ComputeDynamicallyChangeUpdateStep; +import pepper.domain.services.update.EffortUpdateStep; +import pepper.domain.services.update.TaskBoundaryUpdateStep; +import pepper.domain.services.update.TaskUpdateService; import pepper.peppermm.AbstractTask; import pepper.peppermm.DependencyLink; import pepper.peppermm.DependencyRelatedObject; @@ -77,7 +76,6 @@ import pepper.peppermm.provider.PepperItemProviderAdapterFactory; import pepper.starter.messages.MessageConstants; import pepper.starter.messages.PepperMessageService; -import pepper.starter.services.representations.PepperMMJavaService; /** * Customizes the properties view for {@link AbstractTask} sub classes. @@ -95,28 +93,23 @@ public class AbstractTaskPropertiesConfigurer implements IPropertiesDescriptionR private final ILabelService labelService; - private final TaskComputationService taskComputationService; - - private final WorkpackageComputationService workpackageComputationService; - private final PepperItemProviderAdapterFactory pepperItemProviderAdapterFactory = new PepperItemProviderAdapterFactory(); private final ItemProviderAdapter abstractTaskAdapter = (ItemProviderAdapter) pepperItemProviderAdapterFactory.createTaskAdapter(); - private final PepperMMJavaService service; - private final PepperMessageService pepperMessageService; + private final TaskUpdateService taskUpdateService; + + @SuppressWarnings("checkstyle:ParameterNumber") public AbstractTaskPropertiesConfigurer(IIdentityService identityService, PropertiesConfigurerService propertiesConfigurerService, IPropertiesWidgetCreationService propertiesWidgetCreationService, - ILabelService labelService, TaskComputationService taskComputationService, WorkpackageComputationService workpackageComputationService, PepperMessageService pepperMessageService) { + ILabelService labelService, PepperMessageService pepperMessageService, TaskUpdateService taskUpdateService) { this.identityService = Objects.requireNonNull(identityService); this.propertiesConfigurerService = Objects.requireNonNull(propertiesConfigurerService); this.propertiesWidgetCreationService = Objects.requireNonNull(propertiesWidgetCreationService); this.labelService = labelService; - this.taskComputationService = Objects.requireNonNull(taskComputationService); - this.workpackageComputationService = Objects.requireNonNull(workpackageComputationService); this.pepperMessageService = pepperMessageService; - this.service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), this.taskComputationService, this.workpackageComputationService); + this.taskUpdateService = taskUpdateService; } @Override @@ -155,17 +148,13 @@ private List getGeneralControlDescription() { var name = this.propertiesWidgetCreationService.createTextField("namedElement.name", abstractTaskAdapter.getString("_UI_NamedElement_name_feature"), task -> Optional.ofNullable(((AbstractTask) task).getName()).orElse(""), - (task, newValue) -> { - ((AbstractTask) task).setName(newValue); - }, + (task, newValue) -> ((AbstractTask) task).setName(newValue), PepperPackage.Literals.NAMED_ELEMENT__NAME); controls.add(name); var description = this.propertiesWidgetCreationService.createExpressionField("namedElement.description", abstractTaskAdapter.getString("_UI_NamedElement_description_feature"), task -> Optional.ofNullable(((AbstractTask) task).getDescription()).orElse(""), - (task, newValue) -> { - ((AbstractTask) task).setDescription(newValue); - }, + (task, newValue) -> ((AbstractTask) task).setDescription(newValue), PepperPackage.Literals.NAMED_ELEMENT__DESCRIPTION); controls.add(description); @@ -305,22 +294,10 @@ private TextfieldDescription getEffortWidget() { .map(String::valueOf) .orElse("0"); BiFunction newValueHandler = (variableManager, newValue) -> { - return variableManager.get(VariableManager.SELF, AbstractTask.class) + return variableManager.get(VariableManager.SELF, DependencyRelatedObject.class) .map(abstractTask -> { - if (newValue == null || newValue.isBlank()) { - taskComputationService.updateEffort(abstractTask, 0); - } else { - try { - int valueInHours = this.roundToNearestHalfDayInHours(newValue); - if (valueInHours >= 0) { - taskComputationService.updateEffort(abstractTask, valueInHours); - service.editTask(abstractTask, abstractTask.getName(), abstractTask.getDescription(), abstractTask.getStartTime(), abstractTask.getEndTime(), abstractTask.getProgress(), - true); - } - } catch (NumberFormatException e) { - // Ignore - } - } + taskUpdateService.updateWithImpacts(abstractTask, new EffortUpdateStep(abstractTask, newValue)); + return (IStatus) new Success(); }) .orElse(new Failure("")); @@ -381,7 +358,7 @@ private ReferenceWidgetDescription getDependenciesWidget() { .map(o -> (DependencyLink) o) .map(link -> { AbstractTask task = (AbstractTask) link.getSource(); - String name = task.getName(); + String name = task != null ? task.getName() : "no source"; String sourceKind = link.getSourceKind().toString(); String targetKind = link.getTargetKind().toString(); int delay = link.getDelay(); @@ -474,8 +451,8 @@ private DateTimeDescription getStartTimeWidget() { abstractTask.setStartTime(null); } else { try { - Instant instant = Instant.parse(newValue); - service.editTask(abstractTask, abstractTask.getName(), abstractTask.getDescription(), instant, abstractTask.getEndTime(), abstractTask.getProgress(), true); + Instant newStartTime = Instant.parse(newValue); + taskUpdateService.updateWithImpacts(abstractTask, new TaskBoundaryUpdateStep((DependencyRelatedObject) abstractTask, newStartTime, abstractTask.getEndTime())); } catch (DateTimeParseException e) { // Ignore } @@ -487,7 +464,7 @@ private DateTimeDescription getStartTimeWidget() { String id = "abstractTask.startTime"; return DateTimeDescription.newDateTimeDescription(id) .isReadOnlyProvider(vm -> vm.get(VariableManager.SELF, AbstractTask.class) - .map(task -> task.getCalculationOption() == TaskTimeBoundariesConstraint.END_EFFORT || this.isDateOptionForced(task) || this.isPointed(task, StartOrEnd.START)) + .map(task -> task.getCalculationOption() == TaskTimeBoundariesConstraint.END_EFFORT || this.isDateOptionForced(task) || this.isPointed(task, StartOrEnd.START) || task.isComputeStartEndDynamically()) .orElse(true)) .idProvider(variableManager -> id) .targetObjectIdProvider(this.propertiesConfigurerService.getSemanticTargetIdProvider()) @@ -522,8 +499,8 @@ private DateTimeDescription getEndTimeWidget() { abstractTask.setEndTime(null); } else { try { - Instant instant = Instant.parse(newValue); - service.editTask(abstractTask, abstractTask.getName(), abstractTask.getDescription(), abstractTask.getStartTime(), instant, abstractTask.getProgress(), true); + Instant newEndTime = Instant.parse(newValue); + taskUpdateService.updateWithImpacts(abstractTask, new TaskBoundaryUpdateStep((DependencyRelatedObject) abstractTask, abstractTask.getStartTime(), newEndTime)); } catch (DateTimeParseException e) { // Ignore } @@ -535,7 +512,7 @@ private DateTimeDescription getEndTimeWidget() { String id = "abstractTask.endTime"; return DateTimeDescription.newDateTimeDescription(id) .isReadOnlyProvider(vm -> vm.get(VariableManager.SELF, AbstractTask.class) - .map(task -> task.getCalculationOption() == TaskTimeBoundariesConstraint.START_EFFORT || this.isDateOptionForced(task) || this.isPointed(task, StartOrEnd.END)) + .map(task -> task.getCalculationOption() == TaskTimeBoundariesConstraint.START_EFFORT || this.isDateOptionForced(task) || this.isPointed(task, StartOrEnd.END) || task.isComputeStartEndDynamically()) .orElse(true)) .idProvider(variableManager -> id) .targetObjectIdProvider(this.propertiesConfigurerService.getSemanticTargetIdProvider()) @@ -591,8 +568,7 @@ private IfDescription createComputeDynamicallyWidget() { var taskOpt = variableManager.get(VariableManager.SELF, AbstractTask.class); if (taskOpt.isPresent()) { Task task = (Task) taskOpt.get(); - task.setComputeStartEndDynamically(newValue != null && newValue); - service.followMoveDependency(task); + taskUpdateService.updateWithImpacts(task, new ComputeDynamicallyChangeUpdateStep(task, newValue)); return new Success(); } else { return new Failure(""); @@ -642,7 +618,7 @@ private Function> getPersonsProvider() { } private Stream getAllResourceContentStream(Resource resource) { - Iterable content = () -> resource.getAllContents(); + Iterable content = resource::getAllContents; return StreamSupport.stream(content.spliterator(), false); } @@ -656,15 +632,5 @@ private Function> getTeamsProvider() { .toList(); } - private int roundToNearestHalfDayInHours(String nbDaysString) { - double doubleValue = Double.parseDouble(nbDaysString.replace(',', '.')); - - Duration inputDuration = Duration.ofHours((int) (doubleValue * 24)); - Duration duration = inputDuration.isNegative() - ? inputDuration.minusHours(6).truncatedTo(ChronoUnit.HALF_DAYS) - : inputDuration.plusMinutes(6).truncatedTo(ChronoUnit.HALF_DAYS); - - return Math.toIntExact(duration.toHours()); - } } diff --git a/backend/pepper-starter/src/main/java/pepper/starter/services/representations/details/DependencyLinkPropertiesConfigurer.java b/backend/pepper-starter/src/main/java/pepper/starter/services/representations/details/DependencyLinkPropertiesConfigurer.java index 062ae63..0b48237 100644 --- a/backend/pepper-starter/src/main/java/pepper/starter/services/representations/details/DependencyLinkPropertiesConfigurer.java +++ b/backend/pepper-starter/src/main/java/pepper/starter/services/representations/details/DependencyLinkPropertiesConfigurer.java @@ -33,7 +33,6 @@ import org.eclipse.emf.edit.provider.ItemProviderAdapter; import org.eclipse.sirius.components.collaborative.forms.services.api.IPropertiesDescriptionRegistry; import org.eclipse.sirius.components.collaborative.forms.services.api.IPropertiesDescriptionRegistryConfigurer; -import org.eclipse.sirius.components.core.api.IFeedbackMessageService; import org.eclipse.sirius.components.core.api.IIdentityService; import org.eclipse.sirius.components.core.api.ILabelService; import org.eclipse.sirius.components.forms.components.SelectComponent; @@ -52,8 +51,8 @@ import org.eclipse.sirius.components.widget.reference.ReferenceWidgetDescription; import org.springframework.stereotype.Service; -import pepper.domain.services.TaskComputationService; -import pepper.domain.services.WorkpackageComputationService; +import pepper.domain.services.update.DependencyUpdateStep; +import pepper.domain.services.update.TaskUpdateService; import pepper.peppermm.AbstractTask; import pepper.peppermm.DependencyLink; import pepper.peppermm.DependencyRelatedObject; @@ -63,7 +62,6 @@ import pepper.peppermm.Task; import pepper.peppermm.Workpackage; import pepper.peppermm.provider.PepperItemProviderAdapterFactory; -import pepper.starter.services.representations.PepperMMJavaService; /** * Customizes the properties view for {@link DependencyLink} sub classes. @@ -87,13 +85,15 @@ public class DependencyLinkPropertiesConfigurer implements IPropertiesDescriptio private final ItemProviderAdapter dependencyLinkAdapter = (ItemProviderAdapter) pepperItemProviderAdapterFactory.createDependencyLinkAdapter(); - private final PepperMMJavaService service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskComputationService(), new WorkpackageComputationService()); + private final TaskUpdateService taskUpdateService; - public DependencyLinkPropertiesConfigurer(IIdentityService identityService, ILabelService labelService, PropertiesConfigurerService propertiesConfigurerService, IPropertiesWidgetCreationService propertiesWidgetCreationService) { + public DependencyLinkPropertiesConfigurer(IIdentityService identityService, ILabelService labelService, PropertiesConfigurerService propertiesConfigurerService, IPropertiesWidgetCreationService propertiesWidgetCreationService, + TaskUpdateService taskUpdateService) { this.identityService = identityService; this.labelService = labelService; this.propertiesConfigurerService = Objects.requireNonNull(propertiesConfigurerService); this.propertiesWidgetCreationService = Objects.requireNonNull(propertiesWidgetCreationService); + this.taskUpdateService = taskUpdateService; } @Override @@ -196,7 +196,7 @@ private RadioDescription getSourceTargetKindWidget(boolean isSource) { StartOrEnd newStartOrEnd = StartOrEnd.get(integer); depLink.setTargetKind(newStartOrEnd); } - service.editDependencyLinkDelay(depLink, depLink.getDelay()); + this.editDependencyLinkDelay(depLink, depLink.getDelay()); return new Success(); } else { return new Failure(""); @@ -321,12 +321,14 @@ private TextfieldDescription getDurationWidget() { dependencyLink.setDelay(0); } else { try { + int value = 0; if (dependencyLink.eContainer() instanceof Workpackage) { - int integer = Integer.parseInt(newValue); - service.editDependencyLinkDelay(dependencyLink, integer); + value = Integer.parseInt(newValue); } else { - int valueInHours = this.roundToNearestHalfDayInHours(newValue); - service.editDependencyLinkDelay(dependencyLink, valueInHours); + value = this.roundToNearestHalfDayInHours(newValue); + } + if (value >= 0) { + this.editDependencyLinkDelay(dependencyLink, value); } } catch (NumberFormatException e) { // Ignore @@ -400,5 +402,12 @@ private Stream getAllContentStream(EObject eObject) { return StreamSupport.stream(content.spliterator(), false); } + private void editDependencyLinkDelay(DependencyLink depLink, int newDelay) { + depLink.setDelay(newDelay); + if (depLink.eContainer() instanceof DependencyRelatedObject target) { + taskUpdateService.updateWithImpacts(target, new DependencyUpdateStep(target)); + } + } + } diff --git a/backend/pepper-starter/src/main/java/pepper/starter/services/representations/details/WorkpackagePropertiesConfigurer.java b/backend/pepper-starter/src/main/java/pepper/starter/services/representations/details/WorkpackagePropertiesConfigurer.java index 2fab299..c9266a8 100644 --- a/backend/pepper-starter/src/main/java/pepper/starter/services/representations/details/WorkpackagePropertiesConfigurer.java +++ b/backend/pepper-starter/src/main/java/pepper/starter/services/representations/details/WorkpackagePropertiesConfigurer.java @@ -37,7 +37,6 @@ import org.eclipse.emf.edit.provider.ItemProviderAdapter; import org.eclipse.sirius.components.collaborative.forms.services.api.IPropertiesDescriptionRegistry; import org.eclipse.sirius.components.collaborative.forms.services.api.IPropertiesDescriptionRegistryConfigurer; -import org.eclipse.sirius.components.core.api.IFeedbackMessageService; import org.eclipse.sirius.components.core.api.IIdentityService; import org.eclipse.sirius.components.core.api.ILabelService; import org.eclipse.sirius.components.forms.DateTimeType; @@ -58,8 +57,9 @@ import org.eclipse.sirius.components.widget.reference.ReferenceWidgetDescription; import org.springframework.stereotype.Service; -import pepper.domain.services.TaskComputationService; -import pepper.domain.services.WorkpackageComputationService; +import pepper.domain.services.update.EffortUpdateStep; +import pepper.domain.services.update.TaskBoundaryUpdateStep; +import pepper.domain.services.update.TaskUpdateService; import pepper.peppermm.DependencyLink; import pepper.peppermm.DependencyRelatedObject; import pepper.peppermm.PepperPackage; @@ -71,7 +71,6 @@ import pepper.peppermm.provider.PepperItemProviderAdapterFactory; import pepper.starter.messages.IPepperMessageService; import pepper.starter.messages.MessageConstants; -import pepper.starter.services.representations.PepperMMJavaService; /** * Customizes the properties view for {@link Workpackage} sub classes. @@ -93,19 +92,17 @@ public class WorkpackagePropertiesConfigurer implements IPropertiesDescriptionRe private final ItemProviderAdapter workpackageAdapter = (ItemProviderAdapter) pepperItemProviderAdapterFactory.createWorkpackageAdapter(); - private final PepperMMJavaService service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskComputationService(), new WorkpackageComputationService()); - - private final WorkpackageComputationService workpackageComputationService; + private final TaskUpdateService taskUpdateService; private final IPepperMessageService pepperMessageService; - public WorkpackagePropertiesConfigurer(IIdentityService identityService, PropertiesConfigurerService propertiesConfigurerService, IPropertiesWidgetCreationService propertiesWidgetCreationService, ILabelService labelService, - WorkpackageComputationService workpackageComputationService, IPepperMessageService pepperMMMessageService) { + public WorkpackagePropertiesConfigurer(IIdentityService identityService, PropertiesConfigurerService propertiesConfigurerService, IPropertiesWidgetCreationService propertiesWidgetCreationService, + ILabelService labelService, TaskUpdateService taskUpdateService, IPepperMessageService pepperMMMessageService) { this.identityService = identityService; this.propertiesConfigurerService = Objects.requireNonNull(propertiesConfigurerService); this.propertiesWidgetCreationService = Objects.requireNonNull(propertiesWidgetCreationService); this.labelService = labelService; - this.workpackageComputationService = workpackageComputationService; + this.taskUpdateService = taskUpdateService; this.pepperMessageService = pepperMMMessageService; } @@ -290,20 +287,7 @@ private TextfieldDescription getEffortWidget() { BiFunction newValueHandler = (variableManager, newValue) -> { return variableManager.get(VariableManager.SELF, Workpackage.class) .map(workpackage -> { - if (newValue == null || newValue.isBlank()) { - workpackageComputationService.updateEffort(workpackage, 0); - } else { - try { - int integer = Integer.parseInt(newValue); - if (integer >= 0) { - workpackageComputationService.updateEffort(workpackage, integer); - service.editWorkpackage(workpackage, workpackage.getName(), workpackage.getDescription(), workpackage.getStartDate(), workpackage.getEndDate(), - workpackage.getProgress(), true); - } - } catch (NumberFormatException e) { - // Ignore - } - } + taskUpdateService.updateWithImpacts(workpackage, new EffortUpdateStep(workpackage, newValue)); return (IStatus) new Success(); }) .orElse(new Failure("")); @@ -366,7 +350,7 @@ private ReferenceWidgetDescription getDependenciesWidget() { .map(o -> (DependencyLink) o) .map(link -> { Workpackage workpackage = (Workpackage) link.getSource(); - String name = workpackage.getName(); + String name = workpackage != null ? workpackage.getName() : "no source"; String sourceKind = link.getSourceKind().toString(); String targetKind = link.getTargetKind().toString(); int duration = link.getDelay(); @@ -427,8 +411,8 @@ private DateTimeDescription getStartDateWidget() { workpackage.setStartDate(null); } else { try { - LocalDate localDate = LocalDate.parse(newValue); - service.editWorkpackage(workpackage, workpackage.getName(), workpackage.getDescription(), localDate, workpackage.getEndDate(), workpackage.getProgress(), true); + LocalDate newStartlDate = LocalDate.parse(newValue); + taskUpdateService.updateWithImpacts(workpackage, new TaskBoundaryUpdateStep(workpackage, newStartlDate, workpackage.getEndDate())); } catch (DateTimeParseException e) { // Ignore } @@ -478,8 +462,8 @@ private DateTimeDescription getEndDateWidget() { workpackage.setEndDate(null); } else { try { - LocalDate localDate = LocalDate.parse(newValue); - service.editWorkpackage(workpackage, workpackage.getName(), workpackage.getDescription(), workpackage.getStartDate(), localDate, workpackage.getProgress(), true); + LocalDate newEndDate = LocalDate.parse(newValue); + taskUpdateService.updateWithImpacts(workpackage, new TaskBoundaryUpdateStep(workpackage, workpackage.getStartDate(), newEndDate)); } catch (DateTimeParseException e) { // Ignore } diff --git a/backend/pepper-starter/src/main/java/pepper/starter/services/representations/gantt/ViewGanttDescriptionBuilder.java b/backend/pepper-starter/src/main/java/pepper/starter/services/representations/gantt/ViewGanttDescriptionBuilder.java index 6e0b445..0896cab 100644 --- a/backend/pepper-starter/src/main/java/pepper/starter/services/representations/gantt/ViewGanttDescriptionBuilder.java +++ b/backend/pepper-starter/src/main/java/pepper/starter/services/representations/gantt/ViewGanttDescriptionBuilder.java @@ -136,7 +136,7 @@ private DeleteTaskDependencyTool createDeleteTaskDependencyTool() { return new GanttBuilders().newDeleteTaskDependencyTool() .name("Delete Task Dependency") .body(new ChangeContextBuilder() - .expression("aql:targetObject.deleteDependencyLink(sourceObject)") + .expression("aql:sourceObject.deleteDependencyLink(targetObject)") .build()) .build(); } diff --git a/backend/pepper-starter/src/test/java/pepper/starter/configuration/PepperMMSampleBuilderTests.java b/backend/pepper-starter/src/test/java/pepper/starter/configuration/PepperMMSampleBuilderTests.java index 49f4d51..82557e1 100644 --- a/backend/pepper-starter/src/test/java/pepper/starter/configuration/PepperMMSampleBuilderTests.java +++ b/backend/pepper-starter/src/test/java/pepper/starter/configuration/PepperMMSampleBuilderTests.java @@ -16,8 +16,6 @@ import org.junit.jupiter.api.Test; -import pepper.domain.services.TaskComputationService; -import pepper.domain.services.WorkpackageComputationService; import pepper.peppermm.Organization; import pepper.peppermm.PepperPackage; import pepper.starter.services.project.PepperMMSampleBuilder; @@ -31,11 +29,11 @@ public class PepperMMSampleBuilderTests { @Test public void testExampleContent() { - var content = new PepperMMSampleBuilder(new TaskComputationService(), new WorkpackageComputationService()).getSampleContent(); + var content = new PepperMMSampleBuilder().getSampleContent(); assertThat(content).isNotNull(); assertThat(content.eClass()).isEqualTo(PepperPackage.eINSTANCE.getOrganization()); - Organization organization = (Organization) content; + Organization organization = content; assertThat(organization.getOwnedProjects()).hasSize(4); assertThat(organization.getOwnedProjects()).anySatisfy(project -> assertThat(project.getName()).isEqualTo("Project Dev")); assertThat(organization.getOwnedProjects()).anySatisfy(project -> assertThat(project.getName()).isEqualTo("Daily Project Dev")); diff --git a/backend/pepper-starter/src/test/java/pepper/starter/configuration/view/EditDependenciesServiceTests.java b/backend/pepper-starter/src/test/java/pepper/starter/configuration/view/EditDependenciesServiceTests.java index b915011..3753edd 100644 --- a/backend/pepper-starter/src/test/java/pepper/starter/configuration/view/EditDependenciesServiceTests.java +++ b/backend/pepper-starter/src/test/java/pepper/starter/configuration/view/EditDependenciesServiceTests.java @@ -36,6 +36,7 @@ import pepper.domain.services.TaskComputationService; import pepper.domain.services.WorkpackageComputationService; +import pepper.domain.services.update.TaskUpdateService; import pepper.peppermm.DependencyLink; import pepper.peppermm.PepperFactory; import pepper.peppermm.StartOrEnd; @@ -63,7 +64,7 @@ public class EditDependenciesServiceTests { private final Workpackage workpackage = PepperFactory.eINSTANCE.createWorkpackage(); private final TaskComputationService taskComputationService = new TaskComputationService(); private final WorkpackageComputationService workpackageComputationService = new WorkpackageComputationService(); - private final PepperMMJavaService service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), taskComputationService, workpackageComputationService); + private final PepperMMJavaService service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskUpdateService(new IFeedbackMessageService.NoOp())); private final SimpleCrossReferenceProvider simpleCrossReferenceProvider = new SimpleCrossReferenceProvider(); @BeforeEach diff --git a/backend/pepper-starter/src/test/java/pepper/starter/configuration/view/PepperMMJavaServiceTests.java b/backend/pepper-starter/src/test/java/pepper/starter/configuration/view/PepperMMJavaServiceTests.java index 06b19a4..39024cf 100644 --- a/backend/pepper-starter/src/test/java/pepper/starter/configuration/view/PepperMMJavaServiceTests.java +++ b/backend/pepper-starter/src/test/java/pepper/starter/configuration/view/PepperMMJavaServiceTests.java @@ -33,6 +33,7 @@ import pepper.domain.services.TaskComputationService; import pepper.domain.services.WorkpackageComputationService; +import pepper.domain.services.update.TaskUpdateService; import pepper.peppermm.AbstractTask; import pepper.peppermm.DependencyLink; import pepper.peppermm.PepperFactory; @@ -95,7 +96,7 @@ public void editTask() { taskComputationService.updateStartTime(task, Instant.now()); taskComputationService.updateEndTime(task, Instant.now()); workpackage.getOwnedTasks().add(task); - var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskComputationService(), new WorkpackageComputationService()); + var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskUpdateService(new IFeedbackMessageService.NoOp())); service.editTask(task, NEW_NAME, NEW_DESCRIPTION, Instant.parse(MONDAY_2026_01_05_T00_00_00), Instant.parse(MONDAY_2026_01_05_T23_59_00), 10, false); assertThat(task.getName()).isEqualTo(NEW_NAME); assertThat(task.getDescription()).isEqualTo(NEW_DESCRIPTION); @@ -138,7 +139,7 @@ public void editTaskWithDependency() { dependencyLinkOfTaskDependency.setSource(task1); task2.getDependencies().add(dependencyLinkOfTaskDependency); - var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskComputationService(), new WorkpackageComputationService()); + var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskUpdateService(new IFeedbackMessageService.NoOp())); service.editTask(task2, null, null, Instant.parse(TUESDAY_2026_01_06_T00_00_00), Instant.parse(TUESDAY_2026_01_06_T00_00_00).plus(1, ChronoUnit.DAYS), null, false); assertThat(task2.getStartTime()).isEqualTo(Instant.parse(MONDAY_2026_01_05_T00_00_00)); assertThat(task2.getEndTime()).isEqualTo(Instant.parse(MONDAY_2026_01_05_T23_59_00)); @@ -181,7 +182,7 @@ public void editSubTaskOfDynamicTaskWithDependency() { task3.getSubTasks().add(task31); task3.setComputeStartEndDynamically(true); - var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskComputationService(), new WorkpackageComputationService()); + var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskUpdateService(new IFeedbackMessageService.NoOp())); DependencyLink dependencyLinkFromTask3ToTask1 = PepperFactory.eINSTANCE.createDependencyLink(); dependencyLinkFromTask3ToTask1.setDelay(0); @@ -201,43 +202,46 @@ public void editSubTaskOfDynamicTaskWithDependency() { @Test public void createDependencyLink() { - Task task = PepperFactory.eINSTANCE.createTask(); - task.setCalculationOption(TaskTimeBoundariesConstraint.START_EFFORT); - taskComputationService.updateStartTime(task, Instant.parse(MONDAY_2026_01_05_T00_00_00)); - taskComputationService.updateEffort(task, 24); + Task task1 = PepperFactory.eINSTANCE.createTask(); + task1.setName("task1"); + task1.setCalculationOption(TaskTimeBoundariesConstraint.START_EFFORT); + taskComputationService.updateStartTime(task1, Instant.parse(MONDAY_2026_01_05_T00_00_00)); + taskComputationService.updateEffort(task1, 24); - Task taskDependency = PepperFactory.eINSTANCE.createTask(); - taskComputationService.updateStartTime(taskDependency, Instant.parse(MONDAY_2026_01_05_T00_00_00)); - taskComputationService.updateEndTime(taskDependency, Instant.parse(MONDAY_2026_01_05_T23_59_00)); + Task task2 = PepperFactory.eINSTANCE.createTask(); + task2.setName("task2"); + taskComputationService.updateStartTime(task2, Instant.parse(MONDAY_2026_01_05_T00_00_00)); + taskComputationService.updateEndTime(task2, Instant.parse(MONDAY_2026_01_05_T23_59_00)); - Task masterTask = PepperFactory.eINSTANCE.createTask(); - taskComputationService.updateStartTime(masterTask, Instant.parse(MONDAY_2026_01_05_T00_00_00)); - taskComputationService.updateEndTime(masterTask, Instant.parse(MONDAY_2026_01_05_T23_59_00)); + Task task3 = PepperFactory.eINSTANCE.createTask(); + task3.setName("task3"); + taskComputationService.updateStartTime(task3, Instant.parse(MONDAY_2026_01_05_T00_00_00)); + taskComputationService.updateEndTime(task3, Instant.parse(MONDAY_2026_01_05_T23_59_00)); - workpackage.getOwnedTasks().add(task); - workpackage.getOwnedTasks().add(taskDependency); - workpackage.getOwnedTasks().add(masterTask); - - var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskComputationService(), new WorkpackageComputationService()); - service.createDependencyLink(taskDependency, task, StartOrEnd.END, StartOrEnd.START); - assertThat(task.getDependencies().size()).isEqualTo(1); - assertThat(task.getDependencies().get(0).getSource()).isEqualTo(taskDependency); - assertThat(task.getStartTime()).isEqualTo(Instant.parse(TUESDAY_2026_01_06_T00_00_00)); - assertThat(task.getEndTime()).isEqualTo(Instant.parse(TUESDAY_2026_01_06_T23_59_00)); - - service.createDependencyLink(masterTask, taskDependency, StartOrEnd.END, StartOrEnd.END); - assertThat(taskDependency.getDependencies().size()).isEqualTo(1); - assertThat(taskDependency.getDependencies().get(0).getSource()).isEqualTo(masterTask); - assertThat(taskDependency.getStartTime()).isEqualTo(masterTask.getStartTime()); - assertThat(taskDependency.getEndTime()).isEqualTo(masterTask.getEndTime()); + workpackage.getOwnedTasks().add(task1); + workpackage.getOwnedTasks().add(task2); + workpackage.getOwnedTasks().add(task3); + + var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskUpdateService(new IFeedbackMessageService.NoOp())); + service.createDependencyLink(task2, task1, StartOrEnd.END, StartOrEnd.START); + assertThat(task1.getDependencies().size()).isEqualTo(1); + assertThat(task1.getDependencies().get(0).getSource()).isEqualTo(task2); + assertThat(task1.getStartTime()).isEqualTo(Instant.parse(TUESDAY_2026_01_06_T00_00_00)); + assertThat(task1.getEndTime()).isEqualTo(Instant.parse(TUESDAY_2026_01_06_T23_59_00)); + + service.createDependencyLink(task3, task2, StartOrEnd.END, StartOrEnd.END); + assertThat(task2.getDependencies().size()).isEqualTo(1); + assertThat(task2.getDependencies().get(0).getSource()).isEqualTo(task3); + assertThat(task2.getStartTime()).isEqualTo(task3.getStartTime()); + assertThat(task2.getEndTime()).isEqualTo(task3.getEndTime()); // Verify transitive dependency propagation - assertThat(task.getStartTime()).isEqualTo(taskDependency.getEndTime().plus(1, ChronoUnit.MINUTES)); - assertThat(task.getEndTime()).isEqualTo(Instant.parse(TUESDAY_2026_01_06_T23_59_00)); + assertThat(task1.getStartTime()).isEqualTo(task2.getEndTime().plus(1, ChronoUnit.MINUTES)); + assertThat(task1.getEndTime()).isEqualTo(Instant.parse(TUESDAY_2026_01_06_T23_59_00)); // Verify that cyclic dependencies are impossible - assertThat(masterTask.getDependencies()).isEmpty(); - service.createDependencyLink(task, masterTask, StartOrEnd.END, StartOrEnd.START); - assertThat(masterTask.getDependencies()).isEmpty(); + assertThat(task3.getDependencies()).isEmpty(); + service.createDependencyLink(task1, task3, StartOrEnd.END, StartOrEnd.START); + assertThat(task3.getDependencies()).isEmpty(); } @@ -260,12 +264,12 @@ public void deleteDependencyLink() { workpackage.getOwnedTasks().add(task2); workpackage.getOwnedTasks().add(task3); - DependencyLink dependencyLinkFromTask1ToTask2 = PepperFactory.eINSTANCE.createDependencyLink(); - dependencyLinkFromTask1ToTask2.setDelay(0); - dependencyLinkFromTask1ToTask2.setTargetKind(pepper.peppermm.StartOrEnd.START); - dependencyLinkFromTask1ToTask2.setSourceKind(pepper.peppermm.StartOrEnd.END); - dependencyLinkFromTask1ToTask2.setSource(task2); - task1.getDependencies().add(dependencyLinkFromTask1ToTask2); + DependencyLink dependencyLinkFromTask2ToTask1 = PepperFactory.eINSTANCE.createDependencyLink(); + dependencyLinkFromTask2ToTask1.setDelay(0); + dependencyLinkFromTask2ToTask1.setTargetKind(pepper.peppermm.StartOrEnd.START); + dependencyLinkFromTask2ToTask1.setSourceKind(pepper.peppermm.StartOrEnd.END); + dependencyLinkFromTask2ToTask1.setSource(task2); + task1.getDependencies().add(dependencyLinkFromTask2ToTask1); DependencyLink dependencyLinkFromTask3ToTask1 = PepperFactory.eINSTANCE.createDependencyLink(); dependencyLinkFromTask3ToTask1.setDelay(0); @@ -275,8 +279,8 @@ public void deleteDependencyLink() { task1.getDependencies().add(dependencyLinkFromTask3ToTask1); assertThat(task1.getDependencies().size()).isEqualTo(2); - var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskComputationService(), new WorkpackageComputationService()); - service.deleteDependencyLink(task1, task2); + var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskUpdateService(new IFeedbackMessageService.NoOp())); + service.deleteDependencyLink(task2, task1); assertThat(task1.getDependencies().size()).isEqualTo(1); assertThat(task1.getStartTime()).isEqualTo(task3.getEndTime().plus(1, ChronoUnit.MINUTES)); } @@ -330,7 +334,7 @@ public void createTask() { task1.getSubTasks().add(task11); workpackage.getOwnedTasks().add(task1); - var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskComputationService(), new WorkpackageComputationService()); + var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskUpdateService(new IFeedbackMessageService.NoOp())); service.createTask(workpackage); assertThat(workpackage.getOwnedTasks()).hasSize(2); @@ -368,7 +372,7 @@ public void deleteTask() { workpackage.getOwnedTasks().add(task1); workpackage.getOwnedTasks().add(task2); workpackage.getOwnedTasks().add(task3); - var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskComputationService(), new WorkpackageComputationService()); + var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskUpdateService(new IFeedbackMessageService.NoOp())); service.createDependencyLink(task3, task2, StartOrEnd.END, StartOrEnd.START); service.createDependencyLink(task11, task2, StartOrEnd.END, StartOrEnd.START); @@ -387,7 +391,7 @@ public void createWorkpackage() { workpackageComputationService.updateEndDate(projectWorkpackage, WEDNESDAY_20260107); project.getOwnedWorkpackages().add(projectWorkpackage); - var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskComputationService(), new WorkpackageComputationService()); + var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskUpdateService(new IFeedbackMessageService.NoOp())); service.createWorkpackage(projectWorkpackage); assertThat(project.getOwnedWorkpackages()).hasSize(2); assertThat(project.getOwnedWorkpackages().get(1).getStartDate()).isEqualTo(WEDNESDAY_20260107); @@ -399,7 +403,7 @@ public void editWorkpackage() { workpackageComputationService.updateStartDate(workpackage, MONDAY_20260105); workpackageComputationService.updateEndDate(workpackage, TUESDAY_20260106); - var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskComputationService(), new WorkpackageComputationService()); + var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskUpdateService(new IFeedbackMessageService.NoOp())); service.editWorkpackage(workpackage, NEW_NAME, NEW_DESCRIPTION, WEDNESDAY_20260107, FRIDAY_20260109, 10, false); assertThat(workpackage.getName()).isEqualTo(NEW_NAME); assertThat(workpackage.getDescription()).isEqualTo(NEW_DESCRIPTION); @@ -417,7 +421,7 @@ public void deleteWorkpackage() { project.getOwnedWorkpackages().add(projectWorkpackage); assertThat(project.getOwnedWorkpackages()).hasSize(1); - var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskComputationService(), new WorkpackageComputationService()); + var service = new PepperMMJavaService(new IFeedbackMessageService.NoOp(), new TaskUpdateService(new IFeedbackMessageService.NoOp())); service.deleteWorkpackage(projectWorkpackage); assertThat(project.getOwnedWorkpackages()).hasSize(0); } From 819801b9dc831d735cca37a9be722f8dc05fe5e7 Mon Sep 17 00:00:00 2001 From: Laurent Fasani Date: Thu, 10 Sep 2026 10:57:48 +0200 Subject: [PATCH 2/3] [60] Update tasks impacted by Person changes A update of all impacted tasks is done: - When a person is assigned or removed from a Person - When a person is deleted - When a person has its availability periods changed. Issue: https://github.com/ObeoNetwork/pepper/issues/60 --- .../pepper/domain/services/TaskHelper.java | 8 + .../services/update/SimpleUpdateStep.java | 87 +++++++++++ .../services/update/TaskUpdateAdapter.java | 57 +++++++ .../services/update/TaskUpdateService.java | 139 +++++++++++++++++- .../services/update/TaskUpdateStep.java | 1 + .../TaskUpdateServiceOrderingTests.java | 88 +++++++++++ .../TaskAutomaticUpdateAdapterInstaller.java | 48 ++++++ 7 files changed, 425 insertions(+), 3 deletions(-) create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/update/SimpleUpdateStep.java create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateAdapter.java create mode 100644 backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateServiceOrderingTests.java create mode 100644 backend/pepper-starter/src/main/java/pepper/starter/services/project/TaskAutomaticUpdateAdapterInstaller.java diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskHelper.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskHelper.java index b890a01..e9e1758 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskHelper.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskHelper.java @@ -15,6 +15,9 @@ import java.time.temporal.Temporal; import java.util.List; import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.StreamSupport; import org.eclipse.emf.ecore.EObject; @@ -74,6 +77,11 @@ public Optional getParent(EObject eObject, Class clazz) { return objectOpt; } + public boolean isParent(EObject parent, EObject child) { + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(parent.eAllContents(), Spliterator.ORDERED), false) + .anyMatch(eObject -> eObject.equals(child)); + } + public TaskTimeBoundariesConstraint getCalculationOption(DependencyRelatedObject task) { TaskTimeBoundariesConstraint calculationOption = null; if (task instanceof AbstractTask abstractTask) { diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/SimpleUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/SimpleUpdateStep.java new file mode 100644 index 0000000..fa4e705 --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/SimpleUpdateStep.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +package pepper.domain.services.update; + +import java.time.Instant; +import java.time.LocalDate; + +import pepper.domain.services.TaskComputationService; +import pepper.domain.services.TaskHelper; +import pepper.domain.services.WorkpackageComputationService; +import pepper.peppermm.AbstractTask; +import pepper.peppermm.DependencyRelatedObject; +import pepper.peppermm.TaskTimeBoundariesConstraint; +import pepper.peppermm.Workpackage; + +/** + * This class represents an update step for a task that may be changed due to Person assignment change or Person change. + * The step preserves the effort + * @author lfasani + */ +public final class SimpleUpdateStep extends TaskUpdateStep { + private static final TaskHelper TASK_HELPER = new TaskHelper(); + private static final TaskComputationService TASK_COMPUTATION_SERVICE = new TaskComputationService(); + private static final WorkpackageComputationService WORKPACKAGE_COMPUTATION_SERVICE = new WorkpackageComputationService(); + + private final DependencyRelatedObject task; + + public SimpleUpdateStep(DependencyRelatedObject task) { + this.task = task; + } + + @Override + public Object getImpactedTask() { + return task; + } + + @Override + public String getName() { + return TASK_HELPER.getName(task); + } + + @SuppressWarnings("checkstyle:MissingSwitchDefault") + @Override + public void update() { + + if (task instanceof AbstractTask abstractTask) { + Instant roundedStartTime = TASK_COMPUTATION_SERVICE.roundToNearestHalfDay(abstractTask.getStartTime()); + Instant roundedEndTime = TASK_COMPUTATION_SERVICE.roundToNearestHalfDay(abstractTask.getEndTime()); + + if (roundedStartTime != null && roundedEndTime != null) { + TaskTimeBoundariesConstraint calculationOption = abstractTask.getCalculationOption(); + switch (calculationOption) { + case START_EFFORT -> TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, roundedStartTime); + case END_EFFORT -> TASK_COMPUTATION_SERVICE.updateEndTime(abstractTask, roundedEndTime); + case START_END -> { + abstractTask.setCalculationOption(TaskTimeBoundariesConstraint.START_EFFORT); + TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, roundedStartTime); + abstractTask.setCalculationOption(calculationOption); + } + } + } + } else if (task instanceof Workpackage workpackage) { + LocalDate startDate = workpackage.getStartDate(); + LocalDate endDate = workpackage.getEndDate(); + + TaskTimeBoundariesConstraint calculationOption = workpackage.getCalculationOption(); + switch (calculationOption) { + case START_EFFORT -> WORKPACKAGE_COMPUTATION_SERVICE.updateStartDate(workpackage, startDate); + case END_EFFORT -> WORKPACKAGE_COMPUTATION_SERVICE.updateEndDate(workpackage, endDate); + case START_END -> { + WORKPACKAGE_COMPUTATION_SERVICE.updateStartDate(workpackage, startDate); + } + } + } + } +} diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateAdapter.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateAdapter.java new file mode 100644 index 0000000..95191db --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateAdapter.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +package pepper.domain.services.update; + +import org.eclipse.emf.common.notify.Notification; +import org.eclipse.emf.ecore.util.EContentAdapter; + +import pepper.peppermm.DependencyRelatedObject; +import pepper.peppermm.PepperPackage; +import pepper.peppermm.Person; +import pepper.peppermm.UnavailabilityPeriod; + +/** + * This adapter is used to update tasks. + * @author lfasani + */ +public class TaskUpdateAdapter extends EContentAdapter { + private final TaskUpdateService taskUpdateService; + + public TaskUpdateAdapter(TaskUpdateService taskUpdateService) { + this.taskUpdateService = taskUpdateService; + } + + @Override + public void notifyChanged(Notification notification) { + super.notifyChanged(notification); + if (!notification.isTouch()) { + this.handleNotification(notification); + } + } + + private void handleNotification(Notification notification) { + Object notifier = notification.getNotifier(); + Object feature = notification.getFeature(); + if (notifier instanceof DependencyRelatedObject task && feature.equals(PepperPackage.eINSTANCE.getAssignableObject_AssignedPersons())) { + taskUpdateService.updateTasksFollowingPersonChange(task); + } else if (notifier instanceof Person person && feature.equals(PepperPackage.eINSTANCE.getResource_UnavailabilityPeriods())) { + taskUpdateService.updateTasksFollowingPersonChange(person); + } else if (notifier instanceof UnavailabilityPeriod unavailabilityPeriod + && (feature.equals(PepperPackage.eINSTANCE.getUnavailabilityPeriod_StartDate()) || feature.equals(PepperPackage.eINSTANCE.getUnavailabilityPeriod_EndDate()))) { + if (unavailabilityPeriod.eContainer() instanceof Person person) { + taskUpdateService.updateTasksFollowingPersonChange(person); + } + } + } +} diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateService.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateService.java index 6d2d27d..a025fbd 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateService.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateService.java @@ -13,12 +13,22 @@ package pepper.domain.services.update; +import java.time.temporal.Temporal; import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EStructuralFeature; +import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.sirius.components.core.api.IFeedbackMessageService; import org.eclipse.sirius.components.interpreter.SimpleCrossReferenceProvider; import org.eclipse.sirius.components.representations.Message; @@ -31,6 +41,10 @@ import pepper.peppermm.AbstractTask; import pepper.peppermm.DependencyLink; import pepper.peppermm.DependencyRelatedObject; +import pepper.peppermm.NamedElement; +import pepper.peppermm.Person; +import pepper.peppermm.Project; +import pepper.peppermm.Workpackage; /** * Service to manage the update of a task and the impacted tasks. @@ -69,14 +83,14 @@ public void updateWithImpacts(EObject task, List taskUpdateSteps } } - public void doUpdate(List tasksToUpdate) { + private void doUpdate(Collection tasksToUpdate) { tasksToUpdate.forEach(TaskUpdateStep::update); } /** * Aggregates the tasksToUpdate with tasks that are dependencies of currentTask. */ - public void computeTaskToUpdate(EObject currentTask, List tasksToUpdate, List currentBranchOfTasksToUpdate) throws IllegalStateException { + private void computeTaskToUpdate(EObject currentTask, List tasksToUpdate, List currentBranchOfTasksToUpdate) throws IllegalStateException { for (var inverseReference : simpleCrossReferenceProvider.getInverseReferences(currentTask)) { if (inverseReference.getEObject() instanceof DependencyLink dependencyLink) { if (dependencyLink.eContainer() instanceof DependencyRelatedObject targetTask && currentTask instanceof DependencyRelatedObject sourceTask) { @@ -112,10 +126,129 @@ private void computeTaskToUpdate(List tasksToUpdate, List getAllTasksOfGantt(DependencyRelatedObject dependencyRelatedObject) { + if (dependencyRelatedObject instanceof AbstractTask abstractTask) { + return taskHelper.getParent(abstractTask, Workpackage.class) + .map(workpackage -> { + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(workpackage.eAllContents(), Spliterator.ORDERED), false) + .filter(AbstractTask.class::isInstance) + .map(DependencyRelatedObject.class::cast) + .toList(); + }) + .orElse(List.of()); + } else if (dependencyRelatedObject instanceof Workpackage workpackage) { + return taskHelper.getParent(workpackage, Project.class) + .map(project -> { + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(project.eAllContents(), Spliterator.ORDERED), false) + .filter(Workpackage.class::isInstance) + .map(DependencyRelatedObject.class::cast) + .toList(); + }) + .orElse(List.of()); + } + return List.of(); + } + + public void updateTasksFollowingPersonChange(DependencyRelatedObject task) { + this.updateTasksAfterGivenTemporal(task, taskHelper.getStartTemporal(task)); + } + + public void updateTasksFollowingPersonChange(Person updatePerson) { + List tasksToUpdate = simpleCrossReferenceProvider.getInverseReferences(updatePerson).stream() + .map(EStructuralFeature.Setting::getEObject) + .filter(DependencyRelatedObject.class::isInstance) + .map(DependencyRelatedObject.class::cast) + .toList(); + + Comparator temporalComparator = Comparator.comparing(temporal -> (Comparable) temporal); + Optional minTemporal = tasksToUpdate.stream() + .map(taskHelper::getStartTemporal) + .filter(Objects::nonNull) + .min(temporalComparator); + + if (minTemporal.isEmpty()) { + return; + } + + this.updateTasksAfterGivenTemporal(tasksToUpdate.get(0), minTemporal.get()); + } + + private void updateTasksAfterGivenTemporal(DependencyRelatedObject task, Temporal minTemporal) { + Comparator temporalComparator = Comparator.comparing(temporal -> (Comparable) temporal); + Collection taskUpdateSteps = this.getAllTasksOfGantt(task).stream() + .filter(dependencyRelatedObject -> { + Temporal startTemporal = taskHelper.getStartTemporal(dependencyRelatedObject); + return startTemporal != null && temporalComparator.compare(startTemporal, minTemporal) >= 0; + }) + .flatMap(dependencyRelatedObject -> { + List updateSteps = new ArrayList<>(List.of(new SimpleUpdateStep(dependencyRelatedObject))); + this.computeTaskToUpdate(dependencyRelatedObject, updateSteps, List.copyOf(updateSteps)); + return updateSteps.stream(); + }) + .sorted(Comparator.comparing((TaskUpdateStep taskUpdateStep) -> taskHelper.getStartTemporal((DependencyRelatedObject) taskUpdateStep.getImpactedTask()), temporalComparator)) + .toList(); + LinkedHashSet orderedTaskUpdateSteps = this.filterAndOrderTaskUpdateSteps(taskUpdateSteps); + orderedTaskUpdateSteps.forEach(taskUpdateStep -> { + System.out.println(((NamedElement) taskUpdateStep.getImpactedTask()).getName() + " " + taskUpdateStep.getClass().getSimpleName()); + }); + this.doUpdate(orderedTaskUpdateSteps); + } + + private LinkedHashSet filterAndOrderTaskUpdateSteps(Collection taskUpdateSteps) { + // Part 1: eliminate identical steps (same type and same impacted task). + List distinctSteps = new ArrayList<>(); + for (TaskUpdateStep step : taskUpdateSteps) { + boolean alreadyPresent = distinctSteps.stream().anyMatch(otherStep -> otherStep.getClass() == step.getClass() + && otherStep.getImpactedTask() == step.getImpactedTask()); + if (!alreadyPresent) { + distinctSteps.add(step); + } + } + + // Part 2: reduce to the preferred step per task, preserving first encounter order. + List remainingSteps = new ArrayList<>(); + for (TaskUpdateStep step : distinctSteps) { + boolean taskAlreadyPresent = remainingSteps.stream().anyMatch(otherStep -> otherStep.getImpactedTask() == step.getImpactedTask()); + if (!taskAlreadyPresent) { + TaskUpdateStep preferredStep = distinctSteps.stream() + .filter(otherStep -> otherStep.getImpactedTask() == step.getImpactedTask()) + .reduce(step, this::getPreferredTaskUpdateStep); + remainingSteps.add(preferredStep); + } + } + + // Part 3: order descendants before their ParentUpdateStep. + LinkedHashSet orderedSteps = new LinkedHashSet<>(); + while (!remainingSteps.isEmpty()) { + // Take the first available step, deferring parents until all descendants have been placed. + TaskUpdateStep nextStep = remainingSteps.stream() + .filter(step -> !(step instanceof ParentUpdateStep) || remainingSteps.stream().noneMatch(otherStep -> + otherStep.getImpactedTask() != step.getImpactedTask() + && EcoreUtil.isAncestor((EObject) step.getImpactedTask(), (EObject) otherStep.getImpactedTask()))) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Cannot order task updates: cyclic task containment.")); + orderedSteps.add(nextStep); + remainingSteps.remove(nextStep); + } + return orderedSteps; + } + + @SuppressWarnings("checkstyle:ReturnCount") + private TaskUpdateStep getPreferredTaskUpdateStep(TaskUpdateStep currentTaskUpdateStep, TaskUpdateStep candidateTaskUpdateStep) { + if (currentTaskUpdateStep instanceof DependencyUpdateStep) { + return currentTaskUpdateStep; + } + if (candidateTaskUpdateStep instanceof DependencyUpdateStep + || candidateTaskUpdateStep instanceof ParentUpdateStep && currentTaskUpdateStep instanceof SimpleUpdateStep) { + return candidateTaskUpdateStep; + } + return currentTaskUpdateStep; + } } diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateStep.java index 8b4c7bd..5370f54 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateStep.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateStep.java @@ -15,6 +15,7 @@ /** * Represent a step in the update of a task. + * Steps are equal when their impacted task instances are the same. * @author lfasani */ public abstract class TaskUpdateStep { diff --git a/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateServiceOrderingTests.java b/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateServiceOrderingTests.java new file mode 100644 index 0000000..e2ded5e --- /dev/null +++ b/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateServiceOrderingTests.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +package pepper.domain.services.update; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.List; + +import org.eclipse.sirius.components.core.api.IFeedbackMessageService; +import org.junit.jupiter.api.Test; + +import pepper.peppermm.PepperFactory; +import pepper.peppermm.Task; + +/** + * Tests priority selection and child-before-parent update ordering. + */ +public class TaskUpdateServiceOrderingTests { + + private final TaskUpdateService service = new TaskUpdateService(mock(IFeedbackMessageService.class)); + + @Test + public void dependencyWinsAcrossSeparatedDuplicatesInAnyOrder() { + Task task = PepperFactory.eINSTANCE.createTask(); + TaskUpdateStep simple = new SimpleUpdateStep(task); + TaskUpdateStep parent = new ParentUpdateStep(task); + TaskUpdateStep dependency = new DependencyUpdateStep(task); + TaskUpdateStep unrelated = new SimpleUpdateStep(PepperFactory.eINSTANCE.createTask()); + List> permutations = List.of( + List.of(simple, parent, dependency), List.of(simple, dependency, parent), + List.of(parent, simple, dependency), List.of(parent, dependency, simple), + List.of(dependency, simple, parent), List.of(dependency, parent, simple)); + + for (List steps : permutations) { + List result = List.copyOf(service.filterAndOrderTaskUpdateSteps( + List.of(steps.get(0), unrelated, steps.get(1), steps.get(2)))); + + assertThat(result).hasSize(2); + assertThat(result.get(0)).isSameAs(dependency); + assertThat(result.get(1)).isSameAs(unrelated); + } + } + + @Test + public void parentWinsOverSimpleRegardlessOfEncounterOrder() { + Task task = PepperFactory.eINSTANCE.createTask(); + TaskUpdateStep simple = new SimpleUpdateStep(task); + TaskUpdateStep parent = new ParentUpdateStep(task); + TaskUpdateStep unrelated = new SimpleUpdateStep(PepperFactory.eINSTANCE.createTask()); + + for (List steps : List.of(List.of(simple, unrelated, parent), List.of(parent, unrelated, simple))) { + List result = List.copyOf(service.filterAndOrderTaskUpdateSteps(steps)); + assertThat(result).hasSize(2); + assertThat(result.get(0)).isSameAs(parent); + assertThat(result.get(1)).isSameAs(unrelated); + } + } + + @Test + public void nestedParentsWaitForAllDescendants() { + Task root = PepperFactory.eINSTANCE.createTask(); + Task child = PepperFactory.eINSTANCE.createTask(); + Task grandchild = PepperFactory.eINSTANCE.createTask(); + Task sibling = PepperFactory.eINSTANCE.createTask(); + root.getSubTasks().addAll(List.of(child, sibling)); + child.getSubTasks().add(grandchild); + TaskUpdateStep rootStep = new ParentUpdateStep(root); + TaskUpdateStep childStep = new ParentUpdateStep(child); + TaskUpdateStep grandchildStep = new DependencyUpdateStep(grandchild); + TaskUpdateStep siblingStep = new SimpleUpdateStep(sibling); + TaskUpdateStep unrelated = new SimpleUpdateStep(PepperFactory.eINSTANCE.createTask()); + + assertThat(service.filterAndOrderTaskUpdateSteps(List.of(rootStep, childStep, unrelated, grandchildStep, siblingStep))) + .containsExactly(unrelated, grandchildStep, childStep, siblingStep, rootStep); + } +} diff --git a/backend/pepper-starter/src/main/java/pepper/starter/services/project/TaskAutomaticUpdateAdapterInstaller.java b/backend/pepper-starter/src/main/java/pepper/starter/services/project/TaskAutomaticUpdateAdapterInstaller.java new file mode 100644 index 0000000..7a3e9be --- /dev/null +++ b/backend/pepper-starter/src/main/java/pepper/starter/services/project/TaskAutomaticUpdateAdapterInstaller.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package pepper.starter.services.project; + +import java.util.Objects; + +import org.eclipse.sirius.components.collaborative.editingcontext.api.IChangeDescriptionConsumer; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IEditingContextProcessor; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.springframework.stereotype.Service; + +import pepper.domain.services.update.TaskUpdateAdapter; +import pepper.domain.services.update.TaskUpdateService; + +/** + * Installs the adapter responsible for updating tasks. + * + * @author lfasani + */ +@Service +public class TaskAutomaticUpdateAdapterInstaller implements IEditingContextProcessor, IChangeDescriptionConsumer { + private final TaskUpdateService taskUpdateService; + + public TaskAutomaticUpdateAdapterInstaller(TaskUpdateService taskUpdateService) { + this.taskUpdateService = Objects.requireNonNull(taskUpdateService); + } + + @Override + public void postProcess(IEditingContext editingContext) { + if (editingContext instanceof IEMFEditingContext siriusWebEditingContext) { + var resourceSet = siriusWebEditingContext.getDomain().getResourceSet(); + if (resourceSet.eAdapters().stream().noneMatch(TaskUpdateAdapter.class::isInstance)) { + resourceSet.eAdapters().add(new TaskUpdateAdapter(taskUpdateService)); + } + } + } +} From 1996731f2955eec58aea15f1c716cff728d2115e Mon Sep 17 00:00:00 2001 From: Laurent Fasani Date: Fri, 11 Sep 2026 15:42:39 +0200 Subject: [PATCH 3/3] [60] Make task computation consume Person availability Issue: https://github.com/ObeoNetwork/pepper/issues/60 --- backend/pepper-domain-services/README.md | 48 +++- .../services/NonWorkingDaysService.java | 49 +++- .../services/PersonCapacityAllocation.java | 113 ++++++++ .../services/TaskComputationService.java | 25 +- .../pepper/domain/services/TaskHelper.java | 51 +++- .../WorkpackageComputationService.java | 16 +- .../services/update/DependencyUpdateStep.java | 69 +++-- .../services/update/ParentUpdateStep.java | 12 + ...eUpdateStep.java => PersonUpdateStep.java} | 27 +- .../update/TaskBoundaryUpdateStep.java | 10 +- .../services/update/TaskUpdateAdapter.java | 6 +- .../services/update/TaskUpdateService.java | 255 +++++++++++++----- .../services/update/TaskUpdateStep.java | 10 + .../TaskUpdateServiceOrderingTests.java | 85 +++--- .../TaskUpdateServicePersonCapacityTests.java | 149 ++++++++++ .../view/PepperMMJavaServiceTests.java | 9 + 16 files changed, 764 insertions(+), 170 deletions(-) create mode 100644 backend/pepper-domain-services/src/main/java/pepper/domain/services/PersonCapacityAllocation.java rename backend/pepper-domain-services/src/main/java/pepper/domain/services/update/{SimpleUpdateStep.java => PersonUpdateStep.java} (74%) create mode 100644 backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateServicePersonCapacityTests.java diff --git a/backend/pepper-domain-services/README.md b/backend/pepper-domain-services/README.md index eb6a3cf..d5fe0e7 100644 --- a/backend/pepper-domain-services/README.md +++ b/backend/pepper-domain-services/README.md @@ -3,7 +3,6 @@ Pepper only supports a single time zone. The server and the browser must use the same time zone. - ## Upadate philosophy There are two approaches to solve the update. @@ -20,34 +19,57 @@ The philophy is "I know what I am doing, so I let the algorithm change task boun If this occurs, then conflicting changes are not applied until the user cancel or accept the changes. +## Task definition + +Gantt task are supported for AbstractTask and Workpackage. +A task has start and end boundaries. +A task has an effort which is the amount of time worked on this task. +Non-working days (in week and configured fixed non-working days) do not consume any effort. + +An AbstractTask has its boundaries defined as Instant. +When modifying an AbstractTask, either from Gantt, details view or by the algorithm, its boundaries are rounded to the closest half-day. + +The workpackage has its boundaries defined as LocalDate. +Both workpackage startDate and endDate are included. + ## Considering task constraints +### Internal constraints + A task is constrained by two `TimeConstraint` values among: - `START` - `END` - `EFFORT` - If a constrained boundary (`START` or `END`) is also constrained by a dependency, the dependency constraint is considered stronger than the task constraint. -A boundary constrained by a dependency can not be changed by a user. +The value that is not constraining is generally computed from the two others. +### Task dependencies -## Task boundaries computation - -An AbstractTask has its boundaries defined as Instant. -When modifying the task, either from Gantt, details view or by the algorithm, the AbstractTask boundaries are rounded to the closest half-day. -Non-working days (in week and configured fixed non-working days) do not consume any effort. - -The workpackage has its boundaries defined as LocalDate. -Both workpackage startDate and endDate are included. +A dependency is a link between the boundary of two tasks. One is a constraining task and the others is the constrained tasks. ### Task with assigned persons If a task has assigned persons, then the calculation of time constraints will consider the unavailability periods of Person. An unavailability period does not consume any effort. -On the contrary if multiple persons are available on a task, the effort is more consumed. +On the contrary, if multiple persons are available on a task, the effort is more consumed. If no person is assigned, one working day consume an effort of one day. +A person can not be "used" for multiple tasks at the same time. A person can be assigned to two tasks though. +It only means that person will produce an effort only for one task at a time. The computation will shift the task on the time line to avoid that a person is used for multiple tasks. + +## Constraint priority + +The order of priority is: + +- the assigned persons +- the dependencies + If two tasks have an END-END dependencies (both finishing at the same time) and are assigned to the same person, the second task is shifted after the first +- the internal constraints + If a constrained boundary (`START` or `END`) is also constrained by a dependency, the dependency constraint is considered stronger than the task constraint. + +A boundary constrained by a dependency can not be changed by a user. + ## Gantt interactions ### Changing one task boundary @@ -60,7 +82,7 @@ If no person is assigned, one working day consume an effort of one day. For example: A START-EFFORT task has its end date constrained by a dependency. If the start date is moved, the end date remains unchanged and the effort is updated accordingly. In any case, the impacted tasks are updating keeping the effort unchanged - -- [FUTURE ENHANCEMENT] Clicking on an alternative key while updating the task, the impacted task would be updated according to their constraint. +-- [FUTURE ENHANCEMENT] Clicking on an alternative key while updating the task, the impacted task would be updated according to their constraint. ### Moving a task diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/NonWorkingDaysService.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/NonWorkingDaysService.java index fffb830..ccf598c 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/NonWorkingDaysService.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/NonWorkingDaysService.java @@ -66,10 +66,9 @@ public Duration getEffort(Instant startTime, Instant endTime, List assig Duration effort = Duration.ZERO; Instant currentTime = startTime; while (currentTime.isBefore(endTime)) { - LocalDate currentDate = currentTime.atZone(ZoneOffset.UTC).toLocalDate(); - Instant nextDayStart = currentDate.plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant(); - Instant intervalEnd = endTime.isBefore(nextDayStart) ? endTime : nextDayStart; - int nbWorkingPersons = this.getNbWorkingPersons(currentDate, assignedPersons); + Instant nextHalfDayStart = currentTime.truncatedTo(ChronoUnit.HALF_DAYS).plus(1, ChronoUnit.HALF_DAYS); + Instant intervalEnd = endTime.isBefore(nextHalfDayStart) ? endTime : nextHalfDayStart; + int nbWorkingPersons = this.getNbWorkingPersons(currentTime, assignedPersons); if (nbWorkingPersons > 0) { effort = effort.plus(Duration.ofHours(Duration.between(currentTime, intervalEnd).toHours() * nbWorkingPersons)); } @@ -176,10 +175,10 @@ public Instant getNextEndTime(Instant startTime, int effortInHours, List Duration remainingDuration = Duration.ofHours(effortInHours); Instant currentEndTime = startTime; while (!remainingDuration.isZero() - || !this.isWorkingDay(currentEndTime.minus(1, ChronoUnit.MINUTES).atZone(ZoneOffset.UTC).toLocalDate(), persons)) { + || !this.isWorkingTime(currentEndTime.minus(1, ChronoUnit.MINUTES), persons)) { LocalDate currentDate = currentEndTime.atZone(ZoneOffset.UTC).toLocalDate(); Instant nextHalfDayStart = currentEndTime.truncatedTo(ChronoUnit.HALF_DAYS).plus(1, ChronoUnit.HALF_DAYS); - int nbWorkingPersons = this.getNbWorkingPersons(currentDate, persons); + int nbWorkingPersons = this.getNbWorkingPersons(currentEndTime, persons); if (nbWorkingPersons > 0) { Duration availableDuration = Duration.ofHours(Duration.between(currentEndTime, nextHalfDayStart).toHours() * nbWorkingPersons); Duration consumedDuration = remainingDuration.compareTo(availableDuration) < 0 @@ -225,10 +224,10 @@ public Instant getNextStartTime(Instant startTime, int effortInHours, List 0) { Duration availableDuration = Duration.ofHours(Duration.between(currentStartTime, nextHalfDayStart).toHours() * nbWorkingPersons); Duration consumedDuration = remainingDuration.compareTo(availableDuration) < 0 @@ -274,10 +273,10 @@ public Instant getPreviousStartTime(Instant endTime, int effortInHours, List 0) { Duration availableDuration = Duration.ofHours(Duration.between(previousHalfDayStart, currentStartTime).toHours() * nbWorkingPersons); Duration consumedDuration = remainingDuration.compareTo(availableDuration) < 0 @@ -388,6 +387,26 @@ private boolean isWorkingDay(LocalDate date, List persons) { return this.getNbWorkingPersons(date, persons) > 0; } + private boolean isWorkingTime(Instant instant, List persons) { + return this.getNbWorkingPersons(instant, persons) > 0; + } + + private int getNbWorkingPersons(Instant instant, List assignedPersons) { + LocalDate date = instant.atZone(ZoneOffset.UTC).toLocalDate(); + boolean isNonWorkingDay = NON_WORKING_DAYS_IN_WEEK.contains(date.getDayOfWeek()) || FRENCH_NON_WORKING_DAYS_2026.contains(date); + if (isNonWorkingDay) { + return 0; + } + if (assignedPersons == null || assignedPersons.isEmpty()) { + return 1; + } + PersonCapacityAllocation allocation = PersonCapacityAllocation.current(); + return Math.toIntExact(assignedPersons.stream() + .filter(person -> this.isAvailable(person, date)) + .filter(person -> allocation == null || allocation.isAvailable(person, instant)) + .count()); + } + private int getNbWorkingPersons(LocalDate date, List assignedPersons) { long nbWorkingDays = 0; boolean isNonWorkingDay = NON_WORKING_DAYS_IN_WEEK.contains(date.getDayOfWeek()) || FRENCH_NON_WORKING_DAYS_2026.contains(date); @@ -395,9 +414,10 @@ private int getNbWorkingPersons(LocalDate date, List assignedPersons) { if (assignedPersons == null || assignedPersons.isEmpty()) { nbWorkingDays = 1; } else { + PersonCapacityAllocation allocation = PersonCapacityAllocation.current(); nbWorkingDays = assignedPersons.stream() - .filter(person -> person.getUnavailabilityPeriods().stream() - .noneMatch(unavailabilityPeriod -> !date.isBefore(unavailabilityPeriod.getStartDate()) && !date.isAfter(unavailabilityPeriod.getEndDate()))) + .filter(person -> this.isAvailable(person, date)) + .filter(person -> allocation == null || allocation.isAvailableForDate(person, date)) .count(); } } @@ -405,6 +425,11 @@ private int getNbWorkingPersons(LocalDate date, List assignedPersons) { return Math.toIntExact(nbWorkingDays); } + private boolean isAvailable(Person person, LocalDate date) { + return person.getUnavailabilityPeriods().stream() + .noneMatch(unavailabilityPeriod -> !date.isBefore(unavailabilityPeriod.getStartDate()) && !date.isAfter(unavailabilityPeriod.getEndDate())); + } + public Duration roundToNearestHalfDay(Duration duration) { return duration.isNegative() ? duration.minusHours(6).truncatedTo(ChronoUnit.HALF_DAYS) diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/PersonCapacityAllocation.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/PersonCapacityAllocation.java new file mode 100644 index 0000000..384ea5a --- /dev/null +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/PersonCapacityAllocation.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ******************************************************************************/ +package pepper.domain.services; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import pepper.peppermm.AbstractTask; +import pepper.peppermm.Person; +import pepper.peppermm.Workpackage; + +/** + * Per-update-batch allocation of person capacity in UTC half-day slots. + * @author not me + */ +public final class PersonCapacityAllocation { + private static final ThreadLocal CURRENT = new ThreadLocal<>(); + + private final Map> reservedSlots = new IdentityHashMap<>(); + + public static Scope activate(PersonCapacityAllocation allocation) { + CURRENT.set(allocation); + return CURRENT::remove; + } + + public static PersonCapacityAllocation current() { + return CURRENT.get(); + } + + public boolean isAvailable(Person person, Instant instant) { + return !reservedSlots.getOrDefault(person, Collections.emptySet()).contains(this.slotStart(instant)); + } + + public Optional getNextAvailableSlot(Person person) { + return Optional.ofNullable(reservedSlots.get(person)) + .map(instants -> instants.stream() + .max(Instant::compareTo) + .orElse(null)); + } + + public boolean isAvailableForDate(Person person, LocalDate date) { + Instant start = date.atStartOfDay(ZoneOffset.UTC).toInstant(); + return this.isAvailable(person, start) && this.isAvailable(person, start.plus(12, ChronoUnit.HOURS)); + } + + public void reserve(AbstractTask task) { + if (task.isComputeStartEndDynamically()) { + return; + } + Instant start = task.getStartTime(); + Instant end = task.getEndTime(); + if (start == null || end == null || end.isBefore(start)) { + return; + } + Instant slot = this.slotStart(start); + Instant lastSlot = this.slotStart(end); + while (!slot.isAfter(lastSlot)) { + this.reserveAvailablePersons(task.getAssignedPersons(), slot); + slot = slot.plus(12, ChronoUnit.HOURS); + } + } + + public void reserve(Workpackage workpackage) { + LocalDate start = workpackage.getStartDate(); + LocalDate end = workpackage.getEndDate(); + if (start == null || end == null || end.isBefore(start)) { + return; + } + for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) { + Instant firstSlot = date.atStartOfDay(ZoneOffset.UTC).toInstant(); + for (Person person : workpackage.getAssignedPersons()) { + if (this.isAvailableForDate(person, date)) { + this.reserveAvailablePersons(List.of(person), firstSlot); + this.reserveAvailablePersons(List.of(person), firstSlot.plus(12, ChronoUnit.HOURS)); + } + } + } + } + + private void reserveAvailablePersons(List persons, Instant slot) { + for (Person person : persons) { + if (this.isAvailable(person, slot)) { + reservedSlots.computeIfAbsent(person, key -> new HashSet<>()).add(this.slotStart(slot)); + } + } + } + + private Instant slotStart(Instant instant) { + return instant.truncatedTo(ChronoUnit.HALF_DAYS); + } + + /** Scope that makes an allocation available to scheduling calculations on the current thread. */ + public interface Scope extends AutoCloseable { + @Override + void close(); + } +} diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskComputationService.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskComputationService.java index 99d2de1..817c8cc 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskComputationService.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskComputationService.java @@ -47,19 +47,26 @@ public class TaskComputationService { * Update the newStartTime and potentially effort or endTime according to the calculationOption. It also rounds newStartTime and shifts it sooner if included in a non-working day period. */ public void updateStartTime(AbstractTask abstractTask, Instant newStartTime) { + this.updateStartTime(abstractTask, newStartTime, false); + } + + /** + * Update the newStartTime and potentially effort or endTime according to the calculationOption. It also rounds newStartTime and shifts it sooner if included in a non-working day period. + */ + public void updateStartTime(AbstractTask abstractTask, Instant newStartTime, boolean forceKeepEffort) { TaskTimeBoundariesConstraint calculationOption = abstractTask.getCalculationOption(); Instant roundedNewStartTime = this.roundToNearestHalfDay(newStartTime); - Instant previousStartTime = nonWorkingDaysService.getPreviousStartTime(roundedNewStartTime, abstractTask.getAssignedPersons()); - abstractTask.setStartTime(this.convertAccordingToTimeZone(previousStartTime)); + Instant nextStartTime = nonWorkingDaysService.getNextStartTime(roundedNewStartTime, 0, abstractTask.getAssignedPersons()); + abstractTask.setStartTime(this.convertAccordingToTimeZone(nextStartTime)); Instant currentEndTime = this.roundToNearestHalfDay(abstractTask.getEndTime()); int currentEffort = abstractTask.getEffort(); - if (calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT) && !taskHelper.isBoundaryConstrainedByDependency(((DependencyRelatedObject) abstractTask).getDependencies(), StartOrEnd.END) && previousStartTime != null) { - Instant newEndTime = nonWorkingDaysService.getNextEndTime(previousStartTime, currentEffort, abstractTask.getAssignedPersons()).minus(1, ChronoUnit.MINUTES); + if ((calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT) || forceKeepEffort) && nextStartTime != null) { + Instant newEndTime = nonWorkingDaysService.getNextEndTime(nextStartTime, currentEffort, abstractTask.getAssignedPersons()).minus(1, ChronoUnit.MINUTES); abstractTask.setEndTime(this.convertAccordingToTimeZone(newEndTime)); } else { - if (currentEndTime != null && previousStartTime != null) { - long hourEffort = nonWorkingDaysService.getEffort(previousStartTime, currentEndTime, abstractTask.getAssignedPersons()).toHours(); + if (currentEndTime != null && nextStartTime != null) { + long hourEffort = nonWorkingDaysService.getEffort(nextStartTime, currentEndTime, abstractTask.getAssignedPersons()).toHours(); abstractTask.setEffort((int) hourEffort); } } @@ -109,9 +116,15 @@ public void updateEffort(AbstractTask abstractTask, int newEffort) { Instant currentStartTime = this.roundToNearestHalfDay(abstractTask.getStartTime()); Instant currentEndTime = this.roundToNearestHalfDay(abstractTask.getEndTime()); if (calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT) && currentStartTime != null) { + Instant newStartTime = nonWorkingDaysService.getNextStartTime(currentStartTime, 0, abstractTask.getAssignedPersons()); //.plus(1, ChronoUnit.MINUTES); + abstractTask.setStartTime(this.convertAccordingToTimeZone(newStartTime)); + Instant newEndTime = nonWorkingDaysService.getNextEndTime(currentStartTime, newEffortRouned, abstractTask.getAssignedPersons()).minus(1, ChronoUnit.MINUTES); abstractTask.setEndTime(this.convertAccordingToTimeZone(newEndTime)); } else if (calculationOption.equals(TaskTimeBoundariesConstraint.END_EFFORT) && currentEndTime != null) { + Instant newEndTime = nonWorkingDaysService.getNextEndTime(currentEndTime, 0, abstractTask.getAssignedPersons()).minus(1, ChronoUnit.MINUTES); + abstractTask.setEndTime(this.convertAccordingToTimeZone(newEndTime)); + Instant newStartTime = nonWorkingDaysService.getPreviousStartTime(currentEndTime, newEffortRouned, abstractTask.getAssignedPersons()); //.plus(1, ChronoUnit.MINUTES); abstractTask.setStartTime(this.convertAccordingToTimeZone(newStartTime)); } diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskHelper.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskHelper.java index e9e1758..4770ebe 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskHelper.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/TaskHelper.java @@ -12,7 +12,9 @@ *******************************************************************************/ package pepper.domain.services; +import java.time.Instant; import java.time.temporal.Temporal; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.Spliterator; @@ -22,9 +24,11 @@ import org.eclipse.emf.ecore.EObject; import pepper.peppermm.AbstractTask; +import pepper.peppermm.AssignableObject; import pepper.peppermm.DependencyLink; import pepper.peppermm.DependencyRelatedObject; import pepper.peppermm.NamedElement; +import pepper.peppermm.Person; import pepper.peppermm.StartOrEnd; import pepper.peppermm.TaskTimeBoundariesConstraint; import pepper.peppermm.Workpackage; @@ -35,16 +39,30 @@ * @author lfasani */ public class TaskHelper { - public Temporal getStartTemporal(DependencyRelatedObject dependencyRelatedObject) { + private final NonWorkingDaysService nonWorkingDaysService = new NonWorkingDaysService(); + + private final TemporalHelper temporalHelper = new TemporalHelper(); + + public Temporal getStartTemporal(Object object) { Temporal startTemporal = null; - if (dependencyRelatedObject instanceof AbstractTask abstractTask) { + if (object instanceof AbstractTask abstractTask) { startTemporal = abstractTask.getStartTime(); - } else if (dependencyRelatedObject instanceof Workpackage workpackage) { + } else if (object instanceof Workpackage workpackage) { startTemporal = workpackage.getStartDate(); } return startTemporal; } + public Temporal getEndTemporal(Object object) { + Temporal startTemporal = null; + if (object instanceof AbstractTask abstractTask) { + startTemporal = abstractTask.getEndTime(); + } else if (object instanceof Workpackage workpackage) { + startTemporal = workpackage.getEndDate(); + } + return startTemporal; + } + public String getName(Object task) { return Optional.of(task) .filter(NamedElement.class::isInstance) @@ -104,4 +122,31 @@ public boolean isBoundaryConstrainedByDependency(List dependenci return dependencies.stream() .anyMatch(dep -> dep.getTargetKind() == boundary); } + + /** + * The task must be computed from the start if there is not enough assigned person manpower between the earliest moment of an available assigned person and the endTime. + */ + public boolean mustBeComputedFromStartDateConsideringPersonAvailability(DependencyRelatedObject targetTask) { + boolean mustBeComputedFromEndDate = false; + if (targetTask instanceof AssignableObject assignableObject && !assignableObject.getAssignedPersons().isEmpty()) { + if (targetTask instanceof AbstractTask abstractTask) { + return this.getEarlierAvailableInstantOfPerson(abstractTask.getAssignedPersons()) + .map(earlierAvailableInstantOfPerson -> { + Instant nextEndTime = nonWorkingDaysService.getNextEndTime(temporalHelper.roundToNearestHalfDay(earlierAvailableInstantOfPerson), abstractTask.getEffort(), + abstractTask.getAssignedPersons()); + return temporalHelper.roundToNearestHalfDay(nextEndTime).isAfter(temporalHelper.roundToNearestHalfDay(abstractTask.getEndTime())); + }) + .orElse(false); + } else if (targetTask instanceof Workpackage workpackage) { + // TODO + } + } + return mustBeComputedFromEndDate; + } + + public Optional getEarlierAvailableInstantOfPerson(List persons) { + return persons.stream() + .flatMap(person -> PersonCapacityAllocation.current().getNextAvailableSlot(person).stream()) + .min(Comparator.naturalOrder()); + } } diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/WorkpackageComputationService.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/WorkpackageComputationService.java index 45ea139..dce1c0f 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/WorkpackageComputationService.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/WorkpackageComputationService.java @@ -36,18 +36,22 @@ public class WorkpackageComputationService { private final TaskHelper taskHelper = new TaskHelper(); public void updateStartDate(Workpackage workpackage, LocalDate newStartDate) { - LocalDate previousNewStartDate = nonWorkingDaysService.getPreviousStartDate(newStartDate, workpackage.getAssignedPersons()); + this.updateStartDate(workpackage, newStartDate, false); + } + + public void updateStartDate(Workpackage workpackage, LocalDate newStartDate, boolean keepEffort) { + LocalDate nextNewStartDate = nonWorkingDaysService.getNextEndDate(newStartDate, 0, workpackage.getAssignedPersons()); TaskTimeBoundariesConstraint calculationOption = workpackage.getCalculationOption(); - workpackage.setStartDate(previousNewStartDate); + workpackage.setStartDate(nextNewStartDate); LocalDate currentEndDate = workpackage.getEndDate(); int currentEffort = workpackage.getEffort(); - if (calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT) && !taskHelper.isBoundaryConstrainedByDependency(workpackage.getDependencies(), StartOrEnd.END) && previousNewStartDate != null) { - LocalDate newEndDate = nonWorkingDaysService.getNextEndDate(previousNewStartDate, currentEffort, workpackage.getAssignedPersons()); + if ((calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT) || keepEffort) && nextNewStartDate != null) { + LocalDate newEndDate = nonWorkingDaysService.getNextEndDate(nextNewStartDate, currentEffort, workpackage.getAssignedPersons()); workpackage.setEndDate(newEndDate); } else { - if (currentEndDate != null && previousNewStartDate != null) { - long newEffort = nonWorkingDaysService.getEffort(previousNewStartDate, currentEndDate, workpackage.getAssignedPersons()).toDays(); + if (currentEndDate != null && nextNewStartDate != null) { + long newEffort = nonWorkingDaysService.getEffort(nextNewStartDate, currentEndDate, workpackage.getAssignedPersons()).toDays(); workpackage.setEffort((int) newEffort); } } diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/DependencyUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/DependencyUpdateStep.java index 9fd7455..fc1f862 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/DependencyUpdateStep.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/DependencyUpdateStep.java @@ -15,6 +15,7 @@ import java.time.Instant; import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.Comparator; import java.util.List; @@ -24,6 +25,7 @@ import pepper.domain.services.TemporalHelper; import pepper.domain.services.WorkpackageComputationService; import pepper.peppermm.AbstractTask; +import pepper.peppermm.AssignableObject; import pepper.peppermm.DependencyLink; import pepper.peppermm.DependencyRelatedObject; import pepper.peppermm.StartOrEnd; @@ -41,9 +43,9 @@ public final class DependencyUpdateStep extends TaskUpdateStep { private static final WorkpackageComputationService WORKPACKAGE_COMPUTATION_SERVICE = new WorkpackageComputationService(); - private final NonWorkingDaysService nonWorkingDaysService = new NonWorkingDaysService(); + private static final NonWorkingDaysService NON_WORKING_DAYS_SERVICE = new NonWorkingDaysService(); - private final TemporalHelper temporalHelper = new TemporalHelper(); + private static final TemporalHelper TEMPORAL_HELPER = new TemporalHelper(); private final DependencyRelatedObject targetTask; @@ -68,18 +70,32 @@ public void update() { boolean endTimeControlledByDependency = TASK_HELPER.isBoundaryConstrainedByDependency(dependencies, StartOrEnd.END); TaskTimeBoundariesConstraint initialCalculationOption = TASK_HELPER.getCalculationOption(targetTask); - if (startTimeControlledByDependency) { //Whatever endTimeControlledByDependency - TASK_HELPER.setCalculationOption(targetTask, TaskTimeBoundariesConstraint.START_EFFORT); - + if (startTimeControlledByDependency) { if (targetTask instanceof AbstractTask abstractTask) { Instant nextTimeFromDependency = this.getNextTimeFromDependency(dependencies, StartOrEnd.START); - TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, nextTimeFromDependency); + TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, nextTimeFromDependency, true); } else if (targetTask instanceof Workpackage workpackage) { LocalDate nextDateFromDependency = this.getNextDateFromDependency(dependencies, StartOrEnd.START); - WORKPACKAGE_COMPUTATION_SERVICE.updateStartDate(workpackage, nextDateFromDependency); + WORKPACKAGE_COMPUTATION_SERVICE.updateStartDate(workpackage, nextDateFromDependency, true); } } - if (endTimeControlledByDependency) { + + boolean mustBeComputedFromStartDate = endTimeControlledByDependency && this.mustBeComputedFromStartDateConsideringPersonAvailability(dependencies); + if (mustBeComputedFromStartDate) { + TASK_HELPER.setCalculationOption(targetTask, TaskTimeBoundariesConstraint.START_EFFORT); + + if (targetTask instanceof AbstractTask abstractTask) { + TASK_HELPER.getEarlierAvailableInstantOfPerson(abstractTask.getAssignedPersons()) + .ifPresent(nextStartTime -> TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, nextStartTime)); + } else if (targetTask instanceof Workpackage workpackage) { + TASK_HELPER.getEarlierAvailableInstantOfPerson(workpackage.getAssignedPersons()) + .ifPresent(nextStartTime -> { + // TODO to test + LocalDate nextStartDate = nextStartTime.atOffset(ZoneOffset.UTC).toLocalDate(); + WORKPACKAGE_COMPUTATION_SERVICE.updateStartDate(workpackage, nextStartDate); + }); + } + } else if (endTimeControlledByDependency) { TASK_HELPER.setCalculationOption(targetTask, TaskTimeBoundariesConstraint.END_EFFORT); if (targetTask instanceof AbstractTask abstractTask) { @@ -94,25 +110,46 @@ public void update() { } + // We need to know if there is enough assigned persons manpower between the latest moment of the earliest assigned person available or the next time from dependency on one side and the endTime on the other side + private boolean mustBeComputedFromStartDateConsideringPersonAvailability(List dependencies) { + boolean mustBeComputedFromEndDate = false; + if (targetTask instanceof AssignableObject assignableObject && !assignableObject.getAssignedPersons().isEmpty()) { + if (targetTask instanceof AbstractTask abstractTask) { + Instant nextStartTimeFromDependency = this.getNextTimeFromDependency(dependencies, StartOrEnd.END); + return TASK_HELPER.getEarlierAvailableInstantOfPerson(abstractTask.getAssignedPersons()) + .map(earlierAvailableInstantOfPerson -> { + Instant latestInstantForStart = earlierAvailableInstantOfPerson.isAfter(nextStartTimeFromDependency) ? earlierAvailableInstantOfPerson : nextStartTimeFromDependency; + Instant nextEndTime = NON_WORKING_DAYS_SERVICE.getNextEndTime(TEMPORAL_HELPER.roundToNearestHalfDay(latestInstantForStart), abstractTask.getEffort(), + abstractTask.getAssignedPersons()); + return TEMPORAL_HELPER.roundToNearestHalfDay(nextEndTime).isAfter(TEMPORAL_HELPER.roundToNearestHalfDay(abstractTask.getEndTime())); + }) + .orElse(false); + } else if (targetTask instanceof Workpackage workpackage) { + // TODO + } + } + return mustBeComputedFromEndDate; + } + @SuppressWarnings("checkstyle:ReturnCount") Instant getNextTimeFromDependency(List dependencies, StartOrEnd targetBoundary) { return dependencies.stream() .filter(dep -> dep.getTargetKind() == targetBoundary) .filter(dependencyLink -> dependencyLink.getSource() instanceof AbstractTask) .map(dependencyLink -> { - Instant roundedStartTime = temporalHelper.roundToNearestHalfDay(((AbstractTask) dependencyLink.getSource()).getStartTime()); - Instant roundedEndTime = temporalHelper.roundToNearestHalfDay(((AbstractTask) dependencyLink.getSource()).getEndTime()); + Instant roundedStartTime = TEMPORAL_HELPER.roundToNearestHalfDay(((AbstractTask) dependencyLink.getSource()).getStartTime()); + Instant roundedEndTime = TEMPORAL_HELPER.roundToNearestHalfDay(((AbstractTask) dependencyLink.getSource()).getEndTime()); if (targetBoundary == StartOrEnd.START) { if (dependencyLink.getSourceKind() == StartOrEnd.START) { - return nonWorkingDaysService.getNextStartTime(roundedStartTime, dependencyLink.getDelay(), List.of()); + return NON_WORKING_DAYS_SERVICE.getNextStartTime(roundedStartTime, dependencyLink.getDelay(), List.of()); } else { - return nonWorkingDaysService.getNextStartTime(roundedEndTime, dependencyLink.getDelay(), List.of()); + return NON_WORKING_DAYS_SERVICE.getNextStartTime(roundedEndTime, dependencyLink.getDelay(), List.of()); } } else { if (dependencyLink.getSourceKind() == StartOrEnd.START) { - return nonWorkingDaysService.getNextEndTime(roundedStartTime, dependencyLink.getDelay(), List.of()); + return NON_WORKING_DAYS_SERVICE.getNextEndTime(roundedStartTime, dependencyLink.getDelay(), List.of()); } else { - return nonWorkingDaysService.getNextEndTime(roundedEndTime, dependencyLink.getDelay(), List.of()); + return NON_WORKING_DAYS_SERVICE.getNextEndTime(roundedEndTime, dependencyLink.getDelay(), List.of()); } } }) @@ -126,9 +163,9 @@ LocalDate getNextDateFromDependency(List dependencies, StartOrEn .filter(dependencyLink -> dependencyLink.getSource() instanceof Workpackage) .map(dependencyLink -> { if (dependencyLink.getSourceKind() == StartOrEnd.START) { - return nonWorkingDaysService.getNextEndDate(((Workpackage) dependencyLink.getSource()).getStartDate().plusDays(1), dependencyLink.getDelay() + 1, List.of()); + return NON_WORKING_DAYS_SERVICE.getNextEndDate(((Workpackage) dependencyLink.getSource()).getStartDate().plusDays(1), dependencyLink.getDelay() + 1, List.of()); } else { - return nonWorkingDaysService.getNextEndDate(((Workpackage) dependencyLink.getSource()).getEndDate().plusDays(1), dependencyLink.getDelay() + 1, List.of()); + return NON_WORKING_DAYS_SERVICE.getNextEndDate(((Workpackage) dependencyLink.getSource()).getEndDate().plusDays(1), dependencyLink.getDelay() + 1, List.of()); } }) .max(Comparator.naturalOrder()) diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/ParentUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/ParentUpdateStep.java index 4870fe1..b1707c8 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/ParentUpdateStep.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/ParentUpdateStep.java @@ -58,6 +58,18 @@ public void update() { .max(Comparator.naturalOrder()) .orElse(null); abstractTask.setEndTime(endTime); + + int duration = subTasks.stream() + .map(AbstractTask::getDuration) + .mapToInt(Integer::intValue) + .sum(); + abstractTask.setDuration(duration); + + int effort = subTasks.stream() + .map(AbstractTask::getEffort) + .mapToInt(Integer::intValue) + .sum(); + abstractTask.setEffort(effort); } } diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/SimpleUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/PersonUpdateStep.java similarity index 74% rename from backend/pepper-domain-services/src/main/java/pepper/domain/services/update/SimpleUpdateStep.java rename to backend/pepper-domain-services/src/main/java/pepper/domain/services/update/PersonUpdateStep.java index fa4e705..3994373 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/SimpleUpdateStep.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/PersonUpdateStep.java @@ -26,17 +26,20 @@ /** * This class represents an update step for a task that may be changed due to Person assignment change or Person change. - * The step preserves the effort + * This step preserves the effort + * * @author lfasani */ -public final class SimpleUpdateStep extends TaskUpdateStep { +public final class PersonUpdateStep extends TaskUpdateStep { private static final TaskHelper TASK_HELPER = new TaskHelper(); + private static final TaskComputationService TASK_COMPUTATION_SERVICE = new TaskComputationService(); + private static final WorkpackageComputationService WORKPACKAGE_COMPUTATION_SERVICE = new WorkpackageComputationService(); private final DependencyRelatedObject task; - public SimpleUpdateStep(DependencyRelatedObject task) { + public PersonUpdateStep(DependencyRelatedObject task) { this.task = task; } @@ -62,7 +65,13 @@ public void update() { TaskTimeBoundariesConstraint calculationOption = abstractTask.getCalculationOption(); switch (calculationOption) { case START_EFFORT -> TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, roundedStartTime); - case END_EFFORT -> TASK_COMPUTATION_SERVICE.updateEndTime(abstractTask, roundedEndTime); + case END_EFFORT -> { + if (TASK_HELPER.mustBeComputedFromStartDateConsideringPersonAvailability(task)) { + TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, roundedStartTime, true); + } else { + TASK_COMPUTATION_SERVICE.updateEndTime(abstractTask, roundedEndTime); + } + } case START_END -> { abstractTask.setCalculationOption(TaskTimeBoundariesConstraint.START_EFFORT); TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, roundedStartTime); @@ -77,9 +86,17 @@ public void update() { TaskTimeBoundariesConstraint calculationOption = workpackage.getCalculationOption(); switch (calculationOption) { case START_EFFORT -> WORKPACKAGE_COMPUTATION_SERVICE.updateStartDate(workpackage, startDate); - case END_EFFORT -> WORKPACKAGE_COMPUTATION_SERVICE.updateEndDate(workpackage, endDate); + case END_EFFORT -> { + if (TASK_HELPER.mustBeComputedFromStartDateConsideringPersonAvailability(task)) { + WORKPACKAGE_COMPUTATION_SERVICE.updateStartDate(workpackage, startDate, true); + } else { + WORKPACKAGE_COMPUTATION_SERVICE.updateEndDate(workpackage, endDate); + } + } case START_END -> { + workpackage.setCalculationOption(TaskTimeBoundariesConstraint.START_EFFORT); WORKPACKAGE_COMPUTATION_SERVICE.updateStartDate(workpackage, startDate); + workpackage.setCalculationOption(calculationOption); } } } diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskBoundaryUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskBoundaryUpdateStep.java index 4f94134..2f63fa3 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskBoundaryUpdateStep.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskBoundaryUpdateStep.java @@ -32,6 +32,7 @@ /** * This class represents an update step for the change of boundary of a task. + * @author lfasani */ public final class TaskBoundaryUpdateStep extends TaskUpdateStep { private static final TaskHelper TASK_HELPER = new TaskHelper(); @@ -50,6 +51,10 @@ public TaskBoundaryUpdateStep(DependencyRelatedObject task, Temporal start, Temp this.end = end; } + public Temporal getStart() { + return start; + } + @Override public Object getImpactedTask() { return task; @@ -83,7 +88,10 @@ public void update() { TaskTimeBoundariesConstraint calculationOption = abstractTask.getCalculationOption(); switch (calculationOption) { case START_EFFORT -> TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, newStartTime); - case END_EFFORT -> TASK_COMPUTATION_SERVICE.updateEndTime(abstractTask, newEndTime); + case END_EFFORT -> { + // TODO case if a person is assigned + TASK_COMPUTATION_SERVICE.updateEndTime(abstractTask, newEndTime); + } case START_END -> { TASK_COMPUTATION_SERVICE.updateStartTime(abstractTask, newStartTime); TASK_COMPUTATION_SERVICE.updateEndTime(abstractTask, newEndTime); diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateAdapter.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateAdapter.java index 95191db..c42c1ba 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateAdapter.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateAdapter.java @@ -44,13 +44,13 @@ private void handleNotification(Notification notification) { Object notifier = notification.getNotifier(); Object feature = notification.getFeature(); if (notifier instanceof DependencyRelatedObject task && feature.equals(PepperPackage.eINSTANCE.getAssignableObject_AssignedPersons())) { - taskUpdateService.updateTasksFollowingPersonChange(task); + taskUpdateService.updateTaskWithImpacts(task); } else if (notifier instanceof Person person && feature.equals(PepperPackage.eINSTANCE.getResource_UnavailabilityPeriods())) { - taskUpdateService.updateTasksFollowingPersonChange(person); + taskUpdateService.updateTasksWithImpacts(person); } else if (notifier instanceof UnavailabilityPeriod unavailabilityPeriod && (feature.equals(PepperPackage.eINSTANCE.getUnavailabilityPeriod_StartDate()) || feature.equals(PepperPackage.eINSTANCE.getUnavailabilityPeriod_EndDate()))) { if (unavailabilityPeriod.eContainer() instanceof Person person) { - taskUpdateService.updateTasksFollowingPersonChange(person); + taskUpdateService.updateTasksWithImpacts(person); } } } diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateService.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateService.java index a025fbd..67b9e73 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateService.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateService.java @@ -17,13 +17,16 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Spliterator; import java.util.Spliterators; import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.eclipse.emf.ecore.EObject; @@ -35,10 +38,10 @@ import org.eclipse.sirius.components.representations.MessageLevel; import org.springframework.stereotype.Service; -import pepper.domain.services.TaskComputationService; +import pepper.domain.services.PersonCapacityAllocation; import pepper.domain.services.TaskHelper; -import pepper.domain.services.WorkpackageComputationService; import pepper.peppermm.AbstractTask; +import pepper.peppermm.AssignableObject; import pepper.peppermm.DependencyLink; import pepper.peppermm.DependencyRelatedObject; import pepper.peppermm.NamedElement; @@ -54,16 +57,18 @@ @Service public class TaskUpdateService { + // Highest priority first for steps affecting the same task. + private static final List> IMPACTED_STEPS = List.of( + ParentUpdateStep.class, + DependencyUpdateStep.class, + PersonUpdateStep.class); + private final TaskHelper taskHelper = new TaskHelper(); private final SimpleCrossReferenceProvider simpleCrossReferenceProvider = new SimpleCrossReferenceProvider(); private final IFeedbackMessageService feedbackMessageService; - private final TaskComputationService taskComputationService = new TaskComputationService(); - - private final WorkpackageComputationService workpackageComputationService = new WorkpackageComputationService(); - public TaskUpdateService(IFeedbackMessageService feedbackMessageService) { this.feedbackMessageService = Objects.requireNonNull(feedbackMessageService); } @@ -73,18 +78,70 @@ public void updateWithImpacts(EObject task, TaskUpdateStep taskUpdateStep) { } public void updateWithImpacts(EObject task, List taskUpdateSteps) { - List tasksToUpdate = new ArrayList<>(taskUpdateSteps); - List currentBranchOfTasksToUpdate = new ArrayList<>(tasksToUpdate); - try { - this.computeTaskToUpdate(task, tasksToUpdate, currentBranchOfTasksToUpdate); - this.doUpdate(tasksToUpdate); - } catch (IllegalStateException e) { - // logged in IFeedbackMessageService + if (!taskUpdateSteps.isEmpty()) { + Temporal minTemporal = this.getStartTemporal(taskUpdateSteps); + if (minTemporal != null) { + try { + List taskUpdateStepsWithImpacts = new ArrayList<>(taskUpdateSteps); + this.computeTaskToUpdate((EObject) taskUpdateSteps.get(taskUpdateSteps.size() - 1).getImpactedTask(), taskUpdateStepsWithImpacts, List.copyOf(taskUpdateSteps)); + this.updateTasksAfterGivenTemporal((DependencyRelatedObject) task, minTemporal, taskUpdateStepsWithImpacts); + } catch (IllegalStateException e) { + // logged with IFeedbackMessage; + } + } } } + private Temporal getStartTemporal(List taskUpdateSteps) { + return taskUpdateSteps.stream() + .filter(TaskBoundaryUpdateStep.class::isInstance) + .map(TaskBoundaryUpdateStep.class::cast) + .filter(taskBoundaryUpdateStep -> taskHelper.getStartTemporal(taskBoundaryUpdateStep.getImpactedTask()) != null) + .findFirst() + .flatMap(taskBoundaryUpdateStep -> { + Temporal startTemp = taskHelper.getStartTemporal(taskBoundaryUpdateStep.getImpactedTask()); + Temporal start = taskBoundaryUpdateStep.getStart(); + Optional earliest = Stream.of(startTemp, start) + .min(Comparator.comparing(t -> (Comparable) t)); + return earliest; + }) + .orElseGet(() -> taskHelper.getStartTemporal(taskUpdateSteps.get(taskUpdateSteps.size() - 1).getImpactedTask())); + } + private void doUpdate(Collection tasksToUpdate) { - tasksToUpdate.forEach(TaskUpdateStep::update); + PersonCapacityAllocation allocation = new PersonCapacityAllocation(); + this.seedAllocation(allocation, tasksToUpdate); + try (PersonCapacityAllocation.Scope ignored = PersonCapacityAllocation.activate(allocation)) { + tasksToUpdate.forEach(step -> { + step.update(allocation); + this.reserve(allocation, step.getImpactedTask()); + }); + } + } + + /** + * Reserve allocation for non being updated tasks. + */ + private void seedAllocation(PersonCapacityAllocation allocation, Collection tasksToUpdate) { + var impactedTasks = tasksToUpdate.stream() + .map(TaskUpdateStep::getImpactedTask) + .filter(DependencyRelatedObject.class::isInstance) + .map(DependencyRelatedObject.class::cast) + .toList(); + + impactedTasks.stream() + .findFirst() + .ifPresent(task -> this.getAllTasksOfGantt(task).stream() + .filter(ganttTask -> !impactedTasks.contains(ganttTask)) + .forEach(ganttTask -> this.reserve(allocation, ganttTask))); + } + + private void reserve(PersonCapacityAllocation allocation, Object task) { + if (task instanceof AbstractTask abstractTask) { + allocation.reserve(abstractTask); + } else if (task instanceof Workpackage workpackage) { + allocation.reserve(workpackage); + } } /** @@ -155,75 +212,127 @@ private List getAllTasksOfGantt(DependencyRelatedObject return List.of(); } - public void updateTasksFollowingPersonChange(DependencyRelatedObject task) { - this.updateTasksAfterGivenTemporal(task, taskHelper.getStartTemporal(task)); + public void updateTaskWithImpacts(DependencyRelatedObject task) { + this.updateTasksAfterGivenTemporal(task, taskHelper.getStartTemporal(task), List.of()); } - public void updateTasksFollowingPersonChange(Person updatePerson) { - List tasksToUpdate = simpleCrossReferenceProvider.getInverseReferences(updatePerson).stream() + public void updateTasksWithImpacts(Person updatePerson) { + Map> rootToAnyTask = new LinkedHashMap<>(); + simpleCrossReferenceProvider.getInverseReferences(updatePerson).stream() .map(EStructuralFeature.Setting::getEObject) .filter(DependencyRelatedObject.class::isInstance) .map(DependencyRelatedObject.class::cast) - .toList(); + .forEach(dependencyRelatedObject -> { + if (dependencyRelatedObject instanceof AbstractTask abstractTask) { + taskHelper.getParent(abstractTask, Workpackage.class) + .ifPresent(workpackage -> + rootToAnyTask.computeIfAbsent(workpackage, k -> new ArrayList<>()) + .add((DependencyRelatedObject) abstractTask)); + } else if (dependencyRelatedObject instanceof Workpackage workpackage) { + taskHelper.getParent(workpackage, Project.class) + .ifPresent(project -> + rootToAnyTask.computeIfAbsent(project, k -> new ArrayList<>()) + .add(workpackage)); + } + }); - Comparator temporalComparator = Comparator.comparing(temporal -> (Comparable) temporal); - Optional minTemporal = tasksToUpdate.stream() - .map(taskHelper::getStartTemporal) - .filter(Objects::nonNull) - .min(temporalComparator); + rootToAnyTask.forEach((object, dependencyRelatedObjects) -> { + Comparator temporalComparator = Comparator.comparing(temporal -> (Comparable) temporal); + Optional minTemporal = dependencyRelatedObjects.stream() + .map(taskHelper::getStartTemporal) + .filter(Objects::nonNull) + .min(temporalComparator); - if (minTemporal.isEmpty()) { - return; - } + if (minTemporal.isEmpty()) { + return; + } - this.updateTasksAfterGivenTemporal(tasksToUpdate.get(0), minTemporal.get()); + this.updateTasksAfterGivenTemporal(dependencyRelatedObjects.get(0), minTemporal.get(), List.of()); + }); } - private void updateTasksAfterGivenTemporal(DependencyRelatedObject task, Temporal minTemporal) { - Comparator temporalComparator = Comparator.comparing(temporal -> (Comparable) temporal); - Collection taskUpdateSteps = this.getAllTasksOfGantt(task).stream() - .filter(dependencyRelatedObject -> { - Temporal startTemporal = taskHelper.getStartTemporal(dependencyRelatedObject); - return startTemporal != null && temporalComparator.compare(startTemporal, minTemporal) >= 0; - }) - .flatMap(dependencyRelatedObject -> { - List updateSteps = new ArrayList<>(List.of(new SimpleUpdateStep(dependencyRelatedObject))); - this.computeTaskToUpdate(dependencyRelatedObject, updateSteps, List.copyOf(updateSteps)); - return updateSteps.stream(); - }) - .sorted(Comparator.comparing((TaskUpdateStep taskUpdateStep) -> taskHelper.getStartTemporal((DependencyRelatedObject) taskUpdateStep.getImpactedTask()), temporalComparator)) - .toList(); - LinkedHashSet orderedTaskUpdateSteps = this.filterAndOrderTaskUpdateSteps(taskUpdateSteps); - orderedTaskUpdateSteps.forEach(taskUpdateStep -> { - System.out.println(((NamedElement) taskUpdateStep.getImpactedTask()).getName() + " " + taskUpdateStep.getClass().getSimpleName()); - }); - this.doUpdate(orderedTaskUpdateSteps); + private void updateTasksAfterGivenTemporal(DependencyRelatedObject aTaskInGantt, Temporal minTemporal, Collection preleminaryTaskUpdateSteps) { + try { + Comparator temporalComparator = Comparator.comparing(temporal -> (Comparable) temporal); + List taskUpdateStepsInGantt = this.getAllTasksOfGantt(aTaskInGantt).stream() + .filter(task -> { + Temporal startTemporal = taskHelper.getStartTemporal(task); + return startTemporal != null && temporalComparator.compare(startTemporal, minTemporal) >= 0; + }) + .flatMap(task -> { + List updateSteps = new ArrayList<>(); + if (!task.getDependencies().isEmpty()) { + updateSteps.add(new DependencyUpdateStep(task)); + this.computeTaskToUpdate(task, updateSteps, List.copyOf(updateSteps)); + } else if (task instanceof AssignableObject assignableObject && !assignableObject.getAssignedPersons().isEmpty()) { + updateSteps.add(new PersonUpdateStep(task)); + this.computeTaskToUpdate(task, updateSteps, List.copyOf(updateSteps)); + } + return updateSteps.stream(); + }) +// .sorted(Comparator.comparing((TaskUpdateStep taskUpdateStep) -> taskHelper.getStartTemporal((DependencyRelatedObject) taskUpdateStep.getImpactedTask()), temporalComparator)) + .toList(); + + Collection taskUpdateSteps = Stream.concat(preleminaryTaskUpdateSteps.stream(), taskUpdateStepsInGantt.stream()) + .toList(); + + LinkedHashSet orderedTaskUpdateSteps = this.filterAndOrderTaskUpdateSteps(taskUpdateSteps); + orderedTaskUpdateSteps.forEach(taskUpdateStep -> { + System.out.println(((NamedElement) taskUpdateStep.getImpactedTask()).getName() + " " + taskUpdateStep.getClass().getSimpleName()); + }); + this.doUpdate(orderedTaskUpdateSteps); + + } catch (IllegalStateException e) { + // logged with IFeedbackMessage; + } } private LinkedHashSet filterAndOrderTaskUpdateSteps(Collection taskUpdateSteps) { - // Part 1: eliminate identical steps (same type and same impacted task). + // Part 1.1: eliminate + // * identical steps (same type and same impacted task). + // * PersonUpdateStep if already managed by another step List distinctSteps = new ArrayList<>(); for (TaskUpdateStep step : taskUpdateSteps) { - boolean alreadyPresent = distinctSteps.stream().anyMatch(otherStep -> otherStep.getClass() == step.getClass() - && otherStep.getImpactedTask() == step.getImpactedTask()); + boolean alreadyPresent = distinctSteps.stream() + .anyMatch(otherStep -> { + boolean value = otherStep.getClass() == step.getClass() + && otherStep.getImpactedTask() == step.getImpactedTask(); + value = value || step instanceof PersonUpdateStep && step.getImpactedTask() == otherStep.getImpactedTask(); + return value; + }); if (!alreadyPresent) { distinctSteps.add(step); } } - // Part 2: reduce to the preferred step per task, preserving first encounter order. + // Part 1.2: eliminate task with no start or end + List remainingSteps1 = distinctSteps.stream() + .filter(taskUpdateStep -> { + return taskHelper.getStartTemporal(taskUpdateStep.getImpactedTask()) != null || taskHelper.getEndTemporal(taskUpdateStep.getImpactedTask()) != null; + }) + .toList(); + + // Part 2.1: order from the oldest to the most recent. + Comparator temporalComparator = Comparator.comparing(temporal -> (Comparable) temporal); + List remainingStep2s = new ArrayList<>(remainingSteps1.stream() + .sorted(Comparator.comparing((TaskUpdateStep taskUpdateStep) -> taskHelper.getStartTemporal(taskUpdateStep.getImpactedTask()), temporalComparator)) + .toList()); + + + // Part 2.2: move higher-priority steps before lower-priority steps. List remainingSteps = new ArrayList<>(); - for (TaskUpdateStep step : distinctSteps) { - boolean taskAlreadyPresent = remainingSteps.stream().anyMatch(otherStep -> otherStep.getImpactedTask() == step.getImpactedTask()); - if (!taskAlreadyPresent) { - TaskUpdateStep preferredStep = distinctSteps.stream() - .filter(otherStep -> otherStep.getImpactedTask() == step.getImpactedTask()) - .reduce(step, this::getPreferredTaskUpdateStep); - remainingSteps.add(preferredStep); - } + List pendingSteps = new ArrayList<>(remainingStep2s); + while (!pendingSteps.isEmpty()) { + TaskUpdateStep nextStep = pendingSteps.stream() + .filter(step -> pendingSteps.stream().noneMatch(otherStep -> otherStep != step && this.hasHigherPriority(otherStep, step))) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Cannot order task updates: cyclic step priorities.")); + remainingSteps.add(nextStep); + // Remove this instance, since different step types may compare equal. + pendingSteps.removeIf(step -> step == nextStep); } - // Part 3: order descendants before their ParentUpdateStep. + // Part 2.3: order descendants before their ParentUpdateStep. LinkedHashSet orderedSteps = new LinkedHashSet<>(); while (!remainingSteps.isEmpty()) { // Take the first available step, deferring parents until all descendants have been placed. @@ -239,16 +348,24 @@ private LinkedHashSet filterAndOrderTaskUpdateSteps(Collection this.getStepPriority(otherStep); + if (!hasHigherPriority) { + if (step instanceof DependencyUpdateStep && otherStep instanceof DependencyUpdateStep otherUpdateStep) { + if (otherUpdateStep.getImpactedTask() instanceof DependencyRelatedObject dependencyRelatedObject) { + hasHigherPriority = dependencyRelatedObject.getDependencies().stream() + .anyMatch(dependencyLink -> step.getImpactedTask().equals(dependencyLink.getSource())); + } + } } - return currentTaskUpdateStep; + + return hasHigherPriority; } + private int getStepPriority(TaskUpdateStep step) { + if (IMPACTED_STEPS.contains(step.getClass())) { + return 0; + } + return Integer.MAX_VALUE; + } } diff --git a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateStep.java b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateStep.java index 5370f54..326cd12 100644 --- a/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateStep.java +++ b/backend/pepper-domain-services/src/main/java/pepper/domain/services/update/TaskUpdateStep.java @@ -13,6 +13,8 @@ package pepper.domain.services.update; +import pepper.domain.services.PersonCapacityAllocation; + /** * Represent a step in the update of a task. * Steps are equal when their impacted task instances are the same. @@ -30,6 +32,14 @@ public abstract class TaskUpdateStep { public abstract void update(); + /** + * Updates this step while a batch allocation is active. Custom steps that only implement the + * legacy method keep their existing behavior. + */ + public void update(PersonCapacityAllocation allocation) { + this.update(); + } + @Override public final boolean equals(Object object) { return object instanceof TaskUpdateStep that && this.getImpactedTask() == that.getImpactedTask(); diff --git a/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateServiceOrderingTests.java b/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateServiceOrderingTests.java index e2ded5e..f4f0a97 100644 --- a/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateServiceOrderingTests.java +++ b/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateServiceOrderingTests.java @@ -16,73 +16,86 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import java.time.Instant; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; import org.eclipse.sirius.components.core.api.IFeedbackMessageService; import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; +import pepper.peppermm.DependencyLink; import pepper.peppermm.PepperFactory; import pepper.peppermm.Task; +import pepper.peppermm.TaskTimeBoundariesConstraint; /** * Tests priority selection and child-before-parent update ordering. + * @author not me */ public class TaskUpdateServiceOrderingTests { private final TaskUpdateService service = new TaskUpdateService(mock(IFeedbackMessageService.class)); @Test - public void dependencyWinsAcrossSeparatedDuplicatesInAnyOrder() { - Task task = PepperFactory.eINSTANCE.createTask(); - TaskUpdateStep simple = new SimpleUpdateStep(task); - TaskUpdateStep parent = new ParentUpdateStep(task); - TaskUpdateStep dependency = new DependencyUpdateStep(task); - TaskUpdateStep unrelated = new SimpleUpdateStep(PepperFactory.eINSTANCE.createTask()); - List> permutations = List.of( - List.of(simple, parent, dependency), List.of(simple, dependency, parent), - List.of(parent, simple, dependency), List.of(parent, dependency, simple), - List.of(dependency, simple, parent), List.of(dependency, parent, simple)); - - for (List steps : permutations) { - List result = List.copyOf(service.filterAndOrderTaskUpdateSteps( - List.of(steps.get(0), unrelated, steps.get(1), steps.get(2)))); + public void higherPriorityStepsMoveBeforeOtherTasksWhileTiesKeepTheirOrder() { + TaskUpdateStep low = new PersonUpdateStep(this.createTask("low")); + TaskUpdateStep firstHigh = new EffortUpdateStep(this.createTask("first high"), "1"); + TaskUpdateStep secondHigh = new EffortUpdateStep(this.createTask("second high"), "1"); - assertThat(result).hasSize(2); - assertThat(result.get(0)).isSameAs(dependency); - assertThat(result.get(1)).isSameAs(unrelated); - } + assertThat(this.filterAndOrderTaskUpdateSteps(List.of(low, firstHigh, secondHigh))) + .containsExactly(firstHigh, secondHigh, low); } @Test - public void parentWinsOverSimpleRegardlessOfEncounterOrder() { - Task task = PepperFactory.eINSTANCE.createTask(); - TaskUpdateStep simple = new SimpleUpdateStep(task); - TaskUpdateStep parent = new ParentUpdateStep(task); - TaskUpdateStep unrelated = new SimpleUpdateStep(PepperFactory.eINSTANCE.createTask()); + public void dependencySourcesMoveBeforeDependentsAcrossTheWholeList() { + Task first = this.createTask("first"); + Task middle = this.createTask("middle"); + Task last = this.createTask("last"); + DependencyLink firstToMiddle = PepperFactory.eINSTANCE.createDependencyLink(); + firstToMiddle.setSource(first); + middle.getDependencies().add(firstToMiddle); + DependencyLink middleToLast = PepperFactory.eINSTANCE.createDependencyLink(); + middleToLast.setSource(middle); + last.getDependencies().add(middleToLast); + TaskUpdateStep firstStep = new DependencyUpdateStep(first); + TaskUpdateStep middleStep = new DependencyUpdateStep(middle); + TaskUpdateStep lastStep = new DependencyUpdateStep(last); - for (List steps : List.of(List.of(simple, unrelated, parent), List.of(parent, unrelated, simple))) { - List result = List.copyOf(service.filterAndOrderTaskUpdateSteps(steps)); - assertThat(result).hasSize(2); - assertThat(result.get(0)).isSameAs(parent); - assertThat(result.get(1)).isSameAs(unrelated); - } + assertThat(this.filterAndOrderTaskUpdateSteps(List.of(lastStep, firstStep, middleStep))) + .containsExactly(firstStep, middleStep, lastStep); } @Test public void nestedParentsWaitForAllDescendants() { - Task root = PepperFactory.eINSTANCE.createTask(); - Task child = PepperFactory.eINSTANCE.createTask(); - Task grandchild = PepperFactory.eINSTANCE.createTask(); - Task sibling = PepperFactory.eINSTANCE.createTask(); + Task root = this.createTask("root"); + Task child = this.createTask("child"); + Task grandchild = this.createTask("grandchild"); + Task sibling = this.createTask("sibling"); root.getSubTasks().addAll(List.of(child, sibling)); child.getSubTasks().add(grandchild); TaskUpdateStep rootStep = new ParentUpdateStep(root); TaskUpdateStep childStep = new ParentUpdateStep(child); TaskUpdateStep grandchildStep = new DependencyUpdateStep(grandchild); - TaskUpdateStep siblingStep = new SimpleUpdateStep(sibling); - TaskUpdateStep unrelated = new SimpleUpdateStep(PepperFactory.eINSTANCE.createTask()); + TaskUpdateStep siblingStep = new PersonUpdateStep(sibling); + TaskUpdateStep unrelated = new PersonUpdateStep(this.createTask("unrelated")); - assertThat(service.filterAndOrderTaskUpdateSteps(List.of(rootStep, childStep, unrelated, grandchildStep, siblingStep))) + assertThat(this.filterAndOrderTaskUpdateSteps(List.of(rootStep, childStep, unrelated, grandchildStep, siblingStep))) .containsExactly(unrelated, grandchildStep, childStep, siblingStep, rootStep); } + + private Task createTask(String name) { + Task task = PepperFactory.eINSTANCE.createTask(); + task.setName(name); + task.setCalculationOption(TaskTimeBoundariesConstraint.START_EFFORT); + task.setStartTime(Instant.parse("2026-07-06T00:00:00Z")); + task.setEndTime(Instant.parse("2026-07-06T12:00:00Z").minusSeconds(60)); + task.setEffort(12); + return task; + } + + private LinkedHashSet filterAndOrderTaskUpdateSteps(Collection steps) { + return ReflectionTestUtils.invokeMethod(service, "filterAndOrderTaskUpdateSteps", steps); + } } diff --git a/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateServicePersonCapacityTests.java b/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateServicePersonCapacityTests.java new file mode 100644 index 0000000..a1c7f14 --- /dev/null +++ b/backend/pepper-domain-services/src/test/java/pepper/domain/services/update/TaskUpdateServicePersonCapacityTests.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ******************************************************************************/ +package pepper.domain.services.update; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.time.Instant; +import java.util.List; + +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.resource.impl.ResourceImpl; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; +import org.eclipse.emf.ecore.util.ECrossReferenceAdapter; +import org.eclipse.sirius.components.core.api.IFeedbackMessageService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import pepper.peppermm.DependencyLink; +import pepper.peppermm.PepperFactory; +import pepper.peppermm.Person; +import pepper.peppermm.Task; +import pepper.peppermm.TaskTimeBoundariesConstraint; +import pepper.peppermm.Workpackage; + +/** + * Tests person capacity allocation while updating a Gantt batch. + * @author lfasani + */ +public class TaskUpdateServicePersonCapacityTests { + private static final Instant MONDAY_2026_07_06_MORNING = Instant.parse("2026-07-06T00:00:00Z"); + private static final Instant TUESDAY_NOON = Instant.parse("2026-07-07T12:00:00Z"); + + private final Workpackage workpackage = PepperFactory.eINSTANCE.createWorkpackage(); + + @BeforeEach + public void beforeEach() { + ResourceSet resourceSet = new ResourceSetImpl(); + Resource resource = new ResourceImpl(); + resourceSet.getResources().add(resource); + ECrossReferenceAdapter adapter = new ECrossReferenceAdapter(); + resourceSet.eAdapters().add(adapter); + resource.getContents().add(workpackage); + } + + @ParameterizedTest + @EnumSource(value = TaskTimeBoundariesConstraint.class, names = {"START_EFFORT", "START_END"}) + public void unchangedTaskReservesItsPersonBeforeTheUpdatedTaskIsComputed(TaskTimeBoundariesConstraint calculationOption) { + Person bob = PepperFactory.eINSTANCE.createPerson(); + Task unchanged = this.task("unchanged", List.of(bob), calculationOption); + Task updated = this.task("updated", List.of(bob), calculationOption); + workpackage.getOwnedTasks().addAll(List.of(unchanged, updated)); + + new TaskUpdateService(mock(IFeedbackMessageService.class)).updateTaskWithImpacts(updated); + + assertThat(unchanged.getEffort()).isEqualTo(36); + assertThat(updated.getEffort()).isEqualTo(36); + assertThat(updated.getStartTime()).isAfterOrEqualTo(unchanged.getEndTime()); + } + + @ParameterizedTest + @EnumSource(value = TaskTimeBoundariesConstraint.class, names = {"START_EFFORT", "START_END"}) + public void unchangedTaskUsesOtherPerson(TaskTimeBoundariesConstraint calculationOption) { + Person paul = PepperFactory.eINSTANCE.createPerson(); + Person bob = PepperFactory.eINSTANCE.createPerson(); + Task unchanged = this.task("unchanged", List.of(bob), calculationOption); + Task unchanged2 = this.task("unchanged2", List.of(paul, bob), calculationOption); + workpackage.getOwnedTasks().addAll(List.of(unchanged, unchanged2)); + + new TaskUpdateService(mock(IFeedbackMessageService.class)).updateTaskWithImpacts(unchanged2); + + assertThat(unchanged.getEffort()).isEqualTo(36); + assertThat(unchanged2.getEffort()).isEqualTo(36); + assertThat(unchanged2.getStartTime()).isEqualTo(unchanged.getStartTime()); + assertThat(unchanged2.getEndTime()).isEqualTo(unchanged.getEndTime()); + } + + @ParameterizedTest + @EnumSource(value = TaskTimeBoundariesConstraint.class, names = {"START_EFFORT", "START_END", "END_EFFORT"}) + public void earlierUpdatedStepWinsSharedPersonCapacity(TaskTimeBoundariesConstraint calculationOption) { + Person bob = PepperFactory.eINSTANCE.createPerson(); + Task first = this.task("first", List.of(bob), calculationOption); + Task second = this.task("second", List.of(bob), calculationOption); + workpackage.getOwnedTasks().addAll(List.of(first, second)); + + new TaskUpdateService(mock(IFeedbackMessageService.class)).updateTaskWithImpacts(first); + + assertThat(first.getEffort()).isEqualTo(36); + assertThat(second.getEffort()).isEqualTo(36); + assertThat(second.getStartTime()).isAfterOrEqualTo(first.getEndTime()); + } + + @ParameterizedTest + @EnumSource(value = TaskTimeBoundariesConstraint.class, names = {"START_EFFORT", "START_END", "END_EFFORT"}) + public void dependencyStepConsiderPreviousStepPersonCapacity(TaskTimeBoundariesConstraint calculationOption) { + Person bob = PepperFactory.eINSTANCE.createPerson(); + Task first = this.task("first", List.of(bob), calculationOption); + Task second = this.task("second", List.of(bob), calculationOption); + workpackage.getOwnedTasks().addAll(List.of(first, second)); + + DependencyLink dependencyLinkFromFirstToSecond = PepperFactory.eINSTANCE.createDependencyLink(); + dependencyLinkFromFirstToSecond.setDelay(0); + dependencyLinkFromFirstToSecond.setTargetKind(pepper.peppermm.StartOrEnd.END); + dependencyLinkFromFirstToSecond.setSourceKind(pepper.peppermm.StartOrEnd.END); + dependencyLinkFromFirstToSecond.setSource(first); + second.getDependencies().add(dependencyLinkFromFirstToSecond); + + new TaskUpdateService(mock(IFeedbackMessageService.class)).updateTaskWithImpacts(first); + + assertThat(first.getEffort()).isEqualTo(36); + assertThat(second.getEffort()).isEqualTo(36); + assertThat(second.getStartTime()).isAfterOrEqualTo(first.getEndTime()); + } + + @ParameterizedTest + @EnumSource(value = TaskTimeBoundariesConstraint.class, names = {"START_EFFORT"}) + public void effortStepExtendsPastReservedCapacity(TaskTimeBoundariesConstraint calculationOption) { + Person bob = PepperFactory.eINSTANCE.createPerson(); + Task updated = this.task("updated", List.of(bob), calculationOption); + Task effortChanged = this.task("effortChanged", List.of(bob), calculationOption); + workpackage.getOwnedTasks().addAll(List.of(updated, effortChanged)); + + new TaskUpdateService(mock(IFeedbackMessageService.class)).updateWithImpacts(effortChanged, new EffortUpdateStep(effortChanged, "1")); + + assertThat(effortChanged.getEffort()).isEqualTo(24); + assertThat(updated.getEffort()).isEqualTo(36); + assertThat(updated.getStartTime()).isAfterOrEqualTo(effortChanged.getEndTime()); + } + + private Task task(String name, List persons, TaskTimeBoundariesConstraint calculationOption) { + Task task = PepperFactory.eINSTANCE.createTask(); + task.setName(name); + task.setCalculationOption(calculationOption); + task.setStartTime(MONDAY_2026_07_06_MORNING); + task.setEndTime(TUESDAY_NOON.minusSeconds(60)); + task.setEffort(36); + task.getAssignedPersons().addAll(persons); + return task; + } +} diff --git a/backend/pepper-starter/src/test/java/pepper/starter/configuration/view/PepperMMJavaServiceTests.java b/backend/pepper-starter/src/test/java/pepper/starter/configuration/view/PepperMMJavaServiceTests.java index 39024cf..0d75f86 100644 --- a/backend/pepper-starter/src/test/java/pepper/starter/configuration/view/PepperMMJavaServiceTests.java +++ b/backend/pepper-starter/src/test/java/pepper/starter/configuration/view/PepperMMJavaServiceTests.java @@ -108,15 +108,18 @@ public void editTask() { @Test public void editTaskWithDependency() { Task task1 = PepperFactory.eINSTANCE.createTask(); + task1.setName("t1"); taskComputationService.updateStartTime(task1, Instant.parse(MONDAY_2026_01_05_T00_00_00)); taskComputationService.updateEndTime(task1, Instant.parse(MONDAY_2026_01_05_T23_59_00)); Task task2 = PepperFactory.eINSTANCE.createTask(); + task2.setName("t2"); task2.setCalculationOption(TaskTimeBoundariesConstraint.START_EFFORT); taskComputationService.updateStartTime(task2, Instant.parse(MONDAY_2026_01_05_T00_00_00)); taskComputationService.updateEffort(task2, 24); Task task3 = PepperFactory.eINSTANCE.createTask(); + task3.setName("t3"); task3.setCalculationOption(TaskTimeBoundariesConstraint.START_EFFORT); taskComputationService.updateStartTime(task3, Instant.parse(MONDAY_2026_01_05_T00_00_00)); taskComputationService.updateEffort(task3, 24); @@ -157,21 +160,25 @@ public void editTaskWithDependency() { @Test public void editSubTaskOfDynamicTaskWithDependency() { Task task1 = PepperFactory.eINSTANCE.createTask(); + task1.setName("task1"); task1.setCalculationOption(TaskTimeBoundariesConstraint.START_END); taskComputationService.updateStartTime(task1, Instant.parse(MONDAY_2026_01_05_T00_00_00)); taskComputationService.updateEndTime(task1, Instant.parse(MONDAY_2026_01_05_T23_59_00)); Task task2 = PepperFactory.eINSTANCE.createTask(); + task2.setName("task2"); task2.setCalculationOption(TaskTimeBoundariesConstraint.START_END); taskComputationService.updateStartTime(task2, Instant.parse(MONDAY_2026_01_05_T00_00_00)); taskComputationService.updateEndTime(task2, Instant.parse(MONDAY_2026_01_05_T23_59_00)); Task task3 = PepperFactory.eINSTANCE.createTask(); + task3.setName("task3"); task3.setCalculationOption(TaskTimeBoundariesConstraint.START_END); taskComputationService.updateStartTime(task3, Instant.parse(MONDAY_2026_01_05_T00_00_00)); taskComputationService.updateEndTime(task3, Instant.parse(MONDAY_2026_01_05_T23_59_00)); Task task31 = PepperFactory.eINSTANCE.createTask(); + task31.setName("task31"); task31.setCalculationOption(TaskTimeBoundariesConstraint.START_END); taskComputationService.updateStartTime(task31, Instant.parse(TUESDAY_2026_01_06_T00_00_00)); taskComputationService.updateEndTime(task31, Instant.parse(TUESDAY_2026_01_06_T23_59_00)); @@ -195,6 +202,8 @@ public void editSubTaskOfDynamicTaskWithDependency() { assertThat(task3.getSubTasks().size()).isEqualTo(1); assertThat(task31.getEndTime()).isEqualTo(Instant.parse(TUESDAY_2026_01_06_T23_59_00).plus(1, ChronoUnit.DAYS)); + assertThat(task3.getStartTime()).isEqualTo(task31.getStartTime()); + assertThat(task3.getEndTime()).isEqualTo(task31.getEndTime()); assertThat(task1.getStartTime()).isEqualTo(task31.getEndTime().plus(1, ChronoUnit.MINUTES)); }