Compare commits
28 Commits
split-paye
...
scrollToLo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83cd364c5f | ||
|
|
b61b1758d6 | ||
|
|
dbc434c84e | ||
|
|
b1ea639e11 | ||
|
|
bc6098fbb3 | ||
|
|
15869eca61 | ||
|
|
c5c098ea0c | ||
|
|
f6b88cc1ba | ||
|
|
ca1d067921 | ||
|
|
18e55800e4 | ||
|
|
18314acd25 | ||
|
|
042058ec7b | ||
|
|
9580be7bc4 | ||
|
|
569b995278 | ||
|
|
9590a93e9f | ||
|
|
c20ebd9dbd | ||
|
|
112f066b8b | ||
|
|
cf6825a541 | ||
|
|
9ec0bdec33 | ||
|
|
4c57596117 | ||
|
|
9e7ebb405f | ||
|
|
7ba3a37ead | ||
|
|
201e1dab54 | ||
|
|
ab124105c2 | ||
|
|
129b2c3061 | ||
|
|
9d6b574708 | ||
|
|
bbec585305 | ||
|
|
2476e45735 |
2
.github/workflows/trafico.yml
vendored
@@ -21,6 +21,8 @@ on:
|
||||
- edited
|
||||
- review_requested
|
||||
- review_request_removed
|
||||
pull_request_review:
|
||||
types: [submitted, edited, dismissed]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
|
||||
1
.gitignore
vendored
@@ -21,7 +21,6 @@ packages/api/dist
|
||||
packages/api/@types
|
||||
packages/crdt/dist
|
||||
packages/desktop-electron/client-build
|
||||
packages/desktop-electron/build
|
||||
packages/desktop-electron/.electron-symbols
|
||||
packages/desktop-electron/dist
|
||||
packages/desktop-electron/loot-core
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "6.8.2",
|
||||
"version": "6.8.1",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
|
||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
@@ -44,7 +44,7 @@ export class RulesPage {
|
||||
.first()
|
||||
.click();
|
||||
await this.page
|
||||
.getByRole('button', { exact: true, name: data.conditionsOp })
|
||||
.getByRole('option', { exact: true, name: data.conditionsOp })
|
||||
.click();
|
||||
}
|
||||
|
||||
@@ -97,13 +97,13 @@ export class RulesPage {
|
||||
if (field) {
|
||||
await row.getByRole('button').first().click();
|
||||
await this.page
|
||||
.getByRole('button', { exact: true, name: field })
|
||||
.getByRole('option', { exact: true, name: field })
|
||||
.click();
|
||||
}
|
||||
|
||||
if (op) {
|
||||
await row.getByRole('button', { name: 'is' }).click();
|
||||
await this.page.getByRole('button', { name: op, exact: true }).click();
|
||||
await this.page.getByRole('option', { name: op, exact: true }).click();
|
||||
}
|
||||
|
||||
if (value) {
|
||||
|
||||
@@ -84,10 +84,6 @@ export class SchedulesPage {
|
||||
|
||||
if (data.amount) {
|
||||
await this.page.getByLabel('Amount').fill(String(data.amount));
|
||||
// For some readon, the input field does not trigger the change event on tests
|
||||
// but it works on the browser. We can revisit this once migration to
|
||||
// react aria components is complete.
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
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: 80 KiB After Width: | Height: | Size: 81 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: 76 KiB After Width: | Height: | Size: 77 KiB |
@@ -89,7 +89,7 @@ test.describe('Rules', () => {
|
||||
splitActions: [
|
||||
[
|
||||
{
|
||||
field: 'a fixed percent of the remainder',
|
||||
field: 'a fixed percent',
|
||||
value: '90',
|
||||
},
|
||||
{
|
||||
@@ -120,7 +120,7 @@ test.describe('Rules', () => {
|
||||
});
|
||||
|
||||
const transaction = accountPage.getNthTransaction(0);
|
||||
await expect(transaction.payee).toHaveText('Split');
|
||||
await expect(transaction.payee).toHaveText('Ikea');
|
||||
await expect(transaction.notes).toHaveText('food / entertainment');
|
||||
await expect(transaction.category).toHaveText('Split');
|
||||
await expect(transaction.debit).toHaveText('100.00');
|
||||
|
||||
|
Before Width: | Height: | Size: 105 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: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 83 KiB |
@@ -120,7 +120,7 @@ test.describe('Transactions', () => {
|
||||
]);
|
||||
|
||||
const firstTransaction = accountPage.getNthTransaction(0);
|
||||
await expect(firstTransaction.payee).toHaveText('Split');
|
||||
await expect(firstTransaction.payee).toHaveText('Krogger');
|
||||
await expect(firstTransaction.notes).toHaveText('Notes');
|
||||
await expect(firstTransaction.category).toHaveText('Split');
|
||||
await expect(firstTransaction.debit).toHaveText('333.33');
|
||||
|
||||
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.6 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: 9.1 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/web",
|
||||
"version": "24.7.0",
|
||||
"version": "24.6.0",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"build"
|
||||
@@ -8,6 +8,7 @@
|
||||
"devDependencies": {
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@playwright/test": "1.41.1",
|
||||
"@reach/listbox": "^0.18.0",
|
||||
"@react-aria/focus": "^3.16.0",
|
||||
"@react-aria/listbox": "^3.11.3",
|
||||
"@react-aria/utils": "^3.23.0",
|
||||
@@ -48,7 +49,7 @@
|
||||
"pikaday": "1.8.2",
|
||||
"promise-retry": "^2.0.1",
|
||||
"react": "18.2.0",
|
||||
"react-aria-components": "^1.2.1",
|
||||
"react-aria-components": "^1.1.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path d="M1.857 7.612H.98a.98.98 0 0 0-.98.98v2.224c0 .449.184.857.51 1.163.612.551 1.816 1.49 3.735 2.368a.571.571 0 0 1 .306.346l.939 3.286a.484.484 0 0 0 .469.347h1.47a.479.479 0 0 0 .469-.367l.47-1.817a.262.262 0 0 1 .326-.183c.571.122 1.183.163 1.795.163.613 0 1.225-.061 1.796-.163.143-.02.286.06.327.183l.47 1.817a.502.502 0 0 0 .468.367h1.47a.484.484 0 0 0 .47-.347l1.224-4.245a.487.487 0 0 1 .122-.224c.919-.98 1.53-2.163 1.735-3.47h.49c.53 0 .959-.448.938-.979-.02-.51-.47-.918-.98-.918h-.448C18.06 4.673 14.632 2 10.489 2c-1.775 0-3.428.49-4.755 1.326-.591-.408-1.469-.734-2.693-.632-.49.04-.715.612-.388.959.408.429.796 1 .877 1.735L1.857 7.612Zm3.122.98a.862.862 0 0 1-.857-.858c0-.469.388-.857.857-.857.47 0 .858.388.858.857 0 .47-.388.858-.858.858Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 838 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path d="M19 18h-1v-8c0-.6-.4-1-1-1s-1 .4-1 1v8h-3V1c0-.6-.4-1-1-1s-1 .4-1 1v17H8V7c0-.6-.4-1-1-1s-1 .4-1 1v11H3V3c0-.6-.4-1-1-1s-1 .4-1 1v15c-.6 0-1 .4-1 1s.4 1 1 1h18c.6 0 1-.4 1-1s-.4-1-1-1z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 268 B |
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M23 11.5a1.5 1.5 0 0 1-1.5 1.5h-20a1.5 1.5 0 0 1 0-3h20a1.5 1.5 0 0 1 1.5 1.5Z" />
|
||||
<path d="M11.5 23a1.5 1.5 0 0 1-1.5-1.5v-20a1.5 1.5 0 0 1 3 0v20a1.5 1.5 0 0 1-1.5 1.5Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 256 B |
@@ -28,44 +28,6 @@
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Add Transaction",
|
||||
"short_name": "Add Transaction",
|
||||
"description": "Add a new transaction",
|
||||
"url": "/transactions/new",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/shortcut-transaction.svg",
|
||||
"sizes": "150x150"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Accounts",
|
||||
"short_name": "Accounts",
|
||||
"description": "View all accounts",
|
||||
"url": "/accounts",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/shortcut-accounts.svg",
|
||||
"sizes": "150x150"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Reports",
|
||||
"short_name": "Reports",
|
||||
"description": "View reports",
|
||||
"url": "/reports",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/shortcut-reports.svg",
|
||||
"sizes": "150x150"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/screenshot_wide.png",
|
||||
@@ -81,7 +43,7 @@
|
||||
"type": "image/png",
|
||||
"sizes": "350x600"
|
||||
}
|
||||
],
|
||||
],
|
||||
"theme_color": "#8812E1",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone",
|
||||
|
||||
@@ -173,7 +173,6 @@ export function Modals() {
|
||||
<ConfirmTransactionEdit
|
||||
key={name}
|
||||
modalProps={modalProps}
|
||||
onCancel={options.onCancel}
|
||||
onConfirm={options.onConfirm}
|
||||
confirmReason={options.confirmReason}
|
||||
/>
|
||||
@@ -402,6 +401,7 @@ export function Modals() {
|
||||
actions={actions}
|
||||
transactionIds={options?.transactionIds}
|
||||
getTransaction={options?.getTransaction}
|
||||
pushModal={options?.pushModal}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { PureComponent, createRef, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Navigate, useParams, useLocation } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { Navigate, useParams, useLocation, useMatch } from 'react-router-dom';
|
||||
|
||||
import { debounce } from 'debounce';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import { validForTransfer } from 'loot-core/client/transfer';
|
||||
import * as actions from 'loot-core/src/client/actions';
|
||||
import { useFilters } from 'loot-core/src/client/data-hooks/filters';
|
||||
import { SchedulesProvider } from 'loot-core/src/client/data-hooks/schedules';
|
||||
import * as queries from 'loot-core/src/client/queries';
|
||||
@@ -21,13 +22,10 @@ import {
|
||||
realizeTempTransactions,
|
||||
ungroupTransaction,
|
||||
ungroupTransactions,
|
||||
makeChild,
|
||||
makeAsNonChildTransactions,
|
||||
} from 'loot-core/src/shared/transactions';
|
||||
import { applyChanges, groupById } from 'loot-core/src/shared/util';
|
||||
|
||||
import { useAccounts } from '../../hooks/useAccounts';
|
||||
import { useActions } from '../../hooks/useActions';
|
||||
import { useCategories } from '../../hooks/useCategories';
|
||||
import { useDateFormat } from '../../hooks/useDateFormat';
|
||||
import { useFailedAccounts } from '../../hooks/useFailedAccounts';
|
||||
@@ -40,7 +38,7 @@ import {
|
||||
useSplitsExpanded,
|
||||
} from '../../hooks/useSplitsExpanded';
|
||||
import { styles, theme } from '../../style';
|
||||
import { Button } from '../common/Button2';
|
||||
import { Button } from '../common/Button';
|
||||
import { Text } from '../common/Text';
|
||||
import { View } from '../common/View';
|
||||
import { TransactionList } from '../transactions/TransactionList';
|
||||
@@ -73,7 +71,7 @@ function EmptyMessage({ onAdd }) {
|
||||
manage it locally yourself.
|
||||
</Text>
|
||||
|
||||
<Button variant="primary" style={{ marginTop: 20 }} onPress={onAdd}>
|
||||
<Button type="primary" style={{ marginTop: 20 }} onClick={onAdd}>
|
||||
Add account
|
||||
</Button>
|
||||
|
||||
@@ -181,9 +179,7 @@ class AccountInternal extends PureComponent {
|
||||
|
||||
this.state = {
|
||||
search: '',
|
||||
filterConditions: props.filterConditions || [],
|
||||
filterId: [],
|
||||
filterConditionsOp: 'and',
|
||||
filters: props.conditions || [],
|
||||
loading: true,
|
||||
workingHard: false,
|
||||
reconcileAmount: null,
|
||||
@@ -196,8 +192,9 @@ class AccountInternal extends PureComponent {
|
||||
editingName: false,
|
||||
isAdding: false,
|
||||
latestDate: null,
|
||||
filterId: [],
|
||||
conditionsOp: 'and',
|
||||
sort: [],
|
||||
filteredAmount: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -259,7 +256,7 @@ class AccountInternal extends PureComponent {
|
||||
// Important that any async work happens last so that the
|
||||
// listeners are set up synchronously
|
||||
await this.props.initiallyLoadPayees();
|
||||
await this.fetchTransactions(this.state.filterConditions);
|
||||
await this.fetchTransactions(this.state.filters);
|
||||
|
||||
// If there is a pending undo, apply it immediately (this happens
|
||||
// when an undo changes the location to this page)
|
||||
@@ -288,7 +285,7 @@ class AccountInternal extends PureComponent {
|
||||
|
||||
//Resest sort/filter/search on account change
|
||||
if (this.props.accountId !== prevProps.accountId) {
|
||||
this.setState({ sort: [], search: '', filterConditions: [] });
|
||||
this.setState({ sort: [], search: '', filters: [] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,10 +313,10 @@ class AccountInternal extends PureComponent {
|
||||
this.paged?.run();
|
||||
};
|
||||
|
||||
fetchTransactions = filterConditions => {
|
||||
fetchTransactions = filters => {
|
||||
const query = this.makeRootQuery();
|
||||
this.rootQuery = this.currentQuery = query;
|
||||
if (filterConditions) this.applyFilters(filterConditions);
|
||||
if (filters) this.applyFilters(filters);
|
||||
else this.updateQuery(query);
|
||||
|
||||
if (this.props.accountId) {
|
||||
@@ -374,7 +371,6 @@ class AccountInternal extends PureComponent {
|
||||
balances: this.state.showBalances
|
||||
? await this.calculateBalances()
|
||||
: null,
|
||||
filteredAmount: await this.getFilteredAmount(),
|
||||
},
|
||||
() => {
|
||||
if (firstLoad) {
|
||||
@@ -422,10 +418,7 @@ class AccountInternal extends PureComponent {
|
||||
|
||||
onSearchDone = debounce(() => {
|
||||
if (this.state.search === '') {
|
||||
this.updateQuery(
|
||||
this.currentQuery,
|
||||
this.state.filterConditions.length > 0,
|
||||
);
|
||||
this.updateQuery(this.currentQuery, this.state.filters.length > 0);
|
||||
} else {
|
||||
this.updateQuery(
|
||||
queries.makeTransactionSearchQuery(
|
||||
@@ -518,7 +511,7 @@ class AccountInternal extends PureComponent {
|
||||
return (
|
||||
account &&
|
||||
this.state.search === '' &&
|
||||
this.state.filterConditions.length === 0 &&
|
||||
this.state.filters.length === 0 &&
|
||||
(this.state.sort.length === 0 ||
|
||||
(this.state.sort.field === 'date' &&
|
||||
this.state.sort.ascDesc === 'desc'))
|
||||
@@ -606,7 +599,7 @@ class AccountInternal extends PureComponent {
|
||||
{
|
||||
transactions: [],
|
||||
transactionCount: 0,
|
||||
filterConditions: [],
|
||||
filters: [],
|
||||
search: '',
|
||||
sort: [],
|
||||
showBalances: true,
|
||||
@@ -619,9 +612,9 @@ class AccountInternal extends PureComponent {
|
||||
break;
|
||||
case 'remove-sorting': {
|
||||
this.setState({ sort: [] }, () => {
|
||||
const filterConditions = this.state.filterConditions;
|
||||
if (filterConditions.length > 0) {
|
||||
this.applyFilters([...filterConditions]);
|
||||
const filters = this.state.filters;
|
||||
if (filters.length > 0) {
|
||||
this.applyFilters([...filters]);
|
||||
} else {
|
||||
this.fetchTransactions();
|
||||
}
|
||||
@@ -644,12 +637,12 @@ class AccountInternal extends PureComponent {
|
||||
if (this.state.showReconciled) {
|
||||
this.props.savePrefs({ ['hide-reconciled-' + accountId]: true });
|
||||
this.setState({ showReconciled: false }, () =>
|
||||
this.fetchTransactions(this.state.filterConditions),
|
||||
this.fetchTransactions(this.state.filters),
|
||||
);
|
||||
} else {
|
||||
this.props.savePrefs({ ['hide-reconciled-' + accountId]: false });
|
||||
this.setState({ showReconciled: true }, () =>
|
||||
this.fetchTransactions(this.state.filterConditions),
|
||||
this.fetchTransactions(this.state.filters),
|
||||
);
|
||||
}
|
||||
break;
|
||||
@@ -687,11 +680,24 @@ class AccountInternal extends PureComponent {
|
||||
};
|
||||
}
|
||||
|
||||
getFilteredAmount = async () => {
|
||||
const { data: amount } = await runQuery(
|
||||
this.paged.getQuery().calculate({ $sum: '$amount' }),
|
||||
getFilteredAmount = async (filters, conditionsOpKey) => {
|
||||
const filter = queries.getAccountFilter(this.props.accountId);
|
||||
|
||||
let query = q('transactions').filter({
|
||||
[conditionsOpKey]: [...filters],
|
||||
});
|
||||
if (filter) {
|
||||
query = query.filter(filter);
|
||||
}
|
||||
|
||||
const filteredQuery = await runQuery(
|
||||
query.select([{ amount: { $sum: '$amount' } }]),
|
||||
);
|
||||
return amount;
|
||||
const filteredAmount = filteredQuery.data.reduce(
|
||||
(a, v) => (a = a + v.amount),
|
||||
0,
|
||||
);
|
||||
return filteredAmount;
|
||||
};
|
||||
|
||||
isNew = id => {
|
||||
@@ -811,7 +817,7 @@ class AccountInternal extends PureComponent {
|
||||
onShowTransactions = async ids => {
|
||||
this.onApplyFilter({
|
||||
customName: 'Selected transactions',
|
||||
queryFilter: { id: { $oneof: ids } },
|
||||
filter: { id: { $oneof: ids } },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1052,114 +1058,6 @@ class AccountInternal extends PureComponent {
|
||||
);
|
||||
};
|
||||
|
||||
onMakeAsSplitTransaction = async ids => {
|
||||
this.setState({ workingHard: true });
|
||||
|
||||
const { data: transactions } = await runQuery(
|
||||
q('transactions')
|
||||
.filter({ id: { $oneof: ids } })
|
||||
.select('*')
|
||||
.options({ splits: 'none' }),
|
||||
);
|
||||
|
||||
if (!transactions || transactions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [firstTransaction] = transactions;
|
||||
const parentTransaction = {
|
||||
id: uuidv4(),
|
||||
is_parent: true,
|
||||
cleared: transactions.every(t => !!t.cleared),
|
||||
date: firstTransaction.date,
|
||||
account: firstTransaction.account,
|
||||
amount: transactions
|
||||
.map(t => t.amount)
|
||||
.reduce((total, amount) => total + amount, 0),
|
||||
};
|
||||
const childTransactions = transactions.map(t =>
|
||||
makeChild(parentTransaction, t),
|
||||
);
|
||||
|
||||
await send('transactions-batch-update', {
|
||||
added: [parentTransaction],
|
||||
updated: childTransactions,
|
||||
});
|
||||
|
||||
this.refetchTransactions();
|
||||
};
|
||||
|
||||
onMakeAsNonSplitTransactions = async ids => {
|
||||
this.setState({ workingHard: true });
|
||||
|
||||
const { data: groupedTransactions } = await runQuery(
|
||||
q('transactions')
|
||||
.filter({ id: { $oneof: ids } })
|
||||
.select('*')
|
||||
.options({ splits: 'grouped' }),
|
||||
);
|
||||
|
||||
let changes = {
|
||||
updated: [],
|
||||
deleted: [],
|
||||
};
|
||||
|
||||
const groupedTransactionsToUpdate = groupedTransactions.filter(
|
||||
t => t.is_parent,
|
||||
);
|
||||
|
||||
for (const groupedTransaction of groupedTransactionsToUpdate) {
|
||||
const transactions = ungroupTransaction(groupedTransaction);
|
||||
const [parentTransaction, ...childTransactions] = transactions;
|
||||
|
||||
if (ids.includes(parentTransaction.id)) {
|
||||
// Unsplit all child transactions.
|
||||
const diff = makeAsNonChildTransactions(
|
||||
childTransactions,
|
||||
transactions,
|
||||
);
|
||||
|
||||
changes = {
|
||||
updated: [...changes.updated, ...diff.updated],
|
||||
deleted: [...changes.deleted, ...diff.deleted],
|
||||
};
|
||||
|
||||
// Already processed the child transactions above, no need to process them below.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unsplit selected child transactions.
|
||||
|
||||
const selectedChildTransactions = childTransactions.filter(t =>
|
||||
ids.includes(t.id),
|
||||
);
|
||||
|
||||
if (selectedChildTransactions.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const diff = makeAsNonChildTransactions(
|
||||
selectedChildTransactions,
|
||||
transactions,
|
||||
);
|
||||
|
||||
changes = {
|
||||
updated: [...changes.updated, ...diff.updated],
|
||||
deleted: [...changes.deleted, ...diff.deleted],
|
||||
};
|
||||
}
|
||||
|
||||
await send('transactions-batch-update', changes);
|
||||
|
||||
this.refetchTransactions();
|
||||
|
||||
const transactionsToSelect = changes.updated.map(t => t.id);
|
||||
this.dispatchSelected({
|
||||
type: 'select-all',
|
||||
ids: transactionsToSelect,
|
||||
});
|
||||
};
|
||||
|
||||
checkForReconciledTransactions = async (ids, confirmReason, onConfirm) => {
|
||||
const { data } = await runQuery(
|
||||
q('transactions')
|
||||
@@ -1311,10 +1209,10 @@ class AccountInternal extends PureComponent {
|
||||
);
|
||||
};
|
||||
|
||||
onConditionsOpChange = (value, conditions) => {
|
||||
this.setState({ filterConditionsOp: value });
|
||||
onCondOpChange = (value, filters) => {
|
||||
this.setState({ conditionsOp: value });
|
||||
this.setState({ filterId: { ...this.state.filterId, status: 'changed' } });
|
||||
this.applyFilters([...conditions]);
|
||||
this.applyFilters([...filters]);
|
||||
if (this.state.search !== '') {
|
||||
this.onSearch(this.state.search);
|
||||
}
|
||||
@@ -1322,14 +1220,14 @@ class AccountInternal extends PureComponent {
|
||||
|
||||
onReloadSavedFilter = (savedFilter, item) => {
|
||||
if (item === 'reload') {
|
||||
const [savedFilter] = this.props.savedFilters.filter(
|
||||
const [getFilter] = this.props.filtersList.filter(
|
||||
f => f.id === this.state.filterId.id,
|
||||
);
|
||||
this.setState({ filterConditionsOp: savedFilter.conditionsOp });
|
||||
this.applyFilters([...savedFilter.conditions]);
|
||||
this.setState({ conditionsOp: getFilter.conditionsOp });
|
||||
this.applyFilters([...getFilter.conditions]);
|
||||
} else {
|
||||
if (savedFilter.status) {
|
||||
this.setState({ filterConditionsOp: savedFilter.conditionsOp });
|
||||
this.setState({ conditionsOp: savedFilter.conditionsOp });
|
||||
this.applyFilters([...savedFilter.conditions]);
|
||||
}
|
||||
}
|
||||
@@ -1337,7 +1235,7 @@ class AccountInternal extends PureComponent {
|
||||
};
|
||||
|
||||
onClearFilters = () => {
|
||||
this.setState({ filterConditionsOp: 'and' });
|
||||
this.setState({ conditionsOp: 'and' });
|
||||
this.setState({ filterId: [] });
|
||||
this.applyFilters([]);
|
||||
if (this.state.search !== '') {
|
||||
@@ -1345,11 +1243,9 @@ class AccountInternal extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
onUpdateFilter = (oldCondition, updatedCondition) => {
|
||||
onUpdateFilter = (oldFilter, updatedFilter) => {
|
||||
this.applyFilters(
|
||||
this.state.filterConditions.map(c =>
|
||||
c === oldCondition ? updatedCondition : c,
|
||||
),
|
||||
this.state.filters.map(f => (f === oldFilter ? updatedFilter : f)),
|
||||
);
|
||||
this.setState({
|
||||
filterId: {
|
||||
@@ -1362,11 +1258,11 @@ class AccountInternal extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteFilter = condition => {
|
||||
this.applyFilters(this.state.filterConditions.filter(c => c !== condition));
|
||||
if (this.state.filterConditions.length === 1) {
|
||||
onDeleteFilter = filter => {
|
||||
this.applyFilters(this.state.filters.filter(f => f !== filter));
|
||||
if (this.state.filters.length === 1) {
|
||||
this.setState({ filterId: [] });
|
||||
this.setState({ filterConditionsOp: 'and' });
|
||||
this.setState({ conditionsOp: 'and' });
|
||||
} else {
|
||||
this.setState({
|
||||
filterId: {
|
||||
@@ -1380,31 +1276,23 @@ class AccountInternal extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
onApplyFilter = async conditionOrSavedFilter => {
|
||||
let filterConditions = this.state.filterConditions;
|
||||
if (conditionOrSavedFilter.customName) {
|
||||
filterConditions = filterConditions.filter(
|
||||
c => c.customName !== conditionOrSavedFilter.customName,
|
||||
);
|
||||
onApplyFilter = async cond => {
|
||||
let filters = this.state.filters;
|
||||
if (cond.customName) {
|
||||
filters = filters.filter(f => f.customName !== cond.customName);
|
||||
}
|
||||
if (conditionOrSavedFilter.conditions) {
|
||||
// A saved filter was passed in.
|
||||
const savedFilter = conditionOrSavedFilter;
|
||||
this.setState({
|
||||
filterId: { ...savedFilter, status: 'saved' },
|
||||
});
|
||||
this.setState({ filterConditionsOp: savedFilter.conditionsOp });
|
||||
this.applyFilters([...savedFilter.conditions]);
|
||||
if (cond.conditions) {
|
||||
this.setState({ filterId: { ...cond, status: 'saved' } });
|
||||
this.setState({ conditionsOp: cond.conditionsOp });
|
||||
this.applyFilters([...cond.conditions]);
|
||||
} else {
|
||||
// A condition was passed in.
|
||||
const condition = conditionOrSavedFilter;
|
||||
this.setState({
|
||||
filterId: {
|
||||
...this.state.filterId,
|
||||
status: this.state.filterId && 'changed',
|
||||
},
|
||||
});
|
||||
this.applyFilters([...filterConditions, condition]);
|
||||
this.applyFilters([...filters, cond]);
|
||||
}
|
||||
if (this.state.search !== '') {
|
||||
this.onSearch(this.state.search);
|
||||
@@ -1432,35 +1320,30 @@ class AccountInternal extends PureComponent {
|
||||
|
||||
applyFilters = async conditions => {
|
||||
if (conditions.length > 0) {
|
||||
const customQueryFilters = conditions
|
||||
const customFilters = conditions
|
||||
.filter(cond => !!cond.customName)
|
||||
.map(f => f.queryFilter);
|
||||
const { filters: queryFilters } = await send(
|
||||
'make-filters-from-conditions',
|
||||
{
|
||||
conditions: conditions.filter(cond => !cond.customName),
|
||||
},
|
||||
.map(f => f.filter);
|
||||
const { filters } = await send('make-filters-from-conditions', {
|
||||
conditions: conditions.filter(cond => !cond.customName),
|
||||
});
|
||||
const conditionsOpKey = this.state.conditionsOp === 'or' ? '$or' : '$and';
|
||||
this.filteredAmount = await this.getFilteredAmount(
|
||||
filters,
|
||||
conditionsOpKey,
|
||||
);
|
||||
const conditionsOpKey =
|
||||
this.state.filterConditionsOp === 'or' ? '$or' : '$and';
|
||||
this.currentQuery = this.rootQuery.filter({
|
||||
[conditionsOpKey]: [...queryFilters, ...customQueryFilters],
|
||||
[conditionsOpKey]: [...filters, ...customFilters],
|
||||
});
|
||||
|
||||
this.setState(
|
||||
{
|
||||
filterConditions: conditions,
|
||||
},
|
||||
() => {
|
||||
this.updateQuery(this.currentQuery, true);
|
||||
},
|
||||
);
|
||||
this.setState({ filters: conditions }, () => {
|
||||
this.updateQuery(this.currentQuery, true);
|
||||
});
|
||||
} else {
|
||||
this.setState(
|
||||
{
|
||||
transactions: [],
|
||||
transactionCount: 0,
|
||||
filterConditions: conditions,
|
||||
filters: conditions,
|
||||
},
|
||||
() => {
|
||||
this.fetchTransactions();
|
||||
@@ -1474,8 +1357,8 @@ class AccountInternal extends PureComponent {
|
||||
};
|
||||
|
||||
applySort = (field, ascDesc, prevField, prevAscDesc) => {
|
||||
const filterConditions = this.state.filterConditions;
|
||||
const isFiltered = filterConditions.length > 0;
|
||||
const filters = this.state.filters;
|
||||
const isFiltered = filters.length > 0;
|
||||
const sortField = getField(!field ? this.state.sort.field : field);
|
||||
const sortAscDesc = !ascDesc ? this.state.sort.ascDesc : ascDesc;
|
||||
const sortPrevField = getField(
|
||||
@@ -1542,7 +1425,7 @@ class AccountInternal extends PureComponent {
|
||||
// called directly from UI by sorting a column.
|
||||
// active filters need to be applied before sorting
|
||||
case isFiltered:
|
||||
this.applyFilters([...filterConditions]);
|
||||
this.applyFilters([...filters]);
|
||||
sortCurrentQuery(this, sortField, sortAscDesc);
|
||||
break;
|
||||
|
||||
@@ -1604,6 +1487,7 @@ class AccountInternal extends PureComponent {
|
||||
addNotification,
|
||||
accountsSyncing,
|
||||
failedAccounts,
|
||||
pushModal,
|
||||
replaceModal,
|
||||
showExtraBalances,
|
||||
accountId,
|
||||
@@ -1621,7 +1505,6 @@ class AccountInternal extends PureComponent {
|
||||
balances,
|
||||
showCleared,
|
||||
showReconciled,
|
||||
filteredAmount,
|
||||
} = this.state;
|
||||
|
||||
const account = accounts.find(account => account.id === accountId);
|
||||
@@ -1665,13 +1548,14 @@ class AccountInternal extends PureComponent {
|
||||
>
|
||||
<View style={styles.page}>
|
||||
<AccountHeader
|
||||
filteredAmount={this.filteredAmount}
|
||||
tableRef={this.table}
|
||||
editingName={editingName}
|
||||
isNameEditable={isNameEditable}
|
||||
workingHard={workingHard}
|
||||
account={account}
|
||||
filterId={filterId}
|
||||
savedFilters={this.props.savedFilters}
|
||||
filtersList={this.props.filtersList}
|
||||
location={this.props.location}
|
||||
accountName={accountName}
|
||||
accountsSyncing={accountsSyncing}
|
||||
@@ -1685,13 +1569,11 @@ class AccountInternal extends PureComponent {
|
||||
showEmptyMessage={showEmptyMessage}
|
||||
balanceQuery={balanceQuery}
|
||||
canCalculateBalance={this.canCalculateBalance}
|
||||
filteredAmount={filteredAmount}
|
||||
isFiltered={transactionsFiltered}
|
||||
isSorted={this.state.sort.length !== 0}
|
||||
reconcileAmount={reconcileAmount}
|
||||
search={this.state.search}
|
||||
filterConditions={this.state.filterConditions}
|
||||
filterConditionsOp={this.state.filterConditionsOp}
|
||||
filters={this.state.filters}
|
||||
conditionsOp={this.state.conditionsOp}
|
||||
savePrefs={this.props.savePrefs}
|
||||
pushModal={this.props.pushModal}
|
||||
onSearch={this.onSearch}
|
||||
@@ -1716,13 +1598,11 @@ class AccountInternal extends PureComponent {
|
||||
onUpdateFilter={this.onUpdateFilter}
|
||||
onClearFilters={this.onClearFilters}
|
||||
onReloadSavedFilter={this.onReloadSavedFilter}
|
||||
onConditionsOpChange={this.onConditionsOpChange}
|
||||
onCondOpChange={this.onCondOpChange}
|
||||
onDeleteFilter={this.onDeleteFilter}
|
||||
onApplyFilter={this.onApplyFilter}
|
||||
onScheduleAction={this.onScheduleAction}
|
||||
onSetTransfer={this.onSetTransfer}
|
||||
onMakeAsSplitTransaction={this.onMakeAsSplitTransaction}
|
||||
onMakeAsNonSplitTransactions={this.onMakeAsNonSplitTransactions}
|
||||
/>
|
||||
|
||||
<View style={{ flex: 1 }}>
|
||||
@@ -1751,7 +1631,9 @@ class AccountInternal extends PureComponent {
|
||||
isAdding={this.state.isAdding}
|
||||
isNew={this.isNew}
|
||||
isMatched={this.isMatched}
|
||||
isFiltered={transactionsFiltered}
|
||||
isFiltered={
|
||||
this.state.search !== '' || this.state.filters.length > 0
|
||||
}
|
||||
dateFormat={dateFormat}
|
||||
hideFraction={hideFraction}
|
||||
addNotification={addNotification}
|
||||
@@ -1771,6 +1653,7 @@ class AccountInternal extends PureComponent {
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
pushModal={pushModal}
|
||||
onSort={this.onSort}
|
||||
sortField={this.state.sort.field}
|
||||
ascDesc={this.state.sort.ascDesc}
|
||||
@@ -1786,7 +1669,6 @@ class AccountInternal extends PureComponent {
|
||||
this.setState({ isAdding: false })
|
||||
}
|
||||
onCreatePayee={this.onCreatePayee}
|
||||
onApplyFilter={this.onApplyFilter}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -1799,11 +1681,13 @@ class AccountInternal extends PureComponent {
|
||||
|
||||
function AccountHack(props) {
|
||||
const { dispatch: splitsExpandedDispatch } = useSplitsExpanded();
|
||||
const match = useMatch(props.location.pathname);
|
||||
|
||||
return (
|
||||
<AccountInternal
|
||||
splitsExpandedDispatch={splitsExpandedDispatch}
|
||||
{...props}
|
||||
match={match}
|
||||
splitsExpandedDispatch={splitsExpandedDispatch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1832,10 +1716,36 @@ export function Account() {
|
||||
const modalShowing = useSelector(state => state.modals.modalStack.length > 0);
|
||||
const accountsSyncing = useSelector(state => state.account.accountsSyncing);
|
||||
const lastUndoState = useSelector(state => state.app.lastUndoState);
|
||||
const filterConditions = location?.state?.filterConditions || [];
|
||||
const conditions =
|
||||
location.state && location.state.conditions
|
||||
? location.state.conditions
|
||||
: [];
|
||||
|
||||
const savedFiters = useFilters();
|
||||
const actionCreators = useActions();
|
||||
const state = {
|
||||
newTransactions,
|
||||
matchedTransactions,
|
||||
accounts,
|
||||
failedAccounts,
|
||||
dateFormat,
|
||||
hideFraction,
|
||||
expandSplits,
|
||||
showBalances,
|
||||
showCleared: !hideCleared,
|
||||
showReconciled: !hideReconciled,
|
||||
showExtraBalances,
|
||||
payees,
|
||||
modalShowing,
|
||||
accountsSyncing,
|
||||
lastUndoState,
|
||||
conditions,
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const filtersList = useFilters();
|
||||
const actionCreators = useMemo(
|
||||
() => bindActionCreators(actions, dispatch),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const transform = useMemo(() => {
|
||||
const filterByAccount = queries.getAccountFilter(params.id, '_account');
|
||||
@@ -1864,31 +1774,17 @@ export function Account() {
|
||||
return (
|
||||
<SchedulesProvider transform={transform}>
|
||||
<SplitsExpandedProvider
|
||||
initialMode={expandSplits ? 'collapse' : 'expand'}
|
||||
initialMode={state.expandSplits ? 'collapse' : 'expand'}
|
||||
>
|
||||
<AccountHack
|
||||
newTransactions={newTransactions}
|
||||
matchedTransactions={matchedTransactions}
|
||||
accounts={accounts}
|
||||
failedAccounts={failedAccounts}
|
||||
dateFormat={dateFormat}
|
||||
hideFraction={hideFraction}
|
||||
expandSplits={expandSplits}
|
||||
showBalances={showBalances}
|
||||
showCleared={!hideCleared}
|
||||
showReconciled={!hideReconciled}
|
||||
showExtraBalances={showExtraBalances}
|
||||
payees={payees}
|
||||
modalShowing={modalShowing}
|
||||
accountsSyncing={accountsSyncing}
|
||||
lastUndoState={lastUndoState}
|
||||
filterConditions={filterConditions}
|
||||
{...state}
|
||||
categoryGroups={categoryGroups}
|
||||
{...actionCreators}
|
||||
modalShowing={state.modalShowing}
|
||||
accountId={params.id}
|
||||
categoryId={location?.state?.categoryId}
|
||||
location={location}
|
||||
savedFilters={savedFiters}
|
||||
filtersList={filtersList}
|
||||
/>
|
||||
</SplitsExpandedProvider>
|
||||
</SchedulesProvider>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useAccounts } from '../../hooks/useAccounts';
|
||||
import { useActions } from '../../hooks/useActions';
|
||||
import { SvgExclamationOutline } from '../../icons/v1';
|
||||
import { theme } from '../../style';
|
||||
import { Button } from '../common/Button2';
|
||||
import { Button } from '../common/Button';
|
||||
import { Link } from '../common/Link';
|
||||
import { Popover } from '../common/Popover';
|
||||
import { View } from '../common/View';
|
||||
@@ -94,7 +94,7 @@ export function AccountSyncCheck() {
|
||||
<View>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
variant="bare"
|
||||
type="bare"
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -103,7 +103,7 @@ export function AccountSyncCheck() {
|
||||
padding: '4px 8px',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
onPress={() => setOpen(true)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<SvgExclamationOutline
|
||||
style={{ width: 14, height: 14, marginRight: 5 }}
|
||||
@@ -129,17 +129,13 @@ export function AccountSyncCheck() {
|
||||
<View style={{ justifyContent: 'flex-end', flexDirection: 'row' }}>
|
||||
{showAuth ? (
|
||||
<>
|
||||
<Button onPress={unlink}>Unlink</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onPress={reauth}
|
||||
style={{ marginLeft: 5 }}
|
||||
>
|
||||
<Button onClick={unlink}>Unlink</Button>
|
||||
<Button type="primary" onClick={reauth} style={{ marginLeft: 5 }}>
|
||||
Reauthorize
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button onPress={unlink}>Unlink account</Button>
|
||||
<Button onClick={unlink}>Unlink account</Button>
|
||||
)}
|
||||
</View>
|
||||
</Popover>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { useHover } from 'usehooks-ts';
|
||||
import React from 'react';
|
||||
|
||||
import { isPreviewId } from 'loot-core/shared/transactions';
|
||||
import { useCachedSchedules } from 'loot-core/src/client/data-hooks/schedules';
|
||||
@@ -10,7 +8,7 @@ import { getScheduledAmount } from 'loot-core/src/shared/schedules';
|
||||
import { useSelectedItems } from '../../hooks/useSelected';
|
||||
import { SvgArrowButtonRight1 } from '../../icons/v2';
|
||||
import { theme } from '../../style';
|
||||
import { Button } from '../common/Button2';
|
||||
import { Button } from '../common/Button';
|
||||
import { Text } from '../common/Text';
|
||||
import { View } from '../common/View';
|
||||
import { PrivacyFilter } from '../PrivacyFilter';
|
||||
@@ -139,12 +137,10 @@ export function Balances({
|
||||
showExtraBalances,
|
||||
onToggleExtraBalances,
|
||||
account,
|
||||
isFiltered,
|
||||
filteredItems,
|
||||
filteredAmount,
|
||||
}) {
|
||||
const selectedItems = useSelectedItems();
|
||||
const buttonRef = useRef(null);
|
||||
const isButtonHovered = useHover(buttonRef);
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -156,11 +152,14 @@ export function Balances({
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
data-testid="account-balance"
|
||||
variant="bare"
|
||||
onPress={onToggleExtraBalances}
|
||||
type="bare"
|
||||
onClick={onToggleExtraBalances}
|
||||
style={{
|
||||
'& svg': {
|
||||
opacity: selectedItems.size > 0 || showExtraBalances ? 1 : 0,
|
||||
},
|
||||
'&:hover svg': { opacity: 1 },
|
||||
paddingTop: 1,
|
||||
paddingBottom: 1,
|
||||
}}
|
||||
@@ -189,10 +188,6 @@ export function Balances({
|
||||
marginLeft: 10,
|
||||
color: theme.pillText,
|
||||
transform: showExtraBalances ? 'rotateZ(180deg)' : 'rotateZ(0)',
|
||||
opacity:
|
||||
isButtonHovered || selectedItems.size > 0 || showExtraBalances
|
||||
? 1
|
||||
: 0,
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
@@ -201,7 +196,9 @@ export function Balances({
|
||||
{selectedItems.size > 0 && (
|
||||
<SelectedBalance selectedItems={selectedItems} account={account} />
|
||||
)}
|
||||
{isFiltered && <FilteredBalance filteredAmount={filteredAmount} />}
|
||||
{filteredItems.length > 0 && (
|
||||
<FilteredBalance filteredAmount={filteredAmount} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from '../../icons/v2';
|
||||
import { theme, styles } from '../../style';
|
||||
import { AnimatedRefresh } from '../AnimatedRefresh';
|
||||
import { Button } from '../common/Button2';
|
||||
import { Button } from '../common/Button';
|
||||
import { InitialFocus } from '../common/InitialFocus';
|
||||
import { Input } from '../common/Input';
|
||||
import { Menu } from '../common/Menu';
|
||||
@@ -26,12 +26,13 @@ import { View } from '../common/View';
|
||||
import { FilterButton } from '../filters/FiltersMenu';
|
||||
import { FiltersStack } from '../filters/FiltersStack';
|
||||
import { NotesButton } from '../NotesButton';
|
||||
import { SelectedTransactionsButton } from '../transactions/SelectedTransactionsButton';
|
||||
import { SelectedTransactionsButton } from '../transactions/SelectedTransactions';
|
||||
|
||||
import { Balances } from './Balance';
|
||||
import { ReconcilingMessage, ReconcileMenu } from './Reconcile';
|
||||
|
||||
export function AccountHeader({
|
||||
filteredAmount,
|
||||
tableRef,
|
||||
editingName,
|
||||
isNameEditable,
|
||||
@@ -39,7 +40,7 @@ export function AccountHeader({
|
||||
accountName,
|
||||
account,
|
||||
filterId,
|
||||
savedFilters,
|
||||
filtersList,
|
||||
accountsSyncing,
|
||||
failedAccounts,
|
||||
accounts,
|
||||
@@ -52,12 +53,10 @@ export function AccountHeader({
|
||||
balanceQuery,
|
||||
reconcileAmount,
|
||||
canCalculateBalance,
|
||||
isFiltered,
|
||||
filteredAmount,
|
||||
isSorted,
|
||||
search,
|
||||
filterConditions,
|
||||
filterConditionsOp,
|
||||
filters,
|
||||
conditionsOp,
|
||||
pushModal,
|
||||
onSearch,
|
||||
onAddTransaction,
|
||||
@@ -80,12 +79,10 @@ export function AccountHeader({
|
||||
onUpdateFilter,
|
||||
onClearFilters,
|
||||
onReloadSavedFilter,
|
||||
onConditionsOpChange,
|
||||
onCondOpChange,
|
||||
onDeleteFilter,
|
||||
onScheduleAction,
|
||||
onSetTransfer,
|
||||
onMakeAsSplitTransaction,
|
||||
onMakeAsNonSplitTransactions,
|
||||
}) {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const searchInput = useRef(null);
|
||||
@@ -214,10 +211,10 @@ export function AccountHeader({
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="bare"
|
||||
type="bare"
|
||||
aria-label="Edit account name"
|
||||
className="hover-visible"
|
||||
onPress={() => onExposeName(true)}
|
||||
onClick={() => onExposeName(true)}
|
||||
>
|
||||
<SvgPencil1
|
||||
style={{
|
||||
@@ -246,7 +243,7 @@ export function AccountHeader({
|
||||
showExtraBalances={showExtraBalances}
|
||||
onToggleExtraBalances={onToggleExtraBalances}
|
||||
account={account}
|
||||
isFiltered={isFiltered}
|
||||
filteredItems={filters}
|
||||
filteredAmount={filteredAmount}
|
||||
/>
|
||||
|
||||
@@ -258,9 +255,9 @@ export function AccountHeader({
|
||||
>
|
||||
{((account && !account.closed) || canSync) && (
|
||||
<Button
|
||||
variant="bare"
|
||||
onPress={canSync ? onSync : onImport}
|
||||
isDisabled={canSync && isServerOffline}
|
||||
type="bare"
|
||||
onClick={canSync ? onSync : onImport}
|
||||
disabled={canSync && isServerOffline}
|
||||
>
|
||||
{canSync ? (
|
||||
<>
|
||||
@@ -289,7 +286,7 @@ export function AccountHeader({
|
||||
</Button>
|
||||
)}
|
||||
{!showEmptyMessage && (
|
||||
<Button variant="bare" onPress={onAddTransaction}>
|
||||
<Button type="bare" onClick={onAddTransaction}>
|
||||
<SvgAdd width={10} height={10} style={{ marginRight: 3 }} /> Add
|
||||
New
|
||||
</Button>
|
||||
@@ -321,34 +318,24 @@ export function AccountHeader({
|
||||
onScheduleAction={onScheduleAction}
|
||||
pushModal={pushModal}
|
||||
showMakeTransfer={showMakeTransfer}
|
||||
onMakeAsSplitTransaction={onMakeAsSplitTransaction}
|
||||
onMakeAsNonSplitTransactions={onMakeAsNonSplitTransactions}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="bare"
|
||||
aria-label={
|
||||
type="bare"
|
||||
disabled={search !== '' || filters.length > 0}
|
||||
style={{ padding: 6, marginLeft: 10 }}
|
||||
onClick={onToggleSplits}
|
||||
title={
|
||||
splitsExpanded.state.mode === 'collapse'
|
||||
? 'Collapse split transactions'
|
||||
: 'Expand split transactions'
|
||||
}
|
||||
isDisabled={search !== '' || filterConditions.length > 0}
|
||||
style={{ padding: 6, marginLeft: 10 }}
|
||||
onPress={onToggleSplits}
|
||||
>
|
||||
<View
|
||||
title={
|
||||
splitsExpanded.state.mode === 'collapse'
|
||||
? 'Collapse split transactions'
|
||||
: 'Expand split transactions'
|
||||
}
|
||||
>
|
||||
{splitsExpanded.state.mode === 'collapse' ? (
|
||||
<SvgArrowsShrink3 style={{ width: 14, height: 14 }} />
|
||||
) : (
|
||||
<SvgArrowsExpand3 style={{ width: 14, height: 14 }} />
|
||||
)}
|
||||
</View>
|
||||
{splitsExpanded.state.mode === 'collapse' ? (
|
||||
<SvgArrowsShrink3 style={{ width: 14, height: 14 }} />
|
||||
) : (
|
||||
<SvgArrowsExpand3 style={{ width: 14, height: 14 }} />
|
||||
)}
|
||||
</Button>
|
||||
{account ? (
|
||||
<View>
|
||||
@@ -404,17 +391,17 @@ export function AccountHeader({
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{filterConditions?.length > 0 && (
|
||||
{filters && filters.length > 0 && (
|
||||
<FiltersStack
|
||||
conditions={filterConditions}
|
||||
conditionsOp={filterConditionsOp}
|
||||
filters={filters}
|
||||
conditionsOp={conditionsOp}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onDeleteFilter={onDeleteFilter}
|
||||
onClearFilters={onClearFilters}
|
||||
onReloadSavedFilter={onReloadSavedFilter}
|
||||
filterId={filterId}
|
||||
savedFilters={savedFilters}
|
||||
onConditionsOpChange={onConditionsOpChange}
|
||||
filtersList={filtersList}
|
||||
onCondOpChange={onCondOpChange}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import * as queries from 'loot-core/src/client/queries';
|
||||
import { currencyToInteger } from 'loot-core/src/shared/util';
|
||||
|
||||
import { SvgCheckCircle1 } from '../../icons/v2';
|
||||
import { styles, theme } from '../../style';
|
||||
import { Button } from '../common/Button2';
|
||||
import { Button } from '../common/Button';
|
||||
import { InitialFocus } from '../common/InitialFocus';
|
||||
import { Input } from '../common/Input';
|
||||
import { Text } from '../common/Text';
|
||||
@@ -78,13 +78,13 @@ export function ReconcilingMessage({
|
||||
</View>
|
||||
)}
|
||||
<View style={{ marginLeft: 15 }}>
|
||||
<Button variant="primary" onPress={onDone}>
|
||||
<Button type="primary" onClick={onDone}>
|
||||
Done Reconciling
|
||||
</Button>
|
||||
</View>
|
||||
{targetDiff !== 0 && (
|
||||
<View style={{ marginLeft: 15 }}>
|
||||
<Button onPress={() => onCreateTransaction(targetDiff)}>
|
||||
<Button onClick={() => onCreateTransaction(targetDiff)}>
|
||||
Create Reconciliation Transaction
|
||||
</Button>
|
||||
</View>
|
||||
@@ -102,20 +102,17 @@ export function ReconcileMenu({ account, onReconcile, onClose }) {
|
||||
query: balanceQuery.query.filter({ cleared: true }),
|
||||
});
|
||||
const format = useFormat();
|
||||
const [inputValue, setInputValue] = useState(null);
|
||||
const [inputFocused, setInputFocused] = useState(false);
|
||||
|
||||
function onSubmit() {
|
||||
if (inputValue === '') {
|
||||
setInputFocused(true);
|
||||
return;
|
||||
function onSubmit(e) {
|
||||
e.preventDefault();
|
||||
const input = e.target.elements[0];
|
||||
const amount = currencyToInteger(input.value);
|
||||
if (amount != null) {
|
||||
onReconcile(amount == null ? clearedBalance : amount);
|
||||
onClose();
|
||||
} else {
|
||||
input.select();
|
||||
}
|
||||
|
||||
const amount =
|
||||
inputValue != null ? currencyToInteger(inputValue) : clearedBalance;
|
||||
|
||||
onReconcile(amount);
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -124,20 +121,17 @@ export function ReconcileMenu({ account, onReconcile, onClose }) {
|
||||
Enter the current balance of your bank account that you want to
|
||||
reconcile with:
|
||||
</Text>
|
||||
{clearedBalance != null && (
|
||||
<InitialFocus>
|
||||
<Input
|
||||
defaultValue={format(clearedBalance, 'financial')}
|
||||
onChangeValue={setInputValue}
|
||||
style={{ margin: '7px 0' }}
|
||||
focused={inputFocused}
|
||||
onEnter={onSubmit}
|
||||
/>
|
||||
</InitialFocus>
|
||||
)}
|
||||
<Button variant="primary" onPress={onSubmit}>
|
||||
Reconcile
|
||||
</Button>
|
||||
<form onSubmit={onSubmit}>
|
||||
{clearedBalance != null && (
|
||||
<InitialFocus>
|
||||
<Input
|
||||
defaultValue={format(clearedBalance, 'financial')}
|
||||
style={{ margin: '7px 0' }}
|
||||
/>
|
||||
</InitialFocus>
|
||||
)}
|
||||
<Button type="primary">Reconcile</Button>
|
||||
</form>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
@@ -11,51 +12,24 @@ import { useSheetValue } from '../spreadsheet/useSheetValue';
|
||||
|
||||
import { makeBalanceAmountStyle } from './util';
|
||||
|
||||
type CarryoverIndicatorProps = {
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
type BalanceWithCarryoverProps = Omit<
|
||||
ComponentPropsWithoutRef<typeof CellValue>,
|
||||
'binding'
|
||||
> & {
|
||||
carryover: Binding;
|
||||
balance: Binding;
|
||||
goal: Binding;
|
||||
budgeted: Binding;
|
||||
goal?: Binding;
|
||||
budgeted?: Binding;
|
||||
disabled?: boolean;
|
||||
carryoverIndicator?: ({ style }: CarryoverIndicatorProps) => JSX.Element;
|
||||
carryoverStyle?: CSSProperties;
|
||||
};
|
||||
|
||||
export function DefaultCarryoverIndicator({ style }: CarryoverIndicatorProps) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 2,
|
||||
position: 'absolute',
|
||||
right: '-4px',
|
||||
alignSelf: 'center',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<SvgArrowThinRight
|
||||
width={style?.width || 7}
|
||||
height={style?.height || 7}
|
||||
style={style}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function BalanceWithCarryover({
|
||||
carryover,
|
||||
balance,
|
||||
goal,
|
||||
budgeted,
|
||||
disabled,
|
||||
carryoverIndicator = DefaultCarryoverIndicator,
|
||||
carryoverStyle,
|
||||
...props
|
||||
}: BalanceWithCarryoverProps) {
|
||||
const carryoverValue = useSheetValue(carryover);
|
||||
@@ -63,21 +37,11 @@ export function BalanceWithCarryover({
|
||||
const goalValue = useSheetValue(goal);
|
||||
const budgetedValue = useSheetValue(budgeted);
|
||||
const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled');
|
||||
const valueStyle = makeBalanceAmountStyle(
|
||||
balanceValue,
|
||||
isGoalTemplatesEnabled ? goalValue : null,
|
||||
budgetedValue,
|
||||
);
|
||||
|
||||
const { isNarrowWidth } = useResponsive();
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'right',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<CellValue
|
||||
{...props}
|
||||
binding={balance}
|
||||
@@ -90,8 +54,6 @@ export function BalanceWithCarryover({
|
||||
)
|
||||
}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
textAlign: 'right',
|
||||
...(!disabled && {
|
||||
cursor: 'pointer',
|
||||
@@ -99,7 +61,30 @@ export function BalanceWithCarryover({
|
||||
...props.style,
|
||||
}}
|
||||
/>
|
||||
{carryoverValue && carryoverIndicator({ style: valueStyle })}
|
||||
</span>
|
||||
{carryoverValue && (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
marginLeft: 2,
|
||||
position: 'absolute',
|
||||
right: isNarrowWidth ? '-8px' : '-4px',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
...carryoverStyle,
|
||||
}}
|
||||
>
|
||||
<SvgArrowThinRight
|
||||
width={carryoverStyle?.width || 7}
|
||||
height={carryoverStyle?.height || 7}
|
||||
style={makeBalanceAmountStyle(
|
||||
balanceValue,
|
||||
goalValue,
|
||||
budgetedValue,
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
import React, { 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';
|
||||
@@ -29,12 +30,38 @@ export function BudgetTable(props) {
|
||||
onBudgetAction,
|
||||
} = 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) => {
|
||||
@@ -201,6 +228,7 @@ export function BudgetTable(props) {
|
||||
collapseAllCategories={collapseAllCategories}
|
||||
/>
|
||||
<View
|
||||
id="scrollableDiv"
|
||||
style={{
|
||||
overflowY: 'scroll',
|
||||
overflowAnchor: 'none',
|
||||
@@ -208,6 +236,7 @@ export function BudgetTable(props) {
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
}}
|
||||
innerRef={scrollableDivRef}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
@@ -228,7 +257,7 @@ export function BudgetTable(props) {
|
||||
onReorderCategory={_onReorderCategory}
|
||||
onReorderGroup={_onReorderGroup}
|
||||
onBudgetAction={onBudgetAction}
|
||||
onShowActivity={onShowActivity}
|
||||
onShowActivity={onShowActivityWithScroll}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -276,8 +276,8 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
dispatch(applyBudgetAction(month, type, args));
|
||||
};
|
||||
|
||||
const onShowActivity = (categoryId, month) => {
|
||||
const filterConditions = [
|
||||
const onShowActivity = (categoryId, month, scrollPosition) => {
|
||||
const conditions = [
|
||||
{ field: 'category', op: 'is', value: categoryId, type: 'id' },
|
||||
{
|
||||
field: 'date',
|
||||
@@ -287,10 +287,16 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
type: 'date',
|
||||
},
|
||||
];
|
||||
|
||||
navigate('/budget', {
|
||||
replace: true,
|
||||
state: { scrollPosition }
|
||||
});
|
||||
|
||||
navigate('/accounts', {
|
||||
state: {
|
||||
goBack: true,
|
||||
filterConditions,
|
||||
conditions,
|
||||
categoryId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -302,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)}
|
||||
@@ -321,6 +323,7 @@ export const CategoryMonth = memo(function CategoryMonth({
|
||||
{!category.is_income && (
|
||||
<Field
|
||||
name="balance"
|
||||
truncate={false}
|
||||
width="flex"
|
||||
style={{ paddingRight: styles.monthRightPadding, textAlign: 'right' }}
|
||||
>
|
||||
|
||||
@@ -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)}
|
||||
@@ -310,6 +312,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
|
||||
</Field>
|
||||
<Field
|
||||
name="balance"
|
||||
truncate={false}
|
||||
width="flex"
|
||||
style={{ paddingRight: styles.monthRightPadding, textAlign: 'right' }}
|
||||
>
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
import React, { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
||||
import {
|
||||
Button as ReactAriaButton,
|
||||
type ButtonProps as ReactAriaButtonProps,
|
||||
} from 'react-aria-components';
|
||||
|
||||
import { AnimatedLoading } from '../../icons/AnimatedLoading';
|
||||
import { type CSSProperties, styles, theme } from '../../style';
|
||||
|
||||
import { View } from './View';
|
||||
|
||||
const backgroundColor: {
|
||||
[key in ButtonVariant | `${ButtonVariant}Disabled`]?: string;
|
||||
} = {
|
||||
normal: theme.buttonNormalBackground,
|
||||
normalDisabled: theme.buttonNormalDisabledBackground,
|
||||
primary: theme.buttonPrimaryBackground,
|
||||
primaryDisabled: theme.buttonPrimaryDisabledBackground,
|
||||
bare: theme.buttonBareBackground,
|
||||
bareDisabled: theme.buttonBareDisabledBackground,
|
||||
menu: theme.buttonMenuBackground,
|
||||
menuSelected: theme.buttonMenuSelectedBackground,
|
||||
};
|
||||
|
||||
const backgroundColorHover: Record<
|
||||
ButtonVariant | `${ButtonVariant}Disabled`,
|
||||
CSSProperties['backgroundColor']
|
||||
> = {
|
||||
normal: theme.buttonNormalBackgroundHover,
|
||||
primary: theme.buttonPrimaryBackgroundHover,
|
||||
bare: theme.buttonBareBackgroundHover,
|
||||
menu: theme.buttonMenuBackgroundHover,
|
||||
menuSelected: theme.buttonMenuSelectedBackgroundHover,
|
||||
normalDisabled: 'transparent',
|
||||
primaryDisabled: 'transparent',
|
||||
bareDisabled: 'transparent',
|
||||
menuDisabled: 'transparent',
|
||||
menuSelectedDisabled: 'transparent',
|
||||
};
|
||||
|
||||
const borderColor: {
|
||||
[key in
|
||||
| ButtonVariant
|
||||
| `${ButtonVariant}Disabled`]?: CSSProperties['borderColor'];
|
||||
} = {
|
||||
normal: theme.buttonNormalBorder,
|
||||
normalDisabled: theme.buttonNormalDisabledBorder,
|
||||
primary: theme.buttonPrimaryBorder,
|
||||
primaryDisabled: theme.buttonPrimaryDisabledBorder,
|
||||
menu: theme.buttonMenuBorder,
|
||||
menuSelected: theme.buttonMenuSelectedBorder,
|
||||
};
|
||||
|
||||
const textColor: {
|
||||
[key in ButtonVariant | `${ButtonVariant}Disabled`]?: CSSProperties['color'];
|
||||
} = {
|
||||
normal: theme.buttonNormalText,
|
||||
normalDisabled: theme.buttonNormalDisabledText,
|
||||
primary: theme.buttonPrimaryText,
|
||||
primaryDisabled: theme.buttonPrimaryDisabledText,
|
||||
bare: theme.buttonBareText,
|
||||
bareDisabled: theme.buttonBareDisabledText,
|
||||
menu: theme.buttonMenuText,
|
||||
menuSelected: theme.buttonMenuSelectedText,
|
||||
};
|
||||
|
||||
const textColorHover: {
|
||||
[key in ButtonVariant]?: string;
|
||||
} = {
|
||||
normal: theme.buttonNormalTextHover,
|
||||
primary: theme.buttonPrimaryTextHover,
|
||||
bare: theme.buttonBareTextHover,
|
||||
menu: theme.buttonMenuTextHover,
|
||||
menuSelected: theme.buttonMenuSelectedTextHover,
|
||||
};
|
||||
|
||||
const _getBorder = (
|
||||
variant: ButtonVariant,
|
||||
variantWithDisabled: keyof typeof borderColor,
|
||||
): string => {
|
||||
switch (variant) {
|
||||
case 'bare':
|
||||
return 'none';
|
||||
|
||||
default:
|
||||
return '1px solid ' + borderColor[variantWithDisabled];
|
||||
}
|
||||
};
|
||||
|
||||
const _getPadding = (variant: ButtonVariant): string => {
|
||||
switch (variant) {
|
||||
case 'bare':
|
||||
return '5px';
|
||||
default:
|
||||
return '5px 10px';
|
||||
}
|
||||
};
|
||||
|
||||
const _getActiveStyles = (
|
||||
variant: ButtonVariant,
|
||||
bounce: boolean,
|
||||
): CSSProperties => {
|
||||
switch (variant) {
|
||||
case 'bare':
|
||||
return { backgroundColor: theme.buttonBareBackgroundActive };
|
||||
default:
|
||||
return {
|
||||
transform: bounce ? 'translateY(1px)' : undefined,
|
||||
boxShadow: `0 1px 4px 0 ${
|
||||
variant === 'primary'
|
||||
? theme.buttonPrimaryShadow
|
||||
: theme.buttonNormalShadow
|
||||
}`,
|
||||
transition: 'none',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
type ButtonProps = ComponentPropsWithoutRef<typeof ReactAriaButton> & {
|
||||
variant?: ButtonVariant;
|
||||
bounce?: boolean;
|
||||
};
|
||||
|
||||
type ButtonVariant = 'normal' | 'primary' | 'bare' | 'menu' | 'menuSelected';
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
children,
|
||||
variant = 'normal',
|
||||
bounce = true,
|
||||
style,
|
||||
isDisabled,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const variantWithDisabled: ButtonVariant | `${ButtonVariant}Disabled` =
|
||||
isDisabled ? `${variant}Disabled` : variant;
|
||||
|
||||
const hoveredStyle = {
|
||||
...(variant !== 'bare' && styles.shadow),
|
||||
backgroundColor: backgroundColorHover[variant],
|
||||
color: textColorHover[variant],
|
||||
};
|
||||
const pressedStyle = {
|
||||
..._getActiveStyles(variant, bounce),
|
||||
};
|
||||
|
||||
const buttonStyle: ComponentPropsWithoutRef<
|
||||
typeof Button
|
||||
>['style'] = props => ({
|
||||
...props.defaultStyle,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
padding: _getPadding(variant),
|
||||
margin: 0,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
borderRadius: 4,
|
||||
backgroundColor: backgroundColor[variantWithDisabled],
|
||||
border: _getBorder(variant, variantWithDisabled),
|
||||
color: textColor[variantWithDisabled],
|
||||
transition: 'box-shadow .25s',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
...styles.smallText,
|
||||
...(props.isHovered && !isDisabled ? hoveredStyle : {}),
|
||||
...(props.isPressed && !isDisabled ? pressedStyle : {}),
|
||||
...(typeof style === 'function' ? style(props) : style),
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactAriaButton ref={ref} style={buttonStyle} {...restProps}>
|
||||
{children}
|
||||
</ReactAriaButton>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
type ButtonWithLoadingProps = ButtonProps & {
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export const ButtonWithLoading = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ButtonWithLoadingProps
|
||||
>((props, ref) => {
|
||||
const { isLoading, children, ...buttonProps } = props;
|
||||
return (
|
||||
<Button
|
||||
{...buttonProps}
|
||||
ref={ref}
|
||||
style={{ position: 'relative', ...buttonProps.style }}
|
||||
>
|
||||
{renderProps => (
|
||||
<>
|
||||
{isLoading && (
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<AnimatedLoading style={{ width: 20, height: 20 }} />
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
opacity: isLoading ? 0 : 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{typeof children === 'function' ? children(renderProps) : children}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
ButtonWithLoading.displayName = 'ButtonWithLoading';
|
||||
@@ -23,6 +23,14 @@ export const Popover = ({
|
||||
...style,
|
||||
})}`}
|
||||
shouldCloseOnInteractOutside={element => {
|
||||
// Disable closing the popover when a reach listbox is clicked (Select component)
|
||||
if (
|
||||
element.getAttribute('data-reach-listbox-list') !== null ||
|
||||
element.getAttribute('data-reach-listbox-option') !== null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shouldCloseOnInteractOutside) {
|
||||
return shouldCloseOnInteractOutside(element);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import {
|
||||
ListboxInput,
|
||||
ListboxButton,
|
||||
ListboxPopover,
|
||||
ListboxList,
|
||||
ListboxOption,
|
||||
} from '@reach/listbox';
|
||||
import { css } from 'glamor';
|
||||
|
||||
import { SvgExpandArrow } from '../../icons/v0';
|
||||
import { type CSSProperties } from '../../style';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { Menu } from './Menu';
|
||||
import { Popover } from './Popover';
|
||||
import { View } from './View';
|
||||
|
||||
function isValueOption<Value extends string>(
|
||||
option: [Value, string] | typeof Menu.line,
|
||||
): option is [Value, string] {
|
||||
return option !== Menu.line;
|
||||
}
|
||||
import { theme, styles, type CSSProperties } from '../../style';
|
||||
|
||||
type SelectProps<Value extends string> = {
|
||||
bare?: boolean;
|
||||
options: Array<[Value, string] | typeof Menu.line>;
|
||||
options: Array<[Value, string]>;
|
||||
value: Value;
|
||||
defaultLabel?: string;
|
||||
onChange?: (newValue: Value) => void;
|
||||
style?: CSSProperties;
|
||||
wrapperStyle?: CSSProperties;
|
||||
line?: number;
|
||||
disabled?: boolean;
|
||||
disabledKeys?: Value[];
|
||||
buttonStyle?: CSSProperties;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -31,6 +29,7 @@ type SelectProps<Value extends string> = {
|
||||
* @param {string} [defaultLabel] - The label to display when the selected value is not in the options.
|
||||
* @param {function} [onChange] - A callback function invoked when the selected value changes.
|
||||
* @param {CSSProperties} [style] - Custom styles to apply to the selected button.
|
||||
* @param {CSSProperties} [wrapperStyle] - Custom style to apply to the select wrapper.
|
||||
* @param {string[]} [disabledKeys] - An array of option values to disable.
|
||||
*
|
||||
* @example
|
||||
@@ -38,96 +37,128 @@ type SelectProps<Value extends string> = {
|
||||
* // <Select options={[['1', 'Option 1'], ['2', 'Option 2']]} value="1" onChange={handleOnChange} />
|
||||
* // <Select options={[['1', 'Option 1'], ['2', 'Option 2']]} value="3" defaultLabel="Select an option" onChange={handleOnChange} />
|
||||
*/
|
||||
|
||||
export function Select<Value extends string>({
|
||||
bare,
|
||||
options,
|
||||
value,
|
||||
defaultLabel = '',
|
||||
onChange,
|
||||
disabled = false,
|
||||
style,
|
||||
wrapperStyle,
|
||||
line,
|
||||
disabled,
|
||||
disabledKeys = [],
|
||||
buttonStyle = {},
|
||||
}: SelectProps<Value>) {
|
||||
const targetOption = options
|
||||
.filter(isValueOption)
|
||||
.find(option => option[0] === value);
|
||||
|
||||
const triggerRef = useRef(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const arrowSize = 7;
|
||||
const minHeight = style?.minHeight ? style.minHeight : '18px';
|
||||
const targetOption = options.filter(option => option[0] === value);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
type={bare ? 'bare' : 'normal'}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
style={buttonStyle}
|
||||
hoveredStyle={{
|
||||
backgroundColor: bare ? 'transparent' : undefined,
|
||||
...buttonStyle,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 5,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: 'calc(100% - 7px)',
|
||||
}}
|
||||
>
|
||||
{targetOption ? targetOption[1] : defaultLabel}
|
||||
</span>
|
||||
<ListboxInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
color: bare ? 'inherit' : theme.formInputText,
|
||||
backgroundColor: bare ? 'transparent' : theme.cardBackground,
|
||||
borderRadius: styles.menuBorderRadius,
|
||||
border: bare ? 'none' : '1px solid ' + theme.formInputBorder,
|
||||
lineHeight: '1em',
|
||||
...wrapperStyle,
|
||||
}}
|
||||
>
|
||||
<ListboxButton
|
||||
className={`${css([
|
||||
{ borderWidth: 0, padding: 5, borderRadius: 4 },
|
||||
style,
|
||||
])}`}
|
||||
arrow={
|
||||
<SvgExpandArrow
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
width: arrowSize,
|
||||
height: arrowSize,
|
||||
color: 'inherit',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</Button>
|
||||
|
||||
<Popover
|
||||
triggerRef={triggerRef}
|
||||
placement="bottom start"
|
||||
isOpen={isOpen}
|
||||
onOpenChange={() => setIsOpen(false)}
|
||||
}
|
||||
>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
onChange?.(item);
|
||||
setIsOpen(false);
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: `calc(100% - ${arrowSize + 5}px)`,
|
||||
alignItems: 'center',
|
||||
minHeight,
|
||||
}}
|
||||
items={options.map(item =>
|
||||
item === Menu.line
|
||||
? Menu.line
|
||||
: {
|
||||
name: item[0],
|
||||
text: item[1],
|
||||
disabled: disabledKeys.includes(item[0]),
|
||||
},
|
||||
)}
|
||||
getItemStyle={option => {
|
||||
if (targetOption && targetOption[0] === option.name) {
|
||||
return { fontWeight: 'bold' };
|
||||
}
|
||||
return {};
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
>
|
||||
{targetOption.length !== 0 ? targetOption[0][1] : defaultLabel}
|
||||
</span>
|
||||
</ListboxButton>
|
||||
<ListboxPopover
|
||||
style={{
|
||||
zIndex: 100000,
|
||||
outline: 0,
|
||||
borderRadius: styles.menuBorderRadius,
|
||||
backgroundColor: theme.menuBackground,
|
||||
color: theme.menuItemText,
|
||||
boxShadow: styles.cardShadow,
|
||||
border: '1px solid ' + theme.menuBorder,
|
||||
}}
|
||||
className={`${css({
|
||||
'[data-reach-listbox-option]': {
|
||||
background: theme.menuItemBackground,
|
||||
color: theme.menuItemText,
|
||||
},
|
||||
'[data-reach-listbox-option][data-current-nav]': {
|
||||
background: theme.menuItemBackgroundHover,
|
||||
color: theme.menuItemTextHover,
|
||||
},
|
||||
})}`}
|
||||
>
|
||||
{!line ? (
|
||||
<ListboxList style={{ maxHeight: 250, overflowY: 'auto' }}>
|
||||
{options.map(([value, label]) => (
|
||||
<ListboxOption
|
||||
key={value}
|
||||
value={value}
|
||||
disabled={disabledKeys.includes(value)}
|
||||
>
|
||||
{label}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxList>
|
||||
) : (
|
||||
<ListboxList style={{ maxHeight: 250, overflowY: 'auto' }}>
|
||||
{options.slice(0, line).map(([value, label]) => (
|
||||
<ListboxOption
|
||||
key={value}
|
||||
value={value}
|
||||
disabled={disabledKeys.includes(value)}
|
||||
>
|
||||
{label}
|
||||
</ListboxOption>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
padding: '2px',
|
||||
marginTop: 5,
|
||||
borderTop: '1px solid ' + theme.menuBorder,
|
||||
}}
|
||||
/>
|
||||
{options.slice(line, options.length).map(([value, label]) => (
|
||||
<ListboxOption
|
||||
key={value}
|
||||
value={value}
|
||||
disabled={disabledKeys.includes(value)}
|
||||
>
|
||||
{label}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxList>
|
||||
)}
|
||||
</ListboxPopover>
|
||||
</ListboxInput>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,29 +4,26 @@ import { type RuleConditionEntity } from 'loot-core/src/types/models';
|
||||
|
||||
import { View } from '../common/View';
|
||||
|
||||
import { ConditionsOpMenu } from './ConditionsOpMenu';
|
||||
import { CondOpMenu } from './CondOpMenu';
|
||||
import { FilterExpression } from './FilterExpression';
|
||||
|
||||
type AppliedFiltersProps = {
|
||||
conditions: RuleConditionEntity[];
|
||||
filters: RuleConditionEntity[];
|
||||
onUpdate: (
|
||||
filter: RuleConditionEntity,
|
||||
newFilter: RuleConditionEntity,
|
||||
) => void;
|
||||
onDelete: (filter: RuleConditionEntity) => void;
|
||||
conditionsOp: string;
|
||||
onConditionsOpChange: (
|
||||
value: string,
|
||||
conditions: RuleConditionEntity[],
|
||||
) => void;
|
||||
onCondOpChange: (value: string, filters: RuleConditionEntity[]) => void;
|
||||
};
|
||||
|
||||
export function AppliedFilters({
|
||||
conditions,
|
||||
filters,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
conditionsOp,
|
||||
onConditionsOpChange,
|
||||
onCondOpChange,
|
||||
}: AppliedFiltersProps) {
|
||||
return (
|
||||
<View
|
||||
@@ -36,12 +33,12 @@ export function AppliedFilters({
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<ConditionsOpMenu
|
||||
<CondOpMenu
|
||||
conditionsOp={conditionsOp}
|
||||
onChange={onConditionsOpChange}
|
||||
conditions={conditions}
|
||||
onCondOpChange={onCondOpChange}
|
||||
filters={filters}
|
||||
/>
|
||||
{conditions.map((filter: RuleConditionEntity, i: number) => (
|
||||
{filters.map((filter: RuleConditionEntity, i: number) => (
|
||||
<FilterExpression
|
||||
key={i}
|
||||
customName={filter.customName}
|
||||
|
||||
@@ -7,16 +7,16 @@ import { Text } from '../common/Text';
|
||||
import { View } from '../common/View';
|
||||
import { FieldSelect } from '../modals/EditRule';
|
||||
|
||||
export function ConditionsOpMenu({
|
||||
export function CondOpMenu({
|
||||
conditionsOp,
|
||||
onChange,
|
||||
conditions,
|
||||
onCondOpChange,
|
||||
filters,
|
||||
}: {
|
||||
conditionsOp: string;
|
||||
onChange: (value: string, conditions: RuleConditionEntity[]) => void;
|
||||
conditions: RuleConditionEntity[];
|
||||
onCondOpChange: (value: string, filters: RuleConditionEntity[]) => void;
|
||||
filters: RuleConditionEntity[];
|
||||
}) {
|
||||
return conditions.length > 1 ? (
|
||||
return filters.length > 1 ? (
|
||||
<Text style={{ color: theme.pageText, marginTop: 11, marginRight: 5 }}>
|
||||
<FieldSelect
|
||||
style={{ display: 'inline-flex' }}
|
||||
@@ -25,7 +25,9 @@ export function ConditionsOpMenu({
|
||||
['or', 'any'],
|
||||
]}
|
||||
value={conditionsOp}
|
||||
onChange={(name: string, value: string) => onChange(value, conditions)}
|
||||
onChange={(name: string, value: string) =>
|
||||
onCondOpChange(value, filters)
|
||||
}
|
||||
/>
|
||||
of:
|
||||
</Text>
|
||||
@@ -61,6 +61,7 @@ export function FilterExpression({
|
||||
type="bare"
|
||||
disabled={customName != null}
|
||||
onClick={() => setEditing(true)}
|
||||
style={{ marginRight: -7 }}
|
||||
>
|
||||
<div style={{ paddingBlock: 1, paddingLeft: 5, paddingRight: 2 }}>
|
||||
{customName ? (
|
||||
@@ -75,11 +76,7 @@ export function FilterExpression({
|
||||
value={value}
|
||||
field={field}
|
||||
inline={true}
|
||||
valueIsRaw={
|
||||
op === 'contains' ||
|
||||
op === 'matches' ||
|
||||
op === 'doesNotContain'
|
||||
}
|
||||
valueIsRaw={op === 'contains' || op === 'doesNotContain'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -90,7 +87,8 @@ export function FilterExpression({
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
margin: 4,
|
||||
margin: 5,
|
||||
marginLeft: 3,
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
@@ -87,6 +87,7 @@ function ConfigureField({
|
||||
<Stack direction="row" align="flex-start">
|
||||
{field === 'amount' || field === 'date' ? (
|
||||
<Select
|
||||
bare
|
||||
options={
|
||||
field === 'amount'
|
||||
? [
|
||||
@@ -110,6 +111,7 @@ function ConfigureField({
|
||||
dispatch({ type: 'set-op', op: 'is' });
|
||||
}
|
||||
}}
|
||||
style={{ borderWidth: 1 }}
|
||||
/>
|
||||
) : (
|
||||
titleFirst(mapField(field))
|
||||
@@ -197,8 +199,7 @@ function ConfigureField({
|
||||
field={field}
|
||||
subfield={subfield}
|
||||
type={
|
||||
type === 'id' &&
|
||||
(op === 'contains' || op === 'matches' || op === 'doesNotContain')
|
||||
type === 'id' && (op === 'contains' || op === 'doesNotContain')
|
||||
? 'string'
|
||||
: type
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import { type TransactionFilterEntity } from 'loot-core/types/models';
|
||||
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
|
||||
|
||||
import { Stack } from '../common/Stack';
|
||||
@@ -13,17 +12,17 @@ import {
|
||||
} from './SavedFilterMenuButton';
|
||||
|
||||
export function FiltersStack({
|
||||
conditions,
|
||||
filters,
|
||||
conditionsOp,
|
||||
onUpdateFilter,
|
||||
onDeleteFilter,
|
||||
onClearFilters,
|
||||
onReloadSavedFilter,
|
||||
filterId,
|
||||
savedFilters,
|
||||
onConditionsOpChange,
|
||||
filtersList,
|
||||
onCondOpChange,
|
||||
}: {
|
||||
conditions: RuleConditionEntity[];
|
||||
filters: RuleConditionEntity[];
|
||||
conditionsOp: string;
|
||||
onUpdateFilter: (
|
||||
filter: RuleConditionEntity,
|
||||
@@ -33,8 +32,8 @@ export function FiltersStack({
|
||||
onClearFilters: () => void;
|
||||
onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
|
||||
filterId: SavedFilter;
|
||||
savedFilters: TransactionFilterEntity[];
|
||||
onConditionsOpChange: () => void;
|
||||
filtersList: RuleConditionEntity[];
|
||||
onCondOpChange: () => void;
|
||||
}) {
|
||||
return (
|
||||
<View>
|
||||
@@ -45,20 +44,20 @@ export function FiltersStack({
|
||||
align="flex-start"
|
||||
>
|
||||
<AppliedFilters
|
||||
conditions={conditions}
|
||||
filters={filters}
|
||||
conditionsOp={conditionsOp}
|
||||
onConditionsOpChange={onConditionsOpChange}
|
||||
onCondOpChange={onCondOpChange}
|
||||
onUpdate={onUpdateFilter}
|
||||
onDelete={onDeleteFilter}
|
||||
/>
|
||||
<View style={{ flex: 1 }} />
|
||||
<SavedFilterMenuButton
|
||||
conditions={conditions}
|
||||
filters={filters}
|
||||
conditionsOp={conditionsOp}
|
||||
filterId={filterId}
|
||||
onClearFilters={onClearFilters}
|
||||
onReloadSavedFilter={onReloadSavedFilter}
|
||||
savedFilters={savedFilters}
|
||||
filtersList={filtersList}
|
||||
/>
|
||||
</Stack>
|
||||
</View>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import { send, sendCatch } from 'loot-core/src/platform/client/fetch';
|
||||
import { type TransactionFilterEntity } from 'loot-core/types/models';
|
||||
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
|
||||
|
||||
import { SvgExpandArrow } from '../../icons/v0';
|
||||
@@ -22,19 +21,19 @@ export type SavedFilter = {
|
||||
};
|
||||
|
||||
export function SavedFilterMenuButton({
|
||||
conditions,
|
||||
filters,
|
||||
conditionsOp,
|
||||
filterId,
|
||||
onClearFilters,
|
||||
onReloadSavedFilter,
|
||||
savedFilters,
|
||||
filtersList,
|
||||
}: {
|
||||
conditions: RuleConditionEntity[];
|
||||
filters: RuleConditionEntity[];
|
||||
conditionsOp: string;
|
||||
filterId: SavedFilter;
|
||||
onClearFilters: () => void;
|
||||
onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
|
||||
savedFilters: TransactionFilterEntity[];
|
||||
filtersList: RuleConditionEntity[];
|
||||
}) {
|
||||
const [nameOpen, setNameOpen] = useState(false);
|
||||
const [adding, setAdding] = useState(false);
|
||||
@@ -65,7 +64,7 @@ export function SavedFilterMenuButton({
|
||||
setAdding(false);
|
||||
setMenuOpen(false);
|
||||
savedFilter = {
|
||||
conditions,
|
||||
conditions: filters,
|
||||
conditionsOp,
|
||||
id: filterId.id,
|
||||
name: filterId.name,
|
||||
@@ -73,7 +72,7 @@ export function SavedFilterMenuButton({
|
||||
};
|
||||
const response = await sendCatch('filter-update', {
|
||||
state: savedFilter,
|
||||
filters: [...savedFilters],
|
||||
filters: [...filtersList],
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
@@ -109,7 +108,7 @@ export function SavedFilterMenuButton({
|
||||
async function onAddUpdate() {
|
||||
if (adding) {
|
||||
const newSavedFilter = {
|
||||
conditions,
|
||||
conditions: filters,
|
||||
conditionsOp,
|
||||
name,
|
||||
status: 'saved',
|
||||
@@ -117,7 +116,7 @@ export function SavedFilterMenuButton({
|
||||
|
||||
const response = await sendCatch('filter-create', {
|
||||
state: newSavedFilter,
|
||||
filters: [...savedFilters],
|
||||
filters: [...filtersList],
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
@@ -143,7 +142,7 @@ export function SavedFilterMenuButton({
|
||||
|
||||
const response = await sendCatch('filter-update', {
|
||||
state: updatedFilter,
|
||||
filters: [...savedFilters],
|
||||
filters: [...filtersList],
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
@@ -158,7 +157,7 @@ export function SavedFilterMenuButton({
|
||||
|
||||
return (
|
||||
<View>
|
||||
{conditions.length > 0 && (
|
||||
{filters.length > 0 && (
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
type="bare"
|
||||
|
||||
@@ -12,7 +12,6 @@ export function updateFilterReducer(
|
||||
if (
|
||||
(type === 'id' || type === 'string') &&
|
||||
(action.op === 'contains' ||
|
||||
action.op === 'matches' ||
|
||||
action.op === 'is' ||
|
||||
action.op === 'doesNotContain' ||
|
||||
action.op === 'isNot')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { memo, useRef } from 'react';
|
||||
import React, { memo, useRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { AutoTextSize } from 'auto-text-size';
|
||||
import memoizeOne from 'memoize-one';
|
||||
@@ -8,7 +9,6 @@ import { collapseModals, pushModal } from 'loot-core/client/actions';
|
||||
import { rolloverBudget, reportBudget } from 'loot-core/src/client/queries';
|
||||
import * as monthUtils from 'loot-core/src/shared/months';
|
||||
|
||||
import { useFeatureFlag } from '../../../hooks/useFeatureFlag';
|
||||
import { useLocalPref } from '../../../hooks/useLocalPref';
|
||||
import { useNavigate } from '../../../hooks/useNavigate';
|
||||
import { SvgLogo } from '../../../icons/logo';
|
||||
@@ -16,14 +16,13 @@ import { SvgExpandArrow } from '../../../icons/v0';
|
||||
import {
|
||||
SvgArrowThinLeft,
|
||||
SvgArrowThinRight,
|
||||
SvgArrowThickRight,
|
||||
SvgCheveronRight,
|
||||
} from '../../../icons/v1';
|
||||
import { SvgViewShow } from '../../../icons/v2';
|
||||
import { useResponsive } from '../../../ResponsiveProvider';
|
||||
import { theme, styles } from '../../../style';
|
||||
import { BalanceWithCarryover } from '../../budget/BalanceWithCarryover';
|
||||
import { makeAmountGrey, makeBalanceAmountStyle } from '../../budget/util';
|
||||
import { makeAmountFullStyle, makeAmountGrey } from '../../budget/util';
|
||||
import { Button } from '../../common/Button';
|
||||
import { Card } from '../../common/Card';
|
||||
import { Label } from '../../common/Label';
|
||||
@@ -330,15 +329,11 @@ const ExpenseCategory = memo(function ExpenseCategory({
|
||||
onBudgetAction,
|
||||
show3Cols,
|
||||
showBudgetedCol,
|
||||
setScrollPosition,
|
||||
onShowActivityWithScroll,
|
||||
}) {
|
||||
const opacity = blank ? 0 : 1;
|
||||
|
||||
const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled');
|
||||
const goalTemp = useSheetValue(goal);
|
||||
const goalValue = isGoalTemplatesEnabled ? goalTemp : null;
|
||||
const budgetedTemp = useSheetValue(budgeted);
|
||||
const budgetedValue = isGoalTemplatesEnabled ? budgetedTemp : null;
|
||||
|
||||
const [budgetType = 'rollover'] = useLocalPref('budgetType');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -404,10 +399,6 @@ const ExpenseCategory = memo(function ExpenseCategory({
|
||||
|
||||
const listItemRef = useRef();
|
||||
const format = useFormat();
|
||||
const navigate = useNavigate();
|
||||
const onShowActivity = () => {
|
||||
navigate(`/categories/${category.id}?month=${month}`);
|
||||
};
|
||||
|
||||
const sidebarColumnWidth = getColumnWidth({ show3Cols, isSidebar: true });
|
||||
const columnWidth = getColumnWidth({ show3Cols });
|
||||
@@ -525,7 +516,9 @@ const ExpenseCategory = memo(function ExpenseCategory({
|
||||
binding={spent}
|
||||
getStyle={makeAmountGrey}
|
||||
type="financial"
|
||||
onClick={onShowActivity}
|
||||
onClick={() => {
|
||||
onShowActivityWithScroll(category.id, month);
|
||||
}}
|
||||
formatter={value => (
|
||||
<Button
|
||||
type="bare"
|
||||
@@ -582,11 +575,9 @@ const ExpenseCategory = memo(function ExpenseCategory({
|
||||
mode="oneline"
|
||||
style={{
|
||||
maxWidth: columnWidth,
|
||||
...makeBalanceAmountStyle(
|
||||
value,
|
||||
goalValue,
|
||||
budgetedValue,
|
||||
),
|
||||
...makeAmountFullStyle(value, {
|
||||
zeroColor: theme.pillTextSubdued,
|
||||
}),
|
||||
textAlign: 'right',
|
||||
fontSize: 12,
|
||||
}}
|
||||
@@ -595,23 +586,6 @@ const ExpenseCategory = memo(function ExpenseCategory({
|
||||
</AutoTextSize>
|
||||
</Button>
|
||||
)}
|
||||
carryoverIndicator={({ style }) => (
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '-3px',
|
||||
top: '-5px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: style?.color ?? theme.pillText,
|
||||
}}
|
||||
>
|
||||
<SvgArrowThickRight
|
||||
width={11}
|
||||
height={11}
|
||||
style={{ color: theme.pillBackgroundLight }}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</View>
|
||||
@@ -1262,6 +1236,8 @@ const ExpenseGroup = memo(function ExpenseGroup({
|
||||
showHiddenCategories,
|
||||
collapsed,
|
||||
onToggleCollapse,
|
||||
setScrollPosition,
|
||||
onShowActivityWithScroll,
|
||||
}) {
|
||||
function editable(content) {
|
||||
if (!editMode) {
|
||||
@@ -1374,6 +1350,8 @@ const ExpenseGroup = memo(function ExpenseGroup({
|
||||
month={month}
|
||||
// onReorder={onReorderCategory}
|
||||
onBudgetAction={onBudgetAction}
|
||||
setScrollPosition={setScrollPosition}
|
||||
onShowActivityWithScroll={onShowActivityWithScroll}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -1485,6 +1463,8 @@ function BudgetGroups({
|
||||
showBudgetedCol,
|
||||
show3Cols,
|
||||
showHiddenCategories,
|
||||
setScrollPosition,
|
||||
onShowActivityWithScroll,
|
||||
}) {
|
||||
const separateGroups = memoizeOne(groups => {
|
||||
return {
|
||||
@@ -1534,6 +1514,8 @@ function BudgetGroups({
|
||||
showHiddenCategories={showHiddenCategories}
|
||||
collapsed={collapsedGroupIds.includes(group.id)}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
setScrollPosition={setScrollPosition}
|
||||
onShowActivityWithScroll={onShowActivityWithScroll}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -1584,6 +1566,8 @@ export function BudgetTable({
|
||||
}) {
|
||||
const { width } = useResponsive();
|
||||
const show3Cols = width >= 360;
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// let editMode = false; // neuter editMode -- sorry, not rewriting drag-n-drop right now
|
||||
|
||||
@@ -1591,6 +1575,44 @@ export function BudgetTable({
|
||||
'mobile.showSpentColumn',
|
||||
);
|
||||
|
||||
const getCurrentScrollPosition = () => {
|
||||
const scrollableDiv = document.getElementById('scrollableDiv');
|
||||
return scrollableDiv?.scrollTop || 0;
|
||||
};
|
||||
|
||||
const setScrollPosition = () => {
|
||||
const scrollPosition = getCurrentScrollPosition();
|
||||
|
||||
navigate('/budget', {
|
||||
replace: true,
|
||||
state: { scrollPosition }
|
||||
});
|
||||
};
|
||||
|
||||
const onShowActivityWithScroll = (categoryId, month) => {
|
||||
const scrollPosition = getCurrentScrollPosition();
|
||||
|
||||
navigate('/budget', {
|
||||
replace: true,
|
||||
state: { scrollPosition }
|
||||
});
|
||||
|
||||
navigate(`/categories/${categoryId}?month=${month}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const savedScrollPosition = location.state?.scrollPosition;
|
||||
|
||||
if (savedScrollPosition) {
|
||||
const scrollableDiv = document.getElementById('scrollableDiv');
|
||||
if (scrollableDiv) {
|
||||
requestAnimationFrame(() => {
|
||||
scrollableDiv.scrollTop = savedScrollPosition;
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [location.state?.scrollPosition]);
|
||||
|
||||
function toggleSpentColumn() {
|
||||
setShowSpentColumnPref(!showSpentColumn);
|
||||
}
|
||||
@@ -1650,9 +1672,12 @@ export function BudgetTable({
|
||||
<PullToRefresh onRefresh={onRefresh}>
|
||||
<View
|
||||
data-testid="budget-table"
|
||||
id="scrollableDiv"
|
||||
style={{
|
||||
backgroundColor: theme.pageBackground,
|
||||
paddingBottom: MOBILE_NAV_HEIGHT,
|
||||
overflowY: 'auto',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<BudgetGroups
|
||||
@@ -1674,6 +1699,8 @@ export function BudgetTable({
|
||||
onReorderCategory={onReorderCategory}
|
||||
onReorderGroup={onReorderGroup}
|
||||
onBudgetAction={onBudgetAction}
|
||||
setScrollPosition={setScrollPosition}
|
||||
onShowActivityWithScroll={onShowActivityWithScroll}
|
||||
/>
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
|
||||
@@ -118,19 +118,14 @@ export const Transaction = memo(function Transaction({
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
color: textStyle.color || theme.tableText,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
{schedule && (
|
||||
<SvgArrowsSynchronize
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
marginRight: 5,
|
||||
color: textStyle.color || theme.menuItemText,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -140,18 +135,13 @@ export const Transaction = memo(function Transaction({
|
||||
...textStyle,
|
||||
fontSize: 14,
|
||||
fontWeight: added ? '600' : '400',
|
||||
...(!isPreview &&
|
||||
prettyDescription === '' && {
|
||||
color: theme.tableTextSubdued,
|
||||
}),
|
||||
...(!isPreview &&
|
||||
(prettyDescription === '' ||
|
||||
prettyDescription === 'Split') && {
|
||||
fontStyle: 'italic',
|
||||
}),
|
||||
...(prettyDescription === '' && {
|
||||
color: theme.tableTextLight,
|
||||
fontStyle: 'italic',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{prettyDescription || 'No payee'}
|
||||
{prettyDescription || 'Empty'}
|
||||
</TextOneLine>
|
||||
</View>
|
||||
{isPreview ? (
|
||||
@@ -180,7 +170,7 @@ export const Transaction = memo(function Transaction({
|
||||
height: 11,
|
||||
color: cleared
|
||||
? theme.noticeTextLight
|
||||
: theme.tableTextSubdued,
|
||||
: theme.pageTextSubdued,
|
||||
marginRight: 5,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -69,11 +69,9 @@ function getFieldName(transactionId, field) {
|
||||
}
|
||||
|
||||
export function getDescriptionPretty(transaction, payee, transferAcct) {
|
||||
const { amount, is_parent: isParent } = transaction;
|
||||
const { amount } = transaction;
|
||||
|
||||
if (isParent) {
|
||||
return 'Split';
|
||||
} else if (transferAcct) {
|
||||
if (transferAcct) {
|
||||
return `Transfer ${amount > 0 ? 'from' : 'to'} ${transferAcct.name}`;
|
||||
} else if (payee) {
|
||||
return payee.name;
|
||||
@@ -364,7 +362,7 @@ const ChildTransactionEdit = forwardRef(
|
||||
<View>
|
||||
<FieldLabel title="Category" />
|
||||
<TapField
|
||||
textStyle={{
|
||||
style={{
|
||||
...((isOffBudget || isBudgetTransfer(transaction)) && {
|
||||
fontStyle: 'italic',
|
||||
color: theme.pageTextSubdued,
|
||||
@@ -492,9 +490,6 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
};
|
||||
|
||||
const getPrettyPayee = trans => {
|
||||
if (trans && trans.is_parent) {
|
||||
return 'Split';
|
||||
}
|
||||
const transPayee = trans && getPayee(trans);
|
||||
const transTransferAcct = trans && getTransferAcct(trans);
|
||||
return getDescriptionPretty(trans, transPayee, transTransferAcct);
|
||||
@@ -768,17 +763,11 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
<View>
|
||||
<FieldLabel title="Payee" />
|
||||
<TapField
|
||||
textStyle={{
|
||||
...(transaction.is_parent && {
|
||||
fontStyle: 'italic',
|
||||
fontWeight: 300,
|
||||
}),
|
||||
}}
|
||||
value={getPrettyPayee(transaction)}
|
||||
disabled={
|
||||
editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'payee')
|
||||
}
|
||||
value={getPrettyPayee(transaction)}
|
||||
onClick={() => onEditField(transaction.id, 'payee')}
|
||||
data-testid="payee-field"
|
||||
/>
|
||||
|
||||
@@ -9,14 +9,12 @@ import { type CommonModalProps } from '../Modals';
|
||||
|
||||
type ConfirmTransactionEditProps = {
|
||||
modalProps: Partial<CommonModalProps>;
|
||||
onCancel?: () => void;
|
||||
onConfirm: () => void;
|
||||
confirmReason: string;
|
||||
};
|
||||
|
||||
export function ConfirmTransactionEdit({
|
||||
modalProps,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
confirmReason,
|
||||
}: ConfirmTransactionEditProps) {
|
||||
@@ -73,13 +71,7 @@ export function ConfirmTransactionEdit({
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{ marginRight: 10 }}
|
||||
onClick={() => {
|
||||
modalProps.onClose();
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
<Button style={{ marginRight: 10 }} onClick={modalProps.onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -41,7 +35,6 @@ import { SvgDelete, SvgAdd, SvgSubtract } from '../../icons/v0';
|
||||
import { SvgInformationOutline } from '../../icons/v1';
|
||||
import { styles, theme } from '../../style';
|
||||
import { Button } from '../common/Button';
|
||||
import { Menu } from '../common/Menu';
|
||||
import { Modal } from '../common/Modal';
|
||||
import { Select } from '../common/Select';
|
||||
import { Stack } from '../common/Stack';
|
||||
@@ -89,13 +82,12 @@ function getTransactionFields(conditions, actions) {
|
||||
|
||||
export function FieldSelect({ fields, style, value, onChange }) {
|
||||
return (
|
||||
<View style={style}>
|
||||
<View style={{ color: theme.pageTextPositive, ...style }}>
|
||||
<Select
|
||||
bare
|
||||
options={fields}
|
||||
value={value}
|
||||
onChange={value => onChange('field', value)}
|
||||
buttonStyle={{ color: theme.pageTextPositive }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -105,36 +97,31 @@ export function OpSelect({
|
||||
ops,
|
||||
type,
|
||||
style,
|
||||
wrapperStyle,
|
||||
value,
|
||||
formatOp = friendlyOp,
|
||||
onChange,
|
||||
}) {
|
||||
const opOptions = useMemo(() => {
|
||||
const options = ops
|
||||
// We don't support the `contains`, `doesNotContain`, `matches` operators
|
||||
// for the id type rules yet
|
||||
// TODO: Add matches op support for payees, accounts, categories.
|
||||
.filter(op =>
|
||||
type === 'id'
|
||||
? !['contains', 'matches', 'doesNotContain'].includes(op)
|
||||
: true,
|
||||
)
|
||||
.map(op => [op, formatOp(op, type)]);
|
||||
|
||||
if (type === 'string' || type === 'id') {
|
||||
options.splice(options.length / 2, 0, Menu.line);
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [ops, type]);
|
||||
let line;
|
||||
// We don't support the `contains` operator for the id type for
|
||||
// rules yet
|
||||
if (type === 'id') {
|
||||
ops = ops.filter(op => op !== 'contains' && op !== 'doesNotContain');
|
||||
line = ops.length / 2;
|
||||
}
|
||||
if (type === 'string') {
|
||||
line = ops.length / 2;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
bare
|
||||
options={opOptions}
|
||||
options={ops.map(op => [op, formatOp(op, type)])}
|
||||
value={value}
|
||||
onChange={value => onChange('op', value)}
|
||||
buttonStyle={style}
|
||||
line={line}
|
||||
style={{ minHeight: '1px', ...style }}
|
||||
wrapperStyle={wrapperStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -487,8 +487,9 @@ function SelectField({
|
||||
]),
|
||||
]}
|
||||
value={value === null ? 'choose-field' : value}
|
||||
onChange={onChange}
|
||||
buttonStyle={style}
|
||||
style={{ width: '100%' }}
|
||||
wrapperStyle={style}
|
||||
onChange={value => onChange(value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -519,7 +520,8 @@ function DateFormatSelect({
|
||||
f.label.replace(/ /g, delimiter),
|
||||
])}
|
||||
value={parseDateFormat || ''}
|
||||
onChange={onChange}
|
||||
onChange={value => onChange(value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { type ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
import { useAccounts } from '../../hooks/useAccounts';
|
||||
import { useNavigate } from '../../hooks/useNavigate';
|
||||
import { usePayees } from '../../hooks/usePayees';
|
||||
import { useResponsive } from '../../ResponsiveProvider';
|
||||
import { theme } from '../../style';
|
||||
@@ -22,7 +21,6 @@ export function PayeeAutocompleteModal({
|
||||
}: PayeeAutocompleteModalProps) {
|
||||
const payees = usePayees() || [];
|
||||
const accounts = useAccounts() || [];
|
||||
const navigate = useNavigate();
|
||||
|
||||
const _onClose = () => {
|
||||
modalProps.onClose();
|
||||
@@ -34,8 +32,6 @@ export function PayeeAutocompleteModal({
|
||||
containerProps: { style: { height: isNarrowWidth ? '90vh' : 275 } },
|
||||
};
|
||||
|
||||
const onManagePayees = () => navigate('/payees');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
@@ -60,19 +56,20 @@ export function PayeeAutocompleteModal({
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<PayeeAutocomplete
|
||||
payees={payees}
|
||||
accounts={accounts}
|
||||
focused={true}
|
||||
embedded={true}
|
||||
closeOnBlur={false}
|
||||
onClose={_onClose}
|
||||
onManagePayees={onManagePayees}
|
||||
showManagePayees={!isNarrowWidth}
|
||||
showMakeTransfer={!isNarrowWidth}
|
||||
{...defaultAutocompleteProps}
|
||||
{...autocompleteProps}
|
||||
/>
|
||||
{() => (
|
||||
<PayeeAutocomplete
|
||||
payees={payees}
|
||||
accounts={accounts}
|
||||
focused={true}
|
||||
embedded={true}
|
||||
closeOnBlur={false}
|
||||
onClose={_onClose}
|
||||
showManagePayees={false}
|
||||
showMakeTransfer={!isNarrowWidth}
|
||||
{...defaultAutocompleteProps}
|
||||
{...autocompleteProps}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@ import { reportBudget } from 'loot-core/client/queries';
|
||||
|
||||
import { useCategory } from '../../hooks/useCategory';
|
||||
import { type CSSProperties, theme, styles } from '../../style';
|
||||
import {
|
||||
BalanceWithCarryover,
|
||||
DefaultCarryoverIndicator,
|
||||
} from '../budget/BalanceWithCarryover';
|
||||
import { BalanceWithCarryover } from '../budget/BalanceWithCarryover';
|
||||
import { BalanceMenu } from '../budget/report/BalanceMenu';
|
||||
import { Modal, ModalTitle } from '../common/Modal';
|
||||
import { Text } from '../common/Text';
|
||||
@@ -66,21 +63,11 @@ export function ReportBalanceMenuModal({
|
||||
textAlign: 'center',
|
||||
...styles.veryLargeText,
|
||||
}}
|
||||
carryoverStyle={{ right: -20, width: 15, height: 15 }}
|
||||
carryover={reportBudget.catCarryover(categoryId)}
|
||||
balance={reportBudget.catBalance(categoryId)}
|
||||
goal={reportBudget.catGoal(categoryId)}
|
||||
budgeted={reportBudget.catBudgeted(categoryId)}
|
||||
carryoverIndicator={({ style }) =>
|
||||
DefaultCarryoverIndicator({
|
||||
style: {
|
||||
width: 15,
|
||||
height: 15,
|
||||
display: 'inline-flex',
|
||||
position: 'relative',
|
||||
...style,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<BalanceMenu
|
||||
|
||||
@@ -4,10 +4,7 @@ import { rolloverBudget } from 'loot-core/client/queries';
|
||||
|
||||
import { useCategory } from '../../hooks/useCategory';
|
||||
import { type CSSProperties, theme, styles } from '../../style';
|
||||
import {
|
||||
BalanceWithCarryover,
|
||||
DefaultCarryoverIndicator,
|
||||
} from '../budget/BalanceWithCarryover';
|
||||
import { BalanceWithCarryover } from '../budget/BalanceWithCarryover';
|
||||
import { BalanceMenu } from '../budget/rollover/BalanceMenu';
|
||||
import { Modal, ModalTitle } from '../common/Modal';
|
||||
import { Text } from '../common/Text';
|
||||
@@ -68,21 +65,11 @@ export function RolloverBalanceMenuModal({
|
||||
textAlign: 'center',
|
||||
...styles.veryLargeText,
|
||||
}}
|
||||
carryoverStyle={{ right: -20, width: 15, height: 15 }}
|
||||
carryover={rolloverBudget.catCarryover(categoryId)}
|
||||
balance={rolloverBudget.catBalance(categoryId)}
|
||||
goal={rolloverBudget.catGoal(categoryId)}
|
||||
budgeted={rolloverBudget.catBudgeted(categoryId)}
|
||||
carryoverIndicator={({ style }) =>
|
||||
DefaultCarryoverIndicator({
|
||||
style: {
|
||||
width: 15,
|
||||
height: 15,
|
||||
display: 'inline-flex',
|
||||
position: 'relative',
|
||||
...style,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<BalanceMenu
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { useStableCallback } from '../../hooks/useStableCallback';
|
||||
import { SvgExpandArrow } from '../../icons/v0';
|
||||
import { theme } from '../../style';
|
||||
import { Button } from '../common/Button2';
|
||||
import { Button } from '../common/Button';
|
||||
import { Popover } from '../common/Popover';
|
||||
import { Search } from '../common/Search';
|
||||
import { View } from '../common/View';
|
||||
@@ -236,10 +236,10 @@ export const ManagePayees = forwardRef(
|
||||
<View style={{ flexShrink: 0 }}>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
variant="bare"
|
||||
type="bare"
|
||||
style={{ marginRight: 10 }}
|
||||
isDisabled={buttonsDisabled}
|
||||
onPress={() => setMenuOpen(true)}
|
||||
disabled={buttonsDisabled}
|
||||
onClick={() => setMenuOpen(true)}
|
||||
>
|
||||
{buttonsDisabled
|
||||
? 'No payees selected'
|
||||
@@ -273,9 +273,9 @@ export const ManagePayees = forwardRef(
|
||||
{(orphanedOnly ||
|
||||
(orphanedPayees && orphanedPayees.length > 0)) && (
|
||||
<Button
|
||||
variant="bare"
|
||||
type="bare"
|
||||
style={{ marginRight: 10 }}
|
||||
onPress={() => {
|
||||
onClick={() => {
|
||||
setOrphanedOnly(!orphanedOnly);
|
||||
applyFilter(filter);
|
||||
tableNavigator.onEdit(null);
|
||||
|
||||
@@ -27,7 +27,7 @@ export function Header({
|
||||
onApply,
|
||||
onUpdateFilter,
|
||||
onDeleteFilter,
|
||||
onConditionsOpChange,
|
||||
onCondOpChange,
|
||||
headerPrefixItems,
|
||||
children,
|
||||
}) {
|
||||
@@ -151,11 +151,11 @@ export function Header({
|
||||
align="flex-start"
|
||||
>
|
||||
<AppliedFilters
|
||||
conditions={filters}
|
||||
filters={filters}
|
||||
onUpdate={onUpdateFilter}
|
||||
onDelete={onDeleteFilter}
|
||||
conditionsOp={conditionsOp}
|
||||
onConditionsOpChange={onConditionsOpChange}
|
||||
onCondOpChange={onCondOpChange}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -35,8 +35,6 @@ const balanceTypeOptions = [
|
||||
{ description: 'Payment', format: 'totalDebts' as const },
|
||||
{ description: 'Deposit', format: 'totalAssets' as const },
|
||||
{ description: 'Net', format: 'totalTotals' as const },
|
||||
{ description: 'Net Payment', format: 'netDebts' as const },
|
||||
{ description: 'Net Deposit', format: 'netAssets' as const },
|
||||
];
|
||||
|
||||
const groupByOptions = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useRef, useState, type ComponentProps } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import * as monthUtils from 'loot-core/src/shared/months';
|
||||
import { type CategoryEntity } from 'loot-core/types/models/category';
|
||||
@@ -131,19 +131,6 @@ export function ReportSidebar({
|
||||
setBalanceType(cond);
|
||||
};
|
||||
|
||||
const rangeOptions = useMemo(() => {
|
||||
const options: ComponentProps<typeof Select>['options'] =
|
||||
ReportOptions.dateRange
|
||||
.filter(f => f[customReportItems.interval as keyof dateRangeProps])
|
||||
.map(option => [option.description, option.description]);
|
||||
|
||||
// Append separator if necessary
|
||||
if (dateRangeLine > 0) {
|
||||
options.splice(dateRangeLine, 0, Menu.line);
|
||||
}
|
||||
return options;
|
||||
}, [customReportItems, dateRangeLine]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -432,8 +419,15 @@ export function ReportSidebar({
|
||||
</Text>
|
||||
<Select
|
||||
value={customReportItems.dateRange}
|
||||
onChange={onSelectRange}
|
||||
options={rangeOptions}
|
||||
onChange={e => {
|
||||
onSelectRange(e);
|
||||
}}
|
||||
options={ReportOptions.dateRange
|
||||
.filter(
|
||||
f => f[customReportItems.interval as keyof dateRangeProps],
|
||||
)
|
||||
.map(option => [option.description, option.description])}
|
||||
line={dateRangeLine > 0 ? dateRangeLine : undefined}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
|
||||
@@ -6,10 +6,7 @@ import {
|
||||
integerToCurrency,
|
||||
amountToInteger,
|
||||
} from 'loot-core/src/shared/util';
|
||||
import {
|
||||
type balanceTypeOpType,
|
||||
type DataEntity,
|
||||
} from 'loot-core/src/types/models/reports';
|
||||
import { type DataEntity } from 'loot-core/src/types/models/reports';
|
||||
|
||||
import { theme, styles } from '../../style';
|
||||
import { Text } from '../common/Text';
|
||||
@@ -22,7 +19,7 @@ type ReportSummaryProps = {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
data: DataEntity;
|
||||
balanceTypeOp: balanceTypeOpType;
|
||||
balanceTypeOp: 'totalDebts' | 'totalAssets' | 'totalTotals';
|
||||
interval: string;
|
||||
intervalsCount: number;
|
||||
};
|
||||
@@ -36,13 +33,9 @@ export function ReportSummary({
|
||||
intervalsCount,
|
||||
}: ReportSummaryProps) {
|
||||
const net =
|
||||
balanceTypeOp === 'netAssets'
|
||||
? 'DEPOSIT'
|
||||
: balanceTypeOp === 'netDebts'
|
||||
? 'PAYMENT'
|
||||
: Math.abs(data.totalDebts) > Math.abs(data.totalAssets)
|
||||
? 'PAYMENT'
|
||||
: 'DEPOSIT';
|
||||
Math.abs(data.totalDebts) > Math.abs(data.totalAssets)
|
||||
? 'PAYMENT'
|
||||
: 'DEPOSIT';
|
||||
const average = amountToInteger(data[balanceTypeOp]) / intervalsCount;
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -63,7 +63,7 @@ const totalGraphOptions: graphOptions[] = [
|
||||
description: 'BarGraph',
|
||||
disabledSplit: [],
|
||||
defaultSplit: 'Category',
|
||||
disabledType: [],
|
||||
disabledType: ['Net'],
|
||||
defaultType: 'Payment',
|
||||
},
|
||||
{
|
||||
@@ -88,7 +88,7 @@ const timeGraphOptions: graphOptions[] = [
|
||||
description: 'TableGraph',
|
||||
disabledSplit: ['Interval'],
|
||||
defaultSplit: 'Category',
|
||||
disabledType: ['Net Payment', 'Net Deposit'],
|
||||
disabledType: [],
|
||||
defaultType: 'Payment',
|
||||
disableLegend: true,
|
||||
disableLabel: true,
|
||||
@@ -97,14 +97,14 @@ const timeGraphOptions: graphOptions[] = [
|
||||
description: 'StackedBarGraph',
|
||||
disabledSplit: ['Interval'],
|
||||
defaultSplit: 'Category',
|
||||
disabledType: [],
|
||||
disabledType: ['Net'],
|
||||
defaultType: 'Payment',
|
||||
},
|
||||
{
|
||||
description: 'LineGraph',
|
||||
disabledSplit: ['Interval'],
|
||||
defaultSplit: 'Category',
|
||||
disabledType: [],
|
||||
disabledType: ['Net'],
|
||||
defaultType: 'Payment',
|
||||
disableLegend: false,
|
||||
disableLabel: true,
|
||||
|
||||
@@ -16,10 +16,7 @@ import {
|
||||
amountToCurrency,
|
||||
amountToCurrencyNoDecimal,
|
||||
} from 'loot-core/src/shared/util';
|
||||
import {
|
||||
type balanceTypeOpType,
|
||||
type DataEntity,
|
||||
} from 'loot-core/src/types/models/reports';
|
||||
import { type DataEntity } from 'loot-core/src/types/models/reports';
|
||||
|
||||
import { usePrivacyMode } from '../../../hooks/usePrivacyMode';
|
||||
import { useResponsive } from '../../../ResponsiveProvider';
|
||||
@@ -36,8 +33,6 @@ type PayloadItem = {
|
||||
date: string;
|
||||
totalAssets: number | string;
|
||||
totalDebts: number | string;
|
||||
netAssets: number | string;
|
||||
netDebts: number | string;
|
||||
totalTotals: number | string;
|
||||
};
|
||||
};
|
||||
@@ -45,7 +40,7 @@ type PayloadItem = {
|
||||
type CustomTooltipProps = {
|
||||
active?: boolean;
|
||||
payload?: PayloadItem[];
|
||||
balanceTypeOp: balanceTypeOpType;
|
||||
balanceTypeOp: 'totalAssets' | 'totalTotals' | 'totalDebts';
|
||||
};
|
||||
|
||||
const CustomTooltip = ({
|
||||
@@ -79,22 +74,10 @@ const CustomTooltip = ({
|
||||
)}
|
||||
{['totalDebts', 'totalTotals'].includes(balanceTypeOp) && (
|
||||
<AlignedText
|
||||
left="Debts:"
|
||||
left="Debt:"
|
||||
right={amountToCurrency(payload[0].payload.totalDebts)}
|
||||
/>
|
||||
)}
|
||||
{['netAssets'].includes(balanceTypeOp) && (
|
||||
<AlignedText
|
||||
left="Net Assets:"
|
||||
right={amountToCurrency(payload[0].payload.netAssets)}
|
||||
/>
|
||||
)}
|
||||
{['netDebts'].includes(balanceTypeOp) && (
|
||||
<AlignedText
|
||||
left="Net Debts:"
|
||||
right={amountToCurrency(payload[0].payload.netDebts)}
|
||||
/>
|
||||
)}
|
||||
{['totalTotals'].includes(balanceTypeOp) && (
|
||||
<AlignedText
|
||||
left="Net:"
|
||||
@@ -149,7 +132,7 @@ const customLabel = ({
|
||||
type AreaGraphProps = {
|
||||
style?: CSSProperties;
|
||||
data: DataEntity;
|
||||
balanceTypeOp: balanceTypeOpType;
|
||||
balanceTypeOp: 'totalAssets' | 'totalTotals' | 'totalDebts';
|
||||
compact?: boolean;
|
||||
viewLabels: boolean;
|
||||
};
|
||||
|
||||
@@ -19,10 +19,7 @@ import {
|
||||
amountToCurrency,
|
||||
amountToCurrencyNoDecimal,
|
||||
} from 'loot-core/src/shared/util';
|
||||
import {
|
||||
type balanceTypeOpType,
|
||||
type DataEntity,
|
||||
} from 'loot-core/src/types/models/reports';
|
||||
import { type DataEntity } from 'loot-core/src/types/models/reports';
|
||||
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
|
||||
|
||||
import { useAccounts } from '../../../hooks/useAccounts';
|
||||
@@ -53,8 +50,6 @@ type PayloadItem = {
|
||||
name: string;
|
||||
totalAssets: number | string;
|
||||
totalDebts: number | string;
|
||||
netAssets: number | string;
|
||||
netDebts: number | string;
|
||||
totalTotals: number | string;
|
||||
networth: number | string;
|
||||
totalChange: number | string;
|
||||
@@ -65,7 +60,7 @@ type PayloadItem = {
|
||||
type CustomTooltipProps = {
|
||||
active?: boolean;
|
||||
payload?: PayloadItem[];
|
||||
balanceTypeOp?: balanceTypeOpType;
|
||||
balanceTypeOp?: 'totalAssets' | 'totalDebts' | 'totalTotals';
|
||||
yAxis?: string;
|
||||
};
|
||||
|
||||
@@ -101,22 +96,10 @@ const CustomTooltip = ({
|
||||
)}
|
||||
{['totalDebts', 'totalTotals'].includes(balanceTypeOp) && (
|
||||
<AlignedText
|
||||
left="Debts:"
|
||||
left="Debt:"
|
||||
right={amountToCurrency(payload[0].payload.totalDebts)}
|
||||
/>
|
||||
)}
|
||||
{['netAssets'].includes(balanceTypeOp) && (
|
||||
<AlignedText
|
||||
left="Net Assets:"
|
||||
right={amountToCurrency(payload[0].payload.netAssets)}
|
||||
/>
|
||||
)}
|
||||
{['netDebts'].includes(balanceTypeOp) && (
|
||||
<AlignedText
|
||||
left="Net Debts:"
|
||||
right={amountToCurrency(payload[0].payload.netDebts)}
|
||||
/>
|
||||
)}
|
||||
{['totalTotals'].includes(balanceTypeOp) && (
|
||||
<AlignedText
|
||||
left="Net:"
|
||||
@@ -154,7 +137,7 @@ type BarGraphProps = {
|
||||
data: DataEntity;
|
||||
filters: RuleConditionEntity[];
|
||||
groupBy: string;
|
||||
balanceTypeOp: balanceTypeOpType;
|
||||
balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
|
||||
compact?: boolean;
|
||||
viewLabels: boolean;
|
||||
showHiddenCategories?: boolean;
|
||||
@@ -184,15 +167,11 @@ export function BarGraph({
|
||||
const labelsMargin = viewLabels ? 30 : 0;
|
||||
|
||||
const getVal = obj => {
|
||||
if (balanceTypeOp === 'totalTotals' && groupBy === 'Interval') {
|
||||
if (balanceTypeOp === 'totalDebts') {
|
||||
return -1 * obj.totalDebts;
|
||||
} else {
|
||||
return obj.totalAssets;
|
||||
}
|
||||
|
||||
if (['totalDebts', 'netDebts'].includes(balanceTypeOp)) {
|
||||
return -1 * obj[balanceTypeOp];
|
||||
}
|
||||
|
||||
return obj[balanceTypeOp];
|
||||
};
|
||||
|
||||
const longestLabelLength = data[splitData]
|
||||
|
||||
@@ -4,10 +4,7 @@ import React, { useState } from 'react';
|
||||
import { PieChart, Pie, Cell, Sector, ResponsiveContainer } from 'recharts';
|
||||
|
||||
import { amountToCurrency } from 'loot-core/src/shared/util';
|
||||
import {
|
||||
type balanceTypeOpType,
|
||||
type DataEntity,
|
||||
} from 'loot-core/src/types/models/reports';
|
||||
import { type DataEntity } from 'loot-core/src/types/models/reports';
|
||||
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
|
||||
|
||||
import { useAccounts } from '../../../hooks/useAccounts';
|
||||
@@ -184,7 +181,7 @@ type DonutGraphProps = {
|
||||
data: DataEntity;
|
||||
filters: RuleConditionEntity[];
|
||||
groupBy: string;
|
||||
balanceTypeOp: balanceTypeOpType;
|
||||
balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
|
||||
compact?: boolean;
|
||||
viewLabels: boolean;
|
||||
showHiddenCategories?: boolean;
|
||||
@@ -212,7 +209,7 @@ export function DonutGraph({
|
||||
const [pointer, setPointer] = useState('');
|
||||
|
||||
const getVal = obj => {
|
||||
if (['totalDebts', 'netDebts'].includes(balanceTypeOp)) {
|
||||
if (balanceTypeOp === 'totalDebts') {
|
||||
return -1 * obj[balanceTypeOp];
|
||||
} else {
|
||||
return obj[balanceTypeOp];
|
||||
|
||||
@@ -16,10 +16,7 @@ import {
|
||||
amountToCurrency,
|
||||
amountToCurrencyNoDecimal,
|
||||
} from 'loot-core/src/shared/util';
|
||||
import {
|
||||
type balanceTypeOpType,
|
||||
type DataEntity,
|
||||
} from 'loot-core/types/models/reports';
|
||||
import { type DataEntity } from 'loot-core/types/models/reports';
|
||||
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
|
||||
|
||||
import { useAccounts } from '../../../hooks/useAccounts';
|
||||
@@ -118,7 +115,7 @@ type LineGraphProps = {
|
||||
filters: RuleConditionEntity[];
|
||||
groupBy: string;
|
||||
compact?: boolean;
|
||||
balanceTypeOp: balanceTypeOpType;
|
||||
balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
|
||||
showHiddenCategories?: boolean;
|
||||
showOffBudget?: boolean;
|
||||
interval?: string;
|
||||
|
||||
@@ -17,10 +17,7 @@ import {
|
||||
amountToCurrency,
|
||||
amountToCurrencyNoDecimal,
|
||||
} from 'loot-core/src/shared/util';
|
||||
import {
|
||||
type balanceTypeOpType,
|
||||
type DataEntity,
|
||||
} from 'loot-core/src/types/models/reports';
|
||||
import { type DataEntity } from 'loot-core/src/types/models/reports';
|
||||
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
|
||||
|
||||
import { useAccounts } from '../../../hooks/useAccounts';
|
||||
@@ -147,7 +144,7 @@ type StackedBarGraphProps = {
|
||||
groupBy: string;
|
||||
compact?: boolean;
|
||||
viewLabels: boolean;
|
||||
balanceTypeOp: balanceTypeOpType;
|
||||
balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
|
||||
showHiddenCategories?: boolean;
|
||||
showOffBudget?: boolean;
|
||||
interval?: string;
|
||||
@@ -197,7 +194,6 @@ export function StackedBarGraph({
|
||||
data={data.intervalData}
|
||||
margin={{ top: 0, right: 0, left: leftMargin, bottom: 10 }}
|
||||
style={{ cursor: pointer }}
|
||||
stackOffset="sign" //stacked by sign
|
||||
>
|
||||
{(!isNarrowWidth || !compact) && (
|
||||
<Tooltip
|
||||
|
||||
@@ -4,7 +4,6 @@ import * as monthUtils from 'loot-core/src/shared/months';
|
||||
import { type AccountEntity } from 'loot-core/types/models/account';
|
||||
import { type CategoryEntity } from 'loot-core/types/models/category';
|
||||
import { type CategoryGroupEntity } from 'loot-core/types/models/category-group';
|
||||
import { type balanceTypeOpType } from 'loot-core/types/models/reports';
|
||||
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
|
||||
|
||||
import { ReportOptions } from '../ReportOptions';
|
||||
@@ -13,7 +12,7 @@ type showActivityProps = {
|
||||
navigate: NavigateFunction;
|
||||
categories: { list: CategoryEntity[]; grouped: CategoryGroupEntity[] };
|
||||
accounts: AccountEntity[];
|
||||
balanceTypeOp: balanceTypeOpType;
|
||||
balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
|
||||
filters: RuleConditionEntity[];
|
||||
showHiddenCategories: boolean;
|
||||
showOffBudget: boolean;
|
||||
@@ -51,7 +50,7 @@ export function showActivity({
|
||||
'FromDate') as 'dayFromDate' | 'monthFromDate' | 'yearFromDate');
|
||||
const isDateOp = interval === 'Weekly' || type !== 'time';
|
||||
|
||||
const filterConditions = [
|
||||
const conditions = [
|
||||
...filters,
|
||||
id && { field, op: 'is', value: id, type: 'id' },
|
||||
{
|
||||
@@ -67,9 +66,8 @@ export function showActivity({
|
||||
options: { date: true },
|
||||
},
|
||||
!(
|
||||
['netAssets', 'netDebts'].includes(balanceTypeOp) ||
|
||||
(balanceTypeOp === 'totalTotals' &&
|
||||
(type === 'totals' || type === 'time'))
|
||||
balanceTypeOp === 'totalTotals' &&
|
||||
(type === 'totals' || type === 'time')
|
||||
) && {
|
||||
field: 'amount',
|
||||
op: 'gte',
|
||||
@@ -98,7 +96,7 @@ export function showActivity({
|
||||
navigate('/accounts', {
|
||||
state: {
|
||||
goBack: true,
|
||||
filterConditions,
|
||||
conditions,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import React, {
|
||||
import {
|
||||
type GroupedEntity,
|
||||
type DataEntity,
|
||||
type balanceTypeOpType,
|
||||
} from 'loot-core/src/types/models/reports';
|
||||
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
|
||||
|
||||
@@ -29,7 +28,7 @@ type ReportTableProps = {
|
||||
totalScrollRef: RefObject<HTMLDivElement>;
|
||||
handleScroll: UIEventHandler<HTMLDivElement>;
|
||||
groupBy: string;
|
||||
balanceTypeOp: balanceTypeOpType;
|
||||
balanceTypeOp: 'totalDebts' | 'totalTotals' | 'totalAssets';
|
||||
data: DataEntity;
|
||||
filters?: RuleConditionEntity[];
|
||||
mode: string;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import React, { type RefObject, type UIEventHandler } from 'react';
|
||||
|
||||
import {
|
||||
type balanceTypeOpType,
|
||||
type IntervalEntity,
|
||||
} from 'loot-core/src/types/models/reports';
|
||||
import { type IntervalEntity } from 'loot-core/src/types/models/reports';
|
||||
|
||||
import { theme } from '../../../../style';
|
||||
import { type CSSProperties } from '../../../../style/types';
|
||||
@@ -15,7 +12,7 @@ type ReportTableHeaderProps = {
|
||||
groupBy: string;
|
||||
interval: string;
|
||||
data: IntervalEntity[];
|
||||
balanceTypeOp: balanceTypeOpType;
|
||||
balanceTypeOp: 'totalDebts' | 'totalTotals' | 'totalAssets';
|
||||
headerScrollRef: RefObject<HTMLDivElement>;
|
||||
handleScroll: UIEventHandler<HTMLDivElement>;
|
||||
compact: boolean;
|
||||
|
||||
@@ -38,8 +38,6 @@ export function ReportTableList({
|
||||
date: interval.date,
|
||||
totalAssets: interval.totalAssets,
|
||||
totalDebts: interval.totalDebts,
|
||||
netAssets: interval.netAssets,
|
||||
netDebts: interval.netDebts,
|
||||
totalTotals: interval.totalTotals,
|
||||
intervalData: [],
|
||||
categories: [],
|
||||
|
||||
@@ -5,10 +5,7 @@ import {
|
||||
amountToInteger,
|
||||
integerToCurrency,
|
||||
} from 'loot-core/src/shared/util';
|
||||
import {
|
||||
type balanceTypeOpType,
|
||||
type GroupedEntity,
|
||||
} from 'loot-core/types/models/reports';
|
||||
import { type GroupedEntity } from 'loot-core/types/models/reports';
|
||||
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
|
||||
|
||||
import { useAccounts } from '../../../../hooks/useAccounts';
|
||||
@@ -16,14 +13,13 @@ import { useCategories } from '../../../../hooks/useCategories';
|
||||
import { useNavigate } from '../../../../hooks/useNavigate';
|
||||
import { useResponsive } from '../../../../ResponsiveProvider';
|
||||
import { type CSSProperties, theme } from '../../../../style';
|
||||
import { Text } from '../../../common/Text';
|
||||
import { View } from '../../../common/View';
|
||||
import { Row, Cell } from '../../../table';
|
||||
import { showActivity } from '../showActivity';
|
||||
|
||||
type ReportTableRowProps = {
|
||||
item: GroupedEntity;
|
||||
balanceTypeOp: balanceTypeOpType;
|
||||
balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
|
||||
groupBy: string;
|
||||
mode: string;
|
||||
filters?: RuleConditionEntity[];
|
||||
@@ -129,9 +125,7 @@ export const ReportTableRow = memo(
|
||||
style={{
|
||||
minWidth: compact ? 50 : 85,
|
||||
}}
|
||||
unexposedContent={({ value }) => (
|
||||
<Text style={hoverUnderline}>{value}</Text>
|
||||
)}
|
||||
linkStyle={hoverUnderline}
|
||||
valueStyle={compactStyle}
|
||||
value={amountToCurrency(intervalItem[balanceTypeOp])}
|
||||
title={
|
||||
@@ -179,9 +173,7 @@ export const ReportTableRow = memo(
|
||||
style={{
|
||||
minWidth: compact ? 50 : 85,
|
||||
}}
|
||||
unexposedContent={({ value }) => (
|
||||
<Text style={hoverUnderline}>{value}</Text>
|
||||
)}
|
||||
linkStyle={hoverUnderline}
|
||||
valueStyle={compactStyle}
|
||||
onClick={() =>
|
||||
!isNarrowWidth &&
|
||||
@@ -216,9 +208,7 @@ export const ReportTableRow = memo(
|
||||
style={{
|
||||
minWidth: compact ? 50 : 85,
|
||||
}}
|
||||
unexposedContent={({ value }) => (
|
||||
<Text style={hoverUnderline}>{value}</Text>
|
||||
)}
|
||||
linkStyle={hoverUnderline}
|
||||
valueStyle={compactStyle}
|
||||
onClick={() =>
|
||||
!isNarrowWidth &&
|
||||
@@ -254,9 +244,7 @@ export const ReportTableRow = memo(
|
||||
fontWeight: 600,
|
||||
minWidth: compact ? 50 : 85,
|
||||
}}
|
||||
unexposedContent={({ value }) => (
|
||||
<Text style={hoverUnderline}>{value}</Text>
|
||||
)}
|
||||
linkStyle={hoverUnderline}
|
||||
valueStyle={compactStyle}
|
||||
onClick={() =>
|
||||
!isNarrowWidth &&
|
||||
|
||||
@@ -85,8 +85,6 @@ export function ReportTableTotals({
|
||||
intervalData: data.intervalData,
|
||||
totalAssets: data.totalAssets,
|
||||
totalDebts: data.totalDebts,
|
||||
netAssets: data.netAssets,
|
||||
netDebts: data.netDebts,
|
||||
totalTotals: data.totalTotals,
|
||||
};
|
||||
|
||||
|
||||
@@ -28,12 +28,12 @@ import { useReport } from '../useReport';
|
||||
|
||||
export function CashFlow() {
|
||||
const {
|
||||
conditions,
|
||||
filters,
|
||||
conditionsOp,
|
||||
onApply: onApplyFilter,
|
||||
onDelete: onDeleteFilter,
|
||||
onUpdate: onUpdateFilter,
|
||||
onConditionsOpChange,
|
||||
onCondOpChange,
|
||||
} = useFilters<RuleConditionEntity>();
|
||||
|
||||
const [allMonths, setAllMonths] = useState<null | Array<{
|
||||
@@ -55,8 +55,8 @@ export function CashFlow() {
|
||||
});
|
||||
|
||||
const params = useMemo(
|
||||
() => cashFlowByDate(start, end, isConcise, conditions, conditionsOp),
|
||||
[start, end, isConcise, conditions, conditionsOp],
|
||||
() => cashFlowByDate(start, end, isConcise, filters, conditionsOp),
|
||||
[start, end, isConcise, filters, conditionsOp],
|
||||
);
|
||||
const data = useReport('cash_flow', params);
|
||||
|
||||
@@ -129,11 +129,11 @@ export function CashFlow() {
|
||||
show1Month
|
||||
onChangeDates={onChangeDates}
|
||||
onApply={onApplyFilter}
|
||||
filters={conditions}
|
||||
filters={filters}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onDeleteFilter={onDeleteFilter}
|
||||
conditionsOp={conditionsOp}
|
||||
onConditionsOpChange={onConditionsOpChange}
|
||||
onCondOpChange={onCondOpChange}
|
||||
headerPrefixItems={undefined}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -8,7 +8,6 @@ import * as monthUtils from 'loot-core/src/shared/months';
|
||||
import { amountToCurrency } from 'loot-core/src/shared/util';
|
||||
import { type CategoryEntity } from 'loot-core/types/models/category';
|
||||
import {
|
||||
type balanceTypeOpType,
|
||||
type CustomReportEntity,
|
||||
type DataEntity,
|
||||
} from 'loot-core/types/models/reports';
|
||||
@@ -69,12 +68,12 @@ export function CustomReport() {
|
||||
useLocalPref('reportsViewLabel');
|
||||
|
||||
const {
|
||||
conditions,
|
||||
filters,
|
||||
conditionsOp,
|
||||
onApply: onApplyFilter,
|
||||
onDelete: onDeleteFilter,
|
||||
onUpdate: onUpdateFilter,
|
||||
onConditionsOpChange,
|
||||
onCondOpChange,
|
||||
} = useFilters();
|
||||
|
||||
const location = useLocation();
|
||||
@@ -249,7 +248,7 @@ export function CustomReport() {
|
||||
}
|
||||
}, [interval, startDate, endDate, firstDayOfWeekIdx]);
|
||||
|
||||
const balanceTypeOp: balanceTypeOpType =
|
||||
const balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals' =
|
||||
ReportOptions.balanceTypeMap.get(balanceType) || 'totalDebts';
|
||||
const payees = usePayees();
|
||||
const accounts = useAccounts();
|
||||
@@ -261,7 +260,7 @@ export function CustomReport() {
|
||||
interval,
|
||||
categories,
|
||||
selectedCategories,
|
||||
conditions,
|
||||
conditions: filters,
|
||||
conditionsOp,
|
||||
showEmpty,
|
||||
showOffBudget,
|
||||
@@ -277,7 +276,7 @@ export function CustomReport() {
|
||||
balanceTypeOp,
|
||||
categories,
|
||||
selectedCategories,
|
||||
conditions,
|
||||
filters,
|
||||
conditionsOp,
|
||||
showEmpty,
|
||||
showOffBudget,
|
||||
@@ -294,7 +293,7 @@ export function CustomReport() {
|
||||
interval,
|
||||
categories,
|
||||
selectedCategories,
|
||||
conditions,
|
||||
conditions: filters,
|
||||
conditionsOp,
|
||||
showEmpty,
|
||||
showOffBudget,
|
||||
@@ -318,7 +317,7 @@ export function CustomReport() {
|
||||
selectedCategories,
|
||||
payees,
|
||||
accounts,
|
||||
conditions,
|
||||
filters,
|
||||
conditionsOp,
|
||||
showEmpty,
|
||||
showOffBudget,
|
||||
@@ -350,7 +349,7 @@ export function CustomReport() {
|
||||
showUncategorized,
|
||||
selectedCategories,
|
||||
graphType,
|
||||
conditions,
|
||||
conditions: filters,
|
||||
conditionsOp,
|
||||
};
|
||||
|
||||
@@ -495,7 +494,7 @@ export function CustomReport() {
|
||||
setGraphType(input.graphType);
|
||||
onApplyFilter(null);
|
||||
(input.conditions || []).forEach(condition => onApplyFilter(condition));
|
||||
onConditionsOpChange(input.conditionsOp);
|
||||
onCondOpChange(input.conditionsOp);
|
||||
};
|
||||
|
||||
const onReportChange = ({
|
||||
@@ -624,7 +623,7 @@ export function CustomReport() {
|
||||
defaultItems={defaultItems}
|
||||
/>
|
||||
)}
|
||||
{conditions && conditions.length > 0 && (
|
||||
{filters && filters.length > 0 && (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 10,
|
||||
@@ -636,11 +635,11 @@ export function CustomReport() {
|
||||
}}
|
||||
>
|
||||
<AppliedFilters
|
||||
conditions={conditions}
|
||||
filters={filters}
|
||||
onUpdate={(oldFilter, newFilter) => {
|
||||
setSessionReport(
|
||||
'conditions',
|
||||
conditions.map(f => (f === oldFilter ? newFilter : f)),
|
||||
filters.map(f => (f === oldFilter ? newFilter : f)),
|
||||
);
|
||||
onReportChange({ type: 'modify' });
|
||||
onUpdateFilter(oldFilter, newFilter);
|
||||
@@ -648,14 +647,14 @@ export function CustomReport() {
|
||||
onDelete={deletedFilter => {
|
||||
setSessionReport(
|
||||
'conditions',
|
||||
conditions.filter(f => f !== deletedFilter),
|
||||
filters.filter(f => f !== deletedFilter),
|
||||
);
|
||||
onDeleteFilter(deletedFilter);
|
||||
onReportChange({ type: 'modify' });
|
||||
}}
|
||||
conditionsOp={conditionsOp}
|
||||
onConditionsOpChange={co => {
|
||||
onConditionsOpChange(co);
|
||||
onCondOpChange={co => {
|
||||
onCondOpChange(co);
|
||||
onReportChange({ type: 'modify' });
|
||||
}}
|
||||
/>
|
||||
@@ -693,7 +692,7 @@ export function CustomReport() {
|
||||
right={
|
||||
<Text>
|
||||
<PrivacyFilter blurIntensity={5}>
|
||||
{amountToCurrency(data[balanceTypeOp])}
|
||||
{amountToCurrency(Math.abs(data[balanceTypeOp]))}
|
||||
</PrivacyFilter>
|
||||
</Text>
|
||||
}
|
||||
@@ -705,7 +704,7 @@ export function CustomReport() {
|
||||
{dataCheck ? (
|
||||
<ChooseGraph
|
||||
data={data}
|
||||
filters={conditions}
|
||||
filters={filters}
|
||||
mode={mode}
|
||||
graphType={graphType}
|
||||
balanceType={balanceType}
|
||||
|
||||
@@ -26,13 +26,13 @@ import { fromDateRepr } from '../util';
|
||||
export function NetWorth() {
|
||||
const accounts = useAccounts();
|
||||
const {
|
||||
conditions,
|
||||
filters,
|
||||
saved,
|
||||
conditionsOp,
|
||||
onApply: onApplyFilter,
|
||||
onDelete: onDeleteFilter,
|
||||
onUpdate: onUpdateFilter,
|
||||
onConditionsOpChange,
|
||||
onCondOpChange,
|
||||
} = useFilters();
|
||||
|
||||
const [allMonths, setAllMonths] = useState(null);
|
||||
@@ -42,8 +42,8 @@ export function NetWorth() {
|
||||
const [end, setEnd] = useState(monthUtils.currentMonth());
|
||||
|
||||
const params = useMemo(
|
||||
() => netWorthSpreadsheet(start, end, accounts, conditions, conditionsOp),
|
||||
[start, end, accounts, conditions, conditionsOp],
|
||||
() => netWorthSpreadsheet(start, end, accounts, filters, conditionsOp),
|
||||
[start, end, accounts, filters, conditionsOp],
|
||||
);
|
||||
const data = useReport('net_worth', params);
|
||||
useEffect(() => {
|
||||
@@ -108,13 +108,13 @@ export function NetWorth() {
|
||||
start={start}
|
||||
end={end}
|
||||
onChangeDates={onChangeDates}
|
||||
filters={conditions}
|
||||
filters={filters}
|
||||
saved={saved}
|
||||
onApply={onApplyFilter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onDeleteFilter={onDeleteFilter}
|
||||
conditionsOp={conditionsOp}
|
||||
onConditionsOpChange={onConditionsOpChange}
|
||||
onCondOpChange={onCondOpChange}
|
||||
/>
|
||||
|
||||
<View
|
||||
|
||||
@@ -29,12 +29,12 @@ export function Spending() {
|
||||
const categories = useCategories();
|
||||
|
||||
const {
|
||||
conditions,
|
||||
filters,
|
||||
conditionsOp,
|
||||
onApply: onApplyFilter,
|
||||
onDelete: onDeleteFilter,
|
||||
onUpdate: onUpdateFilter,
|
||||
onConditionsOpChange,
|
||||
onCondOpChange,
|
||||
} = useFilters<RuleConditionEntity>();
|
||||
|
||||
const [dataCheck, setDataCheck] = useState(false);
|
||||
@@ -44,11 +44,11 @@ export function Spending() {
|
||||
setDataCheck(false);
|
||||
return createSpendingSpreadsheet({
|
||||
categories,
|
||||
conditions,
|
||||
conditions: filters,
|
||||
conditionsOp,
|
||||
setDataCheck,
|
||||
});
|
||||
}, [categories, conditions, conditionsOp]);
|
||||
}, [categories, filters, conditionsOp]);
|
||||
|
||||
const data = useReport('default', getGraphData);
|
||||
const navigate = useNavigate();
|
||||
@@ -100,7 +100,7 @@ export function Spending() {
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{conditions && (
|
||||
{filters && (
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<FilterButton
|
||||
onApply={onApplyFilter}
|
||||
@@ -126,7 +126,7 @@ export function Spending() {
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{conditions && conditions.length > 0 && (
|
||||
{filters && filters.length > 0 && (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 10,
|
||||
@@ -139,11 +139,11 @@ export function Spending() {
|
||||
}}
|
||||
>
|
||||
<AppliedFilters
|
||||
conditions={conditions}
|
||||
filters={filters}
|
||||
onUpdate={onUpdateFilter}
|
||||
onDelete={onDeleteFilter}
|
||||
conditionsOp={conditionsOp}
|
||||
onConditionsOpChange={onConditionsOpChange}
|
||||
onCondOpChange={onCondOpChange}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
type LegendEntity,
|
||||
type IntervalEntity,
|
||||
type GroupedEntity,
|
||||
type balanceTypeOpType,
|
||||
} from 'loot-core/src/types/models/reports';
|
||||
|
||||
import { theme } from '../../../style';
|
||||
@@ -13,7 +12,7 @@ export function calculateLegend(
|
||||
calcDataFiltered: GroupedEntity[],
|
||||
groupBy: string,
|
||||
graphType?: string,
|
||||
balanceTypeOp?: balanceTypeOpType,
|
||||
balanceTypeOp?: 'totalAssets' | 'totalDebts' | 'totalTotals',
|
||||
): LegendEntity[] {
|
||||
const colorScale = getColorScale('qualitative');
|
||||
const chooseData =
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
type CategoryGroupEntity,
|
||||
} from 'loot-core/src/types/models';
|
||||
import {
|
||||
type balanceTypeOpType,
|
||||
type DataEntity,
|
||||
type GroupedEntity,
|
||||
type IntervalEntity,
|
||||
@@ -47,7 +46,7 @@ export type createCustomSpreadsheetProps = {
|
||||
showHiddenCategories: boolean;
|
||||
showUncategorized: boolean;
|
||||
groupBy?: string;
|
||||
balanceTypeOp?: balanceTypeOpType;
|
||||
balanceTypeOp?: 'totalAssets' | 'totalDebts' | 'totalTotals';
|
||||
payees?: PayeeEntity[];
|
||||
accounts?: AccountEntity[];
|
||||
graphType?: string;
|
||||
@@ -154,16 +153,11 @@ export function createCustomSpreadsheet({
|
||||
|
||||
let totalAssets = 0;
|
||||
let totalDebts = 0;
|
||||
let netAssets = 0;
|
||||
let netDebts = 0;
|
||||
|
||||
const intervalData = intervals.reduce(
|
||||
(arr: IntervalEntity[], intervalItem, index) => {
|
||||
let perIntervalAssets = 0;
|
||||
let perIntervalDebts = 0;
|
||||
let perIntervalNetAssets = 0;
|
||||
let perIntervalNetDebts = 0;
|
||||
let perIntervalTotals = 0;
|
||||
const stacked: Record<string, number> = {};
|
||||
|
||||
groupByList.map(item => {
|
||||
@@ -199,43 +193,20 @@ export function createCustomSpreadsheet({
|
||||
.reduce((a, v) => (a = a + v.amount), 0);
|
||||
perIntervalDebts += intervalDebts;
|
||||
|
||||
const netAmounts = intervalAssets + intervalDebts;
|
||||
|
||||
if (balanceTypeOp === 'totalAssets') {
|
||||
stackAmounts += intervalAssets;
|
||||
}
|
||||
if (balanceTypeOp === 'totalDebts') {
|
||||
stackAmounts += Math.abs(intervalDebts);
|
||||
}
|
||||
if (balanceTypeOp === 'netAssets') {
|
||||
stackAmounts += netAmounts > 0 ? netAmounts : 0;
|
||||
}
|
||||
if (balanceTypeOp === 'netDebts') {
|
||||
stackAmounts = netAmounts < 0 ? Math.abs(netAmounts) : 0;
|
||||
}
|
||||
if (balanceTypeOp === 'totalTotals') {
|
||||
stackAmounts += netAmounts;
|
||||
stackAmounts += intervalDebts;
|
||||
}
|
||||
if (stackAmounts !== 0) {
|
||||
stacked[item.name] = integerToAmount(stackAmounts);
|
||||
stacked[item.name] = integerToAmount(Math.abs(stackAmounts));
|
||||
}
|
||||
|
||||
perIntervalNetAssets =
|
||||
netAmounts > 0
|
||||
? perIntervalNetAssets + netAmounts
|
||||
: perIntervalNetAssets;
|
||||
perIntervalNetDebts =
|
||||
netAmounts < 0
|
||||
? perIntervalNetDebts + netAmounts
|
||||
: perIntervalNetDebts;
|
||||
perIntervalTotals += netAmounts;
|
||||
|
||||
return null;
|
||||
});
|
||||
totalAssets += perIntervalAssets;
|
||||
totalDebts += perIntervalDebts;
|
||||
netAssets += perIntervalNetAssets;
|
||||
netDebts += perIntervalNetDebts;
|
||||
|
||||
arr.push({
|
||||
date: d.format(
|
||||
@@ -248,11 +219,9 @@ export function createCustomSpreadsheet({
|
||||
index + 1 === intervals.length
|
||||
? endDate
|
||||
: monthUtils.subDays(intervals[index + 1], 1),
|
||||
totalAssets: integerToAmount(perIntervalAssets),
|
||||
totalDebts: integerToAmount(perIntervalDebts),
|
||||
netAssets: integerToAmount(perIntervalNetAssets),
|
||||
netDebts: integerToAmount(perIntervalNetDebts),
|
||||
totalTotals: integerToAmount(perIntervalTotals),
|
||||
totalAssets: integerToAmount(perIntervalAssets),
|
||||
totalTotals: integerToAmount(perIntervalDebts + perIntervalAssets),
|
||||
});
|
||||
|
||||
return arr;
|
||||
@@ -293,10 +262,8 @@ export function createCustomSpreadsheet({
|
||||
legend,
|
||||
startDate,
|
||||
endDate,
|
||||
totalAssets: integerToAmount(totalAssets),
|
||||
totalDebts: integerToAmount(totalDebts),
|
||||
netAssets: integerToAmount(netAssets),
|
||||
netDebts: integerToAmount(netDebts),
|
||||
totalAssets: integerToAmount(totalAssets),
|
||||
totalTotals: integerToAmount(totalAssets + totalDebts),
|
||||
});
|
||||
setDataCheck?.(true);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
type balanceTypeOpType,
|
||||
type GroupedEntity,
|
||||
} from 'loot-core/src/types/models/reports';
|
||||
import { type GroupedEntity } from 'loot-core/src/types/models/reports';
|
||||
|
||||
export function filterEmptyRows({
|
||||
showEmpty,
|
||||
@@ -10,7 +7,7 @@ export function filterEmptyRows({
|
||||
}: {
|
||||
showEmpty: boolean;
|
||||
data: GroupedEntity;
|
||||
balanceTypeOp?: balanceTypeOpType;
|
||||
balanceTypeOp?: 'totalAssets' | 'totalDebts' | 'totalTotals';
|
||||
}): boolean {
|
||||
let showHide: boolean;
|
||||
if (balanceTypeOp === 'totalTotals') {
|
||||
|
||||
@@ -111,16 +111,11 @@ export function createGroupedSpreadsheet({
|
||||
group => {
|
||||
let totalAssets = 0;
|
||||
let totalDebts = 0;
|
||||
let netAssets = 0;
|
||||
let netDebts = 0;
|
||||
|
||||
const intervalData = intervals.reduce(
|
||||
(arr: IntervalEntity[], intervalItem) => {
|
||||
let groupedAssets = 0;
|
||||
let groupedDebts = 0;
|
||||
let groupedNetAssets = 0;
|
||||
let groupedNetDebts = 0;
|
||||
let groupedTotals = 0;
|
||||
|
||||
if (!group.categories) {
|
||||
return [];
|
||||
@@ -156,32 +151,16 @@ export function createGroupedSpreadsheet({
|
||||
)
|
||||
.reduce((a, v) => (a = a + v.amount), 0);
|
||||
groupedDebts += intervalDebts;
|
||||
|
||||
const intervalTotals = intervalAssets + intervalDebts;
|
||||
|
||||
groupedNetAssets =
|
||||
intervalTotals > 0
|
||||
? groupedNetAssets + intervalTotals
|
||||
: groupedNetAssets;
|
||||
groupedNetDebts =
|
||||
intervalTotals < 0
|
||||
? groupedNetDebts + intervalTotals
|
||||
: groupedNetDebts;
|
||||
groupedTotals += intervalTotals;
|
||||
});
|
||||
|
||||
totalAssets += groupedAssets;
|
||||
totalDebts += groupedDebts;
|
||||
netAssets += groupedNetAssets;
|
||||
netDebts += groupedNetDebts;
|
||||
|
||||
arr.push({
|
||||
date: intervalItem,
|
||||
totalAssets: integerToAmount(groupedAssets),
|
||||
totalDebts: integerToAmount(groupedDebts),
|
||||
netAssets: integerToAmount(groupedNetAssets),
|
||||
netDebts: integerToAmount(groupedNetDebts),
|
||||
totalTotals: integerToAmount(groupedTotals),
|
||||
totalTotals: integerToAmount(groupedDebts + groupedAssets),
|
||||
});
|
||||
|
||||
return arr;
|
||||
@@ -212,8 +191,6 @@ export function createGroupedSpreadsheet({
|
||||
name: group.name,
|
||||
totalAssets: integerToAmount(totalAssets),
|
||||
totalDebts: integerToAmount(totalDebts),
|
||||
netAssets: integerToAmount(netAssets),
|
||||
netDebts: integerToAmount(netDebts),
|
||||
totalTotals: integerToAmount(totalAssets + totalDebts),
|
||||
intervalData,
|
||||
categories:
|
||||
|
||||
@@ -73,18 +73,14 @@ export function recalculate({
|
||||
.reduce((a, v) => (a = a + v.amount), 0);
|
||||
totalDebts += intervalDebts;
|
||||
|
||||
const intervalTotals = intervalAssets + intervalDebts;
|
||||
|
||||
const change = last
|
||||
? intervalTotals - amountToInteger(last.totalTotals)
|
||||
? intervalAssets + intervalDebts - amountToInteger(last.totalTotals)
|
||||
: 0;
|
||||
|
||||
arr.push({
|
||||
totalAssets: integerToAmount(intervalAssets),
|
||||
totalDebts: integerToAmount(intervalDebts),
|
||||
netAssets: intervalTotals > 0 ? integerToAmount(intervalTotals) : 0,
|
||||
netDebts: intervalTotals < 0 ? integerToAmount(intervalTotals) : 0,
|
||||
totalTotals: integerToAmount(intervalTotals),
|
||||
totalTotals: integerToAmount(intervalAssets + intervalDebts),
|
||||
change,
|
||||
intervalStartDate: index === 0 ? startDate : intervalItem,
|
||||
intervalEndDate:
|
||||
@@ -98,16 +94,12 @@ export function recalculate({
|
||||
[],
|
||||
);
|
||||
|
||||
const totalTotals = totalAssets + totalDebts;
|
||||
|
||||
return {
|
||||
id: item.id || '',
|
||||
name: item.name,
|
||||
totalAssets: integerToAmount(totalAssets),
|
||||
totalDebts: integerToAmount(totalDebts),
|
||||
netAssets: totalTotals > 0 ? integerToAmount(totalTotals) : 0,
|
||||
netDebts: totalTotals < 0 ? integerToAmount(totalTotals) : 0,
|
||||
totalTotals: integerToAmount(totalTotals),
|
||||
totalTotals: integerToAmount(totalAssets + totalDebts),
|
||||
intervalData,
|
||||
};
|
||||
}
|
||||
|
||||