Files
firefly-iii/frontend/src/pages/recurring/Create.vue
2022-05-07 19:26:37 +02:00

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>