Skip to content

Commit

Permalink
Add category spending report (#1382)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyrias authored Aug 7, 2023
1 parent 9fed15f commit e17d90c
Show file tree
Hide file tree
Showing 17 changed files with 844 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from 'loot-core/src/shared/transactions';

import { useActions } from '../../hooks/useActions';
import useCategories from '../../hooks/useCategories';
import { useSetThemeColor } from '../../hooks/useSetThemeColor';
import { colors } from '../../style';
import SyncRefresh from '../SyncRefresh';
Expand Down Expand Up @@ -81,7 +82,6 @@ export default function Account(props) {
let state = useSelector(state => ({
payees: state.queries.payees,
newTransactions: state.queries.newTransactions,
categories: state.queries.categories.list,
prefs: state.prefs.local,
dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
}));
Expand Down Expand Up @@ -134,9 +134,6 @@ export default function Account(props) {
}
});

if (state.categories.length === 0) {
await actionCreators.getCategories();
}
if (accounts.length === 0) {
await actionCreators.getAccounts();
}
Expand All @@ -152,6 +149,9 @@ export default function Account(props) {
return () => unlisten();
}, []);

// Load categories if necessary.
useCategories();

const updateSearchQuery = debounce(() => {
if (searchText === '' && currentQuery) {
updateQuery(currentQuery);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom';
import * as queries from 'loot-core/src/client/queries';

import { useActions } from '../../hooks/useActions';
import useCategories from '../../hooks/useCategories';
import { useSetThemeColor } from '../../hooks/useSetThemeColor';
import { colors, styles } from '../../style';
import Button from '../common/Button';
Expand Down Expand Up @@ -233,27 +234,21 @@ export default function Accounts() {
let accounts = useSelector(state => state.queries.accounts);
let newTransactions = useSelector(state => state.queries.newTransactions);
let updatedAccounts = useSelector(state => state.queries.updatedAccounts);
let categories = useSelector(state => state.queries.categories.list);
let numberFormat = useSelector(
state => state.prefs.local.numberFormat || 'comma-dot',
);
let hideFraction = useSelector(
state => state.prefs.local.hideFraction || false,
);

let { getCategories, getAccounts } = useActions();
const { list: categories } = useCategories();
let { getAccounts } = useActions();

const transactions = useState({});
const navigate = useNavigate();

useEffect(() => {
(async () => {
if (categories.length === 0) {
await getCategories();
}

getAccounts();
})();
(async () => getAccounts())();
}, []);

// const sync = async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,21 @@ import View from '../common/View';

import Autocomplete, { defaultFilterSuggestion } from './Autocomplete';

type CategoryGroup = {
export type Category = {
id: string;
cat_group: unknown;
groupName: string;
name: string;
categories: Array<{ id: string; name: string }>;
};

type CategoryListProps = {
items: Array<{
id: string;
cat_group: unknown;
groupName: string;
name: string;
}>;
export type CategoryGroup = {
id: string;
name: string;
categories: Array<Category>;
};

export type CategoryListProps = {
items: Array<Category>;
getItemProps?: (arg: { item }) => Partial<ComponentProps<typeof View>>;
highlightedIndex: number;
embedded: boolean;
Expand Down
171 changes: 171 additions & 0 deletions packages/desktop-client/src/components/reports/CategorySelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React, { useState } from 'react';

import Eye from '../../icons/v2/Eye';
import EyeSlashed from '../../icons/v2/EyeSlashed';
import {
type Category,
type CategoryGroup,
type CategoryListProps,
} from '../autocomplete/CategorySelect';
import Button from '../common/Button';
import { Checkbox } from '../forms';

type CategorySelectorProps = {
categoryGroups: Array<CategoryGroup>;
selectedCategories: CategoryListProps['items'];
setSelectedCategories: (selectedCategories: Category[]) => null;
};

export default function CategorySelector({
categoryGroups,
selectedCategories,
setSelectedCategories,
}: CategorySelectorProps) {
const [uncheckedHidden, setUncheckedHidden] = useState(false);

return (
<>
<div>
<Button
type="bare"
style={{ padding: 4 }}
onClick={e => setUncheckedHidden(!uncheckedHidden)}
>
{uncheckedHidden ? (
<>
<Eye width={20} height={20} />
{'Checked'}
</>
) : (
<>
<EyeSlashed width={20} height={20} />
{'All'}
</>
)}
</Button>
</div>
<ul
style={{
listStyle: 'none',
marginLeft: 0,
paddingLeft: 0,
paddingRight: 10,
}}
>
{categoryGroups &&
categoryGroups.map(categoryGroup => {
const allCategoriesInGroupSelected = categoryGroup.categories.every(
category =>
selectedCategories.some(
selectedCategory => selectedCategory.id === category.id,
),
);
const noCategorySelected = categoryGroup.categories.every(
category =>
!selectedCategories.some(
selectedCategory => selectedCategory.id === category.id,
),
);
return (
<>
<li
style={{
display:
noCategorySelected && uncheckedHidden ? 'none' : 'flex',
marginBottom: 4,
flexDirection: 'row',
}}
key={categoryGroup.id}
>
<Checkbox
id={`form_${categoryGroup.id}`}
checked={allCategoriesInGroupSelected}
onChange={e => {
const selectedCategoriesExcludingGroupCategories =
selectedCategories.filter(
selectedCategory =>
!categoryGroup.categories.some(
groupCategory =>
groupCategory.id === selectedCategory.id,
),
);
if (allCategoriesInGroupSelected) {
setSelectedCategories(
selectedCategoriesExcludingGroupCategories,
);
} else {
setSelectedCategories(
selectedCategoriesExcludingGroupCategories.concat(
categoryGroup.categories,
),
);
}
}}
/>
<label
htmlFor={`form_${categoryGroup.id}`}
style={{ userSelect: 'none', fontWeight: 'bold' }}
>
{categoryGroup.name}
</label>
</li>
<li>
<ul
style={{
listStyle: 'none',
marginLeft: 0,
marginBottom: 10,
paddingLeft: 10,
}}
>
{categoryGroup.categories.map((category, index) => {
const isChecked = selectedCategories.some(
selectedCategory => selectedCategory.id === category.id,
);
return (
<li
key={category.id}
style={{
display:
!isChecked && uncheckedHidden ? 'none' : 'flex',
flexDirection: 'row',
marginBottom: 2,
}}
>
<Checkbox
id={`form_${category.id}`}
checked={isChecked}
onChange={e => {
if (isChecked) {
setSelectedCategories(
selectedCategories.filter(
selectedCategory =>
selectedCategory.id !== category.id,
),
);
} else {
setSelectedCategories([
...selectedCategories,
category,
]);
}
}}
/>
<label
htmlFor={`form_${category.id}`}
style={{ userSelect: 'none' }}
>
{category.name}
</label>
</li>
);
})}
</ul>
</li>
</>
);
})}
</ul>
</>
);
}
Loading

0 comments on commit e17d90c

Please sign in to comment.