WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
This repository was archived by the owner on Jul 14, 2020. It is now read-only.
10 changes: 9 additions & 1 deletion app/components/BudgetGridRow/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// @flow
import * as React from 'react';
import { Link } from 'react-router-dom';

import permalinks from 'routes/permalinks';

import formatAmount from 'utils/formatAmount';
import type { Transaction } from 'modules/transactions';
import type { Categories } from 'modules/categories';
Expand All @@ -22,10 +26,14 @@ const BudgetGridRow = ({ transaction, categories }: BudgetGridRowProps) => {
<div className={styles.cellLabel}>Category</div>
<div className={styles.cellContent}>{category}</div>
</td>

<td>
<div className={styles.cellLabel}>Description</div>
<div className={styles.cellContent}>{description}</div>
<div className={styles.cellContent}>
<Link to={`/${permalinks.transaction}/${id}`}>{description}</Link>
</div>
</td>

<td className={amountCls}>
<div className={styles.cellLabel}>Amount</div>
<div className={styles.cellContent}>{amount.text}</div>
Expand Down
13 changes: 10 additions & 3 deletions app/components/DonutChart/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ class DonutChart extends React.Component<DonutChartProps> {

getPathArc = () => {
const { height, innerRatio } = this.props;
const calculatedInnerRadius = innerRatio===0 ? innerRatio : (height / innerRatio);
// zero innerRadius makes donut chart look like pie chart

return arc()
.innerRadius(height / innerRatio)
.innerRadius( calculatedInnerRadius )
.outerRadius(height / 2);
};

Expand All @@ -70,7 +73,7 @@ class DonutChart extends React.Component<DonutChartProps> {
};

render() {
const { data, dataLabel, dataValue, dataKey } = this.props;
const { data, dataLabel, dataValue, dataKey, format } = this.props;
const { outerRadius, pathArc, colorFn, boxLength, chartPadding } = this;

return (
Expand All @@ -86,7 +89,11 @@ class DonutChart extends React.Component<DonutChartProps> {
))}
</Chart>

<Legend color={colorFn} {...{ data, dataValue, dataLabel, dataKey }} />
<Legend
color={colorFn}
{...{ data, dataValue, dataLabel, dataKey }}
{...{format}} />

</div>
);
}
Expand Down
4 changes: 2 additions & 2 deletions app/components/Legend/LegendItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ type LegendItemProps = {
label: string,
};

