feat(currency): add currency display to budget (#5908)

* feat(currency): add currency display to budget

* coderabbit suggestions

* fix: lint and typecheck

* fix TransferMenu

* coderabbit suggestions

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Michael Süssemilch
2025-12-08 16:55:57 +01:00
committed by GitHub
parent 7f5f3cc5e9
commit 9a9de5ee09
14 changed files with 197 additions and 59 deletions

View File

@@ -335,6 +335,7 @@ type ApplyBudgetActionPayload =
to: CategoryEntity['id'];
from: CategoryEntity['id'];
amount?: IntegerAmount;
currencyCode: string;
};
}
| {
@@ -351,6 +352,7 @@ type ApplyBudgetActionPayload =
args: {
category: CategoryEntity['id'];
amount?: IntegerAmount;
currencyCode: string;
};
}
| {
@@ -360,6 +362,7 @@ type ApplyBudgetActionPayload =
amount: number;
from: CategoryEntity['id'];
to: CategoryEntity['id'];
currencyCode: string;
};
}
| {
@@ -499,6 +502,7 @@ export const applyBudgetAction = createAppAsyncThunk(
to: args.to,
from: args.from,
amount: args.amount,
currencyCode: args.currencyCode,
});
break;
case 'transfer-available':
@@ -513,6 +517,7 @@ export const applyBudgetAction = createAppAsyncThunk(
month,
category: args.category,
amount: args.amount,
currencyCode: args.currencyCode,
});
break;
case 'transfer-category':
@@ -521,6 +526,7 @@ export const applyBudgetAction = createAppAsyncThunk(
amount: args.amount,
from: args.from,
to: args.to,
currencyCode: args.currencyCode,
});
break;
case 'carryover': {

View File

@@ -5,6 +5,7 @@ import { CoverMenu } from './CoverMenu';
import { useEnvelopeSheetValue } from './EnvelopeBudgetComponents';
import { TransferMenu } from './TransferMenu';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { envelopeBudget } from '@desktop-client/spreadsheet/bindings';
type BalanceMovementMenuProps = {
@@ -20,6 +21,8 @@ export function BalanceMovementMenu({
onBudgetAction,
onClose = () => {},
}: BalanceMovementMenuProps) {
const format = useFormat();
const catBalance =
useEnvelopeSheetValue(envelopeBudget.catBalance(categoryId)) ?? 0;
@@ -63,6 +66,7 @@ export function BalanceMovementMenu({
amount,
from: categoryId,
to: toCategoryId,
currencyCode: format.currency.code,
});
}}
/>
@@ -78,6 +82,7 @@ export function BalanceMovementMenu({
to: categoryId,
from: fromCategoryId,
amount,
currencyCode: format.currency.code,
});
}}
/>

View File

