mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-21 15:36:50 -05:00
Add an option to swap payee/memo when importing transaction form file (#7101)
* Add swap payee-memo to ofx import * Add mock files to test payee-memo swap * Add swap payee-memo to qif import * Add swap payee-memo to camt import * Minor code cleanup for swap payee-memo on import * Add release note * lint fixing * Fixe Payee and Memo capitalization * change swapPayeeAndMemo to ofxSwapPayeeAndMemo * correct release note typo * Add getSwapOption base on file type * Support qfx * Add CheckboxToggle to simplify ImportTransactionsModal options * Fix split reac import * Fix react import lint
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { useCallback, useEffect, useEffectEvent, useState } from 'react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import type {
|
||||
ComponentProps,
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
} from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button, ButtonWithLoading } from '@actual-app/components/button';
|
||||
@@ -53,6 +58,28 @@ import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
|
||||
import { useSyncedPrefs } from '@desktop-client/hooks/useSyncedPrefs';
|
||||
import { payeeQueries } from '@desktop-client/payees';
|
||||
|
||||
function CheckboxToggle({
|
||||
id,
|
||||
checked,
|
||||
onChange,
|
||||
children,
|
||||
}: {
|
||||
id: string;
|
||||
checked: boolean;
|
||||
onChange: Dispatch<SetStateAction<boolean>>;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<LabeledCheckbox
|
||||
id={id}
|
||||
checked={checked}
|
||||
onChange={() => onChange(prev => !prev)}
|
||||
>
|
||||
{children}
|
||||
</LabeledCheckbox>
|
||||
);
|
||||
}
|
||||
|
||||
function getFileType(filepath: string): string {
|
||||
const m = filepath.match(/\.([^.]*)$/);
|
||||
if (!m) return 'ofx';
|
||||
@@ -213,6 +240,15 @@ export function ImportTransactionsModal({
|
||||
const [fallbackMissingPayeeToMemo, setFallbackMissingPayeeToMemo] = useState(
|
||||
String(prefs[`ofx-fallback-missing-payee-${accountId}`]) !== 'false',
|
||||
);
|
||||
const [ofxSwapPayeeAndMemo, setOfxSwapPayeeAndMemo] = useState(
|
||||
String(prefs[`ofx-swap-payee-memo-${accountId}`]) === 'true',
|
||||
);
|
||||
const [qifSwapPayeeAndMemo, setQifSwapPayeeAndMemo] = useState(
|
||||
String(prefs[`qif-swap-payee-memo-${accountId}`]) === 'true',
|
||||
);
|
||||
const [camtSwapPayeeAndMemo, setCamtSwapPayeeAndMemo] = useState(
|
||||
String(prefs[`camt-swap-payee-memo-${accountId}`]) === 'true',
|
||||
);
|
||||
|
||||
const [parseDateFormat, setParseDateFormat] = useState<DateFormat | null>(
|
||||
null,
|
||||
@@ -413,6 +449,12 @@ export function ImportTransactionsModal({
|
||||
skipEndLines,
|
||||
fallbackMissingPayeeToMemo,
|
||||
importNotes,
|
||||
swapPayeeAndMemo: getSwapOption(
|
||||
fileType,
|
||||
ofxSwapPayeeAndMemo,
|
||||
qifSwapPayeeAndMemo,
|
||||
camtSwapPayeeAndMemo,
|
||||
),
|
||||
});
|
||||
|
||||
void parse(originalFileName, parseOptions);
|
||||
@@ -424,6 +466,9 @@ export function ImportTransactionsModal({
|
||||
skipEndLines,
|
||||
fallbackMissingPayeeToMemo,
|
||||
importNotes,
|
||||
ofxSwapPayeeAndMemo,
|
||||
qifSwapPayeeAndMemo,
|
||||
camtSwapPayeeAndMemo,
|
||||
parse,
|
||||
]);
|
||||
|
||||
@@ -471,6 +516,12 @@ export function ImportTransactionsModal({
|
||||
skipEndLines,
|
||||
fallbackMissingPayeeToMemo,
|
||||
importNotes,
|
||||
swapPayeeAndMemo: getSwapOption(
|
||||
fileType,
|
||||
ofxSwapPayeeAndMemo,
|
||||
qifSwapPayeeAndMemo,
|
||||
camtSwapPayeeAndMemo,
|
||||
),
|
||||
});
|
||||
|
||||
void parse(res[0], parseOptions);
|
||||
@@ -625,6 +676,7 @@ export function ImportTransactionsModal({
|
||||
[`ofx-fallback-missing-payee-${accountId}`]: String(
|
||||
fallbackMissingPayeeToMemo,
|
||||
),
|
||||
[`ofx-swap-payee-memo-${accountId}`]: String(ofxSwapPayeeAndMemo),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -649,6 +701,18 @@ export function ImportTransactionsModal({
|
||||
});
|
||||
}
|
||||
|
||||
if (filetype === 'qif') {
|
||||
savePrefs({
|
||||
[`qif-swap-payee-memo-${accountId}`]: String(qifSwapPayeeAndMemo),
|
||||
});
|
||||
}
|
||||
|
||||
if (isCamtFile(filetype)) {
|
||||
savePrefs({
|
||||
[`camt-swap-payee-memo-${accountId}`]: String(camtSwapPayeeAndMemo),
|
||||
});
|
||||
}
|
||||
|
||||
importTransactions.mutate(
|
||||
{
|
||||
accountId,
|
||||
@@ -949,39 +1013,62 @@ export function ImportTransactionsModal({
|
||||
)}
|
||||
|
||||
{isOfxFile(filetype) && (
|
||||
<LabeledCheckbox
|
||||
id="form_fallback_missing_payee"
|
||||
checked={fallbackMissingPayeeToMemo}
|
||||
onChange={() => {
|
||||
setFallbackMissingPayeeToMemo(state => !state);
|
||||
}}
|
||||
>
|
||||
<Trans>Use Memo as a fallback for empty Payees</Trans>
|
||||
</LabeledCheckbox>
|
||||
<>
|
||||
<CheckboxToggle
|
||||
id="form_fallback_missing_payee"
|
||||
checked={fallbackMissingPayeeToMemo}
|
||||
onChange={setFallbackMissingPayeeToMemo}
|
||||
>
|
||||
<Trans>Use Memo as a fallback for empty Payees</Trans>
|
||||
</CheckboxToggle>
|
||||
<CheckboxToggle
|
||||
id="form_ofx_swap_payee_memo"
|
||||
checked={ofxSwapPayeeAndMemo}
|
||||
onChange={setOfxSwapPayeeAndMemo}
|
||||
>
|
||||
<Trans>Swap Payee and Memo</Trans>
|
||||
</CheckboxToggle>
|
||||
</>
|
||||
)}
|
||||
|
||||
{filetype !== 'csv' && (
|
||||
<LabeledCheckbox
|
||||
<CheckboxToggle
|
||||
id="import_notes"
|
||||
checked={importNotes}
|
||||
onChange={() => {
|
||||
setImportNotes(!importNotes);
|
||||
}}
|
||||
onChange={setImportNotes}
|
||||
>
|
||||
<Trans>Import notes from file</Trans>
|
||||
</LabeledCheckbox>
|
||||
</CheckboxToggle>
|
||||
)}
|
||||
|
||||
{filetype === 'qif' && (
|
||||
<CheckboxToggle
|
||||
id="form_qif_swap_payee_memo"
|
||||
checked={qifSwapPayeeAndMemo}
|
||||
onChange={setQifSwapPayeeAndMemo}
|
||||
>
|
||||
<Trans>Swap Payee and Memo</Trans>
|
||||
</CheckboxToggle>
|
||||
)}
|
||||
|
||||
{isCamtFile(filetype) && (
|
||||
<CheckboxToggle
|
||||
id="form_camt_swap_payee_memo"
|
||||
checked={camtSwapPayeeAndMemo}
|
||||
onChange={setCamtSwapPayeeAndMemo}
|
||||
>
|
||||
<Trans>Swap Payee and Memo</Trans>
|
||||
</CheckboxToggle>
|
||||
)}
|
||||
|
||||
{(isOfxFile(filetype) || isCamtFile(filetype)) && (
|
||||
<LabeledCheckbox
|
||||
<CheckboxToggle
|
||||
id="form_dont_reconcile"
|
||||
checked={reconcile}
|
||||
onChange={() => {
|
||||
setReconcile(!reconcile);
|
||||
}}
|
||||
onChange={setReconcile}
|
||||
>
|
||||
<Trans>Merge with existing transactions</Trans>
|
||||
</LabeledCheckbox>
|
||||
</CheckboxToggle>
|
||||
)}
|
||||
|
||||
{/*Import Options */}
|
||||
@@ -1079,33 +1166,27 @@ export function ImportTransactionsModal({
|
||||
style={{ width: 50 }}
|
||||
/>
|
||||
</label>
|
||||
<LabeledCheckbox
|
||||
<CheckboxToggle
|
||||
id="form_has_header"
|
||||
checked={hasHeaderRow}
|
||||
onChange={() => {
|
||||
setHasHeaderRow(!hasHeaderRow);
|
||||
}}
|
||||
onChange={setHasHeaderRow}
|
||||
>
|
||||
<Trans>File has header row</Trans>
|
||||
</LabeledCheckbox>
|
||||
<LabeledCheckbox
|
||||
</CheckboxToggle>
|
||||
<CheckboxToggle
|
||||
id="clear_on_import"
|
||||
checked={clearOnImport}
|
||||
onChange={() => {
|
||||
setClearOnImport(!clearOnImport);
|
||||
}}
|
||||
onChange={setClearOnImport}
|
||||
>
|
||||
<Trans>Clear transactions on import</Trans>
|
||||
</LabeledCheckbox>
|
||||
<LabeledCheckbox
|
||||
</CheckboxToggle>
|
||||
<CheckboxToggle
|
||||
id="form_dont_reconcile"
|
||||
checked={reconcile}
|
||||
onChange={() => {
|
||||
setReconcile(!reconcile);
|
||||
}}
|
||||
onChange={setReconcile}
|
||||
>
|
||||
<Trans>Merge with existing transactions</Trans>
|
||||
</LabeledCheckbox>
|
||||
</CheckboxToggle>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1113,15 +1194,13 @@ export function ImportTransactionsModal({
|
||||
|
||||
<View style={{ marginRight: 10, gap: 5 }}>
|
||||
<SectionLabel title={t('AMOUNT OPTIONS')} />
|
||||
<LabeledCheckbox
|
||||
<CheckboxToggle
|
||||
id="form_flip"
|
||||
checked={flipAmount}
|
||||
onChange={() => {
|
||||
setFlipAmount(!flipAmount);
|
||||
}}
|
||||
onChange={setFlipAmount}
|
||||
>
|
||||
<Trans>Flip amount</Trans>
|
||||
</LabeledCheckbox>
|
||||
</CheckboxToggle>
|
||||
<MultiplierOption
|
||||
multiplierEnabled={multiplierEnabled}
|
||||
multiplierAmount={multiplierAmount}
|
||||
@@ -1205,17 +1284,43 @@ function getParseOptions(fileType: string, options: ParseFileOptions = {}) {
|
||||
return { delimiter, hasHeaderRow, skipStartLines, skipEndLines };
|
||||
}
|
||||
if (isOfxFile(fileType)) {
|
||||
const { fallbackMissingPayeeToMemo, importNotes } = options;
|
||||
return { fallbackMissingPayeeToMemo, importNotes };
|
||||
const { fallbackMissingPayeeToMemo, importNotes, swapPayeeAndMemo } =
|
||||
options;
|
||||
return { fallbackMissingPayeeToMemo, importNotes, swapPayeeAndMemo };
|
||||
}
|
||||
if (fileType === 'qif') {
|
||||
const { importNotes, swapPayeeAndMemo } = options;
|
||||
return { importNotes, swapPayeeAndMemo };
|
||||
}
|
||||
if (isCamtFile(fileType)) {
|
||||
const { importNotes } = options;
|
||||
return { importNotes };
|
||||
const { importNotes, swapPayeeAndMemo } = options;
|
||||
return { importNotes, swapPayeeAndMemo };
|
||||
}
|
||||
const { importNotes } = options;
|
||||
return { importNotes };
|
||||
}
|
||||
|
||||
function getSwapOption(
|
||||
fileType: string,
|
||||
ofxSwapPayeeAndMemo: boolean,
|
||||
qifSwapPayeeAndMemo: boolean,
|
||||
camtSwapPayeeAndMemo: boolean,
|
||||
) {
|
||||
if (isOfxFile(fileType)) {
|
||||
return ofxSwapPayeeAndMemo;
|
||||
}
|
||||
|
||||
if (fileType === 'qif') {
|
||||
return qifSwapPayeeAndMemo;
|
||||
}
|
||||
|
||||
if (isCamtFile(fileType)) {
|
||||
return camtSwapPayeeAndMemo;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isOfxFile(fileType: string) {
|
||||
return fileType === 'ofx' || fileType === 'qfx';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user