mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-15 23:47:33 +08:00
995 lines
42 KiB
Vue
995 lines
42 KiB
Vue
<template>
|
|
<v-dialog :persistent="!!persistent" v-model="showState">
|
|
<v-card class="pa-sm-1 pa-md-2">
|
|
<template #title>
|
|
<div class="d-flex align-center justify-center">
|
|
<div class="d-flex w-100 align-center">
|
|
<h4 class="text-h4">{{ tt('Import Transactions') }}</h4>
|
|
<v-progress-circular indeterminate size="22" class="ms-2" v-if="loading"></v-progress-circular>
|
|
</div>
|
|
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
|
|
:icon="true" :disabled="loading || submitting"
|
|
v-if="currentStep === 'defineColumn' && importTransactionDefineColumnTab?.menus">
|
|
<v-icon :icon="mdiDotsVertical" />
|
|
<v-menu activator="parent" max-height="500">
|
|
<v-list>
|
|
<v-list-item :key="index"
|
|
:prepend-icon="menu.prependIcon"
|
|
:title="menu.title"
|
|
:disabled="menu.disabled"
|
|
@click="menu.onClick()"
|
|
v-for="(menu, index) in importTransactionDefineColumnTab.menus"/>
|
|
</v-list>
|
|
</v-menu>
|
|
</v-btn>
|
|
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
|
|
:icon="true" :disabled="loading || submitting"
|
|
v-if="currentStep === 'executeCustomScript' && importTransactionExecuteCustomScriptTab?.menus">
|
|
<v-icon :icon="mdiDotsVertical" />
|
|
<v-menu activator="parent" max-height="500">
|
|
<v-list>
|
|
<v-list-item :key="index"
|
|
:prepend-icon="menu.prependIcon"
|
|
:title="menu.title"
|
|
:disabled="menu.disabled"
|
|
@click="menu.onClick()"
|
|
v-for="(menu, index) in importTransactionExecuteCustomScriptTab.menus"/>
|
|
</v-list>
|
|
</v-menu>
|
|
</v-btn>
|
|
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
|
|
:icon="true" :disabled="loading || submitting"
|
|
v-if="currentStep === 'checkData' && importTransactionCheckDataTab?.filterMenus">
|
|
<v-icon :icon="mdiFilterOutline" />
|
|
<v-menu activator="parent" max-height="500">
|
|
<v-list>
|
|
<template :key="groupIndex" v-for="(group, groupIndex) in importTransactionCheckDataTab.filterMenus">
|
|
<v-divider class="my-2" v-if="groupIndex > 0" />
|
|
<v-list-subheader :title="group.title" v-if="group.title" />
|
|
<v-list-item :key="`menu_${groupIndex}_${index}`"
|
|
:prepend-icon="menu.prependIcon"
|
|
:title="menu.title"
|
|
:subtitle="menu.subTitle"
|
|
:append-icon="menu.appendIcon"
|
|
:disabled="menu.disabled"
|
|
@click="menu.onClick()"
|
|
v-for="(menu, index) in group.items" />
|
|
</template>
|
|
</v-list>
|
|
</v-menu>
|
|
</v-btn>
|
|
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
|
|
:icon="true" :disabled="loading || submitting"
|
|
v-if="currentStep === 'checkData' && importTransactionCheckDataTab?.toolMenus">
|
|
<v-icon :icon="mdiDotsVertical" />
|
|
<v-menu activator="parent" max-height="500">
|
|
<v-list>
|
|
<template :key="index" v-for="(menu, index) in importTransactionCheckDataTab.toolMenus">
|
|
<v-divider class="my-2" v-if="menu.divider" />
|
|
<v-list-item :prepend-icon="menu.prependIcon"
|
|
:title="menu.title"
|
|
:subtitle="menu.subTitle"
|
|
:append-icon="menu.appendIcon"
|
|
:disabled="menu.disabled"
|
|
@click="menu.onClick()" />
|
|
</template>
|
|
</v-list>
|
|
</v-menu>
|
|
</v-btn>
|
|
</div>
|
|
</template>
|
|
|
|
<v-card-text>
|
|
<div class="cursor-default">
|
|
<steps-bar min-width="700" :clickable="false" :steps="allSteps" :current-step="currentStep" />
|
|
</div>
|
|
|
|
<v-window class="disable-tab-transition" v-model="currentStep">
|
|
<v-window-item value="uploadFile">
|
|
<v-row class="pt-2">
|
|
<v-col cols="12" md="12">
|
|
<two-column-select primary-key-field="displayCategoryName"
|
|
primary-value-field="displayCategoryName"
|
|
primary-title-field="displayCategoryName"
|
|
primary-sub-items-field="fileTypes"
|
|
secondary-key-field="type"
|
|
secondary-value-field="type"
|
|
secondary-title-field="displayName"
|
|
:disabled="submitting"
|
|
:enable-filter="true"
|
|
:filter-placeholder="tt('Find file type')"
|
|
:filter-no-items-text="tt('No available file type')"
|
|
:label="tt('File Type')"
|
|
:placeholder="tt('File Type')"
|
|
:items="allSupportedImportFileCategoryAndTypes"
|
|
:auto-update-menu-position="true"
|
|
v-model="fileType">
|
|
</two-column-select>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="12" v-if="allFileSubTypes">
|
|
<v-select
|
|
item-title="displayName"
|
|
item-value="type"
|
|
:disabled="submitting"
|
|
:label="tt('Format')"
|
|
:placeholder="tt('Format')"
|
|
:items="allFileSubTypes"
|
|
v-model="fileSubType"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="12" v-if="!isImportDataFromTextbox && allSupportedEncodings">
|
|
<v-select
|
|
item-title="displayName"
|
|
item-value="encoding"
|
|
:disabled="submitting"
|
|
:label="tt('File Encoding')"
|
|
:placeholder="tt('File Encoding')"
|
|
:items="allSupportedEncodings"
|
|
v-model="fileEncoding"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="12" v-if="fileType === 'dsv' || fileType === 'dsv_data'">
|
|
<v-select
|
|
item-title="displayName"
|
|
item-value="type"
|
|
:disabled="submitting"
|
|
:label="tt('Handling Method')"
|
|
:placeholder="tt('Handling Method')"
|
|
:items="[
|
|
{ displayName: tt('Column Mapping'), type: ImportDSVProcessMethod.ColumnMapping },
|
|
{ displayName: tt('Custom Script'), type: ImportDSVProcessMethod.CustomScript }
|
|
]"
|
|
v-model="processDSVMethod"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="12" v-if="supportedAdditionalOptions">
|
|
<v-select
|
|
:disabled="submitting"
|
|
:label="tt('Additional Options')"
|
|
:placeholder="tt('Additional Options')"
|
|
v-model="fileType"
|
|
v-model:menu="additionalOptionsMenuState"
|
|
>
|
|
<template #selection>
|
|
<span class="cursor-pointer">{{ displaySelectedAdditionalOptions }}</span>
|
|
</template>
|
|
|
|
<template #no-data>
|
|
<v-list class="py-0">
|
|
<template v-for="item in allSupportedAdditionalOptions">
|
|
<v-list-item :key="item.key"
|
|
:append-icon="importAdditionalOptions[item.key] ? mdiCheck : undefined"
|
|
@click="importAdditionalOptions[item.key] = !importAdditionalOptions[item.key]"
|
|
v-if="isDefined(supportedAdditionalOptions[item.key])">{{ tt(item.name) }}</v-list-item>
|
|
</template>
|
|
</v-list>
|
|
</template>
|
|
</v-select>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="12" v-if="!isImportDataFromTextbox">
|
|
<v-text-field
|
|
readonly
|
|
persistent-placeholder
|
|
type="text"
|
|
class="always-cursor-pointer"
|
|
:disabled="submitting"
|
|
:label="tt('Data File')"
|
|
:placeholder="tt('format.misc.clickToSelectedFile', { extensions: supportedImportFileExtensions })"
|
|
v-model="fileName"
|
|
@click="showOpenFileDialog"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="12" v-if="isImportDataFromTextbox">
|
|
<v-textarea
|
|
type="text"
|
|
persistent-placeholder
|
|
rows="5"
|
|
:disabled="submitting"
|
|
:placeholder="tt('Data to import')"
|
|
v-model="importData"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="12" v-if="exportFileGuideDocumentUrl">
|
|
<a :href="exportFileGuideDocumentUrl" :class="{ 'disabled': submitting }" target="_blank">
|
|
<v-icon :icon="mdiHelpCircleOutline" size="16" />
|
|
<span class="ms-1" v-if="fileType === 'dsv' || fileType === 'dsv_data'">{{ tt('How to import this file?') }}</span>
|
|
<span class="ms-1" v-if="fileType !== 'dsv' && fileType !== 'dsv_data'">{{ tt('How to export this file?') }}</span>
|
|
<span class="ms-1" v-if="exportFileGuideDocumentLanguageName">[{{ exportFileGuideDocumentLanguageName }}]</span>
|
|
</a>
|
|
</v-col>
|
|
</v-row>
|
|
</v-window-item>
|
|
<v-window-item value="defineColumn">
|
|
<import-transaction-define-column-tab
|
|
ref="importTransactionDefineColumnTab"
|
|
:parsed-file-data="parsedFileData"
|
|
:disabled="loading || submitting"
|
|
/>
|
|
</v-window-item>
|
|
<v-window-item value="executeCustomScript">
|
|
<import-transaction-execute-custom-script-tab
|
|
ref="importTransactionExecuteCustomScriptTab"
|
|
:parsed-file-data="parsedFileData"
|
|
:disabled="loading || submitting"
|
|
/>
|
|
</v-window-item>
|
|
<v-window-item value="checkData">
|
|
<import-transaction-check-data-tab
|
|
ref="importTransactionCheckDataTab"
|
|
:import-transactions="importTransactions"
|
|
:disabled="loading || submitting"
|
|
/>
|
|
</v-window-item>
|
|
<v-window-item value="finalResult">
|
|
<h4 class="text-h4 mb-1">{{ tt('Data Import Completed') }}</h4>
|
|
<p class="my-5">{{ tt('format.misc.importTransactionResult', { count: getDisplayCount(importedCount || 0) }) }}</p>
|
|
</v-window-item>
|
|
</v-window>
|
|
</v-card-text>
|
|
<v-card-text>
|
|
<div class="d-flex justify-center justify-sm-space-between flex-wrap mt-sm-1 mt-md-2 gap-4">
|
|
<v-btn color="secondary" variant="tonal" :disabled="loading || submitting"
|
|
:prepend-icon="mdiClose" @click="close(false)"
|
|
v-if="currentStep !== 'finalResult'">{{ tt('Cancel') }}</v-btn>
|
|
<v-btn class="button-icon-with-direction" color="primary"
|
|
:disabled="loading || submitting || (!isImportDataFromTextbox && !importFile) || (isImportDataFromTextbox && !importData) || (!isImportDataFromTextbox && allSupportedEncodings && fileEncoding === 'auto' && !autoDetectedFileEncoding)"
|
|
:append-icon="!submitting ? mdiArrowRight : undefined" @click="parseData"
|
|
v-if="currentStep === 'defineColumn' || currentStep === 'executeCustomScript' || currentStep === 'uploadFile'">
|
|
{{ tt('Next') }}
|
|
<v-progress-circular indeterminate size="22" class="ms-2" v-if="submitting"></v-progress-circular>
|
|
</v-btn>
|
|
<v-btn class="button-icon-with-direction" color="teal"
|
|
:disabled="submitting || importTransactionCheckDataTab?.isEditing || !importTransactionCheckDataTab?.canImport"
|
|
:append-icon="!submitting ? mdiArrowRight : undefined" @click="submit"
|
|
v-if="currentStep === 'checkData'">
|
|
{{ (submitting && importProcess > 0 ? tt('format.misc.importingTransactions', { process: formatNumberToLocalizedNumerals(importProcess, 2) }) : tt('Import')) }}
|
|
<v-progress-circular indeterminate size="22" class="ms-2" v-if="submitting"></v-progress-circular>
|
|
</v-btn>
|
|
<v-btn color="secondary" variant="tonal"
|
|
:append-icon="mdiCheck"
|
|
@click="close(true)"
|
|
v-if="currentStep === 'finalResult'">{{ tt('Close') }}</v-btn>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
<confirm-dialog ref="confirmDialog"/>
|
|
<snack-bar ref="snackbar" />
|
|
<input ref="fileInput" type="file" style="display: none" :accept="supportedImportFileExtensions" @change="setImportFile($event)" />
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { StepBarItem } from '@/components/desktop/StepsBar.vue';
|
|
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
|
import SnackBar from '@/components/desktop/SnackBar.vue';
|
|
import ImportTransactionDefineColumnTab from './tabs/ImportTransactionDefineColumnTab.vue';
|
|
import ImportTransactionExecuteCustomScriptTab from './tabs/ImportTransactionExecuteCustomScriptTab.vue';
|
|
import ImportTransactionCheckDataTab from './tabs/ImportTransactionCheckDataTab.vue';
|
|
|
|
import { ref, computed, useTemplateRef, watch } from 'vue';
|
|
|
|
import { useI18n } from '@/locales/helpers.ts';
|
|
|
|
import { useSettingsStore } from '@/stores/setting.ts';
|
|
import { useAccountsStore } from '@/stores/account.ts';
|
|
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
|
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
|
import { useTransactionsStore } from '@/stores/transaction.ts';
|
|
import { useOverviewStore } from '@/stores/overview.ts';
|
|
import { useStatisticsStore } from '@/stores/statistics.ts';
|
|
|
|
import { type KeyAndName, itemAndIndex } from '@/core/base.ts';
|
|
import { type NumeralSystem } from '@/core/numeral.ts';
|
|
import { TransactionType } from '@/core/transaction.ts';
|
|
import {
|
|
type ImportFileTypeSupportedAdditionalOptions,
|
|
type LocalizedImportFileCategoryAndTypes,
|
|
type LocalizedImportFileType,
|
|
type LocalizedImportFileTypeSubType,
|
|
type LocalizedImportFileTypeSupportedEncodings,
|
|
KnownFileType
|
|
} from '@/core/file.ts';
|
|
import { UTF_8 } from '@/consts/file.ts';
|
|
|
|
import { ImportTransaction } from '@/models/imported_transaction.ts';
|
|
|
|
import { isDefined, isNumber } from '@/lib/common.ts';
|
|
import { findExtensionByType, isFileExtensionSupported, detectFileEncoding } from '@/lib/file.ts';
|
|
import { generateRandomUUID } from '@/lib/misc.ts';
|
|
import logger from '@/lib/logger.ts';
|
|
|
|
import {
|
|
mdiFilterOutline,
|
|
mdiCheck,
|
|
mdiDotsVertical,
|
|
mdiHelpCircleOutline,
|
|
mdiClose,
|
|
mdiArrowRight
|
|
} from '@mdi/js';
|
|
|
|
type ConfirmDialogType = InstanceType<typeof ConfirmDialog>;
|
|
type SnackBarType = InstanceType<typeof SnackBar>;
|
|
type ImportTransactionDefineColumnTabType = InstanceType<typeof ImportTransactionDefineColumnTab>;
|
|
type ImportTransactionExecuteCustomScriptTabType = InstanceType<typeof ImportTransactionExecuteCustomScriptTab>;
|
|
type ImportTransactionCheckDataTabType = InstanceType<typeof ImportTransactionCheckDataTab>;
|
|
|
|
type ImportTransactionDialogStep = 'uploadFile' | 'defineColumn' | 'executeCustomScript' | 'checkData' | 'finalResult';
|
|
enum ImportDSVProcessMethod {
|
|
ColumnMapping,
|
|
CustomScript
|
|
};
|
|
|
|
defineProps<{
|
|
persistent?: boolean;
|
|
}>();
|
|
|
|
const {
|
|
tt,
|
|
joinMultiText,
|
|
getCurrentNumeralSystemType,
|
|
getAllSupportedImportFileCagtegoryAndTypes,
|
|
formatNumberToLocalizedNumerals,
|
|
getLocalizedFileEncodingName
|
|
} = useI18n();
|
|
|
|
const settingsStore = useSettingsStore();
|
|
const accountsStore = useAccountsStore();
|
|
const transactionCategoriesStore = useTransactionCategoriesStore();
|
|
const transactionTagsStore = useTransactionTagsStore();
|
|
const transactionsStore = useTransactionsStore();
|
|
const overviewStore = useOverviewStore();
|
|
const statisticsStore = useStatisticsStore();
|
|
|
|
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
|
|
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
|
const importTransactionDefineColumnTab = useTemplateRef<ImportTransactionDefineColumnTabType>('importTransactionDefineColumnTab');
|
|
const importTransactionExecuteCustomScriptTab = useTemplateRef<ImportTransactionExecuteCustomScriptTabType>('importTransactionExecuteCustomScriptTab');
|
|
const importTransactionCheckDataTab = useTemplateRef<ImportTransactionCheckDataTabType>('importTransactionCheckDataTab');
|
|
const fileInput = useTemplateRef<HTMLInputElement>('fileInput');
|
|
|
|
const allSupportedAdditionalOptions: KeyAndName[] = [
|
|
{
|
|
key: 'payeeAsTag',
|
|
name: 'Parse Payee as Tag'
|
|
},
|
|
{
|
|
key: 'payeeAsDescription',
|
|
name: 'Parse Payee as Description'
|
|
},
|
|
{
|
|
key: 'memberAsTag',
|
|
name: 'Parse Member as Tag'
|
|
},
|
|
{
|
|
key: 'projectAsTag',
|
|
name: 'Parse Project as Tag'
|
|
},
|
|
{
|
|
key: 'merchantAsTag',
|
|
name: 'Parse Merchant as Tag'
|
|
}
|
|
];
|
|
|
|
const showState = ref<boolean>(false);
|
|
const additionalOptionsMenuState = ref<boolean>(false);
|
|
const clientSessionId = ref<string>('');
|
|
const currentStep = ref<ImportTransactionDialogStep>('uploadFile');
|
|
const importProcess = ref<number>(0);
|
|
const fileType = ref<string>('ezbookkeeping');
|
|
const fileSubType = ref<string>('ezbookkeeping_csv');
|
|
const fileEncoding = ref<string>('auto');
|
|
const detectingFileEncoding = ref<boolean>(false);
|
|
const autoDetectedFileEncoding = ref<string | undefined>(undefined);
|
|
const processDSVMethod = ref<ImportDSVProcessMethod>(ImportDSVProcessMethod.ColumnMapping);
|
|
const importFile = ref<File | null>(null);
|
|
const importData = ref<string>('');
|
|
const importAdditionalOptions = ref<ImportFileTypeSupportedAdditionalOptions>({});
|
|
const parsedFileData = ref<string[][] | undefined>(undefined);
|
|
const importTransactions = ref<ImportTransaction[] | undefined>(undefined);
|
|
|
|
const importedCount = ref<number | null>(null);
|
|
const loading = ref<boolean>(true);
|
|
const submitting = ref<boolean>(false);
|
|
|
|
let resolveFunc: (() => void) | null = null;
|
|
let rejectFunc: ((reason?: unknown) => void) | null = null;
|
|
|
|
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
|
|
|
|
const allSupportedImportFileCategoryAndTypes = computed<LocalizedImportFileCategoryAndTypes[]>(() => getAllSupportedImportFileCagtegoryAndTypes());
|
|
const allFileSubTypes = computed<LocalizedImportFileTypeSubType[] | undefined>(() => allSupportedImportFileTypesMap.value[fileType.value]?.subTypes);
|
|
const allSupportedEncodings = computed<LocalizedImportFileTypeSupportedEncodings[] | undefined>(() => {
|
|
const supportedEncodings = allSupportedImportFileTypesMap.value[fileType.value]?.supportedEncodings;
|
|
|
|
if (!supportedEncodings) {
|
|
return undefined;
|
|
}
|
|
|
|
const ret: LocalizedImportFileTypeSupportedEncodings[] = [];
|
|
let autoDetectDisplayName = tt('Auto detect');
|
|
|
|
if (importFile.value) {
|
|
if (detectingFileEncoding.value) {
|
|
autoDetectDisplayName += ` [${tt('Detecting...')}]`;
|
|
} else if (autoDetectedFileEncoding.value) {
|
|
autoDetectDisplayName += ` [${getLocalizedFileEncodingName(autoDetectedFileEncoding.value)}]`;
|
|
} else {
|
|
autoDetectDisplayName += ` [${tt('Unknown')}]`;
|
|
}
|
|
}
|
|
|
|
const autoDetectEncoding: LocalizedImportFileTypeSupportedEncodings = {
|
|
displayName: autoDetectDisplayName,
|
|
encoding: 'auto'
|
|
};
|
|
|
|
ret.push(autoDetectEncoding);
|
|
|
|
if (supportedEncodings && supportedEncodings.length) {
|
|
ret.push(...supportedEncodings);
|
|
}
|
|
|
|
return ret;
|
|
});
|
|
const isImportDataFromTextbox = computed<boolean>(() => allSupportedImportFileTypesMap.value[fileType.value]?.dataFromTextbox ?? false);
|
|
const supportedAdditionalOptions = computed<ImportFileTypeSupportedAdditionalOptions | undefined>(() => allSupportedImportFileTypesMap.value[fileType.value]?.supportedAdditionalOptions);
|
|
|
|
const allSteps = computed<StepBarItem[]>(() => {
|
|
const steps: StepBarItem[] = [
|
|
{
|
|
name: 'uploadFile',
|
|
title: tt('Upload File'),
|
|
subTitle: tt('Upload Transaction Data File')
|
|
}
|
|
];
|
|
|
|
if (fileType.value === 'dsv' || fileType.value === 'dsv_data') {
|
|
if (processDSVMethod.value === ImportDSVProcessMethod.CustomScript) {
|
|
steps.push({
|
|
name: 'executeCustomScript',
|
|
title: tt('Execute Custom Script'),
|
|
subTitle: tt('Execute Custom Script to Parse Data')
|
|
});
|
|
} else {
|
|
steps.push({
|
|
name: 'defineColumn',
|
|
title: tt('Define Column'),
|
|
subTitle: tt('Define and Check Column Mapping')
|
|
});
|
|
}
|
|
}
|
|
|
|
steps.push(...[
|
|
{
|
|
name: 'checkData',
|
|
title: tt('Check & Modify'),
|
|
subTitle: tt('Check and Modify Your Data')
|
|
},
|
|
{
|
|
name: 'finalResult',
|
|
title: tt('Complete'),
|
|
subTitle: tt('Data Import Completed')
|
|
}
|
|
]);
|
|
|
|
return steps;
|
|
});
|
|
|
|
const allSupportedImportFileTypesMap = computed<Record<string, LocalizedImportFileType>>(() => {
|
|
const ret: Record<string, LocalizedImportFileType> = {};
|
|
|
|
for (const importFileCategoryAndTypes of allSupportedImportFileCategoryAndTypes.value) {
|
|
for (const importFileType of importFileCategoryAndTypes.fileTypes) {
|
|
ret[importFileType.type] = importFileType;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
});
|
|
|
|
const supportedImportFileExtensions = computed<string | undefined>(() => {
|
|
if (allFileSubTypes.value && allFileSubTypes.value.length) {
|
|
const subTypeExtensions = findExtensionByType(allFileSubTypes.value, fileSubType.value);
|
|
|
|
if (subTypeExtensions) {
|
|
return subTypeExtensions;
|
|
}
|
|
}
|
|
|
|
return allSupportedImportFileTypesMap.value[fileType.value]?.extensions;
|
|
});
|
|
|
|
const displaySelectedAdditionalOptions = computed<string>(() => {
|
|
if (!supportedAdditionalOptions.value) {
|
|
return tt('None');
|
|
}
|
|
|
|
const selectedOptions: string[] = [];
|
|
|
|
for (const option of allSupportedAdditionalOptions) {
|
|
if (isDefined(supportedAdditionalOptions.value[option.key]) && importAdditionalOptions.value[option.key]) {
|
|
selectedOptions.push(tt(option.name));
|
|
}
|
|
}
|
|
|
|
if (selectedOptions.length < 1) {
|
|
return tt('None');
|
|
}
|
|
|
|
return joinMultiText(selectedOptions);
|
|
});
|
|
|
|
const exportFileGuideDocumentUrl = computed<string | undefined>(() => {
|
|
const document = allSupportedImportFileTypesMap.value[fileType.value]?.document;
|
|
|
|
if (!document) {
|
|
return undefined;
|
|
}
|
|
|
|
const language = document.language ? document.language + '/' : '';
|
|
const anchor = document.anchor ? '#' + document.anchor : '';
|
|
return `https://ezbookkeeping.mayswind.net/${language}export_and_import${anchor}`;
|
|
});
|
|
|
|
const exportFileGuideDocumentLanguageName = computed<string | undefined>(() => allSupportedImportFileTypesMap.value[fileType.value]?.document?.displayLanguageName);
|
|
|
|
const fileName = computed<string>(() => importFile.value?.name || '');
|
|
|
|
function getDisplayCount(count: number): string {
|
|
return numeralSystem.value.formatNumber(count);
|
|
}
|
|
|
|
function loadInitFileTypeFromSettings(): void {
|
|
if (!settingsStore.appSettings.lastSelectedFileTypeInImportTransactionDialog) {
|
|
return;
|
|
}
|
|
|
|
const lastSelectedFileTypes = settingsStore.appSettings.lastSelectedFileTypeInImportTransactionDialog.split('|');
|
|
const lastSelectedFileType = lastSelectedFileTypes[0];
|
|
|
|
if (!lastSelectedFileType || !allSupportedImportFileTypesMap.value[lastSelectedFileType]) {
|
|
return;
|
|
}
|
|
|
|
fileType.value = lastSelectedFileType;
|
|
|
|
const fileSubTypes = allSupportedImportFileTypesMap.value[lastSelectedFileType].subTypes;
|
|
|
|
if (!fileSubTypes || fileSubTypes.length < 1) {
|
|
return;
|
|
}
|
|
|
|
const lastSelectedFileSubType = lastSelectedFileTypes[1];
|
|
|
|
if (lastSelectedFileSubType) {
|
|
for (const subType of fileSubTypes) {
|
|
if (subType.type === lastSelectedFileSubType) {
|
|
fileSubType.value = lastSelectedFileSubType;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const firstFileSubType = fileSubTypes[0];
|
|
|
|
if (firstFileSubType) {
|
|
fileSubType.value = firstFileSubType.type;
|
|
}
|
|
}
|
|
|
|
function open(): Promise<void> {
|
|
fileType.value = 'ezbookkeeping';
|
|
fileSubType.value = 'ezbookkeeping_csv';
|
|
|
|
if (settingsStore.appSettings.rememberLastSelectedFileTypeInImportTransactionDialog && settingsStore.appSettings.lastSelectedFileTypeInImportTransactionDialog) {
|
|
loadInitFileTypeFromSettings();
|
|
}
|
|
|
|
fileEncoding.value = 'auto';
|
|
detectingFileEncoding.value = false;
|
|
autoDetectedFileEncoding.value = undefined;
|
|
processDSVMethod.value = ImportDSVProcessMethod.ColumnMapping;
|
|
currentStep.value = 'uploadFile';
|
|
importProcess.value = 0;
|
|
importFile.value = null;
|
|
importData.value = '';
|
|
importAdditionalOptions.value = Object.assign({}, supportedAdditionalOptions.value ?? {});
|
|
parsedFileData.value = undefined;
|
|
importTransactionDefineColumnTab.value?.reset();
|
|
importTransactionExecuteCustomScriptTab.value?.reset();
|
|
importTransactions.value = undefined;
|
|
importTransactionCheckDataTab.value?.reset();
|
|
showState.value = true;
|
|
clientSessionId.value = generateRandomUUID();
|
|
|
|
const promises = [
|
|
accountsStore.loadAllAccounts({ force: false }),
|
|
transactionCategoriesStore.loadAllCategories({ force: false }),
|
|
transactionTagsStore.loadAllTags({ force: false })
|
|
];
|
|
|
|
Promise.all(promises).then(() => {
|
|
loading.value = false;
|
|
}).catch(error => {
|
|
logger.error('failed to load essential data for importing transaction', error);
|
|
|
|
loading.value = false;
|
|
showState.value = false;
|
|
|
|
if (!error.processed) {
|
|
if (rejectFunc) {
|
|
rejectFunc(error);
|
|
}
|
|
}
|
|
});
|
|
|
|
return new Promise((resolve, reject) => {
|
|
resolveFunc = resolve;
|
|
rejectFunc = reject;
|
|
});
|
|
}
|
|
|
|
function showOpenFileDialog(): void {
|
|
if (submitting.value) {
|
|
return;
|
|
}
|
|
|
|
fileInput.value?.click();
|
|
}
|
|
|
|
function setImportFile(event: Event): void {
|
|
if (!event || !event.target) {
|
|
return;
|
|
}
|
|
|
|
const el = event.target as HTMLInputElement;
|
|
|
|
if (!el.files || !el.files.length || !el.files[0]) {
|
|
return;
|
|
}
|
|
|
|
importFile.value = el.files[0] as File;
|
|
detectingFileEncoding.value = false;
|
|
autoDetectedFileEncoding.value = undefined;
|
|
el.value = '';
|
|
|
|
if (allSupportedEncodings.value) {
|
|
detectingFileEncoding.value = true;
|
|
|
|
detectFileEncoding(importFile.value).then(detectedEncoding => {
|
|
detectingFileEncoding.value = false;
|
|
autoDetectedFileEncoding.value = detectedEncoding;
|
|
}).catch(() => {
|
|
detectingFileEncoding.value = false;
|
|
autoDetectedFileEncoding.value = undefined;
|
|
});
|
|
}
|
|
}
|
|
|
|
function parseData(): void {
|
|
let uploadFile: File;
|
|
let type: string = fileType.value;
|
|
let encoding: string | undefined = undefined;
|
|
|
|
if (allFileSubTypes.value) {
|
|
type = fileSubType.value;
|
|
}
|
|
|
|
if (allSupportedEncodings.value) {
|
|
if (fileEncoding.value === 'auto') {
|
|
encoding = autoDetectedFileEncoding.value;
|
|
} else {
|
|
encoding = fileEncoding.value;
|
|
}
|
|
}
|
|
|
|
if (!isImportDataFromTextbox.value) {
|
|
if (!importFile.value) {
|
|
snackbar.value?.showError('Please select a file to import');
|
|
return;
|
|
}
|
|
|
|
if (allSupportedEncodings.value) {
|
|
if (fileEncoding.value === 'auto' && !autoDetectedFileEncoding.value) {
|
|
snackbar.value?.showError('Unable to detect the file encoding automatically. Please select the actual encoding.');
|
|
return;
|
|
}
|
|
}
|
|
|
|
uploadFile = importFile.value;
|
|
} else if (isImportDataFromTextbox.value) {
|
|
if (!importData.value) {
|
|
snackbar.value?.showError('No data to import');
|
|
return;
|
|
}
|
|
|
|
if (type === 'custom_csv') {
|
|
uploadFile = KnownFileType.CSV.createFile(importData.value, 'import');
|
|
} else if (type === 'custom_tsv') {
|
|
uploadFile = KnownFileType.TSV.createFile(importData.value, 'import');
|
|
} else {
|
|
snackbar.value?.showError('Parameter Invalid');
|
|
return;
|
|
}
|
|
|
|
encoding = UTF_8;
|
|
} else { // should not happen, but ts would check whether uploadFile has been assigned a value
|
|
snackbar.value?.showMessage('An error occurred');
|
|
return;
|
|
}
|
|
|
|
const isDsvFileType: boolean = fileType.value === 'dsv' || fileType.value === 'dsv_data';
|
|
|
|
if (isDsvFileType && currentStep.value === 'uploadFile') {
|
|
submitting.value = true;
|
|
|
|
transactionsStore.parseImportDsvFile({
|
|
fileType: type,
|
|
fileEncoding: encoding,
|
|
importFile: uploadFile
|
|
}).then(response => {
|
|
if (response && response.length) {
|
|
if (processDSVMethod.value === ImportDSVProcessMethod.CustomScript) {
|
|
importTransactionExecuteCustomScriptTab.value?.reset();
|
|
parsedFileData.value = response;
|
|
currentStep.value = 'executeCustomScript';
|
|
} else {
|
|
importTransactionDefineColumnTab.value?.reset();
|
|
parsedFileData.value = response;
|
|
currentStep.value = 'defineColumn';
|
|
}
|
|
} else {
|
|
parsedFileData.value = undefined;
|
|
snackbar.value?.showError('No data to import');
|
|
}
|
|
|
|
submitting.value = false;
|
|
}).catch(error => {
|
|
submitting.value = false;
|
|
|
|
if (!error.processed) {
|
|
snackbar.value?.showError(error);
|
|
}
|
|
});
|
|
} else {
|
|
let columnMapping: Record<number, number> | undefined = undefined;
|
|
let transactionTypeMapping: Record<string, TransactionType> | undefined = undefined;
|
|
let hasHeaderLine: boolean | undefined = undefined;
|
|
let timeFormat: string | undefined = undefined;
|
|
let timezoneFormat: string | undefined = undefined;
|
|
let amountDecimalSeparator: string | undefined = undefined;
|
|
let amountDigitGroupingSymbol: string | undefined = undefined;
|
|
let geoLocationSeparator: string | undefined = undefined;
|
|
let geoLocationOrder: string | undefined = undefined;
|
|
let tagSeparator: string | undefined = undefined;
|
|
|
|
if (isDsvFileType && processDSVMethod.value === ImportDSVProcessMethod.ColumnMapping) {
|
|
const defineColumnResult = importTransactionDefineColumnTab.value?.generateResult();
|
|
|
|
if (!defineColumnResult) {
|
|
return;
|
|
}
|
|
|
|
columnMapping = defineColumnResult.columnMapping;
|
|
transactionTypeMapping = defineColumnResult.transactionTypeMapping;
|
|
hasHeaderLine = defineColumnResult.includeHeader;
|
|
timeFormat = defineColumnResult.timeFormat;
|
|
timezoneFormat = defineColumnResult.timezoneFormat;
|
|
amountDecimalSeparator = defineColumnResult.amountDecimalSeparator;
|
|
amountDigitGroupingSymbol = defineColumnResult.amountDigitGroupingSymbol;
|
|
geoLocationSeparator = defineColumnResult.geoLocationSeparator;
|
|
geoLocationOrder = defineColumnResult.geoLocationOrder;
|
|
tagSeparator = defineColumnResult.tagSeparator;
|
|
} else if (isDsvFileType && processDSVMethod.value === ImportDSVProcessMethod.CustomScript) {
|
|
const executeCustomScriptResult = importTransactionExecuteCustomScriptTab.value?.generateResult();
|
|
|
|
if (!executeCustomScriptResult) {
|
|
return;
|
|
}
|
|
|
|
type = 'ezbookkeeping_json';
|
|
encoding = undefined;
|
|
uploadFile = KnownFileType.JSON.createFile(executeCustomScriptResult, 'import');
|
|
}
|
|
|
|
submitting.value = true;
|
|
|
|
transactionsStore.parseImportTransaction({
|
|
fileType: type,
|
|
additionalOptions: importAdditionalOptions.value,
|
|
fileEncoding: encoding,
|
|
importFile: uploadFile,
|
|
columnMapping: columnMapping,
|
|
transactionTypeMapping: transactionTypeMapping,
|
|
hasHeaderLine: hasHeaderLine,
|
|
timeFormat: timeFormat,
|
|
timezoneFormat: timezoneFormat,
|
|
amountDecimalSeparator: amountDecimalSeparator,
|
|
amountDigitGroupingSymbol: amountDigitGroupingSymbol,
|
|
geoSeparator: geoLocationSeparator,
|
|
geoOrder: geoLocationOrder,
|
|
tagSeparator: tagSeparator
|
|
}).then(response => {
|
|
const parsedTransactions: ImportTransaction[] = [];
|
|
|
|
if (response.items) {
|
|
for (const [importTransaction, index] of itemAndIndex(response.items)) {
|
|
const parsedTransaction = ImportTransaction.of(importTransaction, index);
|
|
parsedTransactions.push(parsedTransaction);
|
|
}
|
|
}
|
|
|
|
importTransactionCheckDataTab.value?.reset();
|
|
|
|
if (parsedTransactions && parsedTransactions.length >= 0 && parsedTransactions.length < 10) {
|
|
importTransactionCheckDataTab.value?.setCountPerPage(-1);
|
|
} else {
|
|
importTransactionCheckDataTab.value?.setCountPerPage(10);
|
|
}
|
|
|
|
importTransactions.value = parsedTransactions;
|
|
currentStep.value = 'checkData';
|
|
submitting.value = false;
|
|
}).catch(error => {
|
|
submitting.value = false;
|
|
|
|
if (!error.processed) {
|
|
snackbar.value?.showError(error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function submit(): void {
|
|
if (importTransactionCheckDataTab.value?.isEditing) {
|
|
return;
|
|
}
|
|
|
|
const transactions: ImportTransaction[] = [];
|
|
|
|
if (importTransactions.value) {
|
|
for (const importTransaction of importTransactions.value) {
|
|
if (importTransaction.valid && importTransaction.selected) {
|
|
transactions.push(importTransaction);
|
|
} else if (!importTransaction.valid && importTransaction.selected) {
|
|
snackbar.value?.showError('Cannot import invalid transactions');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (transactions.length < 1) {
|
|
snackbar.value?.showError('No data to import');
|
|
return;
|
|
}
|
|
|
|
confirmDialog.value?.open('format.misc.confirmImportTransactions', {
|
|
count: getDisplayCount(transactions.length)
|
|
}).then(() => {
|
|
submitting.value = true;
|
|
|
|
let showProcessTimer : number | undefined = undefined;
|
|
|
|
if (transactions.length > 100) {
|
|
setTimeout(() => {
|
|
if (!submitting.value) {
|
|
logger.warn('transaction import is not submitting');
|
|
return;
|
|
}
|
|
|
|
// @ts-expect-error the return value of setInterval is number, but lint shows it as NodeJS.Timer
|
|
showProcessTimer = setInterval(() => {
|
|
if (submitting.value) {
|
|
transactionsStore.getImportTransactionsProcess({
|
|
clientSessionId: clientSessionId.value
|
|
}).then(response => {
|
|
if (isNumber(response) && 0 <= response && response < 100) {
|
|
importProcess.value = response;
|
|
} else {
|
|
importProcess.value = 0;
|
|
clearInterval(showProcessTimer);
|
|
showProcessTimer = undefined;
|
|
}
|
|
}).catch(() => {
|
|
importProcess.value = 0;
|
|
clearInterval(showProcessTimer);
|
|
showProcessTimer = undefined;
|
|
});
|
|
}
|
|
}, 2000);
|
|
}, 2000);
|
|
}
|
|
|
|
transactionsStore.importTransactions({
|
|
transactions: transactions,
|
|
clientSessionId: clientSessionId.value
|
|
}).then(response => {
|
|
if (showProcessTimer) {
|
|
importProcess.value = 0;
|
|
clearInterval(showProcessTimer);
|
|
showProcessTimer = undefined;
|
|
}
|
|
|
|
importedCount.value = response;
|
|
currentStep.value = 'finalResult';
|
|
|
|
accountsStore.updateAccountListInvalidState(true);
|
|
transactionsStore.updateTransactionListInvalidState(true);
|
|
overviewStore.updateTransactionOverviewInvalidState(true);
|
|
statisticsStore.updateTransactionStatisticsInvalidState(true);
|
|
|
|
submitting.value = false;
|
|
}).catch(error => {
|
|
if (showProcessTimer) {
|
|
importProcess.value = 0;
|
|
clearInterval(showProcessTimer);
|
|
showProcessTimer = undefined;
|
|
}
|
|
|
|
submitting.value = false;
|
|
|
|
if (!error.processed) {
|
|
snackbar.value?.showError(error);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function close(completed: boolean): void {
|
|
if (completed) {
|
|
if (resolveFunc) {
|
|
resolveFunc();
|
|
}
|
|
} else {
|
|
if (rejectFunc) {
|
|
rejectFunc();
|
|
}
|
|
}
|
|
|
|
showState.value = false;
|
|
}
|
|
|
|
watch(fileType, (newValue) => {
|
|
if (allFileSubTypes.value && allFileSubTypes.value.length) {
|
|
fileSubType.value = allFileSubTypes.value[0]!.type;
|
|
} else {
|
|
if (settingsStore.appSettings.rememberLastSelectedFileTypeInImportTransactionDialog) {
|
|
settingsStore.setLastSelectedFileTypeInImportTransactionDialog(`${newValue}|`);
|
|
}
|
|
}
|
|
|
|
importFile.value = null;
|
|
parsedFileData.value = undefined;
|
|
importAdditionalOptions.value = Object.assign({}, supportedAdditionalOptions.value ?? {});
|
|
importTransactions.value = undefined;
|
|
});
|
|
|
|
watch(fileSubType, (newValue) => {
|
|
if (settingsStore.appSettings.rememberLastSelectedFileTypeInImportTransactionDialog) {
|
|
settingsStore.setLastSelectedFileTypeInImportTransactionDialog(`${fileType.value}|${newValue}`);
|
|
}
|
|
|
|
let supportedExtensions: string | undefined = findExtensionByType(allFileSubTypes.value, newValue);
|
|
|
|
if (!supportedExtensions) {
|
|
supportedExtensions = allSupportedImportFileTypesMap.value[fileType.value]?.extensions;
|
|
}
|
|
|
|
if (importFile.value && importFile.value.name && !isFileExtensionSupported(importFile.value.name, supportedExtensions || '')) {
|
|
importFile.value = null;
|
|
}
|
|
});
|
|
|
|
defineExpose({
|
|
open
|
|
});
|
|
</script>
|