Skip to content

Commit 02f2e3a

Browse files
committed
account balance
1 parent a719498 commit 02f2e3a

File tree

13 files changed

+106
-36
lines changed

13 files changed

+106
-36
lines changed

packages/core/pages/AccountPage.tsx

+36-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ interface Props {
1616
accountId: string
1717
}
1818

19+
interface Row extends Required<Transaction.Props> {
20+
id: string
21+
balance: number
22+
}
23+
1924
const path = '/accounts/:accountId'
2025
const route = docuri.route<Props, string>(path)
2126

@@ -62,7 +67,6 @@ export const AccountPage = Object.assign(
6267
[deleteTransaction]
6368
)
6469

65-
type Row = Transaction
6670
const columns = useMemo<Array<TableColumn<Row>>>(
6771
() => [
6872
{
@@ -99,6 +103,14 @@ export const AccountPage = Object.assign(
99103
format: (value: number) =>
100104
formatCurrency(intl, value, account ? account.currencyCode : defaultCurrency),
101105
},
106+
{
107+
dataIndex: 'balance',
108+
align: 'right',
109+
width: 100,
110+
title: intl.formatMessage(messages.colBalance),
111+
format: (value: number) =>
112+
formatCurrency(intl, value, account ? account.currencyCode : defaultCurrency),
113+
},
102114
],
103115
[intl]
104116
)
@@ -113,9 +125,23 @@ export const AccountPage = Object.assign(
113125
}
114126

115127
const transactions = getTransactions(account.id)
128+
.sort((a, b) => a.time.getTime() - b.time.getTime())
129+
.reduce(
130+
(txs, tx) => {
131+
const prevBalance = txs.length === 0 ? 0 : txs[txs.length - 1].balance
132+
txs.push({ ...tx, balance: prevBalance + tx.amount })
133+
return txs
134+
},
135+
[] as Row[]
136+
)
137+
.reverse()
116138

117139
const title = account.name
118140
const subtitle = bank.name
141+
const tableTitle = intl.formatMessage(messages.tableTitle, {
142+
name: account.name,
143+
balance: formatCurrency(intl, account.balance, account.currencyCode),
144+
})
119145

120146
return (
121147
<Page
@@ -128,6 +154,7 @@ export const AccountPage = Object.assign(
128154
}}
129155
>
130156
<Table
157+
titleText={tableTitle}
131158
rowKey={'id'}
132159
columns={columns}
133160
emptyText={intl.formatMessage(messages.noTransactions)}
@@ -160,6 +187,10 @@ const messages = defineMessages({
160187
id: 'AccountPage.getTransactionsComplete',
161188
defaultMessage: 'Downloaded transactions for account {name}',
162189
},
190+
tableTitle: {
191+
id: 'AccountPage.tableTitle',
192+
defaultMessage: '{name}: {balance}',
193+
},
163194
noTransactions: {
164195
id: 'AccountPage.noTransactions',
165196
defaultMessage: 'No transactions',
@@ -172,6 +203,10 @@ const messages = defineMessages({
172203
id: 'AccountPage.colAmount',
173204
defaultMessage: 'Amount',
174205
},
206+
colBalance: {
207+
id: 'AccountPage.colBalance',
208+
defaultMessage: 'Balance',
209+
},
175210
colTime: {
176211
id: 'AccountPage.colTime',
177212
defaultMessage: 'Date',

packages/core/pages/AccountsPage.tsx

+18-10
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ import docuri from 'docuri'
66
import React, { useCallback, useMemo } from 'react'
77
import { defineMessages } from 'react-intl'
88
import { actions } from '../actions'
9-
import { ActionDesc, TableColumn, useAction, useIntl, useSelector, useUi } from '../context'
9+
import {
10+
ActionDesc,
11+
ImageId,
12+
TableColumn,
13+
useAction,
14+
useIntl,
15+
useSelector,
16+
useUi,
17+
} from '../context'
1018
import { selectors } from '../reducers'
1119
import { thunks } from '../thunks'
1220

@@ -97,7 +105,7 @@ const BankTable = Object.assign(
97105
width: 30,
98106
align: 'center',
99107
title: intl.formatMessage(messages.colVisible),
100-
render: (text: string, account: Row) => <Text>*</Text>,
108+
render: (visible: boolean) => <Text>*</Text>,
101109
},
102110
{
103111
dataIndex: 'number', // TODO
@@ -110,26 +118,26 @@ const BankTable = Object.assign(
110118
dataIndex: 'iconId',
111119
title: '',
112120
width: 30,
113-
render: (text: string, account: Row) => (
114-
<Image id={account.iconId || bank.iconId} size='1.5em' />
121+
render: (iconId: ImageId, account: Row) => (
122+
<Image id={iconId || bank.iconId} size='1.5em' />
115123
),
116124
},
117125
{
118126
dataIndex: 'name',
119127
title: intl.formatMessage(messages.colName),
120-
render: (text: string, account: Row) => (
128+
render: (name: string, account: Row) => (
121129
<Link onClick={() => navAccount({ accountId: account.id })}>
122-
<Text>{account.name}</Text>
130+
<Text>{name}</Text>
123131
</Link>
124132
),
125133
},
126134
{
127-
dataIndex: 'sortOrder', // TODO
135+
dataIndex: 'balance',
128136
width: 100,
129-
title: 'amount',
137+
title: 'balance',
130138
align: 'right',
131-
render: (text: string, account: Row) => (
132-
<Text>{formatCurrency(intl, 123.45, account.currencyCode)}</Text>
139+
render: (balance: number, account: Row) => (
140+
<Text>{formatCurrency(intl, balance, account.currencyCode)}</Text>
133141
),
134142
},
135143
// {

packages/core/reducers/entsReducer.ts

+7-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Account, Bank, Bill, Budget, Category, DbEntity, Image, Transaction } from '@ag/db';
2-
import { stringComparer } from '@ag/util';
3-
import debug from 'debug';
4-
import { getType } from 'typesafe-actions';
5-
import { actions, CoreAction } from '../actions';
6-
import { ImageSrc } from '../context';
1+
import { Account, Bank, Bill, Budget, Category, DbEntity, Image, Transaction } from '@ag/db'
2+
import { stringComparer } from '@ag/util'
3+
import debug from 'debug'
4+
import { getType } from 'typesafe-actions'
5+
import { actions, CoreAction } from '../actions'
6+
import { ImageSrc } from '../context'
77

88
const log = debug('core:recordsReducer')
99

@@ -55,9 +55,7 @@ export const entsSelectors = {
5555
return accounts
5656
},
5757
getTransactions: (state: EntsState) => (accountId: string): Transaction[] => {
58-
const transactions = Object.values(state.transactions).filter(t => t.accountId === accountId)
59-
transactions.sort((a, b) => a.time.getTime() - b.time.getTime())
60-
return transactions
58+
return Object.values(state.transactions).filter(t => t.accountId === accountId)
6159
},
6260
getTransaction: (state: EntsState) => (transactionId?: string): Transaction | undefined => {
6361
return transactionId ? state.transactions[transactionId] : undefined

packages/core/thunks/dbThunks.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ const dbLoadTransactions = ({ accountId }: { accountId: string }): CoreThunk =>
175175
const state = getState()
176176
const { transactionRepository } = selectors.appDb(state)
177177
const transactions = await transactionRepository.getForAccount(accountId)
178-
log('dbLoadTransactions %o', transactions)
178+
// log('dbLoadTransactions %o', transactions)
179179
dispatch(actions.dbEntities([{ table: Transaction, entities: transactions, deletes: [] }]))
180180
}
181181

packages/core/thunks/transactionThunks.ts

+20-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DbChange, Transaction, TransactionInput } from '@ag/db/entities'
1+
import { Account, DbChange, Transaction, TransactionInput } from '@ag/db/entities'
22
import { diff, uniqueId } from '@ag/util'
33
import assert from 'assert'
44
import { defineMessages } from 'react-intl'
@@ -10,7 +10,7 @@ import { dbWrite } from './dbWrite'
1010
interface SaveTransactionParams {
1111
input: TransactionInput
1212
transactionId?: string
13-
accountId?: string
13+
accountId: string
1414
}
1515

1616
const saveTransaction = ({ input, transactionId, accountId }: SaveTransactionParams): CoreThunk =>
@@ -24,18 +24,24 @@ const saveTransaction = ({ input, transactionId, accountId }: SaveTransactionPar
2424
const table = Transaction
2525
let transaction: Transaction
2626
let changes: DbChange[]
27+
const amount = input.amount || 0
28+
2729
if (transactionId) {
2830
transaction = await transactionRepository.getById(transactionId)
2931
const q = diff<Transaction.Props>(transaction, input)
30-
changes = [{ table, t, edits: [{ id: transactionId, q }] }]
32+
const delta = amount - transaction.amount
33+
changes = [
34+
{ table, t, edits: [{ id: transactionId, q }] },
35+
...Account.change.addTx(t, accountId, delta),
36+
]
3137
transaction.update(t, q)
3238
} else {
33-
if (!accountId) {
34-
throw new Error('when creating an transaction, accountId must be specified')
35-
}
3639
transaction = new Transaction(uniqueId(), accountId, input)
3740
transactionId = transaction.id
38-
changes = [{ table, t, adds: [transaction] }]
41+
changes = [
42+
{ table, t, adds: [transaction] },
43+
...Account.change.addTx(t, accountId, input.amount),
44+
]
3945
}
4046
assert(transaction.accountId === accountId)
4147
await dispatch(dbWrite(changes))
@@ -49,8 +55,11 @@ const deleteTransaction = (transactionId: string): CoreThunk =>
4955
async function _deleteTransaction(dispatch, getState, { ui: { alert, showToast } }) {
5056
const state = getState()
5157
const intl = selectors.intl(state)
58+
const { transactionRepository } = selectors.appDb(state)
5259

5360
try {
61+
const transaction = await transactionRepository.getById(transactionId)
62+
5463
const confirmed = await alert({
5564
title: intl.formatMessage(messages.title),
5665
body: intl.formatMessage(messages.deleteTransactionBody),
@@ -63,7 +72,10 @@ const deleteTransaction = (transactionId: string): CoreThunk =>
6372
if (confirmed) {
6473
const t = Date.now()
6574
const table = Transaction
66-
const changes: DbChange[] = [{ table, t, deletes: [transactionId] }]
75+
const changes: DbChange[] = [
76+
{ table, t, deletes: [transactionId] },
77+
...Account.change.addTx(t, transaction.accountId, -transaction.amount),
78+
]
6779
await dispatch(dbWrite(changes))
6880
showToast(intl.formatMessage(messages.transactionDeleted), true)
6981
}

packages/db/entities/Account.ts

+17
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class AccountInput {
1919
iconId?: string
2020
sortOrder?: number
2121
currencyCode?: string
22+
balance?: number
2223
}
2324

2425
@Entity({ name: 'accounts' })
@@ -36,6 +37,7 @@ export class Account extends DbEntity<Account.Props> {
3637
@Column('text') iconId!: ImageId
3738
@Column() sortOrder!: number
3839
@Column('text') currencyCode!: CurrencyCode
40+
@Column({ default: 0 }) balance!: number
3941

4042
constructor(bankId?: string, id?: string, props?: AccountInput) {
4143
super(id, { ...Account.defaultValues(), ...props })
@@ -113,6 +115,20 @@ export namespace Account {
113115
t,
114116
deletes: [id],
115117
})
118+
119+
export const addTx = (t: number, id: string, amount: number | undefined): DbChange[] => {
120+
if (!amount) {
121+
return []
122+
}
123+
const q: Spec = { balance: { $plus: amount } }
124+
return [
125+
{
126+
table: Account,
127+
t,
128+
edits: [{ id, q }],
129+
},
130+
]
131+
}
116132
}
117133

118134
export const defaultValues = (): Props => ({
@@ -126,5 +142,6 @@ export namespace Account {
126142
sortOrder: -1,
127143
iconId: '',
128144
currencyCode: defaultCurrencyCode,
145+
balance: 0,
129146
})
130147
}
881 Bytes
Binary file not shown.

packages/storybook/stories/helpers.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const data = {
3737
bankId: 'a66d5887b696ceeb594d493f509ef1e25',
3838
accountId: 'ac847f98eacb81553facf199013ce4446',
3939
billId: 'a020dd8555b14acc1c1101b2f7d2b8c62',
40-
transactionId: 'aa87f138575536a4590c637f0b819edfe',
40+
transactionId: 'a4737047481961dad74575ee85b745f7d',
4141
},
4242
full: {
4343
bankId: 'a74780f9d696f3646f3177b83093c0667',

packages/target-electron/src/app/openDb.electron.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const openDb = async (
2525
extra: {
2626
key,
2727
},
28-
logging: true,
28+
// logging: true,
2929
})
3030
log('opened')
3131
return db

packages/ui-antd/Image.antd.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ export const Image = Object.assign(
2020
)
2121
}),
2222
{
23-
displayName: 'Form',
23+
displayName: 'Image',
2424
}
2525
)

packages/ui-antd/Page.antd.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { MemoryHistory } from 'history'
66
import 'nprogress/nprogress.css'
77
import React from 'react'
88
import useReactRouter from 'use-react-router'
9-
import { ImageSourceIcon } from './ImageSourceIcon'
9+
import { Image } from './Image.antd'
1010
import { mapIconName } from './mapIconName.antd'
1111

1212
const { Title } = Typography
@@ -32,7 +32,7 @@ export const Page = Object.assign(
3232
onBack={onBack}
3333
title={
3434
<Title level={2}>
35-
{image && <ImageSourceIcon style={{ margin: 5 }} id={image} />}
35+
{image && <Image id={image} size='1.5em' margin={5} />}
3636
{title}
3737
</Title>
3838
}

packages/ui-antd/SelectField.antd.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,6 @@ export const SelectField = Object.assign(
6363
}
6464
}),
6565
{
66-
displayName: 'Form',
66+
displayName: 'SelectField',
6767
}
6868
)

packages/ui-antd/ui.antd.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ message.config({
4343

4444
export const ui: UiContext = {
4545
// special ui
46-
showToast: (text, danger) => (danger ? message.error(text) : message.success(text)),
46+
showToast: (text, danger) => message.success(text),
4747
alert: ({ title, body, danger, error, confirmText, cancelText }) => {
4848
return new Promise(resolve => {
4949
const modal = cancelText ? Modal.confirm : error ? Modal.error : Modal.info

0 commit comments

Comments
 (0)