Compare commits

...

5 Commits

Author SHA1 Message Date
Joel Jeremy Marquez
764b396008 Remove console.log 2025-08-21 15:33:53 -07:00
Joel Jeremy Marquez
2d4b39cce8 Coderabbit feedback 2025-08-21 15:33:53 -07:00
github-actions[bot]
fc75f468c2 Update VRT 2025-08-21 15:33:53 -07:00
Joel Jeremy Marquez
2f0dd8f0a9 Cleanup 2025-08-21 15:33:53 -07:00
Joel Jeremy Marquez
afca809d94 [Mobile] Show uncategorized/overspending totals on budget banners 2025-08-21 15:33:53 -07:00
87 changed files with 96 additions and 34 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -41,6 +41,7 @@ import { prewarmMonth } from '@desktop-client/components/budget/util';
import { MobilePageHeader, Page } from '@desktop-client/components/Page'; import { MobilePageHeader, Page } from '@desktop-client/components/Page';
import { SyncRefresh } from '@desktop-client/components/SyncRefresh'; import { SyncRefresh } from '@desktop-client/components/SyncRefresh';
import { useCategories } from '@desktop-client/hooks/useCategories'; import { useCategories } from '@desktop-client/hooks/useCategories';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { useLocale } from '@desktop-client/hooks/useLocale'; import { useLocale } from '@desktop-client/hooks/useLocale';
import { useLocalPref } from '@desktop-client/hooks/useLocalPref'; import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
import { useNavigate } from '@desktop-client/hooks/useNavigate'; import { useNavigate } from '@desktop-client/hooks/useNavigate';
@@ -49,13 +50,12 @@ import { SheetNameProvider } from '@desktop-client/hooks/useSheetName';
import { useSheetValue } from '@desktop-client/hooks/useSheetValue'; import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
import { useSpreadsheet } from '@desktop-client/hooks/useSpreadsheet'; import { useSpreadsheet } from '@desktop-client/hooks/useSpreadsheet';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref'; import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import { useTransactions } from '@desktop-client/hooks/useTransactions';
import { useUndo } from '@desktop-client/hooks/useUndo'; import { useUndo } from '@desktop-client/hooks/useUndo';
import { collapseModals, pushModal } from '@desktop-client/modals/modalsSlice'; import { collapseModals, pushModal } from '@desktop-client/modals/modalsSlice';
import { uncategorizedTransactions } from '@desktop-client/queries';
import { useDispatch } from '@desktop-client/redux'; import { useDispatch } from '@desktop-client/redux';
import { import { envelopeBudget } from '@desktop-client/spreadsheet/bindings';
envelopeBudget,
uncategorizedCount,
} from '@desktop-client/spreadsheet/bindings';
function isBudgetType(input?: string): input is 'envelope' | 'tracking' { function isBudgetType(input?: string): input is 'envelope' | 'tracking' {
return ['envelope', 'tracking'].includes(input); return ['envelope', 'tracking'].includes(input);
@@ -676,13 +676,30 @@ function Banner({ type = 'info', children }) {
} }
function UncategorizedTransactionsBanner(props) { function UncategorizedTransactionsBanner(props) {
const count = useSheetValue(uncategorizedCount());
const navigate = useNavigate(); const navigate = useNavigate();
const format = useFormat();
if (count === null || count <= 0) { const transactionsQuery = useMemo(
() => uncategorizedTransactions().select('*'),
[],
);
const { transactions, isLoading } = useTransactions({
query: transactionsQuery,
options: {
pageCount: 1000,
},
});
if (isLoading || transactions.length === 0) {
return null; return null;
} }
const totalUncategorizedAmount = transactions.reduce(
(sum, t) => sum + (t.amount ?? 0),
0,
);
return ( return (
<GridListItem textValue="Uncategorized transactions banner" {...props}> <GridListItem textValue="Uncategorized transactions banner" {...props}>
<Banner type="warning"> <Banner type="warning">
@@ -694,8 +711,9 @@ function UncategorizedTransactionsBanner(props) {
justifyContent: 'space-between', justifyContent: 'space-between',
}} }}
> >
<Trans count={count}> <Trans count={transactions.length}>
You have {{ count }} uncategorized transactions You have {{ count: transactions.length }} uncategorized transactions
({{ amount: format(totalUncategorizedAmount, 'financial') }})
</Trans> </Trans>
<Button <Button
onPress={() => navigate('/categories/uncategorized')} onPress={() => navigate('/categories/uncategorized')}
@@ -802,16 +820,12 @@ function OverspendingBanner({ month, onBudgetAction, budgetType, ...props }) {
const { list: categories, grouped: categoryGroups } = useCategories(); const { list: categories, grouped: categoryGroups } = useCategories();
const categoriesById = useMemo(() => groupById(categories), [categories]); const categoriesById = useMemo(() => groupById(categories), [categories]);
const groupsById = useMemo(() => groupById(categoryGroups), [categoryGroups]);
const dispatch = useDispatch(); const dispatch = useDispatch();
const format = useFormat();
const overspentCategories = useOverspentCategories({ month }).filter(c => { const { categories: overspentCategories, totalAmount: totalOverspending } =
if (budgetType === 'tracking') { useOverspentCategories({ month });
return !c.hidden && !groupsById[c.group].hidden;
}
return true;
});
const categoryGroupsToShow = useMemo( const categoryGroupsToShow = useMemo(
() => () =>
@@ -918,7 +932,8 @@ function OverspendingBanner({ month, onBudgetAction, budgetType, ...props }) {
<Text> <Text>
<Trans count={numberOfOverspentCategories}> <Trans count={numberOfOverspentCategories}>
You have {{ count: numberOfOverspentCategories }} overspent You have {{ count: numberOfOverspentCategories }} overspent
categories categories ({{ amount: format(totalOverspending, 'financial') }}
)
</Trans> </Trans>
</Text> </Text>
</View> </View>

View File

@@ -1,6 +1,8 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import * as monthUtils from 'loot-core/shared/months'; import * as monthUtils from 'loot-core/shared/months';
import { groupById, type IntegerAmount } from 'loot-core/shared/util';
import { type CategoryEntity } from 'loot-core/types/models';
import { useCategories } from './useCategories'; import { useCategories } from './useCategories';
import { useSpreadsheet } from './useSpreadsheet'; import { useSpreadsheet } from './useSpreadsheet';
@@ -15,11 +17,23 @@ type UseOverspentCategoriesProps = {
month: string; month: string;
}; };
export function useOverspentCategories({ month }: UseOverspentCategoriesProps) { type UseOverspentCategoriesResult = {
categories: CategoryEntity[];
amountsByCategory: Map<CategoryEntity['id'], IntegerAmount>;
totalAmount: IntegerAmount;
};
export function useOverspentCategories({
month,
}: UseOverspentCategoriesProps): UseOverspentCategoriesResult {
const spreadsheet = useSpreadsheet(); const spreadsheet = useSpreadsheet();
const [budgetType = 'envelope'] = useSyncedPref('budgetType'); const [budgetType = 'envelope'] = useSyncedPref('budgetType');
const { list: categories } = useCategories(); const { list: categories, grouped: categoryGroups } = useCategories();
const categoryGroupsById = useMemo(
() => groupById(categoryGroups),
[categoryGroups],
);
const categoryBalanceBindings = useMemo( const categoryBalanceBindings = useMemo(
() => () =>
@@ -43,15 +57,15 @@ export function useOverspentCategories({ month }: UseOverspentCategoriesProps) {
[budgetType, categories], [budgetType, categories],
); );
const [overspentByCategory, setOverspentByCategory] = useState< const [overspendingByCategory, setOverspendingByCategory] = useState<
Record<string, number> Record<string, IntegerAmount>
>({}); >({});
const [carryoverFlagByCategory, setCarryoverFlagByCategory] = useState< const [carryoverFlagByCategory, setCarryoverFlagByCategory] = useState<
Record<string, boolean> Record<string, boolean>
>({}); >({});
useEffect(() => { useEffect(() => {
setOverspentByCategory({}); setOverspendingByCategory({});
setCarryoverFlagByCategory({}); setCarryoverFlagByCategory({});
}, [month]); }, [month]);
@@ -89,13 +103,13 @@ export function useOverspentCategories({ month }: UseOverspentCategoriesProps) {
const unbind = spreadsheet.bind(sheetName, balanceBinding, result => { const unbind = spreadsheet.bind(sheetName, balanceBinding, result => {
const balance = result.value as number; const balance = result.value as number;
if (balance < 0) { if (balance < 0) {
setOverspentByCategory(prev => ({ setOverspendingByCategory(prev => ({
...prev, ...prev,
[categoryId]: balance, [categoryId]: balance,
})); }));
} else if (balance >= 0) { } else if (balance >= 0) {
// Update to remove covered category. // Update to remove covered category.
setOverspentByCategory(prev => { setOverspendingByCategory(prev => {
const { [categoryId]: _, ...rest } = prev; const { [categoryId]: _, ...rest } = prev;
return rest; return rest;
}); });
@@ -109,17 +123,44 @@ export function useOverspentCategories({ month }: UseOverspentCategoriesProps) {
}; };
}, [categoryBalanceBindings, sheetName, spreadsheet]); }, [categoryBalanceBindings, sheetName, spreadsheet]);
return useMemo(() => {
// Ignore those that has rollover enabled. // Ignore those that has rollover enabled.
const overspentCategoryIds = Object.keys(overspentByCategory).filter( const categoryIdsToReturn = Object.keys(overspendingByCategory).filter(
id => !carryoverFlagByCategory[id], id => !carryoverFlagByCategory[id],
); );
return useMemo( const categoriesToReturn = categories
() => .filter(
categories.filter(
category => category =>
overspentCategoryIds.includes(category.id) && !category.is_income, categoryIdsToReturn.includes(category.id) && !category.is_income,
), )
[categories, overspentCategoryIds], .filter(category =>
budgetType === 'tracking'
? !category.hidden && !categoryGroupsById[category.group]?.hidden
: true,
); );
const amountsByCategory = new Map(
categoriesToReturn.map(category => [
category.id,
overspendingByCategory[category.id],
]),
);
const totalAmount = amountsByCategory
.values()
.reduce((sum, value) => sum + value, 0);
return {
categories: categoriesToReturn,
amountsByCategory,
totalAmount,
};
}, [
budgetType,
carryoverFlagByCategory,
categories,
categoryGroupsById,
overspendingByCategory,
]);
} }

View File

@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [joel-jeremy]
---
[Mobile] Show uncategorized/overspending totals on budget banners