diff --git a/application/app.py b/application/app.py index 6ae7f00..2cd32c9 100644 --- a/application/app.py +++ b/application/app.py @@ -240,8 +240,29 @@ def delete_account(): def get_transactions(): incoming = request.args account_id = incoming["account_id"] + limit = incoming["limit"] transactionsList = [] - transactionsObjects = Transaction.get_transactions(account_id) + transactionsObjects = Transaction.get_transactions(account_id, limit) + for transaction in transactionsObjects: + transactionsList.append({ + 'transaction_id': transaction.transaction_id, + 'account_id': transaction.account_id, + 'label': transaction.label, + 'amount': str(transaction.amount), # Decimal is not JSON serializable + 'recurring_group_id': transaction.recurring_group_id, + 'date': transaction.date.strftime('%Y-%m-%d'), + 'tick': transaction.tick, + }) + return jsonify(result=transactionsList) + + +@app.route("/api/transactions/future", methods=["GET"]) +@requires_auth +def get_future_transactions(): + incoming = request.args + account_id = incoming["account_id"] + transactionsList = [] + transactionsObjects = Transaction.get_recurring_until_projected(account_id) for transaction in transactionsObjects: transactionsList.append({ 'transaction_id': transaction.transaction_id, diff --git a/application/models.py b/application/models.py index 3dd0f7f..6bc6a9d 100644 --- a/application/models.py +++ b/application/models.py @@ -1,4 +1,6 @@ from index import db, bcrypt +from sqlalchemy import desc, or_ +from datetime import datetime class User(db.Model): @@ -79,10 +81,21 @@ def __init__(self, transaction_id, account_id, label, amount, recurring_group_id self.tick = tick @staticmethod - def get_transactions(account_id): + def get_transactions(account_id, limit): + return Transaction.query \ + .filter(Transaction.account_id == account_id) \ + .filter(or_(db.func.date(Transaction.date) <= datetime.now().date(), Transaction.recurring_group_id == None)) \ + .order_by(desc(Transaction.date)) \ + .limit(limit) \ + .all() + + @staticmethod + def get_recurring_until_projected(account_id): return Transaction.query \ .filter((Transaction.account_id == account_id), - (db.func.date(Transaction.date) <= Account.get_projected_date(account_id))) \ + (db.func.date(Transaction.date) <= Account.get_projected_date(account_id)), + (db.func.date(Transaction.date) > datetime.now().date()), + (Transaction.recurring_group_id != None)) \ .all() diff --git a/static/package.json b/static/package.json index 7b20f1c..3a59c44 100644 --- a/static/package.json +++ b/static/package.json @@ -81,7 +81,7 @@ "material-ui": "^0.16.4", "mocha": "^3.0.2", "morgan": "^1.6.1", - "node-sass": "^3.4.2", + "node-sass": "^4.7.2", "postcss-import": "^9.0.0", "postcss-loader": "^2.0.6", "prettier": "^1.5.3", diff --git a/static/src/actions/transactions.js b/static/src/actions/transactions.js index fd90d62..3987b96 100644 --- a/static/src/actions/transactions.js +++ b/static/src/actions/transactions.js @@ -1,4 +1,6 @@ import { + FETCH_FUTURE_TRANSACTIONS_DATA_REQUEST, + RECEIVE_FUTURE_TRANSACTIONS_DATA, FETCH_TRANSACTIONS_DATA_REQUEST, RECEIVE_TRANSACTIONS_DATA, ADD_TRANSACTION, @@ -8,6 +10,7 @@ import { import { parseJSON } from '../utils/misc' import { data_about_transactions, + data_about_future_transactions, create_transaction, edit_transaction, delete_transaction, @@ -31,6 +34,21 @@ export function fetchTransactionsDataRequest() { } } +export function receiveFutureTransactionsData(data) { + return { + type: RECEIVE_FUTURE_TRANSACTIONS_DATA, + payload: { + data + } + } +} + +export function fetchFutureTransactionsDataRequest() { + return { + type: FETCH_FUTURE_TRANSACTIONS_DATA_REQUEST + } +} + export function addTransactionToStore(data) { return { type: ADD_TRANSACTION, @@ -58,10 +76,26 @@ export function deleteTransactionFromStore(transaction_id) { } } -export function fetchTransactionsData(token, account_id) { +export function fetchFutureTransactionsData(token, account_id) { + return dispatch => { + dispatch(fetchFutureTransactionsDataRequest()) + data_about_future_transactions(token, account_id) + .then(parseJSON) + .then(response => { + dispatch(receiveFutureTransactionsData(response.result)) + }) + .catch(error => { + if (error.status === 401) { + dispatch(logoutAndRedirect(error)) + } + }) + } +} + +export function fetchTransactionsData(token, account_id, limit) { return dispatch => { dispatch(fetchTransactionsDataRequest()) - data_about_transactions(token, account_id) + data_about_transactions(token, account_id, limit) .then(parseJSON) .then(response => { dispatch(receiveTransactionsData(response.result)) diff --git a/static/src/components/TransactionsList/index.js b/static/src/components/TransactionsList/index.js index ad97195..8df1729 100644 --- a/static/src/components/TransactionsList/index.js +++ b/static/src/components/TransactionsList/index.js @@ -1,4 +1,5 @@ import React from 'react' +import ReactDOM from 'react-dom' import 'babel-polyfill' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' @@ -40,20 +41,26 @@ function mapDispatchToProps(dispatch) { @connect(mapStateToProps, mapDispatchToProps) export default class TransactionsList extends React.Component { - state = { - fixedHeader: true, - fixedFooter: true, - stripedRows: true, - showRowHover: false, - selectable: false, - multiSelectable: false, - enableSelectAll: false, - deselectOnClickaway: false, - showCheckboxes: false, - height: '400px', - snackOpen: false, - snackMessage: '', - showRecurring: false + constructor(props) { + super(props) + this.state = { + fixedHeader: true, + fixedFooter: true, + stripedRows: true, + showRowHover: false, + selectable: false, + multiSelectable: false, + enableSelectAll: false, + deselectOnClickaway: false, + showCheckboxes: false, + height: '400px', + snackOpen: false, + snackMessage: '', + showRecurring: false, + recurringLoaded: false, + preventScroll: false + } + this.fireOnScroll = this.fireOnScroll.bind(this) } fetchData(account_id = null) { @@ -62,7 +69,49 @@ export default class TransactionsList extends React.Component { } if (account_id !== null) { const token = this.props.token - this.props.fetchTransactionsData(token, account_id) + const limit = this.props.data ? this.props.data.length + 50 : 50 + this.props.fetchTransactionsData(token, account_id, limit) + } + this.setState({ + showRecurring: false + }) + } + + fetchFutureData(account_id = null) { + if (!this.state.recurringLoaded) { + if (account_id === null) { + account_id = this.props.selectedAccount + } + if (account_id !== null) { + const token = this.props.token + let call = async () => + await await this.props.fetchFutureTransactionsData(token, account_id) + call().then(() => { + this.setState({ + recurringLoaded: true + }) + }) + } + } + } + + fireOnScroll(event) { + const elem = ReactDOM.findDOMNode(this.refs.table.refs.tableDiv) + if (elem.scrollTop == 0 && !this.props.isFetching) { + this.setState({ + preventScroll: true + }) + this.fetchData() + } + } + + toggleRecurring(event, isChecked) { + this.setState({ + showRecurring: isChecked, + recurringLoaded: false + }) + if (!isChecked) { + this.fetchData() } } @@ -79,6 +128,11 @@ export default class TransactionsList extends React.Component { }) } + componentWillUnmount() { + const elem = ReactDOM.findDOMNode(this.refs.table.refs.tableDiv) + elem.removeEventListener('scroll', this.fireOnScroll) + } + componentWillReceiveProps(nextProps) { if (nextProps.selectedAccount !== this.props.selectedAccount) { this.fetchData(nextProps.selectedAccount) @@ -88,7 +142,17 @@ export default class TransactionsList extends React.Component { componentDidUpdate(prevProps, prevState) { // Do not scroll on ticking if (prevProps.data !== this.props.data) { - this.scrollToBottom() + if (!this.state.preventScroll) { + this.scrollToBottom() + } else { + this.setState({ + preventScroll: false + }) + } + } + if (this.refs.table) { + const elem = ReactDOM.findDOMNode(this.refs.table.refs.tableDiv) + elem.addEventListener('scroll', this.fireOnScroll) } } @@ -141,13 +205,12 @@ export default class TransactionsList extends React.Component { call().then(() => {}) } - toggleRecurring(event, isChecked) { - this.setState({ - showRecurring: isChecked - }) - } - renderTransactionsList(transactions) { + if (this.state.showRecurring) { + // add recurring transactions + this.fetchFutureData() + } + transactions.sort((a, b) => { if (new Date(a.date) < new Date(b.date)) return -1 if (new Date(a.date) > new Date(b.date)) return 1 @@ -155,39 +218,16 @@ export default class TransactionsList extends React.Component { if (a.id > b.id) return 1 return 0 }) - // get projected balance date for current account - let projectedDate = this.props.accounts.filter( - element => element.id == this.props.selectedAccount - )[0].projected_date - if (this.state.showRecurring) { - // display recurring transactions of the month - transactions = transactions.filter(element => { - return ( - element.recurring_group_id === null || - (element.recurring_group_id !== null && - new Date(element.date) <= new Date()) || - (element.recurring_group_id !== null && - new Date(element.date) <= new Date(projectedDate)) - ) - }) - } else { - // filter all future recurring transactions - transactions = transactions.filter(element => { - return ( - element.recurring_group_id === null || - (element.recurring_group_id !== null && - new Date(element.date) <= new Date()) - ) - }) - } const rows = transactions.map((row, index) => { let credit = parseFloat(row.amount) < 0 ? '' : Number(row.amount).toFixed(2) let debit = parseFloat(row.amount) < 0 ? Number(row.amount).toFixed(2) : '' let isFutureAndRecurring = - new Date(row.date) > new Date() && row.recurring_group_id !== null + new Date(row.date) > new Date() && + row.recurring_group_id !== null && + row.recurring_group_id !== undefined return ( @@ -316,6 +357,7 @@ export default class TransactionsList extends React.Component { TransactionsList.propTypes = { fetchTransactionsData: React.PropTypes.func, + fetchFutureTransactionsData: React.PropTypes.func, loaded: React.PropTypes.bool, data: React.PropTypes.any, token: React.PropTypes.string, diff --git a/static/src/constants/index.js b/static/src/constants/index.js index 2cbb1d4..fdbfc78 100644 --- a/static/src/constants/index.js +++ b/static/src/constants/index.js @@ -19,6 +19,10 @@ export const EDIT_TRANSACTION = 'EDIT_TRANSACTION' export const DELETE_TRANSACTION = 'DELETE_TRANSACTION' export const FETCH_TRANSACTIONS_DATA_REQUEST = 'FETCH_TRANSACTIONS_DATA_REQUEST' export const RECEIVE_TRANSACTIONS_DATA = 'RECEIVE_TRANSACTIONS_DATA' +export const FETCH_FUTURE_TRANSACTIONS_DATA_REQUEST = + 'FETCH_FUTURE_TRANSACTIONS_DATA_REQUEST' +export const RECEIVE_FUTURE_TRANSACTIONS_DATA = + 'RECEIVE_FUTURE_TRANSACTIONS_DATA' export const FETCH_BALANCES_DATA_REQUEST = 'FETCH_BALANCES_DATA_REQUEST' export const RECEIVE_BALANCES_DATA = 'RECEIVE_BALANCES_DATA' diff --git a/static/src/reducers/transactions.js b/static/src/reducers/transactions.js index 01a358f..98fd976 100644 --- a/static/src/reducers/transactions.js +++ b/static/src/reducers/transactions.js @@ -1,4 +1,6 @@ import { + RECEIVE_FUTURE_TRANSACTIONS_DATA, + FETCH_FUTURE_TRANSACTIONS_DATA_REQUEST, RECEIVE_TRANSACTIONS_DATA, FETCH_TRANSACTIONS_DATA_REQUEST, ADD_TRANSACTION, @@ -10,10 +12,22 @@ import { createReducer } from '../utils/misc' const initialState = { data: null, isFetching: false, - loaded: false + loaded: false, + isFetchingFuture: false, + loadedFuture: false } export default createReducer(initialState, { + [RECEIVE_FUTURE_TRANSACTIONS_DATA]: (state, payload) => + Object.assign({}, state, { + data: [...state.data, ...payload.data], + isFetchingFuture: false, + loadedFuture: true + }), + [FETCH_FUTURE_TRANSACTIONS_DATA_REQUEST]: state => + Object.assign({}, state, { + isFetchingFuture: true + }), [RECEIVE_TRANSACTIONS_DATA]: (state, payload) => Object.assign({}, state, { data: payload.data, diff --git a/static/src/utils/http_functions.js b/static/src/utils/http_functions.js index 37f653b..d5e8c27 100644 --- a/static/src/utils/http_functions.js +++ b/static/src/utils/http_functions.js @@ -102,9 +102,9 @@ export function delete_account(token, id) { ) } -export function data_about_transactions(token, account_id) { +export function data_about_future_transactions(token, account_id) { return axios.get( - 'api/transactions', + 'api/transactions/future', Object.assign( { params: { account_id: account_id } @@ -114,6 +114,18 @@ export function data_about_transactions(token, account_id) { ) } +export function data_about_transactions(token, account_id, limit) { + return axios.get( + 'api/transactions', + Object.assign( + { + params: { account_id: account_id, limit: limit } + }, + tokenConfig(token) + ) + ) +} + export function create_transaction( token, account_id, diff --git a/static/yarn.lock b/static/yarn.lock index 55c9c4b..2a5f13a 100644 --- a/static/yarn.lock +++ b/static/yarn.lock @@ -1221,6 +1221,10 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000539, caniuse-db@^1.0.30000578, ca version "1.0.30000670" resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000670.tgz#90d33b79e3090e25829c311113c56d6b1788bf43" +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -1413,6 +1417,10 @@ commander@2.9.0, commander@^2.5.0: dependencies: graceful-readlink ">= 1.0.0" +commander@^2.9.0: + version "2.12.2" + resolved "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -2655,6 +2663,16 @@ glob@^5.0.15: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^6.0.4: + version "6.0.4" + resolved "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@~7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -2718,6 +2736,15 @@ har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + har-validator@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" @@ -3127,6 +3154,15 @@ is-my-json-valid@^2.10.0: jsonpointer "^4.0.0" xtend "^4.0.0" +is-my-json-valid@^2.12.4: + version "2.16.1" + resolved "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + is-number-object@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799" @@ -3946,9 +3982,9 @@ node-pre-gyp@^0.6.29: tar "^2.2.1" tar-pack "^3.4.0" -node-sass@^3.4.2: - version "3.13.1" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-3.13.1.tgz#7240fbbff2396304b4223527ed3020589c004fc2" +node-sass@^4.7.2: + version "4.7.2" + resolved "https://registry.npmjs.org/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e" dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -3959,13 +3995,16 @@ node-sass@^3.4.2: in-publish "^2.0.0" lodash.assign "^4.2.0" lodash.clonedeep "^4.3.2" + lodash.mergewith "^4.6.0" meow "^3.7.0" mkdirp "^0.5.1" nan "^2.3.2" node-gyp "^3.3.1" npmlog "^4.0.0" - request "^2.61.0" - sass-graph "^2.1.1" + request "~2.79.0" + sass-graph "^2.2.4" + stdout-stream "^1.4.0" + "true-case-path" "^1.0.2" "nopt@2 || 3": version "3.0.6" @@ -4666,6 +4705,10 @@ qs@6.4.0, qs@^6.1.0, qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +qs@~6.3.0: + version "6.3.2" + resolved "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" + query-string@^4.1.0, query-string@^4.2.2: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -5201,7 +5244,7 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request@2, request@^2.61.0, request@^2.72.0, request@^2.81.0: +request@2, request@^2.72.0, request@^2.81.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" dependencies: @@ -5228,6 +5271,31 @@ request@2, request@^2.61.0, request@^2.72.0, request@^2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" +request@~2.79.0: + version "2.79.0" + resolved "https://registry.npmjs.org/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + uuid "^3.0.0" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -5330,9 +5398,9 @@ safe-buffer@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" -sass-graph@^2.1.1: +sass-graph@^2.2.4: version "2.2.4" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" + resolved "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" dependencies: glob "^7.0.0" lodash "^4.0.0" @@ -5619,6 +5687,12 @@ stackframe@^0.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" +stdout-stream@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" + dependencies: + readable-stream "^2.0.1" + stream-browserify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" @@ -5833,6 +5907,12 @@ trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" +"true-case-path@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62" + dependencies: + glob "^6.0.4" + tryit@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" @@ -5847,6 +5927,10 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"