mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-09-19 10:53:37 +00:00
591 lines
18 KiB
Vue
591 lines
18 KiB
Vue
<!--
|
|
- Create.vue
|
|
- Copyright (c) 2022 james@firefly-iii.org
|
|
-
|
|
- This file is part of Firefly III (https://github.com/firefly-iii).
|
|
-
|
|
- This program is free software: you can redistribute it and/or modify
|
|
- it under the terms of the GNU Affero General Public License as
|
|
- published by the Free Software Foundation, either version 3 of the
|
|
- License, or (at your option) any later version.
|
|
-
|
|
- This program is distributed in the hope that it will be useful,
|
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
- GNU Affero General Public License for more details.
|
|
-
|
|
- You should have received a copy of the GNU Affero General Public License
|
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
-->
|
|
|
|
<template>
|
|
<q-page>
|
|
<div class="row q-mx-md">
|
|
<div class="col-12">
|
|
<q-banner inline-actions rounded class="bg-orange text-white" v-if="'' !== errorMessage">
|
|
{{ errorMessage }}
|
|
<template v-slot:action>
|
|
<q-btn flat @click="dismissBanner" label="Dismiss"/>
|
|
</template>
|
|
</q-banner>
|
|
</div>
|
|
</div>
|
|
<div class="row q-mx-md q-mt-md">
|
|
<div class="col-xl-4 col-lg-6 col-md-12 col-xs-12 q-px-xs">
|
|
<q-card bordered>
|
|
<q-card-section>
|
|
<div class="text-h6">Basic options for recurring transaction</div>
|
|
</q-card-section>
|
|
<q-card-section>
|
|
<div class="row">
|
|
<div class="col-12 q-mb-xs">
|
|
<q-input
|
|
:error-message="submissionErrors.title"
|
|
:error="hasSubmissionErrors.title"
|
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="title" :label="$t('form.title')"
|
|
outlined/>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-12 q-mb-xs">
|
|
<q-select
|
|
:error-message="submissionErrors.type"
|
|
:error="hasSubmissionErrors.type"
|
|
bottom-slots
|
|
:disable="disabledInput"
|
|
outlined
|
|
v-model="type"
|
|
emit-value class="q-pr-xs"
|
|
map-options :options="types" label="Transaction type"/>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
</div>
|
|
<div class="col-xl-4 col-lg-6 col-md-12 col-xs-12 q-px-xs">
|
|
<q-card bordered>
|
|
<q-card-section>
|
|
<div class="text-h6">Repeat info</div>
|
|
</q-card-section>
|
|
<q-card-section>
|
|
<div class="row">
|
|
<div class="col-12 q-mb-xs">
|
|
<q-input
|
|
:error-message="submissionErrors.first_date"
|
|
:error="hasSubmissionErrors.first_date"
|
|
clearable
|
|
bottom-slots :disable="disabledInput" type="date" v-model="first_date" :label="$t('form.first_date')"
|
|
hint="The first date you want the recurrence"
|
|
outlined/>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-12 q-mb-xs">
|
|
<q-input
|
|
:error-message="submissionErrors.nr_of_repetitions"
|
|
:error="hasSubmissionErrors.nr_of_repetitions"
|
|
clearable
|
|
bottom-slots :disable="disabledInput" type="number" step="1" v-model="nr_of_repetitions"
|
|
:label="$t('form.repetitions')"
|
|
hint="nr_of_repetitions"
|
|
outlined/>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-12 q-mb-xs">
|
|
<q-input
|
|
:error-message="submissionErrors.repeat_until"
|
|
:error="hasSubmissionErrors.repeat_until"
|
|
bottom-slots :disable="disabledInput" type="date" v-model="repeat_until"
|
|
hint="repeat_until"
|
|
clearable
|
|
outlined/>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row q-mx-md q-mt-md">
|
|
<div class="col-xl-4 col-lg-6 col-md-12 col-xs-12 q-px-xs">
|
|
<q-card bordered>
|
|
<q-card-section>
|
|
<div class="text-h6">Single transaction</div>
|
|
</q-card-section>
|
|
<q-card-section>
|
|
|
|
<q-input
|
|
:error-message="submissionErrors.transactions[index].description"
|
|
:error="hasSubmissionErrors.transactions[index].description"
|
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="transactions[index].description"
|
|
:label="$t('form.description')"
|
|
outlined/>
|
|
|
|
<q-input
|
|
:error-message="submissionErrors.transactions[index].amount"
|
|
:error="hasSubmissionErrors.transactions[index].amount"
|
|
bottom-slots :disable="disabledInput" clearable :mask="balance_input_mask" reverse-fill-mask
|
|
hint="Expects #.##" fill-mask="0"
|
|
v-model="transactions[index].amount"
|
|
:label="$t('firefly.amount')" outlined/>
|
|
|
|
|
|
<q-select
|
|
:error-message="submissionErrors.transactions[index].source_id"
|
|
:error="hasSubmissionErrors.transactions[index].source_id"
|
|
v-model="transactions[index].source_id"
|
|
bottom-slots
|
|
:disable="loading"
|
|
outlined
|
|
emit-value class="q-pr-xs"
|
|
map-options :options="accounts" label="Source account"/>
|
|
|
|
<q-select
|
|
:error-message="submissionErrors.transactions[index].destination_id"
|
|
:error="hasSubmissionErrors.transactions[index].destination_id"
|
|
v-model="transactions[index].destination_id"
|
|
bottom-slots
|
|
:disable="disabledInput"
|
|
outlined
|
|
emit-value class="q-pr-xs"
|
|
map-options :options="accounts" label="Destination account"/>
|
|
</q-card-section>
|
|
</q-card>
|
|
</div>
|
|
<div class="col-xl-4 col-lg-6 col-md-12 col-xs-12 q-px-xs">
|
|
<q-card bordered>
|
|
<q-card-section>
|
|
<div class="text-h6">Single repetition</div>
|
|
</q-card-section>
|
|
<q-card-section>
|
|
<q-select
|
|
:error-message="submissionErrors.repetitions[index].type"
|
|
:error="hasSubmissionErrors.repetitions[index].type"
|
|
bottom-slots
|
|
emit-value
|
|
outlined
|
|
v-model="repetitions[index].type"
|
|
map-options :options="repetition_types" label="Type of repetition"/>
|
|
|
|
<q-input
|
|
:error-message="submissionErrors.repetitions[index].skip"
|
|
:error="hasSubmissionErrors.repetitions[index].skip"
|
|
bottom-slots :disable="disabledInput" clearable
|
|
v-model="repetitions[index].skip"
|
|
type="number"
|
|
min="0" max="31"
|
|
:label="$t('firefly.skip')" outlined
|
|
/>
|
|
|
|
<q-select
|
|
:error-message="submissionErrors.repetitions[index].weekend"
|
|
:error="hasSubmissionErrors.repetitions[index].weekend"
|
|
v-model="repetitions[index].weekend"
|
|
bottom-slots
|
|
:disable="disabledInput"
|
|
outlined
|
|
emit-value class="q-pr-xs"
|
|
map-options :options="weekends" label="Weekend?"/>
|
|
|
|
</q-card-section>
|
|
|
|
</q-card>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="row q-mx-md">
|
|
<div class="col-12 q-pa-xs">
|
|
<q-card class="q-mt-xs">
|
|
<q-card-section>
|
|
<div class="row">
|
|
<div class="col-12 text-right">
|
|
<q-btn :disable="disabledInput" color="primary" label="Submit" @click="submitRecurrence"/>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-12 text-right">
|
|
<q-checkbox :disable="disabledInput" v-model="doReturnHere" left-label
|
|
label="Return here to create another one"/>
|
|
<br/>
|
|
<q-checkbox v-model="doResetForm" left-label :disable="!doReturnHere || disabledInput"
|
|
label="Reset form after submission"/>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
</div>
|
|
</div>
|
|
|
|
</q-page>
|
|
</template>
|
|
|
|
<script>
|
|
import Post from "../../api/recurring/post";
|
|
// import {mapGetters} from "vuex";
|
|
import {getCacheKey} from "../../store/fireflyiii/getters";
|
|
import format from "date-fns/format";
|
|
import List from "../../api/accounts/list";
|
|
import {parseISO} from "date-fns";
|
|
|
|
export default {
|
|
name: 'Create',
|
|
data() {
|
|
return {
|
|
index: 0,
|
|
loading: true,
|
|
submissionErrors: {},
|
|
hasSubmissionErrors: {},
|
|
submitting: false,
|
|
doReturnHere: false,
|
|
doResetForm: false,
|
|
errorMessage: '',
|
|
balance_input_mask: '#.##',
|
|
types: [
|
|
{value: 'withdrawal', label: 'Withdrawal'},
|
|
{value: 'deposit', label: 'Deposit'},
|
|
{value: 'transfer', label: 'Transfer'},
|
|
],
|
|
weekends: [
|
|
{value: 1, label: 'dont care'},
|
|
{value: 2, label: 'skip creation'},
|
|
{value: 3, label: 'jump to previous friday'},
|
|
{value: 4, label: 'jump to next monday'},
|
|
],
|
|
repetition_types: [],
|
|
|
|
// info
|
|
accounts: [],
|
|
|
|
// recurrence fields:
|
|
title: '',
|
|
type: 'withdrawal',
|
|
first_date: '',
|
|
nr_of_repetitions: null,
|
|
repeat_until: null,
|
|
repetitions: {},
|
|
transactions: {}
|
|
}
|
|
},
|
|
watch: {
|
|
'first_date': function () {
|
|
// update actual single repetition value
|
|
this.recalculateRepetitions();
|
|
}
|
|
},
|
|
computed: {
|
|
// ...mapGetters('fireflyiii', ['getCacheKey']),
|
|
disabledInput: function () {
|
|
return this.submitting;
|
|
}
|
|
},
|
|
created() {
|
|
this.resetForm();
|
|
this.getAccounts();
|
|
this.recalculateRepetitions();
|
|
},
|
|
methods: {
|
|
// shared with Edit
|
|
recalculateRepetitions: function () {
|
|
console.log('recalculateRepetitions');
|
|
let date = parseISO(this.first_date + 'T00:00:00');
|
|
let xthDay = this.getXth(date);
|
|
this.repetition_types = [
|
|
{
|
|
value: 'daily',
|
|
label: 'Every day',
|
|
},
|
|
{
|
|
value: 'monthly',
|
|
label: 'Every month on the ' + format(date, 'do') + ' day',
|
|
},
|
|
{
|
|
value: 'ndom',
|
|
label: 'Every month on the ' + xthDay + '-th ' + format(date, 'EEEE'),
|
|
},
|
|
{
|
|
value: 'yearly',
|
|
label: 'Every year on ' + format(date, 'd MMMM'),
|
|
}
|
|
];
|
|
},
|
|
getXth: function (date) {
|
|
let expectedDay = format(date, 'EEEE');
|
|
let start = new Date(date);
|
|
let count = 0;
|
|
start.setDate(1);
|
|
const length = new Date(start.getFullYear(), start.getMonth() + 1, 0).getDate();
|
|
let loop = 1;
|
|
while ((start.getDate() <= length && date.getMonth() === start.getMonth()) || loop <= 32) {
|
|
loop++;
|
|
if (expectedDay === format(start, 'EEEE')) {
|
|
count++;
|
|
}
|
|
if (start.getDate() === date.getDate()) {
|
|
return count;
|
|
}
|
|
start.setDate(start.getDate() + 1);
|
|
}
|
|
return count;
|
|
},
|
|
|
|
resetForm: function () {
|
|
// default fields:
|
|
this.title = '';
|
|
this.type = 'withdrawal';
|
|
this.nr_of_repetitions = null;
|
|
this.repeat_until = null;
|
|
|
|
// first date field
|
|
let date = new Date;
|
|
date.setDate(date.getDate() + 1);
|
|
this.first_date = format(date, 'y-MM-dd');
|
|
|
|
// default repetition:
|
|
this.repetitions = [
|
|
{
|
|
type: 'daily',
|
|
moment: '',
|
|
skip: null,
|
|
weekend: 1,
|
|
}
|
|
];
|
|
|
|
// default transaction:
|
|
this.transactions = [
|
|
{
|
|
description: null,
|
|
amount: null,
|
|
foreign_amount: null,
|
|
currency_id: null, // TODO get default currency
|
|
currency_code: null,
|
|
foreign_currency_id: null,
|
|
foreign_currency_code: null,
|
|
budget_id: null,
|
|
category_id: null,
|
|
source_id: null,
|
|
destination_id: null,
|
|
tags: null,
|
|
piggy_bank_id: null,
|
|
}
|
|
];
|
|
this.resetErrors();
|
|
},
|
|
// same function as Edit
|
|
resetErrors: function () {
|
|
this.submissionErrors =
|
|
{
|
|
title: '',
|
|
type: '',
|
|
first_date: '',
|
|
nr_of_repetitions: '',
|
|
repeat_until: '',
|
|
transactions: [
|
|
{
|
|
description: '',
|
|
amount: '',
|
|
foreign_amount: '',
|
|
currency_id: '',
|
|
currency_code: '',
|
|
foreign_currency_id: '',
|
|
foreign_currency_code: '',
|
|
budget_id: '',
|
|
category_id: '',
|
|
source_id: '',
|
|
destination_id: '',
|
|
tags: '',
|
|
piggy_bank_id: '',
|
|
}
|
|
],
|
|
repetitions: [
|
|
{
|
|
type: '',
|
|
moment: '',
|
|
skip: '',
|
|
weekend: '',
|
|
}
|
|
],
|
|
};
|
|
this.hasSubmissionErrors = {
|
|
title: false,
|
|
type: false,
|
|
first_date: false,
|
|
nr_of_repetitions: false,
|
|
repeat_until: false,
|
|
transactions: [
|
|
{
|
|
description: false,
|
|
amount: false,
|
|
foreign_amount: false,
|
|
currency_id: false,
|
|
currency_code: false,
|
|
foreign_currency_id: false,
|
|
foreign_currency_code: false,
|
|
budget_id: false,
|
|
category_id: false,
|
|
source_id: false,
|
|
destination_id: false,
|
|
tags: false,
|
|
piggy_bank_id: false,
|
|
}
|
|
],
|
|
repetitions: [
|
|
{
|
|
type: false,
|
|
moment: false,
|
|
skip: false,
|
|
weekend: false,
|
|
}
|
|
],
|
|
};
|
|
},
|
|
submitRecurrence: function () {
|
|
this.submitting = true;
|
|
this.errorMessage = '';
|
|
|
|
// reset errors:
|
|
this.resetErrors();
|
|
|
|
// build category array
|
|
const submission = this.buildRecurrence();
|
|
(new Post())
|
|
.post(submission)
|
|
.catch(this.processErrors)
|
|
.then(this.processSuccess);
|
|
},
|
|
buildRecurrence: function () {
|
|
let result = {
|
|
title: this.title,
|
|
type: this.type,
|
|
first_date: this.first_date,
|
|
nr_of_repetitions: this.nr_of_repetitions,
|
|
repeat_until: this.repeat_until,
|
|
transactions: this.transactions,
|
|
repetitions: [],
|
|
};
|
|
// repetitions: this.repetitions,
|
|
for (let i in this.repetitions) {
|
|
if (this.repetitions.hasOwnProperty(i)) {
|
|
|
|
let moment = '';
|
|
let date = parseISO(this.first_date + 'T00:00:00');
|
|
// calculate moment for this type:
|
|
if ('monthly' === this.repetitions[i].type) {
|
|
moment = date.getDate().toString();
|
|
}
|
|
if ('ndom' === this.repetitions[i].type) {
|
|
let xthDay = this.getXth(date);
|
|
moment = xthDay + ',' + format(date, 'i');
|
|
}
|
|
if ('yearly' === this.repetitions[i].type) {
|
|
moment = format(date, 'yyyy-MM-dd');
|
|
}
|
|
|
|
|
|
result.repetitions.push(
|
|
{
|
|
type: this.repetitions[i].type,
|
|
moment: moment,
|
|
skip: this.repetitions[i].skip,
|
|
weekend: this.repetitions[i].weekend,
|
|
}
|
|
);
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
dismissBanner: function () {
|
|
this.errorMessage = '';
|
|
},
|
|
processSuccess: function (response) {
|
|
if (!response) {
|
|
return;
|
|
}
|
|
this.submitting = false;
|
|
let message = {
|
|
level: 'success',
|
|
text: 'I am new recurrence',
|
|
show: true,
|
|
action: {
|
|
show: true,
|
|
text: 'Go to recurrence',
|
|
link: {name: 'recurring.show', params: {id: parseInt(response.data.data.id)}}
|
|
}
|
|
};
|
|
// store flash
|
|
this.$q.localStorage.set('flash', message);
|
|
if (this.doReturnHere) {
|
|
window.dispatchEvent(new CustomEvent('flash', {
|
|
detail: {
|
|
flash: this.$q.localStorage.getItem('flash')
|
|
}
|
|
}));
|
|
}
|
|
if (!this.doReturnHere) {
|
|
// return to previous page.
|
|
this.$router.go(-1);
|
|
}
|
|
|
|
},
|
|
// todo this method is everywhere
|
|
processErrors: function (error) {
|
|
if (error.response) {
|
|
let errors = error.response.data; // => the response payload
|
|
this.errorMessage = errors.message;
|
|
for (let i in errors.errors) {
|
|
if (errors.errors.hasOwnProperty(i)) {
|
|
let errorKey = i;
|
|
if (errorKey.includes('.')) {
|
|
// it's a split
|
|
let parts = errorKey.split('.');
|
|
let series = parts[0];
|
|
let errorIndex = parseInt(parts[1]);
|
|
let errorField = parts[2];
|
|
this.submissionErrors[series][errorIndex][errorField] = errors.errors[i][0]
|
|
this.hasSubmissionErrors[series][errorIndex][errorField] = true;
|
|
}
|
|
if (!errorKey.includes('.')) {
|
|
this.submissionErrors[i] = errors.errors[i][0];
|
|
this.hasSubmissionErrors[i] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.submitting = false;
|
|
},
|
|
getAccounts: function () {
|
|
this.getPage(1);
|
|
},
|
|
getPage: function (page) {
|
|
(new List).list('all', page, this.getCacheKey).then((response) => {
|
|
let totalPages = parseInt(response.data.meta.pagination.total_pages);
|
|
|
|
// parse these accounts:
|
|
for (let i in response.data.data) {
|
|
if (response.data.data.hasOwnProperty(i)) {
|
|
let account = response.data.data[i];
|
|
this.accounts.push(
|
|
{
|
|
value: parseInt(account.id),
|
|
label: account.attributes.type + ': ' + account.attributes.name,
|
|
decimal_places: parseInt(account.attributes.currency_decimal_places)
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
if (page < totalPages) {
|
|
this.getPage(page + 1);
|
|
}
|
|
if (page === totalPages) {
|
|
this.loading = false;
|
|
this.accounts.sort((a, b) => (a.label > b.label) ? 1 : ((b.label > a.label) ? -1 : 0))
|
|
}
|
|
});
|
|
},
|
|
|
|
}
|
|
}
|
|
</script>
|