From 6762b3e72173658b6e0d2c44d60334deeb77e578 Mon Sep 17 00:00:00 2001 From: Aaron Lindsay Date: Wed, 24 May 2017 19:47:18 -0400 Subject: [PATCH] Reduxify CRUD actions for transactions --- js/actions/TransactionActions.js | 219 +++++++++++++++++++++++ js/actions/TransactionPageActions.js | 84 --------- js/components/AccountRegister.js | 135 ++++---------- js/components/AccountsTab.js | 6 + js/constants/TransactionConstants.js | 15 ++ js/constants/TransactionPageConstants.js | 6 - js/containers/AccountsTabContainer.js | 12 +- js/reducers/MoneyGoReducer.js | 2 + js/reducers/TransactionPageReducer.js | 48 +++-- js/reducers/TransactionReducer.js | 32 ++++ transactions.go | 14 +- 11 files changed, 370 insertions(+), 203 deletions(-) create mode 100644 js/actions/TransactionActions.js delete mode 100644 js/actions/TransactionPageActions.js create mode 100644 js/constants/TransactionConstants.js delete mode 100644 js/constants/TransactionPageConstants.js create mode 100644 js/reducers/TransactionReducer.js diff --git a/js/actions/TransactionActions.js b/js/actions/TransactionActions.js new file mode 100644 index 0000000..354ee84 --- /dev/null +++ b/js/actions/TransactionActions.js @@ -0,0 +1,219 @@ +var TransactionConstants = require('../constants/TransactionConstants'); + +var ErrorActions = require('./ErrorActions'); + +var models = require('../models.js'); +var Account = models.Account; +var Transaction = models.Transaction; +var Error = models.Error; + +var Big = require('big.js'); + +function fetchTransactionPage(account, pageSize, page) { + return { + type: TransactionConstants.FETCH_TRANSACTION_PAGE, + account: account, + pageSize: pageSize, + page: page + } +} + +function transactionPageFetched(account, pageSize, page, numPages, + transactions, endingBalance) { + return { + type: TransactionConstants.TRANSACTION_PAGE_FETCHED, + account: account, + pageSize: pageSize, + page: page, + numPages: numPages, + transactions: transactions, + endingBalance: endingBalance + } +} + +function createTransaction() { + return { + type: TransactionConstants.CREATE_TRANSACTION + } +} + +function transactionCreated(transaction) { + return { + type: TransactionConstants.TRANSACTION_CREATED, + transaction: transaction + } +} + +function updateTransaction() { + return { + type: TransactionConstants.UPDATE_TRANSACTION + } +} + +function transactionUpdated(transaction) { + return { + type: TransactionConstants.TRANSACTION_UPDATED, + transaction: transaction + } +} + +function removeTransaction() { + return { + type: TransactionConstants.REMOVE_TRANSACTION + } +} + +function transactionRemoved(transactionId) { + return { + type: TransactionConstants.TRANSACTION_REMOVED, + transactionId: transactionId + } +} + +function transactionSelected(transactionId) { + return { + type: TransactionConstants.TRANSACTION_SELECTED, + transactionId: transactionId + } +} + +function selectionCleared() { + return { + type: TransactionConstants.SELECTION_CLEARED + } +} + +function fetchPage(account, pageSize, page) { + return function (dispatch) { + dispatch(fetchTransactionPage(account, pageSize, page)); + + $.ajax({ + type: "GET", + dataType: "json", + url: "account/"+account.AccountId+"/transactions?sort=date-desc&limit="+pageSize+"&page="+page, + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + dispatch(ErrorActions.serverError(e)); + return; + } + + var transactions = []; + var balance = new Big(data.EndingBalance); + + for (var i = 0; i < data.Transactions.length; i++) { + var t = new Transaction(); + t.fromJSON(data.Transactions[i]); + + t.Balance = balance.plus(0); // Make a copy of the current balance + // Keep a talley of the running balance of these transactions + for (var j = 0; j < data.Transactions[i].Splits.length; j++) { + var split = data.Transactions[i].Splits[j]; + if (account.AccountId == split.AccountId) { + balance = balance.minus(split.Amount); + } + } + transactions.push(t); + } + var a = new Account(); + a.fromJSON(data.Account); + + var numPages = Math.ceil(data.TotalTransactions / pageSize); + + dispatch(transactionPageFetched(account, pageSize, page, + numPages, transactions, new Big(data.EndingBalance))); + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(error)); + } + }); + }; +} + +function create(transaction) { + return function (dispatch) { + dispatch(createTransaction()); + + $.ajax({ + type: "POST", + dataType: "json", + url: "transaction/", + data: {transaction: transaction.toJSON()}, + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + dispatch(ErrorActions.serverError(e)); + } else { + var t = new Transaction(); + t.fromJSON(data); + dispatch(transactionCreated(t)); + } + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(error)); + } + }); + }; +} + +function update(transaction) { + return function (dispatch) { + dispatch(updateTransaction()); + + $.ajax({ + type: "PUT", + dataType: "json", + url: "transaction/"+transaction.TransactionId+"/", + data: {transaction: transaction.toJSON()}, + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + dispatch(ErrorActions.serverError(e)); + } else { + var t = new Transaction(); + t.fromJSON(data); + dispatch(transactionUpdated(t)); + } + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(error)); + } + }); + }; +} + +function remove(transaction) { + return function(dispatch) { + dispatch(removeTransaction()); + + $.ajax({ + type: "DELETE", + dataType: "json", + url: "transaction/"+transaction.TransactionId+"/", + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + dispatch(ErrorActions.serverError(e)); + } else { + dispatch(transactionRemoved(transaction.TransactionId)); + } + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(error)); + } + }); + }; +} + +module.exports = { + fetchPage: fetchPage, + create: create, + update: update, + remove: remove, + select: transactionSelected, + unselect: selectionCleared +}; diff --git a/js/actions/TransactionPageActions.js b/js/actions/TransactionPageActions.js deleted file mode 100644 index f2d29da..0000000 --- a/js/actions/TransactionPageActions.js +++ /dev/null @@ -1,84 +0,0 @@ -var TransactionPageConstants = require('../constants/TransactionPageConstants'); - -var ErrorActions = require('./ErrorActions'); - -var models = require('../models.js'); -var Account = models.Account; -var Transaction = models.Transaction; -var Error = models.Error; - -var Big = require('big.js'); - -function fetchTransactionPage(account, pageSize, page) { - return { - type: TransactionPageConstants.FETCH_TRANSACTION_PAGE, - account: account, - pageSize: pageSize, - page: page - } -} - -function transactionPageFetched(account, pageSize, page, numPages, - transactions, endingBalance) { - return { - type: TransactionPageConstants.TRANSACTION_PAGE_FETCHED, - account: account, - pageSize: pageSize, - page: page, - numPages: numPages, - transactions: transactions, - endingBalance: endingBalance - } -} - -function fetch(account, pageSize, page) { - return function (dispatch) { - dispatch(fetchTransactionPage(account, pageSize, page)); - - $.ajax({ - type: "GET", - dataType: "json", - url: "account/"+account.AccountId+"/transactions?sort=date-desc&limit="+pageSize+"&page="+page, - success: function(data, status, jqXHR) { - var e = new Error(); - e.fromJSON(data); - if (e.isError()) { - dispatch(ErrorActions.serverError(e)); - return; - } - - var transactions = []; - var balance = new Big(data.EndingBalance); - - for (var i = 0; i < data.Transactions.length; i++) { - var t = new Transaction(); - t.fromJSON(data.Transactions[i]); - - t.Balance = balance.plus(0); // Make a copy of the current balance - // Keep a talley of the running balance of these transactions - for (var j = 0; j < data.Transactions[i].Splits.length; j++) { - var split = data.Transactions[i].Splits[j]; - if (account.AccountId == split.AccountId) { - balance = balance.minus(split.Amount); - } - } - transactions.push(t); - } - var a = new Account(); - a.fromJSON(data.Account); - - var numPages = Math.ceil(data.TotalTransactions / pageSize); - - dispatch(transactionPageFetched(account, pageSize, page, - numPages, transactions, new Big(data.EndingBalance))); - }, - error: function(jqXHR, status, error) { - dispatch(ErrorActions.ajaxError(error)); - } - }); - }; -} - -module.exports = { - fetch: fetch -}; diff --git a/js/components/AccountRegister.js b/js/components/AccountRegister.js index f9002f7..a8f4e1c 100644 --- a/js/components/AccountRegister.js +++ b/js/components/AccountRegister.js @@ -51,7 +51,7 @@ const TransactionRow = React.createClass({ const refs = ["date", "number", "description", "account", "status", "amount"]; for (var ref in refs) { if (this.refs[refs[ref]] == e.target) { - this.props.onEdit(this.props.transaction, refs[ref]); + this.props.onSelect(this.props.transaction.TransactionId, refs[ref]); return; } } @@ -63,6 +63,7 @@ const TransactionRow = React.createClass({ var accountName = ""; var status = ""; var security = this.props.securities[this.props.account.SecurityId]; + var balance = security.Symbol + " " + "?" if (this.props.transaction.isTransaction()) { var thisAccountSplit; @@ -86,12 +87,12 @@ const TransactionRow = React.createClass({ } var amount = security.Symbol + " " + thisAccountSplit.Amount.toFixed(security.Precision); - var balance = security.Symbol + " " + this.props.transaction.Balance.toFixed(security.Precision); + if (this.props.transaction.hasOwnProperty("Balance")) + balance = security.Symbol + " " + this.props.transaction.Balance.toFixed(security.Precision); status = TransactionStatusMap[this.props.transaction.Status]; number = thisAccountSplit.Number; } else { var amount = security.Symbol + " " + (new Big(0.0)).toFixed(security.Precision); - var balance = security.Symbol + " " + (new Big(0.0)).toFixed(security.Precision); } return ( @@ -650,8 +651,7 @@ module.exports = React.createClass({ getInitialState: function() { return { importingTransactions: false, - editingTransaction: false, - selectedTransaction: new Transaction(), + newTransaction: null, height: 0 }; }, @@ -659,21 +659,24 @@ module.exports = React.createClass({ var div = ReactDOM.findDOMNode(this); this.setState({height: div.parentElement.clientHeight - 64}); }, + componentWillReceiveProps: function(nextProps) { + if (!nextProps.transactionPage.upToDate && nextProps.selectedAccount != -1) { + nextProps.onFetchTransactionPage(nextProps.accounts[nextProps.selectedAccount], nextProps.transactionPage.pageSize, nextProps.transactionPage.page); + } + }, componentDidMount: function() { this.resize(); var self = this; $(window).resize(function() {self.resize();}); }, handleEditTransaction: function(transaction) { - this.setState({ - selectedTransaction: transaction, - editingTransaction: true - }); + this.props.onSelectTransaction(transaction.TransactionId); }, handleEditingCancel: function() { this.setState({ - editingTransaction: false + newTransaction: null }); + this.props.onUnselectTransaction(); }, handleNewTransactionClicked: function() { var newTransaction = new Transaction(); @@ -684,8 +687,7 @@ module.exports = React.createClass({ newTransaction.Splits[0].AccountId = this.props.accounts[this.props.selectedAccount].AccountId; this.setState({ - editingTransaction: true, - selectedTransaction: newTransaction + newTransaction: newTransaction }); }, handleImportClicked: function() { @@ -717,94 +719,24 @@ module.exports = React.createClass({ } } }, - onNewTransaction: function() { - this.props.onFetchTransactionPage(this.props.accounts[this.props.selectedAccount], this.props.pageSize, this.props.transactionPage.page); - }, - onUpdatedTransaction: function() { - this.props.onFetchTransactionPage(this.props.accounts[this.props.selectedAccount], this.props.pageSize, this.props.transactionPage.page); - }, - onDeletedTransaction: function() { - this.props.onFetchTransactionPage(this.props.accounts[this.props.selectedAccount], this.props.pageSize, this.props.transactionPage.page); - }, - createNewTransaction: function(transaction) { - $.ajax({ - type: "POST", - dataType: "json", - url: "transaction/", - data: {transaction: transaction.toJSON()}, - success: function(data, status, jqXHR) { - var e = new Error(); - e.fromJSON(data); - if (e.isError()) { - this.setState({error: e}); - } else { - this.onNewTransaction(); - } - }.bind(this), - error: this.ajaxError - }); - }, - updateTransaction: function(transaction) { - $.ajax({ - type: "PUT", - dataType: "json", - url: "transaction/"+transaction.TransactionId+"/", - data: {transaction: transaction.toJSON()}, - success: function(data, status, jqXHR) { - var e = new Error(); - e.fromJSON(data); - if (e.isError()) { - this.setState({error: e}); - } else { - this.onUpdatedTransaction(); - } - }.bind(this), - error: this.ajaxError - }); - }, - deleteTransaction: function(transaction) { - $.ajax({ - type: "DELETE", - dataType: "json", - url: "transaction/"+transaction.TransactionId+"/", - success: function(data, status, jqXHR) { - var e = new Error(); - e.fromJSON(data); - if (e.isError()) { - this.setState({error: e}); - } else { - this.onDeletedTransaction(); - } - }.bind(this), - error: this.ajaxError - }); - }, handleImportComplete: function() { this.setState({importingTransactions: false}); this.props.onFetchTransactionPage(this.props.accounts[this.props.selectedAccount], this.props.pageSize, this.props.transactionPage.page); }, handleDeleteTransaction: function(transaction) { - this.setState({ - editingTransaction: false - }); - this.deleteTransaction(transaction); + this.props.onDeleteTransaction(transaction); + this.props.onUnselectTransaction(); }, handleUpdateTransaction: function(transaction) { - this.setState({ - editingTransaction: false - }); if (transaction.TransactionId != -1) { - this.updateTransaction(transaction); + this.props.onUpdateTransaction(transaction); } else { - this.createNewTransaction(transaction); - } - }, - componentWillReceiveProps: function(nextProps) { - if (nextProps.selectedAccount != this.props.selectedAccount) { - this.setState({ - selectedTransaction: new Transaction(), - }); + this.props.onCreateTransaction(transaction); } + this.props.onUnselectTransaction(); + this.setState({ + newTransaction: null + }); }, render: function() { var name = "Please select an account"; @@ -815,15 +747,16 @@ module.exports = React.createClass({ var transactionRows = []; for (var i = 0; i < this.props.transactionPage.transactions.length; i++) { - var t = this.props.transactionPage.transactions[i]; + var transactionId = this.props.transactionPage.transactions[i]; + var transaction = this.props.transactions[transactionId]; transactionRows.push(( + onSelect={this.props.onSelectTransaction}/> )); } @@ -850,11 +783,21 @@ module.exports = React.createClass({ var disabled = (this.props.selectedAccount == -1) ? true : false; + var transactionSelected = false; + var selectedTransaction = new Transaction(); + if (this.state.newTransaction != null) { + selectedTransaction = this.state.newTransaction; + transactionSelected = true; + } else if (this.props.transactionPage.selection != -1) { + selectedTransaction = this.props.transactions[this.props.transactionPage.selection]; + transactionSelected = true; + } + return (
diff --git a/js/constants/TransactionConstants.js b/js/constants/TransactionConstants.js new file mode 100644 index 0000000..6897a9d --- /dev/null +++ b/js/constants/TransactionConstants.js @@ -0,0 +1,15 @@ +var keyMirror = require('keymirror'); + +module.exports = keyMirror({ + FETCH_TRANSACTION_PAGE: null, + TRANSACTION_PAGE_FETCHED: null, + CREATE_TRANSACTION: null, + TRANSACTION_CREATED: null, + UPDATE_TRANSACTION: null, + TRANSACTION_UPDATED: null, + REMOVE_TRANSACTION: null, + SELECT_TRANSACTION: null, + TRANSACTION_REMOVED: null, + TRANSACTION_SELECTED: null, + SELECTION_CLEARED: null +}); diff --git a/js/constants/TransactionPageConstants.js b/js/constants/TransactionPageConstants.js deleted file mode 100644 index 1feda45..0000000 --- a/js/constants/TransactionPageConstants.js +++ /dev/null @@ -1,6 +0,0 @@ -var keyMirror = require('keymirror'); - -module.exports = keyMirror({ - FETCH_TRANSACTION_PAGE: null, - TRANSACTION_PAGE_FETCHED: null -}); diff --git a/js/containers/AccountsTabContainer.js b/js/containers/AccountsTabContainer.js index 78e877d..7180f0a 100644 --- a/js/containers/AccountsTabContainer.js +++ b/js/containers/AccountsTabContainer.js @@ -1,7 +1,7 @@ var connect = require('react-redux').connect; var AccountActions = require('../actions/AccountActions'); -var TransactionPageActions = require('../actions/TransactionPageActions'); +var TransactionActions = require('../actions/TransactionActions'); var AccountsTab = require('../components/AccountsTab'); @@ -17,6 +17,7 @@ function mapStateToProps(state) { securities: state.securities, security_list: security_list, selectedAccount: state.selectedAccount, + transactions: state.transactions, transactionPage: state.transactionPage } } @@ -25,9 +26,14 @@ function mapDispatchToProps(dispatch) { return { onCreateAccount: function(account) {dispatch(AccountActions.create(account))}, onUpdateAccount: function(account) {dispatch(AccountActions.update(account))}, - onDeleteAccount: function(accountId) {dispatch(AccountActions.remove(accountId))}, + onDeleteAccount: function(account) {dispatch(AccountActions.remove(account))}, onSelectAccount: function(accountId) {dispatch(AccountActions.select(accountId))}, - onFetchTransactionPage: function(account, pageSize, page) {dispatch(TransactionPageActions.fetch(account, pageSize, page))}, + onCreateTransaction: function(transaction) {dispatch(TransactionActions.create(transaction))}, + onUpdateTransaction: function(transaction) {dispatch(TransactionActions.update(transaction))}, + onDeleteTransaction: function(transaction) {dispatch(TransactionActions.remove(transaction))}, + onSelectTransaction: function(transactionId) {dispatch(TransactionActions.select(transactionId))}, + onUnselectTransaction: function() {dispatch(TransactionActions.unselect())}, + onFetchTransactionPage: function(account, pageSize, page) {dispatch(TransactionActions.fetchPage(account, pageSize, page))}, } } diff --git a/js/reducers/MoneyGoReducer.js b/js/reducers/MoneyGoReducer.js index 7664a66..62f1b3f 100644 --- a/js/reducers/MoneyGoReducer.js +++ b/js/reducers/MoneyGoReducer.js @@ -9,6 +9,7 @@ var SelectedAccountReducer = require('./SelectedAccountReducer'); var SelectedSecurityReducer = require('./SelectedSecurityReducer'); var ReportReducer = require('./ReportReducer'); var SelectedReportReducer = require('./SelectedReportReducer'); +var TransactionReducer = require('./TransactionReducer'); var TransactionPageReducer = require('./TransactionPageReducer'); var ErrorReducer = require('./ErrorReducer'); @@ -22,6 +23,7 @@ module.exports = Redux.combineReducers({ selectedSecurity: SelectedSecurityReducer, reports: ReportReducer, selectedReport: SelectedReportReducer, + transactions: TransactionReducer, transactionPage: TransactionPageReducer, error: ErrorReducer }); diff --git a/js/reducers/TransactionPageReducer.js b/js/reducers/TransactionPageReducer.js index 5e8696a..b776ef8 100644 --- a/js/reducers/TransactionPageReducer.js +++ b/js/reducers/TransactionPageReducer.js @@ -1,30 +1,34 @@ var assign = require('object-assign'); -var TransactionPageConstants = require('../constants/TransactionPageConstants'); +var TransactionConstants = require('../constants/TransactionConstants'); var UserConstants = require('../constants/UserConstants'); +var AccountConstants = require('../constants/AccountConstants'); var Account = require('../models').Account; -module.exports = function(state = {account: new Account(), pageSize: 1, page: 0, numPages: 0, transactions: [], endingBalance: "0" }, action) { +module.exports = function(state = {account: new Account(), pageSize: 1, page: 0, numPages: 0, transactions: [], endingBalance: "0", selection: -1, upToDate: false }, action) { switch (action.type) { - case TransactionPageConstants.FETCH_TRANSACTION_PAGE: - return { + case AccountConstants.ACCOUNT_SELECTED: + case TransactionConstants.FETCH_TRANSACTION_PAGE: + return assign({}, state, { account: action.account, pageSize: action.pageSize, page: action.page, numPages: 0, transactions: [], - endingBalance: "0" - }; - case TransactionPageConstants.TRANSACTION_PAGE_FETCHED: - return { + endingBalance: "0", + upToDate: true + }); + case TransactionConstants.TRANSACTION_PAGE_FETCHED: + return assign({}, state, { account: action.account, pageSize: action.pageSize, page: action.page, numPages: action.numPages, - transactions: action.transactions, - endingBalance: action.endingBalance - }; + transactions: action.transactions.map(function(t) {return t.TransactionId}), + endingBalance: action.endingBalance, + upToDate: true + }); case UserConstants.USER_LOGGEDOUT: return { account: new Account(), @@ -32,8 +36,28 @@ module.exports = function(state = {account: new Account(), pageSize: 1, page: 0, page: 0, numPages: 0, transactions: [], - endingBalance: "0" + endingBalance: "0", + selection: -1, + upToDate: false }; + case TransactionConstants.TRANSACTION_CREATED: + case TransactionConstants.TRANSACTION_UPDATED: + return assign({}, state, { + upToDate: false + }); + case TransactionConstants.TRANSACTION_REMOVED: + return assign({}, state, { + transactions: state.transactions.filter(function(t) {return t != action.transactionId}), + upToDate: false + }); + case TransactionConstants.TRANSACTION_SELECTED: + return assign({}, state, { + selection: action.transactionId + }); + case TransactionConstants.SELECTION_CLEARED: + return assign({}, state, { + selection: -1 + }); default: return state; } diff --git a/js/reducers/TransactionReducer.js b/js/reducers/TransactionReducer.js new file mode 100644 index 0000000..79d75da --- /dev/null +++ b/js/reducers/TransactionReducer.js @@ -0,0 +1,32 @@ +var assign = require('object-assign'); + +var TransactionConstants = require('../constants/TransactionConstants'); +var UserConstants = require('../constants/UserConstants'); + +module.exports = function(state = {}, action) { + switch (action.type) { + case TransactionConstants.TRANSACTION_PAGE_FETCHED: + var transactions = assign({}, state); + for (var tidx in action.transactions) { + var t = action.transactions[tidx]; + transactions = assign({}, transactions, { + [t.TransactionId]: t + }); + } + return transactions; + case TransactionConstants.TRANSACTION_CREATED: + case TransactionConstants.TRANSACTION_UPDATED: + var transaction = action.transaction; + return assign({}, state, { + [transaction.TransactionId]: transaction + }); + case TransactionConstants.TRANSACTION_REMOVED: + var transactions = assign({}, state); + delete transactions[action.transactionId]; + return transactions; + case UserConstants.USER_LOGGEDOUT: + return {}; + default: + return state; + } +}; diff --git a/transactions.go b/transactions.go index 5598d7d..9aa3a4a 100644 --- a/transactions.go +++ b/transactions.go @@ -508,7 +508,12 @@ func TransactionHandler(w http.ResponseWriter, r *http.Request) { return } - WriteSuccess(w) + err = transaction.Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } } else if r.Method == "GET" { transactionid, err := GetURLID(r.URL.Path) @@ -590,7 +595,12 @@ func TransactionHandler(w http.ResponseWriter, r *http.Request) { return } - WriteSuccess(w) + err = transaction.Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } } else if r.Method == "DELETE" { transactionid, err := GetURLID(r.URL.Path) if err != nil {