/* * 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-2012 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.order; import org.apache.commons.lang3.Validate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.libreplan.business.common.AdHocTransactionService; import org.libreplan.business.common.IAdHocTransactionService; import org.libreplan.business.common.IOnTransaction; import org.libreplan.business.common.Registry; import org.libreplan.business.common.exceptions.InstanceNotFoundException; import org.libreplan.business.orders.daos.IOrderDAO; import org.libreplan.business.orders.entities.HoursGroup; import org.libreplan.business.orders.entities.Order; import org.libreplan.business.orders.entities.OrderElement; import org.libreplan.business.orders.entities.OrderStatusEnum; import org.libreplan.business.planner.chart.ContiguousDaysLine; import org.libreplan.business.planner.entities.IOrderEarnedValueCalculator; import org.libreplan.business.planner.entities.IOrderResourceLoadCalculator; import org.libreplan.business.planner.entities.TaskElement; import org.libreplan.business.resources.entities.CriterionSatisfaction; import org.libreplan.business.resources.entities.Resource; import org.libreplan.business.users.daos.IOrderAuthorizationDAO; import org.libreplan.business.users.daos.IUserDAO; import org.libreplan.business.users.entities.OrderAuthorization; import org.libreplan.business.users.entities.OrderAuthorizationType; import org.libreplan.business.users.entities.User; import org.libreplan.business.users.entities.UserRole; import org.libreplan.business.workingday.EffortDuration; import org.libreplan.web.calendars.BaseCalendarModel; import org.libreplan.web.common.ConfirmCloseUtil; import org.libreplan.web.common.FilterUtils; import org.libreplan.web.common.ViewSwitcher; import org.libreplan.web.planner.adaptplanning.IAdaptPlanningCommand; import org.libreplan.web.planner.advances.AdvanceAssignmentPlanningController; import org.libreplan.web.planner.advances.IAdvanceAssignmentPlanningCommand; import org.libreplan.web.planner.allocation.IAdvancedAllocationCommand; import org.libreplan.web.planner.allocation.IResourceAllocationCommand; import org.libreplan.web.planner.calendar.CalendarAllocationController; import org.libreplan.web.planner.calendar.ICalendarAllocationCommand; import org.libreplan.web.planner.chart.Chart; import org.libreplan.web.planner.chart.EarnedValueChartFiller; import org.libreplan.web.planner.chart.EarnedValueChartFiller.EarnedValueType; import org.libreplan.web.planner.chart.IChartFiller; import org.libreplan.web.planner.chart.LoadChartFiller; import org.libreplan.web.planner.consolidations.AdvanceConsolidationController; import org.libreplan.web.planner.consolidations.IAdvanceConsolidationCommand; import org.libreplan.web.planner.milestone.IAddMilestoneCommand; import org.libreplan.web.planner.milestone.IDeleteMilestoneCommand; import org.libreplan.web.planner.order.ISaveCommand.IAfterSaveListener; import org.libreplan.web.planner.order.PlanningStateCreator.PlanningState; import org.libreplan.web.planner.reassign.IReassignCommand; import org.libreplan.web.planner.taskedition.AdvancedAllocationTaskController; import org.libreplan.web.planner.taskedition.EditTaskController; import org.libreplan.web.planner.taskedition.ITaskPropertiesCommand; import org.libreplan.web.print.CutyPrint; import org.libreplan.web.security.SecurityUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.zkforge.timeplot.Plotinfo; import org.zkforge.timeplot.Timeplot; import org.zkoss.ganttz.IChartVisibilityChangedListener; import org.zkoss.ganttz.Planner; import org.zkoss.ganttz.adapters.PlannerConfiguration; import org.zkoss.ganttz.adapters.PlannerConfiguration.IPrintAction; import org.zkoss.ganttz.adapters.PlannerConfiguration.IReloadChartListener; import org.zkoss.ganttz.extensions.ICommand; import org.zkoss.ganttz.extensions.ICommandOnTask; import org.zkoss.ganttz.extensions.IContext; import org.zkoss.ganttz.extensions.IContextWithPlannerTask; import org.zkoss.ganttz.timetracker.TimeTracker; import org.zkoss.ganttz.util.Interval; import org.zkoss.ganttz.util.ProfilingLogFactory; import org.zkoss.zk.ui.Executions; import org.zkoss.zk.ui.WrongValueException; import org.zkoss.zk.ui.event.Events; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.zkoss.ganttz.timetracker.zoom.IDetailItemModifier; import org.zkoss.ganttz.timetracker.zoom.IZoomLevelChangedListener; import org.zkoss.ganttz.timetracker.zoom.SeveralModifiers; import org.zkoss.ganttz.timetracker.zoom.ZoomLevel; import org.zkoss.zul.Checkbox; import org.zkoss.zul.Constraint; import org.zkoss.zul.Datebox; import org.zkoss.zul.Div; import org.zkoss.zul.Hbox; import org.zkoss.zul.Label; import org.zkoss.zul.Messagebox; import org.zkoss.zul.Tab; import org.zkoss.zul.Tabbox; import org.zkoss.zul.Tabpanel; import org.zkoss.zul.Tabpanels; import org.zkoss.zul.Tabs; import org.zkoss.zul.Vbox; import static org.libreplan.business.planner.chart.ContiguousDaysLine.toSortedMap; import static org.libreplan.business.planner.chart.ContiguousDaysLine.min; import static org.libreplan.business.planner.chart.ContiguousDaysLine.sum; import static org.libreplan.web.I18nHelper._; /** * @author Óscar González Fernández <ogonzalez@igalia.com> * @author Manuel Rego Casasnovas <rego@igalia.com> * @author Lorenzo Tilve Álvaro <ltilve@igalia.com> * @author Diego Pino García <dpino@igalia.com> */ @Component @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class OrderPlanningModel implements IOrderPlanningModel { private static final Log LOG = LogFactory.getLog(OrderPlanningModel.class); private static final Log PROFILING_LOG = ProfilingLogFactory.getLog(OrderPlanningModel.class); private static final String CENTER = "center"; private static final String INDICATOR = "indicator"; @Autowired private IOrderDAO orderDAO; @Autowired private IUserDAO userDAO; @Autowired private IOrderAuthorizationDAO orderAuthorizationDAO; @Autowired private IAdHocTransactionService transactionService; @Autowired private IReassignCommand reassignCommand; @Autowired private IAdaptPlanningCommand adaptPlanningCommand; @Autowired private IResourceAllocationCommand resourceAllocationCommand; @Autowired private IAdvancedAllocationCommand advancedAllocationCommand; @Autowired private IAddMilestoneCommand addMilestoneCommand; @Autowired private IDeleteMilestoneCommand deleteMilestoneCommand; @Autowired private ITaskPropertiesCommand taskPropertiesCommand; @Autowired private IAdvanceConsolidationCommand advanceConsolidationCommand; @Autowired private IAdvanceAssignmentPlanningCommand advanceAssignmentPlanningCommand; @Autowired private ICalendarAllocationCommand calendarAllocationCommand; @Autowired private ISubcontractCommand subcontractCommand; @Autowired private IOrderEarnedValueCalculator earnedValueCalculator; @Autowired private IOrderResourceLoadCalculator resourceLoadCalculator; @Autowired private PlanningStateCreator planningStateCreator; private List<IZoomLevelChangedListener> keepAliveZoomListeners = new ArrayList<>(); private PlanningState planningState; private List<Checkbox> earnedValueChartConfigurationCheckboxes = new ArrayList<>(); private List<IChartVisibilityChangedListener> keepAliveChartVisibilityListeners = new ArrayList<>(); private Planner planner; private String tabSelected = "load_tab"; private OrderEarnedValueChartFiller earnedValueChartFiller; private Vbox earnedValueChartLegendContainer; private Datebox earnedValueChartLegendDatebox; private Chart earnedValueChart; public static <T extends Collection<Resource>> T loadRequiredDataFor(T resources) { for (Resource each : resources) { reattachCalendarFor(each); // Loading criterions so there are no repeated instances forceLoadOfCriterions(each); } return resources; } private static void reattachCalendarFor(Resource each) { if (each.getCalendar() != null) { BaseCalendarModel.forceLoadBaseCalendar(each.getCalendar()); } } static void forceLoadOfCriterions(Resource resource) { Set<CriterionSatisfaction> criterionSatisfactions = resource.getCriterionSatisfactions(); for (CriterionSatisfaction each : criterionSatisfactions) { each.getCriterion().getName(); each.getCriterion().getType(); } } public static ZoomLevel calculateDefaultLevel(PlannerConfiguration<TaskElement> configuration) { if (configuration.getData().isEmpty()) { return ZoomLevel.DETAIL_ONE; } TaskElement earliest = Collections.min(configuration.getData(), TaskElement.getByStartDateComparator()); TaskElement latest = Collections.max(configuration.getData(), TaskElement.getByEndAndDeadlineDateComparator()); LocalDate startDate = earliest.getStartAsLocalDate(); LocalDate endDate = latest.getBiggestAmongEndOrDeadline(); return ZoomLevel.getDefaultZoomByDates(startDate, endDate); } private static class NullSeparatorCommandOnTask<T> implements ICommandOnTask<T> { @Override public String getName() { return "separator"; } @Override public String getIcon() { return null; } @Override public boolean isApplicableTo(T task) { return true; } @Override public void doAction(IContextWithPlannerTask<T> context, T task) { // Do nothing } } @Override @Transactional(readOnly = true) public void setConfigurationToPlanner(final Planner planner, Order order, ViewSwitcher switcher, EditTaskController editTaskController, AdvancedAllocationTaskController advancedAllocationTaskController, AdvanceAssignmentPlanningController advanceAssignmentPlanningController, AdvanceConsolidationController advanceConsolidationController, CalendarAllocationController calendarAllocationController, List<ICommand<TaskElement>> additional) { long time = System.currentTimeMillis(); this.planner = planner; planningState = createPlanningStateFor(order); PlannerConfiguration<TaskElement> configuration = planningState.getConfiguration(); PROFILING_LOG.debug("load data and create configuration took: " + (System.currentTimeMillis() - time) + " ms"); User user; try { user = this.userDAO.findByLoginName(SecurityUtils.getSessionUserLoginName()); } catch (InstanceNotFoundException e) { throw new RuntimeException(e); } configuration.setExpandPlanningViewCharts(user.isExpandOrderPlanningViewCharts()); addAdditional(additional, configuration); planner.setInitialZoomLevel(getZoomLevel(configuration, order)); final boolean writingAllowed = isWritingAllowedOnOrder(); ISaveCommand saveCommand = setupSaveCommand(configuration, writingAllowed); setupEditingCapabilities(configuration, writingAllowed); configuration.addGlobalCommand(buildReassigningCommand()); configuration.addGlobalCommand(buildCancelEditionCommand()); configuration.addGlobalCommand(buildAdaptPlanningCommand()); NullSeparatorCommandOnTask<TaskElement> separator = new NullSeparatorCommandOnTask<>(); final IResourceAllocationCommand resourceAllocationCommand = buildResourceAllocationCommand(editTaskController); final IAdvanceAssignmentPlanningCommand advanceAssignmentPlanningCommand = buildAdvanceAssignmentPlanningCommand(advanceAssignmentPlanningController); // Build context menu configuration.addCommandOnTask(buildMilestoneCommand()); configuration.addCommandOnTask(buildDeleteMilestoneCommand()); configuration.addCommandOnTask(separator); configuration.addCommandOnTask(buildTaskPropertiesCommand(editTaskController)); configuration.addCommandOnTask(resourceAllocationCommand); configuration.addCommandOnTask(buildAdvancedAllocationCommand(advancedAllocationTaskController)); configuration.addCommandOnTask(buildSubcontractCommand(editTaskController)); configuration.addCommandOnTask(buildCalendarAllocationCommand(calendarAllocationController)); configuration.addCommandOnTask(separator); configuration.addCommandOnTask(advanceAssignmentPlanningCommand); configuration.addCommandOnTask(buildAdvanceConsolidationCommand(advanceConsolidationController)); configuration.setDoubleClickCommand(resourceAllocationCommand); addPrintSupport(configuration, order); Tabbox chartComponent = new Tabbox(); chartComponent.setOrient("vertical"); chartComponent.setHeight("200px"); appendTabs(chartComponent); configuration.setChartComponent(chartComponent); configureModifiers(planningState.getOrder(), configuration); long setConfigurationTime = System.currentTimeMillis(); planner.setConfiguration(configuration); PROFILING_LOG.debug("setConfiguration on planner took: " + (System.currentTimeMillis() - setConfigurationTime) + " ms"); long preparingChartsAndMisc = System.currentTimeMillis(); setupZoomLevelListener(planner, order); // Prepare tabpanels Tabpanels chartTabpanels = new Tabpanels(); // Create 'Load' tab Timeplot chartLoadTimeplot = createEmptyTimeplot(); chartTabpanels.appendChild(createLoadTimeplotTab(chartLoadTimeplot)); // Create 'Earned value' tab Timeplot chartEarnedValueTimeplot = createEmptyTimeplot(); this.earnedValueChartFiller = createOrderEarnedValueChartFiller(planner.getTimeTracker()); chartTabpanels.appendChild(createEarnedValueTab(chartEarnedValueTimeplot, earnedValueChartFiller)); // Append tab panels chartComponent.appendChild(chartTabpanels); ChangeHooker changeHooker = new ChangeHooker(configuration, saveCommand); setupLoadChart(chartLoadTimeplot, planner, changeHooker); setupEarnedValueChart(chartEarnedValueTimeplot, earnedValueChartFiller, planner, changeHooker); setupAdvanceAssignmentPlanningController(planner, advanceAssignmentPlanningController); PROFILING_LOG.debug("preparing charts and miscellaneous took: " + (System.currentTimeMillis() - preparingChartsAndMisc) + " ms"); // Calculate critical path progress, needed for 'Project global progress' chart in Dashboard view planner.addGraphChangeListenersFromConfiguration(configuration); long overallProgressContentTime = System.currentTimeMillis(); PROFILING_LOG.debug("overallProgressContent took: " + (System.currentTimeMillis() - overallProgressContentTime)); } private ZoomLevel getZoomLevel(PlannerConfiguration<TaskElement> configuration, Order order) { ZoomLevel sessionZoom = FilterUtils.readZoomLevel(order); if ( sessionZoom != null ) { return sessionZoom; } return OrderPlanningModel.calculateDefaultLevel(configuration); } private void setupZoomLevelListener(Planner planner, Order order) { planner.getTimeTracker().addZoomListener(getSessionZoomLevelListener(order)); } private IZoomLevelChangedListener getSessionZoomLevelListener(final Order order) { IZoomLevelChangedListener zoomListener = detailLevel -> FilterUtils.writeZoomLevel(order, detailLevel); keepAliveZoomListeners.add(zoomListener); return zoomListener; } private void setupAdvanceAssignmentPlanningController( final Planner planner, AdvanceAssignmentPlanningController advanceAssignmentPlanningController) { advanceAssignmentPlanningController.setReloadEarnedValueListener( () -> Registry.getTransactionService().runOnReadOnlyTransaction( (IOnTransaction<Void>) () -> { if ( isExecutingOutsideZKExecution() ) { return null; } if ( planner.isVisibleChart() ) { // Update earned value chart earnedValueChart.fillChart(); // Update earned value legend updateEarnedValueChartLegend(); } return null; })); } private Timeplot createEmptyTimeplot() { Timeplot timeplot = new Timeplot(); timeplot.appendChild(new Plotinfo()); return timeplot; } private Tabpanel createLoadTimeplotTab(Timeplot loadChart) { Tabpanel result = new Tabpanel(); appendLoadChartAndLegend(result, loadChart); return result; } private void appendLoadChartAndLegend(Tabpanel loadChartPanel, Timeplot loadChart) { Hbox hbox = new Hbox(); hbox.appendChild(getLoadChartLegend()); hbox.setSclass("load-chart"); Div div = new Div(); div.appendChild(loadChart); div.setSclass("plannergraph"); div.setHeight("170px"); hbox.appendChild(div); loadChartPanel.appendChild(hbox); } private OrderEarnedValueChartFiller createOrderEarnedValueChartFiller(TimeTracker timeTracker) { OrderEarnedValueChartFiller result = new OrderEarnedValueChartFiller(planningState.getOrder()); result.calculateValues(timeTracker.getRealInterval()); return result; } private Tabpanel createEarnedValueTab( Timeplot chartEarnedValueTimeplot, OrderEarnedValueChartFiller earnedValueChartFiller) { Tabpanel result = new Tabpanel(); appendEarnedValueChartAndLegend(result, chartEarnedValueTimeplot, earnedValueChartFiller); return result; } private void appendEarnedValueChartAndLegend( Tabpanel earnedValueChartPanel, Timeplot chartEarnedValueTimeplot, final OrderEarnedValueChartFiller earnedValueChartFiller) { Vbox vbox = new Vbox(); this.earnedValueChartLegendContainer = vbox; vbox.setClass("legend-container"); vbox.setAlign(CENTER); vbox.setPack(CENTER); Hbox dateHbox = new Hbox(); dateHbox.appendChild(new Label(_("Select date"))); LocalDate initialDateForIndicatorValues = earnedValueChartFiller.initialDateForIndicatorValues(); this.earnedValueChartLegendDatebox = new Datebox(initialDateForIndicatorValues.toDateTimeAtStartOfDay().toDate()); this.earnedValueChartLegendDatebox.setConstraint(dateMustBeInsideVisualizationArea(earnedValueChartFiller)); dateHbox.appendChild(this.earnedValueChartLegendDatebox); appendEventListenerToDateboxIndicators(); vbox.appendChild(dateHbox); vbox.appendChild(getEarnedValueChartConfigurableLegend( earnedValueChartFiller, initialDateForIndicatorValues)); Hbox hbox = new Hbox(); hbox.setSclass("earned-value-chart"); hbox.appendChild(vbox); Div div = new Div(); div.appendChild(chartEarnedValueTimeplot); div.setSclass("plannergraph"); div.setHeight("170px"); hbox.appendChild(div); earnedValueChartPanel.appendChild(hbox); } private void setupLoadChart(Timeplot chartLoadTimeplot, Planner planner, ChangeHooker changeHooker) { Chart loadChart = setupChart( new OrderLoadChartFiller(planningState.getOrder()), chartLoadTimeplot, planner); refillLoadChartWhenNeeded(changeHooker, planner, loadChart, false); } private void setupEarnedValueChart(Timeplot chartEarnedValueTimeplot, OrderEarnedValueChartFiller earnedValueChartFiller, Planner planner, ChangeHooker changeHooker) { earnedValueChart = setupChart(earnedValueChartFiller, chartEarnedValueTimeplot, planner); refillLoadChartWhenNeeded(changeHooker, planner, earnedValueChart, true); setEventListenerConfigurationCheckboxes(earnedValueChart); } enum ChangeTypes { ON_SAVE, ON_RELOAD_CHART_REQUESTED, ON_GRAPH_CHANGED } class ChangeHooker { private PlannerConfiguration<TaskElement> configuration; private ISaveCommand saveCommand; private boolean wrapOnReadOnlyTransaction = false; ChangeHooker(PlannerConfiguration<TaskElement> configuration, final ISaveCommand saveCommand) { Validate.notNull(configuration); this.configuration = configuration; this.saveCommand = saveCommand; } ChangeHooker withReadOnlyTransactionWrapping() { ChangeHooker result = new ChangeHooker(configuration, saveCommand); result.wrapOnReadOnlyTransaction = true; return result; } void hookInto(EnumSet<ChangeTypes> reloadOn, IReloadChartListener reloadChart) { Validate.notNull(reloadChart); hookIntoImpl(wrapIfNeeded(reloadChart), reloadOn); } private IReloadChartListener wrapIfNeeded(IReloadChartListener reloadChart) { return !wrapOnReadOnlyTransaction ? reloadChart : AdHocTransactionService.readOnlyProxy(transactionService, IReloadChartListener.class, reloadChart); } private void hookIntoImpl(IReloadChartListener reloadChart, EnumSet<ChangeTypes> reloadOn) { if ( saveCommand != null && reloadOn.contains(ChangeTypes.ON_GRAPH_CHANGED) ) { hookIntoSaveCommand(reloadChart); } if ( reloadOn.contains(ChangeTypes.ON_RELOAD_CHART_REQUESTED) ) { hookIntoReloadChartRequested(reloadChart); } if ( reloadOn.contains(ChangeTypes.ON_GRAPH_CHANGED) ) { hookIntoGraphChanged(reloadChart); } } private void hookIntoSaveCommand(final IReloadChartListener reloadChart) { IAfterSaveListener afterSaveListener = () -> reloadChart.reloadChart(); saveCommand.addListener(afterSaveListener); } private void hookIntoReloadChartRequested(IReloadChartListener reloadChart) { configuration.addReloadChartListener(reloadChart); } private void hookIntoGraphChanged(final IReloadChartListener reloadChart) { configuration.addPostGraphChangeListener(() -> reloadChart.reloadChart()); } } private void addPrintSupport(PlannerConfiguration<TaskElement> configuration, final Order order) { configuration.setPrintAction(new IPrintAction() { @Override public void doPrint() { CutyPrint.print(order); } @Override public void doPrint(Map<String, String> parameters) { CutyPrint.print(order, parameters); } @Override public void doPrint(Map<String, String> parameters, Planner planner) { CutyPrint.print(order, parameters, planner); } }); } private IDeleteMilestoneCommand buildDeleteMilestoneCommand() { deleteMilestoneCommand.setState(planningState); return deleteMilestoneCommand; } private void configureModifiers(Order orderReloaded, PlannerConfiguration<TaskElement> configuration) { // Either InitDate or DeadLine must be set, depending on forwards or backwards planning configuration.setSecondLevelModifiers(SeveralModifiers.create( BankHolidaysMarker.create(orderReloaded.getCalendar()), createStartDeadlineMarker(orderReloaded))); } private IDetailItemModifier createStartDeadlineMarker(Order order) { final DateTime projectStart = new DateTime(order.getInitDate()); final DateTime deadline = new DateTime(order.getDeadline()); IDetailItemModifier detailItemModifier; if (order.getInitDate() != null) { if (order.getDeadline() != null) { // Both project Start and deadline markers detailItemModifier = (item, z) -> { item.markDeadlineDay(deadline); item.markProjectStart(projectStart); return item; }; } else { // Project Start without deadline detailItemModifier = (item, z) -> { item.markProjectStart(projectStart); return item; }; } } else { // Only project deadline marker detailItemModifier = (item, z) -> { item.markDeadlineDay(deadline); return item; }; } return detailItemModifier; } private void selectTab(String tabName) { tabSelected = tabName; } private void appendTabs(Tabbox chartComponent) { Tabs chartTabs = new Tabs(); chartTabs.appendChild(createTab(_("Load"), "load_tab")); chartTabs.appendChild(createTab(_("Earned value"), "earned_value_tab")); chartComponent.appendChild(chartTabs); chartTabs.setSclass("charts-tabbox"); } private Tab createTab(String name, final String id) { Tab tab = new Tab(name); tab.setId(id); if ( id.equals(tabSelected) ) { tab.setSelected(true); } tab.addEventListener("onClick", event -> selectTab(id)); return tab; } private org.zkoss.zk.ui.Component getLoadChartLegend() { Hbox hbox = new Hbox(); hbox.setClass("legend-container"); hbox.setAlign(CENTER); hbox.setPack(CENTER); Executions.createComponents("/planner/_legendLoadChartOrder.zul", hbox, null); return hbox; } private Constraint dateMustBeInsideVisualizationArea(final OrderEarnedValueChartFiller earnedValueChartFiller) { return (comp, valueObject) -> { Date value = (Date) valueObject; if ( value != null && !EarnedValueChartFiller.includes( earnedValueChartFiller.getIndicatorsDefinitionInterval(), LocalDate.fromDateFields(value)) ) { throw new WrongValueException(comp, _("Date must be inside visualization area")); } }; } private void dateInFutureMessage(Datebox datebox) { Date value = datebox.getValue(); Date today = LocalDate.fromDateFields(new Date()).toDateTimeAtStartOfDay().toDate(); if ( value != null && (value.compareTo(today) > 0) ) { throw new WrongValueException(datebox, _("date in the future")); } } private void appendEventListenerToDateboxIndicators() { earnedValueChartLegendDatebox.addEventListener(Events.ON_CHANGE, event -> { updateEarnedValueChartLegend(); dateInFutureMessage(earnedValueChartLegendDatebox); }); } private void updateEarnedValueChartLegend() { try { // Force the validation again // (getValue alone doesn't work because the result of the validation is cached) earnedValueChartLegendDatebox.setValue(earnedValueChartLegendDatebox.getValue()); } catch (WrongValueException e) { // The user moved the gantt and the legend became out of the visualization area, reset to a correct date earnedValueChartLegendDatebox .setValue(earnedValueChartFiller.initialDateForIndicatorValues().toDateTimeAtStartOfDay().toDate()); } LocalDate date = new LocalDate(earnedValueChartLegendDatebox.getRawValue()); updateEarnedValueChartLegend(date); } private void updateEarnedValueChartLegend(LocalDate date) { for (EarnedValueType type : EarnedValueType.values()) { Label valueLabel = (Label) earnedValueChartLegendContainer.getFellow(type.toString()); valueLabel.setValue(getLabelTextEarnedValueType(earnedValueChartFiller, type, date)); } } private org.zkoss.zk.ui.Component getEarnedValueChartConfigurableLegend( OrderEarnedValueChartFiller earnedValueChartFiller, LocalDate date) { Hbox mainHbox = new Hbox(); mainHbox.setId("indicatorsTable"); Vbox vbox = new Vbox(); vbox.setId("earnedValueChartConfiguration"); vbox.setClass("legend"); Vbox column1 = new Vbox(); Vbox column2 = new Vbox(); column1.setSclass("earned-parameter-column"); column2.setSclass("earned-parameter-column"); int columnNumber = 0; earnedValueChartConfigurationCheckboxes.clear(); for (EarnedValueType type : EarnedValueType.values()) { Checkbox checkbox = new Checkbox(type.getAcronym()); checkbox.setTooltiptext(type.getName()); checkbox.setAttribute(INDICATOR, type); checkbox.setStyle("color: " + type.getColor()); Label valueLabel = new Label(getLabelTextEarnedValueType(earnedValueChartFiller, type, date)); valueLabel.setId(type.toString()); Hbox hbox = new Hbox(); hbox.appendChild(checkbox); hbox.appendChild(valueLabel); columnNumber = columnNumber + 1; switch (columnNumber) { case 1: column1.appendChild(hbox); break; case 2: column2.appendChild(hbox); columnNumber = 0; break; default: break; } earnedValueChartConfigurationCheckboxes.add(checkbox); } Hbox hbox = new Hbox(); hbox.appendChild(column1); hbox.appendChild(column2); vbox.appendChild(hbox); mainHbox.appendChild(vbox); markAsSelectedDefaultIndicators(); return mainHbox; } private String getLabelTextEarnedValueType(OrderEarnedValueChartFiller earnedValueChartFiller, EarnedValueType type, LocalDate date) { BigDecimal value = earnedValueChartFiller.getIndicator(type, date); String units = _("h"); if ( type.equals(EarnedValueType.CPI) || type.equals(EarnedValueType.SPI) ) { value = value.multiply(new BigDecimal(100)); units = "%"; } return value.setScale(0, RoundingMode.HALF_UP) + " " + units; } private void markAsSelectedDefaultIndicators() { for (Checkbox checkbox : earnedValueChartConfigurationCheckboxes) { EarnedValueType type = (EarnedValueType) checkbox.getAttribute(INDICATOR); switch (type) { case BCWS: case ACWP: case BCWP: checkbox.setChecked(true); break; default: checkbox.setChecked(false); break; } } } private void setEventListenerConfigurationCheckboxes(final Chart earnedValueChart) { for (Checkbox checkbox : earnedValueChartConfigurationCheckboxes) { checkbox.addEventListener(Events.ON_CHECK, event -> transactionService.runOnReadOnlyTransaction(() -> { earnedValueChart.fillChart(); // Not necessary to update legend here return null; })); } } private void refillLoadChartWhenNeeded(ChangeHooker changeHooker, final Planner planner, final Chart loadChart, final boolean updateEarnedValueChartLegend) { planner.getTimeTracker().addZoomListener(fillOnZoomChange(loadChart, planner, updateEarnedValueChartLegend)); planner.addChartVisibilityListener(fillOnChartVisibilityChange(loadChart)); changeHooker.withReadOnlyTransactionWrapping().hookInto(EnumSet.allOf(ChangeTypes.class), () -> { if ( isExecutingOutsideZKExecution() ) { return; } if ( planner.isVisibleChart() ) { loadChart.fillChart(); if ( updateEarnedValueChartLegend ) { updateEarnedValueChartLegend(); } } }); } private boolean isExecutingOutsideZKExecution() { return Executions.getCurrent() == null; } private void addAdditional(List<ICommand<TaskElement>> additional, PlannerConfiguration<TaskElement> configuration) { for (ICommand<TaskElement> c : additional) { configuration.addGlobalCommand(c); } } private boolean isWritingAllowedOnOrder() { if ( planningState.getSavedOrderState() == OrderStatusEnum.STORED && planningState.getOrder().getState() == OrderStatusEnum.STORED ) { // STORED orders can't be saved, independently of user permissions return false; } if ( SecurityUtils.isSuperuserOrUserInRoles(UserRole.ROLE_EDIT_ALL_PROJECTS) ) { return true; } return thereIsWriteAuthorizationFor(planningState.getOrder()); } private boolean thereIsWriteAuthorizationFor(Order order) { String loginName = SecurityUtils.getSessionUserLoginName(); try { User user = userDAO.findByLoginName(loginName); for (OrderAuthorization authorization : orderAuthorizationDAO.listByOrderUserAndItsProfiles(order, user)) { if ( authorization.getAuthorizationType() == OrderAuthorizationType.WRITE_AUTHORIZATION ) { return true; } } } catch (InstanceNotFoundException e) { LOG.warn("there isn't a logged user for:" + loginName, e); // This case shouldn't happen, we continue anyway disabling the save button } return false; } private ISaveCommand setupSaveCommand(PlannerConfiguration<TaskElement> configuration, boolean writingAllowed) { ISaveCommand result = planningState.getSaveCommand(); if (!writingAllowed) { result.setDisabled(true); } configuration.addGlobalCommand(result); return result; } private void setupEditingCapabilities(PlannerConfiguration<TaskElement> configuration, boolean writingAllowed) { configuration.setAddingDependenciesEnabled(writingAllowed); configuration.setEditingDatesEnabled(writingAllowed); configuration.setMovingTasksEnabled(writingAllowed); configuration.setResizingTasksEnabled(writingAllowed); } private ICalendarAllocationCommand buildCalendarAllocationCommand( CalendarAllocationController calendarAllocationController) { calendarAllocationCommand.setCalendarAllocationController(calendarAllocationController); return calendarAllocationCommand; } private ITaskPropertiesCommand buildTaskPropertiesCommand(EditTaskController editTaskController) { taskPropertiesCommand.initialize(editTaskController, planningState); return taskPropertiesCommand; } private IAdvanceAssignmentPlanningCommand buildAdvanceAssignmentPlanningCommand( AdvanceAssignmentPlanningController advanceAssignmentPlanningController) { advanceAssignmentPlanningCommand.initialize(advanceAssignmentPlanningController, planningState); return advanceAssignmentPlanningCommand; } private IAdvanceConsolidationCommand buildAdvanceConsolidationCommand( AdvanceConsolidationController advanceConsolidationController) { advanceConsolidationCommand.initialize(advanceConsolidationController, planningState); return advanceConsolidationCommand; } private IAddMilestoneCommand buildMilestoneCommand() { return addMilestoneCommand; } private IResourceAllocationCommand buildResourceAllocationCommand(EditTaskController editTaskController) { resourceAllocationCommand.initialize(editTaskController, planningState); return resourceAllocationCommand; } private IAdvancedAllocationCommand buildAdvancedAllocationCommand( AdvancedAllocationTaskController advancedAllocationTaskController) { advancedAllocationCommand.initialize(advancedAllocationTaskController, planningState); return advancedAllocationCommand; } private ICommand<TaskElement> buildReassigningCommand() { reassignCommand.setState(planningState); return reassignCommand; } private ICommand<TaskElement> buildAdaptPlanningCommand() { adaptPlanningCommand.setState(planningState); return adaptPlanningCommand; } private ICommand<TaskElement> buildCancelEditionCommand() { return new ICommand<TaskElement>() { @Override public String getName() { return _("Cancel"); } @Override public void doAction(IContext<TaskElement> context) { Messagebox.show( _("Unsaved changes will be lost. Are you sure?"), _("Confirm exit dialog"), Messagebox.OK | Messagebox.CANCEL, Messagebox.QUESTION, evt -> { if ("onOK".equals(evt.getName())) { ConfirmCloseUtil.resetConfirmClose(); Executions.sendRedirect("/planner/index.zul;company_scheduling"); } }); } @Override public String getImage() { return "/common/img/ico_back.png"; } @Override public boolean isDisabled() { return false; } @Override public boolean isPlannerCommand() { return false; } }; } private Chart setupChart(IChartFiller loadChartFiller, Timeplot chartComponent, Planner planner) { TimeTracker timeTracker = planner.getTimeTracker(); Chart result = new Chart(chartComponent, loadChartFiller, timeTracker); result.setZoomLevel(planner.getZoomLevel()); if (planner.isVisibleChart()) { result.fillChart(); } return result; } private IChartVisibilityChangedListener fillOnChartVisibilityChange(final Chart loadChart) { IChartVisibilityChangedListener chartVisibilityChangedListener = visible -> transactionService.runOnReadOnlyTransaction(() -> { if (visible) { loadChart.fillChart(); } return null; }); keepAliveChartVisibilityListeners.add(chartVisibilityChangedListener); return chartVisibilityChangedListener; } private IZoomLevelChangedListener fillOnZoomChange(final Chart loadChart, final Planner planner, final boolean updateEarnedValueChartLegend) { IZoomLevelChangedListener zoomListener = detailLevel -> { loadChart.setZoomLevel(detailLevel); transactionService.runOnReadOnlyTransaction((IOnTransaction<Void>) () -> { if (planner.isVisibleChart()) { loadChart.fillChart(); if (updateEarnedValueChartLegend) { updateEarnedValueChartLegend(); } } return null; }); }; keepAliveZoomListeners.add(zoomListener); return zoomListener; } private PlanningState createPlanningStateFor(Order order) { return planningStateCreator.retrieveOrCreate(planner.getDesktop(), order, planningState1 -> { planningState1.reattach(); planningState1.reassociateResourcesWithSession(); }); } /** * Calculates 'Resource Load' values and set them in the Order 'Resource Load' chart. * * @author Óscar González Fernández <ogonzalez@igalia.com> * @author Diego Pino García <dpino@igalia.com> */ private class OrderLoadChartFiller extends LoadChartFiller { /** Soft green */ private static final String COLOR_ASSIGNED_LOAD_GLOBAL = "#E0F3D3"; /** Soft red */ private static final String COLOR_OVERLOAD_GLOBAL = "#FFD4C2"; private final Order order; public OrderLoadChartFiller(Order orderReloaded) { this.order = orderReloaded; } @Override protected String getOptionalJavascriptCall() { return "ganttz.GanttPanel.getInstance().timeplotContainerRescroll()"; } @Override protected Plotinfo[] getPlotInfo(Interval interval) { resourceLoadCalculator.setOrder(order, planningState.getAssignmentsCalculator()); ContiguousDaysLine<EffortDuration> maxCapacityOnResources = resourceLoadCalculator.getMaxCapacityOnResources(); ContiguousDaysLine<EffortDuration> orderLoad = resourceLoadCalculator.getOrderLoad(); ContiguousDaysLine<EffortDuration> allLoad = resourceLoadCalculator.getAllLoad(); ContiguousDaysLine<EffortDuration> orderOverload = resourceLoadCalculator.getOrderOverload(); ContiguousDaysLine<EffortDuration> allOverload = resourceLoadCalculator.getAllOverload(); Plotinfo plotOrderLoad = createPlotinfoFromDurations( groupAsNeededByZoom(toSortedMap(ContiguousDaysLine.min(orderLoad, maxCapacityOnResources))), interval); Plotinfo plotOtherLoad = createPlotinfoFromDurations( groupAsNeededByZoom(toSortedMap(min(allLoad, maxCapacityOnResources))), interval); Plotinfo plotMaxCapacity = createPlotinfoFromDurations( groupAsNeededByZoom(toSortedMap(maxCapacityOnResources)), interval); Plotinfo plotOrderOverload = createPlotinfoFromDurations( groupAsNeededByZoom(toSortedMap(sum(orderOverload, maxCapacityOnResources))), interval); Plotinfo plotOtherOverload = createPlotinfoFromDurations( groupAsNeededByZoom(toSortedMap(sum(allOverload, maxCapacityOnResources))), interval); plotOrderLoad.setFillColor(COLOR_ASSIGNED_LOAD); plotOrderLoad.setLineWidth(0); plotOtherLoad.setFillColor(COLOR_ASSIGNED_LOAD_GLOBAL); plotOtherLoad.setLineWidth(0); plotMaxCapacity.setLineColor(COLOR_CAPABILITY_LINE); plotMaxCapacity.setFillColor("#FFFFFF"); plotMaxCapacity.setLineWidth(2); plotOrderOverload.setFillColor(COLOR_OVERLOAD); plotOrderOverload.setLineWidth(0); plotOtherOverload.setFillColor(COLOR_OVERLOAD_GLOBAL); plotOtherOverload.setLineWidth(0); return new Plotinfo[] { plotOtherOverload, plotOrderOverload, plotMaxCapacity, plotOtherLoad, plotOrderLoad }; } } /** * Calculates 'Earned Value' indicators and set them in the Order 'Earned Valued' chart. * * @author Manuel Rego Casasnovas <mrego@igalia.com> * @author Diego Pino García <dpino@igalia.com> */ class OrderEarnedValueChartFiller extends EarnedValueChartFiller { private Order order; public OrderEarnedValueChartFiller(Order orderReloaded) { this.order = orderReloaded; super.setEarnedValueCalculator(earnedValueCalculator); } @Override protected void calculateBudgetedCostWorkScheduled(Interval interval) { setIndicatorInInterval( EarnedValueType.BCWS, interval, earnedValueCalculator.calculateBudgetedCostWorkScheduled(order)); } @Override protected void calculateActualCostWorkPerformed(Interval interval) { setIndicatorInInterval( EarnedValueType.ACWP, interval, earnedValueCalculator.calculateActualCostWorkPerformed(order)); } @Override protected void calculateBudgetedCostWorkPerformed(Interval interval) { setIndicatorInInterval( EarnedValueType.BCWP, interval, earnedValueCalculator.calculateBudgetedCostWorkPerformed(order)); } @Override protected Set<EarnedValueType> getSelectedIndicators() { return getEarnedValueSelectedIndicators(); } private Set<EarnedValueType> getEarnedValueSelectedIndicators() { Set<EarnedValueType> result = new HashSet<>(); for (Checkbox checkbox : earnedValueChartConfigurationCheckboxes) { if ( checkbox.isChecked() ) { EarnedValueType type = (EarnedValueType) checkbox.getAttribute(INDICATOR); result.add(type); } } return result; } } private ISubcontractCommand buildSubcontractCommand(EditTaskController editTaskController) { subcontractCommand.initialize(editTaskController, planningState); return subcontractCommand; } @Override public Order getOrder() { return planningState.getOrder(); } @Override public PlanningState getPlanningState() { return planningState; } @Override @Transactional(readOnly = true) public void forceLoadLabelsAndCriterionRequirements() { orderDAO.reattach(planningState.getOrder()); forceLoadLabels(planningState.getOrder()); forceLoadCriterionRequirements(planningState.getOrder()); } private void forceLoadLabels(OrderElement orderElement) { orderElement.getLabels().size(); if ( !orderElement.isLeaf() ) { for (OrderElement element : orderElement.getChildren()) { forceLoadLabels(element); } } } private void forceLoadCriterionRequirements(OrderElement orderElement) { orderElement.getCriterionRequirements().size(); for (HoursGroup hoursGroup : orderElement.getHoursGroups()) { hoursGroup.getCriterionRequirements().size(); } if ( !orderElement.isLeaf() ) { for (OrderElement element : orderElement.getChildren()) { forceLoadCriterionRequirements(element); } } } }