Compare commits
5 Commits
js-proxy
...
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 { 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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||