diff --git a/src/App.js b/src/App.js index 39e4f6a..be71301 100644 --- a/src/App.js +++ b/src/App.js @@ -1,319 +1,43 @@ import React, { Component } from 'react'; -import { - Row, - Col, - Button, - Form - } from 'react-bootstrap' -import './App.css'; +import DataTable from './components/DataTable.js'; +// import DataEntry from './components/DataEntry.js'; +import Calculations from './components/Calculations.js'; +import seedData from './data/seedData'; class App extends Component { - constructor() { - super() - // "seed" data initially + constructor(props) { + super(props); this.state = { - revenue: [ - { - name: 'Item 1', - oneTime: 100, - monthly: 50 - }, - { - name: 'Item 2', - oneTime: 50, - monthly: 25 - }, - { - name: 'Item 3', - oneTime: 25, - monthly: 85 - }], - expenses:[{ - name: 'Expense 1', - oneTime: 500, - monthly: 20.00 - }, - { - name: 'Expense 2', - oneTime: 200, - monthly: 40 - }], - oneTimeRevenue: 175, - oneTimeExpense: 700, - monthlyRevenue: 160, - monthlyExpense: 60, - newType: '', - newName: '', - newOneTime: '', - newMonthly: '', - error: false - } - - this.handleDelete = this.handleDelete.bind(this) - this.handleAdd = this.handleAdd.bind(this) - - // controlled form elements functions - this.handleTypeChange = this.handleTypeChange.bind(this) - this.handleNameChange = this.handleNameChange.bind(this) - this.handleOneTimeChange = this.handleOneTimeChange.bind(this) - this.handleMonthlyChange = this.handleMonthlyChange.bind(this) - } - - // Delete expense or revenue from list - handleDelete(type, index) { - // listType will be 'expenses' or 'revenue' depending on item to delete - let listType = this.state[type] - // recalculate and set totals in state - if (type === 'expenses') { - this.setState({ - oneTimeExpense: this.state.oneTimeExpense - this.state.expenses[index]['oneTime'], - monthlyExpense: this.state.monthlyExpense - this.state.expenses[index]['monthly'], - }) - } else { - // for revenue - this.setState({ - oneTimeRevenue: this.state.oneTimeRevenue - this.state.revenue[index]['oneTime'], - monthlyRevenue: this.state.monthlyRevenue - this.state.revenue[index]['monthly'], - }) - } - // remove list item from state array - this.setState({ - [listType]: listType.splice(index, 1), - }) - } - - // controlled form elements, watch for changes - handleTypeChange(e) { - this.setState({ - newType: e.target.value - }) - } - handleNameChange(e) { - this.setState({ - newName: e.target.value - }) - } - - handleMonthlyChange(e) { - this.setState({ - newMonthly: Number(e.target.value) - }) - } - handleOneTimeChange(e) { - this.setState({ - newOneTime: Number(e.target.value) - }) - } - - // add new expense or revenue - handleAdd(e) { - e.preventDefault() - // handle form errors, allows one-time and revenue amounts to be 0 - if (!this.state.newType || !this.state.newName || (!this.state.newOneTime && this.state.newOneTime !== 0) || (!this.state.newMonthly && this.state.newMonthly !== 0)) { - this.setState({ - error: true - }) - } - // if there are no form errors, add accordingly - else { - // typeOfAmount will be either 'expenses' or 'revenue' - let typeOfAmount = this.state.newType - let monthly = typeOfAmount === 'expenses' ? 'monthlyExpense' : 'monthlyRevenue' - let oneTime = typeOfAmount === 'expenses' ? 'oneTimeExpense' : 'oneTimeRevenue' - // grab state array of revenues or expenses - let items = this.state[typeOfAmount] - items.push({ - name: this.state.newName, - oneTime:this.state.newOneTime, - monthly: this.state.newMonthly - }) - // set state with new totals and items array, clear errors displaying and form contents - this.setState({ - error: false, - [typeOfAmount]: items, - [monthly]: this.state[monthly] + this.state.newMonthly, - [oneTime]: this.state[oneTime] + this.state.newOneTime, - // Clear values in form - newName: '', - newMonthly: '', - newOneTime: '', - newType: '' - }) + revenue: seedData.revenue, + expenses: seedData.expenses, } } render() { - // create table rows from revenue state list - let revenueTableData = this.state.revenue.map((item, index) => { - return ( - - {item.name} - ${item.oneTime.toFixed(2)} - ${item.monthly.toFixed(2)} - - - ) - }) - // create table rows from expenses state list - let expensesTableData = this.state.expenses.map((expense, index) => { - return ( - - {expense.name} - ${expense.oneTime.toFixed(2)} - ${expense.monthly.toFixed(2)} - - - ) - }) - - // Calculations for totals - let totalRevenue = this.state.oneTimeRevenue + (this.state.monthlyRevenue * 12) - let totalExpense = this.state.oneTimeExpense + (this.state.monthlyExpense * 12) - let monthlyContributionProfit = this.state.monthlyRevenue - this.state.monthlyExpense - let totalContributionProfit = totalRevenue - totalExpense - // handle case where totalRevenue is 0 (to avoid -Infinity and NaN) - let contributionMargin = totalRevenue !== 0 ? (totalContributionProfit / totalRevenue * 100).toFixed(0) : 0 - // handle case where totalExpense and totalRevenue are 0 (to avoid NaN) - let capitalROI = (totalExpense === 0 && totalRevenue === 0) ? 0 : ((this.state.oneTimeExpense - this.state.oneTimeRevenue) / monthlyContributionProfit).toFixed(1) + const { revenue, expenses } = this.state; + console.log(revenue); + console.log(expenses); return (

