feat!: rewrite frontend in Vue 3 with Pinia, DaisyUI, vite-plugin-pwa
Migrates every component from class-based vue-property-decorator to
<script setup> + Composition API. Replaces Vuex 3 + vuex-class with a
single Pinia store (persisted via pinia-plugin-persistedstate). Swaps
Bulma + bulma-{checkradio,switch,pricingtable} for DaisyUI 5 utilities
on Tailwind 4. Replaces register-service-worker with vite-plugin-pwa
(workbox, skipWaiting/clientsClaim preserved).
Plugins replaced:
- vue-class-component / vue-property-decorator -> <script setup>
- vuex / vuex-class / vuex-persist -> pinia + persistedstate
- vue-i18n 8 -> vue-i18n 11 (composition mode, legacy: false)
- vue-click-outside -> @vueuse/core onClickOutside
- @xkeshi/vue-qrcode -> qrcode.vue
- vue-currency-input 1 -> vue-currency-input 3 (composable wrapper)
- bus-event (Vue instance) -> mitt
- Vue filters -> plain functions imported per component
BREAKING: drops Stripe / pricing entirely (Payment, PricingTable,
/pricing route, vue-stripe-checkout, bulma-pricingtable).
Clears unused Cypress and Jest test scaffolding; leaves a Vitest
harness behind for future tests.
This commit is contained in:
@@ -1,6 +1,194 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, onBeforeMount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { format } from 'date-fns'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import type IAccount from '@/models/IAccount'
|
||||
import type ICurrency from '@/models/ICurrency'
|
||||
import type ITransaction from '@/models/ITransaction'
|
||||
import type ISplit from '@/models/ISplit'
|
||||
import type ILocation from '@/models/ILocation'
|
||||
import TransactionType from '@/enums/TransactionType'
|
||||
import TransactionTag, { TransactionTagLabel } from '@/enums/TransactionTag'
|
||||
import exchangeService from '@/services/ExchangeService'
|
||||
import transactionService from '@/services/TransactionService'
|
||||
import queueNotifService from '@/services/QueueNotifService'
|
||||
import mapService from '@/services/MapService'
|
||||
import { money } from '@/utils/format'
|
||||
import { findContrastColor } from '@/utils'
|
||||
import notif from '@/utils/notif'
|
||||
import FabButton from '@/components/FabButton.vue'
|
||||
import TransactionSplit from '@/components/TransactionSplit.vue'
|
||||
import TransactionTagUpdate from '@/components/TransactionTagUpdate.vue'
|
||||
import EarthMap from '@/components/EarthMap.vue'
|
||||
import CurrencyInput from '@/components/CurrencyInput.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
transaction: ITransaction
|
||||
account: IAccount
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
const { locale } = storeToRefs(userStore)
|
||||
|
||||
const formatToDate = (d: Date | string) => format(new Date(d), 'yyyy-MM-dd')
|
||||
const todayDate = new Date()
|
||||
|
||||
const name = ref('')
|
||||
const amount = ref<number | null>(null)
|
||||
const tag = ref<TransactionTag>(TransactionTag.None)
|
||||
const today = formatToDate(todayDate)
|
||||
const date = ref(today)
|
||||
const currency = ref<ICurrency | null>(null)
|
||||
const payBy = ref('')
|
||||
const payFor = reactive<ISplit[]>([])
|
||||
const maxAmount = 1_000_000
|
||||
const displayLocationModal = ref(false)
|
||||
const isSetLocation = ref(false)
|
||||
const location = ref<ILocation | null>(null)
|
||||
const noTag = TransactionTag.None
|
||||
|
||||
onBeforeMount(() => {
|
||||
setData(props.transaction)
|
||||
})
|
||||
|
||||
const setData = (t: ITransaction) => {
|
||||
name.value = t.name
|
||||
date.value = formatToDate(t.date)
|
||||
tag.value = t.tag
|
||||
amount.value = t.amount || null
|
||||
payBy.value = t.payBy
|
||||
payFor.splice(0, payFor.length, ...t.payFor)
|
||||
if (props.account.users) {
|
||||
props.account.users.forEach((u) => {
|
||||
if (!t.payFor.find((p) => p.alias === u.alias)) {
|
||||
payFor.push({ alias: u.alias || '', weight: 0 })
|
||||
}
|
||||
})
|
||||
}
|
||||
currency.value = t.mainCurrency
|
||||
}
|
||||
|
||||
const payForUsers = computed(() => payFor.filter((p) => p.weight > 0))
|
||||
|
||||
const backgroundColor = computed(() => {
|
||||
if (!props.account || !props.account.color) return undefined
|
||||
return {
|
||||
backgroundColor: props.account.color,
|
||||
color: findContrastColor(props.account.color) || 'black'
|
||||
}
|
||||
})
|
||||
|
||||
watch(payBy, (newValue, oldValue) => {
|
||||
if (oldValue && payFor.length !== props.account.users.length) {
|
||||
const old = payFor.find((p) => p.alias === oldValue)
|
||||
if (old) old.weight = 0
|
||||
}
|
||||
if (newValue) {
|
||||
const newPayBy = payFor.find((p) => p.alias === newValue)
|
||||
if (newPayBy) newPayBy.weight = newPayBy.weight || 1
|
||||
}
|
||||
})
|
||||
|
||||
const selectAll = () => {
|
||||
payFor.forEach((p) => (p.weight = p.weight || 1))
|
||||
}
|
||||
|
||||
const located = (loc: ILocation) => {
|
||||
location.value = loc
|
||||
}
|
||||
|
||||
const validLocation = async () => {
|
||||
try {
|
||||
isSetLocation.value = true
|
||||
if (location.value) {
|
||||
const place = await mapService.getPlace(location.value)
|
||||
props.transaction.location = { ...location.value, place }
|
||||
}
|
||||
} catch {
|
||||
props.transaction.location = props.transaction.location
|
||||
} finally {
|
||||
displayLocationModal.value = false
|
||||
isSetLocation.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const validate = (): boolean => {
|
||||
const el = document.querySelector(':focus') as HTMLInputElement | null
|
||||
el?.blur()
|
||||
if (!name.value || !amount.value || !payBy.value || (!currency.value && !date.value && payFor.length === 0)) {
|
||||
if (amount.value === 0) {
|
||||
queueNotifService.error('Le montant doit être supérieur à 0.')
|
||||
} else {
|
||||
queueNotifService.error('Tous les champs sont requis.')
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (isNaN(amount.value) || amount.value > maxAmount) {
|
||||
queueNotifService.error(`Veuillez saisir un montant numérique inférieur à ${money(maxAmount, currency.value)}.`)
|
||||
return false
|
||||
}
|
||||
if (amount.value < 0) {
|
||||
queueNotifService.error('Le montant doit être supérieur à 0.')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const submitTransaction = async (finish: () => void) => {
|
||||
if (!validate() || !currency.value || !props.account) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
const currencies = props.account.currencies.map((c) => c.code)
|
||||
const d = new Date(date.value)
|
||||
d.setHours(todayDate.getHours(), todayDate.getMinutes())
|
||||
const hasId = !!props.transaction._id
|
||||
const numericAmount = amount.value ? parseFloat(amount.value.toString().replace(',', '.')) : 0
|
||||
const transaction: ITransaction = {
|
||||
_id: props.transaction._id,
|
||||
_rev: props.transaction._rev,
|
||||
doctype: props.transaction.doctype,
|
||||
accountId: props.account._id || '',
|
||||
name: name.value,
|
||||
date: d,
|
||||
tag: tag.value,
|
||||
amount: numericAmount,
|
||||
payBy: payBy.value,
|
||||
payFor: payForUsers.value,
|
||||
userIds: props.account.userIds,
|
||||
mainCurrency: currency.value,
|
||||
currencies: props.account.currencies,
|
||||
transactionType: TransactionType.normal,
|
||||
location: props.transaction.location,
|
||||
exchange: await exchangeService.get(currency.value.code, d, currencies)
|
||||
}
|
||||
const response = hasId
|
||||
? await transactionService.save(transaction)
|
||||
: await transactionService.add(props.account._id || '', transaction)
|
||||
if (response.ok) {
|
||||
if (hasId) {
|
||||
router.push({ name: 'transaction', params: { id: props.transaction._id || '' } })
|
||||
notif.success('Dépense mise à jour.')
|
||||
} else {
|
||||
router.push({ name: 'account', params: { id: props.account._id || '' } })
|
||||
notif.success('Dépense créée.')
|
||||
}
|
||||
} else {
|
||||
console.warn(response)
|
||||
notif.error(`Une erreur s'est produite à la création d'une transaction.`)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="transaction-create">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs" v-if="account">
|
||||
<nav v-if="account" class="breadcrumbs text-sm mb-3" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: 'account', params: { id: account._id } }">
|
||||
@@ -8,9 +196,9 @@
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="tag && tag !== noTag">
|
||||
<a href="#" @click.prevent>{{ transactionTagLabel[tag].label }}</a>
|
||||
<a href="#" @click.prevent>{{ TransactionTagLabel[tag].label }}</a>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<li>
|
||||
<a href="#" @click.prevent>
|
||||
<span v-if="transaction._id">{{ name }}</span>
|
||||
<span v-else>Nouvelle dépense</span>
|
||||
@@ -18,476 +206,136 @@
|
||||
</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 class="grid md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="label w-24">Nom</label>
|
||||
<div class="flex-1">
|
||||
<input
|
||||
v-model.trim="name"
|
||||
type="text"
|
||||
maxlength="30"
|
||||
placeholder="Nom de la dépense"
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
<p class="text-xs text-info mt-1">
|
||||
{{ t('validation.max_char', { max: 30 - name.length }, 30 - name.length) }}
|
||||
</p>
|
||||
</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 class="flex items-center gap-3">
|
||||
<label class="label w-24">Montant</label>
|
||||
<div class="flex-1">
|
||||
<div class="join w-full">
|
||||
<CurrencyInput
|
||||
v-model="amount"
|
||||
:locale="locale"
|
||||
:precision="2"
|
||||
:allow-negative="false"
|
||||
:value-range="{ min: 0, max: 1000000 }"
|
||||
placeholder="Montant"
|
||||
class="join-item flex-1"
|
||||
/>
|
||||
<span v-if="account.currencies.length > 1" class="join-item">
|
||||
<select v-model="currency" name="currency" class="select select-bordered">
|
||||
<option v-for="(c, k) in account.currencies" :key="k" :value="c">
|
||||
{{ c.symbol || c.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<span v-else class="btn btn-disabled join-item">
|
||||
{{ account.mainCurrency.symbol || account.mainCurrency.name }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-info mt-1">
|
||||
montant max : {{ money(maxAmount, currency) }}
|
||||
</p>
|
||||
</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 class="flex items-center gap-3">
|
||||
<label class="label w-24">Localisation</label>
|
||||
<div class="flex-1 flex gap-2">
|
||||
<input
|
||||
v-if="transaction.location"
|
||||
:value="transaction.location.place"
|
||||
type="text"
|
||||
readonly
|
||||
class="input input-bordered flex-1"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="displayLocationModal = true">définir</button>
|
||||
</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 class="flex items-center gap-3">
|
||||
<label class="label w-24">Date</label>
|
||||
<input
|
||||
v-model="date"
|
||||
type="date"
|
||||
class="input input-bordered"
|
||||
placeholder="Date"
|
||||
:max="today"
|
||||
/>
|
||||
</div>
|
||||
<transaction-tag-update v-model="tag" />
|
||||
|
||||
<TransactionTagUpdate 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 v-if="account.users.length > 1" class="space-y-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="label w-24">Payé par</label>
|
||||
<select v-model="payBy" name="pay-by" class="select select-bordered flex-1">
|
||||
<option v-for="(u, k) in account.users" :key="k" :value="u.alias">
|
||||
{{ u.alias }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Pour</label>
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<label class="label w-24">Pour</label>
|
||||
<button
|
||||
class="button is-primary is-small"
|
||||
:class="{
|
||||
'is-outlined': payForUsers.length !== account.users.length
|
||||
}"
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
:class="{ 'btn-outline': payForUsers.length !== account.users.length }"
|
||||
@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 class="space-y-2">
|
||||
<div v-for="(split, k) in payFor" :key="k" class="user-item">
|
||||
<TransactionSplit :split="payFor[k]" />
|
||||
<hr v-if="k !== payFor.length - 1" />
|
||||
</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">
|
||||
|
||||
<dialog class="modal" :class="{ 'modal-open': displayLocationModal }">
|
||||
<div class="modal-box max-w-3xl">
|
||||
<EarthMap
|
||||
v-if="displayLocationModal"
|
||||
:locations="transaction.location ? [transaction.location] : undefined"
|
||||
:define-location="true"
|
||||
@located="located"
|
||||
/>
|
||||
<div class="modal-action">
|
||||
<button
|
||||
class="button is-primary"
|
||||
:class="{ 'is-loading': isSetLocation }"
|
||||
class="btn btn-primary"
|
||||
:class="{ 'btn-loading': isSetLocation }"
|
||||
@click="validLocation"
|
||||
>
|
||||
valider
|
||||
</button>
|
||||
<button class="button" @click="displayLocationModal = false">
|
||||
annuler
|
||||
</button>
|
||||
</footer>
|
||||
<button class="btn" @click="displayLocationModal = false">annuler</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<fab-button
|
||||
@valid="submitTransaction"
|
||||
:margin="true"
|
||||
:button-style="backgroundColor"
|
||||
>
|
||||
</dialog>
|
||||
|
||||
<FabButton :margin="true" :button-style="backgroundColor" @valid="submitTransaction">
|
||||
<vaquant-icon icon="check" />
|
||||
</fab-button>
|
||||
</FabButton>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user