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',
customersPath: '/customers',
currentUserPath: '/user',
accountsPath: '/accounts'
accountsPath: '/accounts',
transfersPath: '/transfers'
}
}
], {

View File

@@ -59,13 +59,16 @@ export function accountCreate(customerId, payload) {
return dispatch(authenticate(true));
})
.catch(err => {
debugger;
dispatch(accountCreateError(err));
return Promise.resolve({ error: err });
})
};
}
export function fetchOwnAccounts(customerId) {
return dispatch => {
//dispatch(accountsListRequested());
@@ -154,9 +157,54 @@ export const createRefOwnerLookup = lookup => {
};
};
export const createRefAccountLookup = lookup => {
export const createRefAccountLookup = customerId => {
return dispatch => {
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}>
<NavItem eventKey={1}>Home</NavItem>
</LinkContainer>
<LinkContainer to="/account">
<NavItem eventKey={2}>Account</NavItem>
</LinkContainer>
</Nav>
<div>
<HeaderLinks />

View File

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

View File

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

View File

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

View File

@@ -9,11 +9,14 @@ import { combineReducers } from 'redux';
import { account } from './account';
import { error } from './errors';
import { bookmarkAccount } from './bookmarkAccount';
import { transfersMake } from './transfersMake';
const uiReducer = combineReducers({
account,
error,
bookmarkAccount
bookmarkAccount,
transfersMake
});
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,
getCurrentUserUrl,
getAccountsUrl,
getCustomersUrl
getCustomersUrl,
getTransfersUrl
} from "./sessionStorage";
import root from './root';
@@ -70,6 +71,24 @@ export function apiCreateAccount(customerId, {
}).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) {
return fetch(`${getAccountsUrl()}?${makeQuery({ customerId })}`, {
@@ -81,6 +100,17 @@ export function apiRetrieveAccounts(customerId) {
}).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) {
return fetch(`${getAccountsUrl()}/${accountId}`, {
headers: {
@@ -114,3 +144,13 @@ export function apiRetrieveUsers(email) {
method: "get"
}).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) => {
errors[name] = ['Required'];
});
return { errors };
if (Object.keys(errors).length) {
return { errors };
}
return { errors: message };
}).then(err => Promise.reject(err));
}
}

View File

@@ -135,6 +135,10 @@ export function getCustomersUrl () {
return `${getSessionEndpoint().customersPath}`
}
export function getTransfersUrl () {
return `${getSessionEndpoint().transfersPath}`
}
/**
* @deprecated
* @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 Select from "react-select";
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 IndexPanel from "./../components/partials/IndexPanel";
import * as Modals from './modals';
import * as A from '../actions/entities';
import read from '../utils/readProp';
const resetModals = {
showAccountModal: false
@@ -26,15 +33,20 @@ export class Account extends React.Component {
this.state = { ...resetModals };
}
componentWillMount() {
loadAccountInfo() {
const {
id: customerId
} = this.props.auth.user.attributes;
} = this.props.auth.user.attributes;
this.props.dispatch(A.fetchOwnAccounts(customerId));
const { dispatch, params } = this.props;
const { accountId } = params;
dispatch(A.fetchAccount(accountId));
dispatch(A.getTransfers(accountId));
}
componentWillMount() {
this.loadAccountInfo();
}
createAccountModal() {
@@ -47,9 +59,6 @@ export class Account extends React.Component {
debugger;
}
accountChanged(){
}
close() {
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 () {
const { showAccountModal } = this.state;
@@ -86,7 +108,7 @@ export class Account extends React.Component {
if (itemAccountId != accountId) {
memo.push({
value: itemAccountId ,
label: `${title}: $${ Number(balance).toFixed(2) }`
label: `${title}: ${ moneyText(balance) }`
});
}
return memo;
@@ -101,12 +123,13 @@ export class Account extends React.Component {
return memo;
}, []));
const { title: titleRaw, description: descriptionRaw, balance: balanceRaw } = account;
const { title: titleRaw, description: descriptionRaw, balance } = account;
const title = titleRaw || '[No title]';
const balance = ((balanceRaw > 0 && balanceRaw < 1) ? '$0' : '$') + Number(balanceRaw).toFixed(2);
const description = descriptionRaw || '[No description]';
const transferDisabled = this.props.transfer.loading;
return (
<div>
<PageHeader>
@@ -128,7 +151,7 @@ export class Account extends React.Component {
<Row>
<Col xs={4}>Balance:</Col>
<Col xs={8}><strong>{ balance }</strong></Col>
<Col xs={8}><strong><Money amount={balance} /></strong></Col>
</Row>
<Row>
@@ -148,22 +171,45 @@ export class Account extends React.Component {
<Col xs={4}>
<label>Transfer To:</label>
<Select
value={''}
clearable={false}
value={read(this.props.transfer, 'form.account', '')}
clearable={true}
options={transferTo}
onChange={this.accountChanged.bind(this)} />
disabled={transferDisabled}
onChange={this.handleInput.bind(this, 'account')}
/>
</Col>
<Col xs={3}>
<label>Amount:</label>
<BS.Input type="text" />
<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 xs={3}>
<label>Description:</label>
<BS.Input type="textarea" />
<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 xs={2}>
<br/>
<Button bsStyle="primary">Transfer</Button>
<Button bsStyle="primary"
onClick={this.initiateTransfer.bind(this)}>Transfer</Button>
</Col>
</Row>
@@ -172,28 +218,7 @@ export class Account extends React.Component {
<h3>Account History:</h3>
</Col>
</Row>
<Table>
<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>
<TransfersTable { ...this.props.transfers } />
<Modals.NewAccountModal show={showAccountModal}
action={this.createAccountModalConfirmed.bind(this)}
@@ -213,5 +238,7 @@ export default connect(({
}) => ({
auth: app.auth,
data: app.data,
ui: app.ui.account
transfers: app.data.transfers,
ui: app.ui.account,
transfer: app.ui.transfersMake
}))(Account);

View File

@@ -13,6 +13,8 @@ import IndexPanel from "./../components/partials/IndexPanel";
import * as A from '../actions/entities';
import read from '../utils/readProp';
import { Money } from '../components/Money';
const resetModals = {
@@ -50,7 +52,11 @@ class MyAccounts extends React.Component {
this.props.dispatch(A.accountCreate(customerId, payload))
.then(() => {
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>
]: null
}</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>
</tr>
));

View File

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

View File

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