From 4c13b7ad02c285e7fe9baf785c88e6a21ac83077 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sun, 22 Sep 2024 14:41:35 +0800 Subject: [PATCH] automatically save transaction draft --- src/lib/services.js | 13 +- src/lib/settings.js | 9 + src/lib/transaction.js | 5 +- src/lib/userstate.js | 39 +++++ src/locales/en.json | 3 + src/locales/zh_Hans.json | 3 + src/stores/setting.js | 5 + src/stores/transaction.js | 156 +++++++++++++++++- src/views/desktop/UnlockPage.vue | 5 +- .../app/settings/tabs/AppBasicSettingTab.vue | 28 ++++ .../app/settings/tabs/AppLockSettingTab.vue | 5 +- .../transactions/list/dialogs/EditDialog.vue | 47 +++++- src/views/mobile/ApplicationLockPage.vue | 5 +- src/views/mobile/UnlockPage.vue | 5 +- .../mobile/settings/PageSettingsPage.vue | 24 ++- src/views/mobile/transactions/EditPage.vue | 58 ++++++- 16 files changed, 384 insertions(+), 26 deletions(-) diff --git a/src/lib/services.js b/src/lib/services.js index 9ba8db3d..55951199 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -2,7 +2,10 @@ import axios from 'axios'; import apiConstants from '@/consts/api.js'; import userState from './userstate.js'; -import { isBoolean } from './common.js'; +import { + isDefined, + isBoolean +} from './common.js'; import { getGoogleMapAPIKey, getBaiduMapAK, @@ -395,8 +398,12 @@ export default { return axios.get(`v1/transactions/amounts.json?use_transaction_timezone=${useTransactionTimezone}` + (queryParams.length ? '&query=' + queryParams.join('|') : '')); }, - getTransaction: ({ id }) => { - return axios.get(`v1/transactions/get.json?id=${id}&with_pictures=true&trim_account=true&trim_category=true&trim_tag=true`); + getTransaction: ({ id, withPictures }) => { + if (!isDefined(withPictures)) { + withPictures = true; + } + + return axios.get(`v1/transactions/get.json?id=${id}&with_pictures=${withPictures}&trim_account=true&trim_category=true&trim_tag=true`); }, addTransaction: ({ type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, pictureIds, comment, geoLocation, utcOffset, clientSessionId }) => { return axios.post('v1/transactions/add.json', { diff --git a/src/lib/settings.js b/src/lib/settings.js index e740c673..7962b060 100644 --- a/src/lib/settings.js +++ b/src/lib/settings.js @@ -11,6 +11,7 @@ const defaultSettings = { applicationLock: false, applicationLockWebAuthn: false, autoUpdateExchangeRatesData: true, + autoSaveTransactionDraft: 'disabled', autoGetCurrentGeoLocation: false, showAmountInHomePage: true, timezoneUsedForStatisticsInHomePage: timezoneConstants.defaultTimezoneTypesUsedForStatistics, @@ -157,6 +158,14 @@ export function setAutoUpdateExchangeRatesData(value) { setOption('autoUpdateExchangeRatesData', value); } +export function getAutoSaveTransactionDraft() { + return getOption('autoSaveTransactionDraft'); +} + +export function setAutoSaveTransactionDraft(value) { + setOption('autoSaveTransactionDraft', value); +} + export function isAutoGetCurrentGeoLocation() { return getOption('autoGetCurrentGeoLocation'); } diff --git a/src/lib/transaction.js b/src/lib/transaction.js index 6e9dd924..8ee5f94c 100644 --- a/src/lib/transaction.js +++ b/src/lib/transaction.js @@ -165,10 +165,7 @@ export function setTransactionModelByTransaction(transaction, transaction2, allC transaction.hideAmount = transaction2.hideAmount; transaction.tagIds = transaction2.tagIds || []; - - if (setContextData) { - transaction.pictures = transaction2.pictures || []; - } + transaction.pictures = transaction2.pictures || []; transaction.comment = transaction2.comment; diff --git a/src/lib/userstate.js b/src/lib/userstate.js index 7bfcd1ab..1e5f4469 100644 --- a/src/lib/userstate.js +++ b/src/lib/userstate.js @@ -9,6 +9,7 @@ const appLockSecretBaseStringPrefix = 'EBK_LOCK_SECRET_'; const tokenLocalStorageKey = 'ebk_user_token'; const webauthnConfigLocalStorageKey = 'ebk_user_webauthn_config'; const userInfoLocalStorageKey = 'ebk_user_info'; +const transactionDraftLocalStorageKey = 'ebk_user_draft_transaction'; const tokenSessionStorageKey = 'ebk_user_session_token'; const encryptedTokenSessionStorageKey = 'ebk_user_session_encrypted_token'; @@ -59,6 +60,21 @@ function getUserInfo() { return JSON.parse(data); } +function getUserTransactionDraft() { + let data = localStorage.getItem(transactionDraftLocalStorageKey); + + if (!data) { + return null; + } + + if (isEnableApplicationLock()) { + const appLockState = getUserAppLockState(); + data = getDecryptedToken(data, appLockState); + } + + return JSON.parse(data); +} + function getUserAppLockState() { const data = sessionStorage.getItem(appLockStateSessionStorageKey); return JSON.parse(data); @@ -183,10 +199,29 @@ function updateUserInfo(user) { } } +function updateUserTransactionDraft(transaction) { + if (!isObject(transaction)) { + return; + } + + let data = JSON.stringify(transaction); + + if (isEnableApplicationLock()) { + const appLockState = getUserAppLockState(); + data = getEncryptedToken(data, appLockState); + } + + localStorage.setItem(transactionDraftLocalStorageKey, data); +} + function clearUserInfo() { localStorage.removeItem(userInfoLocalStorageKey); } +function clearUserTransactionDraft() { + localStorage.removeItem(transactionDraftLocalStorageKey); +} + function clearSessionToken() { sessionStorage.removeItem(tokenSessionStorageKey); sessionStorage.removeItem(encryptedTokenSessionStorageKey); @@ -201,12 +236,14 @@ function clearTokenAndUserInfo(clearAppLockState) { sessionStorage.removeItem(tokenSessionStorageKey); sessionStorage.removeItem(encryptedTokenSessionStorageKey); localStorage.removeItem(tokenLocalStorageKey); + clearUserTransactionDraft(); clearUserInfo(); } export default { getToken, getUserInfo, + getUserTransactionDraft, getUserAppLockState, isUserLogined, isUserUnlocked, @@ -219,8 +256,10 @@ export default { decryptToken, isCorrectPinCode, updateToken, + updateUserTransactionDraft, updateUserInfo, clearUserInfo, + clearUserTransactionDraft, clearSessionToken, clearTokenAndUserInfo }; diff --git a/src/locales/en.json b/src/locales/en.json index b7a1cfe1..fff9fa22 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1521,6 +1521,7 @@ "Unable to save transaction": "Unable to save transaction", "You have added a new transaction": "You have added a new transaction", "You have saved this transaction": "You have saved this transaction", + "Do you want to save this transaction draft?": "Do you want to save this transaction draft?", "Add Picture": "Add Picture", "Remove Picture": "Remove Picture", "Are you sure you want to remove this transaction picture?": "Are you sure you want to remove this transaction picture?", @@ -1608,6 +1609,8 @@ "Show Monthly Total Amount": "Show Monthly Total Amount", "Show Transaction Tag": "Show Transaction Tag", "Transaction Edit Page": "Transaction Edit Page", + "Automatically Save Draft": "Automatically Save Draft", + "Show Confirmation Every Time": "Show Confirmation Every Time", "Automatically Add Geolocation": "Automatically Add Geolocation", "Enable Animation": "Enable Animation", "Basic Information": "Basic Information", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 497aa7b6..fe1be164 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1521,6 +1521,7 @@ "Unable to save transaction": "无法保存交易", "You have added a new transaction": "您已经添加新交易", "You have saved this transaction": "您已经保存该交易", + "Do you want to save this transaction draft?": "您是否要保存这个交易草稿?", "Add Picture": "添加图片", "Remove Picture": "删除图片", "Are you sure you want to remove this transaction picture?": "您确定要删除这张交易图片?", @@ -1608,6 +1609,8 @@ "Show Monthly Total Amount": "显示月度总金额", "Show Transaction Tag": "显示交易标签", "Transaction Edit Page": "交易编辑页面", + "Automatically Save Draft": "自动保存草稿", + "Show Confirmation Every Time": "每次提示确认", "Automatically Add Geolocation": "自动添加地理位置", "Enable Animation": "启用动画", "Basic Information": "基本信息", diff --git a/src/stores/setting.js b/src/stores/setting.js index a1e8e560..28236905 100644 --- a/src/stores/setting.js +++ b/src/stores/setting.js @@ -13,6 +13,7 @@ export const useSettingsStore = defineStore('settings', { applicationLock: settings.isEnableApplicationLock(), applicationLockWebAuthn: settings.isEnableApplicationLockWebAuthn(), autoUpdateExchangeRatesData: settings.isAutoUpdateExchangeRatesData(), + autoSaveTransactionDraft: settings.getAutoSaveTransactionDraft(), autoGetCurrentGeoLocation: settings.isAutoGetCurrentGeoLocation(), showAmountInHomePage: settings.isShowAmountInHomePage(), timezoneUsedForStatisticsInHomePage: settings.getTimezoneUsedForStatisticsInHomePage(), @@ -63,6 +64,10 @@ export const useSettingsStore = defineStore('settings', { settings.setAutoUpdateExchangeRatesData(value); this.appSettings.autoUpdateExchangeRatesData = value; }, + setAutoSaveTransactionDraft(value) { + settings.setAutoSaveTransactionDraft(value); + this.appSettings.autoSaveTransactionDraft = value; + }, setAutoGetCurrentGeoLocation(value) { settings.setAutoGetCurrentGeoLocation(value); this.appSettings.autoGetCurrentGeoLocation = value; diff --git a/src/stores/transaction.js b/src/stores/transaction.js index 362b5c83..d49f3438 100644 --- a/src/stores/transaction.js +++ b/src/stores/transaction.js @@ -1,6 +1,7 @@ import { defineStore } from 'pinia'; import { useSettingsStore } from './setting.js'; +import { useUserStore } from './user.js'; import { useAccountsStore } from './account.js'; import { useTransactionCategoriesStore } from './transactionCategory.js'; import { useOverviewStore } from './overview.js'; @@ -8,10 +9,16 @@ import { useStatisticsStore } from './statistics.js'; import { useExchangeRatesStore } from './exchangeRates.js'; import datetimeConstants from '@/consts/datetime.js'; +import categoryConstants from '@/consts/category.js'; import transactionConstants from '@/consts/transaction.js'; +import userState from '@/lib/userstate.js'; import services from '@/lib/services.js'; import logger from '@/lib/logger.js'; -import { isNumber, isString } from '@/lib/common.js'; +import { + isDefined, + isNumber, + isString +} from '@/lib/common.js'; import { getCurrentUnixTime, getTimezoneOffsetMinutes, @@ -25,6 +32,7 @@ import { getDay, getDayOfWeekName } from '@/lib/datetime.js'; +import { getFirstAvailableCategoryId } from '@/lib/category.js'; const emptyTransactionResult = { items: [], @@ -315,8 +323,47 @@ function buildBasicSubmitTransaction(transaction, dummyTime) { return submitTransaction; } +function buildTransactionDraft(transaction) { + if (!transaction) { + return null; + } + + let categoryId = ''; + + if (transaction.type === transactionConstants.allTransactionTypes.Expense) { + categoryId = transaction.expenseCategory; + } else if (transaction.type === transactionConstants.allTransactionTypes.Income) { + categoryId = transaction.incomeCategory; + } else if (transaction.type === transactionConstants.allTransactionTypes.Transfer) { + categoryId = transaction.transferCategory; + } else { + return null; + } + + const transactionDraft = { + type: transaction.type, + categoryId: categoryId, + sourceAccountId: transaction.sourceAccountId, + sourceAmount: transaction.sourceAmount, + destinationAccountId: '0', + destinationAmount: 0, + hideAmount: transaction.hideAmount, + tagIds: transaction.tagIds, + pictures: transaction.pictures, + comment: transaction.comment, + }; + + if (transaction.type === transactionConstants.allTransactionTypes.Transfer) { + transactionDraft.destinationAccountId = transaction.destinationAccountId; + transactionDraft.destinationAmount = transaction.destinationAmount; + } + + return transactionDraft; +} + export const useTransactionsStore = defineStore('transactions', { state: () => ({ + transactionDraft: userState.getUserTransactionDraft(), transactionsFilter: { dateType: datetimeConstants.allDateRanges.All.type, maxTime: 0, @@ -447,6 +494,104 @@ export const useTransactionsStore = defineStore('transactions', { } }, actions: { + initTransactionDraft() { + const settingsStore = useSettingsStore(); + + if (settingsStore.appSettings.autoSaveTransactionDraft === 'enabled' || settingsStore.appSettings.autoSaveTransactionDraft === 'confirmation') { + this.transactionDraft = userState.getUserTransactionDraft(); + } else { + this.transactionDraft = null; + } + }, + isTransactionDraftModified(transaction) { + if (!transaction) { + return false; + } + + const userStore = useUserStore(); + const transactionCategoriesStore = useTransactionCategoriesStore(); + + if (transaction.sourceAmount !== 0) { + return true; + } + + if (transaction.type === transactionConstants.allTransactionTypes.Transfer && transaction.destinationAmount !== 0) { + return true; + } + + if (transaction.sourceAccountId && transaction.sourceAccountId !== '0' && transaction.sourceAccountId !== userStore.currentUserDefaultAccountId) { + return true; + } + + if (transaction.type === transactionConstants.allTransactionTypes.Transfer && transaction.destinationAccountId && transaction.destinationAccountId !== '0' && transaction.destinationAccountId !== userStore.currentUserDefaultAccountId) { + return true; + } + + const allCategories = transactionCategoriesStore.allTransactionCategories; + + if (allCategories) { + if (transaction.type === transactionConstants.allTransactionTypes.Expense) { + const defaultCategoryId = getFirstAvailableCategoryId(allCategories[categoryConstants.allCategoryTypes.Expense]); + + if (transaction.expenseCategory && transaction.expenseCategory !== '0' && transaction.expenseCategory !== defaultCategoryId) { + return true; + } + } else if (transaction.type === transactionConstants.allTransactionTypes.Income) { + const defaultCategoryId = getFirstAvailableCategoryId(allCategories[categoryConstants.allCategoryTypes.Income]); + + if (transaction.incomeCategory && transaction.incomeCategory !== '0' && transaction.incomeCategory !== defaultCategoryId) { + return true; + } + } else if (transaction.type === transactionConstants.allTransactionTypes.Transfer) { + const defaultCategoryId = getFirstAvailableCategoryId(allCategories[categoryConstants.allCategoryTypes.Transfer]); + + if (transaction.transferCategory && transaction.transferCategory !== '0' && transaction.transferCategory !== defaultCategoryId) { + return true; + } + } + } + + if (transaction.hideAmount) { + return true; + } + + if (transaction.tagIds && transaction.tagIds.length > 0) { + return true; + } + + if (transaction.pictures && transaction.pictures.length > 0) { + return true; + } + + if (transaction.comment && transaction.comment.trim()) { + return true; + } + + return false; + }, + saveTransactionDraft(transaction) { + const settingsStore = useSettingsStore(); + + if (settingsStore.appSettings.autoSaveTransactionDraft !== 'enabled' && settingsStore.appSettings.autoSaveTransactionDraft !== 'confirmation') { + this.clearTransactionDraft(); + return; + } + + if (transaction) { + if (!this.isTransactionDraftModified(transaction)) { + this.clearTransactionDraft(); + return; + } + + this.transactionDraft = buildTransactionDraft(transaction); + } + + userState.updateUserTransactionDraft(this.transactionDraft); + }, + clearTransactionDraft() { + this.transactionDraft = null; + userState.clearUserTransactionDraft(); + }, generateNewTransactionModel(type) { const settingsStore = useSettingsStore(); const now = getCurrentUnixTime(); @@ -827,10 +972,15 @@ export const useTransactionsStore = defineStore('transactions', { }); }); }, - getTransaction({ transactionId }) { + getTransaction({ transactionId, withPictures }) { return new Promise((resolve, reject) => { + if (!isDefined(withPictures)) { + withPictures = true; + } + services.getTransaction({ - id: transactionId + id: transactionId, + withPictures: withPictures }).then(response => { const data = response.data; diff --git a/src/views/desktop/UnlockPage.vue b/src/views/desktop/UnlockPage.vue index 61a6152f..6a627954 100644 --- a/src/views/desktop/UnlockPage.vue +++ b/src/views/desktop/UnlockPage.vue @@ -114,6 +114,7 @@ import { useRootStore } from '@/stores/index.js'; import { useSettingsStore } from '@/stores/setting.js'; import { useUserStore } from '@/stores/user.js'; import { useTokensStore } from '@/stores/token.js'; +import { useTransactionsStore } from '@/stores/transaction.js'; import { useExchangeRatesStore } from '@/stores/exchangeRates.js'; import assetConstants from '@/consts/asset.js'; @@ -129,7 +130,7 @@ export default { }; }, computed: { - ...mapStores(useRootStore, useSettingsStore, useUserStore, useTokensStore, useExchangeRatesStore), + ...mapStores(useRootStore, useSettingsStore, useUserStore, useTokensStore, useTransactionsStore, useExchangeRatesStore), ezBookkeepingLogoPath() { return assetConstants.ezBookkeepingLogoPath; }, @@ -181,6 +182,7 @@ export default { self.verifyingByWebAuthn = false; self.$user.unlockTokenByWebAuthn(id, userName, userSecret); + self.transactionsStore.initTransactionDraft(); self.tokensStore.refreshTokenAndRevokeOldToken().then(response => { if (response.user) { const localeDefaultSettings = self.$locale.setLanguage(response.user.language); @@ -230,6 +232,7 @@ export default { try { self.$user.unlockTokenByPinCode(user.username, pinCode); + self.transactionsStore.initTransactionDraft(); self.tokensStore.refreshTokenAndRevokeOldToken().then(response => { if (response.user) { const localeDefaultSettings = self.$locale.setLanguage(response.user.language); diff --git a/src/views/desktop/app/settings/tabs/AppBasicSettingTab.vue b/src/views/desktop/app/settings/tabs/AppBasicSettingTab.vue index 090d9b41..2728f801 100644 --- a/src/views/desktop/app/settings/tabs/AppBasicSettingTab.vue +++ b/src/views/desktop/app/settings/tabs/AppBasicSettingTab.vue @@ -145,6 +145,22 @@ + + + + { self.submitting = false; @@ -1004,6 +1015,7 @@ export default { } this.editId = null; + this.duplicateFromId = this.transaction.id; this.transaction.id = null; this.transaction.time = getCurrentUnixTime(); this.transaction.timeZone = this.settingsStore.appSettings.timeZone; @@ -1049,11 +1061,40 @@ export default { }); }, cancel() { - if (this.reject) { - this.reject(); + const self = this; + + const doClose = function () { + if (self.reject) { + self.reject(); + } + + self.showState = false; + }; + + if (self.type !== 'transaction' || self.mode !== 'add' || self.addByTemplateId || self.duplicateFromId) { + doClose(); + return; } - this.showState = false; + if (self.settingsStore.appSettings.autoSaveTransactionDraft === 'confirmation') { + if (self.transactionsStore.isTransactionDraftModified(self.transaction)) { + self.$refs.confirmDialog.open('Do you want to save this transaction draft?').then(() => { + self.transactionsStore.saveTransactionDraft(self.transaction); + doClose(); + }).catch(() => { + self.transactionsStore.clearTransactionDraft(); + doClose(); + }); + } else { + self.transactionsStore.clearTransactionDraft(); + doClose(); + } + } else if (self.settingsStore.appSettings.autoSaveTransactionDraft === 'enabled') { + self.transactionsStore.saveTransactionDraft(self.transaction); + doClose(); + } else { + doClose(); + } }, showDateTimeError(error) { this.$refs.snackbar.showError(error); diff --git a/src/views/mobile/ApplicationLockPage.vue b/src/views/mobile/ApplicationLockPage.vue index e2126067..46cd9b1e 100644 --- a/src/views/mobile/ApplicationLockPage.vue +++ b/src/views/mobile/ApplicationLockPage.vue @@ -39,6 +39,7 @@ import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/setting.js'; import { useUserStore } from '@/stores/user.js'; +import { useTransactionsStore } from '@/stores/transaction.js'; import logger from '@/lib/logger.js'; import webauthn from '@/lib/webauthn.js'; @@ -54,7 +55,7 @@ export default { }; }, computed: { - ...mapStores(useSettingsStore, useUserStore), + ...mapStores(useSettingsStore, useUserStore, useTransactionsStore), isEnableApplicationLock: { get: function () { return this.settingsStore.appSettings.applicationLock; @@ -145,6 +146,7 @@ export default { this.$user.encryptToken(user.username, pinCode); this.settingsStore.setEnableApplicationLock(true); + this.transactionsStore.saveTransactionDraft(); this.settingsStore.setEnableApplicationLockWebAuthn(false); this.$user.clearWebAuthnConfig(); @@ -169,6 +171,7 @@ export default { this.$user.decryptToken(); this.settingsStore.setEnableApplicationLock(false); + this.transactionsStore.saveTransactionDraft(); this.settingsStore.setEnableApplicationLockWebAuthn(false); this.$user.clearWebAuthnConfig(); diff --git a/src/views/mobile/UnlockPage.vue b/src/views/mobile/UnlockPage.vue index 42cbd30d..820cb947 100644 --- a/src/views/mobile/UnlockPage.vue +++ b/src/views/mobile/UnlockPage.vue @@ -70,6 +70,7 @@ import { useRootStore } from '@/stores/index.js'; import { useSettingsStore } from '@/stores/setting.js'; import { useUserStore } from '@/stores/user.js'; import { useTokensStore } from '@/stores/token.js'; +import { useTransactionsStore } from '@/stores/transaction.js'; import { useExchangeRatesStore } from '@/stores/exchangeRates.js'; import assetConstants from '@/consts/asset.js'; @@ -88,7 +89,7 @@ export default { } }, computed: { - ...mapStores(useRootStore, useSettingsStore, useUserStore, useTokensStore, useExchangeRatesStore), + ...mapStores(useRootStore, useSettingsStore, useUserStore, useTokensStore, useTransactionsStore, useExchangeRatesStore), ezBookkeepingLogoPath() { return assetConstants.ezBookkeepingLogoPath; }, @@ -134,6 +135,7 @@ export default { self.$hideLoading(); self.$user.unlockTokenByWebAuthn(id, userName, userSecret); + self.transactionsStore.initTransactionDraft(); self.tokensStore.refreshTokenAndRevokeOldToken().then(response => { if (response.user) { const localeDefaultSettings = self.$locale.setLanguage(response.user.language); @@ -188,6 +190,7 @@ export default { try { self.$user.unlockTokenByPinCode(user.username, pinCode); + self.transactionsStore.initTransactionDraft(); self.tokensStore.refreshTokenAndRevokeOldToken().then(response => { if (response.user) { const localeDefaultSettings = self.$locale.setLanguage(response.user.language); diff --git a/src/views/mobile/settings/PageSettingsPage.vue b/src/views/mobile/settings/PageSettingsPage.vue index d27320f1..d5c97002 100644 --- a/src/views/mobile/settings/PageSettingsPage.vue +++ b/src/views/mobile/settings/PageSettingsPage.vue @@ -34,6 +34,15 @@ {{ $t('Transaction Edit Page') }} + + + {{ $t('Automatically Add Geolocation') }} @@ -45,11 +54,12 @@