diff --git a/src/common/Header/index.jsx b/src/common/Header/index.jsx index 9023269..963a0fc 100644 --- a/src/common/Header/index.jsx +++ b/src/common/Header/index.jsx @@ -52,7 +52,6 @@ const Header = ({ children, match }) => { const { currentTarget } = event setAnchorEl(currentTarget) setOpen(!open) - console.log('currentTarget', currentTarget) } const handleClose = () => { @@ -98,10 +97,10 @@ const Header = ({ children, match }) => { - + - + diff --git a/src/common/LoginButton/index.jsx b/src/common/LoginButton/index.jsx index 4d7db48..03a5ac9 100644 --- a/src/common/LoginButton/index.jsx +++ b/src/common/LoginButton/index.jsx @@ -23,7 +23,8 @@ import { userLogout } from '../../store/user/actions' const styles = theme => ({ root: { - display: 'flex' + display: 'flex', + minWidth: 135 }, popper: { zIndex: theme.zIndex.drawer + 1 diff --git a/src/common/Logo/index.jsx b/src/common/Logo/index.jsx index 4bc1277..5e6bfb2 100644 --- a/src/common/Logo/index.jsx +++ b/src/common/Logo/index.jsx @@ -8,7 +8,8 @@ import LinkTo from '../LinkTo' const styles = { logo: { - display: 'flex' + display: 'flex', + minWidth: 135 }, image: { width: '30px', diff --git a/src/common/ModalDialog/index.jsx b/src/common/ModalDialog/index.jsx index 0db7605..eb69d26 100644 --- a/src/common/ModalDialog/index.jsx +++ b/src/common/ModalDialog/index.jsx @@ -71,7 +71,7 @@ const ModalDialog = ({ ModalDialog.propTypes = { open: PropTypes.bool.isRequired, title: PropTypes.string.isRequired, - children: PropTypes.array.isRequired, + children: PropTypes.node.isRequired, onSubmit: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, classes: PropTypes.object.isRequired, diff --git a/src/core/Accounts/ImportTransactions/CsvImportForm.jsx b/src/core/Accounts/ImportTransactions/CsvImportForm.jsx index 45330bf..5a26bf6 100644 --- a/src/core/Accounts/ImportTransactions/CsvImportForm.jsx +++ b/src/core/Accounts/ImportTransactions/CsvImportForm.jsx @@ -278,7 +278,6 @@ export class CsvImportFormComponent extends React.Component { variant="contained" color="primary" disabled={isSubmitting || file === null || error !== null} - onClick={this.handleButtonClick} > Import diff --git a/src/core/Accounts/ImportTransactions/index.jsx b/src/core/Accounts/ImportTransactions/index.jsx index 733d2de..bd48e34 100644 --- a/src/core/Accounts/ImportTransactions/index.jsx +++ b/src/core/Accounts/ImportTransactions/index.jsx @@ -65,7 +65,7 @@ export class ImportTransactionsComponent extends React.Component { .map(transaction => ({ amount: transaction.amount, description: transaction.description, - category: transaction.category, + categoryId: transaction.categoryId, createdAt: transaction.createdAt })) if (transactions.length > 0) { diff --git a/src/core/Accounts/Transactions/TransactionDialog.jsx b/src/core/Accounts/Transactions/TransactionDialog.jsx index be425b7..af994eb 100644 --- a/src/core/Accounts/Transactions/TransactionDialog.jsx +++ b/src/core/Accounts/Transactions/TransactionDialog.jsx @@ -57,13 +57,6 @@ const colourStyles = { } const mapStateToProps = ({ budget }) => ({ budget }) -// const mapStateToProps = ({ budget }) => ({ -// budget, -// budgetForSelect: budget.tree.map(cat => ({ -// label: budget.byId[cat.id].label, -// options: cat.children.map(subCat => budget.byId[subCat]) -// })) -// }) const mapDispatchToProps = (dispatch) => { return { @@ -112,12 +105,12 @@ export const TransactionDialogComponent = ({ ({ }) const mapStateToProps = (state, props) => ({ - budgetColours: state.budget.colours, + budget: state.budget, formatCurrency: currencyFormatter(state.settings.locale, props.account.currency), formatDecimal: decimalFormatter(state.settings.locale, props.account.type), formatDate: dateFormatter(state.settings.locale) @@ -127,9 +127,10 @@ export class TransactionsTableComponent extends React.Component { } filterByDescription = (transaction) => { - let res = transaction.description.toLowerCase().includes(this.state.filters.description) - if (transaction.category !== undefined) { - res = res || transaction.category.toLowerCase().includes(this.state.filters.description) + let res = transaction.description.toLowerCase().includes(this.state.filters.description.toLowerCase()) + if (transaction.categoryId !== undefined) { + const category = this.props.budget.categoriesById[transaction.categoryId].name + res = res || category.toLowerCase().includes(this.state.filters.description.toLowerCase()) } return res } @@ -198,7 +199,7 @@ export class TransactionsTableComponent extends React.Component { className, children, account, - budgetColours, + budget, formatDate, Toolbar, toolbarProps, @@ -285,17 +286,20 @@ export class TransactionsTableComponent extends React.Component { { - const colour = rowData.category in budgetColours ? budgetColours[rowData.category] : '#dddddd' - return rowData.category !== undefined && ( + if (rowData.categoryId === undefined) return null + const category = budget.categoriesById[rowData.categoryId] + if (category === undefined) return null + if (category.colour === undefined) return null + return ( 5 ? 'black' : 'white' + background: category.colour, + color: chroma.contrast(category.colour, 'black') > 5 ? 'black' : 'white' }} /> ) @@ -341,7 +345,7 @@ TransactionsTableComponent.propTypes = { toolbarProps: PropTypes.object, account: PropTypes.object.isRequired, transactions: PropTypes.array.isRequired, - budgetColours: PropTypes.object.isRequired, + budget: PropTypes.object.isRequired, formatCurrency: PropTypes.func.isRequired, formatDecimal: PropTypes.func.isRequired, formatDate: PropTypes.func.isRequired, diff --git a/src/core/Accounts/Transactions/TransactionsToolbar.jsx b/src/core/Accounts/Transactions/TransactionsToolbar.jsx index d035538..511b9e6 100644 --- a/src/core/Accounts/Transactions/TransactionsToolbar.jsx +++ b/src/core/Accounts/Transactions/TransactionsToolbar.jsx @@ -9,8 +9,8 @@ import Icon from '@mdi/react' import DeleteIcon from '@material-ui/icons/Delete' import { mdiImport } from '@mdi/js' import InputBase from '@material-ui/core/InputBase' +import InputAdornment from '@material-ui/core/InputAdornment' import SearchIcon from '@material-ui/icons/Search' -import CancelIcon from '@material-ui/icons/Cancel' import { fade } from '@material-ui/core/styles/colorManipulator' import confirm from '../../../util/confirm' import TableToolbar from '../../../common/TableToolbar' @@ -34,36 +34,19 @@ const styles = theme => ({ backgroundColor: fade(theme.palette.grey[400], 0.25) } }, - searchIcon: { - width: theme.spacing(9), - height: '100%', - position: 'absolute', - pointerEvents: 'none', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - color: '#bbb' - }, - clearIcon: { - position: 'absolute', - right: theme.spacing(2), - padding: 0, - marginTop: theme.spacing(2), - color: '#999' - }, inputRoot: { color: 'inherit', - width: '100%' + width: '100%', + padding: theme.spacing(2), + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1) + }, inputInput: { - paddingTop: theme.spacing(1) * 1.8, - paddingRight: theme.spacing(5), - paddingBottom: theme.spacing(1), - paddingLeft: theme.spacing(8), transition: theme.transitions.create('width'), width: '100%', [theme.breakpoints.up('md')]: { - minWidth: 230 + minWidth: 240 } } }) @@ -88,10 +71,6 @@ export class TransactionsToolbarComponent extends React.Component { } } - onClearClick = () => { - this.props.filterProps.unsetFilter({ attr: 'description' }) - } - render() { const { classes, @@ -116,29 +95,17 @@ export class TransactionsToolbarComponent extends React.Component { ) : (
-
- -
} /> - {('description' in this.props.filterProps.filters) && ( - - - - )}
{ className="" account={account} transactions={[]} - budgetColours={{}} + budget={{}} formatCurrency={mochFormatCurrency} formatDecimal={mochFormatDecimal} formatDate={mochFormatDate} @@ -55,7 +55,7 @@ describe('TransactionsTable', () => { className="" account={account} transactions={transactions} - budgetColours={{}} + budget={{}} formatCurrency={mochFormatCurrency} formatDecimal={mochFormatDecimal} formatDate={mochFormatDate} @@ -78,7 +78,7 @@ describe('TransactionsTable', () => { className="" account={account} transactions={transactions} - budgetColours={{}} + budget={{}} formatCurrency={mochFormatCurrency} formatDecimal={mochFormatDecimal} formatDate={mochFormatDate} diff --git a/src/core/Accounts/Transactions/__tests__/__snapshots__/TransactionsTable.test.jsx.snap b/src/core/Accounts/Transactions/__tests__/__snapshots__/TransactionsTable.test.jsx.snap index 5c23fad..9f893a9 100644 --- a/src/core/Accounts/Transactions/__tests__/__snapshots__/TransactionsTable.test.jsx.snap +++ b/src/core/Accounts/Transactions/__tests__/__snapshots__/TransactionsTable.test.jsx.snap @@ -5,10 +5,10 @@ exports[`TransactionsTable matches snapshot with no transactions 1`] = ` className=" " >
- -
-
+
+ +
@@ -198,7 +198,7 @@ exports[`TransactionsTable matches snapshot with no transactions 1`] = ` >
- -
-
+
+ +
@@ -589,7 +589,7 @@ exports[`TransactionsTable matches snapshot with some transactions 1`] = ` >
-
- -
+
+ +
diff --git a/src/core/Accounts/Transactions/__tests__/__snapshots__/index.test.jsx.snap b/src/core/Accounts/Transactions/__tests__/__snapshots__/index.test.jsx.snap index 2bfb402..a86d366 100644 --- a/src/core/Accounts/Transactions/__tests__/__snapshots__/index.test.jsx.snap +++ b/src/core/Accounts/Transactions/__tests__/__snapshots__/index.test.jsx.snap @@ -19,10 +19,10 @@ exports[`Transactions matches snapshot with no transactions 1`] = ` className="TransactionsTableComponent-tableWrapper-1 " >
- -
-
+
+ +
@@ -214,7 +214,7 @@ exports[`Transactions matches snapshot with no transactions 1`] = ` >
- -
-
+
+ +
@@ -638,7 +638,7 @@ exports[`Transactions matches snapshot with one transaction in a fiat account 1` >
- - - - You don't have any accounts yet - - - + + +
+ + +
+ You don't have any accounts yet. +
+
+
+ + + + + + + + + Add an account + + + + + + + + + + + + + + + + + + + +

+ You can keep track of all the accounts you have from any institution. +

+
+
+
+
+
@@ -91,9 +130,9 @@ exports[`AccountsIndex matches snapshot with one account 1`] = `
- - -
+ + +
@@ -113,9 +152,9 @@ exports[`AccountsIndex matches snapshot with one account 1`] = `
- - -
+ + +
@@ -161,13 +200,13 @@ exports[`AccountsIndex matches snapshot with one account 1`] = ` - - -
- - - - - - - + + + + + + + @@ -210,9 +249,9 @@ exports[`AccountsIndex matches snapshot with one account 1`] = `
+ + + @@ -327,9 +366,9 @@ exports[`AccountsIndex matches snapshot with two accounts in a different currenc
- - -
+ + +
@@ -349,9 +388,9 @@ exports[`AccountsIndex matches snapshot with two accounts in a different currenc
- - -
+ + +
@@ -397,13 +436,13 @@ exports[`AccountsIndex matches snapshot with two accounts in a different currenc - - - - - -
- - - - - - - + + + + + + + @@ -446,9 +485,9 @@ exports[`AccountsIndex matches snapshot with two accounts in a different currenc
+ + + @@ -471,9 +510,9 @@ exports[`AccountsIndex matches snapshot with two accounts in a different currenc
+ + + diff --git a/src/core/Accounts/form.jsx b/src/core/Accounts/form.jsx index 7999d74..89dee0c 100644 --- a/src/core/Accounts/form.jsx +++ b/src/core/Accounts/form.jsx @@ -187,14 +187,11 @@ export class AccountFormComponent extends React.Component { helperText={errors.name} /> ({ balancePaper: { @@ -42,11 +44,6 @@ const styles = theme => ({ balanceIcon: { color: 'white' }, - noAccounts: { - background: grey[100], - margin: '0 20px 20px 25px', - padding: theme.spacing(1) - }, accountsTable: { padding: theme.spacing(2) }, @@ -67,6 +64,14 @@ const styles = theme => ({ padding: theme.spacing(2), height: 240, minWidth: 150 + }, + noAccountsPaper: { + padding: theme.spacing(2), + marginTop: theme.spacing(2), + textAlign: 'center' + }, + newAccountButton: { + margin: theme.spacing(3) } }) @@ -98,9 +103,22 @@ export const AccountsIndexComponent = ({ return ( {!userHasAccounts && ( - - You don't have any accounts yet - + + + You don't have any accounts yet. + + + + You can keep track of all the accounts you have from any institution. + + )} {userHasAccounts && ( diff --git a/src/core/Budget/BudgetChart.jsx b/src/core/Budget/BudgetChart.jsx index 750f0ff..fca453b 100644 --- a/src/core/Budget/BudgetChart.jsx +++ b/src/core/Budget/BudgetChart.jsx @@ -17,19 +17,20 @@ const BudgetChart = () => { const { settings, transactions, - categories, - colours, + budget, formatCurrency } = useSelector(state => ({ settings: state.settings, transactions: state.transactions, - categories: [...new Set(Object.values(state.budget.rules).map(rules => rules.category))], - colours: state.budget.colours, + budget: state.budget, formatCurrency: currencyFormatter(state.settings.locale, state.settings.currency) })) - const initialState = categories.reduce( + const usedCategories = [...new Set(Object.values(budget.rules).map(rules => ( + budget.categoriesById[rules.categoryId] + )))] + const initialState = usedCategories.reduce( (res, cat) => ({ ...res, [cat]: 1 }), {} ) @@ -38,14 +39,15 @@ const BudgetChart = () => { const byMonth = {} transactions.list.forEach((transaction) => { const dateKey = startOfMonth(transaction.createdAt).getTime() - if (transaction.category !== undefined) { + if (transaction.categoryId !== undefined) { + const categoryName = budget.categoriesById[transaction.categoryId].name if (byMonth[dateKey] === undefined) { - byMonth[dateKey] = categories.reduce((res, cat) => ({ ...res, [cat]: 0 }), {}) + byMonth[dateKey] = usedCategories.reduce((res, cat) => ({ ...res, [cat.name]: 0 }), {}) } - if (byMonth[dateKey][transaction.category] === undefined) { - byMonth[dateKey][transaction.category] = 0 + if (byMonth[dateKey][categoryName] === undefined) { + byMonth[dateKey][categoryName] = 0 } - byMonth[dateKey][transaction.category] += Math.abs(transaction.amount) + byMonth[dateKey][categoryName] += Math.abs(transaction.amount) } }) const data = Object.keys(byMonth).sort((a, b) => a - b).map(date => ({ @@ -61,17 +63,17 @@ const BudgetChart = () => { const handleMouseEnter = (obj) => { setOpacity( - categories.reduce((res, value) => ({ + usedCategories.reduce((res, category) => ({ ...res, - [value]: value === obj.dataKey ? 1 : 0.3 + [category.name]: category.name === obj.dataKey ? 1 : 0.3 }), {}) ) } const handleMouseLeave = () => { setOpacity( - categories.reduce((res, value) => ({ + usedCategories.reduce((res, category) => ({ ...res, - [value]: 1 + [category.name]: 1 }), {}) ) } @@ -79,13 +81,13 @@ const BudgetChart = () => { return ( - { categories.map(category => ( + { usedCategories.map(category => ( ))} diff --git a/src/core/BudgetCategories/GroupDialog.jsx b/src/core/BudgetCategories/GroupDialog.jsx new file mode 100644 index 0000000..7a086e7 --- /dev/null +++ b/src/core/BudgetCategories/GroupDialog.jsx @@ -0,0 +1,107 @@ +import React from 'react' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withStyles } from '@material-ui/core/styles' +import { withFormik } from 'formik' +import * as Yup from 'yup' +import PropTypes from 'prop-types' +import TextField from '@material-ui/core/TextField' +import ModalDialog from '../../common/ModalDialog' +import { createCategory, updateCategory } from '../../store/budget/actions' + +const styles = theme => ({ + input: { + marginBottom: theme.spacing(1), + marginTop: theme.spacing(1), + width: '100%' + } +}) + +const mapDispatchToProps = (dispatch) => { + return { + handleSave: (category) => { + if ('id' in category) { + return dispatch(updateCategory(category)) + } + return dispatch(createCategory(category)) + } + } +} + +export const GroupDialogComponent = ({ + classes, + handleSubmit, + values, + errors, + touched, + handleChange, + onCancel, + open, + category +}) => ( + + + +) + +GroupDialogComponent.propTypes = { + classes: PropTypes.object.isRequired, + handleSubmit: PropTypes.func.isRequired, + values: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + touched: PropTypes.object.isRequired, + handleChange: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + onCancel: PropTypes.func.isRequired, + category: PropTypes.object +} + +GroupDialogComponent.defaultProps = { + category: null +} + +export default compose( + connect(null, mapDispatchToProps), + withStyles(styles), + withFormik({ + enableReinitialize: true, + mapPropsToValues: ({ category }) => { + if (category === null) { + return { + name: '' + } + } + return category + }, + validationSchema: Yup.object().shape({ + description: Yup.string() + .max(25, 'Too Long!') + }), + handleSubmit: (values, { props, setSubmitting, resetForm }) => { + setSubmitting(true) + props.handleSave(values) + resetForm() + setSubmitting(false) + props.onCancel() + } + }) +)(GroupDialogComponent) diff --git a/src/core/BudgetCategories/__tests__/__snapshots__/index.test.jsx.snap b/src/core/BudgetCategories/__tests__/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..581a850 --- /dev/null +++ b/src/core/BudgetCategories/__tests__/__snapshots__/index.test.jsx.snap @@ -0,0 +1,7365 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BudgetCAtegoriesIndex matches snapshot with no accounts 1`] = ` +" + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + +

+ Manage categories +

+
+
+
+ + +
+ + +
+ + + + + + + + + +
+
+
+ +
+
+
+ + + +
+ +
+ +
+
+ +
+ + +
+
+ + + +
+ + + + + + + +
+ + + + + + + + + +
+
+
+
+
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + +
+ + +
+ Bills & Utilities +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Home Phone + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Internet + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Mobile Phone + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Education +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Books & Supplies + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Student Loan + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Tuition + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Food & Dining +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Groceries + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Restaurants + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Gifts & Donations +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Charity + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Gift + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Health & Fitness +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Dentist + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Doctor + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Eyecare + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Pharmacy + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Gym + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Sports + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Home +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Furniture + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Home Improvement + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Home Services + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Home Supplies + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Lawn & Garden + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Mortgage & Rent + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Income +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Bonus + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Interest Income + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Paycheque + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Reimbursement + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Rental Income + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Returned Purchase + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Insurance +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Car insurance + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Life insurance + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Health Insurance + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Home insurance + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Investments +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Buy + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Deposit + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Dividend & Cap Gains + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Sell + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Withdrawal + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Kids +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Kids Allowance + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Baby Supplies + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Babysitter & Daycare + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Child Support + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Kids Activities + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Kids Toys + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Pets +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Pet Food & Supplies + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Pet Grooming + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Veterinary + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Shopping +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Books + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Clothing + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Electronics & Software + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Hobbies + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Sporting Goods + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Subscriptions +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Netflix + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Spotify + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Taxes +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Federal Tax + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Local Tax + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Property Tax + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Sales Tax + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Provincial Tax + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Transport +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Auto Payment + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Gas & Fuel + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Parking + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Public Transportation + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Service & Parts + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Travel +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Flights + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Hotel + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Rental Car & Taxi + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Vacation + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + +
+ + +
+ + +
+ Uncategorized +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ + +
+
+
+
+
+ + + + Cash & ATM + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+
+
+
+
+ + + + Cheque + + + + + + + Budget limit: Not set + + + +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+ + + + +
+ + + + + + + + + +
+
+
+
+
+
+ + + + + + + + + +
+ + + + + +" +`; diff --git a/src/core/BudgetCategories/__tests__/index.test.jsx b/src/core/BudgetCategories/__tests__/index.test.jsx new file mode 100644 index 0000000..3b2de4b --- /dev/null +++ b/src/core/BudgetCategories/__tests__/index.test.jsx @@ -0,0 +1,29 @@ +import React from 'react' +import { mount } from 'enzyme' +import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' +import configureMockStore from 'redux-mock-store' +import BudgetCategories from '..' +import { initialState as settingsInitialState } from '../../../store/settings/reducer' +import { initialState as budgetInitialState } from '../../../store/budget/reducer' + +describe('BudgetCAtegoriesIndex', () => { + it('matches snapshot with no accounts', () => { + const mockStore = configureMockStore() + const store = mockStore({ + settings: settingsInitialState, + budget: budgetInitialState + }) + const wrapper = mount(( + + + + + + )) + expect(wrapper.debug()).toMatchSnapshot() + + const groupNames = budgetInitialState.categoryTree.map(cat => cat.label) + expect(wrapper.find('h6').map(node => node.text())).toEqual(groupNames) + }) +}) diff --git a/src/core/BudgetCategories/form.jsx b/src/core/BudgetCategories/form.jsx new file mode 100644 index 0000000..20d9b13 --- /dev/null +++ b/src/core/BudgetCategories/form.jsx @@ -0,0 +1,135 @@ +import React from 'react' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withStyles } from '@material-ui/core/styles' +import { withFormik } from 'formik' +import * as Yup from 'yup' +import PropTypes from 'prop-types' +import TextField from '@material-ui/core/TextField' +import Button from '@material-ui/core/Button' +import SubmitButtonWithProgress from '../../common/SubmitButtonWithProgress' +import { createCategory, updateCategory } from '../../store/budget/actions' + +const styles = theme => ({ + form: { + display: 'flex', + flexDirection: 'column', + padding: theme.spacing(2), + paddingBottom: 0 + }, + input: { + // margin: theme.spacing(2) + }, + formActions: { + display: 'flex', + justifyContent: 'flex-end', + paddingTop: 10 + }, + buttonWrapper: { + margin: theme.spacing(1), + position: 'relative' + } +}) + +const mapStateToProps = state => ({ + budget: state.budget +}) + +const mapDispatchToProps = { + handleCreate: (category, groupId) => createCategory(category, groupId), + handleUpdate: category => updateCategory(category) +} + +const CategoryFormComponent = ({ + classes, + handleSubmit, + isSubmitting, + values, + errors, + touched, + handleChange, + handleCancel +}) => { + return ( +
+ + +
+ +
+ +
+
+ + ) +} + +CategoryFormComponent.propTypes = { + classes: PropTypes.object.isRequired, + handleSubmit: PropTypes.func.isRequired, + isSubmitting: PropTypes.bool.isRequired, + values: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + touched: PropTypes.object.isRequired, + handleChange: PropTypes.func.isRequired, + handleCancel: PropTypes.func.isRequired +} + +export default compose( + connect(mapStateToProps, mapDispatchToProps), + withStyles(styles), + withFormik({ + enableReinitialize: true, + mapPropsToValues: ({ category }) => { + if (category === undefined) { + return { + name: '', + budgetLimit: 0 + } + } + return category + }, + validationSchema: Yup.object().shape({ + name: Yup.string() + .max(100, 'Too Long!') + .required('Please enter a name') + }), + handleSubmit: async (values, { props, setSubmitting }) => { + setSubmitting(true) + if ('id' in values) { + props.handleUpdate(values) + } else { + props.handleCreate(values, props.groupId) + } + props.handleCancel() + setSubmitting(false) + } + }) +)(CategoryFormComponent) diff --git a/src/core/BudgetCategories/index.jsx b/src/core/BudgetCategories/index.jsx index 9380cf2..1b06844 100644 --- a/src/core/BudgetCategories/index.jsx +++ b/src/core/BudgetCategories/index.jsx @@ -1,96 +1,361 @@ +/* eslint-disable react/no-multi-comp */ import React from 'react' -import { useSelector } from 'react-redux' +import { useSelector, useDispatch } from 'react-redux' import { makeStyles } from '@material-ui/core/styles' import Grid from '@material-ui/core/Grid' -import Paper from '@material-ui/core/Paper' -import List from '@material-ui/core/List' -import ListItem from '@material-ui/core/ListItem' -import ListItemIcon from '@material-ui/core/ListItemIcon' -import ListItemText from '@material-ui/core/ListItemText' -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' -import Collapse from '@material-ui/core/Collapse' +import Container from '@material-ui/core/Container' +import InputBase from '@material-ui/core/InputBase' +import InputAdornment from '@material-ui/core/InputAdornment' import Typography from '@material-ui/core/Typography' -import ExpandLess from '@material-ui/icons/ExpandLess' -import ExpandMore from '@material-ui/icons/ExpandMore' -import FolderIcon from '@material-ui/icons/Folder' +import SearchIcon from '@material-ui/icons/Search' +import AddIcon from '@material-ui/icons/Add' import EditIcon from '@material-ui/icons/Edit' +import DeleteIcon from '@material-ui/icons/Delete' +import MoreVertIcon from '@material-ui/icons/MoreVert' import IconButton from '@material-ui/core/IconButton' +import Button from '@material-ui/core/Button' +import Menu from '@material-ui/core/Menu' +import MenuItem from '@material-ui/core/MenuItem' +import MenuList from '@material-ui/core/MenuList' +import ListItemIcon from '@material-ui/core/ListItemIcon' +import ListItemText from '@material-ui/core/ListItemText' +import Card from '@material-ui/core/Card' +import CardHeader from '@material-ui/core/CardHeader' +import Select from 'react-select' +import { fade } from '@material-ui/core/styles/colorManipulator' +import Tooltip from '@material-ui/core/Tooltip' +import pluralize from 'pluralize' +import CategoryForm from './form' +import { currencyFormatter } from '../../util/stringFormatter' +import { deleteCategory } from '../../store/budget/actions' +import GroupDialog from './GroupDialog' +import confirm from '../../util/confirm' const useStyles = makeStyles(theme => ({ root: { - padding: theme.spacing(3), - flexDirection: 'column' + marginTop: theme.spacing(2), + marginBottom: theme.spacing(6) + }, + categoryGroup: { + marginTop: theme.spacing(2) }, - nested: { - paddingLeft: theme.spacing(3) + groupName: { + display: 'inline-block', + marginRight: theme.spacing(1) }, - dot: { - borderRadius: 4, + circle: { + borderRadius: 20, content: '" "', display: 'block', - marginRight: -10, + marginRight: 10, marginLeft: 10, - height: 15, - width: 15 + height: 30, + width: 30 + }, + newCategoryButton: { + marginTop: theme.spacing(2), + textAlign: 'center' + }, + inputRoot: { + verticalAlign: 'bottom', + padding: theme.spacing(2), + paddingTop: theme.spacing(1) * 0.5, + paddingBottom: theme.spacing(1) * 0.5, + borderRadius: theme.shape.borderRadius * 2, + backgroundColor: fade(theme.palette.grey[400], 0.15), + marginRight: theme.spacing(2), + marginLeft: 0, + '&:hover': { + backgroundColor: fade(theme.palette.grey[400], 0.25) + } + }, + inputInput: { + width: 160, + color: 'inherit', + transition: theme.transitions.create('width') + }, + filterParentCategory: { + width: 230, + marginLeft: theme.spacing(1), + marginRight: theme.spacing(2), + display: 'inline-block' } })) + const BudgetCategories = () => { const classes = useStyles() - const budget = useSelector(state => state.budget) - const [open, setOpen] = React.useState(budget.categories.reduce( - (result, category) => ({ ...result, [category.label]: true }), - {} - )) - - function handleClick(category) { - setOpen({ ...open, [category]: !open[category] }) + const { budget, formatCurrency } = useSelector(state => ({ + budget: state.budget, + formatCurrency: currencyFormatter(state.settings.locale, state.settings.currency) + })) + const dispatch = useDispatch() + + const [filter, setfilter] = React.useState({ + category: '', + parentCategoryId: '' + }) + const [popupCategoryId, setPopupCategoryId] = React.useState(null) + const [anchorEl, setAnchorEl] = React.useState(null) + const [editCategory, setEditCategory] = React.useState(null) + const [openGroupDialog, setOpenGroupDialog] = React.useState(false) + const [selectedGroup, setSelectedGroup] = React.useState(null) + const popupIsOpen = Boolean(anchorEl) + + // --- Filters ---- + function handleFilterChange(event) { + if ('persist' in event) event.persist() + setfilter(oldValues => ({ + ...oldValues, + [event.target.name]: event.target.value + })) + } + + const filteredCategories = () => { + const filteredTree = budget.categoryTree.reduce((res, cat) => { + if (filter.parentCategoryId === '' || cat.id === filter.parentCategoryId) { + return [...res, { ...cat, options: cat.options }] + } + return res + }, []) + if (filter.category !== '') { + filteredTree.forEach((parentCategory, index) => { + filteredTree[index].options = parentCategory.options.filter(category => ( + category.label.toLowerCase().includes(filter.category.toLowerCase()) + )) + }) + } + return filteredTree + } + + // --- Popuop --- + const handleClosePopup = () => { + setAnchorEl(null) + } + + const handleOpenPopup = (event, categoryId) => { + setAnchorEl(popupCategoryId === categoryId && popupIsOpen ? null : event.currentTarget) + setPopupCategoryId(categoryId) + setEditCategory(null) + } + + // --- Category form --- + const showNewCategoryForm = (groupId) => { + setEditCategory(groupId) + setAnchorEl(null) + } + + const showEditCategoryForm = () => { + setEditCategory(popupCategoryId) + setAnchorEl(null) + } + + const handleCloseForm = () => { + setEditCategory(null) + } + + const handleDeleteCategory = () => { + const category = budget.categoriesById[popupCategoryId] + let confirmText = 'Are you sure?' + const transactionsCount = Object.values(budget.rules).reduce((count, rule) => ( + rule.categoryId === category.id ? count + rule.count : count + ), 0) + if (transactionsCount > 0) { + confirmText += ` There will be ${pluralize('transaction', transactionsCount, true)} affected.` + } + confirm(`Delete ${category.name}.`, confirmText).then(async () => { + dispatch(deleteCategory(popupCategoryId)) + }) + handleClosePopup() + } + + // --- Group dialog --- + const handleNewGroup = () => { + setSelectedGroup(null) + setOpenGroupDialog(true) + } + + const handleEditGroup = (category) => { + setSelectedGroup(budget.categoriesById[category.id]) + setOpenGroupDialog(true) + } + + const handleDeleteGroup = (category) => { + let title = `Delete ${category.label} group` + const childCategoryIds = category.options.map(item => item.id) + if (category.options.length > 0) { + title += ` and ${pluralize('categories', category.options.length, true)}.` + } + let confirmText = 'Are you sure?' + const transactionsCount = Object.values(budget.rules).reduce((count, rule) => ( + rule.categoryId === category.id || childCategoryIds.includes(rule.categoryId) ? count + rule.count : count + ), 0) + if (transactionsCount > 0) { + confirmText += ` There will be ${pluralize('transaction', transactionsCount, true)} affected.` + } + confirm(title, confirmText).then(async () => { + dispatch(deleteCategory(category.id)) + }) + } + + const handleCloseGroupDialog = () => { + setSelectedGroup(null) + setOpenGroupDialog(false) + } + + const renderCategory = (categoryId) => { + const category = budget.categoriesById[categoryId] + if (editCategory !== null && categoryId === editCategory) { + return ( + + ) + } + return ( + } + action={( + handleOpenPopup(event, category.id)}> + + + )} + title={category.name} + subheader={`Budget limit: ${category.budgetLimit ? formatCurrency(category.budgetLimit) : 'Not set'}`} + subheaderTypographyProps={{ + variant: 'caption' + }} + /> + ) + } + + const renderNewCategory = (groupId) => { + if (editCategory !== null && groupId === editCategory) { + return ( + + + + ) + } + return ( + + ) } return ( - - - Budget categories - - - {budget.categories.map(topCategory => ( -
- handleClick(topCategory.label)}> - - - {open[topCategory.label] ? : } - - - {topCategory.options.map(category => ( - - - -
- - - - - - - - - - ))} - -
- ))} -
- + + + + Manage categories +
+ } + /> +