package org.sigmah.client.ui.view.pivot.table;
/*
* #%L
* Sigmah
* %%
* Copyright (C) 2010 - 2016 URD
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.sigmah.client.i18n.I18N;
import org.sigmah.shared.command.BatchCommand;
import org.sigmah.shared.command.GetIndicators;
import org.sigmah.shared.command.UpdateMonthlyReports;
import org.sigmah.shared.command.result.IndicatorListResult;
import org.sigmah.shared.dto.IndicatorDTO;
import com.extjs.gxt.ui.client.data.BaseTreeModel;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.GridEvent;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.store.Record;
import com.extjs.gxt.ui.client.store.Store;
import com.extjs.gxt.ui.client.store.TreeStore;
import com.extjs.gxt.ui.client.util.DelayedTask;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.grid.ColumnConfig;
import com.extjs.gxt.ui.client.widget.layout.FitLayout;
import com.extjs.gxt.ui.client.widget.treegrid.EditorTreeGrid;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.HasValue;
import com.google.inject.Inject;
import org.sigmah.client.dispatch.DispatchAsync;
import org.sigmah.client.event.EventBus;
import org.sigmah.client.event.PivotCellEvent;
import org.sigmah.client.ui.notif.N10N;
import org.sigmah.client.ui.widget.Loadable;
import org.sigmah.client.util.GWTDates;
import org.sigmah.shared.dto.pivot.model.DateDimension;
import org.sigmah.shared.dto.pivot.model.DateUnit;
import org.sigmah.shared.dto.pivot.model.Dimension;
import org.sigmah.shared.dto.pivot.content.DimensionCategory;
import org.sigmah.shared.dto.pivot.content.EntityCategory;
import org.sigmah.shared.dto.pivot.content.MonthCategory;
import org.sigmah.shared.dto.pivot.model.PivotElement;
import org.sigmah.shared.dto.pivot.content.PivotTableData;
import org.sigmah.shared.dto.referential.DimensionType;
import org.sigmah.shared.util.Dates;
import org.sigmah.shared.util.Month;
/**
* Independent component to display and edited pivoted site / indicator results.
*
* @author Alex Bertram (akbertram@gmail.com)
*/
public class PivotGridPanel extends ContentPanel implements HasValue<PivotElement>, Loadable {
private final EventBus eventBus;
private final DispatchAsync dispatcher;
private PivotElement element;
private EditorTreeGrid<PivotTableRow> grid;
private final TreeStore<PivotTableRow> store;
private ColumnMapping columnMapping;
private HeaderDecorator headerDecorator = new GridHeaderDecorator(this);
private Map<Integer, IndicatorDTO> indicators;
private final Dates dates = new GWTDates();
private boolean showAxisIcons = true;
private boolean showSwapIcon = false;
private boolean loading;
@Inject
public PivotGridPanel(EventBus eventBus, DispatchAsync dispatcher) {
this.eventBus = eventBus;
this.dispatcher = dispatcher;
setLayout(new FitLayout());
store = new TreeStore<PivotTableRow>();
PivotResources.INSTANCE.css().ensureInjected();
}
public boolean isShowAxisIcons() {
return showAxisIcons;
}
public void setShowAxisIcons(boolean showAxisIcons) {
this.showAxisIcons = showAxisIcons;
}
public boolean isShowSwapIcon() {
return showSwapIcon;
}
public void setShowSwapIcon(boolean showSwapIcon) {
this.showSwapIcon = showSwapIcon;
}
// --
// Loadable implementation.
// --
@Override
public void setLoading(boolean loading) {
this.loading = loading;
if(loading) {
mask(I18N.CONSTANTS.loading());
} else {
unmask();
}
}
@Override
public boolean isLoading() {
return loading;
}
public class PivotTableRow extends BaseTreeModel {
private PivotTableData.Axis rowAxis;
public PivotTableRow(PivotTableData.Axis axis) {
this.rowAxis = axis;
set("header", headerDecorator.decorateHeader(axis));
updateFromTree();
for (PivotTableData.Axis child : rowAxis.getChildren()) {
add(new PivotTableRow(child));
}
}
/**
* Updates this model from the Cell values in the PivotTableData tree.
*/
private void updateFromTree() {
this.setProperties(Collections.EMPTY_MAP);
for (Map.Entry<PivotTableData.Axis, PivotTableData.Cell> entry : rowAxis.getCells().entrySet()) {
String property = columnMapping.propertyNameForAxis(entry.getKey());
set(property, entry.getValue().getValue());
}
}
public PivotTableData.Axis getRowAxis() {
return rowAxis;
}
public PivotTableData.Axis getColAxis(String property) {
return columnMapping.columnAxisForProperty(property);
}
public DimensionCategory getCategory(String property, Dimension dimension) {
DimensionCategory category = findCategory(rowAxis, dimension);
if (category == null) {
return findCategory(getColAxis(property), dimension);
}
return category;
}
private DimensionCategory findCategory(PivotTableData.Axis leaf, Dimension dim) {
while (leaf != null) {
if (leaf.getDimension() != null && leaf.getDimension().equals(dim)) {
return leaf.getCategory();
}
leaf = leaf.getParent();
}
return null;
}
public int getIndicatorId(String property) {
Set<Integer> indicatorRestrictions = element.getFilter().getRestrictions(DimensionType.Indicator);
if (indicatorRestrictions.size() == 1) {
return indicatorRestrictions.iterator().next();
}
EntityCategory cat = (EntityCategory) getCategory(property, new Dimension(DimensionType.Indicator));
if (cat != null) {
return cat.getId();
}
return -1;
}
public int getSiteId(String property) {
Set<Integer> siteRestrictions = element.getFilter().getRestrictions(DimensionType.Site);
if (siteRestrictions.size() == 1) {
return siteRestrictions.iterator().next();
}
EntityCategory cat = (EntityCategory) getCategory(property, new Dimension(DimensionType.Site));
if (cat != null) {
return cat.getId();
}
return -1;
}
public Month getMonth(String property) {
MonthCategory cat = (MonthCategory) getCategory(property, new DateDimension(DateUnit.MONTH));
if (cat != null) {
return new Month(cat.getYear(), cat.getMonth());
}
if (element.getFilter().getDateRange().isClosed()) {
// TODO(alex) : this should check to see whether the date range
// is actually a month range
// but dateUtil is behaving weirdly because of conflicting
// timezones
return new Month(dates.getYear(element.getFilter().getMinDate()), dates.getMonth(element
.getFilter().getMinDate()));
}
throw new UnsupportedOperationException("This cell at property '" + property
+ "' is not constrained by month");
}
}
public void clear() {
if (grid != null) {
grid.removeAllListeners();
removeAll();
}
store.removeAll();
}
public void setData(final PivotElement element) {
clear();
this.element = element;
PivotTableData data = element.getContent().getData();
this.columnMapping = new ColumnMapping(data, new RendererProvider(), headerDecorator);
for (PivotTableData.Axis axis : data.getRootRow().getChildren()) {
store.add(new PivotTableRow(axis), true);
}
grid = new EditorTreeGrid<PivotTableRow>(store, columnMapping.getColumnModel());
grid.setView(new PivotGridPanelView());
grid.getStyle().setNodeCloseIcon(null);
grid.getStyle().setNodeOpenIcon(null);
grid.setAutoExpandColumn("header");
grid.setAutoExpandMin(150);
grid.addListener(Events.CellDoubleClick, new Listener<GridEvent<PivotTableRow>>() {
public void handleEvent(GridEvent<PivotTableRow> ge) {
if (ge.getColIndex() != 0) {
eventBus.fireEvent(new PivotCellEvent(PivotCellEvent.Action.DRILLDOWN, element, ge.getModel().getRowAxis(),
columnMapping.columnAxisForIndex(ge.getColIndex())));
}
}
});
grid.addListener(Events.HeaderClick, new Listener<GridEvent<PivotTableRow>>() {
@Override
public void handleEvent(GridEvent<PivotTableRow> event) {
fireEvent(Events.HeaderClick,
new PivotGridHeaderEvent(event, columnMapping.columnAxisForIndex(event.getColIndex())));
}
});
grid.addListener(Events.CellClick, new Listener<GridEvent<PivotTableRow>>() {
@Override
public void handleEvent(GridEvent<PivotTableRow> event) {
if (event.getColIndex() == 0) {
fireEvent(Events.HeaderClick, new PivotGridHeaderEvent(event, event.getModel().getRowAxis()));
} else {
fireEvent(Events.CellClick,
new PivotGridCellEvent(event, columnMapping.columnAxisForIndex(event.getColIndex())));
}
}
});
grid.addListener(Events.BeforeEdit, new Listener<GridEvent<PivotTableRow>>() {
@Override
public void handleEvent(GridEvent<PivotTableRow> be) {
if (!be.getModel().getRowAxis().isLeaf()) {
be.setCancelled(true);
}
PivotGridCellEvent pivotEvent = new PivotGridCellEvent(be, columnMapping.columnAxisForIndex(be
.getColIndex()));
IndicatorDTO indicator = indicators.get(pivotEvent.getIndicatorId());
if(indicator != null) {
if (indicator.isDirectDataEntryEnabled()) {
prepareEditor(pivotEvent, indicator);
} else {
be.setCancelled(true);
N10N.infoNotif(I18N.CONSTANTS.dataEntry(), I18N.CONSTANTS.indicatorDirectEntry());
}
}
}
});
grid.addListener(Events.AfterEdit, new Listener<GridEvent<PivotTableRow>>() {
@Override
public void handleEvent(GridEvent<PivotTableRow> event) {
PivotGridCellEvent pivotEvent = new PivotGridCellEvent(event, columnMapping.columnAxisForIndex(event
.getColIndex()));
updateTotalsAfterEdit(pivotEvent);
fireEvent(Events.AfterEdit, pivotEvent);
}
});
grid.addStyleName(PivotResources.INSTANCE.css().pivotTable());
add(grid);
layout();
new DelayedTask(new Listener<BaseEvent>() {
@Override
public void handleEvent(BaseEvent be) {
for (PivotTableRow row : store.getRootItems()) {
grid.setExpanded(row, true, true);
}
}
}).delay(1);
}
private IndicatorDTO indicatorForCell(PivotGridCellEvent event) {
int indicatorId = event.getIndicatorId();
return indicators.get(indicatorId);
}
protected void prepareEditor(PivotGridCellEvent event, IndicatorDTO indicator) {
if (indicator != null) {
ColumnConfig config = grid.getColumnModel().getColumn(event.getColIndex());
IndicatorValueField field = (IndicatorValueField) config.getEditor().getField();
field.setIndicator(indicator);
}
}
private void updateTotalsAfterEdit(PivotGridCellEvent event) {
// update the PivotTableData.Cell
Double newValue = event.getModel().get(event.getProperty());
event.getOrCreateCell().setValue(newValue);
// update totals
element.getContent().getData().updateTotals();
syncGridWithContent();
}
private void syncGridWithContent() {
for (PivotTableRow row : store.getAllItems()) {
row.updateFromTree();
}
grid.getView().refresh(false);
}
private int findIndicatorId(PivotTableData.Axis axis) {
while (axis != null) {
if (axis.getDimension() != null && axis.getDimension().getType() == DimensionType.Indicator) {
return ((EntityCategory) axis.getCategory()).getId();
}
axis = axis.getParent();
}
return -1;
}
@Override
public HandlerRegistration addValueChangeHandler(ValueChangeHandler<PivotElement> handler) {
return addHandler(handler, ValueChangeEvent.getType());
}
@Override
public PivotElement getValue() {
return element;
}
@Override
public void setValue(final PivotElement value) {
int databaseId = value.getFilter().getRestrictions(DimensionType.Database).iterator().next();
dispatcher.execute(new GetIndicators(databaseId), new AsyncCallback<IndicatorListResult>() {
@Override
public void onFailure(Throwable caught) {
// handled by monitor
}
@Override
public void onSuccess(IndicatorListResult result) {
indicators = new HashMap<Integer, IndicatorDTO>();
for (IndicatorDTO indicator : result.getData()) {
indicators.put(indicator.getId(), indicator);
}
setData(value);
}
}, this);
}
@Override
public void setValue(PivotElement value, boolean fireEvents) {
setData(element);
if (fireEvents) {
ValueChangeEvent.fire(this, value);
}
}
public Store<PivotTableRow> getStore() {
return store;
}
public BatchCommand composeSaveCommand() {
BatchCommand batch = new BatchCommand();
for (Record record : store.getModifiedRecords()) {
PivotTableRow row = (PivotTableRow) record.getModel();
for (String property : record.getChanges().keySet()) {
UpdateMonthlyReports.Change change = new UpdateMonthlyReports.Change();
change.indicatorId = row.getIndicatorId(property);
change.month = row.getMonth(property);
change.value = row.get(property);
batch.add(new UpdateMonthlyReports(row.getSiteId(property), change));
}
}
return batch;
}
private class RendererProvider implements PivotCellRendererProvider {
@Override
public PivotCellRenderer getRendererForColumn(PivotTableData.Axis column) {
int indicatorId = -1;
if (element.getFilter().isRestricted(DimensionType.Indicator)) {
indicatorId = element.getFilter().getRestrictions(DimensionType.Indicator).iterator().next();
} else {
indicatorId = findIndicatorId(column);
}
if (indicatorId == -1) {
return new MixedCellRenderer(indicators);
} else {
IndicatorDTO indicator = indicators.get(indicatorId);
if (indicator.isQualitative()) {
return new QualitativeCellRenderer(indicator);
} else {
return new QuantitativeCellRenderer(indicator);
}
}
}
}
public void setHeaderDecoratorEditable(boolean editable) {
if(editable) {
headerDecorator = new GridHeaderDecorator(this);
} else {
headerDecorator = new ReadOnlyHeaderDecorator(this);
}
}
public EditorTreeGrid<PivotTableRow> getGrid() {
return grid;
}
public boolean hasIndicatorsInStore() {
return (indicators != null && indicators.size() != 0);
}
}