3204 Assign targets by specific periods

This commit is contained in:
Andreas Svanberg 2023-12-06 15:29:53 +01:00
parent a123fd1932
commit 4b727167e5
5 changed files with 133 additions and 36 deletions

@ -14,4 +14,6 @@ public interface ReviewerCapacityService {
List<User> getAllActiveReviewers();
List<User> getActiveReviewersOnUnit(Unit unit);
int getTarget(User reviewerObject, DateRange dateRange);
}

@ -63,6 +63,13 @@ class ReviewerCapacityServiceImpl implements ReviewerCapacityService, ReviewerAs
.toList();
}
@Override
public int getTarget(User reviewerObject, DateRange dateRange) {
return getTarget(reviewerObject, dateRange.from())
.map(ReviewerTarget::getTarget)
.orElse(0);
}
private Optional<ReviewerTarget> getTarget(User reviewer, LocalDate date) {
return reviewerTargetRepository.getReviewerTargets(reviewer, date)
.stream()

@ -1,5 +1,6 @@
package se.su.dsv.scipro.reviewing;
import com.google.inject.persist.Transactional;
import jakarta.persistence.EntityManager;
import se.su.dsv.scipro.system.AbstractRepository;
import se.su.dsv.scipro.system.User;
@ -16,6 +17,7 @@ public class ReviewerTargetRepositoryImpl extends AbstractRepository implements
}
@Override
@Transactional
public void save(ReviewerTarget reviewerTarget) {
EntityManager entityManager = em();
if (entityManager.contains(reviewerTarget)) {

@ -11,10 +11,7 @@
administrators during the assignment process.
</p>
<p>
You can use the below "default period" to quickly set the number of reviews for all
reviewers within a single period. If you want to set the number of reviews for a single
reviewer, you can expand the reviewer by clicking on their box and add individual targets
for each period.
Use the period selector below to select the period you want to manage targets for.
</p>
<p>
You also have the ability to mark reviewers as completely unavailable for a given period.
@ -40,34 +37,34 @@
</div>
</form>
<h3>Default period</h3>
<div class="mb-3 row">
<div class="col">
<div class="input-group">
<input type="text" class="form-control" wicket:id="default_from_date">
</div>
</div>
<div class="col-auto align-self-center">
&mdash;
</div>
<div class="col">
<div class="input-group">
<input type="text" class="form-control" wicket:id="default_to_date">
</div>
</div>
<h3>Period</h3>
<div class="mb-3">
<select class="form-select" wicket:id="periods"></select>
<small class="text-muted">Select the period you want to manage targets for.</small>
</div>
</div>
<div class="col-12 col-xl-6" wicket:id="reviewers_container">
<div class="col-12 col-xl-8" wicket:id="reviewers_container">
<div class="card mb-3" wicket:id="reviewers">
<div class="row g-0">
<div class="row g-0" wicket:id="reviewer">
<div class="col-auto">
<img class="img-fluid rounded-start profile-picture-md" wicket:id="profile_image">
</div>
<div class="col">
<div class="col-auto">
<div class="card-body">
<h4 class="card-title" wicket:id="name">[John Doe]</h4>
</div>
</div>
<div class="col">
<form class="card-body row justify-content-end" wicket:id="form">
<div class="col-auto" wicket:id="feedback"></div>
<div class="col-auto">
<input type="number" class="form-control form-control-sm" min="0" wicket:id="target">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-success btn-sm">Set target for selected period</button>
</div>
</form>
</div>
<div class="col-auto w-64-px align-self-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><defs><clipPath><path fill="#00f" fill-opacity=".514" d="m-7 1024.36h34v34h-34z"/></clipPath><clipPath><path fill="#aade87" fill-opacity=".472" d="m-6 1028.36h32v32h-32z"/></clipPath></defs><path d="m345.44 248.29l-194.29 194.28c-12.359 12.365-32.397 12.365-44.75 0-12.354-12.354-12.354-32.391 0-44.744l171.91-171.91-171.91-171.9c-12.354-12.359-12.354-32.394 0-44.748 12.354-12.359 32.391-12.359 44.75 0l194.29 194.28c6.177 6.18 9.262 14.271 9.262 22.366 0 8.099-3.091 16.196-9.267 22.373" transform="matrix(.03541-.00013.00013.03541 2.98 3.02)" fill="#4d4d4d"/></svg>
</div>

@ -2,40 +2,47 @@ package se.su.dsv.scipro.admin.pages;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.LambdaChoiceRenderer;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.form.NumberTextField;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import se.su.dsv.scipro.components.BootstrapDatePicker;
import se.su.dsv.scipro.data.DetachableServiceModel;
import se.su.dsv.scipro.profile.UserLabel;
import se.su.dsv.scipro.profile.UserProfileImage;
import se.su.dsv.scipro.reviewing.DateRange;
import se.su.dsv.scipro.reviewing.ReviewerCapacityService;
import se.su.dsv.scipro.springdata.services.UnitService;
import se.su.dsv.scipro.system.Unit;
import se.su.dsv.scipro.system.User;
import javax.inject.Inject;
import java.io.Serializable;
import java.time.Clock;
import java.time.LocalDate;
import java.time.Month;
import java.time.YearMonth;
import java.util.List;
import java.util.stream.Stream;
public class AdminReviewerCapacityManagementPage extends AbstractAdminProjectPage {
@Inject
ReviewerCapacityService reviewerCapacityService;
@Inject
UnitService unitService;
@Inject
Clock clock;
private final WebMarkupContainer reviewerList;
private IModel<Unit> selectedUnit;
private IModel<LocalDate> fromDate = new Model<>();
private IModel<LocalDate> toDate = new Model<>();
private IModel<ReviewerPeriod> selectedPeriod = new Model<>();
public AdminReviewerCapacityManagementPage() {
IModel<List<Unit>> units = LoadableDetachableModel.of(() ->
@ -56,13 +63,18 @@ public class AdminReviewerCapacityManagementPage extends AbstractAdminProjectPag
});
add(unitDropDownChoice);
FormComponent<LocalDate> fromDateField = new TextField<>("default_from_date", fromDate, LocalDate.class);
fromDateField.add(new BootstrapDatePicker());
add(fromDateField);
FormComponent<LocalDate> toDateField = new TextField<>("default_to_date", toDate, LocalDate.class);
toDateField.add(new BootstrapDatePicker());
add(toDateField);
DropDownChoice<ReviewerPeriod> periods = new DropDownChoice<>(
"periods",
selectedPeriod,
getPeriods(),
new LambdaChoiceRenderer<>(ReviewerPeriod::displayString));
periods.add(new AjaxFormComponentUpdatingBehavior("change") {
@Override
protected void onUpdate(AjaxRequestTarget target) {
target.add(reviewerList);
}
});
add(periods);
IModel<List<User>> reviewers = LoadableDetachableModel.of(() -> {
if (selectedUnit.getObject() == null) {
@ -74,13 +86,90 @@ public class AdminReviewerCapacityManagementPage extends AbstractAdminProjectPag
});
reviewerList = new WebMarkupContainer("reviewers_container");
reviewerList.add(new ListView<>("reviewers", reviewers) {
{
setReuseItems(true);
}
@Override
protected void populateItem(ListItem<User> item) {
item.add(new UserProfileImage("profile_image", item.getModel(), UserProfileImage.Size.MEDIUM));
item.add(new UserLabel("name", item.getModel()));
item.add(new ReviewerCard("reviewer", item.getModel()));
}
});
reviewerList.setOutputMarkupId(true);
add(reviewerList);
}
private List<ReviewerPeriod> getPeriods() {
YearMonth now = YearMonth.now(clock);
ReviewerPeriod current = ReviewerPeriod.fromYearMonth(now);
return Stream.iterate(current, ReviewerPeriod::next)
.limit(3)
.toList();
}
private record ReviewerPeriod(LocalDate from, LocalDate to) implements Serializable {
public static ReviewerPeriod fromYearMonth(YearMonth yearMonth) {
if (yearMonth.getMonth().compareTo(Month.JUNE) <= 0) {
return new ReviewerPeriod(
LocalDate.of(yearMonth.getYear(), Month.JANUARY, 1),
LocalDate.of(yearMonth.getYear(), Month.JUNE, 30));
}
else {
return new ReviewerPeriod(
LocalDate.of(yearMonth.getYear(), Month.JULY, 1),
LocalDate.of(yearMonth.getYear(), Month.DECEMBER, 31));
}
}
public ReviewerPeriod next() {
return fromYearMonth(YearMonth.from(to.plusDays(1)));
}
public String displayString() {
return String.format("%s - %s", from, to);
}
public DateRange toDateRange() {
return new DateRange(from, to);
}
}
private class ReviewerCard extends WebMarkupContainer {
public ReviewerCard(String id, IModel<User> reviewer) {
super(id, reviewer);
add(new UserProfileImage("profile_image", reviewer, UserProfileImage.Size.MEDIUM));
add(new UserLabel("name", reviewer));
add(new AssignTargetForm("form", reviewer));
}
private class AssignTargetForm extends Form<User> {
private IModel<Integer> target;
public AssignTargetForm(String id, IModel<User> reviewer) {
super(id, reviewer);
add(new FencedFeedbackPanel("feedback", this));
target = LoadableDetachableModel.of(() -> {
if (selectedPeriod.getObject() == null) {
return 0;
}
else {
return reviewerCapacityService.getTarget(reviewer.getObject(), selectedPeriod.getObject().toDateRange());
}
});
NumberTextField<Integer> targetField = new NumberTextField<>("target", target, Integer.class);
targetField.setMinimum(0);
add(targetField);
}
@Override
protected void onSubmit() {
reviewerCapacityService.assignTarget(getModelObject(), selectedPeriod.getObject().toDateRange(), target.getObject());
success("Target assigned");
}
}
}
}