/* * 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. }); } }