Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display balances in category autocomplete #2551

Merged
merged 12 commits into from
Apr 16, 2024
4 changes: 3 additions & 1 deletion packages/desktop-client/src/components/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,10 @@ export function Modals() {
modalProps={modalProps}
autocompleteProps={{
value: null,
categoryGroups: options.categoryGroups,
onSelect: options.onSelect,
showHiddenCategories: options.showHiddenCategories,
}}
month={options.month}
onClose={options.onClose}
/>
);
Expand Down Expand Up @@ -562,6 +562,7 @@ export function Modals() {
<TransferModal
modalProps={modalProps}
title={options.title}
month={options.month}
amount={options.amount}
onSubmit={options.onSubmit}
showToBeBudgeted={options.showToBeBudgeted}
Expand All @@ -573,6 +574,7 @@ export function Modals() {
<CoverModal
modalProps={modalProps}
categoryId={options.categoryId}
month={options.month}
onSubmit={options.onSubmit}
/>
);
Expand Down
82 changes: 59 additions & 23 deletions packages/desktop-client/src/components/accounts/Account.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as queries from 'loot-core/src/client/queries';
import { runQuery, pagedQuery } from 'loot-core/src/client/query-helpers';
import { send, listen } from 'loot-core/src/platform/client/fetch';
import { currentDay } from 'loot-core/src/shared/months';
import * as monthUtils from 'loot-core/src/shared/months';
import { q } from 'loot-core/src/shared/query';
import { getScheduledAmount } from 'loot-core/src/shared/schedules';
import {
Expand Down Expand Up @@ -801,29 +802,31 @@ class AccountInternal extends PureComponent {
};

onBatchEdit = async (name, ids) => {
const { data } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids } })
.select('*')
.options({ splits: 'grouped' }),
);
const transactions = ungroupTransactions(data);

const onChange = async (name, value, mode) => {
let transactionsToChange = transactions;

const newValue = value === null ? '' : value;
this.setState({ workingHard: true });

const { data } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids } })
.select('*')
.options({ splits: 'grouped' }),
);
let transactions = ungroupTransactions(data);

const changes = { deleted: [], updated: [] };

// Cleared is a special case right now
if (name === 'cleared') {
// Clear them if any are uncleared, otherwise unclear them
value = !!transactions.find(t => !t.cleared);
value = !!transactionsToChange.find(t => !t.cleared);
}

const idSet = new Set(ids);

