Compare commits
5 Commits
matiss/chu
...
show-total
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
764b396008 | ||
|
|
2d4b39cce8 | ||
|
|
fc75f468c2 | ||
|
|
2f0dd8f0a9 | ||
|
|
afca809d94 |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/5599.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
[Mobile] Show uncategorized/overspending totals on budget banners
|
||||