org.libreplan.web.planner.allocation.AdvancedAllocationController.java Source code

Java tutorial

Introduction

Here is the source code for org.libreplan.web.planner.allocation.AdvancedAllocationController.java

Source

/*
 * This file is part of LibrePlan
 *
 * Copyright (C) 2009-2010 Fundacin para o Fomento da Calidade Industrial e
 *                         Desenvolvemento Tecnolxico 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 Gonzlez Fernndez <ogonzalez@igalia.com>
 * @author Diego Pino Garca <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 Garca <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;
    }

}