Files
firefly-iii/frontend/src/components/accounts/Index.vue
James Cole eb4b9f0599 Merge pull request #4849 from hamza-fadil/main
Replace i tag with em to fix minor issues in Sonar
2021-06-11 06:13:21 +02:00

596 lines
24 KiB
Vue

<!--
- Index.vue
- Copyright (c) 2020 james@firefly-iii.org
-
- This file is part of Firefly III (https://github.com/firefly-iii).
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<b-pagination
v-model="currentPage"
:total-rows="total"
:per-page="perPage"
aria-controls="my-table"
></b-pagination>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="card">
<div class="card-body p-0">
<b-table id="my-table" striped hover responsive="md" primary-key="id" :no-local-sorting="false"
:items="accounts" :fields="fields"
:per-page="perPage"
sort-icon-left
ref="table"
:current-page="currentPage"
:busy.sync="loading"
:sort-by.sync="sortBy"
:sort-desc.sync="sortDesc"
>
<template #table-busy>
<i class="fas fa-spinner fa-spin"></i>
</template>
<template #cell(name)="data">
<a :class="false === data.item.active ? 'text-muted' : ''" :href="'./accounts/show/' + data.item.id" :title="data.value">{{ data.value }}</a>
</template>
<template #cell(acct_number)="data">
{{ data.item.acct_number }}
</template>
<template #cell(last_activity)="data">
<span v-if="'asset' === type && 'loading' === data.item.last_activity">
<i class="fas fa-spinner fa-spin"></i>
</span>
<span v-if="'asset' === type && 'none' === data.item.last_activity" class="text-muted">
{{ $t('firefly.never') }}
</span>
<span v-if="'asset' === type && 'loading' !== data.item.last_activity && 'none' !== data.item.last_activity">
{{ data.item.last_activity }}
</span>
</template>
<template #cell(amount_due)="data">
<span class="text-success" v-if="parseFloat(data.item.amount_due) > 0">
{{ Intl.NumberFormat('en-US', {style: 'currency', currency: data.item.currency_code}).format(data.item.amount_due) }}
</span>
<span class="text-danger" v-if="parseFloat(data.item.amount_due) < 0">
{{ Intl.NumberFormat('en-US', {style: 'currency', currency: data.item.currency_code}).format(data.item.amount_due) }}
</span>
<span class="text-muted" v-if="parseFloat(data.item.amount_due) === 0.0">
{{ Intl.NumberFormat('en-US', {style: 'currency', currency: data.item.currency_code}).format(data.item.amount_due) }}
</span>
</template>
<template #cell(current_balance)="data">
<span class="text-success" v-if="parseFloat(data.item.current_balance) > 0">
{{
Intl.NumberFormat('en-US', {
style: 'currency', currency:
data.item.currency_code
}).format(data.item.current_balance)
}}
</span>
<span class="text-danger" v-if="parseFloat(data.item.current_balance) < 0">
{{
Intl.NumberFormat('en-US', {
style: 'currency', currency:
data.item.currency_code
}).format(data.item.current_balance)
}}
</span>
<span class="text-muted" v-if="0 === parseFloat(data.item.current_balance)">
{{
Intl.NumberFormat('en-US', {
style: 'currency', currency:
data.item.currency_code
}).format(data.item.current_balance)
}}
</span>
<span v-if="'asset' === type && 'loading' === data.item.balance_diff">
<span class="fas fa-spinner fa-spin"></span>
</span>
<span v-if="'asset' === type && 'loading' !== data.item.balance_diff">
(<span class="text-success" v-if="parseFloat(data.item.balance_diff) > 0">{{
Intl.NumberFormat('en-US', {
style: 'currency', currency:
data.item.currency_code
}).format(data.item.balance_diff)
}}</span><span class="text-muted" v-if="0===parseFloat(data.item.balance_diff)">{{
Intl.NumberFormat('en-US', {
style: 'currency', currency:
data.item.currency_code
}).format(data.item.balance_diff)
}}</span><span class="text-danger" v-if="parseFloat(data.item.balance_diff) < 0">{{
Intl.NumberFormat('en-US', {
style: 'currency', currency:
data.item.currency_code
}).format(data.item.balance_diff)
}}</span>)
</span>
</template>
<template #cell(interest)="data">
{{ parseFloat(data.item.interest) }}% ({{ data.item.interest_period }})
</template>
<template #cell(menu)="data">
<div class="btn-group btn-group-sm">
<div class="dropdown">
<button class="btn btn-light btn-sm dropdown-toggle" type="button" :id="'dropdownMenuButton' + data.item.id" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
{{ $t('firefly.actions') }}
</button>
<div class="dropdown-menu" :aria-labelledby="'dropdownMenuButton' + data.item.id">
<a class="dropdown-item" :href="'./accounts/edit/' + data.item.id"><span class="fa fas fa-pencil-alt"></span> {{ $t('firefly.edit') }}</a>
<a class="dropdown-item" :href="'./accounts/delete/' + data.item.id"><span class="fa far fa-trash"></span> {{ $t('firefly.delete') }}</a>
<a v-if="'asset' === type" class="dropdown-item" :href="'./accounts/reconcile/' + data.item.id + '/index'"><span class="fas fa-check"></span>
{{ $t('firefly.reconcile_this_account') }}</a>
</div>
</div>
</div>
</template>
</b-table>
</div>
<div class="card-footer">
<a :href="'./accounts/create/' + type" class="btn btn-success" :title="$t('firefly.create_new_' + type)">{{ $t('firefly.create_new_' + type) }}</a>
<a href="#" class="btn btn-info"><i class="fas fa-sync"></i></a>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<b-pagination
v-model="currentPage"
:total-rows="total"
:per-page="perPage"
aria-controls="my-table"
></b-pagination>
</div>
</div>
</div>
</template>
<script>
import {mapGetters} from "vuex";
import Sortable from "sortablejs";
import format from "date-fns/format";
import {setup} from 'axios-cache-adapter';
// import {cacheAdapterEnhancer} from 'axios-extensions';
import {configureAxios} from "../../shared/forageStore";
// get all and cache, dont keep the table busy.
export default {
name: "Index",
props: {
accountTypes: String
},
data() {
return {
accounts: [],
allAccounts: [],
type: 'all',
downloaded: false,
loading: false,
ready: false,
fields: [],
currentPage: 1,
perPage: 5,
total: 1,
sortBy: 'order',
sortDesc: false,
api: null,
sortableOptions: {
disabled: false,
chosenClass: 'is-selected',
onEnd: null
},
sortable: null
}
},
watch: {
storeReady: function () {
this.getAccountList();
},
start: function () {
this.getAccountList();
},
end: function () {
this.getAccountList();
},
orderMode: function (value) {
// update the table headers
this.updateFieldList();
// reorder the accounts:
this.reorderAccountList(value);
// make table sortable:
this.makeTableSortable(value);
},
activeFilter: function (value) {
this.filterAccountList();
}
},
computed: {
...mapGetters('root', ['listPageSize', 'cacheKey']),
...mapGetters('accounts/index', ['orderMode', 'activeFilter']),
...mapGetters('dashboard/index', ['start', 'end',]),
'indexReady': function () {
return null !== this.start && null !== this.end && null !== this.listPageSize && this.ready;
},
cardTitle: function () {
return this.$t('firefly.' + this.type + '_accounts');
}
},
created() {
let pathName = window.location.pathname;
let parts = pathName.split('/');
this.type = parts[parts.length - 1];
this.perPage = this.listPageSize ?? 51;
console.log('Per page: ' + this.perPage);
let params = new URLSearchParams(window.location.search);
this.currentPage = params.get('page') ? parseInt(params.get('page')) : 1;
this.updateFieldList();
this.ready = true;
// make object thing:
let token = document.head.querySelector('meta[name="csrf-token"]');
this.api = setup(
{
// `axios` options
//baseURL: './',
headers: {'X-CSRF-TOKEN': token.content, 'X-James': 'yes'},
// `axios-cache-adapter` options
cache: {
maxAge: 15 * 60 * 1000,
readHeaders: false,
exclude: {
query: false,
},
debug: true
}
});
},
methods: {
// itemsProvider: function (ctx, callback) {
// console.log('itemsProvider()');
// console.log('ctx.currentPage = ' + ctx.currentPage);
// console.log('this.currentPage = ' + this.currentPage);
// if (ctx.currentPage === this.currentPage) {
// let direction = this.sortDesc ? '-' : '+';
// let url = 'api/v1/accounts?type=' + this.type + '&page=' + ctx.currentPage + '&sort=' + direction + this.sortBy;
// this.api.get(url)
// .then(async (response) => {
// this.total = parseInt(response.data.meta.pagination.total);
// let items = this.parseAccountsAndReturn(response.data.data);
// items = this.filterAccountListAndReturn(items);
// callback(items);
// }
// );
// return null;
// }
// return [];
// },
saveAccountSort: function (event) {
let oldIndex = parseInt(event.oldIndex);
let newIndex = parseInt(event.newIndex);
let identifier = parseInt(event.item.attributes.getNamedItem('data-pk').value);
for (let i in this.accounts) {
if (this.accounts.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
let current = this.accounts[i];
// the actual account
if (current.id === identifier) {
let newOrder = parseInt(current.order) + (newIndex - oldIndex);
this.accounts[i].order = newOrder;
let url = './api/v1/accounts/' + current.id;
axios.put(url, {order: newOrder}).then(response => {
// TODO should update local account list, not refresh the whole thing.
this.getAccountList();
});
}
}
}
},
reorderAccountList: function (orderMode) {
if (orderMode) {
this.sortBy = 'order';
this.sortDesc = false;
}
},
makeTableSortable: function (orderMode) {
this.sortableOptions.disabled = !orderMode;
this.sortableOptions.onEnd = this.saveAccountSort;
// make sortable of table:
if (null === this.sortable) {
this.sortable = Sortable.create(this.$refs.table.$el.querySelector('tbody'), this.sortableOptions);
}
this.sortable.option('disabled', this.sortableOptions.disabled);
},
updateFieldList: function () {
this.fields = [];
this.fields = [{key: 'name', label: this.$t('list.name'), sortable: !this.orderMode}];
if ('asset' === this.type) {
this.fields.push({key: 'role', label: this.$t('list.role'), sortable: !this.orderMode});
}
if ('liabilities' === this.type) {
this.fields.push({key: 'liability_type', label: this.$t('list.liability_type'), sortable: !this.orderMode});
this.fields.push({key: 'liability_direction', label: this.$t('list.liability_direction'), sortable: !this.orderMode});
this.fields.push({key: 'interest', label: this.$t('list.interest') + ' (' + this.$t('list.interest_period') + ')', sortable: !this.orderMode});
}
// add the rest
this.fields.push({key: 'acct_number', label: this.$t('list.iban'), sortable: !this.orderMode});
this.fields.push({key: 'current_balance', label: this.$t('list.currentBalance'), sortable: !this.orderMode});
if ('liabilities' === this.type) {
this.fields.push({key: 'amount_due', label: this.$t('firefly.left_in_debt'), sortable: !this.orderMode});
}
if ('asset' === this.type || 'liabilities' === this.type) {
this.fields.push({key: 'last_activity', label: this.$t('list.lastActivity'), sortable: !this.orderMode});
}
this.fields.push({key: 'menu', label: ' ', sortable: false});
},
getAccountList: function () {
console.log('getAccountList()');
if (this.indexReady && !this.loading && !this.downloaded) {
console.log('Index ready, not loading and not already downloaded. Reset.');
this.loading = true;
this.perPage = this.listPageSize ?? 51;
this.accounts = [];
this.allAccounts = [];
this.downloadAccountList(1);
}
if (this.indexReady && !this.loading && this.downloaded) {
console.log('Index ready, not loading and not downloaded.');
this.loading = true;
this.filterAccountList();
}
},
downloadAccountList: function (page) {
console.log('downloadAccountList(' + page + ')');
configureAxios().then(async (api) => {
api.get('./api/v1/accounts?type=' + this.type + '&page=' + page + '&key=' + this.cacheKey)
.then(response => {
let currentPage = parseInt(response.data.meta.pagination.current_page);
let totalPage = parseInt(response.data.meta.pagination.total_pages);
this.total = parseInt(response.data.meta.pagination.total);
this.parseAccounts(response.data.data);
if (currentPage < totalPage) {
let nextPage = currentPage + 1;
this.downloadAccountList(nextPage);
}
if (currentPage >= totalPage) {
// console.log('Looks like all downloaded.');
this.downloaded = true;
this.filterAccountList();
}
}
);
});
},
filterAccountListAndReturn: function (allAccounts) {
console.log('filterAccountListAndReturn()');
let accounts = [];
for (let i in allAccounts) {
if (allAccounts.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
// 1 = active only
// 2 = inactive only
// 3 = both
if (1 === this.activeFilter && false === allAccounts[i].active) {
// console.log('Skip account #' + this.allAccounts[i].id + ' because not active.');
continue;
}
if (2 === this.activeFilter && true === allAccounts[i].active) {
// console.log('Skip account #' + this.allAccounts[i].id + ' because active.');
continue;
}
// console.log('Include account #' + this.allAccounts[i].id + '.');
accounts.push(allAccounts[i]);
}
}
return accounts;
},
filterAccountList: function () {
// console.log('filterAccountList()');
this.accounts = [];
for (let i in this.allAccounts) {
if (this.allAccounts.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
// 1 = active only
// 2 = inactive only
// 3 = both
if (1 === this.activeFilter && false === this.allAccounts[i].active) {
// console.log('Skip account #' + this.allAccounts[i].id + ' because not active.');
continue;
}
if (2 === this.activeFilter && true === this.allAccounts[i].active) {
// console.log('Skip account #' + this.allAccounts[i].id + ' because active.');
continue;
}
// console.log('Include account #' + this.allAccounts[i].id + '.');
this.accounts.push(this.allAccounts[i]);
}
}
this.total = this.accounts.length;
this.loading = false;
},
roleTranslate: function (role) {
if (null === role) {
return '';
}
return this.$t('firefly.account_role_' + role);
},
parsePages: function (data) {
this.total = parseInt(data.pagination.total);
console.log('Total is now ' + this.total);
},
// parseAccountsAndReturn: function (data) {
// console.log('In parseAccountsAndReturn()');
// let allAccounts = [];
// for (let key in data) {
// if (data.hasOwnProperty(key) && /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294) {
// let current = data[key];
// let acct = {};
// acct.id = parseInt(current.id);
// acct.order = current.attributes.order;
// acct.name = current.attributes.name;
// acct.active = current.attributes.active;
// acct.role = this.roleTranslate(current.attributes.account_role);
// acct.account_number = current.attributes.account_number;
// acct.current_balance = current.attributes.current_balance;
// acct.currency_code = current.attributes.currency_code;
//
// if ('liabilities' === this.type) {
// acct.liability_type = this.$t('firefly.account_type_' + current.attributes.liability_type);
// acct.liability_direction = this.$t('firefly.liability_direction_' + current.attributes.liability_direction + '_short');
// acct.interest = current.attributes.interest;
// acct.interest_period = this.$t('firefly.interest_calc_' + current.attributes.interest_period);
// acct.amount_due = current.attributes.current_debt;
// }
// acct.balance_diff = 'loading';
// acct.last_activity = 'loading';
//
// if (null !== current.attributes.iban) {
// acct.iban = current.attributes.iban.match(/.{1,4}/g).join(' ');
// }
// if (null === current.attributes.iban) {
// acct.iban = null;
// }
//
// allAccounts.push(acct);
// if ('asset' === this.type) {
// // TODO
// //this.getAccountBalanceDifference(this.allAccounts.length - 1, current);
// //this.getAccountLastActivity(this.allAccounts.length - 1, current);
// }
// }
// }
// return allAccounts;
// },
parseAccounts: function (data) {
// console.log('In parseAccounts()');
for (let key in data) {
if (data.hasOwnProperty(key) && /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294) {
let current = data[key];
let acct = {};
acct.id = parseInt(current.id);
acct.order = current.attributes.order;
acct.name = current.attributes.name;
acct.active = current.attributes.active;
acct.role = this.roleTranslate(current.attributes.account_role);
// account number in 'acct_number'
acct.acct_number = '';
let iban = null;
let acctNr = null;
acct.acct_number = '';
if (null !== current.attributes.iban) {
iban = current.attributes.iban.match(/.{1,4}/g).join(' ');
}
if (null !== current.attributes.account_number) {
acctNr = current.attributes.account_number;
}
// only account nr
if (null === iban && null !== acctNr) {
acct.acct_number = acctNr;
}
// only iban
if (null !== iban && null === acctNr) {
acct.acct_number = iban;
}
// both:
if (null !== iban && null !== acctNr) {
acct.acct_number = iban + ' (' + acctNr + ')';
}
acct.current_balance = current.attributes.current_balance;
acct.currency_code = current.attributes.currency_code;
if ('liabilities' === this.type) {
acct.liability_type = this.$t('firefly.account_type_' + current.attributes.liability_type);
acct.liability_direction = this.$t('firefly.liability_direction_' + current.attributes.liability_direction + '_short');
acct.interest = current.attributes.interest;
acct.interest_period = this.$t('firefly.interest_calc_' + current.attributes.interest_period);
acct.amount_due = current.attributes.current_debt;
}
acct.balance_diff = 'loading';
acct.last_activity = 'loading';
this.allAccounts.push(acct);
if ('asset' === this.type) {
this.getAccountBalanceDifference(this.allAccounts.length - 1, current);
this.getAccountLastActivity(this.allAccounts.length - 1, current);
}
}
}
},
getAccountLastActivity: function (index, acct) {
// console.log('getAccountLastActivity(' + index + ')');
// get single transaction for account:
// /api/v1/accounts/1/transactions?limit=1
axios.get('./api/v1/accounts/' + acct.id + '/transactions?limit=1&key=' + this.cacheKey).then(response => {
if (0 === response.data.data.length) {
this.allAccounts[index].last_activity = 'none';
return;
}
let date = new Date(response.data.data[0].attributes.transactions[0].date);
this.allAccounts[index].last_activity = format(date, this.$t('config.month_and_day_fns'));
});
},
getAccountBalanceDifference: function (index, acct) {
// console.log('getAccountBalanceDifference(' + index + ')');
// get account on day 0
let promises = [];
// add meta data to promise context.
promises.push(new Promise((resolve) => {
resolve(
{
account: acct,
index: index,
}
);
}));
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
promises.push(axios.get('./api/v1/accounts/' + acct.id + '?date=' + startStr + '&key=' + this.cacheKey));
promises.push(axios.get('./api/v1/accounts/' + acct.id + '?date=' + endStr + '&key=' + this.cacheKey));
Promise.all(promises).then(responses => {
let index = responses[0].index;
let startBalance = parseFloat(responses[1].data.data.attributes.current_balance);
let endBalance = parseFloat(responses[2].data.data.attributes.current_balance);
this.allAccounts[index].balance_diff = endBalance - startBalance;
});
},
}
}
</script>