master: change repo
This commit is contained in:
49
src/components/AccountEncrypted.vue
Normal file
49
src/components/AccountEncrypted.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class='account-encrypted'>
|
||||
Le compte est pour l'instant chiffré, la clé sera récupérée dès qu'une
|
||||
connexion internet est établie.
|
||||
<button class="button is-link is-rounded is-loading" v-if="fetching"></button>
|
||||
<online-view @online="fetchKey" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
import { Getter } from 'vuex-class'
|
||||
import axios from 'axios'
|
||||
import { GET_KEY_URL } from '@/utils/serverless-url'
|
||||
import bus, { SYNC } from '@/utils/bus-event'
|
||||
import IUser from '@/models/IUser'
|
||||
import couchService from '@/services/CouchService'
|
||||
import notif from '@/utils/notif'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'online-view': () => import('@/components/OnlineView.vue')
|
||||
}
|
||||
})
|
||||
export default class AccountEncrypted extends Vue {
|
||||
@Getter
|
||||
public user!: IUser
|
||||
@Prop({ type: String, required: true })
|
||||
public id!: string
|
||||
public fetching: boolean = false
|
||||
|
||||
public async fetchKey(): Promise<void> {
|
||||
if (!this.user) {
|
||||
return
|
||||
}
|
||||
this.fetching = true
|
||||
const retrieved: boolean = await couchService.retrievePassword(this.id || '', this.user)
|
||||
if (retrieved) {
|
||||
bus.$emit(SYNC, [this.id])
|
||||
} else {
|
||||
notif.alert(`
|
||||
Impossible de récupérer la clé,
|
||||
veuillez réessayer plus tard...
|
||||
`)
|
||||
}
|
||||
this.fetching = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
109
src/components/AccountList.vue
Normal file
109
src/components/AccountList.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="account-list">
|
||||
<div class="columns is-centered is-multiline" v-if="accounts.length">
|
||||
<div class="column is-2" v-for="(account, k) in accounts" :key="k">
|
||||
<div class="box" :style="getColor(account.color)" @click="goToAccount(account._id)">
|
||||
<router-link
|
||||
:class="{ 'is-primary': !account.color, 'is-white': account.color }"
|
||||
:style="`color: ${findDarkValue(account.color)}`"
|
||||
:to="{ name: 'account', params: { id: account._id } }"
|
||||
>{{ account.name }}</router-link>
|
||||
<div v-if="account.isPublic">
|
||||
<awe-icon icon="users" />
|
||||
</div>
|
||||
<hr v-else />
|
||||
{{ account.users && account.users.map(u => u.alias).join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="fetched">
|
||||
<awe-icon icon="inbox" />Les comptes s'afficheront ici.
|
||||
</div>
|
||||
<fab-button :to="{ name: 'account-new' }" v-if="user" :margin="true">
|
||||
<span slot="fulltext">créer un compte</span>
|
||||
</fab-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
import { Getter } from 'vuex-class'
|
||||
import { findDarkValue } from '@/utils'
|
||||
import bus, { SYNC } from '@/utils/bus-event'
|
||||
import accountService from '@/services/AccountService'
|
||||
import IAccount from '@/models/IAccount'
|
||||
import IColor from '@/models/IColor'
|
||||
import IUser from '@/models/IUser'
|
||||
import FabButton from '@/components/FabButton.vue'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'fab-button': FabButton
|
||||
}
|
||||
})
|
||||
export default class AccountList extends Vue {
|
||||
@Getter
|
||||
public user!: IUser | null
|
||||
@Prop({ type: Boolean, default: false })
|
||||
public archived!: boolean
|
||||
|
||||
public accounts: IAccount[] = []
|
||||
public fetched: boolean = false
|
||||
public textColor: string = ''
|
||||
public async created(): Promise<void> {
|
||||
this.getData()
|
||||
bus.$on(SYNC, this.getData)
|
||||
}
|
||||
|
||||
public async getData(): Promise<void> {
|
||||
this.accounts = await accountService.getAll(this.archived)
|
||||
this.fetched = true
|
||||
}
|
||||
|
||||
public getColor(color: string | null): any | null {
|
||||
if (!color) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
backgroundColor: color,
|
||||
color: this.findDarkValue(color)
|
||||
}
|
||||
}
|
||||
|
||||
public findDarkValue(color: string): string | null {
|
||||
return findDarkValue(color)
|
||||
}
|
||||
|
||||
public goToAccount(id: string): void {
|
||||
this.$router.push({ name: 'account', params: { id } })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../styles/variables';
|
||||
|
||||
.box {
|
||||
border-radius: 8px;
|
||||
word-wrap: break-word;
|
||||
hyphens: auto;
|
||||
|
||||
a {
|
||||
font-size: 1.2rem;
|
||||
color: $main;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
119
src/components/AccountShare.vue
Normal file
119
src/components/AccountShare.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<article class="account-share message is-success is-medium" v-if="show && user">
|
||||
<div class="message-header">
|
||||
<p>Partagez le compte</p>
|
||||
<button class="delete" v-if="canClose" aria-label="close" @click="hide"></button>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
<button class="button is-primary is-medium" @click="share" v-if="canShareAPI">Partager</button>
|
||||
<div class="field is-grouped is-grouped-centered" v-else>
|
||||
<div class="field" :class="{ 'has-addons': canClipboard }">
|
||||
<div class="control">
|
||||
<input class="input" type="text" readonly :value="accountUrl" />
|
||||
</div>
|
||||
<div class="control" if="canClipboard">
|
||||
<a href="#" class="button is-primary" @click.prevent="copy">Copier</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="qr-code">
|
||||
<p>ou via ce QR code :</p>
|
||||
<qr-code :value="accountUrl" />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
declare const navigator: any
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator'
|
||||
import { Getter } from 'vuex-class'
|
||||
import IAccount from '@/models/IAccount'
|
||||
import IUser from '@/models/IUser'
|
||||
import notif from '@/utils/notif'
|
||||
import QrCode from '@xkeshi/vue-qrcode'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'qr-code': QrCode
|
||||
}
|
||||
})
|
||||
export default class Home extends Vue {
|
||||
@Getter
|
||||
public user!: IUser | null
|
||||
@Prop({ type: Object, required: true })
|
||||
public account!: IAccount
|
||||
@Prop({ type: Boolean, default: true })
|
||||
public canClose!: boolean
|
||||
public show: boolean = true
|
||||
|
||||
public hide(): void {
|
||||
if (this.canClose) {
|
||||
this.show = false
|
||||
}
|
||||
}
|
||||
|
||||
public share(): void {
|
||||
if (this.canShareAPI) {
|
||||
navigator.share({
|
||||
title: `Vaquant - Compte ${this.account.name}`,
|
||||
text: `${this.username} vous invite sur Vaquant pour gérer vos dépenses sur le compte ${this.account.name}.`,
|
||||
url: this.accountUrl
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public async copy(): Promise<void> {
|
||||
if (this.canClipboard) {
|
||||
await navigator.clipboard.writeText(this.accountUrl)
|
||||
notif.confirm('Adresse copiée')
|
||||
}
|
||||
}
|
||||
|
||||
public get accountUrl(): string {
|
||||
let url = window.location.href.replace('/setting', '')
|
||||
if (this.account.isPublic) {
|
||||
url = `${url}/public`
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
public get username(): string {
|
||||
if (!this.user) {
|
||||
return ''
|
||||
}
|
||||
return (
|
||||
`${this.user.firstname} ${this.user.lastname}`.trim() || this.user.email
|
||||
)
|
||||
}
|
||||
|
||||
public get canShareAPI(): boolean {
|
||||
return !!navigator.share
|
||||
}
|
||||
|
||||
public get canClipboard(): boolean {
|
||||
return !!navigator.clipboard
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
$padding-fab: 24px;
|
||||
|
||||
.is-fab {
|
||||
bottom: $padding-fab;
|
||||
position: fixed;
|
||||
right: $padding-fab;
|
||||
}
|
||||
|
||||
.fab-margin {
|
||||
margin: calc(#{$padding-fab} * 2) 0;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
canvas {
|
||||
margin: 1em 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
138
src/components/AccountTransactionList.vue
Normal file
138
src/components/AccountTransactionList.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="account-transaction-list" v-if="account">
|
||||
<div class="field has-addons has-addons-centered is-narrow" v-if="showFilter">
|
||||
<div class="control" v-show="filteredBy">
|
||||
<button type="submit" class="button" @click="filteredBy = ''">
|
||||
<awe-icon icon="times" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="filteredBy">
|
||||
<option value>Tout le monde</option>
|
||||
<option v-for="(user, k) in account.users" :key="k" :value="user.alias">{{ user.alias }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table is-hoverable is-striped">
|
||||
<thead v-if="head">
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<awe-icon icon="minus" />
|
||||
</th>
|
||||
<th scope="col" class="align">
|
||||
<awe-icon icon="user" />
|
||||
</th>
|
||||
<th scope="col" class="align">
|
||||
<awe-icon icon="money-bill-wave" />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(transaction, k) in filteredTransactions"
|
||||
:key="k"
|
||||
:data-index="k"
|
||||
:id="transaction._id"
|
||||
>
|
||||
<td class="multiline">
|
||||
<router-link
|
||||
:to="{ name: 'transaction', params: { id: transaction._id } }"
|
||||
>{{ transaction.name }}</router-link>
|
||||
<br />
|
||||
<span class="date">{{ transaction.date | date }}</span>
|
||||
</td>
|
||||
<td class="pay-by">{{ transaction.payBy }}</td>
|
||||
<td class="numeric">{{ transaction.amount | moneypad(transaction.mainCurrency) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot v-if="foot">
|
||||
<tr class="is-selected">
|
||||
<td colspan="2" class="total">total</td>
|
||||
<td class="numeric">{{ total | moneypad(account.mainCurrency) }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
import IAccount from '@/models/IAccount'
|
||||
import ITransaction from '@/models/ITransaction'
|
||||
|
||||
@Component
|
||||
export default class AccountTransactionList extends Vue {
|
||||
@Prop({ type: Object, default: () => null })
|
||||
public account!: IAccount | null
|
||||
@Prop({ type: Array, required: true })
|
||||
public transactions!: ITransaction[]
|
||||
@Prop({ type: Boolean, default: true })
|
||||
public filter!: boolean
|
||||
@Prop({ type: Boolean, default: true })
|
||||
public head!: boolean
|
||||
@Prop({ type: Boolean, default: false })
|
||||
public foot!: boolean
|
||||
public filteredBy: string = ''
|
||||
|
||||
public get total(): number {
|
||||
return this.transactions.reduce(
|
||||
(a: number, transaction: ITransaction) => a + transaction.amount,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
public get filteredTransactions(): ITransaction[] {
|
||||
if (!this.filteredBy) {
|
||||
return this.transactions
|
||||
}
|
||||
return this.transactions.filter(
|
||||
(transaction: ITransaction) => transaction.payBy === this.filteredBy
|
||||
)
|
||||
}
|
||||
|
||||
public get isMultiUser(): boolean {
|
||||
const payBy = new Set(
|
||||
this.transactions.map((transaction: ITransaction) => transaction.payBy)
|
||||
)
|
||||
return payBy.size > 1
|
||||
}
|
||||
|
||||
public get showFilter(): boolean {
|
||||
return this.filter && this.isMultiUser
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.account-transaction-list {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table tr.is-selected {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--primary-font-color);
|
||||
}
|
||||
|
||||
.multiline {
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
th {
|
||||
&.align {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
src/components/AccountUserNew.vue
Normal file
81
src/components/AccountUserNew.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="account-user-new">
|
||||
<h3 class="subtitle is-3">{{ $tc('account.friends', localeUsers.length, { count: localeUsers.length }) }}</h3>
|
||||
<user-new v-if="user" v-model="user" />
|
||||
<div v-for="(user, k) in localeUsers" :key="k">
|
||||
<hr />
|
||||
<user-new v-model="localeUsers[k]" @remove="() => remove(k)" />
|
||||
</div>
|
||||
<br />
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="nouvel ami"
|
||||
v-model="newUserId"
|
||||
@keyup.enter.prevent="newUser"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" class="button is-primary" @click.prevent="newUser">ajouter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Model, Vue, Watch } from 'vue-property-decorator'
|
||||
import { Getter } from 'vuex-class'
|
||||
import IUser from '@/models/IUser'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'user-new': () => import('@/components/UserNew.vue')
|
||||
}
|
||||
})
|
||||
export default class HelloWorld extends Vue {
|
||||
@Getter
|
||||
public user!: IUser | null
|
||||
@Model('input', { type: Array, required: true })
|
||||
public users!: IUser[]
|
||||
public localeUsers: IUser[] = []
|
||||
public userId: string = ''
|
||||
public newUserId: string = ''
|
||||
|
||||
public mounted(): void {
|
||||
this.localeUsers = this.users
|
||||
}
|
||||
|
||||
public newUser(): void {
|
||||
if (!this.newUserId) {
|
||||
return
|
||||
}
|
||||
this.localeUsers.push({
|
||||
userId: this.newUserId,
|
||||
email: '',
|
||||
premium: this.user ? this.user.premium : false,
|
||||
slugEmail: '',
|
||||
alias: this.newUserId,
|
||||
firstname: '',
|
||||
lastname: ''
|
||||
})
|
||||
this.newUserId = ''
|
||||
}
|
||||
|
||||
public remove(index: number): void {
|
||||
this.localeUsers = this.localeUsers.filter(
|
||||
(u: IUser, i: number) => i !== index
|
||||
)
|
||||
}
|
||||
|
||||
public removeUser(): void {
|
||||
this.localeUsers.pop()
|
||||
}
|
||||
|
||||
@Watch('localeUsers')
|
||||
public onUserChange(users: IUser[]): void {
|
||||
this.$emit('input', users)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
121
src/components/AppResume.vue
Normal file
121
src/components/AppResume.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="app-resume" v-once>
|
||||
<div class="columns is-multiline text-description is-centered">
|
||||
<div class="column is-one-third">
|
||||
<div class="main-icon">
|
||||
<img class="icon" src="../assets/icons/baseline-cloud_off-24px.svg" alt="offline" />
|
||||
</div>
|
||||
<p>
|
||||
Vos données sont hors-lignes.
|
||||
Utilisez Vaquant où vous voulez, seul ou en groupe.
|
||||
Les données seront sauvegardées sur internet dès qu'une connexion est disponible
|
||||
afin de les synchroniser avec vos amis.
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<div class="main-icon">
|
||||
<awe-icon icon="users"></awe-icon>
|
||||
</div>
|
||||
<p>Choisissez de rendre le compte privé ou public.</p>
|
||||
<p>Lorsque le compte est privé, seules les personnes du compte accéderont aux dépenses.</p>
|
||||
<p>Lorsque le compte est public, toutes les personnes accédant au lien pourront voir, modifier ou supprimer les dépenses.</p>
|
||||
</div>
|
||||
<!-- <div class="column is-one-third">
|
||||
<div class="main-icon">
|
||||
<awe-icon icon="user-lock"/>
|
||||
</div>
|
||||
<p>
|
||||
Les informations sensibles de vos comptes et transactions sont chiffrées de bout en bout.
|
||||
Seuls les membres d'un compte peuvent visualiser ses données.
|
||||
</p>
|
||||
</div>-->
|
||||
<div class="column is-one-third">
|
||||
<div class="main-icon">
|
||||
<awe-icon icon="euro-sign" />
|
||||
<awe-icon icon="exchange-alt" />
|
||||
<awe-icon icon="dollar-sign" />
|
||||
</div>
|
||||
<p>
|
||||
Vaquant vous permet d'enregistrer les dépenses dans la monnaie locale de vos vacances
|
||||
et se charge de faire la conversion pour vous au jour le jour. 😙
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<div class="main-icon">
|
||||
<awe-icon icon="sync-alt" />
|
||||
</div>
|
||||
<p>
|
||||
Vaquant se met à jour en direct. Vous recevez automatiquement les
|
||||
nouveaux comptes et dépenses sans même vous en rendre compte. 🎉
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<div class="main-icon">
|
||||
<awe-icon icon="mobile" />
|
||||
</div>
|
||||
<p>
|
||||
Vaquant est simple, sécurisée, rapide et accessible depuis un simple lien internet !
|
||||
Votre partenaire idéal pour vos voyages ! ⛺
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<div class="main-icon">
|
||||
<awe-icon icon="hand-holding-usd" />
|
||||
</div>
|
||||
<p>
|
||||
Vaquant propose une offre gratuite sans publicité.
|
||||
Si vous souhaitez encourager son développement, vous pouvez toujours
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
:href="coffee"
|
||||
>offrir un ☕</a> !
|
||||
</p>
|
||||
<p>
|
||||
Ou vous pouvez également me supporter sur
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
:href="brave"
|
||||
>Brave</a> !
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import { COFFEE_LINK, BRAVE_LINK } from '@/utils/constants'
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
coffee: COFFEE_LINK,
|
||||
brave: BRAVE_LINK
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-icon {
|
||||
font-size: 34pt;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-description {
|
||||
font-size: 14pt;
|
||||
text-align: justify;
|
||||
text-align-last: center;
|
||||
|
||||
.column {
|
||||
padding: 30px;
|
||||
.emoji {
|
||||
font-size: 20pt;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
29
src/components/AxisLabel.vue
Normal file
29
src/components/AxisLabel.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<text :x="point.x" :y="point.y">{{ stat.label }}</text>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator'
|
||||
import chartService from '@/services/ChartService'
|
||||
import IStat from '@/models/IStat'
|
||||
import IPoint from '@/models/IPoint'
|
||||
|
||||
@Component
|
||||
export default class AxisLabel extends Vue {
|
||||
@Prop({ type: Object, required: true })
|
||||
public stat!: IStat
|
||||
@Prop({ type: Number, required: true })
|
||||
public index!: number
|
||||
@Prop({ type: Number, required: true })
|
||||
public total!: number
|
||||
|
||||
public get point(): IPoint {
|
||||
return chartService.valueToPoint(
|
||||
this.stat.percent + 10,
|
||||
this.index,
|
||||
this.total
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
74
src/components/ChartBalance.vue
Normal file
74
src/components/ChartBalance.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="chart-balance">
|
||||
<table class="table is-hoverable is-striped" v-if="stats.length">
|
||||
<tbody>
|
||||
<tr v-for="(stat, k) in statOrdered" :key="k">
|
||||
<th scope="row">{{ stat.label }}</th>
|
||||
<td class="numeric">{{ stat.value | money(currency) }}</td>
|
||||
<td
|
||||
v-if="stat.balance.amount !== 0"
|
||||
class="numeric"
|
||||
:class="getColor(stat.balance.amount)"
|
||||
>{{ stat.balance.amount | money(currency) }}</td>
|
||||
<td v-else class="conclude" :class="getColor(stat.balance.amount)"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-else>Aucune dépense faite</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
import ICurrency from '@/models/ICurrency'
|
||||
import IStat from '@/models/IStat'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'axis-label': () => import('@/components/AxisLabel.vue')
|
||||
}
|
||||
})
|
||||
export default class ChartBalance extends Vue {
|
||||
@Prop({ type: Array, default: () => [] })
|
||||
public stats!: IStat[]
|
||||
@Prop({ type: Object, required: true })
|
||||
public currency!: ICurrency
|
||||
|
||||
public getColor(amount: number): string {
|
||||
return amount === 0 ? 'zero' : amount > 0 ? 'positive' : 'negative'
|
||||
}
|
||||
|
||||
public get statOrdered(): IStat[] {
|
||||
return [...this.stats].sort((a: IStat, b: IStat) =>
|
||||
a.balance.amount < b.balance.amount ? 1 : -1
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../styles/variables';
|
||||
|
||||
.chart-balance {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.table {
|
||||
margin: auto;
|
||||
.numeric {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.zero {
|
||||
color: $primary;
|
||||
&.conclude::after {
|
||||
content: '👌🏾';
|
||||
}
|
||||
}
|
||||
.positive {
|
||||
color: $green;
|
||||
}
|
||||
.negative {
|
||||
color: $red;
|
||||
}
|
||||
</style>
|
||||
85
src/components/ChartPie.vue
Normal file
85
src/components/ChartPie.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<svg class="chart-pie" ref="chart" id="chart-pie" viewBox="-1 -1 2 2"></svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// https://codepen.io/soluhmin/pen/jexywg
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'
|
||||
import ISlice from '@/models/ISlice'
|
||||
|
||||
@Component
|
||||
export default class ChartPie extends Vue {
|
||||
@Prop({ type: Array, default: () => [] })
|
||||
public slices!: ISlice[]
|
||||
public svgEl: Element | null = null
|
||||
|
||||
public mounted(): void {
|
||||
this.svgEl = this.$refs.chart as Element
|
||||
this.constructChart()
|
||||
}
|
||||
|
||||
public getCoordinatesForPercent(percent: number): number[] {
|
||||
const x = Math.cos(2 * Math.PI * percent)
|
||||
const y = Math.sin(2 * Math.PI * percent)
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
public clearSvg(): void {
|
||||
if (!this.svgEl) {
|
||||
return
|
||||
}
|
||||
const svgEl = this.svgEl
|
||||
while (svgEl.lastChild) {
|
||||
svgEl.removeChild(svgEl.lastChild)
|
||||
}
|
||||
}
|
||||
|
||||
public constructChart(): void {
|
||||
if (!this.svgEl) {
|
||||
return
|
||||
}
|
||||
const svgEl = this.svgEl
|
||||
this.clearSvg()
|
||||
let cumulativePercent = 0
|
||||
|
||||
this.slices.forEach((slice: ISlice) => {
|
||||
const [startX, startY] = this.getCoordinatesForPercent(cumulativePercent)
|
||||
cumulativePercent += slice.percent
|
||||
const [endX, endY] = this.getCoordinatesForPercent(cumulativePercent)
|
||||
|
||||
/*
|
||||
* if the slice is more than 50%,
|
||||
* take the large arc (the long way around)
|
||||
*/
|
||||
const largeArcFlag = slice.percent > 0.5 ? 1 : 0
|
||||
|
||||
const pathData = [
|
||||
`M ${startX} ${startY}`, // Move
|
||||
`A 1 1 0 ${largeArcFlag} 1 ${endX} ${endY}`, // Arc
|
||||
`L 0 0` // Line
|
||||
].join(' ')
|
||||
|
||||
const pathEl = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'path'
|
||||
)
|
||||
pathEl.setAttribute('d', pathData)
|
||||
pathEl.setAttribute('fill', slice.color)
|
||||
svgEl.appendChild(pathEl)
|
||||
})
|
||||
}
|
||||
|
||||
@Watch('slices')
|
||||
public onSliceChange(): void {
|
||||
this.constructChart()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-pie {
|
||||
transform: rotate(-90deg);
|
||||
/* height: 200px; */
|
||||
max-width: 400pt;
|
||||
}
|
||||
</style>
|
||||
98
src/components/ColorPicker.vue
Normal file
98
src/components/ColorPicker.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="color-picker">
|
||||
<h3 class="subtitle is-3">Couleur</h3>
|
||||
<div class="columns is-mobile is-centered is-multiline">
|
||||
<div class="column" v-for="(color, k) in colors" :key="k">
|
||||
<div
|
||||
@click.prevent="select(color)"
|
||||
class="color-container"
|
||||
:class="{ white: color.label }"
|
||||
:style="colorBackground(color)"
|
||||
>
|
||||
<awe-icon v-if="color.value === localColor" icon="check" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Model, Watch } from 'vue-property-decorator'
|
||||
import IColor from '@/models/IColor'
|
||||
import colors from '@/data/colors'
|
||||
|
||||
@Component
|
||||
export default class ColorPicker extends Vue {
|
||||
@Model('input', { type: String, required: false })
|
||||
public color!: string | null
|
||||
public localColor: string | null = null
|
||||
public colorLabel: string | null = null
|
||||
public colors: IColor[] = colors
|
||||
|
||||
public mounted(): void {
|
||||
this.localColor = this.color
|
||||
if (this.color) {
|
||||
const color: IColor | undefined = this.colors.find(
|
||||
(c: IColor) => c.value === this.color
|
||||
)
|
||||
if (color) {
|
||||
this.colorLabel = color.label
|
||||
}
|
||||
} else {
|
||||
this.localColor = this.colors[0].value
|
||||
}
|
||||
}
|
||||
|
||||
public select(color: IColor): void {
|
||||
this.localColor = color.value
|
||||
this.colorLabel = color.label
|
||||
}
|
||||
|
||||
public colorBackground(color: IColor): any {
|
||||
return {
|
||||
border: !color.label ? '1px solid #95afc0' : '',
|
||||
backgroundColor: color.value
|
||||
}
|
||||
}
|
||||
|
||||
public get background(): any | null {
|
||||
if (!this.localColor) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
'background-color': this.localColor
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('localColor')
|
||||
public onColorChange(color: string | null, oldColor: string | null) {
|
||||
this.$emit('input', color)
|
||||
if (oldColor !== color) {
|
||||
this.$emit('color-change')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.color-container {
|
||||
border-radius: 6px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
font-size: 25px;
|
||||
margin: auto;
|
||||
&.white {
|
||||
color: white;
|
||||
}
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.input {
|
||||
width: 100px;
|
||||
}
|
||||
.button.is-static {
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
51
src/components/ConfirmButton.vue
Normal file
51
src/components/ConfirmButton.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<a
|
||||
href="#"
|
||||
class="button"
|
||||
:class="{ 'is-loading': loading }"
|
||||
@click.prevent="click"
|
||||
v-click-outside="resetTap"
|
||||
>
|
||||
<span v-show="confirmMessage">{{ confirmMessage }}</span>
|
||||
<span v-show="!confirmMessage">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator'
|
||||
import ClickOutside from 'vue-click-outside'
|
||||
|
||||
@Component({
|
||||
directives: {
|
||||
ClickOutside
|
||||
}
|
||||
})
|
||||
export default class ConfirmButton extends Vue {
|
||||
public loading: boolean = false
|
||||
public firstTap: boolean = true
|
||||
public resetTap(): void {
|
||||
this.firstTap = true
|
||||
}
|
||||
public click(): void {
|
||||
if (this.loading) {
|
||||
return
|
||||
}
|
||||
if (this.firstTap) {
|
||||
this.firstTap = false
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.firstTap = true
|
||||
this.$emit('confirm', () => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
public get confirmMessage(): string {
|
||||
return this.firstTap ? '' : 'Confirmer pour valider'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
129
src/components/FabButton.vue
Normal file
129
src/components/FabButton.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="fab-button" :id="`fab-${uniqueId}`">
|
||||
<router-link
|
||||
v-if="to"
|
||||
class="button is-primary is-large is-fab"
|
||||
:class="getClass"
|
||||
:style="buttonStyle"
|
||||
:to="to"
|
||||
@click.native="valid">
|
||||
<span class="icon is-medium">
|
||||
<slot>
|
||||
<awe-icon icon="plus" />
|
||||
</slot>
|
||||
</span>
|
||||
<span class="fulltext" v-if="$slots.fulltext">
|
||||
<slot name="fulltext"></slot>
|
||||
</span>
|
||||
</router-link>
|
||||
<button
|
||||
v-else
|
||||
type="submit"
|
||||
class="button is-primary is-large is-fab"
|
||||
:class="getClass"
|
||||
:style="buttonStyle"
|
||||
@click="valid">
|
||||
<span class="icon is-medium">
|
||||
<slot>
|
||||
<awe-icon icon="plus" />
|
||||
</slot>
|
||||
</span>
|
||||
<span class="fulltext" v-if="$slots.fulltext">
|
||||
<slot name="fulltext"></slot>
|
||||
</span>
|
||||
</button>
|
||||
<div class="fab-margin" v-if="margin"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator'
|
||||
import { throttle } from 'lodash-es'
|
||||
|
||||
@Component
|
||||
export default class Home extends Vue {
|
||||
@Prop({ type: Boolean, default: false })
|
||||
public margin!: boolean
|
||||
@Prop({ type: Object, default: () => null })
|
||||
public to!: any | null
|
||||
@Prop({ type: Object, required: false, default: () => ({})})
|
||||
public buttonStyle!: any
|
||||
public uniqueId: string = Date.now().toString()
|
||||
public isLoading: boolean = false
|
||||
public showFull: boolean = true
|
||||
|
||||
public mounted(): void {
|
||||
if (!this.$slots.fulltext) {
|
||||
return
|
||||
}
|
||||
const main = document.getElementById('main')
|
||||
const fabElm = document.getElementById(`fab-${this.uniqueId}`)
|
||||
if (main && fabElm) {
|
||||
document.addEventListener('scroll', throttle((evt) => {
|
||||
this.showFull = main.getBoundingClientRect().top > 0
|
||||
if (this.showFull) {
|
||||
fabElm.classList.remove('small')
|
||||
} else {
|
||||
fabElm.classList.add('small')
|
||||
}
|
||||
}, 150))
|
||||
}
|
||||
}
|
||||
|
||||
public loading(): void {
|
||||
this.isLoading = true
|
||||
}
|
||||
public valid(): void {
|
||||
this.isLoading = true
|
||||
this.$emit('valid', () => {
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
public get getClass(): any {
|
||||
return { 'is-loading': this.isLoading }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../styles/variables';
|
||||
|
||||
$padding-fab: 24px;
|
||||
.fab-button {
|
||||
.button {
|
||||
padding-right: 0.75em;
|
||||
transition: padding-right 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
}
|
||||
.fulltext {
|
||||
display: inline-block;
|
||||
max-width: 450px;
|
||||
opacity: 1;
|
||||
transition: max-width 0.3s cubic-bezier(0.55, 0, 0.1, 1),
|
||||
opacity 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
}
|
||||
&.small {
|
||||
.button {
|
||||
padding-right: 0.2em;
|
||||
transition: padding-right 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
}
|
||||
.fulltext {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
transition: max-width 0.3s cubic-bezier(0.55, 0, 0.1, 1),
|
||||
opacity 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
.is-fab {
|
||||
bottom: $padding-fab;
|
||||
position: fixed;
|
||||
right: $padding-fab;
|
||||
z-index: 2;
|
||||
border: 2px solid $white;
|
||||
}
|
||||
|
||||
.fab-margin {
|
||||
margin: calc(#{$padding-fab} * 2 + 30px) 0 calc(#{$padding-fab} * 2);
|
||||
}
|
||||
</style>
|
||||
12
src/components/HelloWorld.vue
Normal file
12
src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class HelloWorld extends Vue {
|
||||
}
|
||||
</script>
|
||||
72
src/components/LangChanger.vue
Normal file
72
src/components/LangChanger.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="lang-changer field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Changer de langue</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="lang-changer" v-model="lang">
|
||||
<option
|
||||
v-for="(locale, k) in locales"
|
||||
:key="k"
|
||||
:value="locale.code"
|
||||
>{{ locale.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from 'vue-property-decorator'
|
||||
import { Action, Getter } from 'vuex-class'
|
||||
|
||||
interface ILocale {
|
||||
code: string
|
||||
label: string
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class LangChanger extends Vue {
|
||||
@Getter
|
||||
public locale!: string
|
||||
@Action
|
||||
public setLocale!: any
|
||||
public lang: string = ''
|
||||
public locales: ILocale[] = [
|
||||
{
|
||||
code: 'en',
|
||||
label: 'English'
|
||||
},
|
||||
{
|
||||
code: 'fr',
|
||||
label: 'Français'
|
||||
}
|
||||
]
|
||||
|
||||
public mounted(): void {
|
||||
this.lang = this.locale
|
||||
}
|
||||
|
||||
@Watch('lang')
|
||||
public onlangChange(locale: string): void {
|
||||
if (locale !== this.locale) {
|
||||
this.setLocale(locale)
|
||||
}
|
||||
if (locale !== this.$i18n.locale) {
|
||||
this.$i18n.locale = locale
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('locale', { immediate: true })
|
||||
public onLocaleChange(locale: string): void {
|
||||
if (locale !== this.lang) {
|
||||
this.lang = locale
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
31
src/components/OnlineView.vue
Normal file
31
src/components/OnlineView.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="online-view">
|
||||
<slot v-if="online"></slot>
|
||||
<slot v-else name="offline"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class OnlineView extends Vue {
|
||||
public online: boolean = navigator.onLine
|
||||
|
||||
public mounted(): void {
|
||||
window.addEventListener('online', this.onchange)
|
||||
window.addEventListener('offline', this.onchange)
|
||||
this.$emit(this.online ? 'online' : 'offline')
|
||||
}
|
||||
|
||||
public beforeDestroy(): void {
|
||||
window.removeEventListener('online', this.onchange)
|
||||
window.removeEventListener('offline', this.onchange)
|
||||
}
|
||||
|
||||
public onchange(): void {
|
||||
this.online = navigator.onLine
|
||||
this.$emit(this.online ? 'online' : 'offline')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
60
src/components/PricingTable.vue
Normal file
60
src/components/PricingTable.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="pricing-table">
|
||||
<div class="pricing-plan" :class="{ 'is-active': plan === 'free' }">
|
||||
<div class="plan-header">Gratuit</div>
|
||||
Parfait pour démarrer rapidement
|
||||
<div class="plan-price"><span class="plan-price-amount"><span class="plan-price-currency">€</span>0</span>/mois</div>
|
||||
<div class="plan-items">
|
||||
<div class="plan-item">jusqu'à 20 comptes</div>
|
||||
<div class="plan-item">jusqu'à 20 amis/compte</div>
|
||||
<div class="plan-item">jusqu'à 200 dépenses/compte</div>
|
||||
<div class="plan-item">33 devises</div>
|
||||
</div>
|
||||
<div class="plan-footer">
|
||||
<button class="button is-fullwidth" @click="choose('free')">
|
||||
<span v-if="plan !== 'free'">Choix par défaut</span>
|
||||
<span v-else>Sélectionné</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pricing-plan is-success" :class="{ 'is-active': plan === 'premium' }">
|
||||
<div class="plan-header">Premium</div>
|
||||
Vaquant sans aucune limite !
|
||||
<div class="plan-price"><span class="plan-price-amount"><span class="plan-price-currency">€</span>3</span>/mois</div>
|
||||
<div class="plan-items">
|
||||
<div class="plan-item"><awe-icon icon="infinity" /> comptes</div>
|
||||
<div class="plan-item"><awe-icon icon="infinity" /> amis/compte</div>
|
||||
<div class="plan-item"><awe-icon icon="infinity" /> dépenses/compte</div>
|
||||
<div class="plan-item">150+ devises</div>
|
||||
<div class="plan-item">pièces jointes disponibles</div>
|
||||
</div>
|
||||
<div class="plan-footer">
|
||||
<button class="button is-fullwidth" @click="choose('premium')" disabled>
|
||||
Bientôt disponible
|
||||
<!-- <span v-if="plan !== 'premium'">Passer en premium</span>
|
||||
<span v-else>Sélectionné</span> -->
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class PrincingTable extends Vue {
|
||||
@Prop({ type: String, default: '' })
|
||||
public firstPlan!: string
|
||||
public plan: string = ''
|
||||
|
||||
public mounted(): void {
|
||||
this.plan = this.firstPlan
|
||||
}
|
||||
public choose(plan: string): void {
|
||||
this.plan = plan
|
||||
this.$emit(plan)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
156
src/components/RefundTransaction.vue
Normal file
156
src/components/RefundTransaction.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div class="refund-transaction card">
|
||||
<div class="card-content">
|
||||
<div class="content" v-if="refunded">Somme remboursée...</div>
|
||||
<div class="content" v-else>
|
||||
<span v-if="toMe">
|
||||
<span class="people">{{ refund.from.alias }}</span>
|
||||
vous doit
|
||||
<span class="numeric">{{ refund.amount | money(account.mainCurrency) }}</span>
|
||||
</span>
|
||||
<span v-else-if="fromMe">
|
||||
Vous devez
|
||||
<span class="numeric">{{ refund.amount | money(account.mainCurrency) }}</span>
|
||||
à
|
||||
<span class="people">{{ refund.to.alias }}</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="people">{{ refund.from.alias }}</span>
|
||||
doit
|
||||
<span class="numeric">{{ refund.amount | money(account.mainCurrency) }}</span>
|
||||
à
|
||||
<span class="people">{{ refund.to.alias }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="card-footer" v-if="canRefund && !refunded">
|
||||
<confirm-button class="is-text is-fullwidth" @confirm="refundTo">Rembourser</confirm-button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator'
|
||||
import { Getter } from 'vuex-class'
|
||||
import bus, { SYNC } from '@/utils/bus-event'
|
||||
import ClickOutside from 'vue-click-outside'
|
||||
import transactionService from '@/services/TransactionService'
|
||||
import IAccount from '@/models/IAccount'
|
||||
import ICurrency from '@/models/ICurrency'
|
||||
import IRefund from '@/models/IRefund'
|
||||
import ITransaction from '@/models/ITransaction'
|
||||
import IUser from '@/models/IUser'
|
||||
import TransactionTag from '@/enums/TransactionTag'
|
||||
import TransactionType from '@/enums/TransactionType'
|
||||
import exchangeService from '@/services/ExchangeService'
|
||||
import { confirmation } from '@/utils'
|
||||
|
||||
@Component({
|
||||
directives: {
|
||||
ClickOutside
|
||||
},
|
||||
components: {
|
||||
'confirm-button': () => import('@/components/ConfirmButton.vue')
|
||||
}
|
||||
})
|
||||
export default class RefundTransaction extends Vue {
|
||||
@Getter
|
||||
public user!: IUser | null
|
||||
@Prop({ type: Object, required: true })
|
||||
public refund!: IRefund
|
||||
@Prop({ type: Object, required: true })
|
||||
public account!: IAccount
|
||||
public firstTap: boolean = true
|
||||
public refunded: boolean = false
|
||||
public resetTap(): void {
|
||||
this.firstTap = true
|
||||
}
|
||||
|
||||
public async refundTo(done: any): Promise<void> {
|
||||
if (!this.account._id) {
|
||||
done()
|
||||
return
|
||||
}
|
||||
const date: Date = new Date()
|
||||
const currencies: string[] = this.account.currencies.map(
|
||||
(c: ICurrency) => c.code
|
||||
)
|
||||
const transaction: ITransaction = {
|
||||
accountId: this.account._id,
|
||||
name: 'Remboursement',
|
||||
date,
|
||||
tag: TransactionTag.None,
|
||||
amount: this.refund.amount || 0,
|
||||
payBy: this.refund.from.alias || '',
|
||||
payFor: [
|
||||
{
|
||||
alias: this.refund.to.alias || '',
|
||||
weight: 1
|
||||
}
|
||||
],
|
||||
userIds: this.account.userIds,
|
||||
mainCurrency: this.account.mainCurrency,
|
||||
currencies: this.account.currencies,
|
||||
transactionType: TransactionType.refund,
|
||||
exchange: await exchangeService.get(
|
||||
this.account.mainCurrency.code,
|
||||
date,
|
||||
currencies
|
||||
)
|
||||
}
|
||||
const response: PouchDB.Core.Response = await transactionService.add(
|
||||
this.account._id,
|
||||
transaction
|
||||
)
|
||||
if (response.ok) {
|
||||
this.refunded = true
|
||||
confirmation('Remboursement effectué avec succès')
|
||||
}
|
||||
bus.$emit(SYNC)
|
||||
done()
|
||||
}
|
||||
|
||||
public get canRefund(): boolean {
|
||||
if (!this.user || !this.account.admin) {
|
||||
return true
|
||||
}
|
||||
const emails: string[] = [this.account.admin.email, this.refund.from.email]
|
||||
return emails.includes(this.user.email)
|
||||
}
|
||||
|
||||
public get fromMe(): boolean {
|
||||
if (!this.user || !this.refund.from) {
|
||||
return false
|
||||
}
|
||||
return this.refund.from.email === this.user.email
|
||||
}
|
||||
|
||||
public get toMe(): boolean {
|
||||
if (!this.user || !this.refund.to) {
|
||||
return false
|
||||
}
|
||||
return this.refund.to.email === this.user.email
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.people {
|
||||
font-size: 12pt;
|
||||
font-weight: bold;
|
||||
&.to-me {
|
||||
font-style: italic;
|
||||
&:after {
|
||||
font-style: normal;
|
||||
content: ' (🤗)';
|
||||
}
|
||||
}
|
||||
&.from-me {
|
||||
font-style: italic;
|
||||
&:after {
|
||||
font-style: normal;
|
||||
content: ' (🤨)';
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
186
src/components/TagList.vue
Normal file
186
src/components/TagList.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div class="tag-list columns is-multiline is-centered">
|
||||
<div class="column is-half">
|
||||
<h3 class="subtitle is-3">Légende</h3>
|
||||
<table class="table">
|
||||
<tr v-for="(slice, k) in slices" :key="k" :style="legendStyle(slice)" class="legend-item">
|
||||
<td>
|
||||
<awe-icon :icon="categories[slice.key].tag.icon" />
|
||||
</td>
|
||||
<td class="legend-label">{{ slice.label }}</td>
|
||||
<td class="percent-label numeric">{{ Math.round(slice.percent * 100) }}%</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<h3 class="subtitle is-3">Graphique</h3>
|
||||
<div class="chart-container">
|
||||
<chart-pie :slices="slices" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<h3 class="subtitle is-3">Détails</h3>
|
||||
<div class="field has-addons has-addons-centered is-narrow">
|
||||
<div class="control" v-if="filterCategory">
|
||||
<button type="submit" class="button" @click="filterCategory = ''">
|
||||
<awe-icon icon="times" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="filterCategory">
|
||||
<option value>Toutes</option>
|
||||
<option
|
||||
v-for="(category, k) in categories"
|
||||
:key="k"
|
||||
:value="k"
|
||||
>{{ category.tag.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns is-multiline" :class="{ 'is-centered': !!filterCategory }">
|
||||
<div v-for="(category, k) in filteredCategories" :key="k" class="column is-half">
|
||||
<div class="columns category-list">
|
||||
<div class="column is-2 tag-element account-color">
|
||||
<awe-icon :icon="category.tag.icon" />
|
||||
</div>
|
||||
<div class="column transaction-list" v-if="category.transactions.length">
|
||||
<p>{{ $tc('account.total', category.transactions.length, { count: category.transactions.length }) }}</p>
|
||||
<account-transaction-list
|
||||
:filter="false"
|
||||
:head="false"
|
||||
:foot="true"
|
||||
:account="account"
|
||||
:transactions="category.transactions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
import colors from '@/data/colors'
|
||||
import IAccount from '@/models/IAccount'
|
||||
import ITransaction from '@/models/ITransaction'
|
||||
import ISlice from '@/models/ISlice'
|
||||
import TransactionTag, {
|
||||
ITagLabel,
|
||||
TransactionTagLabel
|
||||
} from '@/enums/TransactionTag'
|
||||
|
||||
interface ICategory {
|
||||
[key: string]: {
|
||||
tag: ITagLabel
|
||||
transactions: ITransaction[]
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'chart-pie': () => import('@/components/ChartPie.vue'),
|
||||
'account-transaction-list': () =>
|
||||
import('@/components/AccountTransactionList.vue')
|
||||
}
|
||||
})
|
||||
export default class TagList extends Vue {
|
||||
@Prop({ type: Object, required: true })
|
||||
public account!: IAccount
|
||||
@Prop({ type: Array, default: () => [] })
|
||||
public transactions!: ITransaction[]
|
||||
public transactionTagLabel: typeof TransactionTagLabel = TransactionTagLabel
|
||||
public filterCategory: string = ''
|
||||
|
||||
public legendStyle(slice: ISlice): any {
|
||||
return {
|
||||
color: slice.color
|
||||
}
|
||||
}
|
||||
|
||||
public get categories(): ICategory {
|
||||
return this.transactions.reduce(
|
||||
(a: ICategory, transaction: ITransaction) => {
|
||||
if (!a[transaction.tag]) {
|
||||
a[transaction.tag] = {
|
||||
tag: this.transactionTagLabel[transaction.tag]
|
||||
? this.transactionTagLabel[transaction.tag]
|
||||
: this.transactionTagLabel[TransactionTag.None],
|
||||
transactions: []
|
||||
}
|
||||
}
|
||||
a[transaction.tag].transactions.push(transaction)
|
||||
return a
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
public get filteredCategories(): ICategory {
|
||||
if (this.filterCategory && this.filterCategory in this.categories) {
|
||||
return {
|
||||
[this.filterCategory]: this.categories[this.filterCategory]
|
||||
}
|
||||
}
|
||||
return this.categories
|
||||
}
|
||||
|
||||
public get slices(): ISlice[] {
|
||||
const tags = Object.keys(this.categories)
|
||||
const total = this.transactions.reduce(
|
||||
(t: number, b: ITransaction) =>
|
||||
t + parseFloat((b.amount || 0).toString()),
|
||||
0
|
||||
)
|
||||
const slices = tags.map((tag: string, index: number) => ({
|
||||
percent:
|
||||
this.categories[tag].transactions.reduce(
|
||||
(t: number, b: ITransaction) => t + b.amount,
|
||||
0
|
||||
) / total,
|
||||
color: colors[index].value || '#ffffff',
|
||||
darkColor: colors[index].darkValue || '#ffffff',
|
||||
key: tag,
|
||||
label: this.categories[tag].tag.label
|
||||
}))
|
||||
|
||||
return slices.sort((a: ISlice, b) => (a.percent >= b.percent ? -1 : 1))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../styles/variables';
|
||||
.category-list:not(:last-child) {
|
||||
border-bottom: 1px solid $main;
|
||||
}
|
||||
.tag-element {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-flow: column;
|
||||
font-size: 30pt;
|
||||
}
|
||||
.chart-container {
|
||||
padding: 15px;
|
||||
}
|
||||
.legend {
|
||||
font-weight: bold;
|
||||
font-size: 16pt;
|
||||
}
|
||||
.legend-item {
|
||||
background-color: $main;
|
||||
font-size: 20px;
|
||||
line-height: 2em;
|
||||
}
|
||||
.legend-label {
|
||||
text-align: center;
|
||||
}
|
||||
.percent-label {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
354
src/components/TransactionCreate.vue
Normal file
354
src/components/TransactionCreate.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<div class="transaction-create">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs" v-if="account">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: 'account', params: { id: account._id } }">{{ account.name }}</router-link>
|
||||
</li>
|
||||
<li v-if="tag && tag !== noTag">
|
||||
<a href="#" @click.prevent>{{ transactionTagLabel[tag].label }}</a>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<a href="#" @click.prevent>
|
||||
<span v-if="transaction._id">{{ name }}</span>
|
||||
<span v-else>Nouvelle dépense</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Nom</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
v-model.trim="name"
|
||||
maxlength="30"
|
||||
placeholder="Nom de la dépense"
|
||||
/>
|
||||
<p
|
||||
class="help is-info"
|
||||
>{{ $tc('validation.max_char', 30 - name.length, { max: 30 - name.length }) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Montant</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="field has-addons">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
type="number"
|
||||
class="input"
|
||||
v-model.number="amount"
|
||||
placeholder="Montant"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
min="0"
|
||||
max="1000000"
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<span v-if="account.currencies.length > 1" class="select">
|
||||
<select name="currency" id="currency" v-model="currency">
|
||||
<option
|
||||
v-for="(currency, k) in account.currencies"
|
||||
:key="k"
|
||||
:value="currency"
|
||||
>{{ currency.symbol || currency.name }}</option>
|
||||
</select>
|
||||
</span>
|
||||
<a
|
||||
v-else
|
||||
class="button is-static"
|
||||
>{{ account.mainCurrency.symbol || account.mainCurrency.name }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<p class="help is-info">montant max : {{ maxAmount | money(currency) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Localisation</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control" v-if="transaction.location">
|
||||
<input type="text" readonly class="input" v-model="transaction.location.place" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Date</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input type="date" class="input" v-model="date" placeholder="Date" :max="today" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<transaction-tag-update v-model="tag" />
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Payé par</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="pay-by" id="pay-by" v-model="payBy">
|
||||
<option
|
||||
v-for="(user, k) in account.users"
|
||||
:key="k"
|
||||
:value="user.alias"
|
||||
>{{ user.alias }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Pour</label>
|
||||
<button
|
||||
class="button is-primary is-small"
|
||||
:class="{ 'is-outlined': payForUsers.length !== account.users.length}"
|
||||
type="button"
|
||||
@click="selectAll"
|
||||
>tous</button>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<div v-for="(split, k) in payFor" :key="k" class="user-item">
|
||||
<transaction-split :split="payFor[k]" />
|
||||
<hr v-if="k !== payFor.length - 1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<fab-button @valid="submitTransaction" :margin="true">
|
||||
<awe-icon icon="check" />
|
||||
</fab-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'
|
||||
import { Getter } from 'vuex-class'
|
||||
import IAccount from '@/models/IAccount'
|
||||
import ICurrency from '@/models/ICurrency'
|
||||
import ITransaction from '@/models/ITransaction'
|
||||
import IUser from '@/models/IUser'
|
||||
import ISplit from '@/models/ISplit'
|
||||
import TransactionType from '@/enums/TransactionType'
|
||||
import TransactionTag, { TransactionTagLabel } from '@/enums/TransactionTag'
|
||||
import accountService from '@/services/AccountService'
|
||||
import exchangeService from '@/services/ExchangeService'
|
||||
import transactionService from '@/services/TransactionService'
|
||||
import formatDate from '@/utils/format-date'
|
||||
import notif from '@/utils/notif'
|
||||
import { money } from '@/utils/filters'
|
||||
import { confirmation, alertMessage } from '@/utils'
|
||||
|
||||
const today: Date = new Date()
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'fab-button': () => import('@/components/FabButton.vue'),
|
||||
'transaction-split': () => import('@/components/TransactionSplit.vue'),
|
||||
'transaction-tag-update': () =>
|
||||
import('@/components/TransactionTagUpdate.vue')
|
||||
}
|
||||
})
|
||||
export default class TransactionCreate extends Vue {
|
||||
@Getter
|
||||
public user!: IUser | null
|
||||
@Prop({ type: Object, required: true })
|
||||
public transaction!: ITransaction
|
||||
@Prop({ type: Object, required: true })
|
||||
public account!: IAccount
|
||||
public name: string = ''
|
||||
public amount: number | null = null
|
||||
public tag: TransactionTag = TransactionTag.None
|
||||
public today: string = formatDate(today)
|
||||
public date: string = formatDate(today)
|
||||
public location: string = ''
|
||||
public currency: ICurrency | null = null
|
||||
public payBy: string = ''
|
||||
public payFor: ISplit[] = []
|
||||
public maxAmount: number = 1000000
|
||||
public noTag: string = TransactionTag.None
|
||||
public transactionTagLabel: typeof TransactionTagLabel = TransactionTagLabel
|
||||
|
||||
public async created(): Promise<void> {
|
||||
this.setData(this.transaction)
|
||||
}
|
||||
|
||||
public setData(transaction: ITransaction): void {
|
||||
this.name = transaction.name
|
||||
this.date = formatDate(transaction.date)
|
||||
this.tag = transaction.tag
|
||||
this.amount = transaction.amount || null
|
||||
this.payBy = transaction.payBy
|
||||
this.payFor = transaction.payFor
|
||||
if (this.account.users) {
|
||||
this.account.users.forEach((user: IUser) => {
|
||||
if (!transaction.payFor.find((p: ISplit) => p.alias === user.alias)) {
|
||||
this.payFor.push({
|
||||
alias: user.alias || '',
|
||||
weight: 0
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
this.currency = transaction.mainCurrency
|
||||
}
|
||||
|
||||
public async submitTransaction(next: any): Promise<void> {
|
||||
if (!this.validate()) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
if (!this.currency || !this.account) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
const currencies: string[] = this.account.currencies.map(
|
||||
(c: ICurrency) => c.code
|
||||
)
|
||||
const date: Date = new Date(this.date)
|
||||
const hasId: boolean = !!this.transaction._id
|
||||
const amount: number = this.amount
|
||||
? parseFloat(this.amount.toString().replace(',', '.'))
|
||||
: 0
|
||||
const transaction: ITransaction = {
|
||||
_id: this.transaction._id,
|
||||
_rev: this.transaction._rev,
|
||||
doctype: this.transaction.doctype,
|
||||
accountId: this.account._id || '',
|
||||
name: this.name,
|
||||
date,
|
||||
tag: this.tag,
|
||||
amount,
|
||||
payBy: this.payBy,
|
||||
payFor: this.payForUsers,
|
||||
userIds: this.account.userIds,
|
||||
mainCurrency: this.currency,
|
||||
currencies: this.account.currencies,
|
||||
transactionType: TransactionType.normal,
|
||||
exchange: await exchangeService.get(this.currency.code, date, currencies)
|
||||
}
|
||||
const response: PouchDB.Core.Response = hasId
|
||||
? await transactionService.save(transaction)
|
||||
: await transactionService.add(this.account._id || '', transaction)
|
||||
if (response.ok) {
|
||||
if (hasId) {
|
||||
this.$router.push({
|
||||
name: 'transaction',
|
||||
params: { id: this.transaction._id || '' }
|
||||
})
|
||||
confirmation('Dépense mise à jour.')
|
||||
} else {
|
||||
this.$router.push({
|
||||
name: 'account',
|
||||
params: { id: this.account._id || '' }
|
||||
})
|
||||
confirmation('Dépense créée.')
|
||||
}
|
||||
} else {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn(response)
|
||||
alertMessage(`Une erreur s'est produite à la création d'une transaction.`)
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
public selectAll(): void {
|
||||
if (!this.account) {
|
||||
return
|
||||
}
|
||||
this.payFor.forEach((p: ISplit) => (p.weight = p.weight || 1))
|
||||
}
|
||||
|
||||
public get payForUsers(): ISplit[] {
|
||||
return this.payFor.filter((p, index) => p.weight > 0)
|
||||
}
|
||||
|
||||
@Watch('payBy')
|
||||
public onPayByChange(payBy: string, oldPayBy: string) {
|
||||
if (
|
||||
oldPayBy &&
|
||||
this.account &&
|
||||
this.payFor.length !== this.account.users.length
|
||||
) {
|
||||
const old = this.payFor.find((p: ISplit) => p.alias === oldPayBy)
|
||||
if (old) {
|
||||
old.weight = 0
|
||||
}
|
||||
}
|
||||
if (payBy) {
|
||||
const newPayBy = this.payFor.find((p: ISplit) => p.alias === payBy)
|
||||
if (newPayBy) {
|
||||
newPayBy.weight = newPayBy.weight || 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validate(): boolean {
|
||||
if (
|
||||
!this.name ||
|
||||
!this.amount ||
|
||||
(!this.currency && !this.date && this.payFor.length === 0)
|
||||
) {
|
||||
if (this.amount === 0) {
|
||||
notif.alert('Le montant doit être supérieur à 0.')
|
||||
} else {
|
||||
notif.alert('Tous les champs sont requis.')
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (isNaN(this.amount) || this.amount > this.maxAmount) {
|
||||
notif.alert(
|
||||
`Veuillez saisir un montant numérique inférieur à ${money(
|
||||
this.maxAmount,
|
||||
this.currency
|
||||
)}.`
|
||||
)
|
||||
return false
|
||||
}
|
||||
if (this.amount < 0) {
|
||||
notif.alert('Le montant doit être supérieur à 0.')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.help {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
81
src/components/TransactionSplit.vue
Normal file
81
src/components/TransactionSplit.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="transaction-split field">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column">
|
||||
<input
|
||||
v-model="checked"
|
||||
class="is-checkradio"
|
||||
:id="`user-${split.alias}`"
|
||||
type="checkbox"
|
||||
:name="`user-${split.alias}`"
|
||||
/>
|
||||
<label
|
||||
:for="`user-${split.alias}`"
|
||||
>{{ split.alias }}, {{ $tc('transaction.split', split.weight, { count: split.weight }) }}</label>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<a @click="plus" class="button is-primary">
|
||||
<awe-icon icon="plus" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a @click="minus" class="button is-warning">
|
||||
<awe-icon icon="minus" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'
|
||||
import { Getter } from 'vuex-class'
|
||||
import IUser from '@/models/IUser'
|
||||
import ISplit from '@/models/ISplit'
|
||||
|
||||
@Component
|
||||
export default class TransactionSplit extends Vue {
|
||||
@Getter
|
||||
public user!: IUser | null
|
||||
@Prop({ type: Object, required: true })
|
||||
public split!: ISplit
|
||||
public checked: boolean = false
|
||||
|
||||
public plus(): void {
|
||||
this.split.weight++
|
||||
}
|
||||
|
||||
public minus(): void {
|
||||
this.split.weight = Math.max(this.split.weight - 1, 0)
|
||||
}
|
||||
|
||||
@Watch('checked')
|
||||
public onCheckedChange(check: boolean): void {
|
||||
if (check && this.split.weight === 0) {
|
||||
this.split.weight = 1
|
||||
} else if (!check && this.split.weight > 0) {
|
||||
this.split.weight = 0
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('split.weight', { immediate: true })
|
||||
public onWeightChange(weight: number): void {
|
||||
if (weight === 0 && this.checked) {
|
||||
this.checked = false
|
||||
} else if (weight > 0 && !this.checked) {
|
||||
this.checked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
float: left;
|
||||
max-width: 160px;
|
||||
}
|
||||
</style>
|
||||
46
src/components/TransactionTagUpdate.vue
Normal file
46
src/components/TransactionTagUpdate.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="transaction-tag-update field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label" for="tag">Catégorie</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<p class="control has-icons-left">
|
||||
<span class="select is-fullwidth">
|
||||
<select v-model="tagLocale" name="tag" id="tag">
|
||||
<option
|
||||
v-for="(transactionTag, k) in transactionTagLabel"
|
||||
:key="k"
|
||||
:value="k">
|
||||
{{ transactionTag.label }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="icon is-small is-left" v-if="transactionTagLabel[tag]">
|
||||
<awe-icon :icon="transactionTagLabel[tag].icon" />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Model, Watch } from 'vue-property-decorator'
|
||||
import TransactionTag, { TransactionTagLabel } from '@/enums/TransactionTag'
|
||||
|
||||
@Component
|
||||
export default class TransactionTagUpdate extends Vue {
|
||||
@Model('input', { type: String })
|
||||
public tag!: TransactionTag
|
||||
public tagLocale: TransactionTag = this.tag
|
||||
public transactionTagLabel: typeof TransactionTagLabel = TransactionTagLabel
|
||||
|
||||
@Watch('tagLocale')
|
||||
public onTagChange(tag: TransactionTag) {
|
||||
if (tag !== this.tag) {
|
||||
this.$emit('input', tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
72
src/components/UserNew.vue
Normal file
72
src/components/UserNew.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="user-new columns">
|
||||
<div class="column is-5" v-if="user">
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label" v-t="'user.pseudo'"></label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<p class="control">
|
||||
<input :readonly="isMainUser" class="input" type="text" v-model="newUser.userId" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5" v-if="newUser">
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label" v-t="'user.alias'"></label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<p class="control">
|
||||
<input required class="input" type="text" maxlength="20" v-model="newUser.alias" />
|
||||
</p>
|
||||
<p class="help is-info">Nom utilisé dans ce compte</p>
|
||||
<p
|
||||
class="help is-info"
|
||||
>{{ $tc('validation.max_char', maxChar(newUser), { max: maxChar(newUser) }) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2" v-if="!isMainUser">
|
||||
<button class="button is-danger" @click="$emit('remove')">supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Model, Vue, Watch } from 'vue-property-decorator'
|
||||
import { Getter } from 'vuex-class'
|
||||
import IUser from '@/models/IUser'
|
||||
import { slug } from '@/utils'
|
||||
|
||||
@Component
|
||||
export default class UserNew extends Vue {
|
||||
@Getter
|
||||
public user!: IUser | null
|
||||
@Model('input', { type: Object })
|
||||
public newUser!: IUser
|
||||
|
||||
public maxChar(user: IUser): number {
|
||||
return 20 - (user.alias ? user.alias.length : 0)
|
||||
}
|
||||
|
||||
public get isMainUser(): boolean {
|
||||
if (!this.newUser) {
|
||||
return false
|
||||
}
|
||||
return !!this.user && this.user.userId === this.newUser.userId
|
||||
}
|
||||
|
||||
@Watch('user.email', { immediate: true })
|
||||
public onEmailChange(email: string) {
|
||||
if (this.newUser) {
|
||||
this.newUser.slugEmail = slug(email || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user