Files
vaquant/src/views/accounts/AccountItem.vue
2019-10-25 19:37:28 +02:00

561 lines
15 KiB
Vue

<template>
<section class="hero is-fullheight account-item" v-if="account">
<account-encrypted v-if="isEncrypted" />
<div class="account-item-container">
<h2 class="title is-2">
{{ account.name }}
<router-link
v-if="isAdmin"
class="tag"
:to="{ name: 'account-setting', params: { id } }"
>
<awe-icon icon="cog" />
</router-link>
</h2>
<div class="columns is-centered" v-if="showShare">
<div class="column is-one-third">
<account-share :account="account" />
</div>
</div>
<article
class="message is-warning"
v-if="transactionWithoutExchange.length"
>
<div class="message-header">
<p>Attention</p>
</div>
<div class="message-body">
Certaines dépenses ne sont pas comptées car en attente de récupérer
les taux pratiqués à leur date respectives.
<ul>
<li v-for="(transaction, k) in transactionWithoutExchange" :key="k">
-
<router-link
:to="{ name: 'transaction', params: { id: transaction._id } }"
>{{ transaction.name }}</router-link
>
</li>
</ul>
<online-view @online="retrieveExchange">
<button
@click="retrieveExchange"
class="button is-success is-large is-rounded"
:class="{ 'is-loading': retrievingExchange }"
>
Récupérer les devises
</button>
</online-view>
</div>
</article>
<div class="tabs is-toggle is-fullwidth" v-if="transactions.length">
<ul>
<li :class="{ 'is-active': activeTab === 'transaction' }">
<a href="#" @click.prevent="toggleTab('transaction')">
<awe-icon icon="dollar-sign" />
</a>
</li>
<li :class="{ 'is-active': activeTab === 'tag' }" v-if="tagCount > 1">
<a href="#" @click.prevent="toggleTab('tag')">
<awe-icon icon="tag" />
</a>
</li>
<li
:class="{ 'is-active': activeTab === 'balance' }"
v-if="isMultiUser"
>
<a href="#" @click.prevent="toggleTab('balance')">
<awe-icon icon="balance-scale" />
</a>
</li>
<li
:class="{ 'is-active': activeTab === 'refund' }"
v-if="isMultiUser"
>
<a href="#" @click.prevent="toggleTab('refund')">
<awe-icon icon="exchange-alt" />
</a>
</li>
</ul>
</div>
<transition-group
:name="transitionName"
tag="div"
mode="out-in"
v-if="transactions.length"
>
<div
key="transaction"
class="transaction-panel tab-content columns is-centered"
v-if="activeTab === 'transaction'"
>
<div class="column">
<account-transaction-list
:account="account"
:transactions="transactions"
/>
</div>
</div>
<div
key="tag"
class="tag-panel tab-content columns is-centered"
v-if="activeTab === 'tag'"
>
<div class="column">
<tag-list
:transactions="transactionWithoutRefund"
:account="account"
/>
</div>
</div>
<div
key="balance"
class="balance-panel tab-content columns is-centered"
v-if="activeTab === 'balance'"
>
<div class="column">
<chart-balance
:stats="userStats"
:currency="account.mainCurrency"
/>
</div>
</div>
<div
key="refund"
class="refund-panel tab-content columns is-centered"
v-if="activeTab === 'refund'"
>
<div class="column">
<div
v-if="userRefunds.length"
class="columns is-centered is-multiline"
>
<div
class="column is-one-fifth"
v-for="(refund, k) in userRefunds"
:key="`${refund.from.alias}-${refund.to.alias}`"
>
<refund-transaction
:refund="userRefunds[k]"
:account="account"
/>
</div>
</div>
<div v-else class="equilibrium">Le compte est à l'équilibre !</div>
</div>
</div>
</transition-group>
</div>
<fab-button
v-if="!account.archive"
:to="{ name: 'transaction-new', params: { id: account._id } }"
:button-style="backgroundColor"
:margin="true"
>
<awe-icon icon="money-bill-wave" />
<span slot="fulltext">dépense</span>
</fab-button>
</section>
</template>
<script lang="ts">
import { Component, Watch } from 'vue-property-decorator'
import { Action, Getter } from 'vuex-class'
import BaseAccount from '@/base-components/BaseAccount'
import bus, { SYNC } from '@/utils/bus-event'
import notif from '@/utils/notif'
import TransactionType from '@/enums/TransactionType'
import accountService from '@/services/AccountService'
import chartService from '@/services/ChartService'
import balanceService from '@/services/BalanceService'
import transactionService from '@/services/TransactionService'
import exchangeService from '@/services/ExchangeService'
import IAccount from '@/models/IAccount'
import IBalance from '@/models/IBalance'
import ICurrency from '@/models/ICurrency'
import IRefund from '@/models/IRefund'
import IStat from '@/models/IStat'
import ITransaction from '@/models/ITransaction'
import IUser from '@/models/IUser'
import { primary, main, findContrastColor, findDarkValue } from '@/utils'
@Component({
components: {
'account-share': () => import('@/components/AccountShare.vue'),
'account-encrypted': () => import('@/components/AccountEncrypted.vue'),
'account-transaction-list': () =>
import('@/components/AccountTransactionList.vue'),
'tag-list': () => import('@/components/TagList.vue'),
'chart-balance': () => import('@/components/ChartBalance.vue'),
'online-view': () => import('@/components/OnlineView.vue'),
'refund-transaction': () => import('@/components/RefundTransaction.vue'),
'fab-button': () => import('@/components/FabButton.vue')
}
})
export default class AccountItem extends BaseAccount {
@Action
public setTitle!: any
public retrieveTransactions: boolean = false
public transactions: ITransaction[] = []
public activeTab: string = 'transaction'
public retrievingExchange: boolean = false
public transitionName: string = ''
public async getData(docIds?: string[]): Promise<void> {
if (docIds && !docIds.find((i: string) => i === this.id)) {
return
}
try {
if (this.id) {
this.account = await accountService.get(this.id)
if (!this.account) {
notif.error(`le compte ${this.id} n'existe pas`)
this.$router.push({ name: 'home' })
return
}
this.transactions = await transactionService.getAllByAccountId(this.id)
this.retrieveTransactions = true
if (document && document.documentElement) {
if (this.account.color) {
document.documentElement.style.setProperty(
'--primary-color',
this.account.color
)
document.documentElement.style.setProperty(
'--primary-font-color',
findDarkValue(this.account.color) || main
)
} else {
document.documentElement.style.setProperty(
'--primary-color',
primary
)
document.documentElement.style.setProperty(
'--primary-font-color',
main
)
}
}
this.setTitle(this.account.name)
}
} catch (error) {
// tslint:disable-next-line
console.error({ error })
this.$router.push({ name: 'home' })
return
}
}
public toggleTab(tab: string): void {
this.activeTab = tab
}
public async retrieveExchange(): Promise<void> {
if (!this.transactionWithoutExchange.length) {
return
}
this.retrievingExchange = true
try {
const transactions: ITransaction[] = [...this.transactionWithoutExchange]
for (const transaction of transactions) {
transaction.exchange = await exchangeService.get(
transaction.mainCurrency.code,
transaction.date
)
}
await transactionService.multipleSave(transactions)
notif.success('Dépenses mises à jour.')
} catch (error) {
// tslint:disable-next-line
console.warn({ error })
}
this.retrievingExchange = false
}
public totalCostByUserAlias(alias: string): number {
if (!this.account) {
return 0
}
const transactions: ITransaction[] = this.transactionWithExchange
return transactionService.getTotalCost(
transactions,
this.account.mainCurrency.code,
alias
)
}
public get totalUserCost(): Array<{ alias: string; total: number }> {
if (!this.account) {
return []
}
return this.account.users
.map((user: IUser) => ({
alias: user.alias || '',
total: this.totalCostByUserAlias(user.alias || '')
}))
.filter((user) => user.total > 0)
}
public get showShare(): boolean {
return !!(
this.account &&
this.account.users.length > 1 &&
this.retrieveTransactions &&
!this.transactions.length
)
}
public get transactionWithoutRefund(): ITransaction[] {
if (!this.transactions) {
return []
}
return this.transactions.filter(
(transaction: ITransaction) =>
transaction.transactionType === TransactionType.normal
)
}
public get tagCount(): number {
const tags = new Set(
this.transactionWithoutRefund.map(
(transaction: ITransaction) => transaction.tag
)
)
return tags.size
}
public get transactionWithExchange(): ITransaction[] {
if (!this.transactions) {
return []
}
return this.transactions.filter(
(transaction: ITransaction) => !!transaction.exchange
)
}
public get transactionWithoutExchange(): ITransaction[] {
if (!this.transactions) {
return []
}
return this.transactions.filter(
(transaction: ITransaction) => !transaction.exchange
)
}
public get userStats(): IStat[] {
const totalCost: number = this.totalCost
if (!this.account || totalCost === 0) {
return []
}
const account: IAccount = this.account
const transactions: ITransaction[] = this.transactionWithExchange
const mainCurrency: ICurrency = account.mainCurrency
return this.account.users.map((user: IUser) => {
const userCost: number = transactionService.getTotalCost(
transactions,
account.mainCurrency.code,
user.alias
)
const percent: number = Math.round((userCost / totalCost) * 100)
return {
label: user.alias || '',
value: userCost,
percent,
balance: balanceService.getBalanceByUser(
user,
mainCurrency,
transactions
)
}
})
}
public get myTotalCost(): number {
if (!this.account) {
return 0
}
const transactions: ITransaction[] = this.transactionWithExchange
return transactionService.getTotalCost(
transactions,
this.account.mainCurrency.code,
this.userAlias
)
}
public get totalCost(): number {
if (!this.account) {
return 0
}
const transactions: ITransaction[] = this.transactionWithExchange
return transactionService.getTotalCost(
transactions,
this.account.mainCurrency.code
)
}
public get userBalance(): IBalance[] {
if (!this.account || !this.account.users) {
return []
}
const mainCurrency: ICurrency = this.account.mainCurrency
const transactions: ITransaction[] = this.transactionWithExchange
return this.account.users.map((user: IUser) =>
balanceService.getBalanceByUser(user, mainCurrency, transactions)
)
}
public get userRefunds(): IRefund[] {
if (!this.account) {
return []
}
return balanceService.getRefund(this.userBalance)
}
public get isAdmin(): boolean {
if (!this.account || !this.account.admin || !this.user) {
return true
}
return this.account.admin.slugEmail === this.user.slugEmail
}
public get userAlias(): string {
if (!this.account || !this.user) {
return ''
}
const mainUser = this.user
const user = this.account.users.find(
(u: IUser) => u.slugEmail === mainUser.slugEmail
)
return (user && user.alias) || ''
}
public get isEncrypted(): boolean {
return !!(this.account && this.account.name.length > 30)
}
public get backgroundColor(): any | null {
if (!this.account || !this.account.color) {
return null
}
return {
backgroundColor: this.account.color,
color: findContrastColor(this.account.color) || 'black'
}
}
public get isMultiUser(): boolean {
return !!this.account && this.account.users.length > 1
}
@Watch('activeTab')
public onActiveTabChange(newTab: string, oldTab: string): void {
const tabs: string[] = ['transaction', 'tag', 'balance', 'refund']
this.transitionName =
tabs.indexOf(newTab) > tabs.indexOf(oldTab) ? 'slide-left' : 'slide-right'
}
}
</script>
<style lang="scss">
@import '../../styles/variables';
.account-item {
font-size: 14pt;
h2.title,
a {
color: var(--primary-font-color);
}
table tr.is-selected {
background-color: var(--primary-color);
color: var(--primary-font-color);
}
.tabs.is-toggle {
max-width: 400pt;
margin: auto;
li.is-active {
a {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: var(--primary-font-color);
}
}
}
.tabs.is-toggle {
a {
height: 35pt;
width: 35pt;
border-radius: 50%;
margin: auto;
border-width: 2px;
font-size: 14pt;
}
li:first-child,
li:last-child {
a {
border-radius: 50%;
}
}
}
&.hero {
&.is-fullheight {
min-height: calc(100vh - 52px);
}
}
.equilibrium {
font-size: 25pt;
color: var(--primary-font-color);
}
table.table {
margin: auto;
}
th,
.pay-by {
text-align: center;
}
.numeric {
text-align: right;
}
.table-container {
overflow-x: auto;
}
.total-panel {
font-size: 16pt;
}
}
.no-transaction {
font-size: 16pt;
}
.tab-content {
position: absolute;
width: calc(100% - 30px);
margin: auto;
transition: opacity 0.5s cubic-bezier(0.55, 0, 0.1, 1),
transform 0.5s cubic-bezier(0.55, 0, 0.1, 1);
&.columns {
&:last-child {
margin-bottom: 60px;
}
}
}
.slide-left-enter,
.slide-right-leave-active {
opacity: 0;
transform: translate(15px, 0);
}
.slide-left-leave-active,
.slide-right-enter {
opacity: 0;
transform: translate(-15px, 0);
}
.table {
th,
td {
vertical-align: middle;
}
}
.account-item-container {
flex: 1;
}
</style>