From c8f564712fc3b2eba63fbf9120ec428e5110dd54 Mon Sep 17 00:00:00 2001 From: Gerben Jongerius Date: Tue, 17 Sep 2024 12:03:37 +0200 Subject: [PATCH 1/2] Add the screens needed for manually creating or assigning accounts during import. --- src/components/form/Autocomplete.tsx | 4 + src/components/form/Form.tsx | 2 + .../localization/translation.component.tsx | 8 +- src/components/lookup-name.util.tsx | 2 +- .../rule/change-field.component.tsx | 41 +++--- .../transaction/rule/conditions.component.tsx | 2 +- .../upload/account-mapping.component.tsx | 2 +- .../upload/analyze-transactions.tsx | 2 + .../upload/create-missing-account.tsx | 137 ++++++++++++++++++ .../import-job-transaction.component.tsx | 10 ++ .../repositories/import-job.repository.ts | 2 + src/pledger-io.tsx | 11 +- 12 files changed, 194 insertions(+), 29 deletions(-) create mode 100644 src/components/upload/create-missing-account.tsx diff --git a/src/components/form/Autocomplete.tsx b/src/components/form/Autocomplete.tsx index 25894f21..7fe85dff 100644 --- a/src/components/form/Autocomplete.tsx +++ b/src/components/form/Autocomplete.tsx @@ -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"; @@ -95,6 +96,9 @@ export const useAutocomplete = function ({ autoCompleteC onKeyDown={ onKeyDown } onKeyUp={ onKeyUp } onChange={ onAutocomplete } + autoComplete={ Resolver.uuid() } + autoCapitalize='off' + autoCorrect='off' defaultValue={ entityLabel(field.value) }/> { hasAutocomplete && diff --git a/src/components/form/Form.tsx b/src/components/form/Form.tsx index 92965962..60ea325a 100644 --- a/src/components/form/Form.tsx +++ b/src/components/form/Form.tsx @@ -101,6 +101,8 @@ export const Form: FC = ({ entity, onSubmit, style = 'group', childre className={`Form ${style}`} noValidate={true} autoComplete='off' + autoCorrect="off" + spellCheck="false" action="#"> {children} diff --git a/src/components/localization/translation.component.tsx b/src/components/localization/translation.component.tsx index 7eed40e6..4712636c 100644 --- a/src/components/localization/translation.component.tsx +++ b/src/components/localization/translation.component.tsx @@ -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 = ({ label, className = '' }) => { +const Translation: FC = ({ label, className = '', noHtml = false }) => { const [localized, setLocalized] = useState(`!Not translated! [${label}]`) useEffect(() => { LocalizationService.get(label).then(setLocalized) }, [label]); + if (noHtml) + return localized + return } diff --git a/src/components/lookup-name.util.tsx b/src/components/lookup-name.util.tsx index 82d51a42..0b209ac5 100644 --- a/src/components/lookup-name.util.tsx +++ b/src/components/lookup-name.util.tsx @@ -18,7 +18,7 @@ async function lookup_entity(type: RuleField, id: Identifier) : Promise { .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 diff --git a/src/components/transaction/rule/change-field.component.tsx b/src/components/transaction/rule/change-field.component.tsx index fcdd40de..ae4304f6 100644 --- a/src/components/transaction/rule/change-field.component.tsx +++ b/src/components/transaction/rule/change-field.component.tsx @@ -20,11 +20,13 @@ 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 return <> @@ -32,16 +34,14 @@ const ChangeFieldComponent = (props: { { (change.field === 'CHANGE_TRANSFER_TO' || change.field === 'CHANGE_TRANSFER_FROM') @@ -71,18 +71,19 @@ const ChangeFieldComponent = (props: { { change.field === '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' && 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' + && 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' && { id={ `cond[${ condition.uuid }].field` }> { PossibleConditions.map(possible => ) } diff --git a/src/components/upload/account-mapping.component.tsx b/src/components/upload/account-mapping.component.tsx index 149412d4..54fc52a8 100644 --- a/src/components/upload/account-mapping.component.tsx +++ b/src/components/upload/account-mapping.component.tsx @@ -30,7 +30,7 @@ const AccountMappingRowComponent = ({ mapping }: { mapping: AccountMapping }) => return <>
{ mapping.name }
- +
} diff --git a/src/components/upload/analyze-transactions.tsx b/src/components/upload/analyze-transactions.tsx index e1af48bd..d13430eb 100644 --- a/src/components/upload/analyze-transactions.tsx +++ b/src/components/upload/analyze-transactions.tsx @@ -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() @@ -48,6 +49,7 @@ const AnalyzeTaskComponent = ({ process }: { process: ProcessInstance }) => { { tasks?.definition === 'task_configure' && } { tasks?.definition === 'confirm_mappings' && } + { tasks?.definition === 'user_create_account' && } } diff --git a/src/components/upload/create-missing-account.tsx b/src/components/upload/create-missing-account.tsx new file mode 100644 index 00000000..5a8a3ab4 --- /dev/null +++ b/src/components/upload/create-missing-account.tsx @@ -0,0 +1,137 @@ +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 Message from "../layout/message.component"; +import Translation from "../localization/translation.component"; + +type AccountCreated = TaskVariable & { + value: Identifier +} + +const _ = ({ task }: { task: ProcessTask }) => { + const [accountName, setAccountName] = useState('') + const [assetAccount, setAssetAccount] = useState(true) + + useEffect(() => { + ProcessRepository.taskVariables('import_job', task.id, 'accountName') + .then(variables => { + const { variables: { accountName: { value } } } = variables + setAccountName(value) + }) + }, [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')) + } + + return <> + + +
+
+ + + +
+ +
+
+
+
+
+
+ + + + + +
+ + + setAssetAccount(!assetAccount) } + value={ true }/> + + +
+ + { assetAccount && } + + { !assetAccount && + + + + + + } + +
+ +
+
+
+ +} + +export default _; \ No newline at end of file diff --git a/src/components/upload/import-job-transaction.component.tsx b/src/components/upload/import-job-transaction.component.tsx index 29a93454..f060cf61 100644 --- a/src/components/upload/import-job-transaction.component.tsx +++ b/src/components/upload/import-job-transaction.component.tsx @@ -1,3 +1,4 @@ +import { mdiRadar } from "@mdi/js"; import React, { useEffect, useState } from "react"; import { Resolver } from "../../core"; import { groupTransactionByYear, YearlyTransactions } from "../../reducers"; @@ -5,6 +6,7 @@ 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"; @@ -36,6 +38,14 @@ const ImportJobTransactionComponent = ({ slug }: { slug: string }) => {

+ +
+
+ { !isLoaded && } { isLoaded && !hasTransactions &&
diff --git a/src/core/repositories/import-job.repository.ts b/src/core/repositories/import-job.repository.ts index 7d83c909..4b71a1a3 100644 --- a/src/core/repositories/import-job.repository.ts +++ b/src/core/repositories/import-job.repository.ts @@ -16,6 +16,8 @@ const ImportJobRepository = (api => { delete: (slug: string) => api.delete(`import/${slug}`), transactions: (slug: string, page: number) => api.post(`import/${slug}/transactions`, { page }), + runTransactionRules: (slug: string) => api.post(`import/${slug}/transactions/run-rule-automation`, {}), + getImportConfigs: (): Promise => api.get('import/config'), createImportConfig: (config: any) => api.put('import/config', config), } diff --git a/src/pledger-io.tsx b/src/pledger-io.tsx index 7567a8f5..78e72e9b 100644 --- a/src/pledger-io.tsx +++ b/src/pledger-io.tsx @@ -19,6 +19,10 @@ import MobileSidebar from "./components/sidebar/mobile-sidebar"; import Loading from "./components/layout/loading.component"; import NotificationCenter from "./components/notification"; +import LoginPage from "./pages/login"; +import RegisterPage from "./pages/register"; +import TwoFactorPage from "./pages/two-factor"; + import account from "./pages/account/routes"; import automation from "./pages/automation/routes"; import budget from "./pages/budget/routes"; @@ -34,19 +38,19 @@ const router = createBrowserRouter([ { id: 'login', path: '/login', - Component: lazy(() => import('./pages/login')), + Component: LoginPage, loader: anonymousLoader }, { id: 'register', path: '/register', - Component: lazy(() => import('./pages/register')), + Component: RegisterPage, loader: anonymousLoader }, { id: 'two-factor', path: '/two-factor', - Component: lazy(() => import('./pages/two-factor')), + Component: TwoFactorPage, }, { id: 'pledger', @@ -89,7 +93,6 @@ function anonymousLoader({ request }: LoaderFunctionArgs) { return redirect(from); } return null; - } async function authenticatedLoader({ request }: LoaderFunctionArgs) { From ae8ea15fffe63746ae4468fe0c830c776b262140 Mon Sep 17 00:00:00 2001 From: Gerben Jongerius Date: Tue, 17 Sep 2024 13:07:35 +0200 Subject: [PATCH 2/2] Add detail information on the transaction to the create account step. --- .../upload/create-missing-account.tsx | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/components/upload/create-missing-account.tsx b/src/components/upload/create-missing-account.tsx index 5a8a3ab4..1cf2f74c 100644 --- a/src/components/upload/create-missing-account.tsx +++ b/src/components/upload/create-missing-account.tsx @@ -9,6 +9,8 @@ import ProcessRepository, { 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"; @@ -16,15 +18,23 @@ type AccountCreated = TaskVariable & { value: Identifier } +type TransactionDetails = { + amount: number, + type: string, + description: string, + transactionDate: string, + opposingName: string +} + const _ = ({ task }: { task: ProcessTask }) => { - const [accountName, setAccountName] = useState('') + const [transaction, setTransaction] = useState() const [assetAccount, setAssetAccount] = useState(true) useEffect(() => { - ProcessRepository.taskVariables('import_job', task.id, 'accountName') - .then(variables => { - const { variables: { accountName: { value } } } = variables - setAccountName(value) + ProcessRepository.taskVariables('import_job', task.id, 'transaction') + .then(({variables}) => { + const transaction: TransactionDetails = variables.transaction.value + setTransaction(transaction) }) }, [task]); @@ -61,9 +71,20 @@ const _ = ({ task }: { task: ProcessTask }) => { .catch(() => NotificationService.warning('page.user.profile.import.error')) } + if (!transaction) return return <> +
+
+
:
+
{ transaction.opposingName }
+
:
+
{ transaction.description }
+
:
+
+
+
@@ -82,7 +103,7 @@ const _ = ({ task }: { task: ProcessTask }) => {