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 SplitStatus = models.SplitStatus; var SplitStatusList = models.SplitStatusList; var SplitStatusMap = models.SplitStatusMap; var Split = models.Split; var Transaction = models.Transaction; var Error = models.Error; var getAccountDisplayName = require('../utils').getAccountDisplayName; var AccountCombobox = require('./AccountCombobox'); class TransactionRow extends React.Component { constructor() { super(); this.onClick = this.handleClick.bind(this); } handleClick(e) { const refs = ["date", "number", "description", "account", "status", "amount"]; for (var ref in refs) { if (this.refs[refs[ref]] == e.target) { this.props.onSelect(this.props.transaction.TransactionId, refs[ref]); return; } } } render() { 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]; var balance = security.Symbol + " " + "?" 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]; status = SplitStatusMap[this.props.transaction.Splits[i].Status]; 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); if (this.props.transaction.hasOwnProperty("Balance")) balance = security.Symbol + " " + this.props.transaction.Balance.toFixed(security.Precision); number = thisAccountSplit.Number; } else { var amount = security.Symbol + " " + (new Big(0.0)).toFixed(security.Precision); } return ( {dateString} {number} {this.props.transaction.Description} {accountName} {status} {amount} {balance} ); } } class AmountInput extends React.Component { getInitialState(props) { if (!props) return { LastGoodAmount: "0", Amount: "0" } // 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 }; } constructor() { super(); this.onChange = this.handleChange.bind(this); this.state = this.getInitialState(); } componentWillReceiveProps(nextProps) { if ((!nextProps.value.eq(this.props.value) && !nextProps.value.eq(this.getValue())) || nextProps.security !== this.props.security) { this.setState(this.getInitialState(nextProps)); } } componentDidMount() { ReactDOM.findDOMNode(this.refs.amount).onblur = this.handleBlur.bind(this); } handleBlur() { 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 }); } handleChange() { this.setState({Amount: ReactDOM.findDOMNode(this.refs.amount).value}); if (this.props.onChange) this.props.onChange(); } getValue() { 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() { var symbol = "?"; if (this.props.security) symbol = this.props.security.Symbol; return ( {symbol} ); } } class AddEditTransactionModal extends React.Component { getInitialState(props) { // Ensure we can edit this without screwing up other copies of it if (props) var t = props.transaction.deepCopy(); else var t = new Transaction(); return { errorAlert: [], transaction: t }; } constructor() { super(); this.state = this.getInitialState(); this.onCancel = this.handleCancel.bind(this); this.onDescriptionChange = this.handleDescriptionChange.bind(this); this.onDateChange = this.handleDateChange.bind(this); this.onAddSplit = this.handleAddSplit.bind(this); this.onDeleteSplit = this.handleDeleteSplit.bind(this); this.onUpdateNumber = this.handleUpdateNumber.bind(this); this.onUpdateStatus = this.handleUpdateStatus.bind(this); this.onUpdateMemo = this.handleUpdateMemo.bind(this); this.onUpdateAccount = this.handleUpdateAccount.bind(this); this.onUpdateAmount = this.handleUpdateAmount.bind(this); this.onSubmit = this.handleSubmit.bind(this); this.onDelete = this.handleDelete.bind(this); } componentWillReceiveProps(nextProps) { if (nextProps.show && !this.props.show) { this.setState(this.getInitialState(nextProps)); } } handleCancel() { if (this.props.onCancel != null) this.props.onCancel(); } handleDescriptionChange() { this.setState({ transaction: react_update(this.state.transaction, { Description: {$set: ReactDOM.findDOMNode(this.refs.description).value} }) }); } handleDateChange(date, string) { if (date == null) return; this.setState({ transaction: react_update(this.state.transaction, { Date: {$set: date} }) }); } handleAddSplit() { var split = new Split(); split.Status = SplitStatus.Entered; this.setState({ transaction: react_update(this.state.transaction, { Splits: {$push: [split]} }) }); } handleDeleteSplit(split) { this.setState({ transaction: react_update(this.state.transaction, { Splits: {$splice: [[split, 1]]} }) }); } handleUpdateNumber(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 }); } handleUpdateStatus(status, split) { var transaction = this.state.transaction; transaction.Splits[split] = react_update(transaction.Splits[split], { Status: {$set: status.StatusId} }); this.setState({ transaction: transaction }); } handleUpdateMemo(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(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(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() { 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() { if (this.props.onDelete != null) this.props.onDelete(this.state.transaction); } render() { 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; var 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.onDeleteSplit(j);}; })(); var updateNumberFn = (function() { var j = i; return function() {self.onUpdateNumber(j);}; })(); var updateStatusFn = (function() { var j = i; return function(status) {self.onUpdateStatus(status, j);}; })(); var updateMemoFn = (function() { var j = i; return function() {self.onUpdateMemo(j);}; })(); var updateAccountFn = (function() { var j = i; return function(account) {self.onUpdateAccount(account, j);}; })(); var updateAmountFn = (function() { var j = i; return function() {self.onUpdateAmount(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, OFXFile: 2, Gnucash: 3 }; var ImportTypeList = []; for (var type in ImportType) { if (ImportType.hasOwnProperty(type)) { var name = ImportType[type] == ImportType.OFX ? "Direct OFX" : type; var name = ImportType[type] == ImportType.OFXFile ? "OFX/QFX File" : type; //QFX is a special snowflake ImportTypeList.push({'TypeId': ImportType[type], 'Name': name}); } } class ImportTransactionsModal extends React.Component { getInitialState() { var startDate = new Date(); startDate.setMonth(startDate.getMonth() - 1); return { importFile: "", importType: ImportType.OFX, startDate: startDate, endDate: new Date(), password: "", }; } constructor() { super(); this.state = this.getInitialState(); this.onCancel = this.handleCancel.bind(this); this.onImportChange = this.handleImportChange.bind(this); this.onTypeChange = this.handleTypeChange.bind(this); this.onPasswordChange = this.handlePasswordChange.bind(this); this.onStartDateChange = this.handleStartDateChange.bind(this); this.onEndDateChange = this.handleEndDateChange.bind(this); this.onSubmit = this.handleSubmit.bind(this); this.onImportTransactions = this.handleImportTransactions.bind(this); } componentWillReceiveProps(nextProps) { if (nextProps.show && !this.props.show) { this.setState(this.getInitialState()); } } handleCancel() { if (this.props.onCancel != null) this.props.onCancel(); } handleImportChange() { this.setState({importFile: ReactDOM.findDOMNode(this.refs.importfile).value}); } handleTypeChange(type) { this.setState({importType: type.TypeId}); } handlePasswordChange() { this.setState({password: ReactDOM.findDOMNode(this.refs.password).value}); } handleStartDateChange(date, string) { if (date == null) return; this.setState({ startDate: date }); } handleEndDateChange(date, string) { if (date == null) return; this.setState({ endDate: date }); } handleSubmit() { if (this.props.onSubmit != null) this.props.onSubmit(this.props.account); } handleImportTransactions() { if (this.state.importType == ImportType.OFX) { this.props.onImportOFX(this.props.account, this.state.password, this.state.startDate, this.state.endDate); } else if (this.state.importType == ImportType.OFXFile) { this.props.onImportOFXFile(ReactDOM.findDOMNode(this.refs.importfile), this.props.account); } else if (this.state.importType == ImportType.Gnucash) { this.props.onImportGnucash(ReactDOM.findDOMNode(this.refs.importfile)); } } render() { 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.props.imports.importing && this.props.imports.uploadProgress == 100) { progressBar = (); } else if (this.props.imports.importing) { progressBar = (); } // Create panel, possibly displaying error or success messages var panel = []; if (this.props.imports.importFailed) { panel = ({this.props.imports.errorMessage}); } else if (this.props.imports.importFinished) { panel = (Your import is now complete.); } // Display proper buttons, possibly disabling them if an import is in progress var button1 = []; var button2 = []; if (!this.props.imports.importFinished && !this.props.imports.importFailed) { var importingDisabled = this.props.imports.importing || (this.state.importType != ImportType.OFX && this.state.importFile == "") || (this.state.importType == ImportType.OFX && this.state.password == ""); button1 = (); button2 = (); } else { button1 = (); } var inputDisabled = (this.props.imports.importing || this.props.imports.importFailed || this.props.imports.importFinished) ? true : false; // Disable OFX/QFX imports if no account is selected var disabledTypes = false; if (this.props.account == null) disabledTypes = [ImportTypeList[ImportType.OFX - 1], ImportTypeList[ImportType.OFXFile - 1]]; var importForm = []; if (this.state.importType == ImportType.OFX) { importForm = (
OFX Password Start Date End Date
); } else { importForm = ( File Select a file to upload. ); } return ( Import Transactions
{accountNameLabel} Import Type {importForm}
{progressBar} {panel}
{button1} {button2}
); } } class AccountRegister extends React.Component { constructor() { super(); this.state = { newTransaction: null, height: 0 }; this.onEditTransaction = this.handleEditTransaction.bind(this); this.onEditingCancel = this.handleEditingCancel.bind(this); this.onNewTransactionClicked = this.handleNewTransactionClicked.bind(this); this.onSelectPage = this.handleSelectPage.bind(this); this.onImportComplete = this.handleImportComplete.bind(this); this.onUpdateTransaction = this.handleUpdateTransaction.bind(this); this.onDeleteTransaction = this.handleDeleteTransaction.bind(this); } resize() { var div = ReactDOM.findDOMNode(this); this.setState({height: div.parentElement.clientHeight - 64}); } componentWillReceiveProps(nextProps) { if (!nextProps.transactionPage.upToDate && nextProps.selectedAccount != -1) { nextProps.onFetchTransactionPage(nextProps.accounts[nextProps.selectedAccount], nextProps.transactionPage.pageSize, nextProps.transactionPage.page); } } componentDidMount() { this.resize(); var self = this; $(window).resize(function() {self.resize();}); } handleEditTransaction(transaction) { this.props.onSelectTransaction(transaction.TransactionId); } handleEditingCancel() { this.setState({ newTransaction: null }); this.props.onUnselectTransaction(); } handleNewTransactionClicked() { var newTransaction = new Transaction(); newTransaction.Date = new Date(); newTransaction.Splits.push(new Split()); newTransaction.Splits.push(new Split()); newTransaction.Splits[0].Status = SplitStatus.Entered; newTransaction.Splits[1].Status = SplitStatus.Entered; newTransaction.Splits[0].AccountId = this.props.accounts[this.props.selectedAccount].AccountId; this.setState({ newTransaction: newTransaction }); } handleSelectPage(eventKey) { var newpage = eventKey - 1; // Don't do pages that don't make sense if (newpage < 0) newpage = 0; if (newpage >= this.props.transactionPage.numPages) newpage = this.props.transactionPage.numPages - 1; if (newpage != this.props.transactionPage.page) { if (this.props.selectedAccount != -1) { this.props.onFetchTransactionPage(this.props.accounts[this.props.selectedAccount], this.props.pageSize, newpage); } } } handleImportComplete() { this.props.onCloseImportModal(); this.props.onFetchAllAccounts(); this.props.onFetchTransactionPage(this.props.accounts[this.props.selectedAccount], this.props.pageSize, this.props.transactionPage.page); } handleDeleteTransaction(transaction) { this.props.onDeleteTransaction(transaction); this.props.onUnselectTransaction(); } handleUpdateTransaction(transaction) { if (transaction.TransactionId != -1) { this.props.onUpdateTransaction(transaction); } else { this.props.onCreateTransaction(transaction); } this.props.onUnselectTransaction(); this.setState({ newTransaction: null }); } render() { var name = "Please select an account"; var register = []; if (this.props.selectedAccount != -1) { name = this.props.accounts[this.props.selectedAccount].Name; var transactionRows = []; for (var i = 0; i < this.props.transactionPage.transactions.length; i++) { var transactionId = this.props.transactionPage.transactions[i]; var transaction = this.props.transactions[transactionId]; 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; 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 (
Transactions for '{name}'
{register}
); } } module.exports = AccountRegister;