move all transactions from one account to another account (#288)
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { useSettingsStore } from '@/stores/setting.ts';
|
||||
import { useAccountsStore } from '@/stores/account.ts';
|
||||
|
||||
import { Account, type CategorizedAccountWithDisplayBalance } from '@/models/account.ts';
|
||||
|
||||
export function useMoveAllTransactionsPageBase() {
|
||||
const { tt, getCategorizedAccountsWithDisplayBalance } = useI18n();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const accountsStore = useAccountsStore();
|
||||
|
||||
const moving = ref<boolean>(false);
|
||||
const fromAccount = ref<Account | undefined>(undefined);
|
||||
const toAccountId = ref<string>('');
|
||||
const toAccountName = ref<string>('');
|
||||
|
||||
const showAccountBalance = computed<boolean>(() => settingsStore.appSettings.showAccountBalance);
|
||||
const allAccounts = computed<Account[]>(() => accountsStore.allPlainAccounts);
|
||||
const allVisibleAccounts = computed<Account[]>(() => accountsStore.allVisiblePlainAccounts);
|
||||
const allVisibleCategorizedAccounts = computed<CategorizedAccountWithDisplayBalance[]>(() => getCategorizedAccountsWithDisplayBalance(allVisibleAccounts.value, showAccountBalance.value));
|
||||
|
||||
const displayToAccountName = computed<string>(() => {
|
||||
if (!toAccountId.value) {
|
||||
return tt('Target Account');
|
||||
}
|
||||
|
||||
return Account.findAccountNameById(allAccounts.value, toAccountId.value, tt('Target Account')) || tt('Target Account');
|
||||
});
|
||||
|
||||
const isToAccountNameValid = computed<boolean>(() => {
|
||||
if (!toAccountId.value || !toAccountName.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expectedAccountName = Account.findAccountNameById(allAccounts.value, toAccountId.value);
|
||||
|
||||
if (!expectedAccountName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return expectedAccountName === toAccountName.value;
|
||||
});
|
||||
|
||||
return {
|
||||
// states
|
||||
moving,
|
||||
fromAccount,
|
||||
toAccountId,
|
||||
toAccountName,
|
||||
// computed states
|
||||
allAccounts,
|
||||
allVisibleAccounts,
|
||||
allVisibleCategorizedAccounts,
|
||||
displayToAccountName,
|
||||
isToAccountNameValid
|
||||
};
|
||||
}
|
||||
@@ -250,10 +250,21 @@
|
||||
</v-btn>
|
||||
<v-btn class="px-2 ms-1" density="comfortable" color="default" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:disabled="loading" :prepend-icon="mdiEraser"
|
||||
v-if="element.type === AccountType.SingleAccount.type || element.getSubAccount(activeSubAccount[element.id])"
|
||||
@click="clearAllTransactions(element)">
|
||||
{{ tt('Clear All Transactions') }}
|
||||
:disabled="loading" :prepend-icon="mdiDotsHorizontalCircleOutline"
|
||||
v-if="element.type === AccountType.SingleAccount.type || element.getSubAccount(activeSubAccount[element.id])">
|
||||
{{ tt('More') }}
|
||||
<v-menu activator="parent" :open-on-hover="true">
|
||||
<v-list>
|
||||
<v-list-item class="text-sm" density="compact"
|
||||
:title="tt('Move All Transactions')"
|
||||
:prepend-icon="mdiSwapHorizontal"
|
||||
@click="moveAllTransactions(element)"></v-list-item>
|
||||
<v-list-item class="text-sm" density="compact"
|
||||
:title="tt('Clear All Transactions')"
|
||||
:prepend-icon="mdiEraser"
|
||||
@click="clearAllTransactions(element)"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
<v-btn class="px-2 ms-1" density="comfortable" color="default" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
@@ -289,6 +300,7 @@
|
||||
<edit-dialog ref="editDialog" />
|
||||
<reconciliation-statement-dialog ref="reconciliationStatementDialog"
|
||||
@error="onShowDateRangeError" />
|
||||
<move-all-transactions-dialog ref="moveAllTransactionsDialog" />
|
||||
<clear-all-transactions-dialog ref="clearAllTransactionsDialog" />
|
||||
|
||||
<date-range-selection-dialog :title="tt('Custom Date Range')"
|
||||
@@ -305,6 +317,7 @@ import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
import EditDialog from './list/dialogs/EditDialog.vue';
|
||||
import ReconciliationStatementDialog from './list/dialogs/ReconciliationStatementDialog.vue';
|
||||
import MoveAllTransactionsDialog from '@/views/desktop/accounts/list/dialogs/MoveAllTransactionsDialog.vue';
|
||||
import ClearAllTransactionsDialog from '@/views/desktop/accounts/list/dialogs/ClearAllTransactionsDialog.vue';
|
||||
import AccountFilterSettingsCard from '@/views/desktop/common/cards/AccountFilterSettingsCard.vue';
|
||||
|
||||
@@ -331,6 +344,8 @@ import {
|
||||
mdiSquareRounded,
|
||||
mdiMenu,
|
||||
mdiPencilOutline,
|
||||
mdiDotsHorizontalCircleOutline,
|
||||
mdiSwapHorizontal,
|
||||
mdiEraser,
|
||||
mdiDeleteOutline,
|
||||
mdiListBoxOutline,
|
||||
@@ -343,6 +358,7 @@ type ConfirmDialogType = InstanceType<typeof ConfirmDialog>;
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
type EditDialogType = InstanceType<typeof EditDialog>;
|
||||
type ReconciliationStatementDialogType = InstanceType<typeof ReconciliationStatementDialog>;
|
||||
type MoveAllTransactionsDialogType = InstanceType<typeof MoveAllTransactionsDialog>;
|
||||
type ClearAllTransactionsDialogType = InstanceType<typeof ClearAllTransactionsDialog>;
|
||||
|
||||
const display = useDisplay();
|
||||
@@ -372,6 +388,7 @@ const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
const editDialog = useTemplateRef<EditDialogType>('editDialog');
|
||||
const reconciliationStatementDialog = useTemplateRef<ReconciliationStatementDialogType>('reconciliationStatementDialog');
|
||||
const moveAllTransactionsDialog = useTemplateRef<MoveAllTransactionsDialogType>('moveAllTransactionsDialog');
|
||||
const clearAllTransactionsDialog = useTemplateRef<ClearAllTransactionsDialogType>('clearAllTransactionsDialog');
|
||||
|
||||
const activeAccountCategoryType = ref<number>(AccountCategory.Default.type);
|
||||
@@ -525,6 +542,16 @@ function showReconciliationStatementCustomDateRangeDialog(account: Account, date
|
||||
});
|
||||
}
|
||||
|
||||
function moveAllTransactions(account: Account): void {
|
||||
moveAllTransactionsDialog.value?.open(account).then(() => {
|
||||
snackbar.value?.showMessage('All transactions in this account has been moved.');
|
||||
|
||||
if (accountsStore.accountListStateInvalid && !loading.value) {
|
||||
reload(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearAllTransactions(account: Account): void {
|
||||
clearAllTransactionsDialog.value?.open(account).then(() => {
|
||||
snackbar.value?.showMessage('All transactions in this account has been cleared');
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<v-dialog width="640" :persistent="true" v-model="showState">
|
||||
<v-card class="pa-2 pa-sm-4 pa-md-4">
|
||||
<template #title>
|
||||
<div class="d-flex align-center justify-center">
|
||||
<h4 class="text-h4 text-wrap">{{ tt('Are you sure you want to move all transactions?') }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<v-card-text>{{ tt('format.misc.moveTransactionsInAccountTip', { fromAccount: fromAccount?.name, toAccount: displayToAccountName }) }}</v-card-text>
|
||||
<v-card-text class="mb-md-4 w-100 d-flex justify-center">
|
||||
<v-row>
|
||||
<v-col cols="12" md="12">
|
||||
<two-column-select primary-key-field="id" primary-value-field="category"
|
||||
primary-title-field="name" primary-footer-field="displayBalance"
|
||||
primary-icon-field="icon" primary-icon-type="account"
|
||||
primary-sub-items-field="accounts"
|
||||
:primary-title-i18n="true"
|
||||
secondary-key-field="id" secondary-value-field="id"
|
||||
secondary-title-field="name" secondary-footer-field="displayBalance"
|
||||
secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color"
|
||||
:disabled="loading || moving || !allVisibleAccounts.length"
|
||||
:enable-filter="true" :filter-placeholder="tt('Find account')" :filter-no-items-text="tt('No available account')"
|
||||
:label="tt('Target Account')"
|
||||
:placeholder="tt('Target Account')"
|
||||
:items="allVisibleCategorizedAccounts"
|
||||
:no-item-text="Account.findAccountNameById(allAccounts, toAccountId, tt('Unspecified'))"
|
||||
v-model="toAccountId">
|
||||
</two-column-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="12">
|
||||
<v-text-field type="text"
|
||||
persistent-placeholder
|
||||
:disabled="moving"
|
||||
:label="tt('Confirm Target Account Name')"
|
||||
:placeholder="tt('Please re-enter the target account name to confirm')"
|
||||
v-model="toAccountName"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text class="overflow-y-visible">
|
||||
<div class="w-100 d-flex justify-center gap-4">
|
||||
<v-btn :disabled="!fromAccount || !toAccountId || fromAccount?.id === toAccountId || !toAccountName || !isToAccountNameValid || moving" @click="confirm">
|
||||
{{ tt('Confirm') }}
|
||||
<v-progress-circular indeterminate size="22" class="ms-2" v-if="moving"></v-progress-circular>
|
||||
</v-btn>
|
||||
<v-btn color="secondary" variant="tonal" :disabled="moving" @click="cancel">
|
||||
{{ tt('Cancel') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<snack-bar ref="snackbar" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
import { useMoveAllTransactionsPageBase } from '@/views/base/accounts/MoveAllTransactionsPageBase.ts'
|
||||
|
||||
import { useAccountsStore } from '@/stores/account.ts';
|
||||
import { useTransactionsStore } from '@/stores/transaction.ts';
|
||||
|
||||
import { Account } from '@/models/account.ts';
|
||||
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const {
|
||||
moving,
|
||||
fromAccount,
|
||||
toAccountId,
|
||||
toAccountName,
|
||||
allAccounts,
|
||||
allVisibleAccounts,
|
||||
allVisibleCategorizedAccounts,
|
||||
displayToAccountName,
|
||||
isToAccountNameValid
|
||||
} = useMoveAllTransactionsPageBase();
|
||||
|
||||
const accountsStore = useAccountsStore();
|
||||
const transactionsStore = useTransactionsStore();
|
||||
|
||||
let resolveFunc: (() => void) | null = null;
|
||||
let rejectFunc: ((reason?: unknown) => void) | null = null;
|
||||
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
|
||||
const showState = ref<boolean>(false);
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
function init(): void {
|
||||
accountsStore.loadAllAccounts({
|
||||
force: false
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
}).catch(error => {
|
||||
loading.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function open(account: Account): Promise<void> {
|
||||
showState.value = true;
|
||||
moving.value = false;
|
||||
fromAccount.value = account;
|
||||
toAccountId.value = '';
|
||||
toAccountName.value = '';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
resolveFunc = resolve;
|
||||
rejectFunc = reject;
|
||||
});
|
||||
}
|
||||
|
||||
function confirm(): void {
|
||||
if (!fromAccount.value || !toAccountId.value || fromAccount.value?.id === toAccountId.value || !toAccountName.value || !isToAccountNameValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
moving.value = true;
|
||||
|
||||
transactionsStore.moveAllTransactionsBetweenAccounts({
|
||||
fromAccountId: fromAccount.value.id,
|
||||
toAccountId: toAccountId.value
|
||||
}).then(() => {
|
||||
moving.value = false;
|
||||
|
||||
resolveFunc?.();
|
||||
showState.value = false;
|
||||
}).catch(error => {
|
||||
moving.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cancel(): void {
|
||||
rejectFunc?.();
|
||||
showState.value = false;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
});
|
||||
|
||||
init();
|
||||
</script>
|
||||
@@ -163,6 +163,7 @@
|
||||
<f7-actions-button @click="showReconciliationStatement(accountForMoreActionSheet)">{{ tt('Reconciliation Statement') }}</f7-actions-button>
|
||||
</f7-actions-group>
|
||||
<f7-actions-group v-if="accountForMoreActionSheet && accountForMoreActionSheet.type === AccountType.SingleAccount.type">
|
||||
<f7-actions-button @click="moveAllTransactions(accountForMoreActionSheet)">{{ tt('Move All Transactions') }}</f7-actions-button>
|
||||
<f7-actions-button color="red" @click="showPasswordSheetForClearAllTransaction(accountForMoreActionSheet)">{{ tt('Clear All Transactions') }}</f7-actions-button>
|
||||
</f7-actions-group>
|
||||
<template v-if="accountForMoreActionSheet && accountForMoreActionSheet.type === AccountType.MultiSubAccounts.type">
|
||||
@@ -171,6 +172,7 @@
|
||||
v-show="showHidden || !subAccount.hidden">
|
||||
<f7-actions-label>{{ subAccount.name }}</f7-actions-label>
|
||||
<f7-actions-button @click="showReconciliationStatement(subAccount)">{{ tt('Reconciliation Statement') }}</f7-actions-button>
|
||||
<f7-actions-button @click="moveAllTransactions(subAccount)">{{ tt('Move All Transactions') }}</f7-actions-button>
|
||||
<f7-actions-button color="red" @click="showPasswordSheetForClearAllTransaction(subAccount)">{{ tt('Clear All Transactions') }}</f7-actions-button>
|
||||
</f7-actions-group>
|
||||
</template>
|
||||
@@ -363,6 +365,17 @@ function showReconciliationStatement(account: Account | null): void {
|
||||
accountForMoreActionSheet.value = null;
|
||||
}
|
||||
|
||||
function moveAllTransactions(account: Account | null): void {
|
||||
if (!account) {
|
||||
showAlert('An error occurred');
|
||||
return;
|
||||
}
|
||||
|
||||
props.f7router.navigate('/account/move_all_transactions?fromAccountId=' + account.id);
|
||||
showAccountMoreActionSheet.value = false;
|
||||
accountForMoreActionSheet.value = null;
|
||||
}
|
||||
|
||||
function showPasswordSheetForClearAllTransaction(account: Account | null): void {
|
||||
if (!account) {
|
||||
showAlert('An error occurred');
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<f7-page @page:afterin="onPageAfterIn">
|
||||
<f7-navbar>
|
||||
<f7-nav-left :back-link="tt('Back')"></f7-nav-left>
|
||||
<f7-nav-title :title="tt('Move All Transactions')"></f7-nav-title>
|
||||
<f7-nav-right>
|
||||
<f7-link :class="{ 'disabled': !fromAccount || !toAccountId || fromAccount?.id === toAccountId || !toAccountName || !isToAccountNameValid || moving }" :text="tt('Confirm')" @click="confirm"></f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
|
||||
<f7-card
|
||||
:title="tt('Are you sure you want to move all transactions?')"
|
||||
:content="tt('format.misc.moveTransactionsInAccountTip', { fromAccount: fromAccount?.name, toAccount: displayToAccountName })">
|
||||
</f7-card>
|
||||
|
||||
<f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
|
||||
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Target Account" title="Unspecified"></f7-list-item>
|
||||
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Confirm Target Account Name" title="Unspecified"></f7-list-item>
|
||||
</f7-list>
|
||||
|
||||
<f7-list form strong inset dividers class="margin-vertical" v-if="!loading">
|
||||
<f7-list-item
|
||||
class="list-item-with-header-and-title"
|
||||
link="#" no-chevron
|
||||
:class="{ 'disabled': moving || !allVisibleAccounts.length }"
|
||||
:header="tt('Target Account')"
|
||||
:title="Account.findAccountNameById(allAccounts, toAccountId, tt('Unspecified'))"
|
||||
@click="showAccountSheet = true"
|
||||
>
|
||||
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
|
||||
primary-title-field="name" primary-footer-field="displayBalance"
|
||||
primary-icon-field="icon" primary-icon-type="account"
|
||||
primary-sub-items-field="accounts"
|
||||
:primary-title-i18n="true"
|
||||
secondary-key-field="id" secondary-value-field="id"
|
||||
secondary-title-field="name" secondary-footer-field="displayBalance"
|
||||
secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color"
|
||||
:enable-filter="true" :filter-placeholder="tt('Find account')" :filter-no-items-text="tt('No available account')"
|
||||
:items="allVisibleCategorizedAccounts"
|
||||
v-model:show="showAccountSheet"
|
||||
v-model="toAccountId">
|
||||
</two-column-list-item-selection-sheet>
|
||||
</f7-list-item>
|
||||
|
||||
<f7-list-input
|
||||
type="text"
|
||||
clear-button
|
||||
:label="tt('Confirm Target Account Name')"
|
||||
:placeholder="tt('Please re-enter the target account name to confirm')"
|
||||
v-model:value="toAccountName"
|
||||
></f7-list-input>
|
||||
</f7-list>
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref} from 'vue';
|
||||
import type { Router } from 'framework7/types';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
import { useI18nUIComponents, showLoading, hideLoading } from '@/lib/ui/mobile.ts';
|
||||
import { useMoveAllTransactionsPageBase } from '@/views/base/accounts/MoveAllTransactionsPageBase.ts'
|
||||
|
||||
import { useAccountsStore } from '@/stores/account.ts';
|
||||
import { useTransactionsStore } from '@/stores/transaction.ts';
|
||||
|
||||
import { Account } from '@/models/account.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
f7route: Router.Route;
|
||||
f7router: Router.Router;
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const { showToast, routeBackOnError } = useI18nUIComponents();
|
||||
|
||||
const {
|
||||
moving,
|
||||
fromAccount,
|
||||
toAccountId,
|
||||
toAccountName,
|
||||
allAccounts,
|
||||
allVisibleAccounts,
|
||||
allVisibleCategorizedAccounts,
|
||||
displayToAccountName,
|
||||
isToAccountNameValid
|
||||
} = useMoveAllTransactionsPageBase();
|
||||
|
||||
const accountsStore = useAccountsStore();
|
||||
const transactionsStore = useTransactionsStore();
|
||||
|
||||
const loading = ref<boolean>(true);
|
||||
const loadingError = ref<unknown | null>(null);
|
||||
const showAccountSheet = ref<boolean>(false);
|
||||
|
||||
function init(): void {
|
||||
const query = props.f7route.query;
|
||||
const fromAccountId = query['fromAccountId'];
|
||||
|
||||
if (!fromAccountId) {
|
||||
showToast('Parameter Invalid');
|
||||
loadingError.value = 'Parameter Invalid';
|
||||
return;
|
||||
}
|
||||
|
||||
accountsStore.loadAllAccounts({
|
||||
force: false
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
|
||||
const account = accountsStore.allAccountsMap[fromAccountId];
|
||||
|
||||
if (!account) {
|
||||
showToast('Parameter Invalid');
|
||||
loadingError.value = 'Parameter Invalid';
|
||||
return;
|
||||
}
|
||||
|
||||
fromAccount.value = account;
|
||||
}).catch(error => {
|
||||
if (error.processed) {
|
||||
loading.value = false;
|
||||
} else {
|
||||
loadingError.value = error;
|
||||
showToast(error.message || error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirm(): void {
|
||||
const router = props.f7router;
|
||||
|
||||
if (!fromAccount.value || !toAccountId.value || fromAccount.value?.id === toAccountId.value || !toAccountName.value || !isToAccountNameValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
moving.value = true;
|
||||
showLoading(() => moving.value);
|
||||
|
||||
transactionsStore.moveAllTransactionsBetweenAccounts({
|
||||
fromAccountId: fromAccount.value.id,
|
||||
toAccountId: toAccountId.value
|
||||
}).then(() => {
|
||||
moving.value = false;
|
||||
hideLoading();
|
||||
|
||||
showToast('All transactions in this account has been moved.');
|
||||
router.back();
|
||||
}).catch(error => {
|
||||
moving.value = false;
|
||||
hideLoading();
|
||||
|
||||
if (!error.processed) {
|
||||
showToast(error.message || error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onPageAfterIn(): void {
|
||||
routeBackOnError(props.f7router, loadingError);
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
Reference in New Issue
Block a user