/*
* This file is part of LibrePlan
*
* Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e
* Desenvolvemento Tecnolóxico de Galicia
* Copyright (C) 2010-2011 Igalia, S.L.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.libreplan.web.limitingresources;
import org.apache.commons.lang3.Validate;
import org.joda.time.LocalDate;
import org.libreplan.business.planner.entities.ResourceAllocation;
import org.libreplan.business.planner.limiting.entities.DateAndHour;
import org.libreplan.business.planner.limiting.entities.Gap;
import org.libreplan.business.planner.limiting.entities.LimitingResourceAllocator;
import org.libreplan.business.planner.limiting.entities.LimitingResourceQueueElement;
import org.libreplan.business.resources.entities.LimitingResourceQueue;
import org.libreplan.business.resources.entities.Resource;
import org.libreplan.web.common.Util;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.zkoss.zk.ui.SuspendNotAllowedException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.SelectEvent;
import org.zkoss.zk.ui.util.Clients;
import org.zkoss.zk.ui.util.GenericForwardComposer;
import org.zkoss.zul.Checkbox;
import org.zkoss.zul.Datebox;
import org.zkoss.zul.Grid;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listcell;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.ListitemRenderer;
import org.zkoss.zul.Messagebox;
import org.zkoss.zul.Radio;
import org.zkoss.zul.Radiogroup;
import org.zkoss.zul.SimpleListModel;
import org.zkoss.zul.Window;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Collection;
import static org.libreplan.web.I18nHelper._;
/**
* Controller for manual allocation of queue elements.
*
* @author Diego Pino García <dpino@igalia.com>
*/
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ManualAllocationController extends GenericForwardComposer {
private LimitingResourcesController limitingResourcesController;
private LimitingResourcesPanel limitingResourcesPanel;
private Radiogroup radioAllocationDate;
private Radio earliestDate, latestDate, selectStartDate;
private Datebox startAllocationDate;
private Listbox listAssignableQueues;
private Listbox listCandidateGaps;
private Checkbox cbAllocationType;
private Map<Gap, DateAndHour> endAllocationDates = new HashMap<>();
private final QueueRenderer queueRenderer = new QueueRenderer();
private final CandidateGapRenderer candidateGapRenderer = new CandidateGapRenderer();
private Grid gridLimitingOrderElementHours;
private Grid gridCurrentQueue;
public ManualAllocationController() {
}
@Override
public void doAfterCompose(org.zkoss.zk.ui.Component comp) {
this.self = comp;
self.setAttribute("manualAllocationController", this, true);
listAssignableQueues = (Listbox) self.getFellowIfAny("listAssignableQueues");
listCandidateGaps = (Listbox) self.getFellowIfAny("listCandidateGaps");
radioAllocationDate = (Radiogroup) self.getFellowIfAny("radioAllocationDate");
earliestDate = (Radio) self.getFellowIfAny("earliestDate");
latestDate = (Radio) self.getFellowIfAny("latestDate");
selectStartDate = (Radio) self.getFellowIfAny("selectStartDate");
startAllocationDate = (Datebox) self.getFellowIfAny("startAllocationDate");
cbAllocationType = (Checkbox) self.getFellowIfAny("cbAllocationType");
gridLimitingOrderElementHours = (Grid) self.getFellowIfAny("gridLimitingOrderElementHours");
gridCurrentQueue = (Grid) self.getFellowIfAny("gridCurrentQueue");
}
public void setLimitingResourcesPanel(LimitingResourcesPanel limitingResourcesPanel) {
this.limitingResourcesPanel = limitingResourcesPanel;
}
public void setLimitingResourcesController(LimitingResourcesController limitingResourcesController) {
this.limitingResourcesController = limitingResourcesController;
}
public ILimitingResourceQueueModel getLimitingResourceQueueModel() {
return limitingResourcesController.getLimitingResourceQueueModel();
}
private void feedValidGaps(LimitingResourceQueueElement element, LimitingResourceQueue queue) {
feedValidGapsSince(element, queue, getStartDayBecauseOfGantt(element));
}
private DateAndHour getStartDayBecauseOfGantt(LimitingResourceQueueElement element) {
return new DateAndHour(new LocalDate(element.getEarliestStartDateBecauseOfGantt()), 0);
}
private void feedValidGapsSince(LimitingResourceQueueElement element, LimitingResourceQueue queue, DateAndHour since) {
List<Gap> gaps = LimitingResourceAllocator.getValidGapsForElementSince(element, queue, since);
endAllocationDates = calculateEndAllocationDates(element.getResourceAllocation(), queue.getResource(), gaps);
listCandidateGaps.setModel(new SimpleListModel<>(gaps));
if (!isAppropriative()) {
if (gaps.isEmpty()) {
disable(radioAllocationDate, true);
} else {
listCandidateGaps.setSelectedIndex(0);
setStartAllocationDate(gaps.get(0).getStartTime());
}
radioAllocationDate.setSelectedIndex(0);
}
enableRadioButtons(isAppropriative());
listCandidateGaps.setSelectedIndex(0);
}
private boolean isAppropriative() {
return cbAllocationType.isChecked();
}
private void enableRadioButtons(boolean isAppropriative) {
final LimitingResourceQueueElement beingEdited = getBeingEditedElement();
if (isAppropriative) {
listCandidateGaps.setDisabled(true);
earliestDate.setDisabled(true);
latestDate.setDisabled(true);
selectStartDate.setDisabled(false);
selectStartDate.setSelected(true);
startAllocationDate.setDisabled(false);
startAllocationDate.setValue(beingEdited.getEarliestStartDateBecauseOfGantt());
} else {
listCandidateGaps.setDisabled(false);
earliestDate.setDisabled(false);
earliestDate.setSelected(true);
latestDate.setDisabled(false);
selectStartDate.setDisabled(false);
startAllocationDate.setDisabled(true);
}
}
private void setStartAllocationDate(DateAndHour time) {
final Date date = (time != null) ? toDate(time.getDate()) : null;
startAllocationDate.setValue(date);
}
private Map<Gap, DateAndHour> calculateEndAllocationDates(ResourceAllocation<?> resourceAllocation,
Resource resource,
List<Gap> gaps) {
Map<Gap, DateAndHour> result = new HashMap<>();
for (Gap each: gaps) {
result.put(each, calculateEndAllocationDate(resourceAllocation, resource, each));
}
return result;
}
private DateAndHour calculateEndAllocationDate(ResourceAllocation<?> resourceAllocation,
Resource resource,
Gap gap) {
if (gap.getEndTime() != null) {
return LimitingResourceAllocator.startTimeToAllocateStartingFromEnd(resourceAllocation, resource, gap);
}
return null;
}
public void selectRadioAllocationDate(Event event) {
Radiogroup radiogroup = (Radiogroup) event.getTarget().getFellow("radioAllocationDate");
startAllocationDate.setDisabled(radiogroup.getSelectedIndex() != 2);
}
private void disable(Radiogroup radiogroup, boolean disabled) {
for (Object obj: radiogroup.getChildren()) {
final Radio each = (Radio) obj;
each.setDisabled(disabled);
}
}
private void setAssignableQueues(final LimitingResourceQueueElement element) {
List<LimitingResourceQueue> queues = getLimitingResourceQueueModel().getAssignableQueues(element);
listAssignableQueues.setModel(new SimpleListModel<>(queues));
listAssignableQueues.setItemRenderer(queueRenderer);
listAssignableQueues.addEventListener(Events.ON_SELECT, new EventListener() {
@Override
public void onEvent(Event event) {
SelectEvent se = (SelectEvent) event;
LimitingResourceQueue queue = getSelectedQueue(se);
if (queue != null) {
feedValidGaps(element, queue);
}
}
public LimitingResourceQueue getSelectedQueue(SelectEvent se) {
final Listitem item = (Listitem) se.getSelectedItems().iterator().next();
return (LimitingResourceQueue) item.getValue();
}
});
listAssignableQueues.setSelectedIndex(0);
feedValidGaps(element, queues.get(0));
}
private LimitingResourceQueue getSelectedQueue() {
LimitingResourceQueue result = null;
final Listitem item = listAssignableQueues.getSelectedItem();
if (item != null) {
result = item.getValue();
}
return result;
}
public void accept(Event e) {
LimitingResourceQueueElement element = getBeingEditedElement();
LimitingResourceQueue queue = getSelectedQueue();
DateAndHour time = getSelectedAllocationTime();
if (isAppropriative()) {
appropriativeAllocation(element, queue, time);
} else {
nonAppropriativeAllocation(element, queue, time);
}
limitingResourcesController.reloadUnassignedLimitingResourceQueueElements();
setStatus(Messagebox.OK);
close(e);
}
private void nonAppropriativeAllocation(LimitingResourceQueueElement element, LimitingResourceQueue queue,
DateAndHour time) {
Validate.notNull(time);
List<LimitingResourceQueueElement> inserted =
getLimitingResourceQueueModel().nonAppropriativeAllocation(element, queue, time);
refreshQueues(LimitingResourceQueue.queuesOf(inserted));
}
private void appropriativeAllocation(LimitingResourceQueueElement element, LimitingResourceQueue queue,
DateAndHour time) {
Validate.notNull(time);
Set<LimitingResourceQueueElement> inserted =
getLimitingResourceQueueModel().appropriativeAllocation(element, queue, time);
refreshQueues(LimitingResourceQueue.queuesOf(inserted));
}
private void refreshQueues(Collection<LimitingResourceQueue> queues) {
for (LimitingResourceQueue each : queues) {
limitingResourcesPanel.refreshQueue(each);
}
}
private DateAndHour getSelectedAllocationTime() {
final Gap selectedGap = getSelectedGap();
int index = radioAllocationDate.getSelectedIndex();
// Earliest date
if (index == 0) {
return getEarliestTime(selectedGap);
// Latest date
} else if (index == 1) {
return getLatestTime(selectedGap);
// Select start date
} else if (index == 2) {
final LocalDate selectedDay = new LocalDate(startAllocationDate.getValue());
if (isAppropriative()) {
LimitingResourceQueueElement beingEdited = getBeingEditedElement();
if (selectedDay.compareTo(new LocalDate(beingEdited.getEarliestStartDateBecauseOfGantt())) < 0) {
throw new WrongValueException(startAllocationDate, _("Day is not valid"));
}
return new DateAndHour(selectedDay, 0);
} else {
DateAndHour allocationTime = getValidDayInGap(selectedDay, getSelectedGap());
if (allocationTime == null) {
throw new WrongValueException(startAllocationDate, _("Day is not valid"));
}
return allocationTime;
}
}
return null;
}
private DateAndHour getEarliestTime(Gap gap) {
Validate.notNull(gap);
return gap.getStartTime();
}
private DateAndHour getLatestTime(Gap gap) {
Validate.notNull(gap);
LimitingResourceQueueElement element = getLimitingResourceQueueModel().getLimitingResourceQueueElement();
LimitingResourceQueue queue = getSelectedQueue();
return LimitingResourceAllocator
.startTimeToAllocateStartingFromEnd(element.getResourceAllocation(), queue.getResource(), gap);
}
private Gap getSelectedGap() {
Listitem item = listCandidateGaps.getSelectedItem();
if (item != null) {
return (Gap) item.getValue();
}
return null;
}
/**
* Checks if date is a valid day within gap.
* A day is valid within a gap if it is included between gap.startTime and the last day from which is
* possible to start doing an allocation (endAllocationDate).
*
* If date is valid, returns DateAndHour in gap associated with that date.
*
* @param date
* @param gap
* @return {@link DateAndHour}
*/
private DateAndHour getValidDayInGap(LocalDate date, Gap gap) {
final DateAndHour endAllocationDate = endAllocationDates.get(gap);
final LocalDate start = gap.getStartTime().getDate();
final LocalDate end = endAllocationDate != null ? endAllocationDate.getDate() : null;
if (start.equals(date)) {
return gap.getStartTime();
}
if (end != null && end.equals(date)) {
return endAllocationDate;
}
if ((start.compareTo(date) <= 0 && (end == null || end.compareTo(date) >= 0))) {
return new DateAndHour(date, 0);
}
return null;
}
public void cancel() {
self.setVisible(false);
setStatus(Messagebox.CANCEL);
}
public void close(Event e) {
self.setVisible(false);
e.stopPropagation();
}
public void setStartAllocationDate() {
setStartAllocationDate(getSelectedGap().getStartTime());
}
public void highlightCalendar(Event event) {
Datebox datebox = (Datebox) event.getTarget();
if (datebox.getValue() == null) {
final LocalDate startDate = getSelectedGap().getStartTime().getDate();
datebox.setValue(toDate(startDate));
}
if (isAppropriative()) {
final LimitingResourceQueueElement beingEdited = getBeingEditedElement();
highlightDaysFromDate(datebox.getUuid(), new LocalDate(beingEdited.getEarliestStartDateBecauseOfGantt()));
} else {
highlightDaysInGap(datebox.getUuid(), getSelectedGap());
}
}
private LimitingResourceQueueElement getBeingEditedElement() {
return getLimitingResourceQueueModel().getLimitingResourceQueueElement();
}
private Date toDate(LocalDate date) {
return date.toDateTimeAtStartOfDay().toDate();
}
/**
* Highlight calendar days within gap.
*
* @param uuid
* @param gap
*/
public void highlightDaysInGap(String uuid, Gap gap) {
final LocalDate start = gap.getStartTime().getDate();
final LocalDate end = gap.getEndTime() != null ? gap.getEndTime().getDate() : null;
final String jsCall = "highlightDaysInInterval('" +
uuid + "', '" +
jsonInterval(formatDate(start), formatDate(end)) + "', '" +
jsonHighlightColor() + "');";
Clients.evalJavaScript(jsCall);
}
/**
* Highlight calendar days starting from start.
*
* @param uuid
* @param start
*/
public void highlightDaysFromDate(String uuid, LocalDate start) {
final String jsCall = "highlightDaysInInterval('" +
uuid + "', '" +
jsonInterval(formatDate(start), null) + "', '" +
jsonHighlightColor() + "');";
Clients.evalJavaScript(jsCall);
}
public String formatDate(LocalDate date) {
return (date != null) ? date.toString() : null;
}
private String jsonInterval(String start, String end) {
StringBuilder result = new StringBuilder();
result.append("{\"start\": \"").append(start).append("\", ");
if (end != null) {
result.append("\"end\": \"").append(end).append("\"");
}
result.append("}");
return result.toString();
}
private String jsonHighlightColor() {
return "{\"color\": \"blue\", \"bgcolor\": \"white\"}";
}
public CandidateGapRenderer getCandidateGapRenderer() {
return candidateGapRenderer;
}
private static class CandidateGapRenderer implements ListitemRenderer {
@Override
public void render(Listitem item, Object data, int i) {
Gap gap = (Gap) data;
item.setValue(gap);
item.appendChild(cell(gap.getStartTime()));
item.appendChild(cell(gap.getEndTime()));
}
public Listcell cell(DateAndHour time) {
return new Listcell(formatTime(time));
}
private String formatTime(DateAndHour time) {
return time == null ? _("END") : Util.formatDate(time.getDate()) + " - " + time.getHour();
}
}
public void show(LimitingResourceQueueElement element) {
try {
clear();
setAssignableQueues(element);
getLimitingResourceQueueModel().init(element);
Util.reloadBindings(gridLimitingOrderElementHours);
Util.reloadBindings(gridCurrentQueue);
((Window) self).doModal();
((Window) self).setTitle(_("Manual assignment"));
} catch (SuspendNotAllowedException e) {
e.printStackTrace();
}
}
private void clear() {
setStatus(Messagebox.CANCEL);
cbAllocationType.setChecked(false);
}
public ListitemRenderer getQueueRenderer() {
return queueRenderer;
}
public Integer getStatus() {
return (Integer) self.getAttribute("status", true);
}
public void setStatus(int status) {
self.setAttribute("status", status, true);
}
private static class QueueRenderer implements ListitemRenderer {
@Override
public void render(Listitem item, Object data, int i) {
final LimitingResourceQueue queue = (LimitingResourceQueue) data;
item.setValue(queue);
item.appendChild(cell(queue));
}
private Listcell cell(LimitingResourceQueue queue) {
Listcell result = new Listcell();
result.setLabel(queue.getResource().getName());
return result;
}
}
public void onCheckAllocationType(Event e) {
Checkbox checkbox = (Checkbox) e.getTarget();
enableRadioButtons(checkbox.isChecked());
}
public int getHours() {
if (getBeingEditedElement() == null) {
return 0;
}
return getBeingEditedElement().getIntentedTotalHours();
}
public String getResourceOrCriteria() {
if (getBeingEditedElement() == null) {
return "";
}
return LimitingResourcesController.getResourceOrCriteria(getBeingEditedElement().getResourceAllocation());
}
public String getCurrentQueue() {
if (getBeingEditedElement() == null || getBeingEditedElement().getLimitingResourceQueue() == null) {
return _("Unassigned");
}
return getBeingEditedElement().getLimitingResourceQueue().getResource().getName();
}
public String getCurrentStart() {
if (getBeingEditedElement() == null || getBeingEditedElement().getStartDate() == null) {
return _("Unassigned");
}
return getBeingEditedElement().getStartDate().toString();
}
public String getCurrentEnd() {
if (getBeingEditedElement() == null || getBeingEditedElement().getEndDate() == null) {
return _("Unassigned");
}
return getBeingEditedElement().getEndDate().toString();
}
}