Reduxify CRUD actions for transactions

This commit is contained in:
Aaron Lindsay 2017-05-24 19:47:18 -04:00
parent 7f736812b3
commit 6762b3e721
11 changed files with 370 additions and 203 deletions

View File

@ -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
};

View File

@ -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
};

View File

@ -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((
<TransactionRow
key={t.TransactionId}
transaction={t}
key={transactionId}
transaction={transaction}
account={this.props.accounts[this.props.selectedAccount]}
accounts={this.props.accounts}
securities={this.props.securities}
onEdit={this.handleEditTransaction}/>
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 (
<div className="transactions-container">
<AddEditTransactionModal
show={this.state.editingTransaction}
transaction={this.state.selectedTransaction}
show={transactionSelected}
transaction={selectedTransaction}
accounts={this.props.accounts}
accountChildren={this.props.accountChildren}
onCancel={this.handleEditingCancel}

View File

@ -482,7 +482,13 @@ module.exports = React.createClass({
accounts={this.props.accounts}
accountChildren={this.props.accountChildren}
securities={this.props.securities}
transactions={this.props.transactions}
transactionPage={this.props.transactionPage}
onCreateTransaction={this.props.onCreateTransaction}
onUpdateTransaction={this.props.onUpdateTransaction}
onDeleteTransaction={this.props.onDeleteTransaction}
onSelectTransaction={this.props.onSelectTransaction}
onUnselectTransaction={this.props.onUnselectTransaction}
onFetchTransactionPage={this.props.onFetchTransactionPage}/>
</Col>
</Row></Grid>

View File

@ -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
});

View File

@ -1,6 +0,0 @@
var keyMirror = require('keymirror');
module.exports = keyMirror({
FETCH_TRANSACTION_PAGE: null,
TRANSACTION_PAGE_FETCHED: null
});

View File

@ -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))},
}
}

View File

@ -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
});

View File

@ -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;
}

View File

@ -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;
}
};

View File

@ -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 {