/* * This file is part of LibrePlan * * Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e * Desenvolvemento Tecnolóxico de Galicia * Copyright (C) 2010-2011 Igalia, S.L. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.libreplan.web.planner.allocation; import static org.libreplan.web.I18nHelper._; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.WeakHashMap; import java.util.concurrent.Callable; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.joda.time.Period; import org.libreplan.business.orders.entities.Order; import org.libreplan.business.planner.entities.AggregateOfResourceAllocations; import org.libreplan.business.planner.entities.AssignmentFunction; import org.libreplan.business.planner.entities.AssignmentFunction.AssignmentFunctionName; import org.libreplan.business.planner.entities.CalculatedValue; import org.libreplan.business.planner.entities.GenericResourceAllocation; import org.libreplan.business.planner.entities.ManualFunction; import org.libreplan.business.planner.entities.ResourceAllocation; import org.libreplan.business.planner.entities.SigmoidFunction; import org.libreplan.business.planner.entities.SpecificResourceAllocation; import org.libreplan.business.planner.entities.StretchesFunctionTypeEnum; import org.libreplan.business.planner.entities.Task; import org.libreplan.business.planner.entities.TaskElement; import org.libreplan.business.resources.entities.Criterion; import org.libreplan.business.workingday.EffortDuration; import org.libreplan.web.common.EffortDurationBox; import org.libreplan.web.common.FilterUtils; import org.libreplan.web.common.IMessagesForUser; import org.libreplan.web.common.MessagesForUser; import org.libreplan.web.common.OnlyOneVisible; import org.libreplan.web.common.Util; import org.libreplan.web.planner.allocation.stretches.StretchesFunctionConfiguration; import org.zkoss.ganttz.timetracker.ICellForDetailItemRenderer; import org.zkoss.ganttz.timetracker.IConvertibleToColumn; import org.zkoss.ganttz.timetracker.PairOfLists; import org.zkoss.ganttz.timetracker.TimeTrackedTable; import org.zkoss.ganttz.timetracker.TimeTrackedTableWithLeftPane; import org.zkoss.ganttz.timetracker.TimeTracker; import org.zkoss.ganttz.timetracker.TimeTracker.IDetailItemFilter; import org.zkoss.ganttz.timetracker.TimeTrackerComponentWithoutColumns; import org.zkoss.ganttz.timetracker.zoom.DetailItem; import org.zkoss.ganttz.timetracker.zoom.ZoomLevel; import org.zkoss.ganttz.util.Interval; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.event.EventListener; import org.zkoss.zk.ui.event.Events; import org.zkoss.zk.ui.util.Clients; import org.zkoss.zk.ui.util.GenericForwardComposer; import org.zkoss.zul.Button; import org.zkoss.zul.Div; import org.zkoss.zul.Grid; import org.zkoss.zul.Hbox; import org.zkoss.zul.Label; import org.zkoss.zul.LayoutRegion; import org.zkoss.zul.ListModel; import org.zkoss.zul.Listbox; import org.zkoss.zul.Listcell; import org.zkoss.zul.Listitem; import org.zkoss.zul.Messagebox; import org.zkoss.zul.SimpleListModel; import org.zkoss.zul.Column; /** * Controller for Advanced Allocation of task. * * @author Óscar González Fernández <ogonzalez@igalia.com> * @author Diego Pino García <dpino@igalia.com> * */ public class AdvancedAllocationController extends GenericForwardComposer { public static class AllocationInput { private final AggregateOfResourceAllocations aggregate; private final IAdvanceAllocationResultReceiver resultReceiver; private final TaskElement task; public AllocationInput(AggregateOfResourceAllocations aggregate, TaskElement task, IAdvanceAllocationResultReceiver resultReceiver) { Validate.notNull(aggregate); Validate.notNull(resultReceiver); Validate.notNull(task); this.aggregate = aggregate; this.task = task; this.resultReceiver = resultReceiver; } List getAllocationsSortedByStartDate() { return getAggregate().getAllocationsSortedByStartDate(); } EffortDuration getTotalEffort() { return getAggregate().getTotalEffort(); } AggregateOfResourceAllocations getAggregate() { return aggregate; } String getTaskName() { return task.getName(); } IAdvanceAllocationResultReceiver getResultReceiver() { return resultReceiver; } Interval calculateInterval() { List<ResourceAllocation<?>> all = getAllocationsSortedByStartDate(); if ( all.isEmpty() ) return new Interval(task.getStartDate(), task.getEndDate()); else { LocalDate start = min(all.get(0).getStartConsideringAssignments(), all.get(0).getStartDate()); LocalDate taskEndDate = LocalDate.fromDateFields(task.getEndDate()); LocalDate end = max(getEnd(all), taskEndDate); return new Interval(asDate(start), asDate(end)); } } private LocalDate min(LocalDate... dates) { return Collections.min(Arrays.asList(dates), null); } private LocalDate max(LocalDate... dates) { return Collections.max(Arrays.asList(dates), null); } private static LocalDate getEnd(List<ResourceAllocation<?>> all) { ArrayList<ResourceAllocation<?>> reversed = reverse(all); LocalDate end = reversed.get(0).getEndDate(); ListIterator<ResourceAllocation<?>> listIterator = reversed.listIterator(1); while (listIterator.hasNext()) { ResourceAllocation<?> current = listIterator.next(); if ( current.getEndDate().compareTo(end) >= 0 ) end = current.getEndDate(); else return end; } return end; } private static ArrayList<ResourceAllocation<?>> reverse(List<ResourceAllocation<?>> all) { ArrayList<ResourceAllocation<?>> reversed = new ArrayList<>(all); Collections.reverse(reversed); return reversed; } private static Date asDate(LocalDate start) { return start.toDateTimeAtStartOfDay().toDate(); } } public interface IAdvanceAllocationResultReceiver { Restriction createRestriction(); void accepted(AggregateOfResourceAllocations modifiedAllocations); void cancel(); } public interface IBack { void goBack(); boolean isAdvanceAssignmentOfSingleTask(); } public interface Restriction { interface IRestrictionSource { /** * Method in use. */ EffortDuration getTotalEffort(); LocalDate getStart(); LocalDate getEnd(); CalculatedValue getCalculatedValue(); } static Restriction build(IRestrictionSource restrictionSource) { switch (restrictionSource.getCalculatedValue()) { case END_DATE: case RESOURCES_PER_DAY: return Restriction.emptyRestriction(); case NUMBER_OF_HOURS: return Restriction.onlyAssignOnInterval(restrictionSource.getStart(), restrictionSource.getEnd()); default: throw new RuntimeException("unhandled case: " + restrictionSource.getCalculatedValue()); } } static Restriction emptyRestriction() { return new NoRestriction(); } static Restriction onlyAssignOnInterval(LocalDate start, LocalDate end){ return new OnlyOnIntervalRestriction(start, end); } LocalDate limitStartDate(LocalDate startDate); LocalDate limitEndDate(LocalDate localDate); boolean isDisabledEditionOn(DetailItem item); boolean isInvalidTotalEffort(EffortDuration totalEffort); void showInvalidEffort(IMessagesForUser messages, EffortDuration totalEffort); void markInvalidEffort(Row groupingRow, EffortDuration currentEffort); } private static class OnlyOnIntervalRestriction implements Restriction { private final LocalDate start; private final LocalDate end; private OnlyOnIntervalRestriction(LocalDate start, LocalDate end) { super(); this.start = start; this.end = end; } private org.joda.time.Interval intervalAllowed() { return new org.joda.time.Interval(start.toDateTimeAtStartOfDay(), end.toDateTimeAtStartOfDay()); } @Override public boolean isDisabledEditionOn(DetailItem item) { return !intervalAllowed().overlaps(new org.joda.time.Interval(item.getStartDate(), item.getEndDate())); } @Override public boolean isInvalidTotalEffort(EffortDuration totalEffort) { return false; } @Override public LocalDate limitEndDate(LocalDate argEnd) { return end.compareTo(argEnd) < 0 ? end : argEnd; } @Override public LocalDate limitStartDate(LocalDate argStart) { return start.compareTo(argStart) > 0 ? start : argStart; } @Override public void showInvalidEffort(IMessagesForUser messages, EffortDuration totalEffort) { throw new UnsupportedOperationException(); } @Override public void markInvalidEffort(Row groupingRow, EffortDuration currentEffort) { throw new UnsupportedOperationException(); } } private static class NoRestriction implements Restriction { @Override public boolean isDisabledEditionOn(DetailItem item) { return false; } @Override public boolean isInvalidTotalEffort(EffortDuration totalEffort) { return false; } @Override public LocalDate limitEndDate(LocalDate endDate) { return endDate; } @Override public LocalDate limitStartDate(LocalDate startDate) { return startDate; } @Override public void markInvalidEffort(Row groupingRow, EffortDuration currentEffort) { throw new UnsupportedOperationException(); } @Override public void showInvalidEffort(IMessagesForUser messages, EffortDuration totalEffort) { throw new UnsupportedOperationException(); } } private static final int VERTICAL_MAX_ELEMENTS = 25; private static final String ADVANCE_ALLOCATIONS_FUNCTION_CALL = "ADVANCE_ALLOCATIONS.listenToScroll();"; private IMessagesForUser messages; private Component insertionPointTimetracker; private Div insertionPointLeftPanel; private LayoutRegion insertionPointRightPanel; private Button paginationDownButton; private Button paginationUpButton; private Button verticalPaginationUpButton; private Button verticalPaginationDownButton; private TimeTracker timeTracker; private PaginatorFilter paginatorFilter; private Listbox advancedAllocationZoomLevel; private TimeTrackerComponentWithoutColumns timeTrackerComponent; private Grid leftPane; private TimeTrackedTable<Row> table; private IBack back; private List<AllocationInput> allocationInputs; private Component associatedComponent; private Listbox advancedAllocationHorizontalPagination; private Listbox advancedAllocationVerticalPagination; private ZoomLevel zoomLevel; private Order order; private List<Row> rowsCached = null; private Map<AllocationInput, Row> groupingRows = new HashMap<>(); private OnlyOneVisible onlyOneVisible; private Component normalLayout; private Component noDataLayout; private TimeTrackedTableWithLeftPane<Row, Row> timeTrackedTableWithLeftPane; private int verticalIndex = 0; private List<Integer> verticalPaginationIndexes; private int verticalPage; public AdvancedAllocationController(Order order, IBack back, List<AllocationInput> allocationInputs) { setInputData(order, back, allocationInputs); } private void setInputData(Order order, IBack back, List<AllocationInput> allocationInputs) { Validate.notNull(order); Validate.notNull(back); Validate.noNullElements(allocationInputs); this.order = order; this.back = back; this.allocationInputs = allocationInputs; } public void reset(Order order, IBack back, List<AllocationInput> allocationInputs) { rowsCached = null; setInputData(order, back, allocationInputs); loadAndInitializeComponents(); } @Override public void doAfterCompose(Component comp) throws Exception { super.doAfterCompose(comp); normalLayout = comp.getFellow("normalLayout"); noDataLayout = comp.getFellow("noDataLayout"); onlyOneVisible = new OnlyOneVisible(normalLayout, noDataLayout); this.associatedComponent = comp; loadAndInitializeComponents(); Clients.evalJavaScript(ADVANCE_ALLOCATIONS_FUNCTION_CALL); } private void loadAndInitializeComponents() { messages = new MessagesForUser(associatedComponent.getFellow("messages")); if ( allocationInputs.isEmpty() ) onlyOneVisible.showOnly(noDataLayout); else { onlyOneVisible.showOnly(normalLayout); createComponents(); insertComponentsInLayout(); timeTrackerComponent.afterCompose(); table.afterCompose(); } } private class PaginatorFilter implements IDetailItemFilter { private DateTime intervalStart; private DateTime intervalEnd; private DateTime paginatorStart; private DateTime paginatorEnd; private ZoomLevel zoomLevel = ZoomLevel.DETAIL_ONE; @Override public Interval getCurrentPaginationInterval() { return new Interval(intervalStart.toDate(), intervalEnd.toDate()); } private Period intervalIncrease() { switch (zoomLevel) { case DETAIL_ONE: case DETAIL_TWO: return Period.years(5); case DETAIL_THREE: return Period.years(2); case DETAIL_FOUR: return Period.months(6); case DETAIL_FIVE: return Period.weeks(6); default: break; } return Period.years(5); } void populateHorizontalListbox() { advancedAllocationHorizontalPagination.getItems().clear(); if ( intervalStart != null ) { DateTime itemStart = intervalStart; DateTime itemEnd = intervalStart.plus(intervalIncrease()); while (intervalEnd.isAfter(itemStart)) { if ( intervalEnd.isBefore(itemEnd) || itemEnd.plus(intervalIncrease()).isAfter(intervalEnd) ) itemEnd = intervalEnd; Listitem item = new Listitem(Util.formatDate(itemStart) + " - " + Util.formatDate(itemEnd.minusDays(1))); advancedAllocationHorizontalPagination.appendChild(item); itemStart = itemEnd; itemEnd = itemEnd.plus(intervalIncrease()); } } int size = advancedAllocationHorizontalPagination.getItems().size(); advancedAllocationHorizontalPagination.setDisabled(size < 2); advancedAllocationHorizontalPagination.setSelectedIndex(0); } void goToHorizontalPage(int interval) { if ( interval >= 0 ) { paginatorStart = intervalStart; for (int i = 0; i < interval; i++) paginatorStart = paginatorStart.plus(intervalIncrease()); paginatorEnd = paginatorStart.plus(intervalIncrease()); // Avoid reduced intervals if ( !intervalEnd.isAfter(paginatorEnd.plus(intervalIncrease())) ) paginatorEnd = intervalEnd; updatePaginationButtons(); } } @Override public Collection<DetailItem> selectsFirstLevel(Collection<DetailItem> firstLevelDetails) { ArrayList<DetailItem> result = new ArrayList<>(); for (DetailItem each : firstLevelDetails) if ((each.getStartDate() == null) || !(each.getStartDate().isBefore(paginatorStart)) && (each.getStartDate().isBefore(paginatorEnd))) { result.add(each); } return result; } @Override public Collection<DetailItem> selectsSecondLevel(Collection<DetailItem> secondLevelDetails) { ArrayList<DetailItem> result = new ArrayList<>(); for (DetailItem each : secondLevelDetails) if ((each.getStartDate() == null) || !(each.getStartDate().isBefore(paginatorStart)) && (each.getStartDate().isBefore(paginatorEnd))) { result.add(each); } return result; } public void next() { paginatorStart = paginatorStart.plus(intervalIncrease()); paginatorEnd = paginatorEnd.plus(intervalIncrease()); // Avoid reduced last intervals if ( !intervalEnd.isAfter(paginatorEnd.plus(intervalIncrease())) ) paginatorEnd = paginatorEnd.plus(intervalIncrease()); updatePaginationButtons(); } public void previous() { paginatorStart = paginatorStart.minus(intervalIncrease()); paginatorEnd = paginatorEnd.minus(intervalIncrease()); updatePaginationButtons(); } private void updatePaginationButtons() { paginationDownButton.setDisabled(isFirstPage()); paginationUpButton.setDisabled(isLastPage()); } boolean isFirstPage() { return !(paginatorStart.isAfter(intervalStart)); } boolean isLastPage() { return (paginatorEnd.isAfter(intervalEnd)) || (paginatorEnd.isEqual(intervalEnd)); } public void setZoomLevel(ZoomLevel detailLevel) { zoomLevel = detailLevel; } public void setInterval(Interval realInterval) { intervalStart = realInterval.getStart().toDateTimeAtStartOfDay(); intervalEnd = realInterval.getFinish().toDateTimeAtStartOfDay(); paginatorStart = intervalStart; paginatorEnd = intervalStart.plus(intervalIncrease()); if ( paginatorEnd.plus(intervalIncrease()).isAfter(intervalEnd) ) paginatorEnd = intervalEnd; updatePaginationButtons(); } @Override public void resetInterval() { setInterval(timeTracker.getRealInterval()); } } private void createComponents() { timeTracker = new TimeTracker(addMarginToInterval(), self); paginatorFilter = new PaginatorFilter(); zoomLevel = FilterUtils.readZoomLevel(order); if ( zoomLevel != null ) timeTracker.setZoomLevel(zoomLevel); paginatorFilter.setZoomLevel(timeTracker.getDetailLevel()); paginatorFilter.setInterval(timeTracker.getRealInterval()); paginationUpButton.setDisabled(isLastPage()); advancedAllocationZoomLevel.setSelectedIndex(timeTracker.getDetailLevel().ordinal()); timeTracker.setFilter(paginatorFilter); timeTracker.addZoomListener(detailLevel -> { FilterUtils.writeZoomLevel(order, detailLevel); zoomLevel = detailLevel; paginatorFilter.setZoomLevel(detailLevel); paginatorFilter.setInterval(timeTracker.getRealInterval()); timeTracker.setFilter(paginatorFilter); populateHorizontalListbox(); Clients.evalJavaScript(ADVANCE_ALLOCATIONS_FUNCTION_CALL); }); timeTrackerComponent = new TimeTrackerComponentWithoutColumns(timeTracker, "timetrackerheader"); timeTrackedTableWithLeftPane = new TimeTrackedTableWithLeftPane<>( getDataSource(), getColumnsForLeft(), getLeftRenderer(), getRightRenderer(), timeTracker); table = timeTrackedTableWithLeftPane.getRightPane(); table.setSclass("timeTrackedTableWithLeftPane"); leftPane = timeTrackedTableWithLeftPane.getLeftPane(); leftPane.setSizedByContent(false); Clients.evalJavaScript(ADVANCE_ALLOCATIONS_FUNCTION_CALL); populateHorizontalListbox(); } /** * It should be public! */ public void paginationDown() { paginatorFilter.previous(); reloadComponent(); advancedAllocationHorizontalPagination .setSelectedIndex(advancedAllocationHorizontalPagination.getSelectedIndex() - 1); } /** * It should be public! */ public void paginationUp() { paginatorFilter.next(); reloadComponent(); advancedAllocationHorizontalPagination .setSelectedIndex(Math.max(0, advancedAllocationHorizontalPagination.getSelectedIndex()) + 1); } /** * It should be public! */ public void goToSelectedHorizontalPage() { paginatorFilter.goToHorizontalPage(advancedAllocationHorizontalPagination.getSelectedIndex()); reloadComponent(); } private void populateHorizontalListbox() { advancedAllocationHorizontalPagination.setVisible(true); paginatorFilter.populateHorizontalListbox(); } private void reloadComponent() { timeTrackedTableWithLeftPane.reload(); timeTrackerComponent.recreate(); // Reattach listener for zoomLevel changes timeTracker.addZoomListener(detailLevel -> { paginatorFilter.setZoomLevel(detailLevel); paginatorFilter.setInterval(timeTracker.getRealInterval()); timeTracker.setFilter(paginatorFilter); populateHorizontalListbox(); Clients.evalJavaScript(ADVANCE_ALLOCATIONS_FUNCTION_CALL); }); Clients.evalJavaScript(ADVANCE_ALLOCATIONS_FUNCTION_CALL); } private boolean isLastPage() { return paginatorFilter.isLastPage(); } private void insertComponentsInLayout() { insertionPointRightPanel.getChildren().clear(); insertionPointRightPanel.appendChild(table); insertionPointLeftPanel.getChildren().clear(); insertionPointLeftPanel.appendChild(leftPane); insertionPointTimetracker.getChildren().clear(); insertionPointTimetracker.appendChild(timeTrackerComponent); } private void checkInvalidTotalEffort() { for (AllocationInput allocationInput : allocationInputs) { EffortDuration totalEffort = allocationInput.getTotalEffort(); Restriction restriction = allocationInput.getResultReceiver().createRestriction(); if ( restriction.isInvalidTotalEffort(totalEffort) ) { Row groupingRow = groupingRows.get(allocationInput); restriction.markInvalidEffort(groupingRow, totalEffort); } } } /** * It should be public! */ public void onClick$acceptButton() { checkInvalidTotalEffort(); back.goBack(); for (AllocationInput allocationInput : allocationInputs) allocationInput.getResultReceiver().accepted(allocationInput.getAggregate()); } /** * It should be public! */ public void onClick$saveButton() { checkInvalidTotalEffort(); for (AllocationInput allocationInput : allocationInputs) allocationInput.getResultReceiver().accepted(allocationInput.getAggregate()); Messagebox.show(_("Changes applied"), _("Information"), Messagebox.OK, Messagebox.INFORMATION); } /** * It should be public! */ public void onClick$cancelButton() { back.goBack(); for (AllocationInput allocationInput : allocationInputs) allocationInput.getResultReceiver().cancel(); } /** * It should be public! */ public ListModel getZoomLevels() { ZoomLevel[] selectableZoomlevels = { ZoomLevel.DETAIL_ONE, ZoomLevel.DETAIL_TWO, ZoomLevel.DETAIL_THREE, ZoomLevel.DETAIL_FOUR, ZoomLevel.DETAIL_FIVE }; return new SimpleListModel<>(selectableZoomlevels); } /** * It should be public! */ public void setZoomLevel(final ZoomLevel zoomLevel) { timeTracker.setZoomLevel(zoomLevel); } /** * It should be public! */ public void onClick$zoomIncrease() { timeTracker.zoomIncrease(); } /** * It should be public! */ public void onClick$zoomDecrease() { timeTracker.zoomDecrease(); } private List<Row> getRows() { if ( rowsCached != null ) return filterRows(rowsCached); rowsCached = new ArrayList<>(); int position = 1; for (AllocationInput allocationInput : allocationInputs) { if ( !allocationInput.getAggregate().getAllocationsSortedByStartDate().isEmpty() ) { Row groupingRow = buildGroupingRow(allocationInput); groupingRow.setDescription(position + " " + allocationInput.getTaskName()); groupingRows.put(allocationInput, groupingRow); rowsCached.add(groupingRow); List<Row> genericRows = genericRows(allocationInput); groupingRow.listenTo(genericRows); rowsCached.addAll(genericRows); List<Row> specificRows = specificRows(allocationInput); groupingRow.listenTo(specificRows); rowsCached.addAll(specificRows); position++; } } populateVerticalListbox(); return filterRows(rowsCached); } private List<Row> filterRows(List<Row> rows) { verticalPaginationUpButton.setDisabled(verticalIndex <= 0); verticalPaginationDownButton.setDisabled((verticalIndex + VERTICAL_MAX_ELEMENTS) >= rows.size()); if ( advancedAllocationVerticalPagination.getChildren().size() >= 2 ) { advancedAllocationVerticalPagination.setDisabled(false); advancedAllocationVerticalPagination.setSelectedIndex(verticalPage); } else { advancedAllocationVerticalPagination.setDisabled(true); } return rows.subList( verticalIndex, verticalPage + 1 < verticalPaginationIndexes.size() ? verticalPaginationIndexes.get(verticalPage + 1) : rows.size()); } /** * It should be public! */ public void verticalPagedown() { verticalPage++; verticalIndex = verticalPaginationIndexes.get(verticalPage); timeTrackedTableWithLeftPane.reload(); } /** * It should be public! */ public void verticalPageup() { verticalPage--; verticalIndex = verticalPaginationIndexes.get(verticalPage); timeTrackedTableWithLeftPane.reload(); } /** * It should be public! */ public void goToSelectedVerticalPage() { verticalPage = advancedAllocationVerticalPagination.getSelectedIndex(); verticalIndex = verticalPaginationIndexes.get(verticalPage); timeTrackedTableWithLeftPane.reload(); } private void populateVerticalListbox() { if ( rowsCached != null ) { verticalPaginationIndexes = new ArrayList<>(); advancedAllocationVerticalPagination.getChildren().clear(); for (int i = 0; i < rowsCached.size(); i = correctVerticalPageDownPosition(i + VERTICAL_MAX_ELEMENTS)) { int endPosition = correctVerticalPageUpPosition(Math.min(rowsCached.size(), i + VERTICAL_MAX_ELEMENTS) - 1); String label = rowsCached.get(i).getDescription() + " - " + rowsCached.get(endPosition).getDescription(); Listitem item = new Listitem(); item.appendChild(new Listcell(label)); advancedAllocationVerticalPagination.appendChild(item); verticalPaginationIndexes.add(i); } if ( !rowsCached.isEmpty() ) advancedAllocationVerticalPagination.setSelectedIndex(0); } } private int correctVerticalPageUpPosition(int position) { int correctedPosition = position; // Moves the pointer up until it finds the previous grouping row or the beginning of the list while (correctedPosition > 0 && !rowsCached.get(correctedPosition).isGroupingRow()) correctedPosition--; return correctedPosition; } private int correctVerticalPageDownPosition(int position) { int correctedPosition = position; // Moves the pointer down until it finds the next grouping row or the end of the list while(correctedPosition < rowsCached.size() && !rowsCached.get(correctedPosition).isGroupingRow()) correctedPosition++; return correctedPosition; } private List<Row> specificRows(AllocationInput allocationInput) { return allocationInput .getAggregate() .getSpecificAllocations() .stream() .map(specificResourceAllocation -> createSpecificRow( specificResourceAllocation, allocationInput.getResultReceiver().createRestriction(), allocationInput.task)) .collect(Collectors.toList()); } private Row createSpecificRow(SpecificResourceAllocation specificResourceAllocation, Restriction restriction, TaskElement task) { return Row.createRow( messages, restriction, specificResourceAllocation.getResource().getName(), 1, Collections.singletonList(specificResourceAllocation), specificResourceAllocation.getResource().getShortDescription(), specificResourceAllocation.getResource().isLimitingResource(), task); } private List<Row> genericRows(AllocationInput allocationInput) { return allocationInput .getAggregate() .getGenericAllocations() .stream() .map(genericResourceAllocation -> buildGenericRow( genericResourceAllocation, allocationInput.getResultReceiver().createRestriction(), allocationInput.task)) .collect(Collectors.toList()); } private Row buildGenericRow(GenericResourceAllocation genericResourceAllocation, Restriction restriction, TaskElement task) { return Row.createRow( messages, restriction, Criterion.getCaptionFor(genericResourceAllocation), 1, Collections.singletonList(genericResourceAllocation), genericResourceAllocation.isLimiting(), task); } private Row buildGroupingRow(AllocationInput allocationInput) { Restriction restriction = allocationInput.getResultReceiver().createRestriction(); String taskName = allocationInput.getTaskName(); return Row.createRow( messages, restriction, taskName, 0, allocationInput.getAllocationsSortedByStartDate(), false, allocationInput.task); } private ICellForDetailItemRenderer<ColumnOnRow, Row> getLeftRenderer() { /* * Do not replace it with method reference or lambda expression. * It will be the reason of * java.lang.IllegalArgumentException: * the generic type cannot be inferred * if actual type parameters are not declared or implements the raw interface */ return new ICellForDetailItemRenderer<ColumnOnRow, Row>() { @Override public Component cellFor(ColumnOnRow column, Row row) { return column.cellFor(row); } }; } private List<ColumnOnRow> getColumnsForLeft() { List<ColumnOnRow> result = new ArrayList<>(); result.add(new ColumnOnRow(_("Name")) { @Override public Component cellFor(Row row) { return row.getNameLabel(); } }); result.add(new ColumnOnRow(_("Efforts"), "52px") { @Override public Component cellFor(Row row) { return row.getAllEffort(); } }); result.add(new ColumnOnRow(_("Function"), "130px") { @Override public Component cellFor(Row row) { return row.getFunction(); } }); return result; } private Callable<PairOfLists<Row, Row>> getDataSource() { return () -> { List<Row> rows = getRows(); return new PairOfLists<>(rows, rows); }; } private ICellForDetailItemRenderer<DetailItem, Row> getRightRenderer() { /* * Do not replace it with method reference or lambda expression. * It will be the reason of * java.lang.IllegalArgumentException: * the generic type cannot be inferred if * actual type parameters are not declared or implements the raw interface */ return new ICellForDetailItemRenderer<DetailItem, Row>() { @Override public Component cellFor(DetailItem item, Row data) { return data.effortOnInterval(item); } }; } private Interval intervalFromData() { Interval result = null; for (AllocationInput each : allocationInputs) { Interval intervalForInput = each.calculateInterval(); result = result == null ? intervalForInput : result.coalesce(intervalForInput); } return result; } private Interval addMarginToInterval() { // No global margin is added by default return intervalFromData(); } /** * It should be public! */ public boolean isAdvancedAllocationOfSingleTask() { return back.isAdvanceAssignmentOfSingleTask(); } } abstract class ColumnOnRow implements IConvertibleToColumn { private final String columnName; private String width = null; ColumnOnRow(String columnName) { this.columnName = columnName; } ColumnOnRow(String columnName, String width) { this.columnName = columnName; this.width = width; } public abstract Component cellFor(Row row); @Override public Column toColumn() { Column column = new Column(); column.setLabel(_(columnName)); column.setSclass(columnName.toLowerCase()); if ( width != null ) column.setWidth(width); return column; } public String getName() { return columnName; } } interface CellChangedListener { void changeOn(DetailItem detailItem); void changeOnGlobal(); } class Row { private EffortDurationBox allEffortInput; private Label nameLabel; private List<CellChangedListener> listeners = new ArrayList<>(); private Map<DetailItem, Component> componentsByDetailItem = new WeakHashMap<>(); private String name; private String description; private int level; private final AggregateOfResourceAllocations aggregate; private final AdvancedAllocationController.Restriction restriction; private final IMessagesForUser messages; private TaskElement task; private Hbox hboxAssignmentFunctionsCombo = null; private AssignmentFunctionListbox assignmentFunctionsCombo = null; private Button assignmentFunctionsConfigureButton = null; private IAssignmentFunctionConfiguration flat = new IAssignmentFunctionConfiguration() { @Override public void goToConfigure() { throw new UnsupportedOperationException("Flat allocation is not configurable"); } @Override public String getName() { return AssignmentFunctionName.FLAT.toString(); } @Override public boolean isTargetedTo(AssignmentFunction function) { return function == null; } @Override public void applyOn(ResourceAllocation<?> resourceAllocation) { resourceAllocation.setAssignmentFunctionWithoutApply(null); resourceAllocation .withPreviousAssociatedResources() .onIntervalWithinTask(resourceAllocation.getStartDate(), resourceAllocation.getEndDate()) .allocate(allEffortInput.getEffortDurationValue()); reloadEfforts(); } private void reloadEfforts() { reloadEffortsSameRowForDetailItems(); reloadAllEffort(); fireCellChanged(); } @Override public boolean isSigmoid() { return false; } @Override public boolean isConfigurable() { return false; } }; private IAssignmentFunctionConfiguration manualFunction = new IAssignmentFunctionConfiguration() { @Override public void goToConfigure() { throw new UnsupportedOperationException("Manual allocation is not configurable"); } @Override public String getName() { return AssignmentFunctionName.MANUAL.toString(); } @Override public boolean isTargetedTo(AssignmentFunction function) { return function instanceof ManualFunction; } @Override public void applyOn(ResourceAllocation<?> resourceAllocation) { resourceAllocation.setAssignmentFunctionAndApplyIfNotFlat(ManualFunction.create()); } @Override public boolean isSigmoid() { return false; } @Override public boolean isConfigurable() { return false; } }; private abstract class CommonStretchesConfiguration extends StretchesFunctionConfiguration { @Override protected void assignmentFunctionChanged() { reloadEffortsSameRowForDetailItems(); reloadAllEffort(); fireCellChanged(); } @Override protected ResourceAllocation<?> getAllocation() { return Row.this.getAllocation(); } @Override protected Component getParentOnWhichOpenWindow() { return allEffortInput.getParent(); } } private IAssignmentFunctionConfiguration defaultStretchesFunction = new CommonStretchesConfiguration() { @Override protected String getTitle() { return _("Stretches list"); } @Override protected boolean getChartsEnabled() { return true; } @Override protected StretchesFunctionTypeEnum getType() { return StretchesFunctionTypeEnum.STRETCHES; } @Override public String getName() { return AssignmentFunctionName.STRETCHES.toString(); } }; private IAssignmentFunctionConfiguration stretchesWithInterpolation = new CommonStretchesConfiguration() { @Override protected String getTitle() { return _("Stretches with Interpolation"); } @Override protected boolean getChartsEnabled() { return false; } @Override protected StretchesFunctionTypeEnum getType() { return StretchesFunctionTypeEnum.INTERPOLATED; } @Override public String getName() { return AssignmentFunctionName.INTERPOLATION.toString(); } }; private IAssignmentFunctionConfiguration sigmoidFunction = new IAssignmentFunctionConfiguration() { @Override public void goToConfigure() { throw new UnsupportedOperationException("Sigmoid function is not configurable"); } @Override public String getName() { return AssignmentFunctionName.SIGMOID.toString(); } @Override public boolean isTargetedTo(AssignmentFunction function) { return function instanceof SigmoidFunction; } @Override public void applyOn(ResourceAllocation<?> resourceAllocation) { resourceAllocation.setAssignmentFunctionAndApplyIfNotFlat(SigmoidFunction.create()); reloadEfforts(); } private void reloadEfforts() { reloadEffortsSameRowForDetailItems(); reloadAllEffort(); fireCellChanged(); } @Override public boolean isSigmoid() { return true; } @Override public boolean isConfigurable() { return false; } }; private IAssignmentFunctionConfiguration[] functions = { flat, manualFunction, defaultStretchesFunction, stretchesWithInterpolation, sigmoidFunction }; private boolean isLimiting; private Row(IMessagesForUser messages, AdvancedAllocationController.Restriction restriction, String name, int level, List<? extends ResourceAllocation<?>> allocations, boolean limiting, TaskElement task) { this.messages = messages; this.restriction = restriction; this.name = name; this.level = level; this.isLimiting = limiting; this.task = task; this.aggregate = AggregateOfResourceAllocations.createFromSatisfied(new ArrayList<>(allocations)); } static Row createRow(IMessagesForUser messages, AdvancedAllocationController.Restriction restriction, String name, int level, List<? extends ResourceAllocation<?>> allocations, String description, boolean limiting, TaskElement task) { Row newRow = new Row(messages, restriction, name, level, allocations, limiting, task); newRow.setDescription(description); return newRow; } static Row createRow(IMessagesForUser messages, AdvancedAllocationController.Restriction restriction, String name, int level, List<? extends ResourceAllocation<?>> allocations, boolean limiting, TaskElement task) { return new Row(messages, restriction, name, level, allocations, limiting, task); } void listenTo(Collection<Row> rows) { rows.forEach(this::listenTo); } private void listenTo(Row row) { row.add(new CellChangedListener() { @Override public void changeOnGlobal() { reloadAllEffort(); reloadEffortsSameRowForDetailItems(); } @Override public void changeOn(DetailItem detailItem) { Component component = componentsByDetailItem.get(detailItem); if (component == null) return; reloadEffortOnInterval(component, detailItem); reloadAllEffort(); } }); } private void add(CellChangedListener listener) { listeners.add(listener); } private void fireCellChanged(DetailItem detailItem) { for (CellChangedListener cellChangedListener : listeners) { cellChangedListener.changeOn(detailItem); } } private void fireCellChanged() { for (CellChangedListener cellChangedListener : listeners) { cellChangedListener.changeOnGlobal(); } } Component getAllEffort() { if ( allEffortInput == null ) { allEffortInput = buildSumAllEffort(); reloadAllEffort(); addListenerIfNeeded(allEffortInput); } return allEffortInput; } private EffortDurationBox buildSumAllEffort() { EffortDurationBox box = isEffortDurationBoxDisabled() ? EffortDurationBox.notEditable() : new EffortDurationBox(); box.setWidth("40px"); return box; } private void addListenerIfNeeded(Component allEffortComponent) { if ( isEffortDurationBoxDisabled() ) return; final EffortDurationBox effortDurationBox = (EffortDurationBox) allEffortComponent; effortDurationBox.addEventListener(Events.ON_CHANGE, (EventListener) event -> { EffortDuration value = effortDurationBox.getEffortDurationValue(); ResourceAllocation<?> resourceAllocation = getAllocation(); resourceAllocation .withPreviousAssociatedResources() .onIntervalWithinTask(resourceAllocation.getStartDate(), resourceAllocation.getEndDate()) .allocate(value); AssignmentFunction assignmentFunction = resourceAllocation.getAssignmentFunction(); if ( assignmentFunction != null ) assignmentFunction.applyTo(resourceAllocation); fireCellChanged(); reloadEffortsSameRowForDetailItems(); reloadAllEffort(); }); } private boolean isEffortDurationBoxDisabled() { return isGroupingRow() || isLimiting || task.isUpdatedFromTimesheets(); } private void reloadEffortsSameRowForDetailItems() { for (Entry<DetailItem, Component> entry : componentsByDetailItem.entrySet()) { reloadEffortOnInterval(entry.getValue(), entry.getKey()); } } private void reloadAllEffort() { if ( allEffortInput == null ) return; EffortDuration allEffort = aggregate.getTotalEffort(); allEffortInput.setValue(allEffort); Clients.clearWrongValue(allEffortInput); if ( isEffortDurationBoxDisabled() ) allEffortInput.setDisabled(true); if ( restriction.isInvalidTotalEffort(allEffort) ) restriction.showInvalidEffort(messages, allEffort); } Component getFunction() { if ( isGroupingRow() ) return new Label(); else if ( isLimiting ) return new Label(_("Queue-based assignment")); else { if ( hboxAssignmentFunctionsCombo == null ) initializeAssignmentFunctionsCombo(); return hboxAssignmentFunctionsCombo; } } private void initializeAssignmentFunctionsCombo() { hboxAssignmentFunctionsCombo = new Hbox(); assignmentFunctionsCombo = new AssignmentFunctionListbox(functions, getAllocation().getAssignmentFunction()); hboxAssignmentFunctionsCombo.appendChild(assignmentFunctionsCombo); assignmentFunctionsConfigureButton = getAssignmentFunctionsConfigureButton(assignmentFunctionsCombo); hboxAssignmentFunctionsCombo.appendChild(assignmentFunctionsConfigureButton); // Disable if task is updated from timesheets assignmentFunctionsCombo.setDisabled(task.isUpdatedFromTimesheets()); assignmentFunctionsConfigureButton .setDisabled(assignmentFunctionsConfigureButton.isDisabled() || task.isUpdatedFromTimesheets()); } /** * Encapsulates the logic of the combobox used for selecting what type of assignment function to apply. * * @author Diego Pino García <dpino@igalia.com> */ private class AssignmentFunctionListbox extends Listbox { private Listitem previousListitem; AssignmentFunctionListbox(IAssignmentFunctionConfiguration[] functions, AssignmentFunction initialValue) { for (IAssignmentFunctionConfiguration each : functions) { Listitem listitem = listItem(each); this.appendChild(listitem); if ( each.isTargetedTo(initialValue) ) selectItemAndSavePreviousValue(listitem); } this.addEventListener(Events.ON_SELECT, onSelectListbox()); this.setMold("select"); this.setStyle("font-size: 10px"); } private void selectItemAndSavePreviousValue(Listitem listitem) { setSelectedItem(listitem); previousListitem = listitem; } private Listitem listItem(IAssignmentFunctionConfiguration assignmentFunction) { Listitem listitem = new Listitem(_(assignmentFunction.getName())); listitem.setValue(assignmentFunction); return listitem; } private EventListener onSelectListbox() { return event -> { IAssignmentFunctionConfiguration function = getSelectedItem().getValue(); // Cannot apply function if task contains consolidated day assignments final ResourceAllocation<?> resourceAllocation = getAllocation(); if ( function.isSigmoid() && !resourceAllocation.getConsolidatedAssignments().isEmpty() ) { showCannotApplySigmoidFunction(); setSelectedItem(getPreviousListitem()); return; } // User didn't accept if ( showConfirmChangeFunctionDialog() != Messagebox.YES ) { setSelectedItem(getPreviousListitem()); return; } // Apply assignment function setPreviousListitem(getSelectedItem()); function.applyOn(resourceAllocation); updateAssignmentFunctionsConfigureButton( assignmentFunctionsConfigureButton, function.isConfigurable()); }; } private Listitem getPreviousListitem() { return previousListitem; } private void setPreviousListitem(Listitem previousListitem) { this.previousListitem = previousListitem; } private void showCannotApplySigmoidFunction() { Messagebox.show( _("Task contains consolidated progress. Cannot apply sigmoid function."), _("Error"), Messagebox.OK, Messagebox.ERROR); } private int showConfirmChangeFunctionDialog() throws InterruptedException { return Messagebox.show( _("Assignment function will be changed. Are you sure?"), _("Confirm change"), Messagebox.YES | Messagebox.NO, Messagebox.QUESTION); } private void setSelectedFunction(String functionName) { List<Listitem> children = getChildren(); for (Listitem item : children) { IAssignmentFunctionConfiguration function = item.getValue(); if ( function.getName().equals(functionName) ) setSelectedItem(item); } } } private Button getAssignmentFunctionsConfigureButton(final Listbox assignmentFunctionsListbox) { Button button = Util.createEditButton(event -> { IAssignmentFunctionConfiguration configuration = assignmentFunctionsListbox.getSelectedItem().getValue(); configuration.goToConfigure(); }); IAssignmentFunctionConfiguration configuration = assignmentFunctionsListbox.getSelectedItem().getValue(); updateAssignmentFunctionsConfigureButton(button, configuration.isConfigurable()); return button; } private void updateAssignmentFunctionsConfigureButton(Button button, boolean configurable) { if ( configurable ) { button.setTooltiptext(_("Configure")); button.setDisabled(false); } else { button.setTooltiptext(_("Not configurable")); button.setDisabled(true); } } Component getNameLabel() { if ( nameLabel == null ) { nameLabel = new Label(); nameLabel.setValue(name); if ( !StringUtils.isBlank(description) ) nameLabel.setTooltiptext(description); else nameLabel.setTooltiptext(name); nameLabel.setSclass("level" + level); } return nameLabel; } private EffortDuration getEffortForDetailItem(DetailItem item) { DateTime startDate = item.getStartDate(); DateTime endDate = item.getEndDate(); return this.aggregate.effortBetween(startDate.toLocalDate(), endDate.toLocalDate()); } Component effortOnInterval(DetailItem item) { Component result = cannotBeEdited(item) ? new Label() : disableIfNeeded(item, new EffortDurationBox()); reloadEffortOnInterval(result, item); componentsByDetailItem.put(item, result); addListenerIfNeeded(item, result); return result; } private boolean cannotBeEdited(DetailItem item) { return isGroupingRow() || doesNotIntersectWithTask(item) || isBeforeLatestConsolidation(item) || task.isUpdatedFromTimesheets(); } private EffortDurationBox disableIfNeeded(DetailItem item, EffortDurationBox effortDurationBox) { effortDurationBox.setDisabled(restriction.isDisabledEditionOn(item)); return effortDurationBox; } private void addListenerIfNeeded(final DetailItem item, final Component component) { if ( cannotBeEdited(item) ) return; component.addEventListener(Events.ON_CHANGE, (EventListener) event -> { EffortDurationBox effortBox = (EffortDurationBox) component; EffortDuration value = effortBox.getEffortDurationValue(); LocalDate startDate = restriction.limitStartDate(item.getStartDate().toLocalDate()); LocalDate endDate = restriction.limitEndDate(item.getEndDate().toLocalDate()); changeAssignmentFunctionToManual(); getAllocation() .withPreviousAssociatedResources() .onIntervalWithinTask(startDate, endDate) .allocate(value); fireCellChanged(item); effortBox.setRawValue(getEffortForDetailItem(item)); reloadAllEffort(); }); } private void changeAssignmentFunctionToManual() { assignmentFunctionsCombo.setSelectedFunction(AssignmentFunctionName.MANUAL.toString()); ResourceAllocation<?> allocation = getAllocation(); if ( !(allocation.getAssignmentFunction() instanceof ManualFunction) ) allocation.setAssignmentFunctionAndApplyIfNotFlat(ManualFunction.create()); } private void reloadEffortOnInterval(Component component, DetailItem item) { if ( cannotBeEdited(item) ) { Label label = (Label) component; label.setValue(getEffortForDetailItem(item).toFormattedString()); label.setClass(getLabelClassFor(item)); } else { EffortDurationBox effortDurationBox = (EffortDurationBox) component; effortDurationBox.setValue(getEffortForDetailItem(item)); if ( isLimiting ) { effortDurationBox.setDisabled(true); effortDurationBox.setSclass(" limiting"); } } } private String getLabelClassFor(DetailItem item) { if ( isGroupingRow() ) return "calculated-hours"; if ( doesNotIntersectWithTask(item) ) return "unmodifiable-hours"; if ( isBeforeLatestConsolidation(item) ) return "consolidated-hours"; return ""; } private boolean doesNotIntersectWithTask(DetailItem item) { return isBeforeTaskStartDate(item) || isAfterTaskEndDate(item); } private boolean isBeforeTaskStartDate(DetailItem item) { return task.getIntraDayStartDate().compareTo(item.getEndDate().toLocalDate()) >= 0; } private boolean isAfterTaskEndDate(DetailItem item) { return task.getIntraDayEndDate().compareTo(item.getStartDate().toLocalDate()) <= 0; } private boolean isBeforeLatestConsolidation(DetailItem item) { if ( !(task).hasConsolidations() ) return false; LocalDate d = ((Task) task).getFirstDayNotConsolidated().getDate(); DateTime firstDayNotConsolidated = new DateTime(d.getYear(), d.getMonthOfYear(), d.getDayOfMonth(), 0, 0, 0, 0); return item.getStartDate().compareTo(firstDayNotConsolidated) < 0; } private ResourceAllocation<?> getAllocation() { if ( isGroupingRow() ) throw new IllegalStateException("is grouping row"); return aggregate.getAllocationsSortedByStartDate().get(0); } boolean isGroupingRow() { return level == 0; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getName() { return name; } public void setName(String name) { this.name = name; } }