feat!: rewrite frontend in Vue 3 with Pinia, DaisyUI, vite-plugin-pwa
Migrates every component from class-based vue-property-decorator to
<script setup> + Composition API. Replaces Vuex 3 + vuex-class with a
single Pinia store (persisted via pinia-plugin-persistedstate). Swaps
Bulma + bulma-{checkradio,switch,pricingtable} for DaisyUI 5 utilities
on Tailwind 4. Replaces register-service-worker with vite-plugin-pwa
(workbox, skipWaiting/clientsClaim preserved).
Plugins replaced:
- vue-class-component / vue-property-decorator -> <script setup>
- vuex / vuex-class / vuex-persist -> pinia + persistedstate
- vue-i18n 8 -> vue-i18n 11 (composition mode, legacy: false)
- vue-click-outside -> @vueuse/core onClickOutside
- @xkeshi/vue-qrcode -> qrcode.vue
- vue-currency-input 1 -> vue-currency-input 3 (composable wrapper)
- bus-event (Vue instance) -> mitt
- Vue filters -> plain functions imported per component
BREAKING: drops Stripe / pricing entirely (Payment, PricingTable,
/pricing route, vue-stripe-checkout, bulma-pricingtable).
Clears unused Cypress and Jest test scaffolding; leaves a Vitest
harness behind for future tests.
This commit is contained in:
@@ -1,569 +1,323 @@
|
||||
<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-title">
|
||||
<span :style="colorStyle()">{{ account.name }}</span>
|
||||
</h2>
|
||||
<div class="columns is-centered no-margin" 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">
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
:style="colorStyle(activeTab === 'transaction')"
|
||||
href="#"
|
||||
@click.prevent="toggleTab('transaction')"
|
||||
>
|
||||
<vaquant-icon icon="currency-dollar" />
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="transactions.length && locations.length">
|
||||
<a
|
||||
:style="colorStyle(activeTab === 'location')"
|
||||
href="#"
|
||||
@click.prevent="toggleTab('location')"
|
||||
>
|
||||
<vaquant-icon icon="world" />
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="transactions.length && tagCount > 1">
|
||||
<a
|
||||
:style="colorStyle(activeTab === 'tag')"
|
||||
href="#"
|
||||
@click.prevent="toggleTab('tag')"
|
||||
>
|
||||
<vaquant-icon icon="tag" />
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="transactions.length && isMultiUser">
|
||||
<a
|
||||
:style="colorStyle(activeTab === 'balance')"
|
||||
href="#"
|
||||
@click.prevent="toggleTab('balance')"
|
||||
>
|
||||
<vaquant-icon icon="scale" />
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="isAdmin">
|
||||
<a
|
||||
:style="colorStyle(activeTab === 'settings')"
|
||||
href="#"
|
||||
@click.prevent="toggleTab('settings')"
|
||||
>
|
||||
<vaquant-icon icon="settings" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
key="transaction"
|
||||
class="transaction-panel tab-content is-centered"
|
||||
v-if="activeTab === 'transaction'"
|
||||
>
|
||||
<account-transaction-list
|
||||
:account="account"
|
||||
:transactions="transactions"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
key="location"
|
||||
class="location-panel tab-content is-centered"
|
||||
v-if="locations.length && activeTab === 'location'"
|
||||
>
|
||||
<earth-map :locations="locations" />
|
||||
</div>
|
||||
<div
|
||||
key="tag"
|
||||
class="tag-panel tab-content is-centered"
|
||||
v-if="activeTab === 'tag'"
|
||||
>
|
||||
<tag-list :transactions="transactionWithoutRefund" :account="account" />
|
||||
</div>
|
||||
<div
|
||||
key="balance"
|
||||
class="balance-panel tab-content is-centered"
|
||||
v-if="activeTab === 'balance'"
|
||||
>
|
||||
<chart-balance
|
||||
:account="account"
|
||||
:stats="userStats"
|
||||
:currency="account.mainCurrency"
|
||||
/>
|
||||
<div
|
||||
v-if="userRefunds.length"
|
||||
class="refund-container no-margin 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 no-margin">
|
||||
<br />
|
||||
Le compte est à l'équilibre !
|
||||
</div>
|
||||
</div>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useBaseAccount } from '@/composables/useBaseAccount'
|
||||
import TransactionType from '@/enums/TransactionType'
|
||||
import queueNotifService from '@/services/QueueNotifService'
|
||||
import accountService from '@/services/AccountService'
|
||||
import balanceService from '@/services/BalanceService'
|
||||
import transactionService from '@/services/TransactionService'
|
||||
import exchangeService from '@/services/ExchangeService'
|
||||
import couchService from '@/services/CouchService'
|
||||
import type IBalance from '@/models/IBalance'
|
||||
import type ICurrency from '@/models/ICurrency'
|
||||
import type IRefund from '@/models/IRefund'
|
||||
import type IStat from '@/models/IStat'
|
||||
import type ITransaction from '@/models/ITransaction'
|
||||
import type IUser from '@/models/IUser'
|
||||
import type ILocation from '@/models/ILocation'
|
||||
import { primary, main, findContrastColor, findDarkValue } from '@/utils'
|
||||
|
||||
<div
|
||||
key="settings"
|
||||
class="settings-panel tab-content is-centered"
|
||||
v-if="activeTab === 'settings'"
|
||||
>
|
||||
<account-setting :account="account" />
|
||||
import AccountShare from '@/components/AccountShare.vue'
|
||||
import AccountTransactionList from '@/components/AccountTransactionList.vue'
|
||||
import EarthMap from '@/components/EarthMap.vue'
|
||||
import TagList from '@/components/TagList.vue'
|
||||
import ChartBalance from '@/components/ChartBalance.vue'
|
||||
import AccountSetting from '@/components/AccountSetting.vue'
|
||||
import OnlineView from '@/components/OnlineView.vue'
|
||||
import RefundTransaction from '@/components/RefundTransaction.vue'
|
||||
import FabButton from '@/components/FabButton.vue'
|
||||
|
||||
const props = defineProps<{ id: string }>()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const { user } = storeToRefs(userStore)
|
||||
|
||||
const retrieveTransactions = ref(false)
|
||||
const transactions = ref<ITransaction[]>([])
|
||||
const activeTab = ref<'transaction' | 'location' | 'tag' | 'balance' | 'settings'>(
|
||||
'transaction'
|
||||
)
|
||||
const retrievingExchange = ref(false)
|
||||
|
||||
const getData = async (docIds?: string[]): Promise<void> => {
|
||||
if (docIds && !docIds.find((i) => i === props.id)) return
|
||||
try {
|
||||
if (props.id) {
|
||||
account.value = await accountService.get(props.id)
|
||||
if (!account.value) {
|
||||
queueNotifService.error(`le compte ${props.id} n'existe pas`)
|
||||
router.push({ name: 'home' })
|
||||
return
|
||||
}
|
||||
if (account.value.isPublic) {
|
||||
userStore.addAccountId(props.id)
|
||||
couchService.initLive()
|
||||
}
|
||||
transactions.value = await transactionService.getAllByAccountId(props.id)
|
||||
retrieveTransactions.value = true
|
||||
if (document.documentElement) {
|
||||
if (account.value.color) {
|
||||
document.documentElement.style.setProperty('--primary-color', account.value.color)
|
||||
document.documentElement.style.setProperty(
|
||||
'--primary-font-color',
|
||||
findDarkValue(account.value.color) || main
|
||||
)
|
||||
} else {
|
||||
document.documentElement.style.setProperty('--primary-color', primary)
|
||||
document.documentElement.style.setProperty('--primary-font-color', main)
|
||||
}
|
||||
}
|
||||
userStore.setTitle(account.value.name)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error({ error })
|
||||
router.push({ name: 'home' })
|
||||
}
|
||||
}
|
||||
|
||||
const { account, colorStyle } = useBaseAccount(getData)
|
||||
|
||||
const toggleTab = (tab: typeof activeTab.value) => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
const retrieveExchange = async () => {
|
||||
if (!transactionWithoutExchange.value.length) return
|
||||
retrievingExchange.value = true
|
||||
try {
|
||||
const list = [...transactionWithoutExchange.value]
|
||||
for (const t of list) {
|
||||
t.exchange = await exchangeService.get(t.mainCurrency.code, t.date)
|
||||
}
|
||||
await transactionService.multipleSave(list)
|
||||
queueNotifService.success('Dépenses mises à jour.')
|
||||
} catch (error) {
|
||||
console.warn({ error })
|
||||
}
|
||||
retrievingExchange.value = false
|
||||
}
|
||||
|
||||
const transactionWithoutRefund = computed(() =>
|
||||
transactions.value.filter((t) => t.transactionType === TransactionType.normal)
|
||||
)
|
||||
const tagCount = computed(
|
||||
() => new Set(transactionWithoutRefund.value.map((t) => t.tag)).size
|
||||
)
|
||||
const transactionWithExchange = computed(() =>
|
||||
transactions.value.filter((t) => !!t.exchange)
|
||||
)
|
||||
const transactionWithoutExchange = computed(() =>
|
||||
transactions.value.filter((t) => !t.exchange)
|
||||
)
|
||||
|
||||
const userAlias = computed<string>(() => {
|
||||
if (!account.value || !user.value) return ''
|
||||
const mainUser = user.value
|
||||
const u = account.value.users.find((x) => x.slugEmail === mainUser.slugEmail)
|
||||
return u?.alias || ''
|
||||
})
|
||||
|
||||
const totalCost = computed(() => {
|
||||
if (!account.value) return 0
|
||||
return transactionService.getTotalCost(
|
||||
transactionWithExchange.value,
|
||||
account.value.mainCurrency.code
|
||||
)
|
||||
})
|
||||
|
||||
const userStats = computed<IStat[]>(() => {
|
||||
if (!account.value || totalCost.value === 0) return []
|
||||
const acc = account.value
|
||||
const list = transactionWithExchange.value
|
||||
const mainCurrency: ICurrency = acc.mainCurrency
|
||||
return acc.users.map((u: IUser) => {
|
||||
const userCost = transactionService.getTotalCost(list, acc.mainCurrency.code, u.alias)
|
||||
const percent = Math.round((userCost / totalCost.value) * 100)
|
||||
return {
|
||||
label: u.alias || '',
|
||||
value: userCost,
|
||||
percent,
|
||||
balance: balanceService.getBalanceByUser(u, mainCurrency, list)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const locations = computed<ILocation[]>(() =>
|
||||
transactions.value
|
||||
.filter((t) => t.location)
|
||||
.map((t) => t.location as ILocation)
|
||||
)
|
||||
|
||||
const userBalance = computed<IBalance[]>(() => {
|
||||
if (!account.value || !account.value.users) return []
|
||||
const mainCurrency: ICurrency = account.value.mainCurrency
|
||||
const list = transactionWithExchange.value
|
||||
return account.value.users.map((u) =>
|
||||
balanceService.getBalanceByUser(u, mainCurrency, list)
|
||||
)
|
||||
})
|
||||
|
||||
const userRefunds = computed<IRefund[]>(() =>
|
||||
account.value ? balanceService.getRefund(userBalance.value) : []
|
||||
)
|
||||
|
||||
const isAdmin = computed(() => {
|
||||
if (!account.value || !account.value.admin || !user.value) return true
|
||||
return account.value.admin.slugEmail === user.value.slugEmail
|
||||
})
|
||||
|
||||
const isMultiUser = computed(() => !!account.value && account.value.users.length > 1)
|
||||
const isEncrypted = computed(
|
||||
() => !!(account.value && account.value.name.length > 30)
|
||||
)
|
||||
|
||||
const showShare = computed(
|
||||
() =>
|
||||
!!account.value &&
|
||||
account.value.users.length > 1 &&
|
||||
retrieveTransactions.value &&
|
||||
!transactions.value.length
|
||||
)
|
||||
|
||||
const backgroundColor = computed(() => {
|
||||
if (!account.value || !account.value.color) return undefined
|
||||
return {
|
||||
backgroundColor: account.value.color,
|
||||
color: findContrastColor(account.value.color) || 'black'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="account" class="account-item p-4">
|
||||
<div v-if="isEncrypted" class="alert alert-warning mb-4">
|
||||
<span>Compte chiffré</span>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-center mb-4 account-title">
|
||||
<span :style="colorStyle()">{{ account.name }}</span>
|
||||
</h2>
|
||||
<div v-if="showShare" class="flex justify-center mb-4">
|
||||
<div class="w-full md:w-1/3">
|
||||
<AccountShare :account="account" />
|
||||
</div>
|
||||
</div>
|
||||
<fab-button
|
||||
<div v-if="transactionWithoutExchange.length" class="alert alert-warning mb-4">
|
||||
<div>
|
||||
<p class="font-bold">Attention</p>
|
||||
<p>
|
||||
Certaines dépenses ne sont pas comptées car en attente de récupérer
|
||||
les taux pratiqués à leur date respectives.
|
||||
</p>
|
||||
<ul class="list-disc list-inside">
|
||||
<li v-for="(t, k) in transactionWithoutExchange" :key="k">
|
||||
<router-link :to="{ name: 'transaction', params: { id: t._id } }">
|
||||
{{ t.name }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<OnlineView @online="retrieveExchange">
|
||||
<button
|
||||
class="btn btn-success btn-lg mt-2"
|
||||
:class="{ 'btn-loading': retrievingExchange }"
|
||||
@click="retrieveExchange"
|
||||
>
|
||||
Récupérer les devises
|
||||
</button>
|
||||
</OnlineView>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tablist" class="tabs tabs-boxed max-w-md mx-auto mb-4">
|
||||
<a
|
||||
role="tab"
|
||||
href="#"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': activeTab === 'transaction' }"
|
||||
:style="colorStyle(activeTab === 'transaction')"
|
||||
@click.prevent="toggleTab('transaction')"
|
||||
>
|
||||
<vaquant-icon icon="currency-dollar" />
|
||||
</a>
|
||||
<a
|
||||
v-if="transactions.length && locations.length"
|
||||
role="tab"
|
||||
href="#"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': activeTab === 'location' }"
|
||||
:style="colorStyle(activeTab === 'location')"
|
||||
@click.prevent="toggleTab('location')"
|
||||
>
|
||||
<vaquant-icon icon="world" />
|
||||
</a>
|
||||
<a
|
||||
v-if="transactions.length && tagCount > 1"
|
||||
role="tab"
|
||||
href="#"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': activeTab === 'tag' }"
|
||||
:style="colorStyle(activeTab === 'tag')"
|
||||
@click.prevent="toggleTab('tag')"
|
||||
>
|
||||
<vaquant-icon icon="tag" />
|
||||
</a>
|
||||
<a
|
||||
v-if="transactions.length && isMultiUser"
|
||||
role="tab"
|
||||
href="#"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': activeTab === 'balance' }"
|
||||
:style="colorStyle(activeTab === 'balance')"
|
||||
@click.prevent="toggleTab('balance')"
|
||||
>
|
||||
<vaquant-icon icon="scale" />
|
||||
</a>
|
||||
<a
|
||||
v-if="isAdmin"
|
||||
role="tab"
|
||||
href="#"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': activeTab === 'settings' }"
|
||||
:style="colorStyle(activeTab === 'settings')"
|
||||
@click.prevent="toggleTab('settings')"
|
||||
>
|
||||
<vaquant-icon icon="settings" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'transaction'" class="mt-4">
|
||||
<AccountTransactionList :account="account" :transactions="transactions" />
|
||||
</div>
|
||||
<div v-if="activeTab === 'location' && locations.length" class="mt-4">
|
||||
<EarthMap :locations="locations" />
|
||||
</div>
|
||||
<div v-if="activeTab === 'tag'" class="mt-4">
|
||||
<TagList :transactions="transactionWithoutRefund" :account="account" />
|
||||
</div>
|
||||
<div v-if="activeTab === 'balance'" class="mt-4">
|
||||
<ChartBalance :account="account" :stats="userStats" :currency="account.mainCurrency" />
|
||||
<div v-if="userRefunds.length" class="grid md:grid-cols-3 gap-4 mt-4">
|
||||
<div v-for="refund in userRefunds" :key="`${refund.from.alias}-${refund.to.alias}`">
|
||||
<RefundTransaction :refund="refund" :account="account" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center text-2xl mt-6">
|
||||
Le compte est à l'équilibre !
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="activeTab === 'settings'" class="mt-4">
|
||||
<AccountSetting :account="account" />
|
||||
</div>
|
||||
|
||||
<FabButton
|
||||
v-if="!account.archive"
|
||||
:to="{ name: 'transaction-new', params: { id: account._id } }"
|
||||
:button-style="backgroundColor"
|
||||
:margin="true"
|
||||
>
|
||||
<vaquant-icon icon="cash" />
|
||||
<span slot="fulltext">dépense</span>
|
||||
</fab-button>
|
||||
<template #fulltext>dépense</template>
|
||||
</FabButton>
|
||||
</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 TransactionType from '@/enums/TransactionType'
|
||||
import queueNotifService from '@/services/QueueNotifService'
|
||||
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'
|
||||
import couchService from '@/services/CouchService'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'account-share': () => import('@/components/AccountShare.vue'),
|
||||
'account-transaction-list': () =>
|
||||
import('@/components/AccountTransactionList.vue'),
|
||||
'earth-map': () => import('@/components/EarthMap.vue'),
|
||||
'tag-list': () => import('@/components/TagList.vue'),
|
||||
'chart-balance': () => import('@/components/ChartBalance.vue'),
|
||||
'account-setting': () => import('@/components/AccountSetting.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
|
||||
@Action
|
||||
public addAccountId!: any
|
||||
public retrieveTransactions: boolean = false
|
||||
public transactions: ITransaction[] = []
|
||||
public activeTab: string = 'transaction'
|
||||
public retrievingExchange: boolean = false
|
||||
|
||||
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) {
|
||||
queueNotifService.error(`le compte ${this.id} n'existe pas`)
|
||||
this.$router.push({ name: 'home' })
|
||||
return
|
||||
}
|
||||
|
||||
if (this.account.isPublic) {
|
||||
this.addAccountId({ accountId: this.id })
|
||||
couchService.initLive()
|
||||
}
|
||||
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)
|
||||
queueNotifService.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 locations() {
|
||||
return this.transactions
|
||||
.filter((transaction) => transaction.location)
|
||||
.map((transaction) => transaction.location)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../styles/variables';
|
||||
|
||||
.account-item {
|
||||
font-size: 14pt;
|
||||
|
||||
h2.title,
|
||||
a {
|
||||
color: var(--primary-font-color);
|
||||
}
|
||||
.account-title {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.account-title {
|
||||
span,
|
||||
a {
|
||||
padding: 0 10px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs.is-toggle {
|
||||
max-width: 400pt;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.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: 100%;
|
||||
margin: 10px 0;
|
||||
&.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>
|
||||
|
||||
Reference in New Issue
Block a user