Files
ezbookkeeping/src/views/mobile/transactions/List.vue
T
2021-01-05 01:31:07 +08:00

1096 lines
50 KiB
Vue

<template>
<f7-page ptr
infinite
:infinite-preloader="loadingMore"
:infinite-distance="400"
@ptr:refresh="reload"
@infinite="loadMore(true)">
<f7-navbar>
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title :title="$t('Transaction List')"></f7-nav-title>
<f7-nav-right class="navbar-compact-icons">
<f7-link icon-f7="search" class="searchbar-enable" data-searchbar=".searchbar-keyword"></f7-link>
<f7-link icon-f7="plus" :href="'/transaction/add?type=' + query.type + '&categoryId=' + query.categoryId + '&accountId=' + query.accountId"></f7-link>
</f7-nav-right>
<f7-searchbar
expandable custom-search
class="searchbar-keyword"
:value="query.keyword"
:placeholder="$t('Search')"
:disable-button-text="$t('Cancel')"
@change="changeKeywordFilter($event.target.value)"
></f7-searchbar>
</f7-navbar>
<f7-toolbar tabbar bottom>
<f7-link class="tabbar-text-with-ellipsis" popover-open=".date-popover-menu">
<span :class="{ 'tabbar-item-changed': query.maxTime > 0 || query.minTime > 0 }">{{ query.dateType | dateName('Date') | t }}</span>
</f7-link>
<f7-link class="tabbar-text-with-ellipsis" popover-open=".type-popover-menu">
<span :class="{ 'tabbar-item-changed': query.type > 0 }">{{ query.type | typeName('Type') | t }}</span>
</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.categoryId > 0 }">{{ query.categoryId | categoryName(allCategories, $t('Category')) }}</span>
</f7-link>
<f7-link class="tabbar-text-with-ellipsis" popover-open=".account-popover-menu">
<span :class="{ 'tabbar-item-changed': query.accountId > 0 }">{{ query.accountId | accountName(allAccounts, $t('Account')) }}</span>
</f7-link>
</f7-toolbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-header>
<div class="full-line">
<small :style="{ opacity: 0.6 }">YYYY-MM</small>
<small class="transaction-amount-statistics">
<span>0.00 USD</span>
<span>0.00 USD</span>
</small>
<f7-icon class="transaction-month-card-chevron-icon float-right" f7="chevron_up"></f7-icon>
</div>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list media-list>
<f7-list-item class="transaction-info" link="#" chevron-center>
<div slot="media" class="display-flex no-padding-horizontal">
<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>
<div class="transaction-icon display-flex align-items-center">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</div>
</div>
<div slot="title" class="no-padding">
<span>Category</span>
</div>
<div slot="footer" class="no-padding-horizontal transaction-footer">
<span>HH:mm</span>
<span>·</span>
<span>Source Account</span>
</div>
<div slot="after" class="no-padding transaction-amount">
<span>0.00 USD</span>
</div>
</f7-list-item>
<f7-list-item class="transaction-info" link="#" chevron-center>
<div slot="media" class="display-flex no-padding-horizontal">
<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>
<div class="transaction-icon display-flex align-items-center">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</div>
</div>
<div slot="title" class="no-padding">
<span>Category 2</span>
</div>
<div slot="footer" class="no-padding-horizontal transaction-footer">
<span>HH:mm</span>
<span>·</span>
<span>Source Account</span>
</div>
<div slot="after" class="no-padding transaction-amount">
<span>0.00 USD</span>
</div>
</f7-list-item>
<f7-list-item class="transaction-info" link="#" chevron-center>
<div slot="media" class="display-flex no-padding-horizontal">
<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>
<div class="transaction-icon display-flex align-items-center">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</div>
</div>
<div slot="title" class="no-padding">
<span>Category 3</span>
</div>
<div slot="footer" class="no-padding-horizontal transaction-footer">
<span>HH:mm</span>
<span>·</span>
<span>Source Account</span>
</div>
<div slot="after" class="no-padding transaction-amount">
<span>0.00 USD</span>
</div>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-if="!loading && noTransaction">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('No transaction data')"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-for="transactionMonthList in transactions" :key="transactionMonthList.yearMonth">
<f7-accordion-item :opened="transactionMonthList.opened"
@accordion:open="transactionMonthList.opened = true"
@accordion:close="transactionMonthList.opened = false">
<f7-card-header>
<f7-accordion-toggle class="full-line">
<small :style="{ opacity: 0.6 }">
<span>{{ transactionMonthList.yearMonth | moment($t('format.date.yearMonth')) }}</span>
</small>
<small class="transaction-amount-statistics" v-if="transactionMonthList.totalAmount">
<span class="text-color-red">
{{ transactionMonthList.totalAmount.income | currency(defaultCurrency) | income(transactionMonthList.totalAmount.incompleteIncome) }}
</span>
<span class="text-color-teal">
{{ transactionMonthList.totalAmount.expense | currency(defaultCurrency) | expense(transactionMonthList.totalAmount.incompleteExpense) }}
</span>
</small>
<f7-icon class="transaction-month-card-chevron-icon float-right" :f7="transactionMonthList.opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
</f7-accordion-toggle>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false" accordion-list>
<f7-accordion-content :style="{ height: transactionMonthList.opened ? 'auto' : '' }">
<f7-list media-list>
<f7-list-item class="transaction-info" chevron-center
v-for="(transaction, idx) in transactionMonthList.items"
:key="transaction.id" :id="transaction | transactionDomId"
:link="transaction.type !== $constants.transaction.allTransactionTypes.ModifyBalance ? '/transaction/detail?id=' + transaction.id : null"
swipeout
>
<div slot="media" class="display-flex no-padding-horizontal">
<div class="display-flex flex-direction-column transaction-date" :style="transaction | transactionDateStyle(idx > 0 ? transactionMonthList.items[idx - 1] : null)">
<span class="transaction-day full-line flex-direction-column">
{{ transaction.day }}
</span>
<span class="transaction-day-of-week full-line flex-direction-column">
{{ transaction.dayOfWeek }}
</span>
</div>
<div class="transaction-icon display-flex align-items-center">
<f7-icon v-if="transaction.category && transaction.category.color"
:icon="transaction.category.icon | categoryIcon"
:style="transaction.category.color | categoryIconStyle('var(--category-icon-color)')">
</f7-icon>
<f7-icon v-else-if="!transaction.category || !transaction.category.color"
f7="pencil_ellipsis_rectangle">
</f7-icon>
</div>
</div>
<div slot="title" class="no-padding">
<span v-if="transaction.type === $constants.transaction.allTransactionTypes.ModifyBalance">
{{ $t('Modify Balance') }}
</span>
<span v-else-if="transaction.type !== $constants.transaction.allTransactionTypes.ModifyBalance && transaction.category">
{{ transaction.category.name }}
</span>
<span v-else-if="transaction.type !== $constants.transaction.allTransactionTypes.ModifyBalance && !transaction.category">
{{ transaction.type | transactionTypeName($constants.transaction.allTransactionTypes) }}
</span>
</div>
<div slot="text" class="transaction-comment" v-if="transaction.comment">
<span>{{ transaction.comment }}</span>
</div>
<div slot="footer" class="transaction-footer">
<span>{{ transaction.time | moment($t('format.time.hourMinute')) }}</span>
<span v-if="transaction.sourceAccount">·</span>
<span v-if="transaction.sourceAccount">{{ transaction.sourceAccount.name }}</span>
<span v-if="transaction.sourceAccount && transaction.type === $constants.transaction.allTransactionTypes.Transfer && transaction.destinationAccount && transaction.sourceAccount.id !== transaction.destinationAccount.id"></span>
<span v-if="transaction.sourceAccount && transaction.type === $constants.transaction.allTransactionTypes.Transfer && transaction.destinationAccount && transaction.sourceAccount.id !== transaction.destinationAccount.id">{{ transaction.destinationAccount.name }}</span>
</div>
<div slot="after" class="transaction-amount" v-if="transaction.sourceAccount">
<span :class="{ 'text-color-teal': transaction.type === $constants.transaction.allTransactionTypes.Expense, 'text-color-red': transaction.type === $constants.transaction.allTransactionTypes.Income }">
{{ transaction.sourceAmount | currency(transaction.sourceAccount.currency) }}
</span>
</div>
<f7-swipeout-actions right>
<f7-swipeout-button color="orange" close
:text="$t('Edit')"
v-if="transaction.type !== $constants.transaction.allTransactionTypes.ModifyBalance"
@click="edit(transaction)"></f7-swipeout-button>
<f7-swipeout-button color="red" class="padding-left padding-right" @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-card-content>
</f7-accordion-item>
</f7-card>
<f7-block class="text-align-center" v-if="!loading && maxTime > 0">
<f7-link :class="{ 'disabled': loadingMore }" href="#" @click="loadMore(false)">{{ $t('Load More') }}</f7-link>
</f7-block>
<f7-popover class="date-popover-menu" :opened="showDatePopover"
@popover:opened="showDatePopover = true" @popover:closed="showDatePopover = false">
<f7-list>
<f7-list-item :title="$t('All')" @click="changeDateFilter(0)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.dateType === 0"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('Today')" @click="changeDateFilter(1)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.dateType === 1"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('Yesterday')" @click="changeDateFilter(2)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.dateType === 2"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('Recent 7 days')" @click="changeDateFilter(3)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.dateType === 3"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('Recent 30 days')" @click="changeDateFilter(4)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.dateType === 4"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('This week')" @click="changeDateFilter(5)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.dateType === 5"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('Last week')" @click="changeDateFilter(6)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.dateType === 6"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('This month')" @click="changeDateFilter(7)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.dateType === 7"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('Last month')" @click="changeDateFilter(8)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.dateType === 8"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('This year')" @click="changeDateFilter(9)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.dateType === 9"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('Last year')" @click="changeDateFilter(10)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.dateType === 10"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('Custom')" @click="changeDateFilter(11)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.dateType === 11"></f7-icon>
<div slot="footer" v-if="query.dateType === 11 && query.minTime && query.maxTime">
<span>{{ query.minTime | moment($t('format.datetime.long-without-second')) }}</span>
<span>&nbsp;-&nbsp;</span>
<br/>
<span>{{ query.maxTime | moment($t('format.datetime.long-without-second')) }}</span>
</div>
</f7-list-item>
</f7-list>
</f7-popover>
<date-range-selection-sheet :title="$t('Custom Date Range')"
:show.sync="showCustomDateRangeSheet"
:min-time="query.minTime"
:max-time="query.maxTime"
@dateRange:change="changeCustomDateFilter">
</date-range-selection-sheet>
<f7-popover class="type-popover-menu" :opened="showTypePopover"
@popover:opened="showTypePopover = true" @popover:closed="showTypePopover = false">
<f7-list>
<f7-list-item :title="$t('All')" @click="changeTypeFilter(0)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.type === 0"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('Modify Balance')" @click="changeTypeFilter(1)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.type === 1"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('Income')" @click="changeTypeFilter(2)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.type === 2"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('Expense')" @click="changeTypeFilter(3)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.type === 3"></f7-icon>
</f7-list-item>
<f7-list-item :title="$t('Transfer')" @click="changeTypeFilter(4)">
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.type === 4"></f7-icon>
</f7-list-item>
</f7-list>
</f7-popover>
<f7-popover class="category-popover-menu" :opened="showCategoryPopover"
@popover:opened="showCategoryPopover = true" @popover:closed="showCategoryPopover = false">
<f7-list>
<f7-list-item :title="$t('All')" @click="changeCategoryFilter('0')">
<f7-icon slot="media" f7="rectangle_badge_checkmark"></f7-icon>
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.categoryId === '0'"></f7-icon>
</f7-list-item>
<f7-list-item v-for="category in allCategories"
v-show="category.parentId > 0 && (!query.type || category.type === query.type - 1)"
:key="category.id"
:title="category.name"
@click="changeCategoryFilter(category.id)"
>
<f7-icon slot="media"
:icon="category.icon | categoryIcon"
:style="category.color | categoryIconStyle('var(--default-icon-color)')">
</f7-icon>
<f7-icon slot="after"
class="list-item-checked"
f7="checkmark_alt"
v-if="query.categoryId === category.id">
</f7-icon>
</f7-list-item>
</f7-list>
</f7-popover>
<f7-popover class="account-popover-menu" :opened="showAccountPopover"
@popover:opened="showAccountPopover = true" @popover:closed="showAccountPopover = false">
<f7-list>
<f7-list-item :title="$t('All')" @click="changeAccountFilter('0')">
<f7-icon slot="media" f7="rectangle_badge_checkmark"></f7-icon>
<f7-icon slot="after" class="list-item-checked" f7="checkmark_alt" v-if="query.accountId === '0'"></f7-icon>
</f7-list-item>
<f7-list-item v-for="account in allAccounts"
v-show="!account.hidden"
:key="account.id"
:title="account.name"
@click="changeAccountFilter(account.id)"
>
<f7-icon slot="media"
:icon="account.icon | accountIcon"
:style="account.color | accountIconStyle('var(--default-icon-color)')">
</f7-icon>
<f7-icon slot="after"
class="list-item-checked"
f7="checkmark_alt"
v-if="query.accountId === account.id">
</f7-icon>
</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>{{ $t('Are you sure you want to delete this transaction?') }}</f7-actions-label>
<f7-actions-button color="red" @click="remove(transactionToDelete, true)">{{ $t('Delete') }}</f7-actions-button>
</f7-actions-group>
<f7-actions-group>
<f7-actions-button bold close>{{ $t('Cancel') }}</f7-actions-button>
</f7-actions-group>
</f7-actions>
</f7-page>
</template>
<script>
export default {
data() {
return {
transactions: [],
query: {
dateType: 0,
maxTime: 0,
minTime: 0,
type: 0,
categoryId: '0',
accountId: '0',
keyword: ''
},
allAccounts: {},
allCategories: {},
allTags: {},
maxTime: 0,
loading: true,
loadingMore: false,
transactionToDelete: null,
showDatePopover: false,
showTypePopover: false,
showCategoryPopover: false,
showAccountPopover: false,
showCustomDateRangeSheet: false,
showDeleteActionSheet: false
};
},
computed: {
defaultCurrency() {
return this.$store.getters.currentUserDefaultCurrency || this.$t('default.currency');
},
noTransaction() {
for (let i = 0; i < this.transactions.length; i++) {
const transactionMonthList = this.transactions[i];
for (let j = 0; j < transactionMonthList.items.length; j++) {
if (transactionMonthList.items[j]) {
return false;
}
}
}
return true;
}
},
created() {
const self = this;
const query = self.$f7route.query;
if (query.type) {
self.query.type = query.type;
}
if (query.categoryId) {
self.query.categoryId = query.categoryId;
}
if (query.accountId) {
self.query.accountId = query.accountId;
}
this.reload(null);
},
methods: {
reload(done) {
const self = this;
const router = self.$f7router;
if (!done) {
self.loading = true;
}
if (self.query.maxTime > 0) {
self.maxTime = self.query.maxTime * 1000 + 999;
} else {
self.maxTime = 0;
}
const promises = [
self.$services.getAllAccounts({ visibleOnly: false }),
self.$services.getAllTransactionCategories({}),
self.$services.getAllTransactionTags(),
self.$services.getTransactions({
maxTime: self.maxTime,
minTime: self.query.minTime * 1000,
type: self.query.type,
categoryId: self.query.categoryId,
accountId: self.query.accountId,
keyword: self.query.keyword
})
];
Promise.all(promises).then(responses => {
if (done) {
done();
}
const accountData = responses[0].data;
const categoryData = responses[1].data;
const tagData = responses[2].data;
const transactionListData = responses[3].data;
if (!accountData || !accountData.success || !accountData.result) {
self.$toast('Unable to get account list');
if (!done) {
router.back();
}
return;
}
if (!categoryData || !categoryData.success || !categoryData.result) {
self.$toast('Unable to get category list');
if (!done) {
router.back();
}
return;
}
if (!tagData || !tagData.success || !tagData.result) {
self.$toast('Unable to get tag list');
if (!done) {
router.back();
}
return;
}
if (!transactionListData || !transactionListData.success || !transactionListData.result) {
self.$toast('Unable to get transaction list');
if (!done) {
router.back();
}
return;
}
const allAccounts = self.$utilities.getPlainAccounts(accountData.result);
self.allAccounts = {};
for (let i = 0; i < allAccounts.length; i++) {
const account = allAccounts[i];
self.allAccounts[account.id] = account;
}
const allCategories = categoryData.result;
self.allCategories = {};
for (let categoryType in allCategories) {
if (!Object.prototype.hasOwnProperty.call(allCategories, categoryType)) {
continue;
}
const categoryList = allCategories[categoryType];
for (let i = 0; i < categoryList.length; i++) {
const category = categoryList[i];
self.allCategories[category.id] = category;
for (let j = 0; j < category.subCategories.length; j++) {
const subCategory = category.subCategories[j];
self.allCategories[subCategory.id] = subCategory;
}
}
}
const allTags = tagData.result;
self.allTags = {};
for (let i = 0; i < allTags.length; i++) {
const tag = allTags[i];
self.allTags[tag.id] = tag;
}
self.transactions = [];
self.setResult(transactionListData.result, true);
self.loading = false;
}).catch(error => {
self.$logger.error('failed to load transaction list', error);
if (done) {
done();
}
if (error.response && error.response.data && error.response.data.errorMessage) {
self.$toast({ error: error.response.data });
if (!done) {
router.back();
}
} else if (!error.processed) {
self.$toast('Unable to get transaction list');
if (!done) {
router.back();
}
}
});
},
loadMore(autoExpand) {
const self = this;
if (self.maxTime <= 0) {
return;
}
if (self.loadingMore || self.loading) {
return;
}
self.loadingMore = true;
self.$services.getTransactions({
maxTime: self.maxTime,
minTime: self.query.minTime * 1000,
type: self.query.type,
categoryId: self.query.categoryId,
accountId: self.query.accountId,
keyword: self.query.keyword
}).then(response => {
self.loadingMore = false;
const data = response.data;
if (!data || !data.success || !data.result) {
self.$toast('Unable to get transaction list');
return;
}
self.setResult(data.result, autoExpand);
}).catch(error => {
self.loadingMore = false;
self.$logger.error('failed to reload transaction list', error);
if (error.response && error.response.data && error.response.data.errorMessage) {
self.$toast({ error: error.response.data });
} else if (!error.processed) {
self.$toast('Unable to get account list');
}
});
},
changeDateFilter(dateType) {
if (dateType === 11) { // Custom
this.showCustomDateRangeSheet = true;
this.showDatePopover = false;
return;
} else if (this.query.dateType === dateType) {
return;
}
if (dateType === 0) { // All
this.query.maxTime = 0;
this.query.minTime = 0;
} else if (dateType === 1) { // Today
this.query.maxTime = this.$utilities.getTodayLastUnixTime();
this.query.minTime = this.$utilities.getTodayFirstUnixTime();
} else if (dateType === 2) { // Yesterday
this.query.maxTime = this.$utilities.getUnixTimeBeforeUnixTime(this.$utilities.getTodayLastUnixTime(), 1, 'days');
this.query.minTime = this.$utilities.getUnixTimeBeforeUnixTime(this.$utilities.getTodayFirstUnixTime(), 1, 'days');
} else if (dateType === 3) { // Last 7 days
this.query.maxTime = this.$utilities.getUnixTime(new Date());
this.query.minTime = this.$utilities.getUnixTimeBeforeUnixTime(this.query.maxTime, 7, 'days');
} else if (dateType === 4) { // Last 30 days
this.query.maxTime = this.$utilities.getUnixTime(new Date());
this.query.minTime = this.$utilities.getUnixTimeBeforeUnixTime(this.query.maxTime, 30, 'days');
} else if (dateType === 5) { // This week
this.query.maxTime = this.$utilities.getThisWeekLastUnixTime();
this.query.minTime = this.$utilities.getThisWeekFirstUnixTime();
} else if (dateType === 6) { // Last week
this.query.maxTime = this.$utilities.getUnixTimeBeforeUnixTime(this.$utilities.getThisWeekLastUnixTime(), 7, 'days');
this.query.minTime = this.$utilities.getUnixTimeBeforeUnixTime(this.$utilities.getThisWeekFirstUnixTime(), 7, 'days');
} else if (dateType === 7) { // This month
this.query.maxTime = this.$utilities.getThisMonthLastUnixTime();
this.query.minTime = this.$utilities.getThisMonthFirstUnixTime();
} else if (dateType === 8) { // Last month
this.query.maxTime = this.$utilities.getUnixTimeBeforeUnixTime(this.$utilities.getThisMonthLastUnixTime(), 1, 'months');
this.query.minTime = this.$utilities.getUnixTimeBeforeUnixTime(this.$utilities.getThisMonthFirstUnixTime(), 1, 'months');
} else if (dateType === 9) { // This year
this.query.maxTime = this.$utilities.getThisYearLastUnixTime();
this.query.minTime = this.$utilities.getThisYearFirstUnixTime();
} else if (dateType === 10) { // Last year
this.query.maxTime = this.$utilities.getUnixTimeBeforeUnixTime(this.$utilities.getThisYearLastUnixTime(), 1, 'years');
this.query.minTime = this.$utilities.getUnixTimeBeforeUnixTime(this.$utilities.getThisYearFirstUnixTime(), 1, 'years');
} else {
return;
}
this.transactions = [];
this.query.dateType = dateType;
this.showDatePopover = false;
this.reload(null);
},
changeCustomDateFilter(minTime, maxTime) {
if (!minTime || !maxTime) {
return;
}
this.query.maxTime = maxTime;
this.query.minTime = minTime;
console.log(this.$utilities.formatUnixTime(this.query.maxTime, 'YYYY-MM-DD HH:mm:ss'));
console.log(this.$utilities.formatUnixTime(this.query.minTime, 'YYYY-MM-DD HH:mm:ss'));
this.transactions = [];
this.query.dateType = 11;
this.showCustomDateRangeSheet = false;
this.reload(null);
},
changeTypeFilter(type) {
if (this.query.type === type) {
return;
}
if (type && this.query.categoryId) {
const category = this.allCategories[this.query.categoryId];
if (category && category.type !== type - 1) {
this.query.categoryId = 0;
}
}
this.transactions = [];
this.query.type = type;
this.showTypePopover = false;
this.reload(null);
},
changeCategoryFilter(categoryId) {
if (this.query.categoryId === categoryId) {
return;
}
this.transactions = [];
this.query.categoryId = categoryId;
this.showCategoryPopover = false;
this.reload(null);
},
changeAccountFilter(accountId) {
if (this.query.accountId === accountId) {
return;
}
this.transactions = [];
this.query.accountId = accountId;
this.showAccountPopover = false;
this.reload(null);
},
changeKeywordFilter(keyword) {
if (this.query.keyword === keyword) {
return;
}
this.transactions = [];
this.query.keyword = keyword;
this.reload(null);
},
edit(transaction) {
this.$f7router.navigate('/transaction/edit?id=' + transaction.id);
},
remove(transaction, confirm) {
const self = this;
const app = self.$f7;
const $$ = app.$;
if (!transaction) {
self.$alert('An error has occurred');
return;
}
if (!confirm) {
self.transactionToDelete = transaction;
self.showDeleteActionSheet = true;
return;
}
self.showDeleteActionSheet = false;
self.transactionToDelete = null;
self.$showLoading();
self.$services.deleteTransaction({
id: transaction.id
}).then(response => {
self.$hideLoading();
const data = response.data;
if (!data || !data.success || !data.result) {
self.$toast('Unable to delete this transaction');
return;
}
app.swipeout.delete($$(`#${self.$options.filters.transactionDomId(transaction)}`), () => {
for (let i = 0; i < self.transactions.length; i++) {
const transactionMonthList = self.transactions[i];
if (!transactionMonthList.items ||
transactionMonthList.items[0].time < transaction.time ||
transactionMonthList.items[transactionMonthList.items.length - 1].time > transaction.time) {
continue;
}
for (let j = 0; j < transactionMonthList.items.length; j++) {
if (transactionMonthList.items[j].id === transaction.id) {
transactionMonthList.items.splice(j, 1);
}
}
if (transactionMonthList.items.length < 1) {
self.transactions.splice(i, 1);
} else {
self.calculateMonthTotalAmount(transactionMonthList, i >= self.transactions.length - 1 && self.maxTime > 0);
}
}
});
}).catch(error => {
self.$logger.error('failed to delete transaction', error);
self.$hideLoading();
if (error.response && error.response.data && error.response.data.errorMessage) {
self.$toast({ error: error.response.data });
} else if (!error.processed) {
self.$toast('Unable to delete this transaction');
}
});
},
setResult(result, autoExpand) {
if (result.items && result.items.length) {
let currentMonthListIndex = -1;
let currentMonthList = null;
for (let i = 0; i < result.items.length; i++) {
const transaction = result.items[i];
const transactionTime = this.$utilities.parseDateFromUnixTime(transaction.time);
transaction.day = this.$utilities.getDay(transactionTime);
transaction.dayOfWeek = this.$t(`datetime.${this.$utilities.getDayOfWeek(transactionTime)}.short`);
transaction.sourceAccount = this.allAccounts[transaction.sourceAccountId];
transaction.destinationAccount = this.allAccounts[transaction.destinationAccountId];
transaction.category = this.allCategories[transaction.categoryId];
transaction.tags = [];
if (transaction.tagIds && transaction.tagIds.length) {
for (let j = 0; j < transaction.tagIds.length; j++) {
const tag = this.allTags[transaction.tagIds[j]];
if (tag) {
transaction.tags.push(tag);
}
}
}
const transactionYear = this.$utilities.getYear(transactionTime);
const transactionMonth = this.$utilities.getMonth(transactionTime);
const transactionYearMonth = this.$utilities.getYearAndMonth(transactionTime);
if (currentMonthList && currentMonthList.year === transactionYear && currentMonthList.month === transactionMonth) {
currentMonthList.items.push(transaction);
this.calculateMonthTotalAmount(currentMonthList, true);
continue;
}
for (let j = currentMonthListIndex + 1; j < this.transactions.length; j++) {
if (this.transactions[j].year === transactionYear && this.transactions[j].month === transactionMonth) {
currentMonthListIndex = j;
currentMonthList = this.transactions[j];
if (j > 0) {
this.calculateMonthTotalAmount(this.transactions[j - 1], false);
}
break;
}
}
if (!currentMonthList && this.transactions.length > 0) {
this.calculateMonthTotalAmount(this.transactions[this.transactions.length - 1], false);
}
if (!currentMonthList || currentMonthList.year !== transactionYear || currentMonthList.month !== transactionMonth) {
this.calculateMonthTotalAmount(currentMonthList, false);
this.transactions.push({
year: transactionYear,
month: transactionMonth,
yearMonth: transactionYearMonth,
opened: autoExpand,
items: []
});
currentMonthListIndex = this.transactions.length - 1;
currentMonthList = this.transactions[this.transactions.length - 1];
}
currentMonthList.items.push(transaction);
this.calculateMonthTotalAmount(currentMonthList, true);
}
}
if (result.nextTimeSequenceId) {
this.maxTime = result.nextTimeSequenceId;
} else {
this.calculateMonthTotalAmount(this.transactions[this.transactions.length - 1], false);
this.maxTime = -1;
}
},
calculateMonthTotalAmount(transactionMonthList, incomplete) {
if (!transactionMonthList) {
return;
}
let totalExpense = 0;
let totalIncome = 0;
let hasUnCalculatedTotalExpense = false;
let hasUnCalculatedTotalIncome = false;
for (let i = 0; i < transactionMonthList.items.length; i++) {
const transaction = transactionMonthList.items[i];
if (!transaction.sourceAccount) {
continue;
}
let amount = transaction.sourceAmount;
if (transaction.sourceAccount.currency !== this.defaultCurrency) {
const balance = this.$exchangeRates.getOtherCurrencyAmount(amount, transaction.sourceAccount.currency, this.defaultCurrency);
if (!this.$utilities.isNumber(balance)) {
if (transaction.type === this.$constants.transaction.allTransactionTypes.Expense) {
hasUnCalculatedTotalExpense = true;
} else if (transaction.type === this.$constants.transaction.allTransactionTypes.Income) {
hasUnCalculatedTotalIncome = true;
}
continue;
}
amount = Math.floor(balance);
}
if (transaction.type === this.$constants.transaction.allTransactionTypes.Expense) {
totalExpense += amount;
} else if (transaction.type === this.$constants.transaction.allTransactionTypes.Income) {
totalIncome += amount;
} else if (transaction.type === this.$constants.transaction.allTransactionTypes.Transfer && this.query.accountId) {
if (this.query.accountId === transaction.sourceAccountId) {
totalExpense += amount;
} else if (this.query.accountId === transaction.destinationAccountId) {
totalIncome += amount;
}
}
}
transactionMonthList.totalAmount = {
expense: totalExpense,
incompleteExpense: incomplete || hasUnCalculatedTotalExpense,
income: totalIncome,
incompleteIncome: incomplete || hasUnCalculatedTotalIncome
};
}
},
filters: {
transactionTypeName(type, allTransactionTypes) {
if (type === allTransactionTypes.Income) {
return 'Income';
} else if (type === allTransactionTypes.Expense) {
return 'Expense';
} else if (type === allTransactionTypes.Income) {
return 'Transfer';
} else {
return 'Transaction';
}
},
transactionDomId(transaction) {
return 'transaction_' + transaction.id;
},
transactionDateStyle(transaction, previousTransaction) {
if (!previousTransaction || transaction.day !== previousTransaction.day) {
return {};
}
return {
color: 'transparent'
}
},
dateName(dateType, defaultName) {
switch (dateType){
case 1:
return 'Today';
case 2:
return 'Yesterday';
case 3:
return 'Recent 7 days';
case 4:
return 'Recent 30 days';
case 5:
return 'This week';
case 6:
return 'Last week';
case 7:
return 'This month';
case 8:
return 'Last month';
case 9:
return 'This year';
case 10:
return 'Last year';
case 11:
return 'Custom Date';
default:
return defaultName;
}
},
typeName(type, defaultName) {
switch (type){
case 1:
return 'Modify Balance';
case 2:
return 'Income';
case 3:
return 'Expense';
case 4:
return 'Transfer';
default:
return defaultName;
}
},
categoryName(categoryId, allCategories, defaultName) {
if (allCategories[categoryId]) {
return allCategories[categoryId].name;
}
return defaultName;
},
accountName(accountId, allAccounts, defaultName) {
if (allAccounts[accountId]) {
return allAccounts[accountId].name;
}
return defaultName;
},
income(value, incomplete) {
return '+' + value + (incomplete ? '+' : '');
},
expense(value, incomplete) {
return '-' + value + (incomplete ? '+' : '');
}
}
};
</script>
<style>
.transaction-month-card-chevron-icon {
color: var(--f7-list-chevron-icon-color);
font-size: var(--f7-list-chevron-icon-font-size);
font-weight: bolder;
}
.transaction-amount-statistics > span {
margin-left: 4px;
}
.transaction-info .item-media + .item-inner {
margin-left: 10px;
}
.transaction-date {
margin-right: 6px;
}
.transaction-day {
opacity: 0.6;
font-size: 16px;
font-weight: bold;
text-align: left;
}
.transaction-day-of-week {
opacity: 0.6;
font-size: 12px;
text-align: center;
}
.transaction-comment {
font-size: 13px;
line-height: 20px;
padding-top: 2px;
padding-bottom: 2px;
}
.transaction-footer {
padding-top: 4px;
}
.transaction-info .item-text + .item-footer .transaction-footer {
padding-top: 2px;
}
.transaction-footer > span {
margin-right: 4px;
}
.transaction-amount {
color: var(--f7-list-item-after-text-color);
}
.transaction-info .item-inner:after {
background-color: transparent;
}
.transaction-info .transaction-icon:after {
content: '';
position: absolute;
background-color: var(--f7-list-item-border-color);
display: block;
z-index: 15;
top: auto;
right: auto;
bottom: 0;
height: 1px;
width: 100%;
transform-origin: 50% 100%;
transform: scaleY(calc(1 / var(--f7-device-pixel-ratio)));
}
.date-popover-menu .popover-inner, .category-popover-menu .popover-inner, .account-popover-menu .popover-inner {
max-height: 400px;
overflow-Y: auto;
}
</style>