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

View File

@@ -1,6 +1,8 @@
import { useEffect, useMemo, useState } from 'react';
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 { useSpreadsheet } from './useSpreadsheet';
@@ -15,11 +17,23 @@ type UseOverspentCategoriesProps = {
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 [budgetType = 'envelope'] = useSyncedPref('budgetType');
const { list: categories } = useCategories();
const { list: categories, grouped: categoryGroups } = useCategories();
const categoryGroupsById = useMemo(
() => groupById(categoryGroups),
[categoryGroups],
);
const categoryBalanceBindings = useMemo(
() =>
@@ -43,15 +57,15 @@ export function useOverspentCategories({ month }: UseOverspentCategoriesProps) {
[budgetType, categories],
);
const [overspentByCategory, setOverspentByCategory] = useState<
Record<string, number>
const [overspendingByCategory, setOverspendingByCategory] = useState<
Record<string, IntegerAmount>
>({});
const [carryoverFlagByCategory, setCarryoverFlagByCategory] = useState<
Record<string, boolean>
>({});
useEffect(() => {
setOverspentByCategory({});
setOverspendingByCategory({});
setCarryoverFlagByCategory({});
}, [month]);
@@ -89,13 +103,13 @@ export function useOverspentCategories({ month }: UseOverspentCategoriesProps) {
const unbind = spreadsheet.bind(sheetName, balanceBinding, result => {
const balance = result.value as number;
if (balance < 0) {
setOverspentByCategory(prev => ({
setOverspendingByCategory(prev => ({
...prev,
[categoryId]: balance,
}));
} else if (balance >= 0) {
// Update to remove covered category.
setOverspentByCategory(prev => {
setOverspendingByCategory(prev => {
const { [categoryId]: _, ...rest } = prev;
return rest;
});
@@ -109,17 +123,44 @@ export function useOverspentCategories({ month }: UseOverspentCategoriesProps) {
};
}, [categoryBalanceBindings, sheetName, spreadsheet]);
// Ignore those that has rollover enabled.
const overspentCategoryIds = Object.keys(overspentByCategory).filter(
id => !carryoverFlagByCategory[id],
);
return useMemo(() => {
// Ignore those that has rollover enabled.
const categoryIdsToReturn = Object.keys(overspendingByCategory).filter(
id => !carryoverFlagByCategory[id],
);
return useMemo(
() =>
categories.filter(
const categoriesToReturn = categories
.filter(
category =>
overspentCategoryIds.includes(category.id) && !category.is_income,
),
[categories, overspentCategoryIds],
);
categoryIdsToReturn.includes(category.id) && !category.is_income,
)
.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