blob: 5b0d8833c734f3f0ee2d884dc72b763a8fa047b4 [file] [log] [blame]
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.sample.expenses.client;
import com.google.gwt.activity.shared.Activity;
import com.google.gwt.cell.client.AbstractInputCell;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.DateCell;
import com.google.gwt.cell.client.FieldUpdater;
import com.google.gwt.cell.client.NumberCell;
import com.google.gwt.cell.client.TextCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.SelectElement;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.NumberFormat;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.sample.expenses.client.place.ReportListPlace;
import com.google.gwt.sample.expenses.client.place.ReportPlace;
import com.google.gwt.sample.expenses.client.style.Styles;
import com.google.gwt.sample.expenses.shared.EmployeeProxy;
import com.google.gwt.sample.expenses.shared.ExpenseProxy;
import com.google.gwt.sample.expenses.shared.ExpenseRequest;
import com.google.gwt.sample.expenses.shared.ExpensesRequestFactory;
import com.google.gwt.sample.expenses.shared.ReportProxy;
import com.google.gwt.sample.expenses.shared.ReportRequest;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.cellview.client.CellTable;
import com.google.gwt.user.cellview.client.Column;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.ListDataProvider;
import com.google.web.bindery.requestfactory.gwt.ui.client.EntityProxyKeyProvider;
import com.google.web.bindery.requestfactory.shared.EntityProxyChange;
import com.google.web.bindery.requestfactory.shared.EntityProxyId;
import com.google.web.bindery.requestfactory.shared.Receiver;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Details about the current expense report on the right side of the app,
* including the list of expenses.
*/
public class ExpenseReportDetails extends Composite implements Activity {
interface Binder extends UiBinder<Widget, ExpenseReportDetails> {
}
/**
* Fetches an employee and a report in parallel. A fine example of the kind of
* thing that will no longer be necessary when RequestFactory provides server
* side method chaining.
*/
class EmployeeReportFetcher {
ReportProxy fetchedReport;
EmployeeProxy fetchedEmployee;
void Run(EntityProxyId<EmployeeProxy> employeeId,
EntityProxyId<ReportProxy> reportId,
final Receiver<EmployeeReportFetcher> callback) {
expensesRequestFactory.find(employeeId).fire(
new Receiver<EmployeeProxy>() {
@Override
public void onSuccess(EmployeeProxy response) {
fetchedEmployee = response;
if (fetchedReport != null) {
callback.onSuccess(EmployeeReportFetcher.this);
}
}
});
expensesRequestFactory.find(reportId).fire(new Receiver<ReportProxy>() {
@Override
public void onSuccess(ReportProxy response) {
fetchedReport = response;
if (fetchedEmployee != null) {
callback.onSuccess(EmployeeReportFetcher.this);
}
}
});
}
}
/**
* The resources applied to the table.
*/
interface TableResources extends CellTable.Resources {
@Source({CellTable.Style.DEFAULT_CSS, "ExpenseDetailsCellTable.css"})
TableStyle cellTableStyle();
}
/**
* The styles applied to the table.
*/
interface TableStyle extends CellTable.Style {
}
interface Template extends SafeHtmlTemplates {
@Template("<select style=\"background-color:white;border:1px solid "
+ "#707172;width:10em;margin-right:10px;\" disabled=\"true\" tabindex=\"-1\">"
+ "<option></option>{0}{1}</select>")
SafeHtml disabled(SafeHtml approvedOption, SafeHtml deniedOption);
@Template("<select style=\"background-color:white;border:1px solid "
+ "#707172;width:10em;margin-right:10px;\" tabindex=\"-1\">"
+ "<option></option>{0}{1}</select>")
SafeHtml enabled(SafeHtml approvedOption, SafeHtml deniedOption);
}
/**
* The cell used for approval status.
*/
private class ApprovalCell extends
AbstractInputCell<String, ApprovalViewData> {
private final String approvedText = Approval.APPROVED.getText();
private final String deniedText = Approval.DENIED.getText();
private final SafeHtml errorIconHtml;
private final SafeHtml pendingIconHtml;
public ApprovalCell() {
super("change", "click");
if (template == null) {
template = GWT.create(Template.class);
}
// Cache the html string for the error icon.
ImageResource errorIcon = Styles.resources().errorIcon();
AbstractImagePrototype errorImg = AbstractImagePrototype.create(errorIcon);
errorIconHtml = SafeHtmlUtils.fromTrustedString(errorImg.getHTML());
// Cache the html string for the pending icon.
ImageResource pendingIcon = Styles.resources().pendingCommit();
AbstractImagePrototype pendingImg = AbstractImagePrototype.create(pendingIcon);
pendingIconHtml = SafeHtmlUtils.fromTrustedString(pendingImg.getHTML());
}
@Override
public boolean isEditing(Context context, Element parent, String value) {
return super.isEditing(context, parent, value) || denialPopup.isShowing();
}
@Override
public void onBrowserEvent(Context context, Element parent, String value,
NativeEvent event, ValueUpdater<String> valueUpdater) {
super.onBrowserEvent(context, parent, value, event, valueUpdater);
Object key = context.getKey();
String type = event.getType();
ApprovalViewData viewData = getViewData(key);
if ("change".equals(type)) {
// Disable the select box.
SelectElement select = parent.getFirstChild().cast();
select.setDisabled(true);
// Add the pending icon if it isn't already visible.
if (viewData == null) {
Element tmpElem = Document.get().createDivElement();
tmpElem.setInnerHTML(pendingIconHtml.asString());
parent.appendChild(tmpElem.getFirstChildElement());
}
// Remember which value is now selected.
int index = select.getSelectedIndex();
String pendingValue = select.getOptions().getItem(index).getValue();
viewData = new ApprovalViewData(pendingValue);
setViewData(key, viewData);
finishEditing(parent, pendingValue, key, valueUpdater);
// Update the value updater.
if (valueUpdater != null) {
valueUpdater.update(pendingValue);
}
} else if ("click".equals(type) && viewData != null
&& parent.getChildCount() >= 3) {
// Alert the user of the error
Element img = parent.getChild(1).cast();
Element anchor = img.getNextSiblingElement();
if (anchor.isOrHasChild(Element.as(event.getEventTarget().cast()))) {
// Alert the user of the error.
showErrorPopup(viewData.getRejectionText());
// Clear the view data now that we've viewed the message.
clearViewData(key);
parent.removeChild(anchor);
parent.removeChild(img);
}
}
}
@Override
public void render(Context context, String value, SafeHtmlBuilder sb) {
// Get the view data.
Object key = context.getKey();
ApprovalViewData viewData = getViewData(key);
if (viewData != null && viewData.getPendingApproval().equals(value)) {
clearViewData(key);
viewData = null;
}
boolean isRejected = false;
boolean isDisabled = false;
String pendingValue = null;
String renderValue = value;
if (viewData != null) {
isRejected = viewData.isRejected();
pendingValue = viewData.getPendingApproval();
if (!isRejected) {
renderValue = pendingValue;
// If there is a delta value that has not been rejected, then the
// combo box should remain disabled.
isDisabled = true;
}
}
boolean isApproved = approvedText.equals(renderValue);
boolean isDenied = deniedText.equals(renderValue);
SafeHtml approvedOption = createOption(isApproved, approvedText);
SafeHtml deniedOption = createOption(isDenied, deniedText);
// Create the select element.
if (isDisabled) {
sb.append(template.disabled(approvedOption, deniedOption));
} else {
sb.append(template.enabled(approvedOption, deniedOption));
}
// Add an icon indicating the commit state.
if (isRejected) {
// Add error icon if viewData does not match.
sb.append(errorIconHtml);
sb.appendHtmlConstant("<a style='padding-left:3px;color:red;' href='javascript:;'>Error!</a>");
} else if (pendingValue != null) {
// Add refresh icon if pending.
sb.append(pendingIconHtml);
}
}
private SafeHtml createOption(boolean selected, String text) {
SafeHtmlBuilder builder = new SafeHtmlBuilder();
if (selected) {
builder.appendHtmlConstant("<option selected=\"selected\">");
} else {
builder.appendHtmlConstant("<option>");
}
builder.appendEscaped(text);
builder.appendHtmlConstant("</option>");
return builder.toSafeHtml();
}
}
/**
* The ViewData associated with the {@link ApprovalCell}.
*/
private static class ApprovalViewData {
private final String pendingApproval;
private String rejectionText;
public ApprovalViewData(String approval) {
this.pendingApproval = approval;
}
public String getPendingApproval() {
return pendingApproval;
}
public String getRejectionText() {
return rejectionText;
}
public boolean isRejected() {
return rejectionText != null;
}
public void reject(String text) {
this.rejectionText = text;
}
}
private static Template template;
/**
* The maximum amount that can be approved for a given report.
*/
private static final int MAX_COST = 250;
/**
* The auto refresh interval in milliseconds.
*/
private static final int REFRESH_INTERVAL = 5000;
private static Binder uiBinder = GWT.create(Binder.class);
@UiField Element approvedLabel;
@UiField Element costLabel;
@UiField Element notes;
@UiField TextBox notesBox;
@UiField Anchor notesEditLink;
@UiField Element notesEditLinkWrapper;
@UiField Element notesPending;
@UiField Element reportName;
@UiField Anchor reportsLink;
@UiField(provided = true)
CellTable<ExpenseProxy> table;
@UiField
Element unreconciledLabel;
private final List<SortableHeader> allHeaders = new ArrayList<SortableHeader>();
private ApprovalCell approvalCell;
/**
* The default {@link Comparator} used for sorting.
*/
private Comparator<ExpenseProxy> defaultComparator;
/**
* The popup used when something is denied.
*/
private final DenialPopup denialPopup = new DenialPopup();
/**
* The popup used to display errors to the user.
*/
private final PopupPanel errorPopup = new PopupPanel(false, true);
/**
* The label inside the error popup.
*/
private final Label errorPopupMessage = new Label();
private final ExpensesRequestFactory expensesRequestFactory;
/**
* The data provider that provides expense items.
*/
private final ListDataProvider<ExpenseProxy> items;
/**
* The set of Expense keys that we have seen. When a new key is added, we
* compare it to the list of known keys to determine if it is new.
*/
private Map<Object, ExpenseProxy> knownExpenseKeys = null;
private Comparator<ExpenseProxy> lastComparator;
/**
* Keep track of the last receiver so we can ignore stale responses.
*/
private Receiver<List<ExpenseProxy>> lastReceiver;
/**
* The {@link Timer} used to periodically refresh the table.
*/
private final Timer refreshTimer = new Timer() {
@Override
public void run() {
requestExpenses();
}
};
/**
* The current report being displayed.
*/
private ReportProxy report;
/**
* The total amount that has been approved.
*/
private double totalApproved;
private ReportPlace place;
public ExpenseReportDetails(ExpensesRequestFactory expensesRequestFactory) {
this.expensesRequestFactory = expensesRequestFactory;
createErrorPopup();
initTable();
initWidget(uiBinder.createAndBindUi(this));
items = new ListDataProvider<ExpenseProxy>(
new EntityProxyKeyProvider<ExpenseProxy>());
items.addDataDisplay(table);
// Switch to edit notes.
notesEditLink.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
setNotesEditState(true, false, report.getNotes());
}
});
// Switch to view mode.
notesBox.addBlurHandler(new BlurHandler() {
public void onBlur(BlurEvent event) {
// The text box will be blurred on cancel, so only save the notes if
// it is visible.
if (notesBox.isVisible()) {
saveNotes();
}
}
});
notesBox.addKeyUpHandler(new KeyUpHandler() {
public void onKeyUp(KeyUpEvent event) {
int keyCode = event.getNativeKeyCode();
switch (keyCode) {
case KeyCodes.KEY_ENTER:
saveNotes();
break;
case KeyCodes.KEY_ESCAPE:
// Cancel the edit.
setNotesEditState(false, false, report.getNotes());
break;
}
}
});
}
public ReportListPlace getReportListPlace() {
ReportListPlace listPlace = place.getListPlace();
return listPlace == null ? ReportListPlace.ALL : listPlace;
}
public Anchor getReportsLink() {
return reportsLink;
}
public String mayStop() {
return null;
}
public void onCancel() {
}
public void onExpenseRecordChanged(EntityProxyChange<ExpenseProxy> event) {
final EntityProxyId<ExpenseProxy> proxyId = event.getProxyId();
int index = 0;
final List<ExpenseProxy> list = items.getList();
for (ExpenseProxy r : list) {
if (items.getKey(r).equals(proxyId)) {
final int i = index;
expensesRequestFactory.find(proxyId).fire(new Receiver<ExpenseProxy>() {
@Override
public void onSuccess(ExpenseProxy newRecord) {
list.set(i, newRecord);
// Update the view data if the approval has been updated.
ApprovalViewData avd = approvalCell.getViewData(proxyId);
if (avd != null
&& avd.getPendingApproval().equals(newRecord.getApproval())) {
syncCommit(newRecord, null);
}
}
});
}
index++;
}
refreshCost();
if (lastComparator != null) {
sortExpenses(list, lastComparator);
}
}
public void onReportChanged(EntityProxyChange<ReportProxy> event) {
EntityProxyId<ReportProxy> changed = event.getProxyId();
if (report != null && report.getId().equals(changed)) {
// Request the updated report.
expensesRequestFactory.reportRequest().findReport(report.getId()).fire(
new Receiver<ReportProxy>() {
@Override
public void onSuccess(ReportProxy response) {
report = response;
setNotesEditState(false, false, response.getNotes());
}
});
}
}
public void onStop() {
}
public void start(AcceptsOneWidget panel, EventBus eventBus) {
final ReportListPlace listPlace = place.getListPlace();
if (listPlace.getEmployeeId() == null) {
expensesRequestFactory.find(place.getReportId()).fire(
new Receiver<ReportProxy>() {
@Override
public void onSuccess(ReportProxy response) {
setReportRecord(response, listPlace.getDepartment(), null);
}
});
} else {
new EmployeeReportFetcher().Run(listPlace.getEmployeeId(),
place.getReportId(),
new Receiver<ExpenseReportDetails.EmployeeReportFetcher>() {
@Override
public void onSuccess(EmployeeReportFetcher response) {
setReportRecord(response.fetchedReport,
listPlace.getDepartment(), response.fetchedEmployee);
}
});
}
EntityProxyChange.registerForProxyType(eventBus, ExpenseProxy.class,
new EntityProxyChange.Handler<ExpenseProxy>() {
public void onProxyChange(EntityProxyChange<ExpenseProxy> event) {
onExpenseRecordChanged(event);
}
});
EntityProxyChange.registerForProxyType(eventBus, ReportProxy.class,
new EntityProxyChange.Handler<ReportProxy>() {
public void onProxyChange(EntityProxyChange<ReportProxy> event) {
onReportChanged(event);
}
});
panel.setWidget(this);
}
/**
* In this application, called by {@link ExpensesActivityMapper} each time a
* ReportListPlace is posted. In a more typical set up, this would be a
* constructor argument to a one shot activity, perhaps managing a shared
* widget view instance.
*/
public void updateForPlace(final ReportPlace place) {
this.place = place;
}
/**
* Add a column of a {@link Comparable} type using default comparators.
*
* @param <C> the column type
* @param table the table
* @param text the header text
* @param cell the cell used to render values
* @param getter the {@link GetValue} used to retrieve cell values
* @return the new column
*/
private <C extends Comparable<C>> Column<ExpenseProxy, C> addColumn(
final CellTable<ExpenseProxy> table, final String text,
final Cell<C> cell, final GetValue<ExpenseProxy, C> getter) {
return addColumn(table, text, cell, getter,
createColumnComparator(getter, false),
createColumnComparator(getter, true));
}
/**
* Add a column with the specified comparators.
*
* @param <C> the column type
* @param table the table
* @param text the header text
* @param cell the cell used to render values
* @param getter the {@link GetValue} used to retrieve cell values
* @param ascComparator the comparator used to sort ascending
* @param descComparator the comparator used to sort ascending
* @return the new column
*/
private <C> Column<ExpenseProxy, C> addColumn(
final CellTable<ExpenseProxy> table, final String text,
final Cell<C> cell, final GetValue<ExpenseProxy, C> getter,
final Comparator<ExpenseProxy> ascComparator,
final Comparator<ExpenseProxy> descComparator) {
// Create the column.
final Column<ExpenseProxy, C> column = new Column<ExpenseProxy, C>(cell) {
@Override
public C getValue(ExpenseProxy object) {
return getter.getValue(object);
}
};
final SortableHeader header = new SortableHeader(text);
allHeaders.add(header);
// Hook up sorting.
header.setUpdater(new ValueUpdater<String>() {
public void update(String value) {
header.setSorted(true);
header.toggleReverseSort();
for (SortableHeader otherHeader : allHeaders) {
if (otherHeader != header) {
otherHeader.setSorted(false);
otherHeader.setReverseSort(true);
}
}
sortExpenses(items.getList(), header.getReverseSort() ? descComparator
: ascComparator);
table.redrawHeaders();
}
});
table.addColumn(column, header);
return column;
}
/**
* Create a comparator for the column.
*
* @param <C> the column type
* @param getter the {@link GetValue} used to get the cell value
* @param descending true if descending, false if ascending
* @return the comparator
*/
private <C extends Comparable<C>> Comparator<ExpenseProxy> createColumnComparator(
final GetValue<ExpenseProxy, C> getter, final boolean descending) {
return new Comparator<ExpenseProxy>() {
public int compare(ExpenseProxy o1, ExpenseProxy o2) {
// Null check the row object.
if (o1 == null && o2 == null) {
return 0;
} else if (o1 == null) {
return descending ? 1 : -1;
} else if (o2 == null) {
return descending ? -1 : 1;
}
// Compare the column value.
C c1 = getter.getValue(o1);
C c2 = getter.getValue(o2);
if (c1 == null && c2 == null) {
return 0;
} else if (c1 == null) {
return descending ? 1 : -1;
} else if (c2 == null) {
return descending ? -1 : 1;
}
int comparison = c1.compareTo(c2);
return descending ? -comparison : comparison;
}
};
}
/**
* Create the error message popup.
*/
private void createErrorPopup() {
errorPopup.setGlassEnabled(true);
errorPopup.setStyleName(Styles.common().popupPanel());
errorPopupMessage.addStyleName(Styles.common().expenseDetailsErrorPopupMessage());
Button closeButton = new Button("Dismiss", new ClickHandler() {
public void onClick(ClickEvent event) {
errorPopup.hide();
}
});
// Organize the widgets in the popup.
VerticalPanel layout = new VerticalPanel();
layout.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER);
layout.add(errorPopupMessage);
layout.add(closeButton);
errorPopup.setWidget(layout);
}
/**
* Return a formatted currency string.
*
* @param amount the amount in dollars
* @return a formatted string
*/
private String formatCurrency(double amount) {
boolean negative = amount < 0;
if (negative) {
amount = -amount;
}
int dollars = (int) amount;
int cents = (int) ((amount * 100) % 100);
StringBuilder sb = new StringBuilder();
if (negative) {
sb.append("-");
}
sb.append("$");
sb.append(dollars);
sb.append('.');
if (cents < 10) {
sb.append('0');
}
sb.append(cents);
return sb.toString();
}
/**
* Get the columns displayed in the expense table.
*/
private String[] getExpenseColumns() {
return new String[] {
"amount", "approval", "category", "created", "description",
"reasonDenied"};
}
private CellTable<ExpenseProxy> initTable() {
CellTable.Resources resources = GWT.create(TableResources.class);
table = new CellTable<ExpenseProxy>(100, resources,
new EntityProxyKeyProvider<ExpenseProxy>());
Styles.Common common = Styles.common();
table.addColumnStyleName(0, common.spacerColumn());
table.addColumnStyleName(1, common.expenseDetailsDateColumn());
table.addColumnStyleName(3, common.expenseDetailsCategoryColumn());
table.addColumnStyleName(4, common.expenseDetailsAmountColumn());
table.addColumnStyleName(5, common.expenseDetailsApprovalColumn());
table.addColumnStyleName(6, common.spacerColumn());
// Spacer column.
table.addColumn(new SpacerColumn<ExpenseProxy>());
// Created column.
GetValue<ExpenseProxy, Date> createdGetter = new GetValue<ExpenseProxy, Date>() {
public Date getValue(ExpenseProxy object) {
return object.getCreated();
}
};
defaultComparator = createColumnComparator(createdGetter, false);
Comparator<ExpenseProxy> createdDesc = createColumnComparator(
createdGetter, true);
addColumn(table, "Created",
new DateCell(DateTimeFormat.getFormat("MMM dd yyyy")), createdGetter,
defaultComparator, createdDesc);
lastComparator = defaultComparator;
// Description column.
addColumn(table, "Description", new TextCell(),
new GetValue<ExpenseProxy, String>() {
public String getValue(ExpenseProxy object) {
return object.getDescription();
}
});
// Category column.
addColumn(table, "Category", new TextCell(),
new GetValue<ExpenseProxy, String>() {
public String getValue(ExpenseProxy object) {
return object.getCategory();
}
});
// Amount column.
final GetValue<ExpenseProxy, Double> amountGetter = new GetValue<ExpenseProxy, Double>() {
public Double getValue(ExpenseProxy object) {
return object.getAmount();
}
};
Comparator<ExpenseProxy> amountAsc = createColumnComparator(amountGetter,
false);
Comparator<ExpenseProxy> amountDesc = createColumnComparator(amountGetter,
true);
addColumn(table, "Amount",
new NumberCell(NumberFormat.getCurrencyFormat()),
new GetValue<ExpenseProxy, Number>() {
public Number getValue(ExpenseProxy object) {
return amountGetter.getValue(object);
}
}, amountAsc, amountDesc);
// Dialog box to obtain a reason for a denial
denialPopup.addCloseHandler(new CloseHandler<PopupPanel>() {
public void onClose(CloseEvent<PopupPanel> event) {
String reasonDenied = denialPopup.getReasonDenied();
ExpenseProxy record = denialPopup.getExpenseRecord();
if (reasonDenied == null || reasonDenied.length() == 0) {
// Clear the view data.
final Object key = items.getKey(record);
approvalCell.clearViewData(key);
// We need to redraw the table to reset the select box.
syncCommit(record, null);
} else {
updateExpenseRecord(record, "Denied", reasonDenied);
}
// Return focus to the table.
table.setFocus(true);
}
});
// Approval column.
approvalCell = new ApprovalCell();
Column<ExpenseProxy, String> approvalColumn = addColumn(table,
"Approval Status", approvalCell, new GetValue<ExpenseProxy, String>() {
public String getValue(ExpenseProxy object) {
return object.getApproval();
}
});
approvalColumn.setFieldUpdater(new FieldUpdater<ExpenseProxy, String>() {
public void update(int index, final ExpenseProxy object, String value) {
if ("Denied".equals(value)) {
denialPopup.setExpenseRecord(object);
denialPopup.setReasonDenied(object.getReasonDenied());
denialPopup.popup();
} else {
updateExpenseRecord(object, value, "");
}
}
});
// Spacer column.
table.addColumn(new SpacerColumn<ExpenseProxy>());
return table;
}
/**
* Refresh the total cost and approved amount.
*/
private void refreshCost() {
double totalCost = 0;
totalApproved = 0;
List<ExpenseProxy> records = items.getList();
for (ExpenseProxy record : records) {
double cost = record.getAmount();
totalCost += cost;
if (Approval.APPROVED.is(record.getApproval())) {
totalApproved += cost;
}
}
double unreconciled = totalCost - totalApproved;
costLabel.setInnerText(formatCurrency(totalCost));
approvedLabel.setInnerText(formatCurrency(totalApproved));
unreconciledLabel.setInnerText(formatCurrency(unreconciled));
}
/**
* Request the expenses.
*/
private void requestExpenses() {
// Cancel the timer since we are about to send a request.
refreshTimer.cancel();
lastReceiver = new Receiver<List<ExpenseProxy>>() {
@Override
public void onSuccess(List<ExpenseProxy> newValues) {
if (this == lastReceiver) {
List<ExpenseProxy> list = new ArrayList<ExpenseProxy>(newValues);
if (lastComparator != null) {
sortExpenses(list, lastComparator);
}
items.setList(list);
refreshCost();
// Add the new keys and changed values to the known keys.
boolean isInitialData = knownExpenseKeys == null;
if (knownExpenseKeys == null) {
knownExpenseKeys = new HashMap<Object, ExpenseProxy>();
}
for (ExpenseProxy value : newValues) {
Object key = items.getKey(value);
if (!isInitialData) {
ExpenseProxy existing = knownExpenseKeys.get(key);
if (existing == null
|| !value.getAmount().equals(existing.getAmount())
|| !value.getDescription().equals(existing.getDescription())
|| !value.getCategory().equals(existing.getCategory())) {
(new PhaseAnimation.CellTablePhaseAnimation<ExpenseProxy>(
table, value, items)).run();
}
}
knownExpenseKeys.put(key, value);
}
}
// Reschedule the timer.
refreshTimer.schedule(REFRESH_INTERVAL);
}
};
expensesRequestFactory.expenseRequest().findExpensesByReport(report.getId()).with(
getExpenseColumns()).fire(lastReceiver);
}
/**
* Save the notes that the user entered in the notes box.
*/
private void saveNotes() {
// Early exit if the notes haven't changed.
final String pendingNotes = notesBox.getText();
if (pendingNotes.equals(report.getNotes())) {
setNotesEditState(false, false, pendingNotes);
return;
}
// Switch to the pending view.
setNotesEditState(false, true, pendingNotes);
// Submit the delta.
ReportRequest editRequest = expensesRequestFactory.reportRequest();
ReportProxy editableReport = editRequest.edit(report);
editableReport.setNotes(pendingNotes);
editRequest.persist().using(editableReport).fire(new Receiver<Void>() {
@Override
public void onSuccess(Void ignore) {
}
});
}
/**
* Set the state of the notes section.
*
* @param editable true for edit state, false for view state
* @param pending true if changes are pending, false if not
* @param notesText the current notes
*/
private void setNotesEditState(boolean editable, boolean pending,
String notesText) {
notesBox.setText(notesText);
notes.setInnerText(notesText);
notesBox.setVisible(editable && !pending);
setVisible(notes, !editable);
setVisible(notesEditLinkWrapper, !editable && !pending);
setVisible(notesPending, pending);
notesBox.setFocus(editable);
}
/**
* Set the {@link ReportProxy} to show.
*
* @param report the {@link ReportProxy}
* @param department the selected department, or ""
* @param employee the selected employee, or null
*/
private void setReportRecord(ReportProxy report, String department,
EmployeeProxy employee) {
this.report = report;
knownExpenseKeys = null;
reportName.setInnerText(report.getPurpose());
costLabel.setInnerText("");
approvedLabel.setInnerText("");
unreconciledLabel.setInnerText("");
setNotesEditState(false, false, report.getNotes());
items.getList().clear();
totalApproved = 0;
// Update the breadcrumb.
reportsLink.setText(ExpenseReportList.getBreadcrumb(department, employee));
// Reset sorting state of table
lastComparator = defaultComparator;
if (allHeaders.size() > 0) {
for (SortableHeader header : allHeaders) {
header.setSorted(false);
header.setReverseSort(true);
}
allHeaders.get(0).setSorted(true);
allHeaders.get(0).setReverseSort(false);
table.redrawHeaders();
}
// Request the expenses.
requestExpenses();
}
/**
* Show the error popup.
*
* @param errorMessage the error message
*/
private void showErrorPopup(String errorMessage) {
errorPopupMessage.setText(errorMessage);
errorPopup.center();
}
private void sortExpenses(List<ExpenseProxy> list,
final Comparator<ExpenseProxy> comparator) {
lastComparator = comparator;
Collections.sort(list, comparator);
}
/**
* Update the state of a pending approval change.
*
* @param record the {@link ExpenseProxy} to sync
* @param message the error message if rejected, or null if accepted
*/
private void syncCommit(ExpenseProxy record, String message) {
final Object key = items.getKey(record);
if (message != null) {
final ApprovalViewData avd = approvalCell.getViewData(key);
if (avd != null) {
avd.reject(message);
}
}
// Redraw the table so the changes are applied.
table.redraw();
}
private void updateExpenseRecord(final ExpenseProxy record, String approval,
String reasonDenied) {
// Verify that the total is under the cap.
if (Approval.APPROVED.is(approval)
&& !Approval.APPROVED.is(record.getApproval())) {
double amount = record.getAmount();
if (amount + totalApproved > MAX_COST) {
syncCommit(record,
"The total approved amount for an expense report cannot exceed $"
+ MAX_COST + ".");
return;
}
}
// Create a delta and sync with the value store.
ExpenseRequest editRequest = expensesRequestFactory.expenseRequest();
ExpenseProxy editableRecord = editRequest.edit(record);
editableRecord.setApproval(approval);
editableRecord.setReasonDenied(reasonDenied);
editRequest.persist().using(editableRecord).fire(new Receiver<Void>() {
@Override
public void onSuccess(Void ignore) {
}
// TODO: use onViolations for checking constraint violations.
});
}
}