Skip to content

Commit 50a1e6a

Browse files
authored
Add create account import (#123)
* Add the screens needed for manually creating or assigning accounts during import. * Add detail information on the transaction to the create account step.
1 parent 684ad26 commit 50a1e6a

12 files changed

+215
-29
lines changed

src/components/form/Autocomplete.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Resolver } from "../../core";
12
import { InputGroup, InputValidationErrors, useInputField } from "./input/InputGroup";
23
import React, { ChangeEventHandler, KeyboardEventHandler, ReactNode, useRef, useState } from "react";
34
import { Identifiable } from "../../types/types";
@@ -95,6 +96,9 @@ export const useAutocomplete = function <T extends Identifiable>({ autoCompleteC
9596
onKeyDown={ onKeyDown }
9697
onKeyUp={ onKeyUp }
9798
onChange={ onAutocomplete }
99+
autoComplete={ Resolver.uuid() }
100+
autoCapitalize='off'
101+
autoCorrect='off'
98102
defaultValue={ entityLabel(field.value) }/>
99103

100104
{ hasAutocomplete &&

src/components/form/Form.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ export const Form: FC<FormProps> = ({ entity, onSubmit, style = 'group', childre
101101
className={`Form ${style}`}
102102
noValidate={true}
103103
autoComplete='off'
104+
autoCorrect="off"
105+
spellCheck="false"
104106
action="#">
105107
<FormContext.Provider value={formContext}>
106108
{children}

src/components/localization/translation.component.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,24 @@ import LocalizationService from "../../service/localization.service";
33

44
type TranslationProps = {
55
label: string
6-
className?: string
6+
className?: string,
7+
noHtml?: boolean
78
}
89

910
/**
1011
* The translation component is able to display a localized message on the screen using a given translation
1112
* key.
1213
*/
13-
const Translation: FC<TranslationProps> = ({ label, className = '' }) => {
14+
const Translation: FC<TranslationProps> = ({ label, className = '', noHtml = false }) => {
1415
const [localized, setLocalized] = useState(`!Not translated! [${label}]`)
1516

1617
useEffect(() => {
1718
LocalizationService.get(label).then(setLocalized)
1819
}, [label]);
1920

21+
if (noHtml)
22+
return localized
23+
2024
return <span className={ `Translation ${className}` } dangerouslySetInnerHTML={ { __html: localized } } />
2125
}
2226

src/components/lookup-name.util.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ async function lookup_entity<T>(type: RuleField, id: Identifier) : Promise<T> {
1818
.then((c: Category) => ({ ...c, name: c.label }))
1919
case 'BUDGET':
2020
return (await BudgetRepository.budgetMonth(new Date().getFullYear(), new Date().getMonth() + 1))
21-
.expenses.filter((e : BudgetExpense) => e.id === id)[0] as T
21+
.expenses.filter((e : BudgetExpense) => e.id == id)[0] as T
2222
case 'CONTRACT':
2323
return await ContractRepository.get(id) as T
2424
case 'TAGS': return (id as string).split(',') as T

src/components/transaction/rule/change-field.component.tsx

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,28 @@ const ChangeFieldComponent = (props: {
2020

2121
useEffect(() => {
2222
if (change.change) {
23-
lookup_entity(change.field, change.change)
24-
.then(setEntity)
25-
.catch(() => setEntity(undefined))
23+
if (change.change !== entity?.id) {
24+
lookup_entity(change.field, change.change)
25+
.then(setEntity)
26+
.catch(e => setEntity(null))
27+
}
2628
}
27-
}, [change])
29+
}, [change.change, change.field])
2830

2931
if (change.change && !entity) return <Loading />
3032
return <>
3133
<div className='flex gap-1 mb-2 items-start'>
3234
<select id={ `chang_${ change.uuid }_field` }
3335
onChange={ (event) => onValueChange(change.uuid, 'field', event.currentTarget.value) }
3436
defaultValue={ change.field }>
35-
<option value="SOURCE_ACCOUNT"><Translation label='TransactionRule.Column.SOURCE_ACCOUNT'/></option>
36-
<option value="TO_ACCOUNT"><Translation label='TransactionRule.Column.TO_ACCOUNT'/></option>
37-
<option value="CATEGORY"><Translation label='TransactionRule.Column.CATEGORY'/></option>
38-
<option value="CHANGE_TRANSFER_TO"><Translation label='TransactionRule.Column.CHANGE_TRANSFER_TO'/>
39-
</option>
40-
<option value="CHANGE_TRANSFER_FROM"><Translation label='TransactionRule.Column.CHANGE_TRANSFER_FROM'/>
41-
</option>
42-
<option value="BUDGET"><Translation label='TransactionRule.Column.BUDGET'/></option>
43-
<option value="CONTRACT"><Translation label='TransactionRule.Column.CONTRACT'/></option>
44-
<option value="TAGS"><Translation label='TransactionRule.Column.TAGS'/></option>
37+
<option value="SOURCE_ACCOUNT"><Translation label='TransactionRule.Column.SOURCE_ACCOUNT' noHtml={ true }/></option>
38+
<option value="TO_ACCOUNT"><Translation label='TransactionRule.Column.TO_ACCOUNT' noHtml={ true }/></option>
39+
<option value="CATEGORY"><Translation label='TransactionRule.Column.CATEGORY' noHtml={ true }/></option>
40+
<option value="CHANGE_TRANSFER_TO"><Translation label='TransactionRule.Column.CHANGE_TRANSFER_TO' noHtml={ true }/></option>
41+
<option value="CHANGE_TRANSFER_FROM"><Translation label='TransactionRule.Column.CHANGE_TRANSFER_FROM' noHtml={ true }/></option>
42+
<option value="BUDGET"><Translation label='TransactionRule.Column.BUDGET' noHtml={ true }/></option>
43+
<option value="CONTRACT"><Translation label='TransactionRule.Column.CONTRACT' noHtml={ true }/></option>
44+
<option value="TAGS"><Translation label='TransactionRule.Column.TAGS' noHtml={ true }/></option>
4545
</select>
4646

4747
{ (change.field === 'CHANGE_TRANSFER_TO' || change.field === 'CHANGE_TRANSFER_FROM')
@@ -71,18 +71,19 @@ const ChangeFieldComponent = (props: {
7171

7272

7373
{ change.field === 'CATEGORY'
74-
&& <Entity.Category value={ { id: -1, name: entity.label } }
74+
&& <Entity.Category value={ { id: -1, name: entity?.label } }
7575
onChange={ (value: Category) => onValueChange(change.uuid, 'change', value.id as string) }
7676
id={ `chang_${ change.uuid }_change` }
7777
className='!m-0 flex-1 [&>label]:!hidden'
7878
inputOnly={ true }
7979
title='dd'/> }
8080

81-
{ change.field === 'BUDGET' && <Entity.Budget value={ entity }
82-
onChange={ (value: BudgetExpense) => onValueChange(change.uuid, 'change', value.id as string) }
83-
id={ `chang_${ change.uuid }_change` }
84-
className='!m-0 flex-1 [&>label]:!hidden'
85-
title='dd'/> }
81+
{ change.field === 'BUDGET'
82+
&& <Entity.Budget value={ entity }
83+
onChange={ (value: BudgetExpense) => onValueChange(change.uuid, 'change', value.id as string) }
84+
id={ `chang_${ change.uuid }_change` }
85+
className='!m-0 flex-1 [&>label]:!hidden'
86+
title='dd'/> }
8687

8788
{ change.field === 'CONTRACT'
8889
&& <Entity.Contract value={ entity }

src/components/transaction/rule/conditions.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const ConditionFieldComponent = (props: any) => {
9292
id={ `cond[${ condition.uuid }].field` }>
9393
{ PossibleConditions.map(possible =>
9494
<option value={ possible.value } key={ possible.value }>
95-
<Translation label={ `TransactionRule.Condition.${ possible.value }` }/>
95+
<Translation noHtml={ true } label={ `TransactionRule.Condition.${ possible.value }` }/>
9696
</option>
9797
) }
9898
</select>

src/components/upload/account-mapping.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const AccountMappingRowComponent = ({ mapping }: { mapping: AccountMapping }) =>
3030
return <>
3131
<div className='w-[25em] font-bold'>{ mapping.name }</div>
3232
<div className='flex-1'>
33-
<Entity.Account id={ mapping.name } value={ account }/>
33+
<Entity.Account id={ mapping.name } value={ account } inputOnly={ true }/>
3434
</div>
3535
</>
3636
}

src/components/upload/analyze-transactions.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Translation from "../localization/translation.component";
1212
import AccountMappingComponent from "./account-mapping.component";
1313

1414
import ConfigureSettingsComponent from "./configure-settings.component";
15+
import CreateMissingAccount from "./create-missing-account";
1516

1617
const AnalyzeTaskComponent = ({ process }: { process: ProcessInstance }) => {
1718
const [tasks, setTasks] = useState<ProcessTask>()
@@ -48,6 +49,7 @@ const AnalyzeTaskComponent = ({ process }: { process: ProcessInstance }) => {
4849

4950
{ tasks?.definition === 'task_configure' && <ConfigureSettingsComponent task={ tasks }/> }
5051
{ tasks?.definition === 'confirm_mappings' && <AccountMappingComponent task={ tasks }/> }
52+
{ tasks?.definition === 'user_create_account' && <CreateMissingAccount task={ tasks } /> }
5153
</>
5254
}
5355

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { mdiSkipNext } from "@mdi/js";
2+
import React, { useEffect, useState } from "react";
3+
import AccountRepository from "../../core/repositories/account-repository";
4+
import ProcessRepository, {
5+
ProcessTask,
6+
TaskVariable,
7+
TaskVariables
8+
} from "../../core/repositories/process.repository";
9+
import NotificationService from "../../service/notification.service";
10+
import { AccountRef, Identifier } from "../../types/types";
11+
import { Entity, Form, Input, SubmitButton } from "../form";
12+
import MoneyComponent from "../format/money.component";
13+
import Loading from "../layout/loading.component";
14+
import Message from "../layout/message.component";
15+
import Translation from "../localization/translation.component";
16+
17+
type AccountCreated = TaskVariable & {
18+
value: Identifier
19+
}
20+
21+
type TransactionDetails = {
22+
amount: number,
23+
type: string,
24+
description: string,
25+
transactionDate: string,
26+
opposingName: string
27+
}
28+
29+
const _ = ({ task }: { task: ProcessTask }) => {
30+
const [transaction, setTransaction] = useState<TransactionDetails>()
31+
const [assetAccount, setAssetAccount] = useState<boolean>(true)
32+
33+
useEffect(() => {
34+
ProcessRepository.taskVariables('import_job', task.id, 'transaction')
35+
.then(({variables}) => {
36+
const transaction: TransactionDetails = variables.transaction.value
37+
setTransaction(transaction)
38+
})
39+
}, [task]);
40+
41+
const onSubmit = (data: any) => {
42+
AccountRepository.create({name: data.name, currency: data.currency, type: data.type})
43+
.then(account => {
44+
const accountCreated: TaskVariables = {
45+
variables: {
46+
accountId: {
47+
'_type': 'com.jongsoft.finance.rest.process.VariableMap$WrappedVariable',
48+
value: account.id
49+
} as AccountCreated
50+
}
51+
}
52+
ProcessRepository.completeTasksVariables('import_job', task.id, accountCreated)
53+
.then(() => document.location.reload())
54+
.catch(() => NotificationService.warning('page.user.profile.import.error'))
55+
})
56+
.catch(() => NotificationService.warning('page.user.profile.import.error'))
57+
}
58+
59+
const continueWithAccount = ({account} : {account: AccountRef}) => {
60+
const accountCreated: TaskVariables = {
61+
variables: {
62+
accountId: {
63+
'_type': 'com.jongsoft.finance.rest.process.VariableMap$WrappedVariable',
64+
value: account.id
65+
} as AccountCreated
66+
}
67+
}
68+
69+
ProcessRepository.completeTasksVariables('import_job', task.id, accountCreated)
70+
.then(() => document.location.reload())
71+
.catch(() => NotificationService.warning('page.user.profile.import.error'))
72+
}
73+
74+
if (!transaction) return <Loading />
75+
return <>
76+
<Message variant='info' label='page.user.profile.import.account.lookup.info'/>
77+
78+
<div className='max-w-[40em] mx-auto grid grid-cols-4 border-[1px] p-2 mb-4'>
79+
<div className='col-span-4 text-center font-extrabold'><Translation label='page.transaction.add.details'/></div>
80+
<div className='font-bold'><Translation label='page.account.accounts.accountdetails'/>:</div>
81+
<div className='col-span-3'>{ transaction.opposingName }</div>
82+
<div className='font-bold'><Translation label='Transaction.description'/>:</div>
83+
<div className='col-span-3'>{ transaction.description }</div>
84+
<div className='font-bold'><Translation label='Transaction.amount'/>:</div>
85+
<div className='col-span-3'><MoneyComponent money={ transaction.amount }/></div>
86+
</div>
87+
88+
<Form entity='' onSubmit={ continueWithAccount }>
89+
<fieldset className='max-w-[40em] mx-auto'>
90+
<legend><Translation label='page.user.profile.import.account.lookup' /></legend>
91+
<Entity.Account id='account'
92+
title='Account.name'
93+
inputOnly={ true }
94+
required={ true }/>
95+
96+
<div className='flex justify-end'>
97+
<SubmitButton label='common.action.next' icon={ mdiSkipNext } iconPos='after'/>
98+
</div>
99+
</fieldset>
100+
</Form>
101+
<hr className='max-w-[45em] mx-auto my-2'/>
102+
<Form entity='' onSubmit={ onSubmit }>
103+
<fieldset className='max-w-[40em] mx-auto'>
104+
<legend><Translation label='page.title.accounts.add'/></legend>
105+
<Input.Text id='name'
106+
value={ transaction.opposingName }
107+
title='Account.name'
108+
help='Account.name.help'
109+
type='text'
110+
required={ true }/>
111+
112+
<Entity.Currency id='currency'
113+
title='Account.currency'
114+
required/>
115+
116+
<div className='flex mb-2'>
117+
<span className='flex-auto max-w-full md-max-w-[15vw]'/>
118+
<span className='flex-[3] flex gap-3'>
119+
<Input.Toggle id='ownAccount'
120+
onChange={ () => setAssetAccount(!assetAccount) }
121+
value={ true }/>
122+
<Translation label='page.nav.accounts.accounts'/>
123+
</span>
124+
</div>
125+
126+
{ assetAccount && <Entity.AccountType id='type'
127+
title='Account.type'
128+
required/> }
129+
130+
{ !assetAccount && <span className='flex mb-2'>
131+
<span className='flex-auto max-w-full md-max-w-[15vw]'/>
132+
133+
<span className='flex-[3]'>
134+
<Input.RadioButtons id='type'
135+
value='creditor'
136+
options={ [
137+
{
138+
label: 'common.credit',
139+
value: 'creditor',
140+
variant: 'warning'
141+
},
142+
{
143+
label: 'common.debit',
144+
value: 'debtor',
145+
variant: 'success'
146+
}] }/>
147+
</span>
148+
</span> }
149+
150+
<div className='flex justify-end'>
151+
<SubmitButton label='common.action.next' icon={ mdiSkipNext } iconPos='after'/>
152+
</div>
153+
</fieldset>
154+
</Form>
155+
</>
156+
}
157+
158+
export default _;

src/components/upload/import-job-transaction.component.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { mdiRadar } from "@mdi/js";
12
import React, { useEffect, useState } from "react";
23
import { Resolver } from "../../core";
34
import { groupTransactionByYear, YearlyTransactions } from "../../reducers";
45
import ImportJobRepository from "../../core/repositories/import-job.repository";
56
import { Pagination } from "../../types/types";
67
import useQueryParam from "../../hooks/query-param.hook";
78
import MoneyComponent from "../format/money.component";
9+
import { Button } from "../layout/button";
810

911
import Loading from "../layout/loading.component";
1012
import { Paginator } from "../layout/paginator.component";
@@ -36,6 +38,14 @@ const ImportJobTransactionComponent = ({ slug }: { slug: string }) => {
3638
<h1 className='mt-5 mb-2 text-lg font-bold'>
3739
<Translation label='page.title.transactions.overview'/>
3840
</h1>
41+
42+
<div className='flex justify-end'>
43+
<Button onClick={ () => ImportJobRepository.runTransactionRules(slug)}
44+
className='mb-2'
45+
icon={ mdiRadar}
46+
label='page.settings.import.details.transactions.rules.run' />
47+
</div>
48+
3949
{ !isLoaded && <Loading/> }
4050

4151
{ isLoaded && !hasTransactions && <div className='text-center text-gray-500'>

src/core/repositories/import-job.repository.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const ImportJobRepository = (api => {
1616
delete: (slug: string) => api.delete(`import/${slug}`),
1717
transactions: (slug: string, page: number) => api.post<PageRequest,TransactionPage>(`import/${slug}/transactions`, { page }),
1818

19+
runTransactionRules: (slug: string) => api.post(`import/${slug}/transactions/run-rule-automation`, {}),
20+
1921
getImportConfigs: (): Promise<BatchConfig[]> => api.get('import/config'),
2022
createImportConfig: (config: any) => api.put<any, any>('import/config', config),
2123
}

0 commit comments

Comments
 (0)