const LegendItem = ({ color, label, value }: LegendItemProps) => (
const LegendItem = ({ color, label, value, format }: LegendItemProps) => (
<li style={{ color }}>
<span>{label}</span>
<span className={styles.value}> {formatAmount(value).text} </span>
<span className={styles.value}> {formatAmount(value, false, format).text} </span>
</li>
);

Expand Down
9 changes: 7 additions & 2 deletions app/components/Legend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ type LegendType = {
reverse: boolean,
};

const Legend = ({ data, color, dataValue, dataLabel, dataKey, reverse }: LegendType) => (
const Legend = ({ data, color, dataValue, dataLabel, dataKey, reverse, format }: LegendType) => (
<ul className={cx(styles.legend, { [styles.reverse]: reverse })}>
{data.map((item, idx) => (
<LegendItem color={color(idx)} key={item[dataKey]} label={item[dataLabel]} value={item[dataValue]} />
<LegendItem
color={color(idx)}
key={item[dataKey]}
label={item[dataLabel]}
value={item[dataValue]}
{...{format}} />
))}
</ul>
);
Expand Down
8 changes: 6 additions & 2 deletions app/containers/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@ import AppError from 'components/AppError';
import Header from 'components/Header';
import Budget from 'routes/Budget';
import Reports from 'routes/Reports';
import Transaction from 'routes/Transaction';
import './style.scss';

import permalinks from 'routes/permalinks';

const App = () => (
<ErrorBoundary fallbackComponent={AppError}>
<main>
<Header />

<Switch>
<Route path="/budget" component={Budget} />
<Route path={`/${permalinks.budget}`} component={Budget} />
<Route path="/reports" component={Reports} />
<Redirect to="/budget" />
<Route path="/transaction/:id" component={Transaction} />
<Redirect to={`/${permalinks.budget}`} />
</Switch>
</main>
</ErrorBoundary>
Expand Down
173 changes: 173 additions & 0 deletions app/containers/TransactionDetails/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// @flow
import * as React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';

import permalinks from 'routes/permalinks';

import transactionReducer from 'modules/transactions';
import { injectAsyncReducers } from 'store';

import {
getTransactions,
getInflowBalance,
getOutflowBalance,
} from 'selectors/transactions';

import type { Transaction } from 'modules/transactions';
import formatAmount from 'utils/formatAmount';
import styles from './style.scss'
import BudgetGridRowStyles from 'components/BudgetGridRow/style.scss';
import BudgetGridStyles from 'containers/BudgetGrid/style.scss';

import DonutChart from 'components/DonutChart';

// inject reducers that might not have been originally there
injectAsyncReducers({
transactions: transactionReducer,
});


type TransactionDetailsProps = {
transactions: Transaction[],
};

class TransactionDetails extends React.Component<TransactionDetailsProps> {
static defaultProps = {
transactions: [],
};

state = {
TransactionID: this.props.params.id,
transaction: this.props.transactions.find( item => item.id === parseInt(this.props.params.id) ),
}

componentWillMount() {
const transaction = this.state.transaction;

if (!transaction) {
return;
}

const amount = formatAmount(transaction.value),
t = Object.assign({}, this.state.transaction);

let chartData = new Array(),
percent = 0;

this.state.amount = amount;
this.state.amountCls = amount.isNegative ? BudgetGridRowStyles.neg : BudgetGridRowStyles.pos;

if( amount.isNegative ) {
percent = t.value / this.props.totals.outflow * 100;

t.percentWithSign = percent;
t.value = percent * -1;

chartData.push({
id: 0,
value: 100 + percent,
description: "Rest outflow",
});
} else {
percent = t.value / this.props.totals.inflow * 100 ;

t.percentWithSign = percent;
t.value = percent;

chartData.push({
id: 0,
value: 100 - percent,
description: "Rest inflow",
});
}
chartData.unshift(t);

this.state.chartData = chartData;
}

render() {
return (
<div>

<Link to={`/${permalinks.budget}`} className={styles.backButton} >&lt; back to all transactions</Link>

{this.state.transaction ? (
<div className={styles.clearfix}>
<div className={styles.leftCol}>{this.renderDetails()}</div>
<div className={styles.rightCol}>{this.renderPieChart()}</div>
</div>
) : (
<h4>Transaction not found.</h4>
)}

</div>
)
}

renderDetails() {
const { transaction, amount, amountCls, chartData } = this.state,
formattedAmount = formatAmount(chartData[0].percentWithSign, true, "percentage").text;

return (
<div>
<h1>{transaction.description}</h1>
<h2 className={amountCls}>{formattedAmount}</h2>

<table className={BudgetGridStyles.budgetGrid}>
<tbody>
<tr>
<td>Item: </td>
<td>{transaction.id}</td>
</tr>

<tr>
<td>Description: </td>
<td>{transaction.description}</td>
</tr>

<tr>
<td>Amount: </td>
<td className={amountCls}>
<span className={BudgetGridRowStyles.cellContent}>{amount.text}</span>
</td>
</tr>

<tr>
<td>Percentage of budget: </td>
<td className={amountCls}>{formattedAmount}</td>
</tr>

</tbody>
</table>
</div>
);
}

renderPieChart() {
const { chartData } = this.state ;

return (
<DonutChart
data={chartData}
dataLabel="description" dataKey="id"
innerRatio={0}
// zero innerRadius makes donut chart look like pie chart

height={200}
format="percentage" />
);
}


}

const mapStateToProps = state => ({
transactions: getTransactions(state),
totals: {
inflow: getInflowBalance(state),
outflow: Math.abs(getOutflowBalance(state)),
},
});

export default connect(mapStateToProps)(TransactionDetails);
36 changes: 36 additions & 0 deletions app/containers/TransactionDetails/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@import 'theme/variables';

.backButton {
text-decoration: none;
color: $gray;
padding: 6px 8px 8px;
border: 1px solid;
border-radius: 4px;
display: inline-block;
margin-bottom: 20px;
&:hover {
color: black;
}
}

.clearfix {
&:before,&:after {
content: " "; // 1
display: table; // 2
}

&:after {
clear: both;
}
}

.leftCol {
float: left;
width: 50%;
}

.rightCol {
float: left;
width: 50%;
}

15 changes: 15 additions & 0 deletions app/routes/Transaction/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @flow
import React, { Component } from 'react';

import Chunk from 'components/Chunk';

const loadTransactionDetails = () => import('containers/TransactionDetails');

class Transaction extends Component<> {
render() {
return <Chunk load={loadTransactionDetails} params={this.props.match.params}/>;
}
}


export default Transaction;
7 changes: 7 additions & 0 deletions app/routes/permalinks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

const permalinks = {
budget: "budget",
transaction: "transaction",
};

export default permalinks;
3 changes: 3 additions & 0 deletions app/theme/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ $white: #fff;
$gray: #b1b1b1;
$lightgray: #dadada;
$verylightgray: #f0f0f0;

$darkgray: rgba(0,0,0,0.16);
// NOTE: darkgray name is correct, rgba(0,0,0,0.16) is lighter than $gray: #b1b1b1;

$red: #eb2a2a;
$green: #189c2d;

Expand Down
19 changes: 14 additions & 5 deletions app/utils/formatAmount.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@ export type FormattedAmount = {
isNegative: boolean,
};

export default function formatAmount(amount: number, showSign: boolean = true): FormattedAmount {
export default function formatAmount(amount: number, showSign: boolean = true, format = null): FormattedAmount {
const isNegative = amount < 0;
const formatValue = Math.abs(amount).toLocaleString('en-us', {
style: 'currency',
currency: 'USD',
});
let formatValue;

if ( format === "percentage" ) {
let valueForFormat = amount / 100;
formatValue = Math.abs(valueForFormat).toLocaleString('en-us', {
style: 'percent',
});
} else {
formatValue = Math.abs(amount).toLocaleString('en-us', {
style: 'currency',
currency: 'USD',
});
}

return {
text: `${isNegative && showSign ? '-' : ''}${formatValue}`,
Expand Down