Transfers (entire patch: list & make transfer) + Specific controls

This commit is contained in:
Andrew Revinsky (DART)
2016-03-22 04:13:27 +03:00
parent bfeb2e2e16
commit 8f86f72d85
18 changed files with 413 additions and 97 deletions

View File

@@ -89,7 +89,8 @@ export function initialize({cookies, isServer, currentLocation, userAgent} = {})
emailSignInPath: '/login', emailSignInPath: '/login',
customersPath: '/customers', customersPath: '/customers',
currentUserPath: '/user', currentUserPath: '/user',
accountsPath: '/accounts' accountsPath: '/accounts',
transfersPath: '/transfers'
} }
} }
], { ], {

View File

@@ -59,13 +59,16 @@ export function accountCreate(customerId, payload) {
return dispatch(authenticate(true)); return dispatch(authenticate(true));
}) })
.catch(err => { .catch(err => {
debugger;
dispatch(accountCreateError(err)); dispatch(accountCreateError(err));
return Promise.resolve({ error: err }); return Promise.resolve({ error: err });
}) })
}; };
} }
export function fetchOwnAccounts(customerId) { export function fetchOwnAccounts(customerId) {
return dispatch => { return dispatch => {
//dispatch(accountsListRequested()); //dispatch(accountsListRequested());
@@ -154,9 +157,54 @@ export const createRefOwnerLookup = lookup => {
}; };
}; };
export const createRefAccountLookup = lookup => { export const createRefAccountLookup = customerId => {
return dispatch => { return dispatch => {
dispatch(createRefAccountLookupStart()); dispatch(createRefAccountLookupStart());
dispatch(createRefAccountLookupComplete([])); return api.apiRetrieveUser(customerId)
.then(data => {
debugger;
dispatch(createRefAccountLookupComplete([]));
});
};
};
export const makeTransferRequested = makeActionCreator(T.TRANSFERS.MAKE_START, 'payload');
export const makeTransferComplete = makeActionCreator(T.TRANSFERS.MAKE_COMPLETE, 'payload');
export const makeTransferError = makeActionCreator(T.TRANSFERS.MAKE_ERROR, 'error');
export const makeTransferFormUpdate = makeActionCreator(T.TRANSFERS.MAKE_FORM_UPDATE, 'key', 'value');
export const makeTransfer = (accountId, payload) => {
return dispatch => {
dispatch(makeTransferRequested());
return api.apiMakeTransfer(accountId, payload)
.then(data => {
const { moneyTransferId } = data;
dispatch(makeTransferComplete(data));
return moneyTransferId;
})
.catch(err => {
dispatch(makeTransferError(err));
return err;
});
};
};
export const getTransfersRequested = makeActionCreator(T.TRANSFERS.LIST_START);
export const getTransfersComplete = makeActionCreator(T.TRANSFERS.LIST_COMPLETE, 'payload');
export const getTransfersError = makeActionCreator(T.TRANSFERS.LIST_ERROR, 'error');
export const getTransfers = (accountId) => {
return dispatch => {
dispatch(getTransfersRequested());
return api.apiRetrieveTransfers(accountId)
.then(data => {
dispatch(getTransfersComplete(data));
return data;
})
.catch(err => {
dispatch(getTransfersError(err));
return err;
});
}; };
}; };

View File

@@ -0,0 +1,46 @@
/**
* Created by andrew on 3/22/16.
*/
import React from "react";
import { connect } from 'react-redux';
import Spinner from "react-loader";
import * as BS from "react-bootstrap";
import * as A from '../actions/entities';
// import { Money } from '../components/Money';
export class AccountInfo extends React.Component {
componentWillMount() {
this.ensureData(this.props);
}
componentWillReceiveProps(nextProps) {
this.ensureData(nextProps);
}
ensureData({ dispatch, entities, accountId }) {
if (entities[accountId]) {
return;
}
dispatch(A.fetchAccount(accountId));
}
render() {
const { entities, accountId } = this.props;
const account = entities[accountId];
if (!account) {
return (<div>{ accountId } <Spinner ref="spinner" loaded={false} /></div>)
}
const { title } = account;
return (<div>{ title } </div>);
}
}
export default connect(({ app }) => ({
entities: app.data.entities
}))(AccountInfo);

View File

@@ -0,0 +1,28 @@
/**
* Created by andrew on 3/22/16.
*/
import React from 'react';
export const moneyText = (amount) => {
if (Number.isNaN(Number(amount))) {
return '';
}
const absNum = Math.abs(Number(amount) / 100);
if (absNum < 0) {
return `$(${absNum.toFixed(2)})`;
}
return `$${absNum.toFixed(2)}`;
};
export const Money = ({ amount }) => {
if (Number.isNaN(Number(amount))) {
return (<span />);
}
const absNum = Math.abs(Number(amount) / 100);
if (absNum < 0) {
return (<span className="text-danger">($${ absNum.toFixed(2) })</span>)
}
return (<span>${ absNum.toFixed(2) }</span>);
};

View File

@@ -0,0 +1,58 @@
/**
* Created by andrew on 3/22/16.
*/
import React from "react";
import Spinner from "react-loader";
import * as BS from "react-bootstrap";
import { Money } from './Money';
import AccountInfo from './AccountInfo';
export class TransfersTable extends React.Component {
render() {
const { loading, data, errors } = this.props;
if (loading) {
return (<h2><Spinner ref="spinner" loaded={false} /> Loading..</h2>);
}
if (Object.keys(errors).length) {
return (<div className="text-danger">Errors..</div>);
}
const transfers = data.map(({
amount,
fromAccountId,
toAccountId,
transactionId,
description = '',
date = null,
status = ''
}, idx) => (<tr key={idx}>
<td>{ date || 'N/a'}</td>
<td><AccountInfo accountId={ fromAccountId } /></td>
<td><AccountInfo accountId={ toAccountId } /></td>
<td><Money amount={ amount } /></td>
<td>{ description || 'N/a'}</td>
<td>{ status || 'N/a' }</td>
</tr>));
return (
<BS.Table striped bordered condensed hover>
<thead>
<tr>
<th>Date</th>
<th>What</th>
<th>Counter Account</th>
<th>Amount</th>
<th>Description</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{ transfers }
</tbody>
</BS.Table>
);
}
}

View File

@@ -32,9 +32,6 @@ class Container extends React.Component {
<LinkContainer to="/" onlyActiveOnIndex={true}> <LinkContainer to="/" onlyActiveOnIndex={true}>
<NavItem eventKey={1}>Home</NavItem> <NavItem eventKey={1}>Home</NavItem>
</LinkContainer> </LinkContainer>
<LinkContainer to="/account">
<NavItem eventKey={2}>Account</NavItem>
</LinkContainer>
</Nav> </Nav>
<div> <div>
<HeaderLinks /> <HeaderLinks />

View File

@@ -58,6 +58,16 @@ export default defineActionTypes({
DELETE_COMPLETE DELETE_COMPLETE
DELETE_ERROR DELETE_ERROR
`, `,
TRANSFERS: `
MAKE_START
MAKE_COMPLETE
MAKE_ERROR
MAKE_FORM_UPDATE
LIST_START
LIST_COMPLETE
LIST_ERROR
`,
ERROR: ` ERROR: `
START START

View File

@@ -1,9 +1,6 @@
/** /**
* Created by andrew on 15/03/16. * Created by andrew on 15/03/16.
*/ */
/**
* Created by andrew on 25/02/16.
*/
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { accounts } from './accounts'; import { accounts } from './accounts';

View File

@@ -7,39 +7,36 @@
import T from '../../constants/ACTION_TYPES'; import T from '../../constants/ACTION_TYPES';
const initialState = { const initialState = {
};
const nodeInitialState = {
loading: false, loading: false,
data: {} errors: {},
data: []
}; };
export const transfers = (state = {...initialState}, action) => { export const transfers = (state = {...initialState}, action) => {
switch(action.type) { switch(action.type) {
case T.ENTITIES.REQUESTED: {
const { id } = action; case T.TRANSFERS.LIST_START: {
return { return {
...state, ...state,
[id]: { loading: true
...nodeInitialState, };
loading: true
}
}
} }
case T.ENTITIES.RECEIVED: { case T.TRANSFERS.LIST_COMPLETE: {
const { id, entity = {} } = action; const { payload } = action;
return {
...initialState,
data: [...payload]
};
}
case T.TRANSFERS.LIST_ERROR: {
const { error } = action;
return { return {
...state, ...state,
[id]: { loading: false,
...(state[id] || nodeInitialState), errors: Object.isSealed(error) ? { message: error } : { ...error }
loading: false, };
data: {
...entity
}
}
}
} }
case T.ENTITIES.RECEIVED_LIST:
default: default:
return state; return state;
} }

View File

@@ -9,11 +9,14 @@ import { combineReducers } from 'redux';
import { account } from './account'; import { account } from './account';
import { error } from './errors'; import { error } from './errors';
import { bookmarkAccount } from './bookmarkAccount'; import { bookmarkAccount } from './bookmarkAccount';
import { transfersMake } from './transfersMake';
const uiReducer = combineReducers({ const uiReducer = combineReducers({
account, account,
error, error,
bookmarkAccount bookmarkAccount,
transfersMake
}); });
export default uiReducer; export default uiReducer;

View File

@@ -0,0 +1,54 @@
/**
* Created by andrew on 15/03/16.
*/
/**
* Created by andrew on 15/03/16.
*/
import T from '../../constants/ACTION_TYPES';
const initialState = {
loading: false,
form: {},
errors: {}
};
export const transfersMake = (state = {...initialState}, action) => {
switch(action.type) {
case T.TRANSFERS.MAKE_START: {
return {
...state,
loading: true
}
}
case T.TRANSFERS.MAKE_ERROR: {
const { error } = action;
return {
...state,
loading: false,
errors: Object.isSealed(error) ? { message: error } : { ...error }
};
}
case T.TRANSFERS.MAKE_COMPLETE: {
return {
...initialState
}
}
case T.TRANSFERS.MAKE_FORM_UPDATE: {
const { key, value } = action;
return {
...state,
form: {
...state.form,
[key]: value
},
errors: {
...state.errors,
[key]: null
}
}
}
default:
return state;
}
};

View File

@@ -7,7 +7,8 @@ import {
getEmailSignUpUrl, getEmailSignUpUrl,
getCurrentUserUrl, getCurrentUserUrl,
getAccountsUrl, getAccountsUrl,
getCustomersUrl getCustomersUrl,
getTransfersUrl
} from "./sessionStorage"; } from "./sessionStorage";
import root from './root'; import root from './root';
@@ -70,6 +71,24 @@ export function apiCreateAccount(customerId, {
}).then(parseResponse); }).then(parseResponse);
} }
export function apiMakeTransfer(fromAccountId, {
account, amount, description }) {
return fetch(getTransfersUrl(), {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "post",
body: root.JSON.stringify({
"amount": amount,
"fromAccountId": fromAccountId,
"toAccountId": account,
description
})
}).then(parseResponse);
}
export function apiRetrieveAccounts(customerId) { export function apiRetrieveAccounts(customerId) {
return fetch(`${getAccountsUrl()}?${makeQuery({ customerId })}`, { return fetch(`${getAccountsUrl()}?${makeQuery({ customerId })}`, {
@@ -81,6 +100,17 @@ export function apiRetrieveAccounts(customerId) {
}).then(parseResponse); }).then(parseResponse);
} }
export function apiRetrieveTransfers(accountId) {
return fetch(`${getAccountsUrl()}/${accountId}/history`, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "get"
}).then(parseResponse);
}
export function apiRetrieveAccount(accountId) { export function apiRetrieveAccount(accountId) {
return fetch(`${getAccountsUrl()}/${accountId}`, { return fetch(`${getAccountsUrl()}/${accountId}`, {
headers: { headers: {
@@ -114,3 +144,13 @@ export function apiRetrieveUsers(email) {
method: "get" method: "get"
}).then(parseResponse); }).then(parseResponse);
} }
export function apiRetrieveUser(customerId) {
return fetch(`${getCustomersUrl()}?${makeQuery({ customerId })}`, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: "get"
}).then(parseResponse);
}

View File

@@ -23,7 +23,11 @@ export function parseResponse (response) {
message.replace(jvmPattern, (m, name) => { message.replace(jvmPattern, (m, name) => {
errors[name] = ['Required']; errors[name] = ['Required'];
}); });
return { errors };
if (Object.keys(errors).length) {
return { errors };
}
return { errors: message };
}).then(err => Promise.reject(err)); }).then(err => Promise.reject(err));
} }
} }

