diff --git a/accounts.go b/accounts.go index 2cb578f..ab319ca 100644 --- a/accounts.go +++ b/accounts.go @@ -31,7 +31,7 @@ type Account struct { // monotonically-increasing account transaction version number. Used for // allowing a client to ensure they have a consistent version when paging // through transactions. - Version int64 + AccountVersion int64 `json:"Version"` } type AccountList struct { @@ -127,7 +127,7 @@ func insertUpdateAccount(a *Account, insert bool) error { return err } - a.Version = oldacct.Version + 1 + a.AccountVersion = oldacct.AccountVersion + 1 count, err := transaction.Update(a) if err != nil { @@ -227,7 +227,7 @@ func AccountHandler(w http.ResponseWriter, r *http.Request) { } account.AccountId = -1 account.UserId = user.UserId - account.Version = 0 + account.AccountVersion = 0 if GetSecurity(account.SecurityId) == nil { WriteError(w, 3 /*Invalid Request*/) diff --git a/static/account_register.js b/static/account_register.js new file mode 100644 index 0000000..4fbd531 --- /dev/null +++ b/static/account_register.js @@ -0,0 +1,489 @@ +// Import all the objects we want to use from ReactBootstrap + +var Modal = ReactBootstrap.Modal; + +var Label = ReactBootstrap.Label; +var Table = ReactBootstrap.Table; +var Grid = ReactBootstrap.Grid; +var Row = ReactBootstrap.Row; +var Col = ReactBootstrap.Col; + +var DateTimePicker = ReactWidgets.DateTimePicker; + +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]].getDOMNode() == 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]; + var accountName = getAccountDisplayName(this.props.account_map[otherSplit.AccountId], this.props.account_map); + } else { + accountName = "--Split Transaction--"; + } + + var amount = "$" + thisAccountSplit.Amount.toFixed(security.Precision); + status = TransactionStatusMap[this.props.transaction.Status]; + number = thisAccountSplit.Number; + } else { + var amount = "$" + (new Big(0.0)).toFixed(security.Precision); + } + + return ( + + {dateString} + {number} + {this.props.transaction.Description} + {accountName} + {status} + {amount} + $??.?? + ); + } +}); + +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 {transaction: t}; + }, + getInitialState: function() { + return this._getInitialState(this.props); + }, + handleCancel: function() { + if (this.props.onCancel != null) + this.props.onCancel(); + }, + handleDescriptionChange: function() { + var transaction = this.state.transaction.deepCopy(); + transaction.Description = this.refs.description.getValue(); + this.setState({ + transaction: transaction + }); + }, + handleDateChange: function(date, string) { + if (date == null) + return; + var transaction = this.state.transaction.deepCopy(); + transaction.Date = date; + this.setState({ + transaction: transaction + }); + }, + handleStatusChange: function(status) { + if (status.hasOwnProperty('StatusId')) { + var transaction = this.state.transaction.deepCopy(); + transaction.Status = status.StatusId; + this.setState({ + transaction: transaction + }); + } + }, + handleDeleteSplit: function(split) { + var transaction = this.state.transaction.deepCopy(); + transaction.Splits.splice(split, 1); + this.setState({ + transaction: transaction + }); + }, + handleUpdateNumber: function(split) { + var transaction = this.state.transaction.deepCopy(); + transaction.Splits[split].Number = this.refs['number-'+split].getValue(); + this.setState({ + transaction: transaction + }); + }, + handleUpdateMemo: function(split) { + var transaction = this.state.transaction.deepCopy(); + transaction.Splits[split].Memo = this.refs['memo-'+split].getValue(); + this.setState({ + transaction: transaction + }); + }, + handleUpdateAccount: function(account, split) { + var transaction = this.state.transaction.deepCopy(); + transaction.Splits[split].AccountId = account.AccountId; + this.setState({ + transaction: transaction + }); + }, + handleUpdateAmount: function(split) { + var transaction = this.state.transaction.deepCopy(); + transaction.Splits[split].Amount = new Big(this.refs['amount-'+split].getValue()); + this.setState({ + transaction: transaction + }); + }, + handleSubmit: function() { + if (this.props.onSubmit != null) + this.props.onSubmit(this.state.transaction); + }, + handleDelete: function() { + if (this.props.onDelete != null) + this.props.onDelete(this.state.transaction); + }, + componentWillReceiveProps: function(nextProps) { + if (nextProps.show && !this.props.show) { + this.setState(this._getInitialState(nextProps)); + } + }, + 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 = ( + + ); + } + + splits = []; + for (var i = 0; i < this.state.transaction.Splits.length; i++) { + var self = this; + var s = this.state.transaction.Splits[i]; + + // 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 + + +
+ + + + + + + + + # + Memo + Account + Amount + + {splits} + + +
+ + + + {deleteButton} + + + +
+ ); + } +}); + +const AccountRegister = React.createClass({ + getInitialState: function() { + return { + editingTransaction: false, + selectedTransaction: new Transaction(), + transactions: [] + }; + }, + handleEditTransaction: function(transaction, fieldName) { + //TODO select fieldName first when editing + this.setState({ + selectedTransaction: transaction, + editingTransaction: true + }); + }, + handleEditingCancel: function() { + this.setState({ + editingTransaction: 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=50&page="+page, + success: function(data, status, jqXHR) { + var e = new Error(); + var transactions = []; + e.fromJSON(data); + if (e.isError()) { + this.setState({error: e}); + } else { + for (var i = 0; i < data.transactions.length; i++) { + var t = new Transaction(); + t.fromJSON(data.transactions[i]); + transactions.push(t); + } + } + var a = new Account(); + a.fromJSON(data.account); + + this.setState({transactions: transactions}); + }.bind(this), + error: this.ajaxError + }); + }, + onNewTransaction: function() { + this.getTransactionPage(this.props.selectedAccount, 0); + }, + onUpdatedTransaction: function() { + this.getTransactionPage(this.props.selectedAccount, 0); + }, + onDeletedTransaction: function() { + this.getTransactionPage(this.props.selectedAccount, 0); + }, + 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) { + console.log("handleDeleteTransaction", 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 + }); + }, + 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.getTransactionPage(nextProps.selectedAccount, 0); + console.log("TODO begin fetching transactions for new account"); + } + }, + render: function() { + var name = "Please select an account"; + if (this.props.selectedAccount != null) + name = this.props.selectedAccount.Name; + + register = []; + if (this.props.selectedAccount != null) { + var newTransaction = new Transaction(); + newTransaction.Description = "Create 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; + + var transactionRows = []; + var allTransactions = [newTransaction].concat(this.state.transactions); + for (var i = 0; i < allTransactions.length; i++) { + var t = allTransactions[i]; + transactionRows.push(( + + )); + } + + register = ( + + + + + + + + + + + + {transactionRows} + +
Date#DescriptionAccountStatusAmountBalance
+ ); + } + + return ( +
+ + {name} + {register} +
+ ); + } +}); diff --git a/static/accounts.js b/static/accounts.js index 2dec758..8cf5022 100644 --- a/static/accounts.js +++ b/static/accounts.js @@ -10,30 +10,10 @@ var Button = ReactBootstrap.Button; var ButtonGroup = ReactBootstrap.ButtonGroup; var Glyphicon = ReactBootstrap.Glyphicon; -var Modal = ReactBootstrap.Modal; - var CollapsibleMixin = ReactBootstrap.CollapsibleMixin; var Combobox = ReactWidgets.Combobox; -const recursiveAccountDisplayInfo = function(account, prefix) { - var name = prefix + account.Name; - var accounts = [{AccountId: account.AccountId, Name: name}]; - for (var i = 0; i < account.Children.length; i++) - accounts = accounts.concat(recursiveAccountDisplayInfo(account.Children[i], name + "/")); - return accounts -}; -const getAccountDisplayList = function(account_list, includeRoot, rootName) { - var accounts = [] - if (includeRoot) - accounts.push({AccountId: -1, Name: rootName}); - for (var i = 0; i < account_list.length; i++) { - if (account_list[i].isRootAccount()) - accounts = accounts.concat(recursiveAccountDisplayInfo(account_list[i], "")); - } - return accounts; -}; - const AccountCombobox = React.createClass({ getDefaultProps: function() { return { @@ -494,7 +474,12 @@ const AccountsTab = React.createClass({ - blah + ); diff --git a/static/index.html b/static/index.html index aed8cc3..6989126 100644 --- a/static/index.html +++ b/static/index.html @@ -21,6 +21,7 @@ + diff --git a/static/models.js b/static/models.js index 11bde73..3a9b681 100644 --- a/static/models.js +++ b/static/models.js @@ -212,8 +212,6 @@ Split.prototype.toJSONobj = function() { } Split.prototype.fromJSONobj = function(json_obj) { - var json_obj = getJSONObj(json_input); - if (json_obj.hasOwnProperty("SplitId")) this.SplitId = json_obj.SplitId; if (json_obj.hasOwnProperty("TransactionId")) @@ -243,6 +241,18 @@ const TransactionStatus = { Reconciled: 3, Voided: 4 } +var TransactionStatusList = []; +for (var type in TransactionStatus) { + if (TransactionStatus.hasOwnProperty(type)) { + TransactionStatusList.push({'StatusId': TransactionStatus[type], 'Name': type}); + } +} +var TransactionStatusMap = {}; +for (var status in TransactionStatus) { + if (TransactionStatus.hasOwnProperty(status)) { + TransactionStatusMap[TransactionStatus[status]] = status; + } +} function Transaction() { this.TransactionId = -1; @@ -262,8 +272,8 @@ Transaction.prototype.toJSON = function() { json_obj.Date = this.Date.toJSON(); json_obj.Splits = []; for (var i = 0; i < this.Splits.length; i++) - json_obj.push(this.Splits[i].toJSONobj()); - return json_obj; + json_obj.Splits.push(this.Splits[i].toJSONobj()); + return JSON.stringify(json_obj); } Transaction.prototype.fromJSON = function(json_input) { @@ -289,8 +299,11 @@ Transaction.prototype.fromJSON = function(json_input) { this.Date = new Date(0); } if (json_obj.hasOwnProperty("Splits")) { - for (var i = 0; i < json_obj.Splits.length; i++) - this.Splits.push(this.Splits[i].fromJSON()); + for (var i = 0; i < json_obj.Splits.length; i++) { + var s = new Split(); + s.fromJSONobj(json_obj.Splits[i]); + this.Splits.push(s); + } } } @@ -300,6 +313,12 @@ Transaction.prototype.isTransaction = function() { this.UserId != empty_transaction.UserId; } +Transaction.prototype.deepCopy = function() { + var t = new Transaction(); + t.fromJSON(this.toJSON()); + return t; +} + function Error() { this.ErrorId = -1; this.ErrorString = ""; diff --git a/static/stylesheet.css b/static/stylesheet.css index 7b7f6f8..486221f 100644 --- a/static/stylesheet.css +++ b/static/stylesheet.css @@ -75,3 +75,24 @@ div.accounttree-root div { padding: 15px; border-right: 1px solid #DDD; } + +.register-row-editing { + background-color: #FFFFE0 !important; +} +.register-row-editing:hover { + background-color: #e8e8e8 !important; +} +.register-row-editing .form-group { + margin: 0; +} + +.row > div > .form-group, +.row > div > .rw-combobox { + margin-right: -7px; + margin-left: -7px; +} + +.split-header { + font-weight: 700; + text-align: center; +} diff --git a/static/utils.js b/static/utils.js new file mode 100644 index 0000000..47ad308 --- /dev/null +++ b/static/utils.js @@ -0,0 +1,27 @@ +const recursiveAccountDisplayInfo = function(account, prefix) { + var name = prefix + account.Name; + var accounts = [{AccountId: account.AccountId, Name: name}]; + for (var i = 0; i < account.Children.length; i++) + accounts = accounts.concat(recursiveAccountDisplayInfo(account.Children[i], name + "/")); + return accounts +}; + +const getAccountDisplayList = function(account_list, includeRoot, rootName) { + var accounts = [] + if (includeRoot) + accounts.push({AccountId: -1, Name: rootName}); + for (var i = 0; i < account_list.length; i++) { + if (account_list[i].isRootAccount()) + accounts = accounts.concat(recursiveAccountDisplayInfo(account_list[i], "")); + } + return accounts; +}; + +const getAccountDisplayName = function(account, account_map) { + var name = account.Name; + while (account.ParentAccountId >= 0) { + account = account_map[account.ParentAccountId]; + name = account.Name + "/" + name; + } + return name; +}; diff --git a/transactions.go b/transactions.go index 93ac4de..481f445 100644 --- a/transactions.go +++ b/transactions.go @@ -18,7 +18,7 @@ type Split struct { SplitId int64 TransactionId int64 AccountId int64 - Number int64 // Check or reference number + Number string // Check or reference number Memo string Amount string // String representation of decimal, suitable for passing to big.Rat.SetString() Debit bool @@ -38,10 +38,8 @@ func (s *Split) Valid() bool { return err == nil } -type TransactionStatus int64 - const ( - Entered TransactionStatus = 1 + Entered int64 = 1 Cleared = 2 Reconciled = 3 Voided = 4 @@ -51,7 +49,7 @@ type Transaction struct { TransactionId int64 UserId int64 Description string - Status TransactionStatus + Status int64 Date time.Time Splits []*Split `db:"-"` } @@ -118,7 +116,7 @@ func GetTransaction(transactionid int64, userid int64) (*Transaction, error) { return nil, err } - err = transaction.SelectOne(&t, "SELECT * from transaction where UserId=? AND TransactionId=?", userid, transactionid) + err = transaction.SelectOne(&t, "SELECT * from transactions where UserId=? AND TransactionId=?", userid, transactionid) if err != nil { return nil, err } @@ -172,7 +170,7 @@ func incrementAccountVersions(transaction *gorp.Transaction, user *User, account if err != nil { return err } - account.Version++ + account.AccountVersion++ count, err := transaction.Update(account) if err != nil { return err