Files
vaquant/src/components/TransactionCreate.vue

494 lines
15 KiB
Vue

<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">
<currency-input
v-model="amount"
placeholder="Montant"
class="input"
:currency="null"
:locale="locale"
:precision="2"
:allow-negative="false"
:value-range="{ 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"
:class="{ 'has-addons': !!transaction.location }"
>
<div class="control set-location-input">
<input
type="text"
readonly
class="input"
v-if="transaction.location"
v-model="transaction.location.place"
/>
</div>
<div class="control set-location">
<button
class="button is-primary"
@click="displayLocationModal = true"
>
définir
</button>
</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" v-if="account.users.length > 1">
<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>
<div class="modal" :class="{ 'is-active': displayLocationModal }">
<div class="modal-background"></div>
<div class="modal-card">
<section>
<earth-map
v-if="displayLocationModal"
:locations="
transaction.location ? [transaction.location] : undefined
"
:define-location="true"
@located="located"
/>
</section>
<footer class="modal-card-foot">
<button
class="button is-primary"
:class="{ 'is-loading': isSetLocation }"
@click="validLocation"
>
valider
</button>
<button class="button" @click="displayLocationModal = false">
annuler
</button>
</footer>
</div>
</div>
<fab-button
@valid="submitTransaction"
:margin="true"
:button-style="backgroundColor"
>
<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 { CurrencyInput } from 'vue-currency-input'
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 queueNotifService from '@/services/QueueNotifService'
import { money } from '@/utils/filters'
import { findContrastColor } from '@/utils'
import ILocation from '@/models/ILocation'
import notif from '@/utils/notif'
import MapService from '../services/MapService'
import { format } from 'date-fns'
const formatToDate = (date: Date) => format(new Date(date), 'yyyy-MM-dd')
const today: Date = new Date()
@Component({
components: {
'currency-input': CurrencyInput,
'earth-map': () => import('@/components/EarthMap.vue'),
'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 locale!: string
@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 = formatToDate(today)
public date: string = formatToDate(today)
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 displayLocationModal = false
public isSetLocation = false
public location: ILocation | null = null
public async created(): Promise<void> {
this.setData(this.transaction)
}
public setData(transaction: ITransaction): void {
this.name = transaction.name
this.date = formatToDate(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(finish: () => void): Promise<void> {
if (!this.validate()) {
finish()
return
}
if (!this.currency || !this.account) {
finish()
return
}
const currencies: string[] = this.account.currencies.map(
(c: ICurrency) => c.code
)
const date: Date = new Date(this.date)
date.setHours(today.getHours(), today.getMinutes())
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,
location: this.transaction.location,
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 || '' }
})
notif.success('Dépense mise à jour.')
} else {
this.$router.push({
name: 'account',
params: { id: this.account._id || '' }
})
notif.success('Dépense créée.')
}
} else {
// tslint:disable-next-line:no-console
console.warn(response)
notif.error(`Une erreur s'est produite à la création d'une transaction.`)
}
finish()
}
public selectAll(): void {
if (!this.account) {
return
}
this.payFor.forEach((p: ISplit) => (p.weight = p.weight || 1))
}
public located(location: ILocation) {
this.location = location
}
public async validLocation() {
try {
this.isSetLocation = true
if (this.location) {
const place = await MapService.getPlace(this.location)
const location = {
...this.location,
place
}
this.$set<ILocation>(this.transaction, 'location', location)
}
} catch (error) {
this.$set(this.transaction, 'location', this.transaction.location)
} finally {
this.displayLocationModal = false
this.isSetLocation = false
}
}
public get payForUsers(): ISplit[] {
return this.payFor.filter((p, index) => p.weight > 0)
}
public get backgroundColor(): any | null {
if (!this.account || !this.account.color) {
return null
}
return {
backgroundColor: this.account.color,
color: findContrastColor(this.account.color) || 'black'
}
}
@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 {
const activeElement = document.activeElement
if (activeElement) {
const input = activeElement as HTMLInputElement
if (input.blur) {
input.blur()
}
}
const el = document.querySelector(':focus')
if (el) {
const input = el as HTMLInputElement
if (input.blur) {
input.blur()
}
}
if (
!this.name ||
!this.amount ||
!this.payBy ||
(!this.currency && !this.date && this.payFor.length === 0)
) {
if (this.amount === 0) {
queueNotifService.error('Le montant doit être supérieur à 0.')
} else {
queueNotifService.error('Tous les champs sont requis.')
}
return false
}
if (isNaN(this.amount) || this.amount > this.maxAmount) {
const maxAmount = money(this.maxAmount, this.currency)
queueNotifService.error(
`Veuillez saisir un montant numérique inférieur à ${maxAmount}.`
)
return false
}
if (this.amount < 0) {
queueNotifService.error('Le montant doit être supérieur à 0.')
return false
}
return true
}
}
</script>
<style scoped>
.help {
text-align: left;
}
.set-location {
text-align: center;
}
.set-location-input {
flex: 1;
}
</style>