View File

@@ -135,6 +135,10 @@ export function getCustomersUrl () {
return `${getSessionEndpoint().customersPath}` return `${getSessionEndpoint().customersPath}`
} }
export function getTransfersUrl () {
return `${getSessionEndpoint().transfersPath}`
}
/** /**
* @deprecated * @deprecated
* @param key * @param key

View File

@@ -9,12 +9,19 @@ import { PageHeader, OverlayTrigger, Tooltip, Grid, Col, Row, Nav, NavItem, Butt
import * as BS from "react-bootstrap"; import * as BS from "react-bootstrap";
import Select from "react-select"; import Select from "react-select";
import Spinner from "react-loader"; import Spinner from "react-loader";
import Input from "../controls/bootstrap/Input";
import { Money, moneyText } from '../components/Money';
import { TransfersTable } from '../components/TransfersTable';
import { Link, IndexLink} from "react-router"; import { Link, IndexLink} from "react-router";
import IndexPanel from "./../components/partials/IndexPanel"; import IndexPanel from "./../components/partials/IndexPanel";
import * as Modals from './modals'; import * as Modals from './modals';
import * as A from '../actions/entities'; import * as A from '../actions/entities';
import read from '../utils/readProp';
const resetModals = { const resetModals = {
showAccountModal: false showAccountModal: false
@@ -26,15 +33,20 @@ export class Account extends React.Component {
this.state = { ...resetModals }; this.state = { ...resetModals };
} }
componentWillMount() { loadAccountInfo() {
const { const {
id: customerId id: customerId
} = this.props.auth.user.attributes; } = this.props.auth.user.attributes;
this.props.dispatch(A.fetchOwnAccounts(customerId)); this.props.dispatch(A.fetchOwnAccounts(customerId));
const { dispatch, params } = this.props; const { dispatch, params } = this.props;
const { accountId } = params; const { accountId } = params;
dispatch(A.fetchAccount(accountId)); dispatch(A.fetchAccount(accountId));
dispatch(A.getTransfers(accountId));
}
componentWillMount() {
this.loadAccountInfo();
} }
createAccountModal() { createAccountModal() {
@@ -47,9 +59,6 @@ export class Account extends React.Component {
debugger; debugger;
} }
accountChanged(){
}
close() { close() {
this.setState({ this.setState({
@@ -57,6 +66,19 @@ export class Account extends React.Component {
}); });
} }
handleInput(key, value) {
this.props.dispatch(A.makeTransferFormUpdate(key, value));
}
initiateTransfer(){
const { dispatch, params, transfer } = this.props;
const { accountId } = params;
dispatch(A.makeTransfer(accountId, transfer.form ))
.then(() => {
this.loadAccountInfo();
});
}
render () { render () {
const { showAccountModal } = this.state; const { showAccountModal } = this.state;
@@ -86,7 +108,7 @@ export class Account extends React.Component {
if (itemAccountId != accountId) { if (itemAccountId != accountId) {
memo.push({ memo.push({
value: itemAccountId , value: itemAccountId ,
label: `${title}: $${ Number(balance).toFixed(2) }` label: `${title}: ${ moneyText(balance) }`
}); });
} }
return memo; return memo;
@@ -101,12 +123,13 @@ export class Account extends React.Component {
return memo; return memo;
}, [])); }, []));
const { title: titleRaw, description: descriptionRaw, balance: balanceRaw } = account; const { title: titleRaw, description: descriptionRaw, balance } = account;
const title = titleRaw || '[No title]'; const title = titleRaw || '[No title]';
const balance = ((balanceRaw > 0 && balanceRaw < 1) ? '$0' : '$') + Number(balanceRaw).toFixed(2);
const description = descriptionRaw || '[No description]'; const description = descriptionRaw || '[No description]';
const transferDisabled = this.props.transfer.loading;
return ( return (
<div> <div>
<PageHeader> <PageHeader>
@@ -128,7 +151,7 @@ export class Account extends React.Component {
<Row> <Row>
<Col xs={4}>Balance:</Col> <Col xs={4}>Balance:</Col>
<Col xs={8}><strong>{ balance }</strong></Col> <Col xs={8}><strong><Money amount={balance} /></strong></Col>
</Row> </Row>
<Row> <Row>
@@ -148,22 +171,45 @@ export class Account extends React.Component {
<Col xs={4}> <Col xs={4}>
<label>Transfer To:</label> <label>Transfer To:</label>
<Select <Select
value={''} value={read(this.props.transfer, 'form.account', '')}
clearable={false} clearable={true}
options={transferTo} options={transferTo}
onChange={this.accountChanged.bind(this)} /> disabled={transferDisabled}
onChange={this.handleInput.bind(this, 'account')}
/>
</Col> </Col>
<Col xs={3}> <Col xs={3}>
<label>Amount:</label> <Input type="text"
<BS.Input type="text" /> className=""
label="Amount:"
placeholder="Amount"
name="amount"
addonBefore={
(<BS.Glyphicon glyph="usd" />)
}
addonAfter=".00"
disabled={transferDisabled}
value={read(this.props.transfer, 'form.amount', '')}
errors={read(this.props.transfer, 'errors.amount', []) || []}
onChange={this.handleInput.bind(this, 'amount')}
/>
</Col> </Col>
<Col xs={3}> <Col xs={3}>
<label>Description:</label> <Input type="textarea"
<BS.Input type="textarea" /> className=""
label="Description:"
placeholder="Description"
name="description"
disabled={transferDisabled}
value={read(this.props.transfer, 'form.description', '') || ''}
errors={read(this.props.transfer, 'errors.description', []) || []}
onChange={this.handleInput.bind(this, 'description')}
/>
</Col> </Col>
<Col xs={2}> <Col xs={2}>
<br/> <br/>
<Button bsStyle="primary">Transfer</Button> <Button bsStyle="primary"
onClick={this.initiateTransfer.bind(this)}>Transfer</Button>
</Col> </Col>
</Row> </Row>
@@ -172,28 +218,7 @@ export class Account extends React.Component {
<h3>Account History:</h3> <h3>Account History:</h3>
</Col> </Col>
</Row> </Row>
<Table> <TransfersTable { ...this.props.transfers } />
<thead>
<tr>
<th>Date</th>
<th>What</th>
<th>Counter Account</th>
<th>Amount</th>
<th>Description</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="#">Account Title #1</a></td>
<td>$100.00</td>
</tr>
<tr>
<td><a href="#">Account Title #2</a></td>
<td>$100.00</td>
</tr>
</tbody>
</Table>
<Modals.NewAccountModal show={showAccountModal} <Modals.NewAccountModal show={showAccountModal}
action={this.createAccountModalConfirmed.bind(this)} action={this.createAccountModalConfirmed.bind(this)}
@@ -213,5 +238,7 @@ export default connect(({
}) => ({ }) => ({
auth: app.auth, auth: app.auth,
data: app.data, data: app.data,
ui: app.ui.account transfers: app.data.transfers,
ui: app.ui.account,
transfer: app.ui.transfersMake
}))(Account); }))(Account);

View File

@@ -13,6 +13,8 @@ import IndexPanel from "./../components/partials/IndexPanel";
import * as A from '../actions/entities'; import * as A from '../actions/entities';
import read from '../utils/readProp'; import read from '../utils/readProp';
import { Money } from '../components/Money';
const resetModals = { const resetModals = {
@@ -50,7 +52,11 @@ class MyAccounts extends React.Component {
this.props.dispatch(A.accountCreate(customerId, payload)) this.props.dispatch(A.accountCreate(customerId, payload))
.then(() => { .then(() => {
this.close.bind(this); this.close.bind(this);
this.props.dispatch(A.fetchOwnAccounts(customerId)); return this.props.dispatch(A.fetchOwnAccounts(customerId));
})
.catch(err => {
debugger;
this.props.dispatch(A.accountCreateError(err));
}); });
} }
@@ -139,7 +145,7 @@ class MyAccounts extends React.Component {
<span>{ description }</span> <span>{ description }</span>
]: null ]: null
}</td> }</td>
<td key={1}>${ balance } </td> <td key={1}><Money amount={balance} /></td>
<td key={2}><BS.Button bsStyle={"link"} onClick={this.remove3rdPartyAccountModal.bind(this, accountId)}><BS.Glyphicon glyph="remove" /></BS.Button></td> <td key={2}><BS.Button bsStyle={"link"} onClick={this.remove3rdPartyAccountModal.bind(this, accountId)}><BS.Glyphicon glyph="remove" /></BS.Button></td>
</tr> </tr>
)); ));

View File

@@ -15,22 +15,17 @@ import * as A from '../../actions/entities';
export class Add3rdPartyAccountModal extends React.Component { export class Add3rdPartyAccountModal extends React.Component {
ownerTypeIn(val) {
this.props.dispatch(A.createRefOwnerLookup(val));
}
ownerChanged(argq, arg2, arg3) {
debugger;
}
accountChanged(argq, arg2, arg3) {
debugger;
}
handleInput(key, value) { handleInput(key, value) {
//this.props.dispatch(A.createRefOwnerLookup(val)); this.props.dispatch(A.accountRefCreateFormUpdate(key, value));
debugger; switch(key) {
case 'owner':
debugger;
if (value) {
this.props.dispatch(A.createRefAccountLookup(value));
} else {
this.props.dispatch(A.createRefAccountLookupComplete({}));
}
}
} }
getOwnersOptions(input) { getOwnersOptions(input) {
@@ -39,6 +34,7 @@ debugger;
} }
return this.props.dispatch(A.createRefOwnerLookup(input)); return this.props.dispatch(A.createRefOwnerLookup(input));
} }
render() { render() {
const disabled = false; const disabled = false;
@@ -80,7 +76,7 @@ debugger;
{value: "bootstrap", label: "Bootstrap"}, {value: "bootstrap", label: "Bootstrap"},
{value: "materialUi", label: "Material UI"} {value: "materialUi", label: "Material UI"}
]} ]}
onChange={this.accountChanged.bind(this)} /> onChange={this.handleInput.bind(this, 'account')} />
<Input type="textarea" <Input type="textarea"
className="account-create-description" className="account-create-description"

View File

@@ -116,7 +116,7 @@ export class NewAccountModal extends React.Component {
value={read(this.props.account, 'form.title', '')} value={read(this.props.account, 'form.title', '')}
errors={read(this.props.account, 'errors.title', [])} errors={read(this.props.account, 'errors.title', [])}
onChange={this.handleInput.bind(this, "title")} onChange={this.handleInput.bind(this, "title")}
{...this.props.inputProps.title} /> />
<Input type="text" <Input type="text"
className="account-create-balance" className="account-create-balance"
@@ -131,7 +131,7 @@ export class NewAccountModal extends React.Component {
value={read(this.props.account, 'form.balance', '')} value={read(this.props.account, 'form.balance', '')}
errors={read(this.props.account, 'errors.balance', [])} errors={read(this.props.account, 'errors.balance', [])}
onChange={this.handleInput.bind(this, 'balance')} onChange={this.handleInput.bind(this, 'balance')}
{...this.props.inputProps.balance} /> />
<Input type="textarea" <Input type="textarea"
className="account-create-description" className="account-create-description"
@@ -139,10 +139,10 @@ export class NewAccountModal extends React.Component {
placeholder="Description" placeholder="Description"
name="description" name="description"
disabled={disabled} disabled={disabled}
value={read(this.props.account, 'form.description', '')} value={read(this.props.account, 'form.description', '') || ''}
errors={read(this.props.account, 'errors.description', [])} errors={read(this.props.account, 'errors.description', [])}
onChange={this.handleInput.bind(this, 'description')} onChange={this.handleInput.bind(this, 'description')}
{...this.props.inputProps.description} /> />
</form> </form>