mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-10-31 16:00:05 -04:00
652 lines
18 KiB
JavaScript
652 lines
18 KiB
JavaScript
// Import all the objects we want to use from ReactBootstrap
|
|
|
|
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 Button = ReactBootstrap.Button;
|
|
var ButtonToolbar = ReactBootstrap.ButtonToolbar;
|
|
|
|
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 = 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 (
|
|
<tr>
|
|
<td ref="date" onClick={this.handleClick}>{dateString}</td>
|
|
<td ref="number" onClick={this.handleClick}>{number}</td>
|
|
<td ref="description" onClick={this.handleClick}>{this.props.transaction.Description}</td>
|
|
<td ref="account" onClick={this.handleClick}>{accountName}</td>
|
|
<td ref="status" onClick={this.handleClick}>{status}</td>
|
|
<td ref="amount" onClick={this.handleClick}>{amount}</td>
|
|
<td>{balance}</td>
|
|
</tr>);
|
|
}
|
|
});
|
|
|
|
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;
|
|
|
|
return (
|
|
<Input type="text"
|
|
value={this.state.Amount}
|
|
onChange={this.onChange}
|
|
addonBefore={symbol}
|
|
ref="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);
|
|
},
|
|
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.addons.update(this.state.transaction, {
|
|
Description: {$set: this.refs.description.getValue()}
|
|
})
|
|
});
|
|
},
|
|
handleDateChange: function(date, string) {
|
|
if (date == null)
|
|
return;
|
|
this.setState({
|
|
transaction: React.addons.update(this.state.transaction, {
|
|
Date: {$set: date}
|
|
})
|
|
});
|
|
},
|
|
handleStatusChange: function(status) {
|
|
if (status.hasOwnProperty('StatusId')) {
|
|
this.setState({
|
|
transaction: React.addons.update(this.state.transaction, {
|
|
Status: {$set: status.StatusId}
|
|
})
|
|
});
|
|
}
|
|
},
|
|
handleDeleteSplit: function(split) {
|
|
this.setState({
|
|
transaction: React.addons.update(this.state.transaction, {
|
|
Splits: {$splice: [[split, 1]]}
|
|
})
|
|
});
|
|
},
|
|
handleUpdateNumber: function(split) {
|
|
var transaction = this.state.transaction;
|
|
transaction.Splits[split] = React.addons.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.addons.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.addons.update(transaction.Splits[split], {
|
|
AccountId: {$set: account.AccountId}
|
|
});
|
|
this.setState({
|
|
transaction: transaction
|
|
});
|
|
},
|
|
handleUpdateAmount: function(split) {
|
|
var transaction = this.state.transaction;
|
|
transaction.Splits[split] = React.addons.update(transaction.Splits[split], {
|
|
Amount: {$set: 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);
|
|
},
|
|
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 = (
|
|
<Button onClick={this.handleDelete} bsStyle="danger">Delete Transaction</Button>
|
|
);
|
|
}
|
|
|
|
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;
|
|
if (this.props.account_map[s.AccountId])
|
|
security = this.props.security_map[this.props.account_map[s.AccountId].SecurityId];
|
|
|
|
// 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 = (
|
|
<Col xs={1}><Button onClick={deleteSplitFn}
|
|
bsStyle="danger">
|
|
<Glyphicon glyph='trash' /></Button></Col>
|
|
);
|
|
}
|
|
|
|
splits.push((
|
|
<Row>
|
|
<Col xs={1}><Input
|
|
type="text"
|
|
value={s.Number}
|
|
onChange={updateNumberFn}
|
|
ref={"number-"+i} /></Col>
|
|
<Col xs={5}><Input
|
|
type="text"
|
|
value={s.Memo}
|
|
onChange={updateMemoFn}
|
|
ref={"memo-"+i} /></Col>
|
|
<Col xs={3}><AccountCombobox
|
|
accounts={this.props.accounts}
|
|
account_map={this.props.account_map}
|
|
value={s.AccountId}
|
|
includeRoot={false}
|
|
onSelect={updateAccountFn}
|
|
ref={"account-"+i} /></Col>
|
|
<Col xs={2}><AmountInput type="text"
|
|
value={s.Amount}
|
|
security={security}
|
|
onChange={updateAmountFn}
|
|
ref={"amount-"+i} /></Col>
|
|
{deleteSplitButton}
|
|
</Row>
|
|
));
|
|
}
|
|
|
|
return (
|
|
<Modal show={this.props.show} onHide={this.handleCancel} bsSize="large">
|
|
<Modal.Header closeButton>
|
|
<Modal.Title>{headerText} Transaction</Modal.Title>
|
|
</Modal.Header>
|
|
<Modal.Body>
|
|
<form onSubmit={this.handleSubmit}
|
|
className="form-horizontal">
|
|
<Input wrapperClassName="wrapper"
|
|
label="Date"
|
|
labelClassName="col-xs-2"
|
|
wrapperClassName="col-xs-10">
|
|
<DateTimePicker
|
|
time={false}
|
|
defaultValue={this.state.transaction.Date}
|
|
onChange={this.handleDateChange} />
|
|
</Input>
|
|
<Input type="text"
|
|
label="Description"
|
|
value={this.state.transaction.Description}
|
|
onChange={this.handleDescriptionChange}
|
|
ref="description"
|
|
labelClassName="col-xs-2"
|
|
wrapperClassName="col-xs-10"/>
|
|
<Input wrapperClassName="wrapper"
|
|
label="Status"
|
|
labelClassName="col-xs-2"
|
|
wrapperClassName="col-xs-10">
|
|
<Combobox
|
|
data={TransactionStatusList}
|
|
valueField='StatusId'
|
|
textField='Name'
|
|
value={this.state.transaction.Status}
|
|
onSelect={this.handleStatusChange}
|
|
ref="status" />
|
|
</Input>
|
|
<Grid fluid={true}><Row>
|
|
<span className="split-header col-xs-1">#</span>
|
|
<span className="split-header col-xs-5">Memo</span>
|
|
<span className="split-header col-xs-3">Account</span>
|
|
<span className="split-header col-xs-2">Amount</span>
|
|
</Row>
|
|
{splits}
|
|
</Grid>
|
|
</form>
|
|
</Modal.Body>
|
|
<Modal.Footer>
|
|
<ButtonGroup>
|
|
<Button onClick={this.handleCancel} bsStyle="warning">Cancel</Button>
|
|
{deleteButton}
|
|
<Button onClick={this.handleSubmit} bsStyle="success">{buttonText}</Button>
|
|
</ButtonGroup>
|
|
</Modal.Footer>
|
|
</Modal>
|
|
);
|
|
}
|
|
});
|
|
|
|
const AccountRegister = React.createClass({
|
|
getInitialState: function() {
|
|
return {
|
|
editingTransaction: false,
|
|
selectedTransaction: new Transaction(),
|
|
transactions: [],
|
|
pageSize: 20,
|
|
numPages: 0,
|
|
currentPage: 0,
|
|
height: 0
|
|
};
|
|
},
|
|
resize: function() {
|
|
var div = React.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
|
|
});
|
|
},
|
|
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
|
|
});
|
|
},
|
|
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
|
|
});
|
|
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((
|
|
<TransactionRow
|
|
transaction={t}
|
|
account={this.props.selectedAccount}
|
|
accounts={this.props.accounts}
|
|
account_map={this.props.account_map}
|
|
securities={this.props.securities}
|
|
security_map={this.props.security_map}
|
|
onEdit={this.handleEditTransaction}/>
|
|
));
|
|
}
|
|
|
|
var style = {height: this.state.height + "px"};
|
|
register = (
|
|
<div style={style} className="transactions-register">
|
|
<Table bordered striped condensed hover>
|
|
<thead><tr>
|
|
<th>Date</th>
|
|
<th>#</th>
|
|
<th>Description</th>
|
|
<th>Account</th>
|
|
<th>Status</th>
|
|
<th>Amount</th>
|
|
<th>Balance</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
{transactionRows}
|
|
</tbody>
|
|
</Table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
var disabled = (this.props.selectedAccount == null) ? "disabled" : "";
|
|
|
|
return (
|
|
<div className="transactions-container">
|
|
<AddEditTransactionModal
|
|
show={this.state.editingTransaction}
|
|
transaction={this.state.selectedTransaction}
|
|
accounts={this.props.accounts}
|
|
account_map={this.props.account_map}
|
|
onCancel={this.handleEditingCancel}
|
|
onSubmit={this.handleUpdateTransaction}
|
|
onDelete={this.handleDeleteTransaction}
|
|
securities={this.props.securities}
|
|
security_map={this.props.security_map}/>
|
|
<div className="transactions-register-toolbar">
|
|
Transactions for '{name}'
|
|
<ButtonToolbar className="pull-right">
|
|
<ButtonGroup>
|
|
<Pagination
|
|
className="skinny-pagination"
|
|
prev next first last ellipses
|
|
items={this.state.numPages}
|
|
maxButtons={Math.min(5, this.state.numPages)}
|
|
activePage={this.state.currentPage+1}
|
|
onSelect={this.handleSelectPage} />
|
|
</ButtonGroup>
|
|
<ButtonGroup>
|
|
<Button
|
|
onClick={this.handleNewTransactionClicked}
|
|
bsStyle="success"
|
|
disabled={disabled}>
|
|
<Glyphicon glyph='plus-sign' /> New Transaction
|
|
</Button>
|
|
</ButtonGroup>
|
|
</ButtonToolbar>
|
|
</div>
|
|
{register}
|
|
</div>
|
|
);
|
|
}
|
|
});
|