diff --git a/app/components/BudgetGridRow/index.js b/app/components/BudgetGridRow/index.js index e9bab1e..11306f0 100644 --- a/app/components/BudgetGridRow/index.js +++ b/app/components/BudgetGridRow/index.js @@ -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'; @@ -22,10 +26,14 @@ const BudgetGridRow = ({ transaction, categories }: BudgetGridRowProps) => {
Category
{category}
+
Description
-
{description}
+
+ {description} +
+
Amount
{amount.text}
diff --git a/app/components/DonutChart/index.js b/app/components/DonutChart/index.js index 4153dbc..695e57e 100644 --- a/app/components/DonutChart/index.js +++ b/app/components/DonutChart/index.js @@ -45,8 +45,11 @@ class DonutChart extends React.Component { 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); }; @@ -70,7 +73,7 @@ class DonutChart extends React.Component { }; render() { - const { data, dataLabel, dataValue, dataKey } = this.props; + const { data, dataLabel, dataValue, dataKey, format } = this.props; const { outerRadius, pathArc, colorFn, boxLength, chartPadding } = this; return ( @@ -86,7 +89,11 @@ class DonutChart extends React.Component { ))} - + + ); } diff --git a/app/components/Legend/LegendItem.js b/app/components/Legend/LegendItem.js index 4ea2676..1c397e3 100644 --- a/app/components/Legend/LegendItem.js +++ b/app/components/Legend/LegendItem.js @@ -9,10 +9,10 @@ type LegendItemProps = { label: string, }; -const LegendItem = ({ color, label, value }: LegendItemProps) => ( +const LegendItem = ({ color, label, value, format }: LegendItemProps) => (
  • {label} - {formatAmount(value).text} + {formatAmount(value, false, format).text}
  • ); diff --git a/app/components/Legend/index.js b/app/components/Legend/index.js index 1426ef6..dcc0f85 100644 --- a/app/components/Legend/index.js +++ b/app/components/Legend/index.js @@ -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) => (
      {data.map((item, idx) => ( - + ))}
    ); diff --git a/app/containers/App/index.js b/app/containers/App/index.js index 4429fc2..0e3ba89 100644 --- a/app/containers/App/index.js +++ b/app/containers/App/index.js @@ -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 = () => (
    - + - + +
    diff --git a/app/containers/TransactionDetails/index.js b/app/containers/TransactionDetails/index.js new file mode 100644 index 0000000..a8e6984 --- /dev/null +++ b/app/containers/TransactionDetails/index.js @@ -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 { + 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 ( +
    + + < back to all transactions + + {this.state.transaction ? ( +
    +
    {this.renderDetails()}
    +
    {this.renderPieChart()}
    +
    + ) : ( +

    Transaction not found.

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

    {transaction.description}

    +

    {formattedAmount}

    + + + + + + + + + + + + + + + + + + + + + + + + +
    Item: {transaction.id}
    Description: {transaction.description}
    Amount: + {amount.text} +
    Percentage of budget: {formattedAmount}
    +
    + ); + } + + renderPieChart() { + const { chartData } = this.state ; + + return ( + + ); + } + + +} + +const mapStateToProps = state => ({ + transactions: getTransactions(state), + totals: { + inflow: getInflowBalance(state), + outflow: Math.abs(getOutflowBalance(state)), + }, +}); + +export default connect(mapStateToProps)(TransactionDetails); diff --git a/app/containers/TransactionDetails/style.scss b/app/containers/TransactionDetails/style.scss new file mode 100644 index 0000000..b1de6c8 --- /dev/null +++ b/app/containers/TransactionDetails/style.scss @@ -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%; +} + diff --git a/app/routes/Transaction/index.js b/app/routes/Transaction/index.js new file mode 100644 index 0000000..626b194 --- /dev/null +++ b/app/routes/Transaction/index.js @@ -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 ; + } +} + + +export default Transaction; diff --git a/app/routes/permalinks.js b/app/routes/permalinks.js new file mode 100644 index 0000000..f9ca223 --- /dev/null +++ b/app/routes/permalinks.js @@ -0,0 +1,7 @@ + +const permalinks = { + budget: "budget", + transaction: "transaction", +}; + +export default permalinks; diff --git a/app/theme/_variables.scss b/app/theme/_variables.scss index 8626749..7b37d4e 100644 --- a/app/theme/_variables.scss +++ b/app/theme/_variables.scss @@ -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; diff --git a/app/utils/formatAmount.js b/app/utils/formatAmount.js index f174992..dea6228 100644 --- a/app/utils/formatAmount.js +++ b/app/utils/formatAmount.js @@ -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}`,