diff --git a/packages/desktop-client/src/components/reports/CategorySpending.js b/packages/desktop-client/src/components/reports/CategorySpending.js new file mode 100644 index 00000000000..5023b777b74 --- /dev/null +++ b/packages/desktop-client/src/components/reports/CategorySpending.js @@ -0,0 +1,184 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { connect } from 'react-redux'; + +import * as d from 'date-fns'; +import { bindActionCreators } from 'redux'; + +import * as actions from 'loot-core/src/client/actions'; +import { send } from 'loot-core/src/platform/client/fetch'; +import * as monthUtils from 'loot-core/src/shared/months'; +import { integerToCurrency } from 'loot-core/src/shared/util'; + +import { colors, styles } from '../../style'; +import { AlignedText, Block, Text, View } from '../common'; + +import categorySpendingSpreadsheet from './graphs/category-spending-spreadsheet'; +import CategorySpendingGraph from './graphs/CategorySpendingGraph'; +import Header from './Header'; +import useReport from './useReport'; +import { fromDateRepr } from './util'; + +function CategoryAverage({ getCategories }) { + const [categoryGroups, setCategoryGroups] = useState([]); + + const [allMonths, setAllMonths] = useState(null); + + const [start, setStart] = useState( + monthUtils.subMonths(monthUtils.currentMonth(), 5), + ); + const [end, setEnd] = useState(monthUtils.currentMonth()); + + const [numberOfMonthsAverage, setNumberOfMonthsAverage] = useState(3); + + const [selectedCategoryId, setSelectedCategoryId] = useState(null); + + const getData = useMemo( + () => + categorySpendingSpreadsheet( + start, + end, + numberOfMonthsAverage, + selectedCategoryId, + ), + [start, end, numberOfMonthsAverage, selectedCategoryId], + ); + const graphData = useReport('category_spending', getData); + + useEffect(() => { + getCategories().then(categories => setCategoryGroups(categories.grouped)); + }, []); + + useEffect(() => { + async function run() { + const trans = await send('get-earliest-transaction'); + const currentMonth = monthUtils.currentMonth(); + let earliestMonth = trans + ? monthUtils.monthFromDate(d.parseISO(fromDateRepr(trans.date))) + : currentMonth; + + // Make sure the month selects are at least populates with a + // year's worth of months. We can undo this when we have fancier + // date selects. + const yearAgo = monthUtils.subMonths(monthUtils.currentMonth(), 12); + if (earliestMonth > yearAgo) { + earliestMonth = yearAgo; + } + + const allMonths = monthUtils + .rangeInclusive(earliestMonth, monthUtils.currentMonth()) + .map(month => ({ + name: month, + pretty: monthUtils.format(month, 'MMMM, yyyy'), + })) + .reverse(); + + setAllMonths(allMonths); + } + run(); + }, []); + + function onChangeDates(start, end) { + setStart(start); + setEnd(end); + } + + if (!allMonths || !graphData) { + return null; + } + + return ( + +
+ + + + Budgeted:} + right={ + + {integerToCurrency( + graphData.length === 0 + ? 0 + : graphData.reduce( + (total, month) => total + month.budgeted, + 0, + ), + )} + + } + /> + Spent:} + right={ + + {integerToCurrency( + graphData.length === 0 + ? 0 + : graphData.reduce( + (total, month) => total + month.total, + 0, + ), + )} + + } + /> + Average:} + right={ + + {integerToCurrency( + Math.round( + graphData.length === 0 + ? 0 + : graphData.reduce( + (total, month) => total + month.total, + 0, + ) / graphData.length, + ), + )} + + } + /> + + + + + + ); +} + +export default connect( + // Deleting this leads to "Uncaught TypeError: dispatch is not a function" + state => ({}), + dispatch => bindActionCreators(actions, dispatch), +)(CategoryAverage); diff --git a/packages/desktop-client/src/components/reports/Header.js b/packages/desktop-client/src/components/reports/Header.js index 70dc7d054ca..afd99c0f595 100644 --- a/packages/desktop-client/src/components/reports/Header.js +++ b/packages/desktop-client/src/components/reports/Header.js @@ -2,6 +2,7 @@ import * as monthUtils from 'loot-core/src/shared/months'; import ArrowLeft from '../../icons/v1/ArrowLeft'; import { styles } from '../../style'; +import CategoryAutocomplete from '../autocomplete/CategorySelect'; import { View, Button, ButtonLink, Select } from '../common'; import { FilterButton, AppliedFilters } from '../filters/FiltersMenu'; @@ -57,6 +58,12 @@ function Header({ onUpdateFilter, onDeleteFilter, onCondOpChange, + category, + categoryGroups, + onChangeCategory, + numberOfMonths, + numberOfMonthsOptions, + onChangeNumberOfMonths, }) { return ( + {categoryGroups && ( + + onChangeCategory(newValue)} + /> + + )} + + {numberOfMonthsOptions && ( + + Average +