优化账户余额调整功能:新增调整余额的逻辑,更新相关页面以显示账户余额和可用额度,调整路由配置以移除不必要的动画效果。
This commit is contained in:
@@ -128,9 +128,6 @@ const routes: Router.RouteParameters[] = [
|
||||
path: '/',
|
||||
async: asyncResolve(HomePage),
|
||||
beforeEnter: [checkLogin],
|
||||
options: {
|
||||
animate: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
@@ -160,7 +157,6 @@ const routes: Router.RouteParameters[] = [
|
||||
path: '/transaction/list',
|
||||
async: asyncResolve(TransactionListPage),
|
||||
beforeEnter: [checkLogin],
|
||||
options: { animate: false }
|
||||
},
|
||||
{
|
||||
path: '/transaction/filter/amount',
|
||||
@@ -186,7 +182,6 @@ const routes: Router.RouteParameters[] = [
|
||||
path: '/account/list',
|
||||
async: asyncResolve(AccountListPage),
|
||||
beforeEnter: [checkLogin],
|
||||
options: { animate: false }
|
||||
},
|
||||
{
|
||||
path: '/account/add',
|
||||
@@ -212,7 +207,6 @@ const routes: Router.RouteParameters[] = [
|
||||
path: '/statistic/transaction',
|
||||
async: asyncResolve(StatisticsTransactionPage),
|
||||
beforeEnter: [checkLogin],
|
||||
options: { animate: false }
|
||||
},
|
||||
{
|
||||
path: '/statistic/settings',
|
||||
@@ -263,7 +257,6 @@ const routes: Router.RouteParameters[] = [
|
||||
path: '/settings',
|
||||
async: asyncResolve(SettingsPage),
|
||||
beforeEnter: [checkLogin],
|
||||
options: { animate: false }
|
||||
},
|
||||
{
|
||||
path: '/app_lock',
|
||||
|
||||
@@ -54,11 +54,12 @@ import {
|
||||
splitItemsToMap,
|
||||
countSplitItems
|
||||
} from '@/lib/common.ts';
|
||||
import { parseDateTimeFromUnixTimeWithTimezoneOffset } from '@/lib/datetime.ts';
|
||||
import { parseDateTimeFromUnixTimeWithTimezoneOffset, getCurrentUnixTime, getTimezoneOffsetMinutes } from '@/lib/datetime.ts';
|
||||
import { getAmountWithDecimalNumberCount } from '@/lib/numeral.ts';
|
||||
import { getCurrencyFraction } from '@/lib/currency.ts';
|
||||
import { getFirstVisibleCategoryId } from '@/lib/category.ts';
|
||||
import services, { type ApiResponsePromise } from '@/lib/services.ts';
|
||||
import { generateRandomUUID } from '@/lib/misc.ts';
|
||||
import logger from '@/lib/logger.ts';
|
||||
|
||||
export interface TransactionListPartialFilter {
|
||||
@@ -1166,6 +1167,59 @@ export const useTransactionsStore = defineStore('transactions', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function adjustAccountBalance({ accountId, targetBalance, currentBalance }: { accountId: string, targetBalance: number, currentBalance: number }): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const now = getCurrentUnixTime();
|
||||
const utcOffset = getTimezoneOffsetMinutes(now);
|
||||
const delta = targetBalance - currentBalance;
|
||||
|
||||
if (delta === 0) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
services.addTransaction({
|
||||
type: TransactionType.ModifyBalance,
|
||||
categoryId: '0',
|
||||
time: now,
|
||||
utcOffset: utcOffset,
|
||||
sourceAccountId: accountId,
|
||||
destinationAccountId: '0',
|
||||
sourceAmount: delta,
|
||||
destinationAmount: 0,
|
||||
hideAmount: false,
|
||||
tagIds: [],
|
||||
pictureIds: [],
|
||||
comment: '',
|
||||
clientSessionId: generateRandomUUID()
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to adjust account balance' });
|
||||
return;
|
||||
}
|
||||
|
||||
const accountsStore = useAccountsStore();
|
||||
accountsStore.loadAllAccounts({ force: true }).then(() => {
|
||||
updateTransactionListInvalidState(true);
|
||||
resolve(true);
|
||||
}).catch(() => {
|
||||
updateTransactionListInvalidState(true);
|
||||
resolve(true);
|
||||
});
|
||||
}).catch(error => {
|
||||
if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
reject({ error: error.response.data });
|
||||
} else if (!error.processed) {
|
||||
reject({ message: 'Unable to adjust account balance' });
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteTransaction({ transaction, defaultCurrency, beforeResolve }: { transaction: TransactionInfoResponse, defaultCurrency: string, beforeResolve?: BeforeResolveFunction }): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
services.deleteTransaction({
|
||||
@@ -1472,6 +1526,7 @@ export const useTransactionsStore = defineStore('transactions', () => {
|
||||
getReconciliationStatements,
|
||||
getTransaction,
|
||||
saveTransaction,
|
||||
adjustAccountBalance,
|
||||
moveAllTransactionsBetweenAccounts,
|
||||
deleteTransaction,
|
||||
recognizeReceiptImage,
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
</v-col>
|
||||
<v-col cols="12" :md="(!editAccountId || isNewAccount(selectedAccount)) && selectedAccount.balance ? 6 : 12"
|
||||
v-if="account.type === AccountType.SingleAccount.type || currentAccountIndex >= 0">
|
||||
<amount-input :disabled="loading || submitting || (!!editAccountId && !isNewAccount(selectedAccount))"
|
||||
<amount-input :disabled="loading || submitting"
|
||||
:persistent-placeholder="true"
|
||||
:currency="selectedAccount.currency"
|
||||
:show-currency="true"
|
||||
@@ -204,6 +204,9 @@ import { useAccountEditPageBase } from '@/views/base/accounts/AccountEditPageBas
|
||||
|
||||
import { useUserStore } from '@/stores/user.ts';
|
||||
import { useAccountsStore } from '@/stores/account.ts';
|
||||
import { useTransactionsStore } from '@/stores/transaction.ts';
|
||||
|
||||
import { KnownErrorCode } from '@/consts/api.ts';
|
||||
|
||||
import { itemAndIndex } from '@/core/base.ts';
|
||||
import { AccountType } from '@/core/account.ts';
|
||||
@@ -254,6 +257,7 @@ const {
|
||||
|
||||
const userStore = useUserStore();
|
||||
const accountsStore = useAccountsStore();
|
||||
const transactionsStore = useTransactionsStore();
|
||||
|
||||
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
@@ -261,6 +265,7 @@ const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
const showState = ref<boolean>(false);
|
||||
const activeTab = ref<string>('account');
|
||||
const currentAccountIndex = ref<number>(-1);
|
||||
const originalBalances = ref<Map<string, number>>(new Map());
|
||||
|
||||
const selectedAccount = computed<Account>(() => {
|
||||
if (currentAccountIndex.value < 0) {
|
||||
@@ -310,6 +315,15 @@ function open(options?: { id?: string, currentAccount?: Account, category?: numb
|
||||
accountId: editAccountId.value
|
||||
}).then(response => {
|
||||
setAccount(response);
|
||||
originalBalances.value.clear();
|
||||
if (account.value.id) {
|
||||
originalBalances.value.set(account.value.id, account.value.balance);
|
||||
}
|
||||
for (const subAccount of subAccounts.value) {
|
||||
if (subAccount.id) {
|
||||
originalBalances.value.set(subAccount.id, subAccount.balance);
|
||||
}
|
||||
}
|
||||
loading.value = false;
|
||||
}).catch(error => {
|
||||
loading.value = false;
|
||||
@@ -337,7 +351,7 @@ function open(options?: { id?: string, currentAccount?: Account, category?: numb
|
||||
});
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
async function save(): Promise<void> {
|
||||
const problemMessage = inputEmptyProblemMessage.value;
|
||||
|
||||
if (problemMessage) {
|
||||
@@ -347,6 +361,30 @@ function save(): void {
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
// Collect balance adjustments needed for existing accounts
|
||||
let balanceChanged = false;
|
||||
if (editAccountId.value) {
|
||||
const allAccounts = [account.value, ...subAccounts.value];
|
||||
for (const acc of allAccounts) {
|
||||
if (!acc.id || isNewAccount(acc)) continue;
|
||||
const origBalance = originalBalances.value.get(acc.id);
|
||||
if (origBalance !== undefined && acc.balance !== origBalance) {
|
||||
balanceChanged = true;
|
||||
try {
|
||||
await transactionsStore.adjustAccountBalance({
|
||||
accountId: acc.id,
|
||||
targetBalance: acc.balance,
|
||||
currentBalance: origBalance
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
submitting.value = false;
|
||||
snackbar.value?.showError(error as { message: string });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
accountsStore.saveAccount({
|
||||
account: account.value,
|
||||
subAccounts: subAccounts.value,
|
||||
@@ -366,6 +404,12 @@ function save(): void {
|
||||
}).catch(error => {
|
||||
submitting.value = false;
|
||||
|
||||
if (balanceChanged && error.error && error.error.errorCode === KnownErrorCode.NothingWillBeUpdated) {
|
||||
resolveFunc?.({ message: 'You have saved this account' });
|
||||
showState.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
|
||||
@@ -211,6 +211,7 @@
|
||||
:disabled="loading || submitting || !allVisibleAccounts.length || (mode === TransactionEditPageMode.Edit && transaction.type === TransactionType.ModifyBalance)"
|
||||
:enable-filter="true" :filter-placeholder="tt('Find account')" :filter-no-items-text="tt('No available account')"
|
||||
:custom-selection-primary-text="sourceAccountName"
|
||||
:custom-selection-secondary-text="sourceAccountBalanceDisplay"
|
||||
:label="tt(sourceAccountTitle)"
|
||||
:placeholder="tt(sourceAccountTitle)"
|
||||
:items="allVisibleCategorizedAccounts"
|
||||
@@ -236,6 +237,7 @@
|
||||
:disabled="loading || submitting || !allVisibleAccounts.length"
|
||||
:enable-filter="true" :filter-placeholder="tt('Find account')" :filter-no-items-text="tt('No available account')"
|
||||
:custom-selection-primary-text="destinationAccountName"
|
||||
:custom-selection-secondary-text="destinationAccountBalanceDisplay"
|
||||
:label="tt('Destination Account')"
|
||||
:placeholder="tt('Destination Account')"
|
||||
:items="allVisibleCategorizedAccounts"
|
||||
@@ -510,6 +512,7 @@ import { SUPPORTED_IMAGE_EXTENSIONS } from '@/consts/file.ts';
|
||||
import { TransactionTemplate } from '@/models/transaction_template.ts';
|
||||
import type { TransactionPictureInfoBasicResponse } from '@/models/transaction_picture_info.ts';
|
||||
import { Transaction } from '@/models/transaction.ts';
|
||||
import type { Account } from '@/models/account.ts';
|
||||
|
||||
import {
|
||||
getTimezoneOffsetMinutes,
|
||||
@@ -567,7 +570,7 @@ const props = defineProps<{
|
||||
show?: boolean;
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
const { tt, formatAmountToLocalizedNumeralsWithCurrency } = useI18n();
|
||||
|
||||
const {
|
||||
mode,
|
||||
@@ -644,6 +647,31 @@ const initOptions = ref<TransactionEditOptions | undefined>(undefined);
|
||||
let resolveFunc: ((response?: TransactionEditResponse) => void) | null = null;
|
||||
let rejectFunc: ((reason?: unknown) => void) | null = null;
|
||||
|
||||
function getAccountBalanceDisplay(account: Account): string {
|
||||
if (account.creditLimit) {
|
||||
const outstanding = -account.balance;
|
||||
const available = account.creditLimit + account.balance;
|
||||
return formatAmountToLocalizedNumeralsWithCurrency(outstanding, account.currency)
|
||||
+ ' · ' + tt('Available') + ' ' + formatAmountToLocalizedNumeralsWithCurrency(available, account.currency);
|
||||
}
|
||||
const displayBalance = account.isLiability ? -account.balance : account.balance;
|
||||
return formatAmountToLocalizedNumeralsWithCurrency(displayBalance, account.currency);
|
||||
}
|
||||
|
||||
const sourceAccountBalanceDisplay = computed<string>(() => {
|
||||
if (!transaction.value.sourceAccountId) return '';
|
||||
const account = allVisibleAccounts.value.find(a => a.id === transaction.value.sourceAccountId);
|
||||
if (!account) return '';
|
||||
return getAccountBalanceDisplay(account);
|
||||
});
|
||||
|
||||
const destinationAccountBalanceDisplay = computed<string>(() => {
|
||||
if (!transaction.value.destinationAccountId) return '';
|
||||
const account = allVisibleAccounts.value.find(a => a.id === transaction.value.destinationAccountId);
|
||||
if (!account) return '';
|
||||
return getAccountBalanceDisplay(account);
|
||||
});
|
||||
|
||||
const sourceAmountColor = computed<string | undefined>(() => {
|
||||
if (transaction.value.type === TransactionType.Expense) {
|
||||
return 'expense';
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
<f7-list-item
|
||||
link="#" no-chevron
|
||||
class="list-item-with-header-and-title"
|
||||
:class="{ 'disabled': editAccountId }"
|
||||
:header="account.isLiability ? tt('Account Outstanding Balance') : tt('Account Balance')"
|
||||
:title="formatAccountDisplayBalance(account)"
|
||||
@click="accountContext.showBalanceSheet = true"
|
||||
@@ -565,11 +564,13 @@ import { useI18nUIComponents, showLoading, hideLoading } from '@/lib/ui/mobile.t
|
||||
import { useAccountEditPageBase } from '@/views/base/accounts/AccountEditPageBase.ts';
|
||||
|
||||
import { useAccountsStore } from '@/stores/account.ts';
|
||||
import { useTransactionsStore } from '@/stores/transaction.ts';
|
||||
|
||||
import { itemAndIndex } from '@/core/base.ts';
|
||||
import type { LocalizedCurrencyInfo } from '@/core/currency.ts';
|
||||
import { AccountType } from '@/core/account.ts';
|
||||
import { ALL_ACCOUNT_ICONS } from '@/consts/icon.ts';
|
||||
import { KnownErrorCode } from '@/consts/api.ts';
|
||||
import { ALL_ACCOUNT_COLORS } from '@/consts/color.ts';
|
||||
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
|
||||
import type { Account } from '@/models/account.ts';
|
||||
@@ -631,6 +632,7 @@ const {
|
||||
} = useAccountEditPageBase();
|
||||
|
||||
const accountsStore = useAccountsStore();
|
||||
const transactionsStore = useTransactionsStore();
|
||||
|
||||
const DEFAULT_ACCOUNT_CONTEXT: AccountContext = {
|
||||
showIconSelectionSheet: false,
|
||||
@@ -643,6 +645,8 @@ const DEFAULT_ACCOUNT_CONTEXT: AccountContext = {
|
||||
balanceDateTimeSheetMode: 'time'
|
||||
};
|
||||
|
||||
const originalBalance = ref<number>(0);
|
||||
|
||||
const accountContext = ref<AccountContext>(Object.assign({}, DEFAULT_ACCOUNT_CONTEXT));
|
||||
const subAccountContexts = ref<AccountContext[]>([]);
|
||||
const subAccountToDelete = ref<Account | null>(null);
|
||||
@@ -697,6 +701,7 @@ function init(): void {
|
||||
accountId: editAccountId.value
|
||||
}).then(response => {
|
||||
setAccount(response);
|
||||
originalBalance.value = response.balance;
|
||||
subAccountContexts.value = [];
|
||||
|
||||
for (let i = 0; i < subAccounts.value.length; i++) {
|
||||
@@ -729,22 +734,43 @@ function save(): void {
|
||||
submitting.value = true;
|
||||
showLoading(() => submitting.value);
|
||||
|
||||
accountsStore.saveAccount({
|
||||
account: account.value,
|
||||
subAccounts: subAccounts.value,
|
||||
isEdit: !!editAccountId.value,
|
||||
clientSessionId: clientSessionId.value
|
||||
}).then(() => {
|
||||
submitting.value = false;
|
||||
hideLoading();
|
||||
const balanceChanged = !!editAccountId.value && account.value.balance !== originalBalance.value;
|
||||
const adjustPromise = balanceChanged
|
||||
? transactionsStore.adjustAccountBalance({ accountId: editAccountId.value!, targetBalance: account.value.balance, currentBalance: originalBalance.value })
|
||||
: Promise.resolve(true);
|
||||
|
||||
if (!editAccountId.value) {
|
||||
showToast('You have added a new account');
|
||||
} else {
|
||||
showToast('You have saved this account');
|
||||
}
|
||||
adjustPromise.then(() => {
|
||||
accountsStore.saveAccount({
|
||||
account: account.value,
|
||||
subAccounts: subAccounts.value,
|
||||
isEdit: !!editAccountId.value,
|
||||
clientSessionId: clientSessionId.value
|
||||
}).then(() => {
|
||||
submitting.value = false;
|
||||
hideLoading();
|
||||
|
||||
router.back();
|
||||
if (!editAccountId.value) {
|
||||
showToast('You have added a new account');
|
||||
} else {
|
||||
showToast('You have saved this account');
|
||||
}
|
||||
|
||||
router.back();
|
||||
}).catch(error => {
|
||||
submitting.value = false;
|
||||
hideLoading();
|
||||
|
||||
if (balanceChanged && error.error && error.error.errorCode === KnownErrorCode.NothingWillBeUpdated) {
|
||||
// Balance was adjusted but other fields unchanged — treat as success
|
||||
showToast('You have saved this account');
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!error.processed) {
|
||||
showToast(error.message || error);
|
||||
}
|
||||
});
|
||||
}).catch(error => {
|
||||
submitting.value = false;
|
||||
hideLoading();
|
||||
|
||||
@@ -195,6 +195,7 @@
|
||||
:class="{ 'disabled': !allVisibleAccounts.length || (mode === TransactionEditPageMode.Edit && transaction.type === TransactionType.ModifyBalance), 'readonly': mode === TransactionEditPageMode.View }"
|
||||
:header="tt(sourceAccountTitle)"
|
||||
:title="sourceAccountName"
|
||||
:footer="sourceAccountBalanceDisplay"
|
||||
@click="showSourceAccountSheet = true"
|
||||
>
|
||||
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
|
||||
@@ -218,6 +219,7 @@
|
||||
:class="{ 'disabled': !allVisibleAccounts.length, 'readonly': mode === TransactionEditPageMode.View }"
|
||||
:header="tt('Destination Account')"
|
||||
:title="destinationAccountName"
|
||||
:footer="destinationAccountBalanceDisplay"
|
||||
v-if="transaction.type === TransactionType.Transfer"
|
||||
@click="showDestinationAccountSheet = true"
|
||||
>
|
||||
@@ -523,6 +525,7 @@ import {
|
||||
import { useSettingsStore } from '@/stores/setting.ts';
|
||||
import { useUserStore } from '@/stores/user.ts';
|
||||
import { useAccountsStore } from '@/stores/account.ts';
|
||||
import type { Account } from '@/models/account.ts';
|
||||
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
import { useTransactionsStore } from '@/stores/transaction.ts';
|
||||
@@ -572,7 +575,8 @@ const {
|
||||
formatDateTimeToLongDate,
|
||||
formatDateTimeToLongTime,
|
||||
formatGregorianTextualYearMonthDayToLongDate,
|
||||
parseAmountFromLocalizedNumerals
|
||||
parseAmountFromLocalizedNumerals,
|
||||
formatAmountToLocalizedNumeralsWithCurrency
|
||||
} = useI18n();
|
||||
const { showAlert, showConfirm, showToast, routeBackOnError } = useI18nUIComponents();
|
||||
|
||||
@@ -679,6 +683,31 @@ const quickSaveButtonFloatingPosition = computed<string>(() => {
|
||||
}
|
||||
});
|
||||
|
||||
function getAccountBalanceDisplay(account: Account): string {
|
||||
if (account.creditLimit) {
|
||||
const outstanding = -account.balance;
|
||||
const available = account.creditLimit + account.balance;
|
||||
return formatAmountToLocalizedNumeralsWithCurrency(outstanding, account.currency)
|
||||
+ ' · ' + tt('Available') + ' ' + formatAmountToLocalizedNumeralsWithCurrency(available, account.currency);
|
||||
}
|
||||
const displayBalance = account.isLiability ? -account.balance : account.balance;
|
||||
return formatAmountToLocalizedNumeralsWithCurrency(displayBalance, account.currency);
|
||||
}
|
||||
|
||||
const sourceAccountBalanceDisplay = computed<string>(() => {
|
||||
if (!transaction.value.sourceAccountId) return '';
|
||||
const account = allVisibleAccounts.value.find(a => a.id === transaction.value.sourceAccountId);
|
||||
if (!account) return '';
|
||||
return getAccountBalanceDisplay(account);
|
||||
});
|
||||
|
||||
const destinationAccountBalanceDisplay = computed<string>(() => {
|
||||
if (!transaction.value.destinationAccountId) return '';
|
||||
const account = allVisibleAccounts.value.find(a => a.id === transaction.value.destinationAccountId);
|
||||
if (!account) return '';
|
||||
return getAccountBalanceDisplay(account);
|
||||
});
|
||||
|
||||
const sourceAmountClass = computed<Record<string, boolean>>(() => {
|
||||
const classes: Record<string, boolean> = {
|
||||
'readonly': mode.value === TransactionEditPageMode.View,
|
||||
|
||||
Reference in New Issue
Block a user