Skip to content

Add create account import #123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/components/form/Autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Resolver } from "../../core";
import { InputGroup, InputValidationErrors, useInputField } from "./input/InputGroup";
import React, { ChangeEventHandler, KeyboardEventHandler, ReactNode, useRef, useState } from "react";
import { Identifiable } from "../../types/types";
Expand Down Expand Up @@ -95,6 +96,9 @@ export const useAutocomplete = function <T extends Identifiable>({ autoCompleteC
onKeyDown={ onKeyDown }
onKeyUp={ onKeyUp }
onChange={ onAutocomplete }
autoComplete={ Resolver.uuid() }
autoCapitalize='off'
autoCorrect='off'
defaultValue={ entityLabel(field.value) }/>

{ hasAutocomplete &&
Expand Down
2 changes: 2 additions & 0 deletions src/components/form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export const Form: FC<FormProps> = ({ entity, onSubmit, style = 'group', childre
className={`Form ${style}`}
noValidate={true}
autoComplete='off'
autoCorrect="off"
spellCheck="false"
action="#">
<FormContext.Provider value={formContext}>
{children}
Expand Down
8 changes: 6 additions & 2 deletions src/components/localization/translation.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@ import LocalizationService from "../../service/localization.service";

type TranslationProps = {
label: string
className?: string
className?: string,
noHtml?: boolean
}

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

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

if (noHtml)
return localized

return <span className={ `Translation ${className}` } dangerouslySetInnerHTML={ { __html: localized } } />
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/lookup-name.util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async function lookup_entity<T>(type: RuleField, id: Identifier) : Promise<T> {
.then((c: Category) => ({ ...c, name: c.label }))
case 'BUDGET':
return (await BudgetRepository.budgetMonth(new Date().getFullYear(), new Date().getMonth() + 1))
.expenses.filter((e : BudgetExpense) => e.id === id)[0] as T
.expenses.filter((e : BudgetExpense) => e.id == id)[0] as T
case 'CONTRACT':
return await ContractRepository.get(id) as T
case 'TAGS': return (id as string).split(',') as T
Expand Down
41 changes: 21 additions & 20 deletions src/components/transaction/rule/change-field.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,28 @@ const ChangeFieldComponent = (props: {

useEffect(() => {
if (change.change) {
lookup_entity(change.field, change.change)
.then(setEntity)
.catch(() => setEntity(undefined))
if (change.change !== entity?.id) {
lookup_entity(change.field, change.change)
.then(setEntity)
.catch(e => setEntity(null))
}
}
}, [change])
}, [change.change, change.field])

if (change.change && !entity) return <Loading />
return <>
<div className='flex gap-1 mb-2 items-start'>
<select id={ `chang_${ change.uuid }_field` }
onChange={ (event) => onValueChange(change.uuid, 'field', event.currentTarget.value) }
defaultValue={ change.field }>
<option value="SOURCE_ACCOUNT"><Translation label='TransactionRule.Column.SOURCE_ACCOUNT'/></option>
<option value="TO_ACCOUNT"><Translation label='TransactionRule.Column.TO_ACCOUNT'/></option>
<option value="CATEGORY"><Translation label='TransactionRule.Column.CATEGORY'/></option>
<option value="CHANGE_TRANSFER_TO"><Translation label='TransactionRule.Column.CHANGE_TRANSFER_TO'/>
</option>
<option value="CHANGE_TRANSFER_FROM"><Translation label='TransactionRule.Column.CHANGE_TRANSFER_FROM'/>
</option>
<option value="BUDGET"><Translation label='TransactionRule.Column.BUDGET'/></option>
<option value="CONTRACT"><Translation label='TransactionRule.Column.CONTRACT'/></option>
<option value="TAGS"><Translation label='TransactionRule.Column.TAGS'/></option>
<option value="SOURCE_ACCOUNT"><Translation label='TransactionRule.Column.SOURCE_ACCOUNT' noHtml={ true }/></option>
<option value="TO_ACCOUNT"><Translation label='TransactionRule.Column.TO_ACCOUNT' noHtml={ true }/></option>
<option value="CATEGORY"><Translation label='TransactionRule.Column.CATEGORY' noHtml={ true }/></option>
<option value="CHANGE_TRANSFER_TO"><Translation label='TransactionRule.Column.CHANGE_TRANSFER_TO' noHtml={ true }/></option>
<option value="CHANGE_TRANSFER_FROM"><Translation label='TransactionRule.Column.CHANGE_TRANSFER_FROM' noHtml={ true }/></option>
<option value="BUDGET"><Translation label='TransactionRule.Column.BUDGET' noHtml={ true }/></option>
<option value="CONTRACT"><Translation label='TransactionRule.Column.CONTRACT' noHtml={ true }/></option>
<option value="TAGS"><Translation label='TransactionRule.Column.TAGS' noHtml={ true }/></option>
</select>

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


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

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

{ change.field === 'CONTRACT'
&& <Entity.Contract value={ entity }
Expand Down
2 changes: 1 addition & 1 deletion src/components/transaction/rule/conditions.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const ConditionFieldComponent = (props: any) => {
id={ `cond[${ condition.uuid }].field` }>
{ PossibleConditions.map(possible =>
<option value={ possible.value } key={ possible.value }>
<Translation label={ `TransactionRule.Condition.${ possible.value }` }/>
<Translation noHtml={ true } label={ `TransactionRule.Condition.${ possible.value }` }/>
</option>
) }
</select>
Expand Down
2 changes: 1 addition & 1 deletion src/components/upload/account-mapping.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const AccountMappingRowComponent = ({ mapping }: { mapping: AccountMapping }) =>
return <>
<div className='w-[25em] font-bold'>{ mapping.name }</div>
<div className='flex-1'>
<Entity.Account id={ mapping.name } value={ account }/>
<Entity.Account id={ mapping.name } value={ account } inputOnly={ true }/>
</div>
</>
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/upload/analyze-transactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Translation from "../localization/translation.component";
import AccountMappingComponent from "./account-mapping.component";

import ConfigureSettingsComponent from "./configure-settings.component";
import CreateMissingAccount from "./create-missing-account";

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

{ tasks?.definition === 'task_configure' && <ConfigureSettingsComponent task={ tasks }/> }
{ tasks?.definition === 'confirm_mappings' && <AccountMappingComponent task={ tasks }/> }
{ tasks?.definition === 'user_create_account' && <CreateMissingAccount task={ tasks } /> }
</>
}

Expand Down
158 changes: 158 additions & 0 deletions src/components/upload/create-missing-account.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { mdiSkipNext } from "@mdi/js";
import React, { useEffect, useState } from "react";
import AccountRepository from "../../core/repositories/account-repository";
import ProcessRepository, {
ProcessTask,
TaskVariable,
TaskVariables
} from "../../core/repositories/process.repository";
import NotificationService from "../../service/notification.service";
import { AccountRef, Identifier } from "../../types/types";
import { Entity, Form, Input, SubmitButton } from "../form";
import MoneyComponent from "../format/money.component";
import Loading from "../layout/loading.component";
import Message from "../layout/message.component";
import Translation from "../localization/translation.component";

type AccountCreated = TaskVariable & {
value: Identifier
}

type TransactionDetails = {
amount: number,
type: string,
description: string,
transactionDate: string,
opposingName: string
}

const _ = ({ task }: { task: ProcessTask }) => {
const [transaction, setTransaction] = useState<TransactionDetails>()
const [assetAccount, setAssetAccount] = useState<boolean>(true)

useEffect(() => {
ProcessRepository.taskVariables('import_job', task.id, 'transaction')
.then(({variables}) => {
const transaction: TransactionDetails = variables.transaction.value
setTransaction(transaction)
})
}, [task]);

const onSubmit = (data: any) => {
AccountRepository.create({name: data.name, currency: data.currency, type: data.type})
.then(account => {
const accountCreated: TaskVariables = {
variables: {
accountId: {
'_type': 'com.jongsoft.finance.rest.process.VariableMap$WrappedVariable',
value: account.id
} as AccountCreated
}
}
ProcessRepository.completeTasksVariables('import_job', task.id, accountCreated)
.then(() => document.location.reload())
.catch(() => NotificationService.warning('page.user.profile.import.error'))
})
.catch(() => NotificationService.warning('page.user.profile.import.error'))
}

const continueWithAccount = ({account} : {account: AccountRef}) => {
const accountCreated: TaskVariables = {
variables: {
accountId: {
'_type': 'com.jongsoft.finance.rest.process.VariableMap$WrappedVariable',
value: account.id
} as AccountCreated
}
}

ProcessRepository.completeTasksVariables('import_job', task.id, accountCreated)
.then(() => document.location.reload())
.catch(() => NotificationService.warning('page.user.profile.import.error'))
}

if (!transaction) return <Loading />
return <>
<Message variant='info' label='page.user.profile.import.account.lookup.info'/>

<div className='max-w-[40em] mx-auto grid grid-cols-4 border-[1px] p-2 mb-4'>
<div className='col-span-4 text-center font-extrabold'><Translation label='page.transaction.add.details'/></div>
<div className='font-bold'><Translation label='page.account.accounts.accountdetails'/>:</div>
<div className='col-span-3'>{ transaction.opposingName }</div>
<div className='font-bold'><Translation label='Transaction.description'/>:</div>
<div className='col-span-3'>{ transaction.description }</div>
<div className='font-bold'><Translation label='Transaction.amount'/>:</div>
<div className='col-span-3'><MoneyComponent money={ transaction.amount }/></div>
</div>

<Form entity='' onSubmit={ continueWithAccount }>
<fieldset className='max-w-[40em] mx-auto'>
<legend><Translation label='page.user.profile.import.account.lookup' /></legend>
<Entity.Account id='account'
title='Account.name'
inputOnly={ true }
required={ true }/>

<div className='flex justify-end'>
<SubmitButton label='common.action.next' icon={ mdiSkipNext } iconPos='after'/>
</div>
</fieldset>
</Form>
<hr className='max-w-[45em] mx-auto my-2'/>
<Form entity='' onSubmit={ onSubmit }>
<fieldset className='max-w-[40em] mx-auto'>
<legend><Translation label='page.title.accounts.add'/></legend>
<Input.Text id='name'
value={ transaction.opposingName }
title='Account.name'
help='Account.name.help'
type='text'
required={ true }/>

<Entity.Currency id='currency'
title='Account.currency'
required/>

<div className='flex mb-2'>
<span className='flex-auto max-w-full md-max-w-[15vw]'/>
<span className='flex-[3] flex gap-3'>
<Input.Toggle id='ownAccount'
onChange={ () => setAssetAccount(!assetAccount) }
value={ true }/>
<Translation label='page.nav.accounts.accounts'/>
</span>
</div>

{ assetAccount && <Entity.AccountType id='type'
title='Account.type'
required/> }

{ !assetAccount && <span className='flex mb-2'>
<span className='flex-auto max-w-full md-max-w-[15vw]'/>

<span className='flex-[3]'>
<Input.RadioButtons id='type'
value='creditor'
options={ [
{
label: 'common.credit',
value: 'creditor',
variant: 'warning'
},
{
label: 'common.debit',
value: 'debtor',
variant: 'success'
}] }/>
</span>
</span> }

<div className='flex justify-end'>
<SubmitButton label='common.action.next' icon={ mdiSkipNext } iconPos='after'/>
</div>
</fieldset>
</Form>
</>
}

export default _;
10 changes: 10 additions & 0 deletions src/components/upload/import-job-transaction.component.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { mdiRadar } from "@mdi/js";
import React, { useEffect, useState } from "react";
import { Resolver } from "../../core";
import { groupTransactionByYear, YearlyTransactions } from "../../reducers";
import ImportJobRepository from "../../core/repositories/import-job.repository";
import { Pagination } from "../../types/types";
import useQueryParam from "../../hooks/query-param.hook";
import MoneyComponent from "../format/money.component";
import { Button } from "../layout/button";

import Loading from "../layout/loading.component";
import { Paginator } from "../layout/paginator.component";
Expand Down Expand Up @@ -36,6 +38,14 @@ const ImportJobTransactionComponent = ({ slug }: { slug: string }) => {
<h1 className='mt-5 mb-2 text-lg font-bold'>
<Translation label='page.title.transactions.overview'/>
</h1>

<div className='flex justify-end'>
<Button onClick={ () => ImportJobRepository.runTransactionRules(slug)}
className='mb-2'
icon={ mdiRadar}
label='page.settings.import.details.transactions.rules.run' />
</div>

{ !isLoaded && <Loading/> }

{ isLoaded && !hasTransactions && <div className='text-center text-gray-500'>
Expand Down
2 changes: 2 additions & 0 deletions src/core/repositories/import-job.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const ImportJobRepository = (api => {
delete: (slug: string) => api.delete(`import/${slug}`),
transactions: (slug: string, page: number) => api.post<PageRequest,TransactionPage>(`import/${slug}/transactions`, { page }),

runTransactionRules: (slug: string) => api.post(`import/${slug}/transactions/run-rule-automation`, {}),

getImportConfigs: (): Promise<BatchConfig[]> => api.get('import/config'),
createImportConfig: (config: any) => api.put<any, any>('import/config', config),
}
Expand Down
Loading
Loading