transactions.forEach(trans => {
transactionsToChange.forEach(trans => {
if (name === 'cleared' && trans.reconciled) {
// Skip transactions that are reconciled. Don't want to set them as
// uncleared.
Expand Down Expand Up @@ -856,13 +859,13 @@ class AccountInternal extends PureComponent {
transaction.reconciled = false;
}

const { diff } = updateTransaction(transactions, transaction);
const { diff } = updateTransaction(transactionsToChange, transaction);

// TODO: We need to keep an updated list of transactions so
// the logic in `updateTransaction`, particularly about
// updating split transactions, works. This isn't ideal and we
// should figure something else out
transactions = applyChanges(diff, transactions);
transactionsToChange = applyChanges(diff, transactionsToChange);

changes.deleted = changes.deleted
? changes.deleted.concat(diff.deleted)
Expand All @@ -879,28 +882,55 @@ class AccountInternal extends PureComponent {
await this.refetchTransactions();

if (this.table.current) {
this.table.current.edit(transactions[0].id, 'select', false);
this.table.current.edit(transactionsToChange[0].id, 'select', false);
}
};

const pushPayeeAutocompleteModal = () => {
this.props.pushModal('payee-autocomplete', {
onSelect: payeeId => onChange(name, payeeId),
});
};

const pushAccountAutocompleteModal = () => {
this.props.pushModal('account-autocomplete', {
onSelect: accountId => onChange(name, accountId),
});
};

const pushCategoryAutocompleteModal = () => {
// Only show balances when all selected transaction are in the same month.
const transactionMonth = transactions[0]?.date
? monthUtils.monthFromDate(transactions[0]?.date)
: null;
const transactionsHaveSameMonth =
transactionMonth &&
transactions.every(
t => monthUtils.monthFromDate(t.date) === transactionMonth,
);
this.props.pushModal('category-autocomplete', {
month: transactionsHaveSameMonth ? transactionMonth : undefined,
onSelect: categoryId => onChange(name, categoryId),
});
};

if (
name === 'amount' ||
name === 'payee' ||
name === 'account' ||
name === 'date'
) {
const { data } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids }, reconciled: true })
.select('*')
.options({ splits: 'grouped' }),
);
const transactions = ungroupTransactions(data);

if (transactions.length > 0) {
const reconciledTransactions = transactions.filter(t => t.reconciled);
if (reconciledTransactions.length > 0) {
this.props.pushModal('confirm-transaction-edit', {
onConfirm: () => {
this.props.pushModal('edit-field', { name, onSubmit: onChange });
if (name === 'payee') {
pushPayeeAutocompleteModal();
} else if (name === 'account') {
pushAccountAutocompleteModal();
} else {
this.props.pushModal('edit-field', { name, onSubmit: onChange });
}
},
confirmReason: 'batchEditWithReconciled',
});
Expand All @@ -912,6 +942,12 @@ class AccountInternal extends PureComponent {
// Cleared just toggles it on/off and it depends on the data
// loaded. Need to clean this up in the future.
onChange('cleared', null);
} else if (name === 'category') {
pushCategoryAutocompleteModal();
} else if (name === 'payee') {
pushPayeeAutocompleteModal();
} else if (name === 'account') {
pushAccountAutocompleteModal();
} else {
this.props.pushModal('edit-field', { name, onSubmit: onChange });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@ import React, {

import { css } from 'glamor';

import { reportBudget, rolloverBudget } from 'loot-core/client/queries';
import { integerToCurrency } from 'loot-core/shared/util';
import {
type CategoryEntity,
type CategoryGroupEntity,
} from 'loot-core/src/types/models';

import { useCategories } from '../../hooks/useCategories';
import { useLocalPref } from '../../hooks/useLocalPref';
import { SvgSplit } from '../../icons/v0';
import { useResponsive } from '../../ResponsiveProvider';
import { type CSSProperties, theme, styles } from '../../style';
import { makeAmountFullStyle } from '../budget/util';
import { Text } from '../common/Text';
import { TextOneLine } from '../common/TextOneLine';
import { View } from '../common/View';
import { useSheetValue } from '../spreadsheet/useSheetValue';

import { Autocomplete, defaultFilterSuggestion } from './Autocomplete';
import { ItemHeader } from './ItemHeader';
Expand All @@ -48,6 +54,7 @@ export type CategoryListProps = {
props: ComponentPropsWithoutRef<typeof CategoryItem>,
) => ReactElement<typeof CategoryItem>;
showHiddenItems?: boolean;
showBalances?: boolean;
};
function CategoryList({
items,
Expand All @@ -59,6 +66,7 @@ function CategoryList({
renderCategoryItemGroupHeader = defaultRenderCategoryItemGroupHeader,
renderCategoryItem = defaultRenderCategoryItem,
showHiddenItems,
showBalances,
}: CategoryListProps) {
let lastGroup: string | undefined | null = null;

Expand Down Expand Up @@ -111,6 +119,7 @@ function CategoryList({
...(showHiddenItems &&
item.hidden && { color: theme.pageTextSubdued }),
},
showBalances,
})}
</Fragment>
</Fragment>
Expand All @@ -125,7 +134,8 @@ function CategoryList({
type CategoryAutocompleteProps = ComponentProps<
typeof Autocomplete<CategoryAutocompleteItem>
> & {
categoryGroups: Array<CategoryGroupEntity>;
categoryGroups?: Array<CategoryGroupEntity>;
showBalances?: boolean;
showSplitOption?: boolean;
renderSplitTransactionButton?: (
props: ComponentPropsWithoutRef<typeof SplitTransactionButton>,
Expand All @@ -141,6 +151,7 @@ type CategoryAutocompleteProps = ComponentProps<

export function CategoryAutocomplete({
categoryGroups,
showBalances = true,
showSplitOption,
embedded,
closeOnBlur,
Expand All @@ -150,9 +161,10 @@ export function CategoryAutocomplete({
showHiddenCategories,
...props
}: CategoryAutocompleteProps) {
const { grouped: defaultCategoryGroups = [] } = useCategories();
const categorySuggestions: CategoryAutocompleteItem[] = useMemo(
() =>
categoryGroups.reduce(
(categoryGroups || defaultCategoryGroups).reduce(
(list, group) =>
list.concat(
(group.categories || [])
Expand All @@ -164,7 +176,7 @@ export function CategoryAutocomplete({
),
showSplitOption ? [{ id: 'split', name: '' } as CategoryEntity] : [],
),
[showSplitOption, categoryGroups],
[defaultCategoryGroups, categoryGroups, showSplitOption],
);

return (
Expand Down Expand Up @@ -200,6 +212,7 @@ export function CategoryAutocomplete({
renderCategoryItemGroupHeader={renderCategoryItemGroupHeader}
renderCategoryItem={renderCategoryItem}
showHiddenItems={showHiddenCategories}
showBalances={showBalances}
/>
)}
{...props}
Expand Down Expand Up @@ -261,9 +274,7 @@ function SplitTransactionButton({
alignItems: 'center',
fontSize: 11,
fontWeight: 500,
color: highlighted
? theme.menuAutoCompleteTextHover
: theme.noticeTextMenu,
color: theme.noticeTextMenu,
padding: '6px 8px',
':active': {
backgroundColor: 'rgba(100, 100, 100, .25)',
Expand Down Expand Up @@ -297,6 +308,7 @@ type CategoryItemProps = {
style?: CSSProperties;
highlighted?: boolean;
embedded?: boolean;
showBalances?: boolean;
};

function CategoryItem({
Expand All @@ -305,6 +317,7 @@ function CategoryItem({
style,
highlighted,
embedded,
showBalances,
...props
}: CategoryItemProps) {
const { isNarrowWidth } = useResponsive();
Expand All @@ -315,6 +328,16 @@ function CategoryItem({
borderTop: `1px solid ${theme.pillBorder}`,
}
: {};
const [budgetType] = useLocalPref('budgetType');

const balance = useSheetValue(
budgetType === 'rollover'
? rolloverBudget.catBalance(item.id)
: reportBudget.catBalance(item.id),
);

const isToBeBudgetedItem = item.id === 'to-be-budgeted';
const toBudget = useSheetValue(rolloverBudget.toBudget);

return (
<div
Expand All @@ -339,10 +362,31 @@ function CategoryItem({
data-highlighted={highlighted || undefined}
{...props}
>
<TextOneLine>
{item.name}
{item.hidden ? ' (hidden)' : null}
</TextOneLine>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<TextOneLine>
{item.name}
{item.hidden ? ' (hidden)' : null}
</TextOneLine>
<TextOneLine
style={{
display: !showBalances ? 'none' : undefined,
marginLeft: 5,
flexShrink: 0,
...makeAmountFullStyle(isToBeBudgetedItem ? toBudget : balance, {
positiveColor: theme.noticeTextMenu,
negativeColor: theme.errorTextMenu,
}),
}}
>
{isToBeBudgetedItem
? toBudget != null
? ` ${integerToCurrency(toBudget || 0)}`
: null
: balance != null
? ` ${integerToCurrency(balance || 0)}`
: null}
</TextOneLine>
</View>
</div>
);
}
Expand Down
18 changes: 14 additions & 4 deletions packages/desktop-client/src/components/budget/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,24 @@ export function makeAmountStyle(
}
}

export function makeAmountFullStyle(value: number) {
export function makeAmountFullStyle(
value: number,
colors?: {
positiveColor?: string;
negativeColor?: string;
zeroColor?: string;
},
) {
const positiveColorToUse = colors.positiveColor || theme.noticeText;
const negativeColorToUse = colors.negativeColor || theme.errorText;
const zeroColorToUse = colors.zeroColor || theme.tableTextSubdued;
return {
color:
value < 0
? theme.errorText
? negativeColorToUse
: value === 0
? theme.tableTextSubdued
: theme.noticeText,
? zeroColorToUse
: positiveColorToUse,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ const ExpenseCategory = memo(function ExpenseCategory({
dispatch(
pushModal('transfer', {
title: `Transfer: ${category.name}`,
month,
amount: catBalance,
onSubmit: (amount, toCategoryId) => {
onBudgetAction(month, 'transfer-category', {
Expand All @@ -281,6 +282,7 @@ const ExpenseCategory = memo(function ExpenseCategory({
dispatch(
pushModal('cover', {
categoryId: category.id,
month,
onSubmit: fromCategoryId => {
onBudgetAction(month, 'cover', {
to: category.id,
Expand Down
Loading
Loading