@@ -19,9 +19,7 @@ import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { css } from '@emotion/css';
import { evalArithmetic } from 'loot-core/shared/arithmetic';
import * as monthUtils from 'loot-core/shared/months';
import { integerToCurrency, amountToInteger } from 'loot-core/shared/util';
import { type CategoryGroupMonthProps, type CategoryMonthProps } from '..';
@@ -43,6 +41,7 @@ import {
} from '@desktop-client/components/table';
import { useCategoryScheduleGoalTemplateIndicator } from '@desktop-client/hooks/useCategoryScheduleGoalTemplateIndicator';
import { useContextMenu } from '@desktop-client/hooks/useContextMenu';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useSheetName } from '@desktop-client/hooks/useSheetName';
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
@@ -213,6 +212,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
onShowActivity,
}: CategoryMonthProps) {
const { t } = useTranslation();
const format = useFormat();
const budgetMenuTriggerRef = useRef(null);
const balanceMenuTriggerRef = useRef(null);
@@ -378,12 +378,8 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
binding: envelopeBudget.catBudgeted(category.id),
type: 'financial',
getValueStyle: makeAmountGrey,
formatExpr: expr => {
return integerToCurrency(expr);
},
unformatExpr: expr => {
return amountToInteger(evalArithmetic(expr, 0) ?? 0);
},
formatExpr: format.forEdit,
unformatExpr: format.fromEdit,
}}
inputProps={{
onBlur: () => {
@@ -393,10 +389,10 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
backgroundColor: theme.tableBackground,
},
}}
onSave={amount => {
onSave={(parsedIntegerAmount: number | null) => {
onBudgetAction(month, 'budget-amount', {
category: category.id,
amount,
amount: parsedIntegerAmount ?? 0,
});
}}
/>

View File

@@ -4,12 +4,11 @@ import { Trans } from 'react-i18next';
import { Button } from '@actual-app/components/button';
import { InitialFocus } from '@actual-app/components/initial-focus';
import { Input } from '@actual-app/components/input';
import { View } from '@actual-app/components/view';
import { evalArithmetic } from 'loot-core/shared/arithmetic';
import { integerToCurrency, amountToInteger } from 'loot-core/shared/util';
import { type IntegerAmount } from 'loot-core/shared/util';
import { FinancialInput } from '@desktop-client/components/util/FinancialInput';
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
type HoldMenuProps = {
@@ -17,20 +16,12 @@ type HoldMenuProps = {
onClose: () => void;
};
export function HoldMenu({ onSubmit, onClose }: HoldMenuProps) {
const [amount, setAmount] = useState<string | null>(null);
const [amount, setAmount] = useState<IntegerAmount | null>(null);
useSheetValue<'envelope-budget', 'to-budget'>('to-budget', ({ value }) => {
setAmount(integerToCurrency(Math.max(value || 0, 0)));
setAmount(Math.max(value || 0, 0));
});
function _onSubmit(newAmount: string) {
const parsedAmount = evalArithmetic(newAmount);
if (parsedAmount) {
onSubmit(amountToInteger(parsedAmount));
}
onClose();
}
if (amount === null) {
// See `TransferMenu` for more info about this
return null;
@@ -40,7 +31,8 @@ export function HoldMenu({ onSubmit, onClose }: HoldMenuProps) {
<Form
onSubmit={e => {
e.preventDefault();
_onSubmit(amount);
onSubmit(amount);
onClose();
}}
>
<View style={{ padding: 10 }}>
@@ -49,7 +41,7 @@ export function HoldMenu({ onSubmit, onClose }: HoldMenuProps) {
</View>
<View>
<InitialFocus>
<Input value={amount} onChangeValue={setAmount} />
<FinancialInput value={amount} onChangeValue={setAmount} />
</InitialFocus>
</View>
<View

View File

@@ -4,15 +4,9 @@ import { Trans } from 'react-i18next';
import { Button } from '@actual-app/components/button';
import { InitialFocus } from '@actual-app/components/initial-focus';
import { Input } from '@actual-app/components/input';
import { View } from '@actual-app/components/view';
import { evalArithmetic } from 'loot-core/shared/arithmetic';
import {
integerToCurrency,
amountToInteger,
type IntegerAmount,
} from 'loot-core/shared/util';
import { type IntegerAmount } from 'loot-core/shared/util';
import { type CategoryEntity } from 'loot-core/types/models';
import { CategoryAutocomplete } from '@desktop-client/components/autocomplete/CategoryAutocomplete';
@@ -20,6 +14,7 @@ import {
addToBeBudgetedGroup,
removeCategoriesFromGroups,
} from '@desktop-client/components/budget/util';
import { FinancialInput } from '@desktop-client/components/util/FinancialInput';
import { useCategories } from '@desktop-client/hooks/useCategories';
type TransferMenuProps = {
@@ -50,16 +45,15 @@ export function TransferMenu({
: categoryGroups;
}, [originalCategoryGroups, categoryId, showToBeBudgeted]);
const _initialAmount = integerToCurrency(initialAmount ?? 0);
const [amount, setAmount] = useState<string | null>(null);
const [amount, setAmount] = useState<IntegerAmount>(
Math.max(initialAmount ?? 0, 0),
);
const [toCategoryId, setToCategoryId] = useState<string | null>(null);
const _onSubmit = () => {
const parsedAmount = evalArithmetic(amount || '');
if (parsedAmount && toCategoryId) {
onSubmit(amountToInteger(parsedAmount), toCategoryId);
if (amount != null && amount > 0 && toCategoryId) {
onSubmit(amount, toCategoryId);
}
onClose();
};
@@ -76,7 +70,7 @@ export function TransferMenu({
</View>
<View>
<InitialFocus>
<Input defaultValue={_initialAmount} onUpdate={setAmount} />
<FinancialInput value={amount} onUpdate={setAmount} />
</InitialFocus>
</View>
<View style={{ margin: '10px 0 5px 0' }}>
@@ -103,6 +97,7 @@ export function TransferMenu({
<Button
type="submit"
variant="primary"
isDisabled={!toCategoryId || amount <= 0}
style={{
fontSize: 12,
paddingTop: 3,

View File

@@ -16,6 +16,7 @@ import { useEnvelopeSheetValue } from '@desktop-client/components/budget/envelop
import { HoldMenu } from '@desktop-client/components/budget/envelope/HoldMenu';
import { TransferMenu } from '@desktop-client/components/budget/envelope/TransferMenu';
import { useContextMenu } from '@desktop-client/hooks/useContextMenu';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { envelopeBudget } from '@desktop-client/spreadsheet/bindings';
type ToBudgetProps = {
@@ -36,6 +37,7 @@ export function ToBudget({
}: ToBudgetProps) {
const [menuStep, _setMenuStep] = useState<string>('actions');
const triggerRef = useRef(null);
const format = useFormat();
const ref = useRef<HTMLSpanElement>(null);
const setMenuStep = useCallback(
@@ -135,6 +137,7 @@ export function ToBudget({
onBudgetAction(month, 'cover-overbudgeted', {
category: categoryId,
amount,
currencyCode: format.currency.code,
});
}}
/>

View File

@@ -21,9 +21,7 @@ import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { css } from '@emotion/css';
import { evalArithmetic } from 'loot-core/shared/arithmetic';
import * as monthUtils from 'loot-core/shared/months';
import { integerToCurrency, amountToInteger } from 'loot-core/shared/util';
import { type CategoryGroupMonthProps, type CategoryMonthProps } from '..';
@@ -42,6 +40,7 @@ import {
type SheetCellProps,
} from '@desktop-client/components/table';
import { useCategoryScheduleGoalTemplateIndicator } from '@desktop-client/hooks/useCategoryScheduleGoalTemplateIndicator';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
import { useUndo } from '@desktop-client/hooks/useUndo';
@@ -213,6 +212,7 @@ export const CategoryMonth = memo(function CategoryMonth({
}: CategoryMonthProps) {
const [menuOpen, setMenuOpen] = useState(false);
const triggerRef = useRef(null);
const format = useFormat();
const [balanceMenuOpen, setBalanceMenuOpen] = useState(false);
const triggerBalanceMenuRef = useRef(null);
@@ -352,12 +352,8 @@ export const CategoryMonth = memo(function CategoryMonth({
binding: trackingBudget.catBudgeted(category.id),
type: 'financial',
getValueStyle: makeAmountGrey,
formatExpr: expr => {
return integerToCurrency(expr);
},
unformatExpr: expr => {
return amountToInteger(evalArithmetic(expr, 0));
},
formatExpr: format.forEdit,
unformatExpr: format.fromEdit,
}}
inputProps={{
onBlur: () => {
@@ -367,10 +363,10 @@ export const CategoryMonth = memo(function CategoryMonth({
backgroundColor: theme.tableBackground,
},
}}
onSave={amount => {
onSave={(parsedIntegerAmount: number | null) => {
onBudgetAction(month, 'budget-amount', {
category: category.id,
amount,
amount: parsedIntegerAmount ?? 0,
});
}}
/>

View File

@@ -5,7 +5,6 @@ import { Button } from '@actual-app/components/button';
import { Text } from '@actual-app/components/text';
import { AutoTextSize } from 'auto-text-size';
import { integerToCurrency } from 'loot-core/shared/util';
import { type CategoryEntity } from 'loot-core/types/models';
import { getColumnWidth, PILL_STYLE } from './BudgetTable';
@@ -65,7 +64,7 @@ export function BudgetCell<
amount,
});
showUndoNotification({
message: `${category.name} budget has been updated to ${integerToCurrency(amount)}.`,
message: `${category.name} budget has been updated to ${format(amount, 'financial')}.`,
});
},
onCopyLastMonthAverage: () => {
@@ -113,6 +112,7 @@ export function BudgetCell<
month,
onBudgetAction,
showUndoNotification,
format,
]);
return (

View File

@@ -731,6 +731,7 @@ function UncategorizedTransactionsBanner(props) {
function OverbudgetedBanner({ month, onBudgetAction, ...props }) {
const { t } = useTranslation();
const format = useFormat();
const toBudgetAmount = useSheetValue<
'envelope-budget',
typeof envelopeBudget.toBudget
@@ -754,6 +755,7 @@ function OverbudgetedBanner({ month, onBudgetAction, ...props }) {
onBudgetAction(month, 'cover-overbudgeted', {
category: categoryId,
amount,
currencyCode: format.currency.code,
});
showUndoNotification({
message: t('Covered overbudgeted from {{categoryName}}', {
@@ -773,6 +775,7 @@ function OverbudgetedBanner({ month, onBudgetAction, ...props }) {
showUndoNotification,
t,
toBudgetAmount,
format.currency.code,
]);
if (!toBudgetAmount || toBudgetAmount >= 0) {
@@ -863,6 +866,7 @@ function OverspendingBanner({ month, onBudgetAction, budgetType, ...props }) {
to: category.id,
from: fromCategoryId,
amount,
currencyCode: format.currency.code,
});
showUndoNotification({
message: t(
@@ -890,6 +894,7 @@ function OverspendingBanner({ month, onBudgetAction, budgetType, ...props }) {
onBudgetAction,
showUndoNotification,
t,
format.currency.code,
],
);

View File

@@ -11,7 +11,7 @@ import { View } from '@actual-app/components/view';
import { type BudgetType } from 'loot-core/server/prefs';
import * as monthUtils from 'loot-core/shared/months';
import { groupById, integerToCurrency } from 'loot-core/shared/util';
import { groupById } from 'loot-core/shared/util';
import { type CategoryEntity } from 'loot-core/types/models';
import { BalanceCell } from './BalanceCell';
@@ -20,6 +20,7 @@ import { getColumnWidth, ROW_HEIGHT } from './BudgetTable';
import { SpentCell } from './SpentCell';
import { useCategories } from '@desktop-client/hooks/useCategories';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
@@ -229,6 +230,7 @@ export function ExpenseCategoryListItem({
const { t } = useTranslation();
const [budgetType = 'envelope'] = useSyncedPref('budgetType');
const format = useFormat();
const balanceMenuModalName =
`${budgetType as BudgetType}-balance-menu` as const;
@@ -278,10 +280,18 @@ export function ExpenseCategoryListItem({
amount,
from: category.id,
to: toCategoryId,
currencyCode: format.currency.code,
});
dispatch(collapseModals({ rootModalName: balanceMenuModalName }));
showUndoNotification({
message: `Transferred ${integerToCurrency(amount)} from ${category.name} to ${categoriesById[toCategoryId].name}.`,
message: t(
'Transferred {{amount}} from {{fromCategoryName}} to {{toCategoryName}}.',
{
amount: format(amount, 'financial'),
fromCategoryName: category.name,
toCategoryName: categoriesById[toCategoryId].name,
},
),
});
},
showToBeBudgeted: true,
@@ -298,6 +308,8 @@ export function ExpenseCategoryListItem({
balanceMenuModalName,
showUndoNotification,
categoriesById,
format,
t,
]);
const onCover = useCallback(() => {
@@ -318,13 +330,14 @@ export function ExpenseCategoryListItem({
to: category.id,
from: fromCategoryId,
amount,
currencyCode: format.currency.code,
});
dispatch(collapseModals({ rootModalName: balanceMenuModalName }));
showUndoNotification({
message: t(
`Covered {{amount}} {{toCategoryName}} overspending from {{fromCategoryName}}.`,
{
amount: integerToCurrency(amount),
amount: format(amount, 'financial'),
toCategoryName: category.name,
fromCategoryName: categoriesById[fromCategoryId].name,
},
@@ -345,6 +358,7 @@ export function ExpenseCategoryListItem({
showUndoNotification,
t,
categoriesById,
format,
]);
const onOpenBalanceMenu = useCallback(() => {

View File

@@ -3,8 +3,12 @@ import { useTranslation } from 'react-i18next';
import { styles } from '@actual-app/components/styles';
import { format, sheetForMonth, prevMonth } from 'loot-core/shared/months';
import { groupById, integerToCurrency } from 'loot-core/shared/util';
import {
format as formatMonth,
sheetForMonth,
prevMonth,
} from 'loot-core/shared/months';
import { groupById } from 'loot-core/shared/util';
import { ToBudgetAmount } from '@desktop-client/components/budget/envelope/budgetsummary/ToBudgetAmount';
import { TotalsList } from '@desktop-client/components/budget/envelope/budgetsummary/TotalsList';
@@ -15,6 +19,7 @@ import {
ModalHeader,
} from '@desktop-client/components/common/Modal';
import { useCategories } from '@desktop-client/hooks/useCategories';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { useLocale } from '@desktop-client/hooks/useLocale';
import { SheetNameProvider } from '@desktop-client/hooks/useSheetName';
import { useUndo } from '@desktop-client/hooks/useUndo';
@@ -36,10 +41,11 @@ export function EnvelopeBudgetSummaryModal({
onBudgetAction,
}: EnvelopeBudgetSummaryModalProps) {
const { t } = useTranslation();
const format = useFormat();
const locale = useLocale();
const dispatch = useDispatch();
const prevMonthName = format(prevMonth(month), 'MMM', locale);
const prevMonthName = formatMonth(prevMonth(month), 'MMM', locale);
const sheetValue =
useEnvelopeSheetValue({
name: envelopeBudget.toBudget,
@@ -68,7 +74,7 @@ export function EnvelopeBudgetSummaryModal({
dispatch(collapseModals({ rootModalName: 'transfer' }));
showUndoNotification({
message: t('Transferred {{amount}} to {{categoryName}}', {
amount: integerToCurrency(amount),
amount: format(amount, 'financial'),
categoryName: categoriesById[toCategoryId].name,
}),
});
@@ -93,6 +99,7 @@ export function EnvelopeBudgetSummaryModal({
onBudgetAction(month, 'cover-overbudgeted', {
category: categoryId,
amount,
currencyCode: format.currency.code,
});
dispatch(collapseModals({ rootModalName: 'cover' }));
showUndoNotification({

View File

@@ -0,0 +1,95 @@
import { useState, useEffect, useRef, type FocusEvent } from 'react';
import { Input, type InputProps } from '@actual-app/components/input';
import { type IntegerAmount } from 'loot-core/shared/util';
import { useFormat } from '@desktop-client/hooks/useFormat';
type FinancialInputProps = Omit<
InputProps,
'value' | 'onUpdate' | 'onChangeValue' | 'onEnter'
> & {
value: IntegerAmount;
onUpdate?: (value: IntegerAmount) => void;
onChangeValue?: (value: IntegerAmount) => void;
onEnter?: (value: IntegerAmount) => void;
};
export function FinancialInput({
ref,
value: integerValue,
onUpdate,
onChangeValue,
onBlur,
onFocus,
onEnter,
...restProps
}: FinancialInputProps) {
const format = useFormat();
const inputRef = useRef<HTMLInputElement>(null);
const [internalValue, setInternalValue] = useState(() =>
format(integerValue, 'financial'),
);
const [isFocused, setIsFocused] = useState(false);
useEffect(() => {
if (!isFocused) {
setInternalValue(format(integerValue, 'financial'));
}
}, [integerValue, format, isFocused]);
const handleFocus = (e: FocusEvent<HTMLInputElement>) => {
setIsFocused(true);
setInternalValue(format.forEdit(integerValue));
setTimeout(() => {
inputRef.current?.select();
}, 0);
onFocus?.(e);
};
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
setIsFocused(false);
const finalInteger = format.fromEdit(internalValue, 0) ?? 0;
onUpdate?.(finalInteger);
setInternalValue(format(finalInteger, 'financial'));
onBlur?.(e);
};
const handleEnter = (stringValue: string) => {
const finalInteger = format.fromEdit(stringValue, 0) ?? 0;
onUpdate?.(finalInteger);
onEnter?.(finalInteger);
};
const handleChange = (stringValue: string) => {
setInternalValue(stringValue);
if (onChangeValue) {
const newInteger = format.fromEdit(stringValue, 0) ?? 0;
onChangeValue(newInteger);
}
};
const setInputRef = (node: HTMLInputElement | null) => {
inputRef.current = node;
if (typeof ref === 'function') {
ref(node);
} else if (ref) {
ref.current = node;
}
};
return (
<Input
{...restProps}
ref={setInputRef}
value={internalValue || ''}
onChangeValue={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
onEnter={handleEnter}
/>
);
}

View File

@@ -1,5 +1,7 @@
// @ts-strict-ignore
import { getCurrency } from 'loot-core/shared/currencies';
import * as asyncStorage from '../../platform/server/asyncStorage';
import { getLocale } from '../../shared/locale';
import * as monthUtils from '../../shared/months';
@@ -437,11 +439,13 @@ export async function coverOverspending({
to,
from,
amount,
currencyCode,
}: {
month: string;
to: CategoryEntity['id'] | 'to-budget';
from: CategoryEntity['id'] | 'to-budget' | 'overbudgeted';
amount?: IntegerAmount;
currencyCode: string;
}): Promise<void> {
const sheetName = monthUtils.sheetForMonth(month);
const toBudgeted = await getSheetValue(sheetName, 'budget-' + to);
@@ -485,6 +489,7 @@ export async function coverOverspending({
amount: coverableAmount,
to,
from,
currencyCode,
});
});
}
@@ -510,10 +515,12 @@ export async function coverOverbudgeted({
month,
category,
amount,
currencyCode,
}: {
month: string;
category: string;
amount?: IntegerAmount;
currencyCode: string;
}): Promise<void> {
const sheetName = monthUtils.sheetForMonth(month);
const categoryBudget = await getSheetValue(sheetName, 'budget-' + category);
@@ -543,6 +550,7 @@ export async function coverOverbudgeted({
amount: coverableAmount,
from: category,
to: 'overbudgeted',
currencyCode,
});
});
}
@@ -552,11 +560,13 @@ export async function transferCategory({
amount,
from,
to,
currencyCode,
}: {
month: string;
amount: number;
to: CategoryEntity['id'] | 'to-budget';
from: CategoryEntity['id'] | 'to-budget';
currencyCode: string;
}): Promise<void> {
const sheetName = monthUtils.sheetForMonth(month);
const fromBudgeted = await getSheetValue(sheetName, 'budget-' + from);
@@ -576,6 +586,7 @@ export async function transferCategory({
amount,
to,
from,
currencyCode,
});
});
}
@@ -608,13 +619,20 @@ async function addMovementNotes({
amount,
to,
from,
currencyCode,
}: {
month: string;
amount: number;
to: CategoryEntity['id'] | 'to-budget' | 'overbudgeted';
from: CategoryEntity['id'] | 'to-budget';
currencyCode: string;
}) {
const displayAmount = integerToCurrency(amount);
const currency = getCurrency(currencyCode);
const displayAmount = integerToCurrency(
amount,
undefined,
currency.decimalPlaces,
);
const monthBudgetNotesId = `budget-${month}`;
const existingMonthBudgetNotes = addNewLine(

View File

@@ -0,0 +1,6 @@
---
category: Features
authors: [misu-dev]
---
Adds currency display to the budget