Files
ezbookkeeping/src/views/desktop/accounts/ListPage.vue
T
2025-08-18 00:45:26 +08:00

753 lines
39 KiB
Vue

<template>
<v-row class="match-height">
<v-col cols="12">
<v-card>
<v-layout>
<v-navigation-drawer :permanent="alwaysShowNav" v-model="showNav">
<div class="mx-6 my-4">
<span class="text-subtitle-2">{{ tt('Net assets') }}</span>
<p class="account-statistic-item-value text-income text-truncate mt-1 mb-3">
<span v-if="!loading || allAccountCount > 0">{{ netAssets }}</span>
<span v-else-if="loading && allAccountCount <= 0">
<v-skeleton-loader class="skeleton-no-margin pt-2 pb-1" type="text" :loading="true"></v-skeleton-loader>
</span>
</p>
<span class="text-subtitle-2">{{ tt('Total liabilities') }}</span>
<p class="account-statistic-item-value text-expense text-truncate mt-1 mb-3">
<span v-if="!loading || allAccountCount > 0">{{ totalLiabilities }}</span>
<span v-else-if="loading && allAccountCount <= 0">
<v-skeleton-loader class="skeleton-no-margin pt-2 pb-1" type="text" :loading="true"></v-skeleton-loader>
</span>
</p>
<span class="text-subtitle-2">{{ tt('Total assets') }}</span>
<p class="account-statistic-item-value mt-1">
<span v-if="!loading || allAccountCount > 0">{{ totalAssets }}</span>
<span v-else-if="loading && allAccountCount <= 0">
<v-skeleton-loader class="skeleton-no-margin pt-2 pb-1" type="text" :loading="true"></v-skeleton-loader>
</span>
</p>
</div>
<v-divider />
<v-tabs show-arrows class="account-category-tabs my-4" direction="vertical"
:disabled="loading" v-model="activeAccountCategoryType">
<v-tab class="tab-text-truncate" :key="accountCategory.type" :value="accountCategory.type"
v-for="accountCategory in AccountCategory.values()">
<ItemIcon icon-type="account" :icon-id="accountCategory.defaultAccountIconId" />
<div class="d-flex flex-column text-truncate ms-2">
<small class="text-truncate text-start smaller" v-if="!loading || allAccountCount > 0">{{ accountCategoryTotalBalance(accountCategory) }}</small>
<small class="text-truncate text-start smaller my-1" v-else-if="loading && allAccountCount <= 0">
<v-skeleton-loader class="skeleton-no-margin"
width="100px" height="16" type="text" :loading="true"></v-skeleton-loader>
</small>
<span class="text-truncate text-start">{{ tt(accountCategory.name) }}</span>
</div>
</v-tab>
</v-tabs>
</v-navigation-drawer>
<v-main>
<v-window class="d-flex flex-grow-1 disable-tab-transition w-100-window-container" v-model="activeTab">
<v-window-item value="accountPage">
<v-card variant="flat" min-height="780">
<template #title>
<div class="title-and-toolbar d-flex align-center">
<v-btn class="me-3 d-md-none" density="compact" color="default" variant="plain"
:ripple="false" :icon="true" @click="showNav = !showNav">
<v-icon :icon="mdiMenu" size="24" />
</v-btn>
<span>{{ tt('Account List') }}</span>
<v-btn class="ms-3" color="default" variant="outlined"
:disabled="loading" @click="add">{{ tt('Add') }}</v-btn>
<v-btn class="ms-3" color="primary" variant="tonal"
:disabled="loading" @click="saveSortResult"
v-if="displayOrderModified">{{ tt('Save Display Order') }}</v-btn>
<v-btn density="compact" color="default" variant="text" size="24"
class="ms-2" :icon="true" :loading="loading" @click="reload(true)">
<template #loader>
<v-progress-circular indeterminate size="20"/>
</template>
<v-icon :icon="mdiRefresh" size="24" />
<v-tooltip activator="parent">{{ tt('Refresh') }}</v-tooltip>
</v-btn>
<v-spacer/>
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
:disabled="loading" :icon="true">
<v-icon :icon="mdiDotsVertical" />
<v-menu activator="parent">
<v-list>
<v-list-item :prepend-icon="mdiEyeOutline"
:title="tt('Show Hidden Accounts')"
v-if="!showHidden" @click="showHidden = true"></v-list-item>
<v-list-item :prepend-icon="mdiEyeOffOutline"
:title="tt('Hide Hidden Accounts')"
v-if="showHidden" @click="showHidden = false"></v-list-item>
<v-divider class="my-2" v-if="hasAnyVisibleAccount"/>
<v-list-item :prepend-icon="mdiCalculatorVariantOutline"
:title="tt('Set Accounts Included in Total')"
v-if="hasAnyVisibleAccount" @click="showAccountsIncludedInTotalDialog = true"></v-list-item>
</v-list>
</v-menu>
</v-btn>
</div>
</template>
<v-card-text class="accounts-overview-title text-truncate pt-0">
<span class="accounts-overview-subtitle">{{ activeAccountCategory?.isLiability ? tt('Outstanding Balance') : tt('Balance') }}</span>
<v-skeleton-loader class="skeleton-no-margin ms-3 mb-2" width="120px" type="text" :loading="true" v-if="loading && activeAccountCategory && !hasAccount(activeAccountCategory)"></v-skeleton-loader>
<span class="accounts-overview-amount ms-3" v-else-if="!loading || !activeAccountCategory || hasAccount(activeAccountCategory)">{{ activeAccountCategoryTotalBalance }}</span>
<v-btn class="ms-2" density="compact" color="default" variant="text"
:icon="true" :disabled="loading"
@click="showAccountBalance = !showAccountBalance">
<v-icon :icon="showAccountBalance ? mdiEyeOffOutline : mdiEyeOutline" size="20" />
<v-tooltip activator="parent">{{ showAccountBalance ? tt('Hide Account Balance') : tt('Show Account Balance') }}</v-tooltip>
</v-btn>
</v-card-text>
<v-row class="ps-6 pe-6 pe-md-8" v-if="loading && activeAccountCategory && !hasAccount(activeAccountCategory)">
<v-col cols="12">
<v-card border class="card-title-with-bg account-card mb-8 h-auto">
<template #title>
<div class="account-title d-flex align-center">
<v-icon class="disabled me-0" size="28px" :icon="mdiSquareRounded" />
<span class="account-name text-truncate ms-2">
<v-skeleton-loader class="skeleton-no-margin my-1"
width="120px" type="text" :loading="true"></v-skeleton-loader>
</span>
<v-spacer/>
<span class="align-self-center">
<v-icon class="disabled" :icon="mdiDrag"/>
</span>
</div>
</template>
<v-divider/>
<v-card-text>
<div class="d-flex account-toolbar align-center">
<v-btn class="px-2" density="comfortable" color="default" variant="text"
:disabled="true" :prepend-icon="mdiListBoxOutline">
{{ tt('Transaction List') }}
</v-btn>
<v-spacer/>
<span class="account-balance ms-2">
<v-skeleton-loader class="skeleton-no-margin"
width="100px" type="text" :loading="true"></v-skeleton-loader>
</span>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row class="ps-5 pe-2 pe-md-4" v-if="!loading && activeAccountCategory && !hasAccount(activeAccountCategory)">
<v-col cols="12">
{{ tt('No available account') }}
</v-col>
</v-row>
<v-row class="ps-6 pe-6 pe-md-8">
<v-col cols="12">
<draggable-list
class="list-group"
item-key="id"
handle=".drag-handle"
ghost-class="dragging-item"
:disabled="activeAccountCategoryVisibleAccountCount <= 1"
:list="allCategorizedAccountsMap[activeAccountCategory.type].accounts"
v-if="activeAccountCategory && allCategorizedAccountsMap[activeAccountCategory.type] && allCategorizedAccountsMap[activeAccountCategory.type].accounts && allCategorizedAccountsMap[activeAccountCategory.type].accounts.length"
@change="onMove"
>
<template #item="{ element }">
<div class="list-group-item">
<v-card border class="card-title-with-bg account-card mb-8 h-auto" v-if="showHidden || !element.hidden">
<template #title>
<div class="account-title d-flex align-baseline">
<ItemIcon size="1.5rem" icon-type="account" :icon-id="element.icon"
:color="element.color" :hidden-status="element.hidden" />
<span class="account-name text-truncate ms-2">{{ element.name }}</span>
<small class="account-currency text-truncate ms-2">
{{ accountCurrency(element) }}
</small>
<v-spacer/>
<span class="align-self-center">
<v-icon :class="!loading && activeAccountCategoryVisibleAccountCount > 1 ? 'drag-handle' : 'disabled'"
:icon="mdiDrag"/>
<v-tooltip activator="parent" v-if="!loading && activeAccountCategoryVisibleAccountCount > 1">{{ tt('Drag to Reorder') }}</v-tooltip>
</span>
</div>
<div class="mt-4" v-if="element.type === AccountType.MultiSubAccounts.type">
<v-btn-toggle
class="account-subaccounts"
variant="outlined"
color="primary"
density="compact"
mandatory="force"
divided rounded="xl"
:disabled="loading"
v-model="activeSubAccount[element.id]"
>
<v-btn :value="''">
<span>{{ tt('All') }}</span>
</v-btn>
<v-btn :key="subAccount.id" :value="subAccount.id"
v-for="subAccount in element.subAccounts"
v-show="showHidden || !subAccount.hidden">
<ItemIcon size="1.5rem" icon-type="account" :icon-id="subAccount.icon"
:color="subAccount.color" :hidden-status="subAccount.hidden" />
<span class="ms-2">{{ subAccount.name }}</span>
</v-btn>
</v-btn-toggle>
</div>
</template>
<v-divider/>
<v-card-text v-if="element.getAccountOrSubAccountComment(activeSubAccount[element.id])">
{{ element.getAccountOrSubAccountComment(activeSubAccount[element.id]) }}
</v-card-text>
<v-card-text>
<div class="d-flex account-toolbar align-center">
<v-btn class="px-2" density="comfortable" color="default" variant="text"
:disabled="loading" :prepend-icon="mdiListBoxOutline"
:to="`/transaction/list?accountIds=${element.getAccountOrSubAccountId(activeSubAccount[element.id])}`">
{{ tt('Transaction List') }}
</v-btn>
<v-btn class="px-2 ms-1" density="comfortable" color="default" variant="text"
:disabled="loading" :prepend-icon="mdiInvoiceListOutline"
@click="showReconciliationStatementCustomDateRangeDialog(element.getAccountOrSubAccount(activeSubAccount[element.id]))"
v-if="element.type === AccountType.SingleAccount.type || element.getSubAccount(activeSubAccount[element.id])">
{{ tt('Reconciliation Statement') }}
<v-menu activator="parent" :open-on-hover="true">
<v-list>
<template :key="dateRange.type"
v-for="dateRange in accountReconciliationStatementDateRanges(element.getAccountOrSubAccount(activeSubAccount[element.id]))">
<v-list-item class="text-sm" density="compact"
:value="dateRange.type">
<v-list-item-title class="cursor-pointer"
@click="showReconciliationStatementCustomDateRangeDialog(element.getAccountOrSubAccount(activeSubAccount[element.id]), dateRange.type)">
<div class="d-flex align-center">
<span class="text-sm ms-3">{{ dateRange.displayName }}</span>
</div>
</v-list-item-title>
</v-list-item>
</template>
</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 }"
:disabled="loading"
:prepend-icon="element.isAccountOrSubAccountHidden(activeSubAccount[element.id]) ? mdiEyeOutline : mdiEyeOffOutline"
v-if="!activeSubAccount[element.id] || element.getSubAccount(activeSubAccount[element.id])"
@click="hide(element, element.getAccountOrSubAccount(activeSubAccount[element.id]), !element.isAccountOrSubAccountHidden(activeSubAccount[element.id]))">
{{ element.isAccountOrSubAccountHidden(activeSubAccount[element.id]) ? tt('Show') : tt('Hide') }}
</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="mdiPencilOutline"
v-if="!activeSubAccount[element.id] || element.getSubAccount(activeSubAccount[element.id])"
@click="edit(element)">
{{ tt('Edit') }}
</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="mdiDeleteOutline"
v-if="!activeSubAccount[element.id] || element.getSubAccount(activeSubAccount[element.id])"
@click="remove(element)">
{{ tt('Delete') }}
</v-btn>
<v-spacer/>
<span class="account-balance ms-2">{{ accountBalance(element, activeSubAccount[element.id]) }}</span>
</div>
</v-card-text>
</v-card>
</div>
</template>
</draggable-list>
</v-col>
</v-row>
</v-card>
</v-window-item>
</v-window>
</v-main>
</v-layout>
</v-card>
</v-col>
</v-row>
<v-dialog width="800" v-model="showAccountsIncludedInTotalDialog">
<account-filter-settings-card type="accountListTotalAmount" :dialog-mode="true"
@settings:change="showAccountsIncludedInTotalDialog = false" />
</v-dialog>
<edit-dialog ref="editDialog" />
<reconciliation-statement-dialog ref="reconciliationStatementDialog"
@error="onShowDateRangeError" />
<date-range-selection-dialog :title="tt('Custom Date Range')"
v-model:show="showCustomDateRangeDialog"
@dateRange:change="onCustomDateRangeChanged"
@error="onShowDateRangeError" />
<confirm-dialog ref="confirmDialog"/>
<snack-bar ref="snackbar" />
</template>
<script setup lang="ts">
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 AccountFilterSettingsCard from '@/views/desktop/common/cards/AccountFilterSettingsCard.vue';
import { ref, computed, useTemplateRef, watch } from 'vue';
import { useDisplay } from 'vuetify';
import { useI18n } from '@/locales/helpers.ts';
import { useAccountListPageBaseBase } from '@/views/base/accounts/AccountListPageBase.ts';
import { useAccountsStore } from '@/stores/account.ts';
import { DateRange, DateRangeScene, type LocalizedDateRange, type TimeRangeAndDateType } from '@/core/datetime.ts';
import { AccountType, AccountCategory } from '@/core/account.ts';
import type { Account } from '@/models/account.ts';
import { isNumber } from '@/lib/common.ts';
import { getDateRangeByDateType, getDateRangeByBillingCycleDateType } from '@/lib/datetime.ts';
import {
mdiEyeOutline,
mdiEyeOffOutline,
mdiCalculatorVariantOutline,
mdiRefresh,
mdiSquareRounded,
mdiMenu,
mdiPencilOutline,
mdiDeleteOutline,
mdiListBoxOutline,
mdiInvoiceListOutline,
mdiDrag,
mdiDotsVertical
} from '@mdi/js';
type ConfirmDialogType = InstanceType<typeof ConfirmDialog>;
type SnackBarType = InstanceType<typeof SnackBar>;
type EditDialogType = InstanceType<typeof EditDialog>;
type ReconciliationStatementDialogType = InstanceType<typeof ReconciliationStatementDialog>;
const display = useDisplay();
const { tt, getAllDateRanges, getCurrencyName, joinMultiText } = useI18n();
const {
loading,
showHidden,
displayOrderModified,
showAccountBalance,
firstDayOfWeek,
fiscalYearStart,
allAccounts,
allCategorizedAccountsMap,
allAccountCount,
netAssets,
totalAssets,
totalLiabilities,
accountCategoryTotalBalance,
accountBalance
} = useAccountListPageBaseBase();
const accountsStore = useAccountsStore();
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
const snackbar = useTemplateRef<SnackBarType>('snackbar');
const editDialog = useTemplateRef<EditDialogType>('editDialog');
const reconciliationStatementDialog = useTemplateRef<ReconciliationStatementDialogType>('reconciliationStatementDialog');
const activeAccountCategoryType = ref<number>(AccountCategory.Default.type);
const activeTab = ref<string>('accountPage');
const activeSubAccount = ref<Record<string, string>>({});
const accountToShowReconciliationStatement = ref<Account | null>(null);
const alwaysShowNav = ref<boolean>(display.mdAndUp.value);
const showNav = ref<boolean>(display.mdAndUp.value);
const showAccountsIncludedInTotalDialog = ref<boolean>(false);
const showCustomDateRangeDialog = ref<boolean>(false);
const hasAnyVisibleAccount = computed<boolean>(() => accountsStore.allVisibleAccountsCount > 0);
const activeAccountCategory = computed<AccountCategory | undefined>(() => AccountCategory.valueOf(activeAccountCategoryType.value));
const activeAccountCategoryTotalBalance = computed<string>(() => accountCategoryTotalBalance(activeAccountCategory.value));
const activeAccountCategoryVisibleAccountCount = computed<number>(() => {
if (!activeAccountCategory.value || !allCategorizedAccountsMap.value[activeAccountCategory.value.type] || !allCategorizedAccountsMap.value[activeAccountCategory.value.type].accounts) {
return 0;
}
const accounts = allCategorizedAccountsMap.value[activeAccountCategory.value.type].accounts;
if (showHidden.value) {
return accounts.length;
}
let visibleCount = 0;
for (let i = 0; i < accounts.length; i++) {
if (!accounts[i].hidden) {
visibleCount++;
}
}
return visibleCount;
});
function reload(force: boolean): void {
loading.value = true;
accountsStore.loadAllAccounts({
force: force
}).then(() => {
loading.value = false;
displayOrderModified.value = false;
if (allAccounts.value) {
for (let i = 0; i < allAccounts.value.length; i++) {
const account = allAccounts.value[i];
if (account.type === AccountType.MultiSubAccounts.type && !activeSubAccount.value[account.id]) {
activeSubAccount.value[account.id] = '';
}
}
}
if (force) {
snackbar.value?.showMessage('Account list has been updated');
}
}).catch(error => {
loading.value = false;
if (error && error.isUpToDate) {
displayOrderModified.value = false;
}
if (!error.processed) {
snackbar.value?.showError(error);
}
});
}
function hasAccount(accountCategory: AccountCategory): boolean {
return accountsStore.hasAccount(accountCategory, !showHidden.value);
}
function accountCurrency(account: Account): string | null {
if (account.type === AccountType.SingleAccount.type) {
return getCurrencyName(account.currency);
} else if (account.type === AccountType.MultiSubAccounts.type) {
const subAccountCurrencies = account.getSubAccountCurrencies(showHidden.value, activeSubAccount.value[account.id])
.map(currencyCode => getCurrencyName(currencyCode));
return joinMultiText(subAccountCurrencies);
} else {
return null;
}
}
function accountReconciliationStatementDateRanges(account: Account): LocalizedDateRange[] {
return getAllDateRanges(DateRangeScene.Normal, true, !!accountsStore.getAccountStatementDate(account.id));
}
function add(): void {
editDialog.value?.open({
category: activeAccountCategoryType.value
}).then(result => {
if (result && result.message) {
snackbar.value?.showMessage(result.message);
}
}).catch(error => {
if (error) {
snackbar.value?.showError(error);
}
});
}
function edit(account: Account): void {
editDialog.value?.open({
id: account.id,
currentAccount: account
}).then(result => {
if (result && result.message) {
snackbar.value?.showMessage(result.message);
}
if (accountsStore.accountListStateInvalid && !loading.value) {
reload(false);
}
}).catch(error => {
if (error) {
snackbar.value?.showError(error);
}
});
}
function showReconciliationStatementCustomDateRangeDialog(account: Account, dateRangeType?: number): void {
if (!isNumber(dateRangeType) || dateRangeType === DateRange.Custom.type) {
accountToShowReconciliationStatement.value = account;
showCustomDateRangeDialog.value = true;
return;
}
let dateRange: TimeRangeAndDateType | null = null;
if (DateRange.isBillingCycle(dateRangeType)) {
dateRange = getDateRangeByBillingCycleDateType(dateRangeType, firstDayOfWeek.value, fiscalYearStart.value, accountsStore.getAccountStatementDate(account.id));
} else {
dateRange = getDateRangeByDateType(dateRangeType, firstDayOfWeek.value, fiscalYearStart.value);
}
if (!dateRange) {
return;
}
reconciliationStatementDialog.value?.open({
accountId: account.id,
startTime: dateRange.minTime,
endTime: dateRange.maxTime
});
}
function hide(account: Account, targetAccount: Account, hidden: boolean): void {
loading.value = true;
accountsStore.hideAccount({
account: targetAccount,
hidden: hidden
}).then(() => {
if (hidden && !showHidden.value && activeSubAccount.value[account.id]) {
activeSubAccount.value[account.id] = '';
}
loading.value = false;
}).catch(error => {
loading.value = false;
if (!error.processed) {
snackbar.value?.showError(error);
}
});
}
function remove(account: Account): void {
if (activeSubAccount.value[account.id]) {
const subAccount: Account | null = account.getSubAccount(activeSubAccount.value[account.id]);
if (!subAccount) {
snackbar.value?.showMessage('Unable to delete this sub-account');
return;
}
confirmDialog.value?.open('Are you sure you want to delete this sub-account?').then(() => {
loading.value = true;
accountsStore.deleteSubAccount({
subAccount: subAccount
}).then(() => {
loading.value = false;
}).catch(error => {
loading.value = false;
if (!error.processed) {
snackbar.value?.showError(error);
}
});
});
} else {
confirmDialog.value?.open('Are you sure you want to delete this account?').then(() => {
loading.value = true;
accountsStore.deleteAccount({
account: account
}).then(() => {
loading.value = false;
}).catch(error => {
loading.value = false;
if (!error.processed) {
snackbar.value?.showError(error);
}
});
});
}
}
function saveSortResult(): void {
if (!displayOrderModified.value) {
return;
}
loading.value = true;
accountsStore.updateAccountDisplayOrders().then(() => {
loading.value = false;
displayOrderModified.value = false;
}).catch(error => {
loading.value = false;
if (!error.processed) {
snackbar.value?.showError(error);
}
});
}
function onMove(event: { moved: { element: { id: string }, oldIndex: number, newIndex: number } }): void {
if (!event || !event.moved) {
return;
}
const moveEvent = event.moved;
if (!moveEvent.element || !moveEvent.element.id) {
snackbar.value?.showMessage('Unable to move account');
return;
}
accountsStore.changeAccountDisplayOrder({
accountId: moveEvent.element.id,
from: moveEvent.oldIndex,
to: moveEvent.newIndex,
updateListOrder: false,
updateGlobalListOrder: true
}).then(() => {
displayOrderModified.value = true;
}).catch(error => {
snackbar.value?.showError(error);
});
}
function onCustomDateRangeChanged(minUnixTime: number, maxUnixTime: number): void {
if (!accountToShowReconciliationStatement.value) {
snackbar.value?.showMessage('An error occurred');
return;
}
showCustomDateRangeDialog.value = false;
reconciliationStatementDialog.value?.open({
accountId: accountToShowReconciliationStatement.value.id,
startTime: minUnixTime,
endTime: maxUnixTime
});
accountToShowReconciliationStatement.value = null;
}
function onShowDateRangeError(message: string): void {
snackbar.value?.showError(message);
}
watch(() => display.mdAndUp.value, (newValue) => {
alwaysShowNav.value = newValue;
if (!showNav.value) {
showNav.value = newValue;
}
});
reload(false);
</script>
<style>
.account-statistic-item-value {
font-size: 1rem;
}
.account-category-tabs .v-tab.v-tab.v-btn {
height: calc(var(--v-tabs-height) * 1.5);
}
.accounts-overview-title {
line-height: 2rem !important;
min-height: 52px;
display: flex;
align-items: flex-end;
}
.accounts-overview-amount {
font-size: 1.5rem;
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
overflow: hidden;
text-overflow: ellipsis;
}
.accounts-overview-subtitle {
font-size: 1rem;
line-height: 1.75rem;
}
.account-card > .v-card-item {
padding-top: 0.875rem;
padding-bottom: 0.875rem;
}
.account-card .account-title {
font-size: 1rem;
line-height: 1.5rem !important;
}
.account-card .account-title .account-name {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
}
.account-card .account-currency {
font-size: 0.8rem;
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
.account-card .account-subaccounts {
overflow-x: auto;
white-space: nowrap;
}
.account-card .account-subaccounts.v-btn-toggle {
height: auto !important;
padding: 0;
border: none;
}
.account-card .account-subaccounts.v-btn-toggle > .v-btn {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
}
.account-card .account-subaccounts.v-btn-toggle > .v-btn:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: none;
}
.account-card .account-subaccounts.v-btn-toggle > .v-btn:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.account-card .account-subaccounts.v-btn-toggle > .v-btn {
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
}
.account-card .account-subaccounts.v-btn-toggle button.v-btn {
width: auto !important;
}
.account-card .account-toolbar {
overflow-x: auto;
white-space: nowrap;
}
.account-card .account-toolbar .hover-display {
display: none;
}
.account-card .account-toolbar:hover .hover-display {
display: grid;
}
.account-card .account-balance {
font-size: 1.25rem;
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
}
</style>