master: change repo

This commit is contained in:
Julien Calixte
2019-08-22 11:50:32 +02:00
commit dbd63d341c
263 changed files with 26153 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 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" />&nbsp;
<awe-icon icon="exchange-alt" />&nbsp;
<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&nbsp;!
Votre partenaire idéal pour vos voyages&nbsp;! ⛺
</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>&nbsp;!
</p>
<p>
Ou vous pouvez également me supporter sur
<a
target="_blank"
rel="noreferrer"
:href="brave"
>Brave</a>&nbsp;!
</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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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>

View 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>

View 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>

View 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>

View 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>