Skip to content

Commit

Permalink
Undoable auto transfer notes + auto notes for cover (#3411)
Browse files Browse the repository at this point in the history
* Undo auto transfer notes + auto notes for cover

* Release notes

* Fix notes

* Fix notes undo

* Do not show clicked category on transfer or cover menus

* Fix typecheck error

* typecheck

* Fix removeCategoriesFromGroups
  • Loading branch information
joel-jeremy authored Sep 19, 2024
1 parent e507b8f commit e6bf6da
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 200 deletions.
3 changes: 2 additions & 1 deletion packages/desktop-client/src/components/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ export function Modals() {
<TransferModal
key={name}
title={options.title}
categoryId={options.categoryId}
month={options.month}
amount={options.amount}
onSubmit={options.onSubmit}
Expand All @@ -509,9 +510,9 @@ export function Modals() {
<CoverModal
key={name}
title={options.title}
categoryId={options.categoryId}
month={options.month}
showToBeBudgeted={options.showToBeBudgeted}
category={options.category}
onSubmit={options.onSubmit}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useState } from 'react';

import { runQuery } from 'loot-core/client/query-helpers';
import { send } from 'loot-core/platform/client/fetch';
import { q } from 'loot-core/shared/query';
import { rolloverBudget } from 'loot-core/src/client/queries';
import * as monthUtils from 'loot-core/src/shared/months';
import { groupById, integerToCurrency } from 'loot-core/src/shared/util';
import { type CategoryEntity } from 'loot-core/types/models';
import { type WithRequired } from 'loot-core/types/util';

import { useCategories } from '../../../hooks/useCategories';

import { BalanceMenu } from './BalanceMenu';
import { CoverMenu } from './CoverMenu';
Expand All @@ -34,8 +25,6 @@ export function BalanceMovementMenu({
);
const [menu, setMenu] = useState('menu');

const { addBudgetTransferNotes } = useBudgetTransferNotes({ month });

return (
<>
{menu === 'menu' && (
Expand All @@ -55,6 +44,7 @@ export function BalanceMovementMenu({

{menu === 'transfer' && (
<TransferMenu
categoryId={categoryId}
initialAmount={catBalance}
showToBeBudgeted={true}
onClose={onClose}
Expand All @@ -64,18 +54,13 @@ export function BalanceMovementMenu({
from: categoryId,
to: toCategoryId,
});
addBudgetTransferNotes({
fromCategoryId: categoryId,
toCategoryId,
amount,
});
}}
/>
)}

{menu === 'cover' && (
<CoverMenu
category={categoryId}
categoryId={categoryId}
onClose={onClose}
onSubmit={fromCategoryId => {
onBudgetAction(month, 'cover-overspending', {
Expand All @@ -88,50 +73,3 @@ export function BalanceMovementMenu({
</>
);
}

const useBudgetTransferNotes = ({ month }: { month: string }) => {
const { list: categories } = useCategories();
const categoriesById = useMemo(() => {
return groupById(categories as WithRequired<CategoryEntity, 'id'>[]);
}, [categories]);

const getNotes = async (id: string) => {
const { data: notes } = await runQuery(
q('notes').filter({ id }).select('note'),
);
return (notes && notes[0]?.note) ?? '';
};

const addNewLine = (notes?: string) => `${notes}${notes && '\n'}`;

const addBudgetTransferNotes = useCallback(
async ({
fromCategoryId,
toCategoryId,
amount,
}: {
fromCategoryId: Required<CategoryEntity['id']>;
toCategoryId: Required<CategoryEntity['id']>;
amount: number;
}) => {
const displayAmount = integerToCurrency(amount);

const monthBudgetNotesId = `budget-${month}`;
const existingMonthBudgetNotes = addNewLine(
await getNotes(monthBudgetNotesId),
);

const displayDay = monthUtils.format(monthUtils.currentDate(), 'MMMM dd');
const fromCategoryName = categoriesById[fromCategoryId || ''].name;
const toCategoryName = categoriesById[toCategoryId || ''].name;

await send('notes-save', {
id: monthBudgetNotesId,
note: `${existingMonthBudgetNotes}- Reassigned ${displayAmount} from ${fromCategoryName} to ${toCategoryName} on ${displayDay}`,
});
},
[categoriesById, month],
);

return { addBudgetTransferNotes };
};
Original file line number Diff line number Diff line change
@@ -1,61 +1,44 @@
import React, { useMemo, useState } from 'react';

import {
type CategoryGroupEntity,
type CategoryEntity,
} from 'loot-core/src/types/models';
import { type CategoryEntity } from 'loot-core/src/types/models';

import { useCategories } from '../../../hooks/useCategories';
import { CategoryAutocomplete } from '../../autocomplete/CategoryAutocomplete';
import { Button } from '../../common/Button2';
import { InitialFocus } from '../../common/InitialFocus';
import { View } from '../../common/View';
import { addToBeBudgetedGroup } from '../util';

function removeSelectedCategory(
categoryGroups: CategoryGroupEntity[],
category?: CategoryEntity['id'],
) {
if (!category) return categoryGroups;

return categoryGroups
.map(group => ({
...group,
categories: group.categories?.filter(cat => cat.id !== category),
}))
.filter(group => group.categories?.length);
}
import { addToBeBudgetedGroup, removeCategoriesFromGroups } from '../util';

type CoverMenuProps = {
showToBeBudgeted?: boolean;
category?: CategoryEntity['id'];
onSubmit: (categoryId: string) => void;
categoryId?: CategoryEntity['id'];
onSubmit: (categoryId: CategoryEntity['id']) => void;
onClose: () => void;
};

export function CoverMenu({
showToBeBudgeted = true,
category,
categoryId,
onSubmit,
onClose,
}: CoverMenuProps) {
const { grouped: originalCategoryGroups } = useCategories();
const expenseGroups = originalCategoryGroups.filter(g => !g.is_income);

const categoryGroups = showToBeBudgeted
? addToBeBudgetedGroup(expenseGroups)
: expenseGroups;
const [fromCategoryId, setFromCategoryId] = useState<string | null>(null);

const [categoryId, setCategoryId] = useState<string | null>(null);

const filteredCategoryGroups = useMemo(
() => removeSelectedCategory(categoryGroups, category),
[categoryGroups, category],
);
const filteredCategoryGroups = useMemo(() => {
const expenseGroups = originalCategoryGroups.filter(g => !g.is_income);
const categoryGroups = showToBeBudgeted
? addToBeBudgetedGroup(expenseGroups)
: expenseGroups;
return categoryId
? removeCategoriesFromGroups(categoryGroups, categoryId)
: categoryGroups;
}, [categoryId, showToBeBudgeted, originalCategoryGroups]);

function submit() {
if (categoryId) {
onSubmit(categoryId);
if (fromCategoryId) {
onSubmit(fromCategoryId);
}
onClose();
}
Expand All @@ -67,9 +50,9 @@ export function CoverMenu({
{node => (
<CategoryAutocomplete
categoryGroups={filteredCategoryGroups}
value={categoryGroups.find(g => g.id === categoryId) ?? null}
value={null}
openOnFocus={true}
onSelect={(id: string | undefined) => setCategoryId(id || null)}
onSelect={(id: string | undefined) => setFromCategoryId(id || null)}
inputProps={{
inputRef: node,
onEnter: event => !event.defaultPrevented && submit(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,48 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';

import { evalArithmetic } from 'loot-core/src/shared/arithmetic';
import { integerToCurrency, amountToInteger } from 'loot-core/src/shared/util';
import { type CategoryEntity } from 'loot-core/types/models';

import { useCategories } from '../../../hooks/useCategories';
import { CategoryAutocomplete } from '../../autocomplete/CategoryAutocomplete';
import { Button } from '../../common/Button2';
import { InitialFocus } from '../../common/InitialFocus';
import { Input } from '../../common/Input';
import { View } from '../../common/View';
import { addToBeBudgetedGroup } from '../util';
import { addToBeBudgetedGroup, removeCategoriesFromGroups } from '../util';

type TransferMenuProps = {
categoryId?: CategoryEntity['id'];
initialAmount?: number;
showToBeBudgeted?: boolean;
onSubmit: (amount: number, categoryId: string) => void;
onSubmit: (amount: number, categoryId: CategoryEntity['id']) => void;
onClose: () => void;
};

export function TransferMenu({
categoryId,
initialAmount = 0,
showToBeBudgeted,
onSubmit,
onClose,
}: TransferMenuProps) {
const { grouped: originalCategoryGroups } = useCategories();
const filteredCategoryGroups = originalCategoryGroups.filter(
g => !g.is_income,
);
const categoryGroups = showToBeBudgeted
? addToBeBudgetedGroup(filteredCategoryGroups)
: filteredCategoryGroups;
const filteredCategoryGroups = useMemo(() => {
const expenseCategoryGroups = originalCategoryGroups.filter(
g => !g.is_income,
);
const categoryGroups = showToBeBudgeted
? addToBeBudgetedGroup(expenseCategoryGroups)
: expenseCategoryGroups;
return categoryId
? removeCategoriesFromGroups(categoryGroups, categoryId)
: categoryGroups;
}, [originalCategoryGroups, categoryId, showToBeBudgeted]);

const _initialAmount = integerToCurrency(Math.max(initialAmount, 0));
const [amount, setAmount] = useState<string | null>(null);
const [categoryId, setCategoryId] = useState<string | null>(null);
const [toCategoryId, setToCategoryId] = useState<string | null>(null);

const _onSubmit = (newAmount: string | null, categoryId: string | null) => {
const parsedAmount = evalArithmetic(newAmount || '');
Expand All @@ -53,20 +61,20 @@ export function TransferMenu({
<Input
defaultValue={_initialAmount}
onUpdate={value => setAmount(value)}
onEnter={() => _onSubmit(amount, categoryId)}
onEnter={() => _onSubmit(amount, toCategoryId)}
/>
</InitialFocus>
</View>
<View style={{ margin: '10px 0 5px 0' }}>To:</View>

<CategoryAutocomplete
categoryGroups={categoryGroups}
value={categoryGroups.find(g => g.id === categoryId) ?? null}
categoryGroups={filteredCategoryGroups}
value={null}
openOnFocus={true}
onSelect={(id: string | undefined) => setCategoryId(id || null)}
onSelect={(id: string | undefined) => setToCategoryId(id || null)}
inputProps={{
onEnter: event =>
!event.defaultPrevented && _onSubmit(amount, categoryId),
!event.defaultPrevented && _onSubmit(amount, toCategoryId),
placeholder: '(none)',
}}
showHiddenCategories={true}
Expand All @@ -85,7 +93,7 @@ export function TransferMenu({
paddingTop: 3,
paddingBottom: 3,
}}
onPress={() => _onSubmit(amount, categoryId)}
onPress={() => _onSubmit(amount, toCategoryId)}
>
Transfer
</Button>
Expand Down
22 changes: 21 additions & 1 deletion packages/desktop-client/src/components/budget/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { type useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
import { send } from 'loot-core/src/platform/client/fetch';
import * as monthUtils from 'loot-core/src/shared/months';
import { type Handlers } from 'loot-core/src/types/handlers';
import { type CategoryGroupEntity } from 'loot-core/src/types/models';
import {
type CategoryEntity,
type CategoryGroupEntity,
} from 'loot-core/src/types/models';
import { type SyncedPrefs } from 'loot-core/src/types/prefs';

import { type CSSProperties, styles, theme } from '../../style';
Expand Down Expand Up @@ -32,6 +35,23 @@ export function addToBeBudgetedGroup(groups: CategoryGroupEntity[]) {
];
}

export function removeCategoriesFromGroups(
categoryGroups: CategoryGroupEntity[],
...categoryIds: CategoryEntity['id'][]
) {
if (!categoryIds || categoryIds.length === 0) return categoryGroups;

const categoryIdsSet = new Set(categoryIds);

return categoryGroups
.map(group => ({
...group,
categories:
group.categories?.filter(cat => !categoryIdsSet.has(cat.id)) ?? [],
}))
.filter(group => group.categories?.length);
}

export function separateGroups(categoryGroups: CategoryGroupEntity[]) {
return [
categoryGroups.filter(g => !g.is_income),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ const ExpenseCategory = memo(function ExpenseCategory({
dispatch(
pushModal('transfer', {
title: category.name,
categoryId: category.id,
month,
amount: catBalance,
onSubmit: (amount, toCategoryId) => {
Expand Down Expand Up @@ -453,7 +454,7 @@ const ExpenseCategory = memo(function ExpenseCategory({
pushModal('cover', {
title: category.name,
month,
category: category.id,
categoryId: category.id,
onSubmit: fromCategoryId => {
onBudgetAction(month, 'cover-overspending', {
to: category.id,
Expand Down
Loading

0 comments on commit e6bf6da

Please sign in to comment.