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 Form = ReactBootstrap.Form; var FormGroup = ReactBootstrap.FormGroup; var FormControl = ReactBootstrap.FormControl; var InputGroup = ReactBootstrap.InputGroup; var ControlLabel = ReactBootstrap.ControlLabel; var HelpBlock = ReactBootstrap.HelpBlock; 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'); 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').getAccountDisplayName; var AccountCombobox = require('./AccountCombobox'); 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.securities[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.securities[otherSplit.SecurityId].Symbol + " transaction"; else var accountName = getAccountDisplayName(this.props.accounts[otherSplit.AccountId], this.props.accounts); } 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() { ReactDOM.findDOMNode(this.refs.amount).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: ReactDOM.findDOMNode(this.refs.amount).value}); if (this.props.onChange) this.props.onChange(); }, getValue: function() { try { var value = ReactDOM.findDOMNode(this.refs.amount).value; 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; return ( {symbol} ); } }); 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: ReactDOM.findDOMNode(this.refs.description).value} }) }); }, 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: ReactDOM.findDOMNode(this.refs['number-'+split]).value} }); this.setState({ transaction: transaction }); }, handleUpdateMemo: function(split) { var transaction = this.state.transaction; transaction.Splits[split] = react_update(transaction.Splits[split], { Memo: {$set: ReactDOM.findDOMNode(this.refs['memo-'+split]).value} }); 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.accounts); 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.accounts)) { 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.accounts); 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.accounts) { security = this.props.securities[this.props.accounts[s.AccountId].SecurityId]; } else { if (s.SecurityId in this.props.securities) { security = this.props.securities[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
Date Description Status # Memo Account Amount {splits} {this.state.errorAlert}
{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: ReactDOM.findDOMNode(this.refs.importfile).value}); }, 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.accounts) + "' 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
{accountNameLabel} Select an OFX/QFX file to upload. {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.accounts[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.accounts[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 != -1) { this.getTransactionPage(this.props.accounts[this.props.selectedAccount], newpage); } this.setState({currentPage: newpage}); } }, onNewTransaction: function() { this.getTransactionPage(this.props.accounts[this.props.selectedAccount], this.state.currentPage); }, onUpdatedTransaction: function() { this.getTransactionPage(this.props.accounts[this.props.selectedAccount], this.state.currentPage); }, onDeletedTransaction: function() { this.getTransactionPage(this.props.accounts[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.accounts[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 != -1) this.getTransactionPage(nextProps.accounts[nextProps.selectedAccount], 0); } }, render: function() { var name = "Please select an account"; register = []; if (this.props.selectedAccount != -1) { name = this.props.accounts[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 = (
{transactionRows}
Date # Description Account Status Amount Balance
); } var disabled = (this.props.selectedAccount == -1) ? true : false; return (
Transactions for '{name}'
{register}
); } });