From c6eb3cfb7421aa1e142d28711ff85cda0823470c Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sun, 1 Mar 2026 16:32:22 +0800 Subject: [PATCH] support automatically applying known column mapping and transaction type mapping rules when importing custom files with column mapping handle method --- src/consts/import_transaction.ts | 200 ++++++++++++++++++ .../tabs/ImportTransactionDefineColumnTab.vue | 80 ++++++- 2 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 src/consts/import_transaction.ts diff --git a/src/consts/import_transaction.ts b/src/consts/import_transaction.ts new file mode 100644 index 00000000..8449d20a --- /dev/null +++ b/src/consts/import_transaction.ts @@ -0,0 +1,200 @@ +import { entries } from '@/core/base.ts'; +import { TransactionType } from '@/core/transaction.ts'; +import { ImportTransactionColumnType } from '@/core/import_transaction.ts'; + +export const KNOWN_COLUMN_NAME_MAPPING: Record = ((mappings: Record[]) => { + const result: Record = {}; + + for (const mapping of mappings) { + for (const [key, value] of entries(mapping)) { + const normalizedKey = key.toLowerCase().replaceAll(' ', '').replaceAll('_', '').replaceAll('-', ''); + + if (result[normalizedKey]) { + continue; + } + + result[normalizedKey] = value; + } + } + + return result; +})([ + // Columns of ezbookkeeping Data Export File + { + ['Time']: ImportTransactionColumnType.TransactionTime, + ['Timezone']: ImportTransactionColumnType.TransactionTimezone, + ['Type']: ImportTransactionColumnType.TransactionType, + ['Category']: ImportTransactionColumnType.Category, + ['Sub Category']: ImportTransactionColumnType.SubCategory, + ['Account']: ImportTransactionColumnType.AccountName, + ['Account Currency']: ImportTransactionColumnType.AccountCurrency, + ['Amount']: ImportTransactionColumnType.Amount, + ['Account2']: ImportTransactionColumnType.RelatedAccountName, + ['Account2 Currency']: ImportTransactionColumnType.RelatedAccountCurrency, + ['Account2 Amount']: ImportTransactionColumnType.RelatedAmount, + ['Geographic Location']: ImportTransactionColumnType.GeographicLocation, + ['Tags']: ImportTransactionColumnType.Tags, + ['Description']: ImportTransactionColumnType.Description + }, + // Other common columns of transaction time + { + // en + ['Date']: ImportTransactionColumnType.TransactionTime, + ['Datetime']: ImportTransactionColumnType.TransactionTime, + ['Timestamp']: ImportTransactionColumnType.TransactionTime, + // zh-Hans + ['日期']: ImportTransactionColumnType.TransactionTime, + ['时间']: ImportTransactionColumnType.TransactionTime, + ['交易日期']: ImportTransactionColumnType.TransactionTime, + ['交易时间']: ImportTransactionColumnType.TransactionTime, + }, + // Other common columns of transaction timezone + { + + }, + // Other common columns of transaction type + { + // en + ['Transaction Type']: ImportTransactionColumnType.TransactionType, + // zh-Hans + ['交易类型']: ImportTransactionColumnType.TransactionType, + ['类型']: ImportTransactionColumnType.TransactionType, + ['收/支']: ImportTransactionColumnType.TransactionType, + }, + // Other common columns of category + { + // en + ['Category Name']: ImportTransactionColumnType.Category, + // zh-Hans + ['交易分类']: ImportTransactionColumnType.Category, + ['类别']: ImportTransactionColumnType.Category, + ['分类']: ImportTransactionColumnType.Category, + }, + // Other common columns of sub category + { + // zh-Hans + ['子类别']: ImportTransactionColumnType.SubCategory, + ['子分类']: ImportTransactionColumnType.SubCategory, + ['二级分类']: ImportTransactionColumnType.SubCategory, + }, + // Other common columns of account name + { + // en + ['Account Name']: ImportTransactionColumnType.AccountName, + ['Source Name']: ImportTransactionColumnType.AccountName, + // zh-Hans + ['账户']: ImportTransactionColumnType.AccountName, + ['账户1']: ImportTransactionColumnType.AccountName, + }, + // Other common columns of account currency + { + // en + ['Currency']: ImportTransactionColumnType.AccountCurrency, + ['Currency Code']: ImportTransactionColumnType.AccountCurrency, + // zh-Hans + ['账户币种']: ImportTransactionColumnType.AccountCurrency, + ['币种']: ImportTransactionColumnType.AccountCurrency, + }, + // Other common columns of amount + { + // zh-Hans + ['金额']: ImportTransactionColumnType.Amount, + }, + // Other common columns of related account name + { + // en + ['Destination Name']: ImportTransactionColumnType.RelatedAccountName, + // zh-Hans + ['账户2']: ImportTransactionColumnType.RelatedAccountName, + }, + // Other common columns of related account currency + { + // en + ['Foreign Currency']: ImportTransactionColumnType.RelatedAccountCurrency, + ['Foreign Currency Code']: ImportTransactionColumnType.RelatedAccountCurrency, + }, + // Other common columns of related amount + { + // en + ['Foreign Amount']: ImportTransactionColumnType.RelatedAmount, + }, + // Other common columns of geographic location + { + + }, + // Other common columns of tags + { + // zh-Hans + ['标签']: ImportTransactionColumnType.Tags, + }, + // Other common columns of description + { + // en + ['Comment']: ImportTransactionColumnType.Description, + ['Note']: ImportTransactionColumnType.Description, + ['Memo']: ImportTransactionColumnType.Description, + // zh-Hans + ['备注']: ImportTransactionColumnType.Description, + } +]); + +export const KNOWN_TRANSACTION_TYPE_NAME_MAPPING: Record = ((mappings: Record[]) => { + const result: Record = {}; + + for (const mapping of mappings) { + for (const [key, value] of entries(mapping)) { + const normalizedKey = key.toLowerCase().replaceAll(' ', '').replaceAll('_', '').replaceAll('-', ''); + + if (result[normalizedKey]) { + continue; + } + + result[normalizedKey] = value; + } + } + + return result; +})([ + // Transaction types of ezbookkeeping Data Export File + { + ['Balance Modification']: TransactionType.ModifyBalance, + ['Income']: TransactionType.Income, + ['Expense']: TransactionType.Expense, + ['Transfer']: TransactionType.Transfer, + }, + // Other common balance modification type + { + // en + ['Opening balance']: TransactionType.ModifyBalance, + // zh-Hans + ['余额变更']: TransactionType.ModifyBalance, + ['负债变更']: TransactionType.ModifyBalance, + }, + // Other common income type + { + // en + ['Deposit']: TransactionType.Income, + // zh-Hans + ['收入']: TransactionType.Income, + }, + // Other common expense type + { + // en + ['Withdrawal']: TransactionType.Expense, + // zh-Hans + ['支出']: TransactionType.Expense, + }, + // Other common transfer type + { + // zh-Hans + ['转账']: TransactionType.Transfer, + ['还款']: TransactionType.Transfer, + ['借入']: TransactionType.Transfer, + ['借出']: TransactionType.Transfer, + ['收债']: TransactionType.Transfer, + ['还债']: TransactionType.Transfer, + ['代付']: TransactionType.Transfer, + ['报销']: TransactionType.Transfer, + ['退款']: TransactionType.Transfer, + } +]); diff --git a/src/views/desktop/transactions/import/tabs/ImportTransactionDefineColumnTab.vue b/src/views/desktop/transactions/import/tabs/ImportTransactionDefineColumnTab.vue index 99991832..7711b3d8 100644 --- a/src/views/desktop/transactions/import/tabs/ImportTransactionDefineColumnTab.vue +++ b/src/views/desktop/transactions/import/tabs/ImportTransactionDefineColumnTab.vue @@ -224,7 +224,7 @@ import SnackBar from '@/components/desktop/SnackBar.vue'; import PaginationButtons from '@/components/desktop/PaginationButtons.vue'; -import { ref, computed, useTemplateRef } from 'vue'; +import { ref, computed, useTemplateRef, watch } from 'vue'; import { useI18n } from '@/locales/helpers.ts'; @@ -235,6 +235,7 @@ import { KnownDateTimezoneFormat } from '@/core/timezone.ts'; import { TransactionType } from '@/core/transaction.ts'; import { ImportTransactionColumnType, ImportTransactionDataMapping } from '@/core/import_transaction.ts'; import { KnownFileType } from '@/core/file.ts'; +import { KNOWN_COLUMN_NAME_MAPPING, KNOWN_TRANSACTION_TYPE_NAME_MAPPING } from '@/consts/import_transaction.ts'; import { isNumber, @@ -477,6 +478,10 @@ function getTablePageOptions(linesCount?: number): NameNumeralValue[] { return pageOptions; } +function getNormalizedKey(key: string): string { + return key.toLowerCase().replaceAll(' ', '').replaceAll('_', '').replaceAll('-', ''); +} + function getParseDataMappedColumnDisplayName(columnIndex: number): string { for (const [columnType, index] of entries(parsedFileDataColumnMapping.value.dataColumnMapping)) { if (index === columnIndex) { @@ -487,6 +492,72 @@ function getParseDataMappedColumnDisplayName(columnIndex: number): string { return tt('Unspecified'); } +function autoSetColumnMapping(): void { + if (!props.parsedFileData) { + return; + } + + const firstLine: string[] = props.parsedFileData.length > 0 ? (props.parsedFileData[0] as string[]) : []; + const displayColumnNamesMap: Record = {}; + + for (const column of ImportTransactionColumnType.values()) { + displayColumnNamesMap[getNormalizedKey(tt(column.name))] = column; + } + + for (const [columnName, index] of itemAndIndex(firstLine)) { + const normalizedColumnName = getNormalizedKey(columnName); + + if (displayColumnNamesMap[normalizedColumnName]) { + const columnType = displayColumnNamesMap[normalizedColumnName]; + + if (!isNumber(parsedFileDataColumnMapping.value.dataColumnMapping[columnType.type])) { + parsedFileDataColumnMapping.value.dataColumnMapping[columnType.type] = index; + } + + continue; + } + + if (KNOWN_COLUMN_NAME_MAPPING[normalizedColumnName]) { + const columnType = KNOWN_COLUMN_NAME_MAPPING[normalizedColumnName]; + + if (!isNumber(parsedFileDataColumnMapping.value.dataColumnMapping[columnType.type])) { + parsedFileDataColumnMapping.value.dataColumnMapping[columnType.type] = index; + } + + continue; + } + } +} + +function autoSetTransactionTypeMapping(): void { + if (!props.parsedFileData) { + return; + } + + const displayTransactinTypeNamesMap: Record = { + [getNormalizedKey(tt('Modify Balance'))]: TransactionType.ModifyBalance, + [getNormalizedKey(tt('Income'))]: TransactionType.Income, + [getNormalizedKey(tt('Expense'))]: TransactionType.Expense, + [getNormalizedKey(tt('Transfer'))]: TransactionType.Transfer + }; + + for (const transactionTypeName of parsedFileAllTransactionTypes.value) { + const normalizedTransactionTypeName = getNormalizedKey(transactionTypeName); + + if (displayTransactinTypeNamesMap[normalizedTransactionTypeName]) { + const transactionType = displayTransactinTypeNamesMap[normalizedTransactionTypeName]; + parsedFileDataColumnMapping.value.transactionTypeMapping[transactionTypeName] = transactionType; + continue; + } + + if (KNOWN_TRANSACTION_TYPE_NAME_MAPPING[normalizedTransactionTypeName]) { + const transactionType = KNOWN_TRANSACTION_TYPE_NAME_MAPPING[normalizedTransactionTypeName]; + parsedFileDataColumnMapping.value.transactionTypeMapping[transactionTypeName] = transactionType; + continue; + } + } +} + function generateResult(): ImportTransactionDefineColumnResult | undefined { const columnMapping: Record = parsedFileDataColumnMapping.value.dataColumnMapping; const transactionTypeMapping: Record = parsedFileValidMappedTransactionTypes.value; @@ -588,6 +659,13 @@ function saveColumnMappingFile(): void { startDownloadFile(fileName, KnownFileType.JSON.createBlob(parsedFileDataColumnMapping.value.toJson())); } +watch(() => props.parsedFileData, (newValue, oldValue) => { + if (newValue && !oldValue && getObjectOwnFieldCount(parsedFileDataColumnMapping.value.dataColumnMapping) < 1) { + autoSetColumnMapping(); + autoSetTransactionTypeMapping(); + } +}, { immediate: true }); + defineExpose({ menus, generateResult,