Compare commits
72 Commits
v24.6.0
...
scrollToLo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83cd364c5f | ||
|
|
b61b1758d6 | ||
|
|
dbc434c84e | ||
|
|
b1ea639e11 | ||
|
|
bc6098fbb3 | ||
|
|
7f658691bb | ||
|
|
15869eca61 | ||
|
|
5b1a730f11 | ||
|
|
0c14eb17c4 | ||
|
|
7bb0425c81 | ||
|
|
c5c098ea0c | ||
|
|
8832c2b234 | ||
|
|
437e202d27 | ||
|
|
f6b88cc1ba | ||
|
|
d34f5eccb6 | ||
|
|
f1d3902e3e | ||
|
|
ca1d067921 | ||
|
|
8b6ef7b325 | ||
|
|
18e55800e4 | ||
|
|
6ad0b47c7c | ||
|
|
96964224f4 | ||
|
|
0ed5e3ebe6 | ||
|
|
64cd6ee3c9 | ||
|
|
abc4636662 | ||
|
|
18314acd25 | ||
|
|
ade25b3304 | ||
|
|
b192ad955e | ||
|
|
e9da476b51 | ||
|
|
042058ec7b | ||
|
|
9580be7bc4 | ||
|
|
569b995278 | ||
|
|
9590a93e9f | ||
|
|
c20ebd9dbd | ||
|
|
12719e3049 | ||
|
|
e178d9914d | ||
|
|
6fd728aa2d | ||
|
|
bf26ca4eb9 | ||
|
|
f606d92c5c | ||
|
|
8b850f1410 | ||
|
|
c992e340ca | ||
|
|
06f9db06b0 | ||
|
|
2b96bb3d52 | ||
|
|
112f066b8b | ||
|
|
cf6825a541 | ||
|
|
9ec0bdec33 | ||
|
|
4c57596117 | ||
|
|
9e7ebb405f | ||
|
|
7ba3a37ead | ||
|
|
201e1dab54 | ||
|
|
ab124105c2 | ||
|
|
129b2c3061 | ||
|
|
9d6b574708 | ||
|
|
ebb9452b8f | ||
|
|
196f03b84e | ||
|
|
93e784a0fe | ||
|
|
026194e5e2 | ||
|
|
bbec585305 | ||
|
|
2476e45735 | ||
|
|
98341b440a | ||
|
|
417f5805a8 | ||
|
|
094f0b8a91 | ||
|
|
b89a32025a | ||
|
|
d62919a357 | ||
|
|
1c7d9bf141 | ||
|
|
6d117f44de | ||
|
|
64821e6a64 | ||
|
|
e7c6611c88 | ||
|
|
6220aadb2d | ||
|
|
2959054d0c | ||
|
|
7d960579f9 | ||
|
|
5fd1d05670 | ||
|
|
0e86dea544 |
39
.github/workflows/trafico.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
##########################################################################################
|
||||
# WARNING! This workflow uses the 'pull_request_target' event. That mans that it will #
|
||||
# always run in the context of the main actualbudget/actual repo, even if the PR is from #
|
||||
# a fork. This is necessary to get access to a GitHub token that can modify the PR. #
|
||||
# Be VERY CAREFUL about adding things to this workflow, since forks can inject #
|
||||
# arbitrary code into their branch, and can pollute the artifacts we download. Arbitrary #
|
||||
# code execution in this workflow could lead to a compromise of the main repo. #
|
||||
##########################################################################################
|
||||
# See: https://securitylab.github.com/research/github-actions-preventing-pwn-requests #
|
||||
##########################################################################################
|
||||
|
||||
name: Trafico Reviews
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- closed
|
||||
- reopened
|
||||
- synchronize
|
||||
- edited
|
||||
- review_requested
|
||||
- review_request_removed
|
||||
pull_request_review:
|
||||
types: [submitted, edited, dismissed]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
manage-review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actualbudget/trafico@main
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "6.8.0",
|
||||
"version": "6.8.1",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
|
||||
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 83 KiB |
@@ -102,10 +102,22 @@
|
||||
"name": "Store",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "620e85b1-2ae7-45b1-bb3e-b875ea5c553a",
|
||||
"name": "Work",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
}
|
||||
],
|
||||
"payee_locations": [],
|
||||
"category_groups": [
|
||||
{
|
||||
"id": "a5c355c2-3b77-4a7f-b8b3-c832b10cfec8",
|
||||
"name": "Income",
|
||||
"hidden": false,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "d5c355c2-3b77-4a7f-b8b3-c832b10cfec9",
|
||||
"name": "Internal Master Category",
|
||||
@@ -611,6 +623,30 @@
|
||||
"goal_overall_funded": null,
|
||||
"goal_overall_left": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "1429f287-50aa-49d8-a89c-752cbd167d6c",
|
||||
"category_group_id": "a5c355c2-3b77-4a7f-b8b3-c832b10cfec8",
|
||||
"name": "Income",
|
||||
"hidden": false,
|
||||
"original_category_group_id": null,
|
||||
"note": null,
|
||||
"budgeted": 0,
|
||||
"activity": 0,
|
||||
"balance": 0,
|
||||
"goal_type": "NEED",
|
||||
"goal_day": null,
|
||||
"goal_cadence": 1,
|
||||
"goal_cadence_frequency": 1,
|
||||
"goal_creation_month": null,
|
||||
"goal_target": 0,
|
||||
"goal_target_month": null,
|
||||
"goal_percentage_complete": null,
|
||||
"goal_months_to_budget": null,
|
||||
"goal_under_funded": null,
|
||||
"goal_overall_funded": null,
|
||||
"goal_overall_left": null,
|
||||
"deleted": false
|
||||
}
|
||||
],
|
||||
"months": [
|
||||
@@ -1711,6 +1747,26 @@
|
||||
"import_payee_name_original": null,
|
||||
"debt_transaction_type": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "9a22f287-f1e0-4667-9fc0-91e4a4262193",
|
||||
"date": "2024-02-02",
|
||||
"amount": 2000000,
|
||||
"memo": "Paycheck",
|
||||
"cleared": "cleared",
|
||||
"approved": true,
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "620e85b1-2ae7-45b1-bb3e-b875ea5c553a",
|
||||
"category_id": "1429f287-50aa-49d8-a89c-752cbd167d6c",
|
||||
"transfer_account_id": null,
|
||||
"transfer_transaction_id": null,
|
||||
"matched_transaction_id": null,
|
||||
"import_id": null,
|
||||
"import_payee_name": null,
|
||||
"import_payee_name_original": null,
|
||||
"debt_transaction_type": null,
|
||||
"deleted": false
|
||||
}
|
||||
],
|
||||
"subtransactions": [
|
||||
|
||||
@@ -42,6 +42,9 @@ test.describe('Mobile', () => {
|
||||
'Mortgage',
|
||||
'Water',
|
||||
'Power',
|
||||
'Starting Balances',
|
||||
'Misc',
|
||||
'Income',
|
||||
]);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 32 KiB |
@@ -59,7 +59,7 @@ test.describe('Onboarding', () => {
|
||||
await expect(budgetPage.budgetTable).toBeVisible({ timeout: 30000 });
|
||||
|
||||
const accountPage = await navigation.goToAccountPage('Checking');
|
||||
await expect(accountPage.accountBalance).toHaveText('600.00');
|
||||
await expect(accountPage.accountBalance).toHaveText('2,600.00');
|
||||
|
||||
await navigation.goToAccountPage('Saving');
|
||||
await expect(accountPage.accountBalance).toHaveText('250.00');
|
||||
|
||||
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
@@ -69,11 +69,6 @@ test.describe('Rules', () => {
|
||||
});
|
||||
|
||||
test('creates a split transaction rule and makes sure it is applied when creating a transaction', async () => {
|
||||
const settingsPage = await navigation.goToSettingsPage();
|
||||
await settingsPage.enableExperimentalFeature('splits in rules');
|
||||
|
||||
await expect(settingsPage.page.getByLabel('splits in rules')).toBeChecked();
|
||||
|
||||
rulesPage = await navigation.goToRulesPage();
|
||||
|
||||
await rulesPage.createRule({
|
||||
|
||||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
1
packages/desktop-client/locale
Submodule
@@ -32,6 +32,7 @@
|
||||
"@use-gesture/react": "^10.3.0",
|
||||
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||
"auto-text-size": "^0.2.3",
|
||||
"chokidar": "^3.5.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-fns": "^2.30.0",
|
||||
|
||||
@@ -544,6 +544,7 @@ export function Modals() {
|
||||
<RolloverToBudgetMenuModal
|
||||
modalProps={modalProps}
|
||||
onTransfer={options.onTransfer}
|
||||
onCover={options.onCover}
|
||||
onHoldBuffer={options.onHoldBuffer}
|
||||
onResetHoldBuffer={options.onResetHoldBuffer}
|
||||
/>
|
||||
@@ -596,8 +597,9 @@ export function Modals() {
|
||||
<CoverModal
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
categoryId={options.categoryId}
|
||||
title={options.title}
|
||||
month={options.month}
|
||||
showToBeBudgeted={options.showToBeBudgeted}
|
||||
onSubmit={options.onSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -35,6 +35,12 @@ function getErrorMessage(type, code) {
|
||||
case 'RATE_LIMIT_EXCEEDED':
|
||||
return 'Rate limit exceeded for this item. Please try again later.';
|
||||
|
||||
case 'INVALID_ACCESS_TOKEN':
|
||||
return 'Your SimpleFIN Access Token is no longer valid. Please reset and generate a new token.';
|
||||
|
||||
case 'ACCOUNT_NEEDS_ATTENTION':
|
||||
return 'The account needs your attention at [SimpleFIN](https://beta-bridge.simplefin.org/auth/login).';
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { InitialFocus } from '../common/InitialFocus';
|
||||
import { Input } from '../common/Input';
|
||||
import { Menu } from '../common/Menu';
|
||||
import { MenuButton } from '../common/MenuButton';
|
||||
import { MenuTooltip } from '../common/MenuTooltip';
|
||||
import { Popover } from '../common/Popover';
|
||||
import { Search } from '../common/Search';
|
||||
import { Stack } from '../common/Stack';
|
||||
import { View } from '../common/View';
|
||||
@@ -29,7 +29,7 @@ import { NotesButton } from '../NotesButton';
|
||||
import { SelectedTransactionsButton } from '../transactions/SelectedTransactions';
|
||||
|
||||
import { Balances } from './Balance';
|
||||
import { ReconcilingMessage, ReconcileTooltip } from './Reconcile';
|
||||
import { ReconcilingMessage, ReconcileMenu } from './Reconcile';
|
||||
|
||||
export function AccountHeader({
|
||||
filteredAmount,
|
||||
@@ -86,6 +86,7 @@ export function AccountHeader({
|
||||
}) {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const searchInput = useRef(null);
|
||||
const triggerRef = useRef(null);
|
||||
const splitsExpanded = useSplitsExpanded();
|
||||
const syncServerStatus = useSyncServerStatus();
|
||||
const isUsingServer = syncServerStatus !== 'no-server';
|
||||
@@ -270,7 +271,7 @@ export function AccountHeader({
|
||||
}
|
||||
style={{ marginRight: 4 }}
|
||||
/>{' '}
|
||||
{isServerOffline ? 'Sync offline' : 'Sync'}
|
||||
{isServerOffline ? 'Bank Sync Offline' : 'Bank Sync'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -338,9 +339,14 @@ export function AccountHeader({
|
||||
</Button>
|
||||
{account ? (
|
||||
<View>
|
||||
<MenuButton onClick={() => setMenuOpen(true)} />
|
||||
<MenuButton ref={triggerRef} onClick={() => setMenuOpen(true)} />
|
||||
|
||||
{menuOpen && (
|
||||
<Popover
|
||||
triggerRef={triggerRef}
|
||||
style={{ width: 275 }}
|
||||
isOpen={menuOpen}
|
||||
onOpenChange={() => setMenuOpen(false)}
|
||||
>
|
||||
<AccountMenu
|
||||
account={account}
|
||||
canSync={canSync}
|
||||
@@ -356,22 +362,31 @@ export function AccountHeader({
|
||||
onReconcile={onReconcile}
|
||||
onClose={() => setMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
<MenuButton onClick={() => setMenuOpen(true)} />
|
||||
<MenuButton ref={triggerRef} onClick={() => setMenuOpen(true)} />
|
||||
|
||||
{menuOpen && (
|
||||
<CategoryMenu
|
||||
<Popover
|
||||
triggerRef={triggerRef}
|
||||
isOpen={menuOpen}
|
||||
onOpenChange={() => setMenuOpen(false)}
|
||||
>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
setMenuOpen(false);
|
||||
onMenuSelect(item);
|
||||
}}
|
||||
onClose={() => setMenuOpen(false)}
|
||||
isSorted={isSorted}
|
||||
items={[
|
||||
isSorted && {
|
||||
name: 'remove-sorting',
|
||||
text: 'Remove all sorting',
|
||||
},
|
||||
{ name: 'export', text: 'Export' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
</View>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -418,76 +433,54 @@ function AccountMenu({
|
||||
const syncServerStatus = useSyncServerStatus();
|
||||
|
||||
return tooltip === 'reconcile' ? (
|
||||
<ReconcileTooltip
|
||||
<ReconcileMenu
|
||||
account={account}
|
||||
onClose={onClose}
|
||||
onReconcile={onReconcile}
|
||||
/>
|
||||
) : (
|
||||
<MenuTooltip width={200} onClose={onClose}>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
if (item === 'reconcile') {
|
||||
setTooltip('reconcile');
|
||||
} else {
|
||||
onMenuSelect(item);
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
isSorted && {
|
||||
name: 'remove-sorting',
|
||||
text: 'Remove all sorting',
|
||||
},
|
||||
canShowBalances && {
|
||||
name: 'toggle-balance',
|
||||
text: (showBalances ? 'Hide' : 'Show') + ' running balance',
|
||||
},
|
||||
{
|
||||
name: 'toggle-cleared',
|
||||
text: (showCleared ? 'Hide' : 'Show') + ' “cleared” checkboxes',
|
||||
},
|
||||
{
|
||||
name: 'toggle-reconciled',
|
||||
text:
|
||||
(showReconciled ? 'Hide' : 'Show') + ' reconciled transactions',
|
||||
},
|
||||
{ name: 'export', text: 'Export' },
|
||||
{ name: 'reconcile', text: 'Reconcile' },
|
||||
account &&
|
||||
!account.closed &&
|
||||
(canSync
|
||||
? {
|
||||
name: 'unlink',
|
||||
text: 'Unlink account',
|
||||
}
|
||||
: syncServerStatus === 'online' && {
|
||||
name: 'link',
|
||||
text: 'Link account',
|
||||
}),
|
||||
account.closed
|
||||
? { name: 'reopen', text: 'Reopen account' }
|
||||
: { name: 'close', text: 'Close account' },
|
||||
].filter(x => x)}
|
||||
/>
|
||||
</MenuTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryMenu({ onClose, onMenuSelect, isSorted }) {
|
||||
return (
|
||||
<MenuTooltip width={200} onClose={onClose}>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
if (item === 'reconcile') {
|
||||
setTooltip('reconcile');
|
||||
} else {
|
||||
onMenuSelect(item);
|
||||
}}
|
||||
items={[
|
||||
isSorted && {
|
||||
name: 'remove-sorting',
|
||||
text: 'Remove all sorting',
|
||||
},
|
||||
{ name: 'export', text: 'Export' },
|
||||
]}
|
||||
/>
|
||||
</MenuTooltip>
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
isSorted && {
|
||||
name: 'remove-sorting',
|
||||
text: 'Remove all sorting',
|
||||
},
|
||||
canShowBalances && {
|
||||
name: 'toggle-balance',
|
||||
text: (showBalances ? 'Hide' : 'Show') + ' running balance',
|
||||
},
|
||||
{
|
||||
name: 'toggle-cleared',
|
||||
text: (showCleared ? 'Hide' : 'Show') + ' “cleared” checkboxes',
|
||||
},
|
||||
{
|
||||
name: 'toggle-reconciled',
|
||||
text: (showReconciled ? 'Hide' : 'Show') + ' reconciled transactions',
|
||||
},
|
||||
{ name: 'export', text: 'Export' },
|
||||
{ name: 'reconcile', text: 'Reconcile' },
|
||||
account &&
|
||||
!account.closed &&
|
||||
(canSync
|
||||
? {
|
||||
name: 'unlink',
|
||||
text: 'Unlink account',
|
||||
}
|
||||
: syncServerStatus === 'online' && {
|
||||
name: 'link',
|
||||
text: 'Link account',
|
||||
}),
|
||||
account.closed
|
||||
? { name: 'reopen', text: 'Reopen account' }
|
||||
: { name: 'close', text: 'Close account' },
|
||||
].filter(x => x)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Text } from '../common/Text';
|
||||
import { View } from '../common/View';
|
||||
import { useFormat } from '../spreadsheet/useFormat';
|
||||
import { useSheetValue } from '../spreadsheet/useSheetValue';
|
||||
import { Tooltip } from '../tooltips';
|
||||
|
||||
export function ReconcilingMessage({
|
||||
balanceQuery,
|
||||
@@ -95,7 +94,7 @@ export function ReconcilingMessage({
|
||||
);
|
||||
}
|
||||
|
||||
export function ReconcileTooltip({ account, onReconcile, onClose }) {
|
||||
export function ReconcileMenu({ account, onReconcile, onClose }) {
|
||||
const balanceQuery = queries.accountBalance(account);
|
||||
const clearedBalance = useSheetValue({
|
||||
name: balanceQuery.name + '-cleared',
|
||||
@@ -117,24 +116,22 @@ export function ReconcileTooltip({ account, onReconcile, onClose }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip position="bottom-right" width={275} onClose={onClose}>
|
||||
<View style={{ padding: '5px 8px' }}>
|
||||
<Text>
|
||||
Enter the current balance of your bank account that you want to
|
||||
reconcile with:
|
||||
</Text>
|
||||
<form onSubmit={onSubmit}>
|
||||
{clearedBalance != null && (
|
||||
<InitialFocus>
|
||||
<Input
|
||||
defaultValue={format(clearedBalance, 'financial')}
|
||||
style={{ margin: '7px 0' }}
|
||||
/>
|
||||
</InitialFocus>
|
||||
)}
|
||||
<Button type="primary">Reconcile</Button>
|
||||
</form>
|
||||
</View>
|
||||
</Tooltip>
|
||||
<View style={{ padding: '5px 8px' }}>
|
||||
<Text>
|
||||
Enter the current balance of your bank account that you want to
|
||||
reconcile with:
|
||||
</Text>
|
||||
<form onSubmit={onSubmit}>
|
||||
{clearedBalance != null && (
|
||||
<InitialFocus>
|
||||
<Input
|
||||
defaultValue={format(clearedBalance, 'financial')}
|
||||
style={{ margin: '7px 0' }}
|
||||
/>
|
||||
</InitialFocus>
|
||||
)}
|
||||
<Button type="primary">Reconcile</Button>
|
||||
</form>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,13 +122,12 @@ export function AccountAutocomplete({
|
||||
.filter(item => {
|
||||
return includeClosedAccounts ? item : !item.closed;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.closed === b.closed) {
|
||||
return a.offbudget === b.offbudget ? 0 : a.offbudget ? 1 : -1;
|
||||
} else {
|
||||
return a.closed ? 1 : -1;
|
||||
}
|
||||
});
|
||||
.sort(
|
||||
(a, b) =>
|
||||
a.closed - b.closed ||
|
||||
a.offbudget - b.offbudget ||
|
||||
a.sort_order - b.sort_order,
|
||||
);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
|
||||
@@ -92,7 +92,16 @@ export function defaultFilterSuggestion<T extends Item>(
|
||||
suggestion: T,
|
||||
value: string,
|
||||
) {
|
||||
return getItemName(suggestion).toLowerCase().includes(value.toLowerCase());
|
||||
return getItemName(suggestion)
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/\p{Diacritic}/gu, '')
|
||||
.includes(
|
||||
value
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/\p{Diacritic}/gu, ''),
|
||||
);
|
||||
}
|
||||
|
||||
function defaultFilterSuggestions<T extends Item>(
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, {
|
||||
type ComponentType,
|
||||
type ComponentPropsWithoutRef,
|
||||
type ReactElement,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { css } from 'glamor';
|
||||
@@ -135,6 +136,21 @@ function CategoryList({
|
||||
);
|
||||
}
|
||||
|
||||
function customSort(obj: CategoryAutocompleteItem, value: string): number {
|
||||
const name = obj.name.toLowerCase();
|
||||
const groupName = obj.group ? obj.group.name.toLowerCase() : '';
|
||||
if (obj.id === 'split') {
|
||||
return -2;
|
||||
}
|
||||
if (name.includes(value)) {
|
||||
return -1;
|
||||
}
|
||||
if (groupName.includes(value)) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
type CategoryAutocompleteProps = ComponentProps<
|
||||
typeof Autocomplete<CategoryAutocompleteItem>
|
||||
> & {
|
||||
@@ -183,6 +199,33 @@ export function CategoryAutocomplete({
|
||||
[defaultCategoryGroups, categoryGroups, showSplitOption],
|
||||
);
|
||||
|
||||
const filterSuggestions = useCallback(
|
||||
(
|
||||
suggestions: CategoryAutocompleteItem[],
|
||||
value: string,
|
||||
): CategoryAutocompleteItem[] => {
|
||||
return suggestions
|
||||
.filter(suggestion => {
|
||||
return (
|
||||
suggestion.id === 'split' ||
|
||||
suggestion.group?.name
|
||||
.toLowerCase()
|
||||
.includes(value.toLowerCase()) ||
|
||||
(suggestion.group?.name + ' ' + suggestion.name)
|
||||
.toLowerCase()
|
||||
.includes(value.toLowerCase()) ||
|
||||
defaultFilterSuggestion(suggestion, value)
|
||||
);
|
||||
})
|
||||
.sort(
|
||||
(a, b) =>
|
||||
customSort(a, value.toLowerCase()) -
|
||||
customSort(b, value.toLowerCase()),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
strict={true}
|
||||
@@ -197,14 +240,7 @@ export function CategoryAutocomplete({
|
||||
}
|
||||
return 0;
|
||||
}}
|
||||
filterSuggestions={(suggestions, value) => {
|
||||
return suggestions.filter(suggestion => {
|
||||
return (
|
||||
suggestion.id === 'split' ||
|
||||
defaultFilterSuggestion(suggestion, value)
|
||||
);
|
||||
});
|
||||
}}
|
||||
filterSuggestions={filterSuggestions}
|
||||
suggestions={categorySuggestions}
|
||||
renderItems={(items, getItemProps, highlightedIndex) => (
|
||||
<CategoryList
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { type ComponentProps } from 'react';
|
||||
import React, { type ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
import { useFeatureFlag } from '../../hooks/useFeatureFlag';
|
||||
import { SvgArrowThinRight } from '../../icons/v1';
|
||||
import { useResponsive } from '../../ResponsiveProvider';
|
||||
import { type CSSProperties } from '../../style';
|
||||
import { View } from '../common/View';
|
||||
import { type Binding } from '../spreadsheet';
|
||||
import { CellValue } from '../spreadsheet/CellValue';
|
||||
import { useSheetValue } from '../spreadsheet/useSheetValue';
|
||||
|
||||
import { makeBalanceAmountStyle } from './util';
|
||||
|
||||
type BalanceWithCarryoverProps = {
|
||||
carryover: ComponentProps<typeof CellValue>['binding'];
|
||||
balance: ComponentProps<typeof CellValue>['binding'];
|
||||
goal?: ComponentProps<typeof CellValue>['binding'];
|
||||
budgeted?: ComponentProps<typeof CellValue>['binding'];
|
||||
type BalanceWithCarryoverProps = Omit<
|
||||
ComponentPropsWithoutRef<typeof CellValue>,
|
||||
'binding'
|
||||
> & {
|
||||
carryover: Binding;
|
||||
balance: Binding;
|
||||
goal?: Binding;
|
||||
budgeted?: Binding;
|
||||
disabled?: boolean;
|
||||
balanceStyle?: CSSProperties;
|
||||
carryoverStyle?: CSSProperties;
|
||||
};
|
||||
export function BalanceWithCarryover({
|
||||
@@ -26,8 +29,8 @@ export function BalanceWithCarryover({
|
||||
goal,
|
||||
budgeted,
|
||||
disabled,
|
||||
balanceStyle,
|
||||
carryoverStyle,
|
||||
...props
|
||||
}: BalanceWithCarryoverProps) {
|
||||
const carryoverValue = useSheetValue(carryover);
|
||||
const balanceValue = useSheetValue(balance);
|
||||
@@ -40,6 +43,7 @@ export function BalanceWithCarryover({
|
||||
return (
|
||||
<>
|
||||
<CellValue
|
||||
{...props}
|
||||
binding={balance}
|
||||
type="financial"
|
||||
getStyle={value =>
|
||||
@@ -53,9 +57,8 @@ export function BalanceWithCarryover({
|
||||
textAlign: 'right',
|
||||
...(!disabled && {
|
||||
cursor: 'pointer',
|
||||
':hover': { textDecoration: 'underline' },
|
||||
}),
|
||||
...balanceStyle,
|
||||
...props.style,
|
||||
}}
|
||||
/>
|
||||
{carryoverValue && (
|
||||
|
||||
@@ -37,7 +37,6 @@ export const BudgetCategories = memo(
|
||||
function onCollapse(value) {
|
||||
setCollapsedGroupIdsPref(value);
|
||||
}
|
||||
|
||||
const [isAddingGroup, setIsAddingGroup] = useState(false);
|
||||
const [newCategoryForGroup, setNewCategoryForGroup] = useState(null);
|
||||
const items = useMemo(() => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useCategories } from '../../hooks/useCategories';
|
||||
import { useLocalPref } from '../../hooks/useLocalPref';
|
||||
import { theme, styles } from '../../style';
|
||||
import { View } from '../common/View';
|
||||
import { IntersectionBoundary } from '../tooltips';
|
||||
|
||||
import { BudgetCategories } from './BudgetCategories';
|
||||
import { BudgetSummaries } from './BudgetSummaries';
|
||||
@@ -31,12 +31,37 @@ export function BudgetTable(props) {
|
||||
} = props;
|
||||
|
||||
const budgetCategoriesRef = useRef();
|
||||
const scrollableDivRef = useRef();
|
||||
const location = useLocation();
|
||||
const { grouped: categoryGroups } = useCategories();
|
||||
const [collapsedGroupIds = [], setCollapsedGroupIdsPref] =
|
||||
useLocalPref('budget.collapsed');
|
||||
const [showHiddenCategories, setShowHiddenCategoriesPef] = useLocalPref(
|
||||
'budget.showHiddenCategories',
|
||||
);
|
||||
|
||||
const getCurrentScrollPosition = () => {
|
||||
return scrollableDivRef.current?.scrollTop || 0;
|
||||
};
|
||||
|
||||
const onShowActivityWithScroll = (categoryId, month) => {
|
||||
const scrollPosition = getCurrentScrollPosition();
|
||||
onShowActivity(categoryId, month, scrollPosition);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const savedScrollPosition = location.state?.scrollPosition;
|
||||
|
||||
if (savedScrollPosition && scrollableDivRef.current) {
|
||||
// Use requestAnimationFrame to ensure the DOM is ready
|
||||
requestAnimationFrame(() => {
|
||||
if (scrollableDivRef.current) {
|
||||
scrollableDivRef.current.scrollTop = savedScrollPosition;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [location.state?.scrollPosition]);
|
||||
|
||||
const [editing, setEditing] = useState(null);
|
||||
|
||||
const onEditMonth = (id, month) => {
|
||||
@@ -202,41 +227,40 @@ export function BudgetTable(props) {
|
||||
expandAllCategories={expandAllCategories}
|
||||
collapseAllCategories={collapseAllCategories}
|
||||
/>
|
||||
<IntersectionBoundary.Provider value={budgetCategoriesRef}>
|
||||
<View
|
||||
id="scrollableDiv"
|
||||
style={{
|
||||
overflowY: 'scroll',
|
||||
overflowAnchor: 'none',
|
||||
flex: 1,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
}}
|
||||
innerRef={scrollableDivRef}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
overflowY: 'scroll',
|
||||
overflowAnchor: 'none',
|
||||
flex: 1,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
innerRef={budgetCategoriesRef}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<BudgetCategories
|
||||
categoryGroups={categoryGroups}
|
||||
editingCell={editing}
|
||||
dataComponents={dataComponents}
|
||||
onEditMonth={onEditMonth}
|
||||
onEditName={onEditName}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onSaveGroup={onSaveGroup}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onDeleteGroup={onDeleteGroup}
|
||||
onReorderCategory={_onReorderCategory}
|
||||
onReorderGroup={_onReorderGroup}
|
||||
onBudgetAction={onBudgetAction}
|
||||
onShowActivity={onShowActivity}
|
||||
/>
|
||||
</View>
|
||||
<BudgetCategories
|
||||
categoryGroups={categoryGroups}
|
||||
editingCell={editing}
|
||||
dataComponents={dataComponents}
|
||||
onEditMonth={onEditMonth}
|
||||
onEditName={onEditName}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onSaveGroup={onSaveGroup}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onDeleteGroup={onDeleteGroup}
|
||||
onReorderCategory={_onReorderCategory}
|
||||
onReorderGroup={_onReorderGroup}
|
||||
onBudgetAction={onBudgetAction}
|
||||
onShowActivity={onShowActivityWithScroll}
|
||||
/>
|
||||
</View>
|
||||
</IntersectionBoundary.Provider>
|
||||
</View>
|
||||
</MonthsProvider>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -276,7 +276,7 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
dispatch(applyBudgetAction(month, type, args));
|
||||
};
|
||||
|
||||
const onShowActivity = (categoryId, month) => {
|
||||
const onShowActivity = (categoryId, month, scrollPosition) => {
|
||||
const conditions = [
|
||||
{ field: 'category', op: 'is', value: categoryId, type: 'id' },
|
||||
{
|
||||
@@ -287,6 +287,12 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
type: 'date',
|
||||
},
|
||||
];
|
||||
|
||||
navigate('/budget', {
|
||||
replace: true,
|
||||
state: { scrollPosition }
|
||||
});
|
||||
|
||||
navigate('/accounts', {
|
||||
state: {
|
||||
goBack: true,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Tooltip } from '../../tooltips';
|
||||
|
||||
import { BalanceMenu } from './BalanceMenu';
|
||||
|
||||
type BalanceTooltipProps = {
|
||||
categoryId: string;
|
||||
tooltip: { close: () => void };
|
||||
month: string;
|
||||
onBudgetAction: (month: string, action: string, arg: unknown) => void;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export function BalanceTooltip({
|
||||
categoryId,
|
||||
tooltip,
|
||||
month,
|
||||
onBudgetAction,
|
||||
onClose,
|
||||
...tooltipProps
|
||||
}: BalanceTooltipProps) {
|
||||
const _onClose = () => {
|
||||
tooltip.close();
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
position="bottom-right"
|
||||
width={200}
|
||||
style={{ padding: 0 }}
|
||||
onClose={_onClose}
|
||||
{...tooltipProps}
|
||||
>
|
||||
<BalanceMenu
|
||||
categoryId={categoryId}
|
||||
onCarryover={carryover => {
|
||||
onBudgetAction?.(month, 'carryover', {
|
||||
category: categoryId,
|
||||
flag: carryover,
|
||||
});
|
||||
_onClose();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { memo, useState } from 'react';
|
||||
import React, { memo, useRef, useState } from 'react';
|
||||
|
||||
import { reportBudget } from 'loot-core/src/client/queries';
|
||||
import { evalArithmetic } from 'loot-core/src/shared/arithmetic';
|
||||
@@ -8,16 +8,16 @@ import { integerToCurrency, amountToInteger } from 'loot-core/src/shared/util';
|
||||
import { SvgCheveronDown } from '../../../icons/v1';
|
||||
import { styles, theme, type CSSProperties } from '../../../style';
|
||||
import { Button } from '../../common/Button';
|
||||
import { Popover } from '../../common/Popover';
|
||||
import { Text } from '../../common/Text';
|
||||
import { View } from '../../common/View';
|
||||
import { CellValue } from '../../spreadsheet/CellValue';
|
||||
import { useFormat } from '../../spreadsheet/useFormat';
|
||||
import { Field, SheetCell } from '../../table';
|
||||
import { Tooltip, useTooltip } from '../../tooltips';
|
||||
import { BalanceWithCarryover } from '../BalanceWithCarryover';
|
||||
import { makeAmountGrey } from '../util';
|
||||
|
||||
import { BalanceTooltip } from './BalanceTooltip';
|
||||
import { BalanceMenu } from './BalanceMenu';
|
||||
import { BudgetMenu } from './BudgetMenu';
|
||||
|
||||
const headerLabelStyle: CSSProperties = {
|
||||
@@ -156,9 +156,12 @@ export const CategoryMonth = memo(function CategoryMonth({
|
||||
onBudgetAction,
|
||||
onShowActivity,
|
||||
}: CategoryMonthProps) {
|
||||
const balanceTooltip = useTooltip();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [hover, setHover] = useState(false);
|
||||
const triggerRef = useRef(null);
|
||||
|
||||
const [balanceMenuOpen, setBalanceMenuOpen] = useState(false);
|
||||
const triggerBalanceMenuRef = useRef(null);
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -196,6 +199,7 @@ export const CategoryMonth = memo(function CategoryMonth({
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
type="bare"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
@@ -212,44 +216,42 @@ export const CategoryMonth = memo(function CategoryMonth({
|
||||
style={menuOpen && { opacity: 1 }}
|
||||
/>
|
||||
</Button>
|
||||
{menuOpen && (
|
||||
<Tooltip
|
||||
position="bottom-left"
|
||||
width={200}
|
||||
style={{ padding: 0 }}
|
||||
onClose={() => setMenuOpen(false)}
|
||||
>
|
||||
<BudgetMenu
|
||||
onCopyLastMonthAverage={() => {
|
||||
onBudgetAction?.(month, 'copy-single-last', {
|
||||
category: category.id,
|
||||
});
|
||||
}}
|
||||
onSetMonthsAverage={numberOfMonths => {
|
||||
if (
|
||||
numberOfMonths !== 3 &&
|
||||
numberOfMonths !== 6 &&
|
||||
numberOfMonths !== 12
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
onBudgetAction?.(
|
||||
month,
|
||||
`set-single-${numberOfMonths}-avg`,
|
||||
{
|
||||
category: category.id,
|
||||
},
|
||||
);
|
||||
}}
|
||||
onApplyBudgetTemplate={() => {
|
||||
onBudgetAction?.(month, 'apply-single-category-template', {
|
||||
category: category.id,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Popover
|
||||
triggerRef={triggerRef}
|
||||
isOpen={menuOpen}
|
||||
onOpenChange={() => setMenuOpen(false)}
|
||||
placement="bottom start"
|
||||
>
|
||||
<BudgetMenu
|
||||
onCopyLastMonthAverage={() => {
|
||||
onBudgetAction?.(month, 'copy-single-last', {
|
||||
category: category.id,
|
||||
});
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
onSetMonthsAverage={numberOfMonths => {
|
||||
if (
|
||||
numberOfMonths !== 3 &&
|
||||
numberOfMonths !== 6 &&
|
||||
numberOfMonths !== 12
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
onBudgetAction?.(month, `set-single-${numberOfMonths}-avg`, {
|
||||
category: category.id,
|
||||
});
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
onApplyBudgetTemplate={() => {
|
||||
onBudgetAction?.(month, 'apply-single-category-template', {
|
||||
category: category.id,
|
||||
});
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</View>
|
||||
)}
|
||||
<SheetCell
|
||||
@@ -300,7 +302,9 @@ export const CategoryMonth = memo(function CategoryMonth({
|
||||
<Field name="spent" width="flex" style={{ textAlign: 'right' }}>
|
||||
<span
|
||||
data-testid="category-month-spent"
|
||||
onClick={() => onShowActivity(category.id, month)}
|
||||
onClick={() => {
|
||||
onShowActivity(category.id, month);
|
||||
}}
|
||||
>
|
||||
<CellValue
|
||||
binding={reportBudget.catSumAmount(category.id)}
|
||||
@@ -323,23 +327,41 @@ export const CategoryMonth = memo(function CategoryMonth({
|
||||
width="flex"
|
||||
style={{ paddingRight: styles.monthRightPadding, textAlign: 'right' }}
|
||||
>
|
||||
<span {...(category.is_income ? {} : balanceTooltip.getOpenEvents())}>
|
||||
<span
|
||||
ref={triggerBalanceMenuRef}
|
||||
{...(category.is_income
|
||||
? {}
|
||||
: { onClick: () => setBalanceMenuOpen(true) })}
|
||||
>
|
||||
<BalanceWithCarryover
|
||||
disabled={category.is_income}
|
||||
carryover={reportBudget.catCarryover(category.id)}
|
||||
balance={reportBudget.catBalance(category.id)}
|
||||
goal={reportBudget.catGoal(category.id)}
|
||||
budgeted={reportBudget.catBudgeted(category.id)}
|
||||
style={{
|
||||
':hover': { textDecoration: 'underline' },
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
{balanceTooltip.isOpen && (
|
||||
<BalanceTooltip
|
||||
|
||||
<Popover
|
||||
triggerRef={triggerBalanceMenuRef}
|
||||
isOpen={balanceMenuOpen}
|
||||
onOpenChange={() => setBalanceMenuOpen(false)}
|
||||
placement="bottom end"
|
||||
>
|
||||
<BalanceMenu
|
||||
categoryId={category.id}
|
||||
tooltip={balanceTooltip}
|
||||
month={month}
|
||||
onBudgetAction={onBudgetAction}
|
||||
onCarryover={carryover => {
|
||||
onBudgetAction?.(month, 'carryover', {
|
||||
category: category.id,
|
||||
flag: carryover,
|
||||
});
|
||||
setBalanceMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
</Field>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { useState } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import { css } from 'glamor';
|
||||
|
||||
@@ -9,11 +9,11 @@ import { SvgDotsHorizontalTriple } from '../../../../icons/v1';
|
||||
import { SvgArrowButtonDown1, SvgArrowButtonUp1 } from '../../../../icons/v2';
|
||||
import { theme, styles } from '../../../../style';
|
||||
import { Button } from '../../../common/Button';
|
||||
import { Popover } from '../../../common/Popover';
|
||||
import { Stack } from '../../../common/Stack';
|
||||
import { View } from '../../../common/View';
|
||||
import { NotesButton } from '../../../NotesButton';
|
||||
import { NamespaceContext } from '../../../spreadsheet/NamespaceContext';
|
||||
import { Tooltip } from '../../../tooltips';
|
||||
import { useReport } from '../ReportContext';
|
||||
|
||||
import { BudgetMonthMenu } from './BudgetMonthMenu';
|
||||
@@ -33,6 +33,8 @@ export function BudgetSummary({ month }: BudgetSummaryProps) {
|
||||
} = useReport();
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const triggerRef = useRef(null);
|
||||
|
||||
function onMenuOpen() {
|
||||
setMenuOpen(true);
|
||||
}
|
||||
@@ -129,48 +131,51 @@ export function BudgetSummary({ month }: BudgetSummaryProps) {
|
||||
/>
|
||||
</View>
|
||||
<View style={{ userSelect: 'none' }}>
|
||||
<Button type="bare" aria-label="Menu" onClick={onMenuOpen}>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
type="bare"
|
||||
aria-label="Menu"
|
||||
onClick={onMenuOpen}
|
||||
>
|
||||
<SvgDotsHorizontalTriple
|
||||
width={15}
|
||||
height={15}
|
||||
style={{ color: theme.pageTextLight }}
|
||||
/>
|
||||
</Button>
|
||||
{menuOpen && (
|
||||
<Tooltip
|
||||
position="bottom-right"
|
||||
width={200}
|
||||
style={{ padding: 0 }}
|
||||
onClose={onMenuClose}
|
||||
>
|
||||
<BudgetMonthMenu
|
||||
onCopyLastMonthBudget={() => {
|
||||
onBudgetAction(month, 'copy-last');
|
||||
onMenuClose();
|
||||
}}
|
||||
onSetBudgetsToZero={() => {
|
||||
onBudgetAction(month, 'set-zero');
|
||||
onMenuClose();
|
||||
}}
|
||||
onSetMonthsAverage={numberOfMonths => {
|
||||
onBudgetAction(month, `set-${numberOfMonths}-avg`);
|
||||
onMenuClose();
|
||||
}}
|
||||
onCheckTemplates={() => {
|
||||
onBudgetAction(month, 'check-templates');
|
||||
onMenuClose();
|
||||
}}
|
||||
onApplyBudgetTemplates={() => {
|
||||
onBudgetAction(month, 'apply-goal-template');
|
||||
onMenuClose();
|
||||
}}
|
||||
onOverwriteWithBudgetTemplates={() => {
|
||||
onBudgetAction(month, 'overwrite-goal-template');
|
||||
onMenuClose();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Popover
|
||||
triggerRef={triggerRef}
|
||||
isOpen={menuOpen}
|
||||
onOpenChange={onMenuClose}
|
||||
>
|
||||
<BudgetMonthMenu
|
||||
onCopyLastMonthBudget={() => {
|
||||
onBudgetAction(month, 'copy-last');
|
||||
onMenuClose();
|
||||
}}
|
||||
onSetBudgetsToZero={() => {
|
||||
onBudgetAction(month, 'set-zero');
|
||||
onMenuClose();
|
||||
}}
|
||||
onSetMonthsAverage={numberOfMonths => {
|
||||
onBudgetAction(month, `set-${numberOfMonths}-avg`);
|
||||
onMenuClose();
|
||||
}}
|
||||
onCheckTemplates={() => {
|
||||
onBudgetAction(month, 'check-templates');
|
||||
onMenuClose();
|
||||
}}
|
||||
onApplyBudgetTemplates={() => {
|
||||
onBudgetAction(month, 'apply-goal-template');
|
||||
onMenuClose();
|
||||
}}
|
||||
onOverwriteWithBudgetTemplates={() => {
|
||||
onBudgetAction(month, 'overwrite-goal-template');
|
||||
onMenuClose();
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -6,13 +6,12 @@ import { reportBudget } from 'loot-core/src/client/queries';
|
||||
|
||||
import { theme, type CSSProperties, styles } from '../../../../style';
|
||||
import { AlignedText } from '../../../common/AlignedText';
|
||||
import { HoverTarget } from '../../../common/HoverTarget';
|
||||
import { Text } from '../../../common/Text';
|
||||
import { Tooltip } from '../../../common/Tooltip';
|
||||
import { View } from '../../../common/View';
|
||||
import { PrivacyFilter } from '../../../PrivacyFilter';
|
||||
import { useFormat } from '../../../spreadsheet/useFormat';
|
||||
import { useSheetValue } from '../../../spreadsheet/useSheetValue';
|
||||
import { Tooltip } from '../../../tooltips';
|
||||
import { makeAmountFullStyle } from '../../util';
|
||||
|
||||
type SavedProps = {
|
||||
@@ -25,6 +24,7 @@ export function Saved({ projected, style }: SavedProps) {
|
||||
const format = useFormat();
|
||||
const saved = projected ? budgetedSaved : totalSaved;
|
||||
const isNegative = saved < 0;
|
||||
const diff = totalSaved - budgetedSaved;
|
||||
|
||||
return (
|
||||
<View style={{ alignItems: 'center', fontSize: 14, ...style }}>
|
||||
@@ -36,42 +36,36 @@ export function Saved({ projected, style }: SavedProps) {
|
||||
</View>
|
||||
)}
|
||||
|
||||
<HoverTarget
|
||||
renderContent={() => {
|
||||
if (!projected) {
|
||||
const diff = totalSaved - budgetedSaved;
|
||||
return (
|
||||
<Tooltip
|
||||
position="bottom-center"
|
||||
style={{ padding: 10, fontSize: 14 }}
|
||||
>
|
||||
<AlignedText
|
||||
left="Projected Savings:"
|
||||
right={
|
||||
<Text
|
||||
style={{
|
||||
...makeAmountFullStyle(budgetedSaved),
|
||||
...styles.tnum,
|
||||
}}
|
||||
>
|
||||
{format(budgetedSaved, 'financial-with-sign')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<AlignedText
|
||||
left="Difference:"
|
||||
right={
|
||||
<Text
|
||||
style={{ ...makeAmountFullStyle(diff), ...styles.tnum }}
|
||||
>
|
||||
{format(diff, 'financial-with-sign')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
<Tooltip
|
||||
style={{ ...styles.tooltip, fontSize: 14, padding: 10 }}
|
||||
content={
|
||||
<>
|
||||
<AlignedText
|
||||
left="Projected Savings:"
|
||||
right={
|
||||
<Text
|
||||
style={{
|
||||
...makeAmountFullStyle(budgetedSaved),
|
||||
...styles.tnum,
|
||||
}}
|
||||
>
|
||||
{format(budgetedSaved, 'financial-with-sign')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<AlignedText
|
||||
left="Difference:"
|
||||
right={
|
||||
<Text style={{ ...makeAmountFullStyle(diff), ...styles.tnum }}>
|
||||
{format(diff, 'financial-with-sign')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
placement="bottom"
|
||||
triggerProps={{
|
||||
isDisabled: Boolean(projected),
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -90,7 +84,7 @@ export function Saved({ projected, style }: SavedProps) {
|
||||
{format(saved, 'financial')}
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
</HoverTarget>
|
||||
</Tooltip>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,16 +43,14 @@ export function BalanceMenu({
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
name: 'transfer',
|
||||
text: 'Transfer to another category',
|
||||
},
|
||||
{
|
||||
name: 'carryover',
|
||||
text: carryover
|
||||
? 'Remove overspending rollover'
|
||||
: 'Rollover overspending',
|
||||
},
|
||||
...(balance > 0
|
||||
? [
|
||||
{
|
||||
name: 'transfer',
|
||||
text: 'Transfer to another category',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(balance < 0
|
||||
? [
|
||||
{
|
||||
@@ -61,6 +59,12 @@ export function BalanceMenu({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: 'carryover',
|
||||
text: carryover
|
||||
? 'Remove overspending rollover'
|
||||
: 'Rollover overspending',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -60,7 +60,7 @@ export function BalanceMovementMenu({
|
||||
<CoverMenu
|
||||
onClose={onClose}
|
||||
onSubmit={fromCategoryId => {
|
||||
onBudgetAction(month, 'cover', {
|
||||
onBudgetAction(month, 'cover-overspending', {
|
||||
to: categoryId,
|
||||
from: fromCategoryId,
|
||||
});
|
||||
|
||||
@@ -8,15 +8,21 @@ import { View } from '../../common/View';
|
||||
import { addToBeBudgetedGroup } from '../util';
|
||||
|
||||
type CoverMenuProps = {
|
||||
showToBeBudgeted?: boolean;
|
||||
onSubmit: (categoryId: string) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function CoverMenu({ onSubmit, onClose }: CoverMenuProps) {
|
||||
export function CoverMenu({
|
||||
showToBeBudgeted = true,
|
||||
onSubmit,
|
||||
onClose,
|
||||
}: CoverMenuProps) {
|
||||
const { grouped: originalCategoryGroups } = useCategories();
|
||||
const categoryGroups = addToBeBudgetedGroup(
|
||||
originalCategoryGroups.filter(g => !g.is_income),
|
||||
);
|
||||
let categoryGroups = originalCategoryGroups.filter(g => !g.is_income);
|
||||
categoryGroups = showToBeBudgeted
|
||||
? addToBeBudgetedGroup(categoryGroups)
|
||||
: categoryGroups;
|
||||
const [categoryId, setCategoryId] = useState<string | null>(null);
|
||||
|
||||
function submit() {
|
||||
|
||||
@@ -295,7 +295,9 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
|
||||
<Field name="spent" width="flex" style={{ textAlign: 'right' }}>
|
||||
<span
|
||||
data-testid="category-month-spent"
|
||||
onClick={() => onShowActivity(category.id, month)}
|
||||
onClick={() => {
|
||||
onShowActivity(category.id, month);
|
||||
}}
|
||||
>
|
||||
<CellValue
|
||||
binding={rolloverBudget.catSumAmount(category.id)}
|
||||
@@ -323,6 +325,9 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
|
||||
balance={rolloverBudget.catBalance(category.id)}
|
||||
goal={rolloverBudget.catGoal(category.id)}
|
||||
budgeted={rolloverBudget.catBudgeted(category.id)}
|
||||
style={{
|
||||
':hover': { textDecoration: 'underline' },
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import { css } from 'glamor';
|
||||
|
||||
@@ -8,10 +8,10 @@ import { SvgDotsHorizontalTriple } from '../../../../icons/v1';
|
||||
import { SvgArrowButtonDown1, SvgArrowButtonUp1 } from '../../../../icons/v2';
|
||||
import { theme, styles } from '../../../../style';
|
||||
import { Button } from '../../../common/Button';
|
||||
import { Popover } from '../../../common/Popover';
|
||||
import { View } from '../../../common/View';
|
||||
import { NotesButton } from '../../../NotesButton';
|
||||
import { NamespaceContext } from '../../../spreadsheet/NamespaceContext';
|
||||
import { Tooltip } from '../../../tooltips';
|
||||
import { useRollover } from '../RolloverContext';
|
||||
|
||||
import { BudgetMonthMenu } from './BudgetMonthMenu';
|
||||
@@ -31,6 +31,8 @@ export function BudgetSummary({ month }: BudgetSummaryProps) {
|
||||
} = useRollover();
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const triggerRef = useRef(null);
|
||||
|
||||
function onMenuOpen() {
|
||||
setMenuOpen(true);
|
||||
}
|
||||
@@ -131,52 +133,55 @@ export function BudgetSummary({ month }: BudgetSummaryProps) {
|
||||
/>
|
||||
</View>
|
||||
<View style={{ userSelect: 'none', marginLeft: 2 }}>
|
||||
<Button type="bare" aria-label="Menu" onClick={onMenuOpen}>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
type="bare"
|
||||
aria-label="Menu"
|
||||
onClick={onMenuOpen}
|
||||
>
|
||||
<SvgDotsHorizontalTriple
|
||||
width={15}
|
||||
height={15}
|
||||
style={{ color: theme.pageTextLight }}
|
||||
/>
|
||||
</Button>
|
||||
{menuOpen && (
|
||||
<Tooltip
|
||||
position="bottom-right"
|
||||
width={200}
|
||||
style={{ padding: 0 }}
|
||||
onClose={onMenuClose}
|
||||
>
|
||||
<BudgetMonthMenu
|
||||
onCopyLastMonthBudget={() => {
|
||||
onBudgetAction(month, 'copy-last');
|
||||
onMenuClose();
|
||||
}}
|
||||
onSetBudgetsToZero={() => {
|
||||
onBudgetAction(month, 'set-zero');
|
||||
onMenuClose();
|
||||
}}
|
||||
onSetMonthsAverage={numberOfMonths => {
|
||||
onBudgetAction(month, `set-${numberOfMonths}-avg`);
|
||||
onMenuClose();
|
||||
}}
|
||||
onCheckTemplates={() => {
|
||||
onBudgetAction(month, 'check-templates');
|
||||
onMenuClose();
|
||||
}}
|
||||
onApplyBudgetTemplates={() => {
|
||||
onBudgetAction(month, 'apply-goal-template');
|
||||
onMenuClose();
|
||||
}}
|
||||
onOverwriteWithBudgetTemplates={() => {
|
||||
onBudgetAction(month, 'overwrite-goal-template');
|
||||
onMenuClose();
|
||||
}}
|
||||
onEndOfMonthCleanup={() => {
|
||||
onBudgetAction(month, 'cleanup-goal-template');
|
||||
onMenuClose();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Popover
|
||||
triggerRef={triggerRef}
|
||||
isOpen={menuOpen}
|
||||
onOpenChange={onMenuClose}
|
||||
>
|
||||
<BudgetMonthMenu
|
||||
onCopyLastMonthBudget={() => {
|
||||
onBudgetAction(month, 'copy-last');
|
||||
onMenuClose();
|
||||
}}
|
||||
onSetBudgetsToZero={() => {
|
||||
onBudgetAction(month, 'set-zero');
|
||||
onMenuClose();
|
||||
}}
|
||||
onSetMonthsAverage={numberOfMonths => {
|
||||
onBudgetAction(month, `set-${numberOfMonths}-avg`);
|
||||
onMenuClose();
|
||||
}}
|
||||
onCheckTemplates={() => {
|
||||
onBudgetAction(month, 'check-templates');
|
||||
onMenuClose();
|
||||
}}
|
||||
onApplyBudgetTemplates={() => {
|
||||
onBudgetAction(month, 'apply-goal-template');
|
||||
onMenuClose();
|
||||
}}
|
||||
onOverwriteWithBudgetTemplates={() => {
|
||||
onBudgetAction(month, 'overwrite-goal-template');
|
||||
onMenuClose();
|
||||
}}
|
||||
onEndOfMonthCleanup={() => {
|
||||
onBudgetAction(month, 'cleanup-goal-template');
|
||||
onMenuClose();
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -195,6 +200,7 @@ export function BudgetSummary({ month }: BudgetSummaryProps) {
|
||||
prevMonthName={prevMonthName}
|
||||
month={month}
|
||||
onBudgetAction={onBudgetAction}
|
||||
isCollapsed
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
|
||||