mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-29 02:54:09 -05:00
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:
committed by
GitHub
parent
7f5f3cc5e9
commit
9a9de5ee09
@@ -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': {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
6
upcoming-release-notes/5908.md
Normal file
6
upcoming-release-notes/5908.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Features
|
||||
authors: [misu-dev]
|
||||
---
|
||||
|
||||
Adds currency display to the budget
|
||||
Reference in New Issue
Block a user