ROI Calculator

- {/* Add new expense or revenue form */} -
- - - - - - - - - - - - - - - - - - - - - -
- {/* form errors */} - { this.state.error && -

Please fill out all fields

- } -
- {/* Revenue Table */} - - - - - - - - - - - - - - {revenueTableData} - -
Revenue
One-TimeMonthly
- {/* Expenses Table */} - - - - - - - - - - - - - - {expensesTableData} - -
Expenses
One-TimeMonthly
- {/* Totals Table */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
One-TimeMonthlyTotal
Revenue${(this.state.oneTimeRevenue).toFixed(2)}${(this.state.monthlyRevenue).toFixed(2)}${totalRevenue.toFixed(2)}
Expenses${(this.state.oneTimeExpense).toFixed(2)}${(this.state.monthlyExpense).toFixed(2)}${totalExpense.toFixed(2)}
Contribution Profit${ monthlyContributionProfit.toFixed(2)}${ totalContributionProfit.toFixed(2)}
Contribution Margin{contributionMargin}%
Capital ROI (monthly){capitalROI}
-
+ this.setState({ revenue })} + /> + this.setState({ expenses })} + /> +
); } } -export default App; +export default App; \ No newline at end of file diff --git a/src/components/Calculations.js b/src/components/Calculations.js new file mode 100644 index 0000000..8b82cea --- /dev/null +++ b/src/components/Calculations.js @@ -0,0 +1,97 @@ +import React, { Component } from 'react'; +import './Data.css'; + +class Calculations extends Component { + constructor(props) { + super(props); + this.state = { + monthTerm: 24 + } + } + + render() { + const { monthTerm } = this.state; + const { revenue, expenses } = this.props; + + // Calculations for totals + let oneTimeRevenue = revenue.reduce(function (prev, cur) { + return prev + cur.oneTime; + }, 0); + + let oneTimeExpense = expenses.reduce(function (prev, cur) { + return prev + cur.oneTime; + }, 0); + + let monthlyRevenue = revenue.reduce(function (prev, cur) { + return prev + cur.monthly; + }, 0); + + let monthlyExpense = expenses.reduce(function (prev, cur) { + return prev + cur.monthly; + }, 0); + + let totalRevenue = oneTimeRevenue + (monthlyRevenue * monthTerm) + let totalExpense = oneTimeExpense + (monthlyExpense * monthTerm) + let monthlyContributionProfit = monthlyRevenue - monthlyExpense + let totalContributionProfit = totalRevenue - totalExpense + // handle case where totalRevenue is 0 (to avoid -Infinity and NaN) + let contributionMargin = totalRevenue !== 0 ? (totalContributionProfit / totalRevenue * 100).toFixed(0) : 0 + // handle case where totalExpense and totalRevenue are 0 (to avoid NaN) + let capitalROI = (totalExpense === 0 && totalRevenue === 0) ? 0 : ((oneTimeExpense - oneTimeRevenue) / monthlyContributionProfit).toFixed(1) + + return ( +
+
+ {/* Totals Table */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{monthTerm} Month Term
TotalOne-TimeMonthlyTotal
Revenue${(oneTimeRevenue).toFixed(2)}${(monthlyRevenue).toFixed(2)}${totalRevenue.toFixed(2)}
Expenses${(oneTimeExpense).toFixed(2)}${(monthlyExpense).toFixed(2)}${totalExpense.toFixed(2)}
Contribution Profit${monthlyContributionProfit.toFixed(2)}${totalContributionProfit.toFixed(2)}
Contribution Margin{contributionMargin}%
Capital ROI (monthly){capitalROI}
+
+
+ ); + } +} + +export default Calculations; \ No newline at end of file diff --git a/src/App.css b/src/components/Data.css similarity index 81% rename from src/App.css rename to src/components/Data.css index bf73fb7..849a7aa 100644 --- a/src/App.css +++ b/src/components/Data.css @@ -1,33 +1,33 @@ .addExpenseOrRevenueForm { - padding-top: 3em; -} - + padding-top: .5em; + } + .input-field, .add-form-button { - padding-bottom: 1em; -} - + padding-bottom: .5em; + } + .roi-tables { - padding-top: 2em; + padding-top: 1em; height: 100%; display: flex; flex-direction: column; align-items: center; -} - + } + .expenses-table, .revenue-table, .totals-table { margin-bottom: 3em; -} + } .expenses-table th, .revenue-table th, .totals-table th { - width: 150px; -} + width: 200px; + } .expenses-table td, .revenue-table td, .totals-table td { width: 100px -} - + } + .error { color: red; -} - + } + @media only screen and (max-width:775px) { .input-field, .add-form-button { margin-left: .25em; @@ -36,4 +36,4 @@ .roi-tables { padding-left: 1.5em; } -} +} \ No newline at end of file diff --git a/src/components/DataEntry.js b/src/components/DataEntry.js new file mode 100644 index 0000000..3828a6d --- /dev/null +++ b/src/components/DataEntry.js @@ -0,0 +1,113 @@ +import React, { Component } from 'react'; +import { + Row, + Col, + Button, + Form + } from 'react-bootstrap'; +import './Data.css'; + +class DataEntry extends Component { + constructor(props) { + super(props); + this.state = { + newName: '', + newOneTime: '', + newMonthly: '', + error: false + } + } + + // controlled form elements, watch for changes + handleNameChange(e) { + this.setState({ + newName: e.target.value + }) + } + + handleMonthlyChange(e) { + this.setState({ + newMonthly: Number(e.target.value) + }) + } + handleOneTimeChange(e) { + this.setState({ + newOneTime: Number(e.target.value) + }) + } + + handleAdd(e) { + e.preventDefault() + const { newName, newOneTime, newMonthly } = this.state; + // handle form errors, allows one-time and revenue amounts to be 0 + if (!newName || (!newOneTime && newOneTime !== 0) || (!newMonthly && newMonthly !== 0)) { + this.setState({ + error: true + }) + } + // if there are no form errors, add accordingly + else { + this.props.onAddData( newName, newOneTime, newMonthly ); + this.setState({ + error: false, + newName: '', + newMonthly: '', + newOneTime: '', + newType: '' + }) + } + } + + render() { + const { newName, newOneTime, newMonthly } = this.state; + + return ( +
+ {/* Add new expense or revenue form */} +
this.handleAdd(e)}> + + + this.handleNameChange(e)} + value={newName ? newName : ''} + /> + + + this.handleOneTimeChange(e)} + step="0.01" + min="0" + value={(newOneTime || newOneTime === 0) ? newOneTime : ''} + /> + + + this.handleMonthlyChange(e)} + step="0.01" + min="0" + value={(newMonthly || newMonthly === 0) ? newMonthly : ''} + /> + + + + + +
+ {/* form errors */} + { this.state.error && +

Please fill out all fields

+ } +
+ ); + } +} + +export default DataEntry; \ No newline at end of file diff --git a/src/components/DataTable.js b/src/components/DataTable.js new file mode 100644 index 0000000..d786769 --- /dev/null +++ b/src/components/DataTable.js @@ -0,0 +1,73 @@ +import React, { Component } from 'react'; +import { Button } from 'react-bootstrap' +import DataEntry from './DataEntry.js'; +import './Data.css'; + +class DataTable extends Component { + constructor(props) { + super(props); + this.state = {} + } + // Delete expense or revenue from list + handleDelete(index) { + const { data, onUpdateTableData } = this.props; + data.splice(index, 1); + onUpdateTableData(data); + } + + // add new expense or revenue + handleAdd(newName, newOneTime, newMonthly) { + const { data, onUpdateTableData } = this.props; + data.push({ + name: newName, + oneTime: newOneTime, + monthly: newMonthly + }) + onUpdateTableData(data); + } + + render() { + const { title, data } = this.props; + + // create table rows from revenue state list + let tableData = data.map((item, index) => { + return ( + + {item.name} + ${item.oneTime.toFixed(2)} + ${item.monthly.toFixed(2)} + + + ) + }) + + return ( +
+
+ {/* Revenue Table */} + + + + + + + + + + + + + + {tableData} + +
{title}
One-TimeMonthly
+ this.handleAdd(newName, newOneTime, newMonthly)} + /> +
+
+ ); + } +} + +export default DataTable; \ No newline at end of file diff --git a/src/data/seedData.js b/src/data/seedData.js new file mode 100644 index 0000000..c8f7f0f --- /dev/null +++ b/src/data/seedData.js @@ -0,0 +1,32 @@ +export default { + revenue: [ + { + name: 'Item 1', + oneTime: 100, + monthly: 50 + }, + { + name: 'Item 2', + oneTime: 50, + monthly: 25 + }, + { + name: 'Item 3', + oneTime: 25, + monthly: 85 + }], + expenses: [{ + name: 'Expense 1', + oneTime: 500, + monthly: 20.00 + }, + { + name: 'Expense 2', + oneTime: 200, + monthly: 40 + }], + oneTimeRevenue: 175, + oneTimeExpense: 700, + monthlyRevenue: 160, + monthlyExpense: 60 +}; \ No newline at end of file