var React = require('react');
var ReactDOM = require('react-dom');
var react_update = require('react-addons-update');
var ReactBootstrap = require('react-bootstrap');
var Alert = ReactBootstrap.Alert;
var Modal = ReactBootstrap.Modal;
var Pagination = ReactBootstrap.Pagination;
var Label = ReactBootstrap.Label;
var Table = ReactBootstrap.Table;
var Grid = ReactBootstrap.Grid;
var Row = ReactBootstrap.Row;
var Col = ReactBootstrap.Col;
var Panel = ReactBootstrap.Panel;
var Input = ReactBootstrap.Input;
var Button = ReactBootstrap.Button;
var ButtonGroup = ReactBootstrap.ButtonGroup;
var ButtonToolbar = ReactBootstrap.ButtonToolbar;
var ProgressBar = ReactBootstrap.ProgressBar;
var Glyphicon = ReactBootstrap.Glyphicon;
var ReactWidgets = require('react-widgets')
var DateTimePicker = ReactWidgets.DateTimePicker;
var Combobox = ReactWidgets.Combobox;
var DropdownList = ReactWidgets.DropdownList;
var Big = require('big.js');
var models = require('./models.js');
var Security = models.Security;
var Account = models.Account;
var Split = models.Split;
var Transaction = models.Transaction;
var TransactionStatus = models.TransactionStatus;
var TransactionStatusList = models.TransactionStatusList;
var TransactionStatusMap = models.TransactionStatusMap;
var Error = models.Error;
var getAccountDisplayName = require('./utils.js').getAccountDisplayName;
var AccountCombobox = require('./AccountCombobox.js');
const TransactionRow = React.createClass({
handleClick: function(e) {
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]);
return;
}
}
},
render: function() {
var date = this.props.transaction.Date;
var dateString = date.getFullYear() + "/" + (date.getMonth()+1) + "/" + date.getDate();
var number = ""
var accountName = "";
var status = "";
var security = this.props.security_map[this.props.account.SecurityId];
if (this.props.transaction.isTransaction()) {
var thisAccountSplit;
for (var i = 0; i < this.props.transaction.Splits.length; i++) {
if (this.props.transaction.Splits[i].AccountId == this.props.account.AccountId) {
thisAccountSplit = this.props.transaction.Splits[i];
break;
}
}
if (this.props.transaction.Splits.length == 2) {
var otherSplit = this.props.transaction.Splits[0];
if (otherSplit.AccountId == this.props.account.AccountId)
var otherSplit = this.props.transaction.Splits[1];
if (otherSplit.AccountId == -1)
var accountName = "Unbalanced " + this.props.security_map[otherSplit.SecurityId].Symbol + " transaction";
else
var accountName = getAccountDisplayName(this.props.account_map[otherSplit.AccountId], this.props.account_map);
} else {
accountName = "--Split Transaction--";
}
var amount = security.Symbol + " " + thisAccountSplit.Amount.toFixed(security.Precision);
var 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 (
{dateString}
{number}
{this.props.transaction.Description}
{accountName}
{status}
{amount}
{balance}
);
}
});
const AmountInput = React.createClass({
_getInitialState: function(props) {
// Ensure we can edit this without screwing up other copies of it
var a;
if (props.security)
a = props.value.toFixed(props.security.Precision);
else
a = props.value.toString();
return {
LastGoodAmount: a,
Amount: a
};
},
getInitialState: function() {
return this._getInitialState(this.props);
},
componentWillReceiveProps: function(nextProps) {
if ((!nextProps.value.eq(this.props.value) &&
!nextProps.value.eq(this.getValue())) ||
nextProps.security !== this.props.security) {
this.setState(this._getInitialState(nextProps));
}
},
componentDidMount: function() {
this.refs.amount.getInputDOMNode().onblur = this.onBlur;
},
onBlur: function() {
var a;
if (this.props.security)
a = (new Big(this.getValue())).toFixed(this.props.security.Precision);
else
a = (new Big(this.getValue())).toString();
this.setState({
Amount: a
});
},
onChange: function() {
this.setState({Amount: this.refs.amount.getValue()});
if (this.props.onChange)
this.props.onChange();
},
getValue: function() {
try {
var value = this.refs.amount.getValue();
var ret = new Big(value);
this.setState({LastGoodAmount: value});
return ret;
} catch(err) {
return new Big(this.state.LastGoodAmount);
}
},
render: function() {
var symbol = "?";
if (this.props.security)
symbol = this.props.security.Symbol;
var bsStyle = undefined;
if (this.props.bsStyle)
bsStyle = this.props.bsStyle;
return (
);
}
});
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 {
errorAlert: [],
transaction: t
};
},
getInitialState: function() {
return this._getInitialState(this.props);
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.show && !this.props.show) {
this.setState(this._getInitialState(nextProps));
}
},
handleCancel: function() {
if (this.props.onCancel != null)
this.props.onCancel();
},
handleDescriptionChange: function() {
this.setState({
transaction: react_update(this.state.transaction, {
Description: {$set: this.refs.description.getValue()}
})
});
},
handleDateChange: function(date, string) {
if (date == null)
return;
this.setState({
transaction: react_update(this.state.transaction, {
Date: {$set: date}
})
});
},
handleStatusChange: function(status) {
if (status.hasOwnProperty('StatusId')) {
this.setState({
transaction: react_update(this.state.transaction, {
Status: {$set: status.StatusId}
})
});
}
},
handleAddSplit: function() {
this.setState({
transaction: react_update(this.state.transaction, {
Splits: {$push: [new Split()]}
})
});
},
handleDeleteSplit: function(split) {
this.setState({
transaction: react_update(this.state.transaction, {
Splits: {$splice: [[split, 1]]}
})
});
},
handleUpdateNumber: function(split) {
var transaction = this.state.transaction;
transaction.Splits[split] = react_update(transaction.Splits[split], {
Number: {$set: this.refs['number-'+split].getValue()}
});
this.setState({
transaction: transaction
});
},
handleUpdateMemo: function(split) {
var transaction = this.state.transaction;
transaction.Splits[split] = react_update(transaction.Splits[split], {
Memo: {$set: this.refs['memo-'+split].getValue()}
});
this.setState({
transaction: transaction
});
},
handleUpdateAccount: function(account, split) {
var transaction = this.state.transaction;
transaction.Splits[split] = react_update(transaction.Splits[split], {
SecurityId: {$set: -1},
AccountId: {$set: account.AccountId}
});
this.setState({
transaction: transaction
});
},
handleUpdateAmount: function(split) {
var transaction = this.state.transaction;
transaction.Splits[split] = react_update(transaction.Splits[split], {
Amount: {$set: new Big(this.refs['amount-'+split].getValue())}
});
this.setState({
transaction: transaction
});
},
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);
},
handleDelete: function() {
if (this.props.onDelete != null)
this.props.onDelete(this.state.transaction);
},
render: function() {
var editing = this.props.transaction != null && this.props.transaction.isTransaction();
var headerText = editing ? "Edit" : "Create New";
var buttonText = editing ? "Save Changes" : "Create Transaction";
var deleteButton = [];
if (editing) {
deleteButton = (
);
}
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;
var amountValidation = undefined;
var accountValidation = "";
if (s.AccountId in this.props.account_map) {
security = this.props.security_map[this.props.account_map[s.AccountId].SecurityId];
} else {
if (s.SecurityId in this.props.security_map) {
security = this.props.security_map[s.SecurityId];
}
accountValidation = "has-error";
}
if (security != null && security.SecurityId in imbalancedSecurityMap)
amountValidation = "error";
// Define all closures for calling split-updating functions
var deleteSplitFn = (function() {
var j = i;
return function() {self.handleDeleteSplit(j);};
})();
var updateNumberFn = (function() {
var j = i;
return function() {self.handleUpdateNumber(j);};
})();
var updateMemoFn = (function() {
var j = i;
return function() {self.handleUpdateMemo(j);};
})();
var updateAccountFn = (function() {
var j = i;
return function(account) {self.handleUpdateAccount(account, j);};
})();
var updateAmountFn = (function() {
var j = i;
return function() {self.handleUpdateAmount(j);};
})();
var deleteSplitButton = [];
if (this.state.transaction.Splits.length > 2) {
deleteSplitButton = (
);
}
splits.push((
{deleteSplitButton}
));
}
return (
{headerText} Transaction
{deleteButton}
);
}
});
const ImportType = {
OFX: 1,
Gnucash: 2
};
var ImportTypeList = [];
for (var type in ImportType) {
if (ImportType.hasOwnProperty(type)) {
var name = ImportType[type] == ImportType.OFX ? "OFX/QFX" : type; //QFX is a special snowflake
ImportTypeList.push({'TypeId': ImportType[type], 'Name': name});
}
}
const ImportTransactionsModal = React.createClass({
getInitialState: function() {
return {
importing: false,
imported: false,
importFile: "",
importType: ImportType.Gnucash,
uploadProgress: -1,
error: null};
},
handleCancel: function() {
this.setState(this.getInitialState());
if (this.props.onCancel != null)
this.props.onCancel();
},
handleImportChange: function() {
this.setState({importFile: this.refs.importfile.getValue()});
},
handleTypeChange: function(type) {
this.setState({importType: type.TypeId});
},
handleSubmit: function() {
if (this.props.onSubmit != null)
this.props.onSubmit(this.props.account);
},
handleSetProgress: function(e) {
if (e.lengthComputable) {
var pct = Math.round(e.loaded/e.total*100);
this.setState({uploadProgress: pct});
} else {
this.setState({uploadProgress: 50});
}
},
handleImportTransactions: function() {
var file = this.refs.importfile.getInputDOMNode().files[0];
var formData = new FormData();
formData.append('importfile', file, this.state.importFile);
var url = ""
if (this.state.importType == ImportType.OFX)
url = "account/"+this.props.account.AccountId+"/import/ofx";
else if (this.state.importType == ImportType.Gnucash)
url = "import/gnucash";
this.setState({importing: true});
$.ajax({
type: "POST",
url: url,
data: formData,
xhr: function() {
var xhrObject = $.ajaxSettings.xhr();
if (xhrObject.upload) {
xhrObject.upload.addEventListener('progress', this.handleSetProgress, false);
} else {
console.log("File upload failed because !xhr.upload")
}
return xhrObject;
}.bind(this),
success: function(data, status, jqXHR) {
var e = new Error();
e.fromJSON(data);
if (e.isError()) {
var errString = e.ErrorString;
if (e.ErrorId == 3 /* Invalid Request */) {
errString = "Please check that the file you uploaded is valid and try again.";
}
this.setState({
importing: false,
error: errString
});
return;
}
this.setState({
uploadProgress: 100,
importing: false,
imported: true
});
}.bind(this),
error: function(e) {
this.setState({importing: false});
console.log("error handler", e);
},
// So jQuery doesn't try to process teh data or content-type
cache: false,
contentType: false,
processData: false
});
},
render: function() {
var accountNameLabel = "Performing global import:"
if (this.props.account != null && this.state.importType != ImportType.Gnucash)
accountNameLabel = "Importing to '" + getAccountDisplayName(this.props.account, this.props.account_map) + "' account:";
// Display the progress bar if an upload/import is in progress
var progressBar = [];
if (this.state.importing && this.state.uploadProgress == 100) {
progressBar = ();
} else if (this.state.importing && this.state.uploadProgress != -1) {
progressBar = ();
}
// Create panel, possibly displaying error or success messages
var panel = [];
if (this.state.error != null) {
panel = ({this.state.error});
} else if (this.state.imported) {
panel = (Your import is now complete.);
}
// Display proper buttons, possibly disabling them if an import is in progress
var button1 = [];
var button2 = [];
if (!this.state.imported && this.state.error == null) {
button1 = ();
button2 = ();
} else {
button1 = ();
}
var inputDisabled = (this.state.importing || this.state.error != null || this.state.imported) ? true : false;
// Disable OFX/QFX imports if no account is selected
var disabledTypes = false;
if (this.props.account == null)
disabledTypes = [ImportTypeList[ImportType.OFX - 1]];
return (
Import Transactions
{progressBar}
{panel}
{button1}
{button2}
);
}
});
module.exports = React.createClass({
displayName: "AccountRegister",
getInitialState: function() {
return {
importingTransactions: false,
editingTransaction: false,
selectedTransaction: new Transaction(),
transactions: [],
pageSize: 20,
numPages: 0,
currentPage: 0,
height: 0
};
},
resize: function() {
var div = ReactDOM.findDOMNode(this);
this.setState({height: div.parentElement.clientHeight - 64});
},
componentDidMount: function() {
this.resize();
var self = this;
$(window).resize(function() {self.resize();});
},
handleEditTransaction: function(transaction) {
this.setState({
selectedTransaction: transaction,
editingTransaction: true
});
},
handleEditingCancel: function() {
this.setState({
editingTransaction: false
});
},
handleNewTransactionClicked: function() {
var newTransaction = new Transaction();
newTransaction.Status = TransactionStatus.Entered;
newTransaction.Date = new Date();
newTransaction.Splits.push(new Split());
newTransaction.Splits.push(new Split());
newTransaction.Splits[0].AccountId = this.props.selectedAccount.AccountId;
this.setState({
editingTransaction: true,
selectedTransaction: newTransaction
});
},
handleImportClicked: function() {
this.setState({
importingTransactions: true
});
},
handleImportingCancel: function() {
this.setState({
importingTransactions: false
});
},
ajaxError: function(jqXHR, status, error) {
var e = new Error();
e.ErrorId = 5;
e.ErrorString = "Request Failed: " + status + error;
this.setState({error: e});
},
getTransactionPage: function(account, page) {
$.ajax({
type: "GET",
dataType: "json",
url: "account/"+account.AccountId+"/transactions?sort=date-desc&limit="+this.state.pageSize+"&page="+page,
success: function(data, status, jqXHR) {
var e = new Error();
e.fromJSON(data);
if (e.isError()) {
this.setState({error: 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 (this.props.selectedAccount.AccountId == split.AccountId) {
balance = balance.minus(split.Amount);
}
}
transactions.push(t);
}
var a = new Account();
a.fromJSON(data.Account);
var pages = Math.ceil(data.TotalTransactions / this.state.pageSize);
this.setState({
transactions: transactions,
numPages: pages
});
}.bind(this),
error: this.ajaxError
});
},
handleSelectPage: function(event, selectedEvent) {
var newpage = selectedEvent.eventKey - 1;
// Don't do pages that don't make sense
if (newpage < 0)
newpage = 0;
if (newpage >= this.state.numPages)
newpage = this.state.numPages-1;
if (newpage != this.state.currentPage) {
if (this.props.selectedAccount != null) {
this.getTransactionPage(this.props.selectedAccount, newpage);
}
this.setState({currentPage: newpage});
}
},
onNewTransaction: function() {
this.getTransactionPage(this.props.selectedAccount, this.state.currentPage);
},
onUpdatedTransaction: function() {
this.getTransactionPage(this.props.selectedAccount, this.state.currentPage);
},
onDeletedTransaction: function() {
this.getTransactionPage(this.props.selectedAccount, this.state.currentPage);
},
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.getTransactionPage(this.props.selectedAccount, this.state.currentPage);
},
handleDeleteTransaction: function(transaction) {
this.setState({
editingTransaction: false
});
this.deleteTransaction(transaction);
},
handleUpdateTransaction: function(transaction) {
this.setState({
editingTransaction: false
});
if (transaction.TransactionId != -1) {
this.updateTransaction(transaction);
} else {
this.createNewTransaction(transaction);
}
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.selectedAccount != this.props.selectedAccount) {
this.setState({
selectedTransaction: new Transaction(),
transactions: [],
currentPage: 0
});
if (nextProps.selectedAccount != null)
this.getTransactionPage(nextProps.selectedAccount, 0);
}
},
render: function() {
var name = "Please select an account";
register = [];
if (this.props.selectedAccount != null) {
name = this.props.selectedAccount.Name;
var transactionRows = [];
for (var i = 0; i < this.state.transactions.length; i++) {
var t = this.state.transactions[i];
transactionRows.push((
));
}
var style = {height: this.state.height + "px"};
register = (