From fcea2d380bd180243c954197c2ed0cbe611b472d Mon Sep 17 00:00:00 2001 From: Aaron Lindsay Date: Sat, 29 Aug 2015 09:50:16 -0400 Subject: [PATCH] Add validation of transactions in UI --- static/account_register.js | 49 ++++++++++++++++++++++++++++++++++---- static/accounts.js | 6 ++++- static/models.js | 27 +++++++++++++++++++++ static/stylesheet.css | 19 +++++++++++++++ 4 files changed, 96 insertions(+), 5 deletions(-) diff --git a/static/account_register.js b/static/account_register.js index e6f780a..8284135 100644 --- a/static/account_register.js +++ b/static/account_register.js @@ -1,5 +1,6 @@ // Import all the objects we want to use from ReactBootstrap +var Alert = ReactBootstrap.Alert; var Modal = ReactBootstrap.Modal; var Pagination = ReactBootstrap.Pagination; @@ -127,12 +128,16 @@ const AmountInput = React.createClass({ var symbol = "?"; if (this.props.security) symbol = this.props.security.Symbol; + var bsStyle = ""; + if (this.props.bsStyle) + bsStyle = this.props.bsStyle; return ( ); } @@ -142,7 +147,10 @@ const AddEditTransactionModal = React.createClass({ _getInitialState: function(props) { // Ensure we can edit this without screwing up other copies of it var t = props.transaction.deepCopy(); - return {transaction: t}; + return { + errorAlert: [], + transaction: t + }; }, getInitialState: function() { return this._getInitialState(this.props); @@ -232,6 +240,24 @@ const AddEditTransactionModal = React.createClass({ }); }, handleSubmit: function() { + var errorString = "" + var imbalancedSecurityList = this.state.transaction.imbalancedSplitSecurities(this.props.account_map); + if (imbalancedSecurityList.length > 0) + errorString = "Transaction must balance" + for (var i = 0; i < this.state.transaction.Splits.length; i++) { + var s = this.state.transaction.Splits[i]; + if (!(s.AccountId in this.props.account_map)) { + errorString = "All accounts must be valid" + } + } + + if (errorString.length > 0) { + this.setState({ + errorAlert: (Error Saving Transaction: {errorString}) + }); + return; + } + if (this.props.onSubmit != null) this.props.onSubmit(this.state.transaction); }, @@ -250,13 +276,25 @@ const AddEditTransactionModal = React.createClass({ ); } + var imbalancedSecurityList = this.state.transaction.imbalancedSplitSecurities(this.props.account_map); + var imbalancedSecurityMap = {}; + for (i = 0; i < imbalancedSecurityList.length; i++) + imbalancedSecurityMap[imbalancedSecurityList[i]] = i; + splits = []; for (var i = 0; i < this.state.transaction.Splits.length; i++) { var self = this; var s = this.state.transaction.Splits[i]; var security = null; - if (this.props.account_map[s.AccountId]) + var amountValidation = ""; + var accountValidation = ""; + if (s.AccountId in this.props.account_map) { security = this.props.security_map[this.props.account_map[s.AccountId].SecurityId]; + if (security.SecurityId in imbalancedSecurityMap) + amountValidation = "error"; + } else { + accountValidation = "has-error"; + } // Define all closures for calling split-updating functions var deleteSplitFn = (function() { @@ -307,12 +345,14 @@ const AddEditTransactionModal = React.createClass({ value={s.AccountId} includeRoot={false} onSelect={updateAccountFn} - ref={"account-"+i} /> + ref={"account-"+i} + className={accountValidation}/> + ref={"amount-"+i} + bsStyle={amountValidation}/> {deleteSplitButton} )); @@ -367,6 +407,7 @@ const AddEditTransactionModal = React.createClass({ bsStyle="success"> + {this.state.errorAlert} diff --git a/static/accounts.js b/static/accounts.js index 8cf5022..2f8fc06 100644 --- a/static/accounts.js +++ b/static/accounts.js @@ -31,6 +31,9 @@ const AccountCombobox = React.createClass({ }, render: function() { var accounts = getAccountDisplayList(this.props.accounts, this.props.includeRoot, this.props.rootName); + var className = ""; + if (this.props.className) + className = this.props.className; return ( + ref="account" + className={className} /> ); } }); diff --git a/static/models.js b/static/models.js index 7a47434..38dfa88 100644 --- a/static/models.js +++ b/static/models.js @@ -323,6 +323,33 @@ Transaction.prototype.deepCopy = function() { return t; } +Transaction.prototype.imbalancedSplitSecurities = function(account_map) { + // Return a list of SecurityIDs for those securities that aren't balanced + // in this transaction's splits. If a split's AccountId is invalid, that + // split is ignored, so those must be checked elsewhere + var splitBalances = {}; + const emptySplit = new Split(); + for (var i = 0; i < this.Splits.length; i++) { + split = this.Splits[i]; + if (split.AccountId == emptySplit.AccountId) { + continue; + } + var securityId = account_map[split.AccountId].SecurityId; + if (securityId in splitBalances) { + splitBalances[securityId] = split.Amount.plus(splitBalances[securityId]); + } else { + splitBalances[securityId] = split.Amount.plus(0); + } + } + var imbalancedIDs = []; + for (var id in splitBalances) { + if (!splitBalances[id].eq(0)) { + imbalancedIDs.push(id); + } + } + return imbalancedIDs; +} + function Error() { this.ErrorId = -1; this.ErrorString = ""; diff --git a/static/stylesheet.css b/static/stylesheet.css index 6f3e0d8..e6a8e77 100644 --- a/static/stylesheet.css +++ b/static/stylesheet.css @@ -115,3 +115,22 @@ div.accounttree-root div { .skinny-pagination { margin: 0px; } + +/* Make Combobox support .has-error class */ +.has-error.rw-widget { + border-color: #843534; +} +.has-error.rw-widget.rw-state-focus { + border-color: #843534; + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset, 0px 0px 6px #CE8483; +} +.has-error.rw-widget > .rw-select { + border-left: 1px solid #843534; + color: #A94442; + background-color: #F2DEDE; +} + +/* Fix Alert Spacing inside */ +.alert.saving-transaction-alert { + margin: 20px 0 0 0; +}