Files
ezbookkeeping/src/views/mobile/transactions/ListPage.vue
2025-11-30 03:22:53 +08:00

1657 lines
72 KiB
Vue

<template>
<f7-page with-subnavbar
ptr
infinite
:infinite-preloader="loadingMore"
:infinite-distance="600"
@ptr:refresh="reload"
@page:afterin="onPageAfterIn"
@infinite="loadMore(true)">
<f7-navbar>
<f7-nav-left :back-link="tt('Back')"></f7-nav-left>
<f7-nav-title>
<f7-link popover-open=".chart-data-type-popover-menu">
<span style="color: var(--f7-text-color)">{{ displayPageTypeName }}</span>
<f7-icon class="page-title-bar-icon" color="gray" style="opacity: 0.5" f7="chevron_down_circle_fill"></f7-icon>
</f7-link>
</f7-nav-title>
<f7-nav-right class="navbar-compact-icons">
<f7-link icon-f7="plus" :class="{ 'disabled': !canAddTransaction }" @click="add"></f7-link>
</f7-nav-right>
<f7-subnavbar :inner="false">
<f7-searchbar
custom-searchs
:value="query.keyword"
:placeholder="tt('Search transaction description')"
:disable-button-text="tt('Cancel')"
@change="changeKeywordFilter($event.target.value)"
></f7-searchbar>
</f7-subnavbar>
</f7-navbar>
<f7-popover class="chart-data-type-popover-menu"
v-model:opened="showTransactionListPageTypePopover">
<f7-list dividers>
<f7-list-item :title="tt(type.name)"
:class="{ 'list-item-selected': pageType === type.type }"
:key="type.type"
v-for="type in TransactionListPageType.values()"
@click="changePageType(type.type); showTransactionListPageTypePopover = false">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="pageType === type.type"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
<f7-toolbar tabbar bottom class="toolbar-item-auto-size transaction-list-toolbar">
<f7-link :class="{ 'disabled': loading || query.dateType === DateRange.All.type }" @click="shiftDateRange(query.minTime, query.maxTime, -1)">
<f7-icon class="icon-with-direction" f7="arrow_left_square"></f7-icon>
</f7-link>
<f7-link :class="{ 'tabbar-text-with-ellipsis': true, 'disabled': loading }" popover-open=".date-popover-menu">
<span :class="{ 'tabbar-item-changed': query.dateType !== DateRange.All.type }">{{ queryDateRangeName }}</span>
</f7-link>
<f7-link :class="{ 'disabled': loading || query.dateType === DateRange.All.type }" @click="shiftDateRange(query.minTime, query.maxTime, 1)">
<f7-icon class="icon-with-direction" f7="arrow_right_square"></f7-icon>
</f7-link>
<f7-link class="tabbar-text-with-ellipsis" popover-open=".category-popover-menu" :class="{ 'disabled': query.type === 1 }">
<span :class="{ 'tabbar-item-changed': query.categoryIds }">{{ queryCategoryName }}</span>
</f7-link>
<f7-link class="tabbar-text-with-ellipsis" popover-open=".account-popover-menu">
<span :class="{ 'tabbar-item-changed': query.accountIds }">{{ queryAccountName }}</span>
</f7-link>
<f7-link popover-open=".more-popover-menu">
<f7-icon f7="ellipsis_vertical" :class="{ 'tabbar-item-changed': query.type > 0 || query.amountFilter || query.tagFilter }"></f7-icon>
</f7-link>
</f7-toolbar>
<f7-block class="transaction-calendar-container margin-vertical" v-if="pageType === TransactionListPageType.Calendar.type">
<transaction-calendar calendar-class="justify-content-center" week-day-name-type="short"
:readonly="loading" :is-dark-mode="isDarkMode"
:default-currency="false"
:min-date="transactionCalendarMinDate"
:max-date="transactionCalendarMaxDate"
:dailyTotalAmounts="currentMonthTransactionData?.dailyTotalAmounts"
v-model="currentCalendarDate"></transaction-calendar>
</f7-block>
<div class="skeleton-text" v-if="loading">
<f7-block class="combination-list-wrapper margin-vertical" :class="{ 'no-accordion-toggle': pageType !== TransactionListPageType.List.type }"
:key="blockIdx" v-for="blockIdx in (pageType === TransactionListPageType.List.type ? [ 1, 2 ] : [ 1 ])">
<f7-accordion-item>
<f7-block-title v-if="pageType === TransactionListPageType.List.type">
<f7-accordion-toggle>
<f7-list strong inset dividers media-list
class="transaction-amount-list combination-list-header combination-list-opened">
<f7-list-item>
<template #title>
<small>YYYY-MM</small>
<small class="transaction-amount-statistics" v-if="showTotalAmountInTransactionListPage">
<span>0.00 USD</span>
<span>0.00 USD</span>
</small>
<f7-icon class="combination-list-chevron-icon" f7="chevron_up"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-toggle>
</f7-block-title>
<f7-accordion-content style="height: auto">
<f7-list strong inset dividers media-list accordion-list class="transaction-info-list combination-list-content">
<f7-list-item link="#" chevron-center class="transaction-info"
:key="itemIdx" v-for="itemIdx in (pageType === TransactionListPageType.List.type && blockIdx === 1 ? [ 1, 2, 3, 4, 5, 6, 7 ] : [ 1, 2, 3 ])">
<template #media>
<div class="display-flex flex-direction-column transaction-date">
<span class="transaction-day full-line flex-direction-column">DD</span>
<span class="transaction-day-of-week full-line flex-direction-column">Sun</span>
</div>
</template>
<template #inner>
<div class="display-flex no-padding-horizontal">
<div class="item-media">
<div class="transaction-icon display-flex align-items-center">
<f7-icon f7="app_fill"></f7-icon>
</div>
</div>
<div class="actual-item-inner">
<div class="item-title-row">
<div class="item-title">
<div class="transaction-category-name no-padding">
<span>Category</span>
</div>
</div>
<div class="item-after">
<div class="transaction-amount">
<span>0.00 USD</span>
</div>
</div>
</div>
<div class="item-text">
<div class="transaction-description">
<span>Transaction Description</span>
</div>
</div>
<div class="item-footer">
<div class="transaction-footer">
<span>HH:mm</span>
<span>·</span>
<span>Source Account</span>
</div>
</div>
</div>
</div>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-accordion-item>
</f7-block>
</div>
<f7-list strong inset dividers class="margin-vertical" v-if="!loading && noTransaction">
<f7-list-item :title="tt('No transaction data')"></f7-list-item>
</f7-list>
<f7-block class="combination-list-wrapper margin-vertical" :class="{ 'no-accordion-toggle': pageType !== TransactionListPageType.List.type }"
:key="transactionMonthList.yearDashMonth" v-for="(transactionMonthList) in transactions">
<f7-accordion-item :opened="transactionMonthList.opened"
@accordion:open="collapseTransactionMonthList(transactionMonthList, false)"
@accordion:opened="onTransactionMonthListCollapseStateChanged"
@accordion:close="collapseTransactionMonthList(transactionMonthList, true)"
@accordion:closed="onTransactionMonthListCollapseStateChanged">
<f7-block-title :id="getTransactionMonthTitleDomId(transactionMonthList.yearDashMonth)" v-if="pageType === TransactionListPageType.List.type">
<f7-accordion-toggle>
<f7-list strong inset dividers media-list
class="transaction-amount-list combination-list-header"
:class="transactionMonthList.opened ? 'combination-list-opened' : 'combination-list-closed'">
<f7-list-item>
<template #title>
<small>
<span>{{ getDisplayLongYearMonth(transactionMonthList) }}</span>
</small>
<small class="transaction-amount-statistics" v-if="showTotalAmountInTransactionListPage && transactionMonthList.totalAmount">
<span class="text-income">
{{ getDisplayMonthTotalAmount(transactionMonthList.totalAmount.income, defaultCurrency, '+', transactionMonthList.totalAmount.incompleteIncome) }}
</span>
<span class="text-expense">
{{ getDisplayMonthTotalAmount(transactionMonthList.totalAmount.expense, defaultCurrency, '-', transactionMonthList.totalAmount.incompleteExpense) }}
</span>
</small>
<f7-icon class="combination-list-chevron-icon" :f7="transactionMonthList.opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-toggle>
</f7-block-title>
<f7-accordion-content>
<f7-block :style="{ height: getTransactionMonthListHeight(transactionMonthList) }"
v-if="isTransactionMonthListInvisible(transactionMonthList)" />
<f7-list strong inset dividers media-list accordion-list
class="transaction-info-list transaction-month-list combination-list-content"
:id="getTransactionMonthListDomId(transactionMonthList.yearDashMonth)"
v-if="!isTransactionMonthListInvisible(transactionMonthList)"
>
<f7-list-item swipeout chevron-center accordion-item
class="transaction-info"
:id="getTransactionDomId(transaction)"
:link="`/transaction/detail?id=${transaction.id}&type=${transaction.type}`"
:key="transaction.id"
v-for="(transaction, idx) in transactionMonthList.items"
>
<template #media>
<div class="display-flex flex-direction-column transaction-date" :style="getTransactionDateStyle(transaction, idx > 0 ? transactionMonthList.items[idx - 1] : undefined)">
<span class="transaction-day full-line flex-direction-column">
{{ getCalendarDisplayDayOfMonthFromUnixTime(transaction.time) }}
</span>
<span class="transaction-day-of-week full-line flex-direction-column" v-if="transaction.displayDayOfWeek">
{{ getWeekdayShortName(transaction.displayDayOfWeek) }}
</span>
</div>
</template>
<template #inner>
<div class="display-flex no-padding-horizontal">
<div class="item-media">
<div class="transaction-icon display-flex align-items-center">
<ItemIcon icon-type="category"
:icon-id="transaction.category.icon"
:color="transaction.category.color"
v-if="transaction.category && transaction.category.color"></ItemIcon>
<f7-icon v-else-if="!transaction.category || !transaction.category.color"
f7="pencil_ellipsis_rectangle">
</f7-icon>
</div>
</div>
<div class="actual-item-inner">
<div class="item-title-row">
<div class="item-title">
<div class="transaction-category-name no-padding">
<span v-if="transaction.type === TransactionType.ModifyBalance">
{{ tt('Modify Balance') }}
</span>
<span v-else-if="transaction.type !== TransactionType.ModifyBalance && transaction.category">
{{ transaction.category.name }}
</span>
<span v-else-if="transaction.type !== TransactionType.ModifyBalance && !transaction.category">
{{ getTransactionTypeName(transaction.type, 'Transaction') }}
</span>
</div>
</div>
<div class="item-after">
<div class="transaction-amount" v-if="transaction.sourceAccount"
:class="{ 'text-expense': transaction.type === TransactionType.Expense, 'text-income': transaction.type === TransactionType.Income }">
<span>{{ getDisplayAmount(transaction) }}</span>
</div>
</div>
</div>
<div class="item-text">
<div class="transaction-description" v-if="transaction.comment">
<span>{{ transaction.comment }}</span>
</div>
</div>
<div class="item-footer">
<div class="transaction-tags" v-if="showTagInTransactionListPage && transaction.tagIds && transaction.tagIds.length">
<f7-chip media-text-color="var(--f7-chip-text-color)" class="transaction-tag"
:text="allTransactionTags[tagId]?.name"
:key="tagId"
v-for="tagId in transaction.tagIds">
<template #media>
<f7-icon f7="number"></f7-icon>
</template>
</f7-chip>
</div>
<div class="transaction-footer">
<span>{{ getDisplayTime(transaction) }}</span>
<span v-if="transaction.utcOffset !== currentTimezoneOffsetMinutes">{{ `(${getDisplayTimezone(transaction)})` }}</span>
<span v-if="transaction.sourceAccount">·</span>
<span v-if="transaction.sourceAccount">{{ transaction.sourceAccount.name }}</span>
<f7-icon class="transaction-account-arrow icon-with-direction" f7="arrow_right" v-if="transaction.sourceAccount && transaction.type === TransactionType.Transfer && transaction.destinationAccount && transaction.sourceAccount.id !== transaction.destinationAccount.id"></f7-icon>
<span v-if="transaction.sourceAccount && transaction.type === TransactionType.Transfer && transaction.destinationAccount && transaction.sourceAccount.id !== transaction.destinationAccount.id">{{ transaction.destinationAccount.name }}</span>
</div>
</div>
</div>
</div>
</template>
<f7-swipeout-actions :left="textDirection === TextDirection.RTL"
:right="textDirection === TextDirection.LTR">
<f7-swipeout-button color="primary" close
:text="tt('Duplicate')"
v-if="transaction.type !== TransactionType.ModifyBalance"
@click="duplicate(transaction)"></f7-swipeout-button>
<f7-swipeout-button color="orange" close
:text="tt('Edit')"
v-if="transaction.editable"
@click="edit(transaction)"></f7-swipeout-button>
<f7-swipeout-button color="red" class="padding-horizontal"
v-if="transaction.editable"
@click="remove(transaction, false)">
<f7-icon f7="trash"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-accordion-item>
</f7-block>
<f7-block class="text-align-center" :class="{ 'disabled': loadingMore }" v-show="!loading && hasMoreTransaction"
v-if="pageType === TransactionListPageType.List.type">
<f7-link href="#" @click="loadMore(false)">{{ tt('Load More') }}</f7-link>
</f7-block>
<f7-popover class="date-popover-menu"
v-model:opened="showDatePopover"
@popover:open="onPopoverOpen">
<f7-list dividers>
<f7-list-item :title="dateRange.displayName"
:class="{ 'list-item-selected': query.dateType === dateRange.type }"
:key="dateRange.type"
v-for="dateRange in allDateRanges"
v-show="pageType === TransactionListPageType.List.type || dateRange.type === DateRange.ThisMonth.type || dateRange.type === DateRange.LastMonth.type || dateRange.type === DateRange.Custom.type"
@click="changeDateFilter(dateRange.type)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.dateType === dateRange.type"></f7-icon>
</template>
<template #footer>
<div v-if="dateRange.isUserCustomRange && query.dateType === dateRange.type && query.minTime && query.maxTime">
<span>{{ queryMinTime }}</span>
<span>&nbsp;-&nbsp;</span>
<br/>
<span>{{ queryMaxTime }}</span>
</div>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
<date-range-selection-sheet :title="tt('Custom Date Range')"
:min-time="customMinDatetime"
:max-time="customMaxDatetime"
v-model:show="showCustomDateRangeSheet"
@dateRange:change="changeCustomDateFilter">
</date-range-selection-sheet>
<month-selection-sheet :title="tt('Select Month')"
:model-value="queryMonth"
v-model:show="showCustomMonthSheet"
@update:modelValue="changeCustomMonthDateFilter">
</month-selection-sheet>
<f7-popover class="category-popover-menu"
v-model:opened="showCategoryPopover"
@popover:open="onPopoverOpen">
<f7-list dividers accordion-list>
<f7-list-item :class="{ 'list-item-selected': !query.categoryIds }" :title="tt('All')" @click="changeCategoryFilter('')">
<template #media>
<f7-icon f7="rectangle_grid_2x2"></f7-icon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="!query.categoryIds"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': query.categoryIds && queryAllFilterCategoryIdsCount > 1 }"
:title="tt('Multiple Categories')"
@click="filterMultipleCategories()"
v-if="allAvailableCategoriesCount > 0">
<template #media>
<f7-icon f7="rectangle_on_rectangle"></f7-icon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.categoryIds && queryAllFilterCategoryIdsCount > 1"></f7-icon>
</template>
</f7-list-item>
</f7-list>
<f7-list dividers accordion-list
class="no-margin-vertical"
:key="categoryType"
v-for="(categories, categoryType) in allPrimaryCategories"
v-show="categories && categories.length"
>
<f7-list-item divider :title="getTransactionTypeName(categoryTypeToTransactionType(parseInt(categoryType)), 'Type')"></f7-list-item>
<f7-list-item accordion-item
:title="category.name"
:class="getCategoryListItemCheckedClass(category, queryAllFilterCategoryIds)"
:key="category.id"
v-for="category in categories"
v-show="!category.hidden || query.categoryIds === category.id || (allCategories[query.categoryIds] && allCategories[query.categoryIds]?.parentId === category.id)"
>
<template #media>
<ItemIcon icon-type="category" :icon-id="category.icon" :color="category.color"></ItemIcon>
</template>
<f7-accordion-content>
<f7-list dividers class="padding-inline-start">
<f7-list-item :class="{ 'list-item-selected': query.categoryIds === category.id, 'item-in-multiple-selection': queryAllFilterCategoryIdsCount > 1 && queryAllFilterCategoryIds[category.id] }"
:title="tt('All')" @click="changeCategoryFilter(category.id)">
<template #media>
<f7-icon f7="rectangle_grid_2x2"></f7-icon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.categoryIds === category.id"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :title="subCategory.name"
:class="{ 'list-item-selected': query.categoryIds === subCategory.id, 'item-in-multiple-selection': queryAllFilterCategoryIdsCount > 1 && queryAllFilterCategoryIds[subCategory.id] }"
:key="subCategory.id"
v-for="subCategory in category.subCategories"
v-show="!subCategory.hidden || query.categoryIds === subCategory.id"
@click="changeCategoryFilter(subCategory.id)"
>
<template #media>
<ItemIcon icon-type="category" :icon-id="subCategory.icon" :color="subCategory.color"></ItemIcon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon"
f7="checkmark_alt"
v-if="query.categoryIds === subCategory.id">
</f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-list-item>
</f7-list>
</f7-popover>
<f7-popover class="account-popover-menu"
v-model:opened="showAccountPopover"
@popover:open="onPopoverOpen">
<f7-list dividers>
<f7-list-item :class="{ 'list-item-selected': !query.accountIds }" :title="tt('All')" @click="changeAccountFilter('')">
<template #media>
<f7-icon f7="rectangle_grid_2x2"></f7-icon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="!query.accountIds"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': query.accountIds && queryAllFilterAccountIdsCount > 1 }"
:title="tt('Multiple Accounts')"
@click="filterMultipleAccounts()"
v-if="allAvailableAccountsCount > 0">
<template #media>
<f7-icon f7="rectangle_on_rectangle"></f7-icon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.accountIds && queryAllFilterAccountIdsCount > 1"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :title="account.name"
:class="{ 'list-item-selected': query.accountIds === account.id, 'item-in-multiple-selection': queryAllFilterAccountIdsCount > 1 && queryAllFilterAccountIds[account.id] }"
:key="account.id"
v-for="account in allAccounts"
v-show="(!account.hidden && (!allAccountsMap[account.parentId] || !allAccountsMap[account.parentId]!.hidden)) || query.accountIds === account.id"
@click="changeAccountFilter(account.id)"
>
<template #media>
<ItemIcon icon-type="account" :icon-id="account.icon" :color="account.color"></ItemIcon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon"
f7="checkmark_alt"
v-if="query.accountIds === account.id">
</f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
<f7-popover class="more-popover-menu"
v-model:opened="showMorePopover">
<f7-list dividers>
<f7-list-item group-title>
<small>{{ tt('Type') }}</small>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': query.type === 0 }" :title="tt('All')" @click="changeTypeFilter(0)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.type === 0"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': query.type === 1 }" :title="tt('Modify Balance')" @click="changeTypeFilter(1)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.type === 1"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': query.type === 2 }" :title="tt('Income')" @click="changeTypeFilter(2)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.type === 2"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': query.type === 3 }" :title="tt('Expense')" @click="changeTypeFilter(3)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.type === 3"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': query.type === 4 }" :title="tt('Transfer')" @click="changeTypeFilter(4)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.type === 4"></f7-icon>
</template>
</f7-list-item>
<f7-list-item group-title>
<small>{{ tt('Amount') }}</small>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': !query.amountFilter }" :title="tt('All')" @click="changeAmountFilter('')">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="!query.amountFilter"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :key="filterType.type"
:class="{ 'list-item-selected': query.amountFilter && query.amountFilter.startsWith(`${filterType.type}:`) }"
:title="tt(filterType.name)"
v-for="filterType in AmountFilterType.values()"
@click="changeAmountFilter(filterType.type)">
<template #after>
<span class="margin-inline-end-half" v-if="query.amountFilter && query.amountFilter.startsWith(`${filterType.type}:`)">{{ queryAmount }}</span>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.amountFilter && query.amountFilter.startsWith(`${filterType.type}:`)"></f7-icon>
</template>
</f7-list-item>
<f7-list-item group-title>
<small>{{ tt('Tags') }}</small>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': !query.tagFilter }" :title="tt('All')" @click="changeTagFilter('')">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="!query.tagFilter"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': query.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue }" :title="tt('Without Tags')" @click="changeTagFilter(TransactionTagFilter.TransactionNoTagFilterValue)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': query.tagFilter && queryAllFilterTagIdsCount > 1 }"
:title="tt('Multiple Tags')" @click="filterMultipleTags()" v-if="allAvailableTagsCount > 0">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.tagFilter && queryAllFilterTagIdsCount > 1"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :title="transactionTag.name"
:class="{ 'list-item-selected': queryAllFilterTagIdsCount === 1 && isDefined(queryAllFilterTagIds[transactionTag.id]), 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && isDefined(queryAllFilterTagIds[transactionTag.id]) }"
:key="transactionTag.id"
v-for="transactionTag in allTransactionTags"
v-show="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])"
@click="changeTagFilter(TransactionTagFilter.of(transactionTag.id).toTextualTagFilter())"
>
<template #before-title>
<f7-icon class="transaction-tag-name transaction-tag-icon" f7="number"></f7-icon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon"
:f7="queryAllFilterTagIds[transactionTag.id] === true ? 'checkmark_alt' : (queryAllFilterTagIds[transactionTag.id] === false ? 'multiply' : undefined)"
v-if="isDefined(queryAllFilterTagIds[transactionTag.id])">
</f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
<f7-actions close-by-outside-click close-on-escape :opened="showDeleteActionSheet" @actions:closed="showDeleteActionSheet = false">
<f7-actions-group>
<f7-actions-label>{{ tt('Are you sure you want to delete this transaction?') }}</f7-actions-label>
<f7-actions-button color="red" @click="remove(transactionToDelete, true)">{{ tt('Delete') }}</f7-actions-button>
</f7-actions-group>
<f7-actions-group>
<f7-actions-button bold close>{{ tt('Cancel') }}</f7-actions-button>
</f7-actions-group>
</f7-actions>
</f7-page>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue';
import type { Router } from 'framework7/types';
import { useI18n } from '@/locales/helpers.ts';
import {
type Framework7Dom,
useI18nUIComponents,
showLoading,
hideLoading,
onSwipeoutDeleted,
getElementActualHeights,
getElementBoundingRect,
scrollToSelectedItem,
onInfiniteScrolling
} from '@/lib/ui/mobile.ts';
import { TransactionListPageType, useTransactionListPageBase } from '@/views/base/transactions/TransactionListPageBase.ts';
import { useEnvironmentsStore } from '@/stores/environment.ts';
import { useAccountsStore } from '@/stores/account.ts';
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
import { type TransactionMonthList, useTransactionsStore } from '@/stores/transaction.ts';
import { keys } from '@/core/base.ts';
import { TextDirection } from '@/core/text.ts';
import {
type TextualYearMonth,
type Year0BasedMonth,
type TimeRangeAndDateType,
DateRangeScene,
DateRange
} from '@/core/datetime.ts';
import { AmountFilterType } from '@/core/numeral.ts';
import { TransactionType } from '@/core/transaction.ts';
import type { TransactionCategory } from '@/models/transaction_category.ts';
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
import {
isDefined,
isNumber,
objectFieldWithValueToArrayItem
} from '@/lib/common.ts';
import {
getCurrentUnixTime,
parseDateTimeFromUnixTime,
getBrowserTimezoneOffsetMinutes,
getActualUnixTimeForStore,
getDayFirstUnixTimeBySpecifiedUnixTime,
getYearMonthFirstUnixTime,
getYearMonthLastUnixTime,
getShiftedDateRangeAndDateType,
getShiftedDateRangeAndDateTypeForBillingCycle,
getDateTypeByDateRange,
getDateTypeByBillingCycleDateRange,
getDateRangeByDateType,
getDateRangeByBillingCycleDateType,
getFullMonthDateRange,
getValidMonthDayOrCurrentDayShortDate
} from '@/lib/datetime.ts';
import {
categoryTypeToTransactionType,
transactionTypeToCategoryType
} from '@/lib/category.ts';
const props = defineProps<{
f7route: Router.Route;
f7router: Router.Router;
}>();
const {
tt,
getCurrentLanguageTextDirection,
getWeekdayShortName,
getCalendarDisplayDayOfMonthFromUnixTime
} = useI18n();
const { showAlert, showToast, routeBackOnError } = useI18nUIComponents();
const {
pageType,
loading,
customMinDatetime,
customMaxDatetime,
currentCalendarDate,
currentTimezoneOffsetMinutes,
firstDayOfWeek,
fiscalYearStart,
defaultCurrency,
showTotalAmountInTransactionListPage,
showTagInTransactionListPage,
allDateRanges,
allAccounts,
allAccountsMap,
allAvailableAccountsCount,
allCategories,
allPrimaryCategories,
allAvailableCategoriesCount,
allTransactionTags,
allAvailableTagsCount,
displayPageTypeName,
query,
queryDateRangeName,
queryMinTime,
queryMaxTime,
queryMonthlyData,
queryMonth,
queryAllFilterCategoryIds,
queryAllFilterAccountIds,
queryAllFilterTagIds,
queryAllFilterCategoryIdsCount,
queryAllFilterAccountIdsCount,
queryAllFilterTagIdsCount,
queryAccountName,
queryCategoryName,
queryAmount,
transactionCalendarMinDate,
transactionCalendarMaxDate,
currentMonthTransactionData,
canAddTransaction,
getDisplayTime,
getDisplayLongYearMonth,
getDisplayTimezone,
getDisplayAmount,
getDisplayMonthTotalAmount,
getTransactionTypeName,
} = useTransactionListPageBase();
const environmentsStore = useEnvironmentsStore();
const accountsStore = useAccountsStore();
const transactionCategoriesStore = useTransactionCategoriesStore();
const transactionTagsStore = useTransactionTagsStore();
const transactionsStore = useTransactionsStore();
const loadingError = ref<unknown | null>(null);
const loadingMore = ref<boolean>(false);
const transactionToDelete = ref<Transaction | null>(null);
const transactionInvisibleYearMonths = ref<Record<TextualYearMonth, boolean>>({});
const transactionYearMonthListHeights = ref<Record<TextualYearMonth, number>>({});
const showTransactionListPageTypePopover = ref<boolean>(false);
const showDatePopover = ref<boolean>(false);
const showCategoryPopover = ref<boolean>(false);
const showAccountPopover = ref<boolean>(false);
const showMorePopover = ref<boolean>(false);
const showCustomDateRangeSheet = ref<boolean>(false);
const showCustomMonthSheet = ref<boolean>(false);
const showDeleteActionSheet = ref<boolean>(false);
const textDirection = computed<TextDirection>(() => getCurrentLanguageTextDirection());
const isDarkMode = computed<boolean>(() => environmentsStore.framework7DarkMode || false);
const transactions = computed<TransactionMonthList[]>(() => {
if (loading.value) {
return [];
}
if (pageType.value === TransactionListPageType.List.type) {
return transactionsStore.transactions;
} else if (pageType.value === TransactionListPageType.Calendar.type) {
if (queryMonthlyData.value) {
const transactionData = currentMonthTransactionData.value;
if (!transactionData || !transactionData.items) {
return [];
}
const transactions :Transaction[] = [];
for (const transaction of transactionData.items) {
if (transaction.gregorianCalendarYearDashMonthDashDay === currentCalendarDate.value) {
transactions.push(transaction);
}
}
const dailyTransactionList: TransactionMonthList = {
year: currentMonthTransactionData.value.year,
month: currentMonthTransactionData.value.month,
yearDashMonth: currentMonthTransactionData.value.yearDashMonth,
opened: true,
items: transactions,
totalAmount: {
income: 0,
expense: 0,
incompleteIncome: false,
incompleteExpense: false
},
dailyTotalAmounts: {}
};
return [dailyTransactionList];
} else {
return [];
}
} else {
return [];
}
});
const noTransaction = computed<boolean>(() => {
if (pageType.value === TransactionListPageType.List.type) {
return transactionsStore.noTransaction;
} else if (pageType.value === TransactionListPageType.Calendar.type) {
return !transactions.value || !transactions.value.length || !transactions.value[0]!.items || !transactions.value[0]!.items.length;
} else {
return true;
}
});
const hasMoreTransaction = computed<boolean>(() => transactionsStore.hasMoreTransaction);
function getTransactionMonthTitleDomId(yearMonth: TextualYearMonth): string {
return 'transaction_month_title_' + yearMonth;
}
function getTransactionMonthListDomId(yearMonth: TextualYearMonth): string {
return 'transaction_month_list_' + yearMonth;
}
function getTransactionDomId(transaction: Transaction): string {
return 'transaction_' + transaction.id;
}
function isTransactionMonthListInvisible(transactionMonthList: TransactionMonthList): boolean {
if (!transactionYearMonthListHeights.value[transactionMonthList.yearDashMonth]) {
return false;
}
if (!transactionMonthList.opened) {
return true;
}
if (transactionInvisibleYearMonths.value[transactionMonthList.yearDashMonth]) {
return true;
}
return false;
}
function getTransactionMonthListHeight(transactionMonthList: TransactionMonthList): string {
if (isTransactionMonthListInvisible(transactionMonthList)) {
return transactionYearMonthListHeights.value[transactionMonthList.yearDashMonth] + 'px';
}
return 'auto';
}
function setTransactionMonthListHeights(reset: boolean): Promise<unknown> {
return nextTick(() => {
if (reset) {
transactionInvisibleYearMonths.value = {};
transactionYearMonthListHeights.value = {};
}
if (transactions.value && transactions.value.length) {
const heights: Record<string, number> = getElementActualHeights('.transaction-month-list');
for (let i = 0; i < transactions.value.length - 1; i++) {
const transactionMonthList = transactions.value[i] as TransactionMonthList;
const yearDashMonth = transactionMonthList.yearDashMonth;
const domId = getTransactionMonthListDomId(yearDashMonth);
const height = heights[domId];
if (!transactionYearMonthListHeights.value[yearDashMonth] && isNumber(height)) {
transactionYearMonthListHeights.value[yearDashMonth] = height;
}
}
}
});
}
function setTransactionInvisibleYearMonthList(): void {
if (!transactions.value || !transactions.value.length) {
return;
}
for (let i = 0; i < transactions.value.length - 1; i++) {
const transactionMonthList = transactions.value[i] as TransactionMonthList;
const yearDashMonth = transactionMonthList.yearDashMonth;
const titleDomId = getTransactionMonthTitleDomId(yearDashMonth);
const titleRect = getElementBoundingRect(`#${titleDomId}`);
if (!titleRect) {
continue;
}
const listHeight = transactionYearMonthListHeights.value[yearDashMonth] || 0;
const listRectTop = titleRect.top + titleRect.height;
const listRectBottom = listRectTop + listHeight;
const invisible = listRectTop > 2 * window.innerHeight || listRectBottom < -2 * window.innerHeight;
if (invisible) {
transactionInvisibleYearMonths.value[yearDashMonth] = true;
} else {
delete transactionInvisibleYearMonths.value[yearDashMonth];
}
}
}
function getTransactionDateStyle(transaction: Transaction, previousTransaction: Transaction | undefined): Record<string, string> {
if (!previousTransaction || transaction.gregorianCalendarDayOfMonth !== previousTransaction.gregorianCalendarDayOfMonth) {
return {};
}
return {
color: 'transparent'
};
}
function getCategoryListItemCheckedClass(category: TransactionCategory, queryCategoryIds: Record<string, boolean>): Record<string, boolean> {
if (queryCategoryIds && queryCategoryIds[category.id]) {
return {
'list-item-checked': true
};
}
if (category.subCategories) {
for (const subCategory of category.subCategories) {
if (queryCategoryIds && queryCategoryIds[subCategory.id]) {
return {
'list-item-checked': true
};
}
}
}
return {};
}
function init(): void {
const initQuery = props.f7route.query;
let dateRange: TimeRangeAndDateType | null = getDateRangeByDateType(initQuery['dateType'] ? parseInt(initQuery['dateType']) : undefined, firstDayOfWeek.value, fiscalYearStart.value);
if (!dateRange && initQuery['dateType'] && initQuery['maxTime'] && initQuery['minTime'] &&
(DateRange.isBillingCycle(parseInt(initQuery['dateType'])) || initQuery['dateType'] === DateRange.Custom.type.toString()) &&
parseInt(initQuery['maxTime']) > 0 && parseInt(initQuery['minTime']) > 0) {
dateRange = {
dateType: parseInt(initQuery['dateType']),
maxTime: parseInt(initQuery['maxTime']),
minTime: parseInt(initQuery['minTime'])
};
}
transactionsStore.initTransactionListFilter({
dateType: dateRange ? dateRange.dateType : undefined,
maxTime: dateRange ? dateRange.maxTime : undefined,
minTime: dateRange ? dateRange.minTime : undefined,
type: initQuery['type'] && parseInt(initQuery['type']) > 0 ? parseInt(initQuery['type']) : undefined,
categoryIds: initQuery['categoryIds'],
accountIds: initQuery['accountIds'],
tagFilter: initQuery['tagFilter'],
keyword: initQuery['keyword']
});
reload();
}
function reload(done?: () => void): void {
const force = !!done;
if (!done) {
loading.value = true;
}
transactionInvisibleYearMonths.value = {};
transactionYearMonthListHeights.value = {};
Promise.all([
accountsStore.loadAllAccounts({ force: false }),
transactionCategoriesStore.loadAllCategories({ force: false }),
transactionTagsStore.loadAllTags({ force: false })
]).then(() => {
if (queryMonthlyData.value) {
const currentMonthMinDate = parseDateTimeFromUnixTime(query.value.minTime);
const currentYear = currentMonthMinDate.getGregorianCalendarYear();
const currentMonth = currentMonthMinDate.getGregorianCalendarMonth();
return transactionsStore.loadMonthlyAllTransactions({
year: currentYear,
month: currentMonth,
autoExpand: true,
defaultCurrency: defaultCurrency.value
});
} else {
return transactionsStore.loadTransactions({
reload: true,
autoExpand: true,
defaultCurrency: defaultCurrency.value
});
}
}).then(() => {
done?.();
if (force) {
showToast('Data has been updated');
}
loading.value = false;
setTransactionMonthListHeights(true);
}).catch(error => {
if (error.processed || done) {
loading.value = false;
}
done?.();
if (!error.processed) {
if (!done) {
loadingError.value = error;
}
showToast(error.message || error);
}
});
}
function loadMore(autoExpand: boolean): void {
if (!hasMoreTransaction.value) {
return;
}
if (loadingMore.value || loading.value) {
return;
}
loadingMore.value = true;
transactionsStore.loadTransactions({
reload: false,
autoExpand: autoExpand,
defaultCurrency: defaultCurrency.value
}).then(() => {
loadingMore.value = false;
setTransactionMonthListHeights(false);
}).catch(error => {
loadingMore.value = false;
if (!error.processed) {
showToast(error.message || error);
}
});
}
function changePageType(type: number): void {
pageType.value = type;
currentCalendarDate.value = getValidMonthDayOrCurrentDayShortDate(query.value.minTime, currentCalendarDate.value);
if (pageType.value === TransactionListPageType.Calendar.type) {
const dateRange = getFullMonthDateRange(query.value.minTime, query.value.maxTime, firstDayOfWeek.value, fiscalYearStart.value);
if (dateRange) {
const changed = transactionsStore.updateTransactionListFilter({
dateType: dateRange.dateType,
maxTime: dateRange.maxTime,
minTime: dateRange.minTime
});
if (changed) {
currentCalendarDate.value = getValidMonthDayOrCurrentDayShortDate(query.value.minTime, currentCalendarDate.value);
reload();
}
}
}
}
function changeDateFilter(dateType: number): void {
if (dateType === DateRange.Custom.type) { // Custom
if (!query.value.minTime || !query.value.maxTime) {
customMaxDatetime.value = getActualUnixTimeForStore(getCurrentUnixTime(), currentTimezoneOffsetMinutes.value, getBrowserTimezoneOffsetMinutes());
customMinDatetime.value = getDayFirstUnixTimeBySpecifiedUnixTime(customMaxDatetime.value);
} else {
customMaxDatetime.value = query.value.maxTime;
customMinDatetime.value = query.value.minTime;
}
if (pageType.value === TransactionListPageType.Calendar.type) {
showCustomMonthSheet.value = true;
} else {
showCustomDateRangeSheet.value = true;
}
showDatePopover.value = false;
return;
} else if (query.value.dateType === dateType) {
return;
}
let dateRange: TimeRangeAndDateType | null = null;
if (DateRange.isBillingCycle(dateType)) {
dateRange = getDateRangeByBillingCycleDateType(dateType, firstDayOfWeek.value, fiscalYearStart.value, accountsStore.getAccountStatementDate(query.value.accountIds));
} else {
dateRange = getDateRangeByDateType(dateType, firstDayOfWeek.value, fiscalYearStart.value);
}
if (!dateRange) {
return;
}
if (pageType.value === TransactionListPageType.Calendar.type) {
currentCalendarDate.value = getValidMonthDayOrCurrentDayShortDate(dateRange.minTime, currentCalendarDate.value);
const fullMonthDateRange = getFullMonthDateRange(dateRange.minTime, dateRange.maxTime, firstDayOfWeek.value, fiscalYearStart.value);
if (fullMonthDateRange) {
dateRange = fullMonthDateRange;
currentCalendarDate.value = getValidMonthDayOrCurrentDayShortDate(dateRange.minTime, currentCalendarDate.value);
}
}
const changed = transactionsStore.updateTransactionListFilter({
dateType: dateRange.dateType,
maxTime: dateRange.maxTime,
minTime: dateRange.minTime
});
showDatePopover.value = false;
if (changed) {
reload();
}
}
function changeCustomDateFilter(minTime: number, maxTime: number): void {
if (!minTime || !maxTime) {
return;
}
let dateType: number | null = getDateTypeByBillingCycleDateRange(minTime, maxTime, firstDayOfWeek.value, fiscalYearStart.value, DateRangeScene.Normal, accountsStore.getAccountStatementDate(query.value.accountIds));
if (!dateType) {
dateType = getDateTypeByDateRange(minTime, maxTime, firstDayOfWeek.value, fiscalYearStart.value, DateRangeScene.Normal);
}
if (pageType.value === TransactionListPageType.Calendar.type) {
currentCalendarDate.value = getValidMonthDayOrCurrentDayShortDate(minTime, currentCalendarDate.value);
const dateRange = getFullMonthDateRange(minTime, maxTime, firstDayOfWeek.value, fiscalYearStart.value);
if (dateRange) {
minTime = dateRange.minTime;
maxTime = dateRange.maxTime;
dateType = dateRange.dateType;
currentCalendarDate.value = getValidMonthDayOrCurrentDayShortDate(minTime, currentCalendarDate.value);
}
}
const changed = transactionsStore.updateTransactionListFilter({
dateType: dateType,
maxTime: maxTime,
minTime: minTime
});
showCustomDateRangeSheet.value = false;
if (changed) {
reload();
}
}
function changeCustomMonthDateFilter(yearMonth: Year0BasedMonth): void {
if (!yearMonth) {
return;
}
const minTime = getYearMonthFirstUnixTime(yearMonth);
const maxTime = getYearMonthLastUnixTime(yearMonth);
const dateType = getDateTypeByDateRange(minTime, maxTime, firstDayOfWeek.value, fiscalYearStart.value, DateRangeScene.Normal);
if (pageType.value === TransactionListPageType.Calendar.type) {
currentCalendarDate.value = getValidMonthDayOrCurrentDayShortDate(minTime, currentCalendarDate.value);
}
const changed = transactionsStore.updateTransactionListFilter({
dateType: dateType,
maxTime: maxTime,
minTime: minTime
});
showCustomMonthSheet.value = false;
if (changed) {
reload();
}
}
function shiftDateRange(minTime: number, maxTime: number, scale: number): void {
if (query.value.dateType === DateRange.All.type) {
return;
}
let newDateRange: TimeRangeAndDateType | null = null;
if (DateRange.isBillingCycle(query.value.dateType) || query.value.dateType === DateRange.Custom.type) {
newDateRange = getShiftedDateRangeAndDateTypeForBillingCycle(minTime, maxTime, scale, firstDayOfWeek.value, fiscalYearStart.value, DateRangeScene.Normal, accountsStore.getAccountStatementDate(query.value.accountIds));
}
if (!newDateRange) {
newDateRange = getShiftedDateRangeAndDateType(minTime, maxTime, scale, firstDayOfWeek.value, fiscalYearStart.value, DateRangeScene.Normal);
}
if (pageType.value === TransactionListPageType.Calendar.type) {
currentCalendarDate.value = getValidMonthDayOrCurrentDayShortDate(newDateRange.minTime, currentCalendarDate.value);
const fullMonthDateRange = getFullMonthDateRange(newDateRange.minTime, newDateRange.maxTime, firstDayOfWeek.value, fiscalYearStart.value);
if (fullMonthDateRange) {
newDateRange = fullMonthDateRange;
currentCalendarDate.value = getValidMonthDayOrCurrentDayShortDate(newDateRange.minTime, currentCalendarDate.value);
}
}
const changed = transactionsStore.updateTransactionListFilter({
dateType: newDateRange.dateType,
maxTime: newDateRange.maxTime,
minTime: newDateRange.minTime
});
if (changed) {
reload();
}
}
function changeTypeFilter(type: number): void {
if (query.value.type === type) {
return;
}
let newCategoryFilter = undefined;
if (type && query.value.categoryIds) {
newCategoryFilter = '';
for (const categoryId of keys(queryAllFilterCategoryIds.value)) {
const category = allCategories.value[categoryId];
if (category && category.type === transactionTypeToCategoryType(type)) {
if (newCategoryFilter.length > 0) {
newCategoryFilter += ',';
}
newCategoryFilter += categoryId;
}
}
}
const changed = transactionsStore.updateTransactionListFilter({
type: type,
categoryIds: newCategoryFilter
});
showMorePopover.value = false;
if (changed) {
reload();
}
}
function changeCategoryFilter(categoryIds: string): void {
if (query.value.categoryIds === categoryIds) {
return;
}
const changed = transactionsStore.updateTransactionListFilter({
categoryIds: categoryIds
});
showCategoryPopover.value = false;
if (changed) {
reload();
}
}
function filterMultipleCategories(): void {
let navigateUrl = '/settings/filter/category?type=transactionListCurrent';
if (TransactionType.Income <= query.value.type && query.value.type <= TransactionType.Transfer) {
navigateUrl += '&allowCategoryTypes=' + transactionTypeToCategoryType(query.value.type);
}
props.f7router.navigate(navigateUrl);
}
function changeAccountFilter(accountIds: string): void {
if (query.value.accountIds === accountIds) {
return;
}
const changed = transactionsStore.updateTransactionListFilter({
accountIds: accountIds
});
showAccountPopover.value = false;
if (changed) {
reload();
}
}
function filterMultipleAccounts(): void {
props.f7router.navigate('/settings/filter/account?type=transactionListCurrent');
}
function changeTagFilter(tagFilter: string): void {
if (query.value.tagFilter === tagFilter) {
return;
}
const changed = transactionsStore.updateTransactionListFilter({
tagFilter: tagFilter
});
showMorePopover.value = false;
if (changed) {
reload();
}
}
function filterMultipleTags(): void {
props.f7router.navigate('/settings/filter/tag?type=transactionListCurrent');
}
function changeKeywordFilter(keyword: string): void {
if (query.value.keyword === keyword) {
return;
}
const changed = transactionsStore.updateTransactionListFilter({
keyword: keyword
});
if (changed) {
reload();
}
}
function changeAmountFilter(filterType: string): void {
if (query.value.amountFilter === filterType) {
return;
}
if (filterType) {
showMorePopover.value = false;
props.f7router.navigate(`/transaction/filter/amount?type=${filterType}&value=${query.value.amountFilter}`);
return;
}
const changed = transactionsStore.updateTransactionListFilter({
amountFilter: filterType
});
showMorePopover.value = false;
if (changed) {
reload();
}
}
function add(): void {
const currentUnixTime = getCurrentUnixTime();
let setTransactionTime = false;
let newTransactionTime: number | undefined = undefined;
if (query.value.maxTime && query.value.minTime) {
if (query.value.maxTime < currentUnixTime) {
setTransactionTime = true;
newTransactionTime = query.value.maxTime;
} else if (currentUnixTime < query.value.minTime) {
setTransactionTime = true;
newTransactionTime = query.value.minTime;
}
}
const params: string[] = [];
if (setTransactionTime) {
params.push(`time=${newTransactionTime}`);
}
if (query.value.type !== TransactionType.ModifyBalance) {
params.push(`type=${query.value.type}`);
}
if (queryAllFilterCategoryIdsCount.value === 1) {
params.push(`categoryId=${query.value.categoryIds}`);
}
if (queryAllFilterAccountIdsCount.value === 1) {
params.push(`accountId=${query.value.accountIds}`);
}
if (query.value.tagFilter) {
params.push(`tagIds=${objectFieldWithValueToArrayItem(queryAllFilterTagIds.value, true).join(',') || ''}`);
}
props.f7router.navigate(`/transaction/add?${params.join('&')}`);
}
function duplicate(transaction: Transaction): void {
props.f7router.navigate(`/transaction/add?id=${transaction.id}&type=${transaction.type}`);
}
function edit(transaction: Transaction): void {
props.f7router.navigate(`/transaction/edit?id=${transaction.id}&type=${transaction.type}`);
}
function remove(transaction: Transaction | null, confirm: boolean): void {
if (!transaction) {
showAlert('An error occurred');
return;
}
if (!confirm) {
transactionToDelete.value = transaction;
showDeleteActionSheet.value = true;
return;
}
showDeleteActionSheet.value = false;
transactionToDelete.value = null;
showLoading();
transactionsStore.deleteTransaction({
transaction: transaction,
defaultCurrency: defaultCurrency.value,
beforeResolve: (done) => {
onSwipeoutDeleted(getTransactionDomId(transaction), done);
}
}).then(() => {
hideLoading();
}).catch(error => {
hideLoading();
if (!error.processed) {
showToast(error.message || error);
}
});
}
function collapseTransactionMonthList(monthList: TransactionMonthList, collapse: boolean): void {
transactionsStore.collapseMonthInTransactionList({
monthList: monthList,
collapse: collapse
});
if (!collapse && transactionInvisibleYearMonths.value[monthList.yearDashMonth]) {
delete transactionInvisibleYearMonths.value[monthList.yearDashMonth];
}
}
function onPopoverOpen(event: { $el: Framework7Dom }): void {
scrollToSelectedItem(event.$el, '.popover-inner', 'li.list-item-selected');
}
function onPageAfterIn(): void {
if (transactionsStore.transactionListStateInvalid && !loading.value) {
reload();
}
routeBackOnError(props.f7router, loadingError);
}
function onResize(): void {
setTransactionMonthListHeights(true)
.then(() => {
setTransactionMonthListHeights(false);
});
}
function onScroll(): void {
setTransactionInvisibleYearMonthList();
}
function onTransactionMonthListCollapseStateChanged(): void {
setTransactionMonthListHeights(false)
.then(() => {
setTransactionInvisibleYearMonthList();
});
}
onMounted(() => {
window.addEventListener('resize', onResize);
onInfiniteScrolling(onScroll);
});
onUnmounted(() => {
window.removeEventListener('resize', onResize);
});
init();
</script>
<style>
.transaction-list-toolbar .toolbar-inner {
padding-inline-end: 8px;
}
.list.transaction-amount-list .transaction-amount-statistics {
overflow: hidden;
text-overflow: ellipsis;
}
.list.transaction-amount-list .transaction-amount-statistics > span {
margin-inline-start: 8px;
font-weight: normal;
}
.list.transaction-info-list li.transaction-info .item-media + .item-inner {
margin-inline-start: 0;
}
.list.transaction-info-list li.transaction-info .actual-item-inner {
width: 100%;
margin-inline-start: 10px;
overflow: hidden;
}
.list.transaction-info-list li.transaction-info .actual-item-inner .item-after {
max-width: 100%;
}
.list.transaction-info-list li.transaction-info .transaction-date {
width: var(--ebk-transaction-date-width);
margin-inline-end: 6px;
}
.list.transaction-info-list li.transaction-info .transaction-day {
opacity: 0.6;
font-size: var(--ebk-transaction-day-font-size);
font-weight: bold;
text-align: left;
}
html[dir="rtl"] .list.transaction-info-list li.transaction-info .transaction-day {
text-align: right;
}
.list.transaction-info-list li.transaction-info .transaction-day-of-week {
opacity: 0.6;
font-size: var(--ebk-transaction-day-of-week-font-size);
}
.list.transaction-info-list li.transaction-info .transaction-description {
font-size: var(--ebk-large-footer-font-size);
line-height: 20px;
padding-top: 2px;
padding-bottom: 2px;
}
.list.transaction-info-list li.transaction-info .chip.transaction-tag {
--f7-chip-media-size: var(--ebk-transaction-tag-chip-media-size);
--f7-chip-media-font-size: var(--ebk-transaction-tag-chip-font-size);
--f7-chip-font-size: var(--ebk-transaction-tag-chip-font-size);
--f7-chip-height: var(--ebk-transaction-tag-chip-height);
--f7-chip-text-color: var(--f7-list-item-footer-text-color);
--f7-chip-bg-color: var(--ebk-transaction-tag-chip-bg-color);
margin-inline-end: 4px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.list.transaction-info-list li.transaction-info .chip.transaction-tag .chip-media+.chip-label {
margin-inline-start: 0;
}
.list.transaction-info-list li.transaction-info .transaction-footer {
padding-top: 4px;
}
.list.transaction-info-list li.transaction-info .transaction-info .item-text + .item-footer .transaction-footer {
padding-top: 2px;
}
.list.transaction-info-list li.transaction-info .transaction-footer > span {
unicode-bidi: isolate;
margin-inline-end: 4px;
}
.list.transaction-info-list li.transaction-info .transaction-footer .transaction-account-arrow {
font-size: var(--ebk-transaction-account-arrow-font-size);
margin-inline-end: 4px;
margin-top: var(--ebk-transaction-account-arrow-margin-top);
}
html[dir="rtl"] .list.transaction-info-list li.transaction-info .transaction-footer .transaction-account-arrow {
margin-inline-end: 0;
margin-inline-start: 4px;
}
.list.transaction-info-list li.transaction-info .transaction-amount {
color: var(--f7-list-item-after-text-color);
overflow: hidden;
text-overflow: ellipsis;
}
.list.transaction-info-list li.transaction-info .transaction-info .item-after {
max-width: 70%;
}
.list.transaction-info-list li.transaction-info .transaction-category-name {
overflow: hidden;
text-overflow: ellipsis;
}
.more-popover-menu .transaction-tag-name {
padding-inline-end: 4px;
font-size: var(--f7-list-item-title-font-size);
}
.date-popover-menu .popover-inner,
.category-popover-menu .popover-inner,
.account-popover-menu .popover-inner,
.more-popover-menu .popover-inner {
max-height: 400px;
> .list {
overflow-y: auto;
}
}
.transaction-calendar-container .dp__theme_light,
.transaction-calendar-container .dp__theme_dark {
--dp-background-color: var(--f7-list-strong-bg-color);
}
.transaction-calendar-container .dp__main .dp__menu {
--dp-border-radius: var(--f7-list-inset-border-radius);
--dp-menu-padding: 4px 6px;
--dp-menu-border-color: transparent;
}
.transaction-calendar-container .dp__main .dp__menu.dp__theme_dark {
--dp-background-color: var(--f7-list-strong-bg-color);
}
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row {
--dp-cell-size: var(--ebk-transaction-calendar-daily-amounts-height);
--dp-cell-padding: 1px;
--dp-primary-text-color: var(--f7-theme-color);
}
.transaction-calendar-container .dp__main.transaction-calendar-with-alternate-date .dp__calendar .dp__calendar_row {
--dp-cell-size: var(--ebk-transaction-calendar-with-alternate-date-daily-amounts-height);
}
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item .transaction-calendar-daily-amounts {
width: 100%;
height: 100%;
background-color: var(--f7-list-group-title-bg-color);
border-radius: 6px;
}
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item > .dp__active_date {
background-color: transparent;
}
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item > .dp__today {
border: inherit;
}
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item > .dp__date_hover_end:hover,
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item > .dp__date_hover_start:hover,
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item > .dp__date_hover:hover {
background-color: transparent;
}
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item > .dp__active_date .transaction-calendar-daily-amounts {
background-color: rgba(var(--ebk-primary-color), 0.16);
}
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item > .dp__today .transaction-calendar-daily-amounts {
border: 1px solid var(--dp-primary-color);
}
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item > .dp__date_hover_end:hover .transaction-calendar-daily-amounts,
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item > .dp__date_hover_start:hover .transaction-calendar-daily-amounts,
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item > .dp__date_hover:hover .transaction-calendar-daily-amounts {
background: var(--dp-hover-color);
}
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item .transaction-calendar-daily-amounts > span.transaction-calendar-alternate-date {
font-size: var(--ebk-transaction-calendar-alternate-date-font-size);
}
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item .transaction-calendar-daily-amounts > span.transaction-calendar-daily-amount {
font-size: var(--ebk-transaction-calendar-amount-font-size);
}
</style>