mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-11-03 20:55:05 +00:00
Initial set of pages.
This commit is contained in:
31
frontend/src/pages/Error404.vue
Normal file
31
frontend/src/pages/Error404.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
|
||||||
|
<div>
|
||||||
|
<div style="font-size: 30vh">
|
||||||
|
404
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-h2" style="opacity:.4">
|
||||||
|
Oops. Nothing here...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
class="q-mt-xl"
|
||||||
|
color="white"
|
||||||
|
text-color="blue"
|
||||||
|
unelevated
|
||||||
|
to="/"
|
||||||
|
label="Go Home"
|
||||||
|
no-caps
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Error404'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
131
frontend/src/pages/Index.vue
Normal file
131
frontend/src/pages/Index.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="q-ma-md" v-if="0 === assetCount">
|
||||||
|
<NewUser
|
||||||
|
v-on:created-accounts="refreshThenCount"
|
||||||
|
></NewUser>
|
||||||
|
</div>
|
||||||
|
<div class="q-ma-md" v-if="assetCount > 0">
|
||||||
|
<Boxes></Boxes>
|
||||||
|
</div>
|
||||||
|
<div class="row q-ma-md" v-if="assetCount > 0">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Firefly III</div>
|
||||||
|
<div class="text-subtitle2">What's playing?</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<HomeChart></HomeChart>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="row q-ma-md">
|
||||||
|
<div class="col-6 q-pr-sm">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Budgets</div>
|
||||||
|
<div class="text-subtitle2">Subheader</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
Content
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-6 q-pl-sm">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Categories</div>
|
||||||
|
<div class="text-subtitle2">Subheader</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
Content
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">Expenses</div>
|
||||||
|
<div class="col-6">Income</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">Account X</div>
|
||||||
|
<div class="col-4">Account X</div>
|
||||||
|
<div class="col-4">Account X</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">Piggies</div>
|
||||||
|
<div class="col-6">Bills</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]" v-if="assetCount > 0">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up"
|
||||||
|
>
|
||||||
|
<!-- <q-fab-action color="primary" square icon="fas fa-bullseye" label="New piggy bank"/> -->
|
||||||
|
<q-fab-action color="primary" square icon="fas fa-chart-pie" label="New budget"
|
||||||
|
:to="{ name: 'budgets.create' }"/>
|
||||||
|
<!-- <q-fab-action color="primary" square icon="fas fa-home" label="New liability"/> -->
|
||||||
|
<q-fab-action color="primary" square icon="far fa-money-bill-alt" label="New asset account"
|
||||||
|
:to="{ name: 'accounts.create', params: {type: 'asset'} }"/>
|
||||||
|
<q-fab-action color="primary" square icon="fas fa-exchange-alt" label="New transfer"
|
||||||
|
:to="{ name: 'transactions.create', params: {type: 'transfer'} }"/>
|
||||||
|
<q-fab-action color="primary" square icon="fas fa-long-arrow-alt-right" label="New deposit"
|
||||||
|
:to="{ name: 'transactions.create', params: {type: 'deposit'} }"/>
|
||||||
|
<q-fab-action color="primary" square icon="fas fa-long-arrow-alt-left" label="New withdrawal"
|
||||||
|
:to="{ name: 'transactions.create', params: {type: 'withdrawal'} }"/>
|
||||||
|
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {defineAsyncComponent, defineComponent} from "vue";
|
||||||
|
import List from "../api/accounts/list";
|
||||||
|
import {mapGetters} from "vuex";
|
||||||
|
|
||||||
|
export default defineComponent(
|
||||||
|
{
|
||||||
|
name: 'PageIndex',
|
||||||
|
components: {
|
||||||
|
Boxes: defineAsyncComponent(() => import('./dashboard/Boxes.vue')),
|
||||||
|
HomeChart: defineAsyncComponent(() => import('./dashboard/HomeChart')),
|
||||||
|
NewUser: defineAsyncComponent(() => import('../components/dashboard/NewUser')),
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
assetCount: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getCacheKey']),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.countAssetAccounts();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
refreshThenCount: function() {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.countAssetAccounts();
|
||||||
|
},
|
||||||
|
countAssetAccounts: function () {
|
||||||
|
let list = new List;
|
||||||
|
list.list('asset',1, this.getCacheKey).then((response) => {
|
||||||
|
this.assetCount = parseInt(response.data.meta.pagination.total);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
217
frontend/src/pages/accounts/Create.vue
Normal file
217
frontend/src/pages/accounts/Create.vue
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<!--
|
||||||
|
- 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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Info for {{ $route.params.type }} {{ index }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.name"
|
||||||
|
:error="hasSubmissionErrors.name"
|
||||||
|
bottom-slots
|
||||||
|
:disable="disabledInput"
|
||||||
|
type="text" clearable v-model="name" :label="$t('form.name')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.iban"
|
||||||
|
:error="hasSubmissionErrors.iban"
|
||||||
|
mask="AA## XXXX XXXX XXXX XXXX XXXX XXXX XXXX XX"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="iban" :label="$t('form.iban')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="submitAccount"/>
|
||||||
|
</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/accounts/post";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Create",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
type: '',
|
||||||
|
// account fields:
|
||||||
|
name: '',
|
||||||
|
iban: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.resetForm();
|
||||||
|
this.type = this.$route.params.type;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetForm: function () {
|
||||||
|
this.name = '';
|
||||||
|
this.iban = '';
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
iban: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
name: false,
|
||||||
|
iban: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitAccount: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build account array
|
||||||
|
const submission = this.buildAccount();
|
||||||
|
|
||||||
|
let accounts = new Post();
|
||||||
|
accounts
|
||||||
|
.post(submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildAccount: function () {
|
||||||
|
let act = {
|
||||||
|
name: this.name,
|
||||||
|
iban: this.iban,
|
||||||
|
type: this.type,
|
||||||
|
};
|
||||||
|
if ('asset' === this.type) {
|
||||||
|
act.account_role = 'defaultAsset';
|
||||||
|
}
|
||||||
|
return act;
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'I am new account lol',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to account',
|
||||||
|
link: {name: 'accounts.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
215
frontend/src/pages/accounts/Edit.vue
Normal file
215
frontend/src/pages/accounts/Edit.vue
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<!--
|
||||||
|
- 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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Edit account</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.name"
|
||||||
|
:error="hasSubmissionErrors.name"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="name" :label="$t('form.name')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.iban"
|
||||||
|
:error="hasSubmissionErrors.iban"
|
||||||
|
mask="AA## XXXX XXXX XXXX XXXX XXXX XXXX XXXX XX"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="iban" :label="$t('form.iban')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="Update" @click="submitAccount"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
<q-checkbox :disable="disabledInput" v-model="doReturnHere" left-label label="Return here"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from '../../api/accounts/get';
|
||||||
|
import Put from '../../api/accounts/put';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Edit",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tab: 'split-0',
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
type: '',
|
||||||
|
// account fields:
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
iban: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.collectAccount();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
collectAccount: function() {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseAccount(response));
|
||||||
|
},
|
||||||
|
parseAccount: function(response) {
|
||||||
|
this.name = response.data.data.attributes.name;
|
||||||
|
this.iban = response.data.data.attributes.iban;
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
iban: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
name: false,
|
||||||
|
iban: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitAccount: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build account array
|
||||||
|
const submission = this.buildAccount();
|
||||||
|
|
||||||
|
let accounts = new Put();
|
||||||
|
accounts
|
||||||
|
.post(this.id, submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildAccount: function () {
|
||||||
|
let act = {
|
||||||
|
name: this.name,
|
||||||
|
iban: this.iban,
|
||||||
|
};
|
||||||
|
return act;
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'TODO I am updated lol',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to account',
|
||||||
|
link: {name: 'accounts.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
206
frontend/src/pages/accounts/Index.vue
Normal file
206
frontend/src/pages/accounts/Index.vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<q-table
|
||||||
|
:title="$t('firefly.' + this.type + '_accounts')"
|
||||||
|
:rows="rows"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="id"
|
||||||
|
@request="onRequest"
|
||||||
|
v-model:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
class="q-ma-md"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="name" :props="props">
|
||||||
|
<router-link :to="{ name: 'accounts.show', params: {id: props.row.id} }" class="text-primary">
|
||||||
|
{{ props.row.name }}
|
||||||
|
</router-link>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="iban" :props="props">
|
||||||
|
{{ formatIban(props.row.iban) }}
|
||||||
|
</q-td>
|
||||||
|
<q-td key="menu" :props="props">
|
||||||
|
<q-btn-dropdown color="primary" label="Actions" size="sm">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-close-popup :to="{name: 'accounts.edit', params: {id: props.row.id}}">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Edit</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup :to="{name: 'accounts.reconcile', params: {id: props.row.id}}" v-if="'asset' === props.row.type">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Reconcile</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="deleteAccount(props.row.id, props.row.name)">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Delete</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up"
|
||||||
|
>
|
||||||
|
<!--<q-fab-action color="primary" square :to="{ name: 'accounts.create', params: {type: 'liability'} }" icon="fas fa-long-arrow-alt-right" label="New liability"/>-->
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'accounts.create', params: {type: 'asset'} }" icon="fas fa-exchange-alt" label="New asset account"/>
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, useStore} from "vuex";
|
||||||
|
import List from "../../api/accounts/list";
|
||||||
|
import Destroy from "../../api/accounts/destroy";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
watch: {
|
||||||
|
$route(to) {
|
||||||
|
// react to route changes...
|
||||||
|
if ('accounts.index' === to.name) {
|
||||||
|
this.type = to.params.type;
|
||||||
|
this.page = 1;
|
||||||
|
this.updateBreadcrumbs();
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
type: 'asset',
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'desc',
|
||||||
|
descending: false,
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 5,
|
||||||
|
rowsNumber: 100
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
columns: [
|
||||||
|
{name: 'name', label: 'Name', field: 'name', align: 'left'},
|
||||||
|
{name: 'iban', label: 'IBAN', field: 'iban', align: 'left'},
|
||||||
|
{name: 'menu', label: ' ', field: 'menu', align: 'right'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getRange', 'getCacheKey', 'getListPageSize']),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.pagination.rowsPerPage = this.getListPageSize;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.type = this.$route.params.type;
|
||||||
|
if (null === this.getRange.start || null === this.getRange.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
const $store = useStore();
|
||||||
|
$store.subscribe((mutation, state) => {
|
||||||
|
if ('fireflyiii/setRange' === mutation.type) {
|
||||||
|
this.range = {start: mutation.payload.start, end: mutation.payload.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
this.range = {start: this.getRange.start, end: this.getRange.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteAccount: function (id, name) {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Do you want to delete account "' + name + '"? Any and all transactions linked to this account will ALSO be deleted.',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
this.destroyAccount(id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyAccount: function (id) {
|
||||||
|
let destr = new Destroy;
|
||||||
|
destr.destroy(id).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.triggerUpdate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateBreadcrumbs: function () {
|
||||||
|
this.$route.meta.pageTitle = 'firefly.' + this.type + '_accounts';
|
||||||
|
this.$route.meta.breadcrumbs = [{title: this.type + '_accounts'}];
|
||||||
|
|
||||||
|
},
|
||||||
|
onRequest: function (props) {
|
||||||
|
this.page = props.pagination.page;
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
formatIban: function (string) {
|
||||||
|
if (null === string) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// https://github.com/arhs/iban.js/blob/master/iban.js
|
||||||
|
let NON_ALPHANUM = /[^a-zA-Z0-9]/g,
|
||||||
|
EVERY_FOUR_CHARS = /(.{4})(?!$)/g;
|
||||||
|
return string.replace(NON_ALPHANUM, '').toUpperCase().replace(EVERY_FOUR_CHARS, "$1 ");
|
||||||
|
},
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (null === this.range.start || null === this.range.end) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
const list = new List();
|
||||||
|
this.rows = [];
|
||||||
|
list.list(this.type, this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
this.pagination.rowsPerPage = response.data.meta.pagination.per_page;
|
||||||
|
this.pagination.rowsNumber = response.data.meta.pagination.total;
|
||||||
|
this.pagination.page = this.page;
|
||||||
|
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
let account = {
|
||||||
|
id: current.id,
|
||||||
|
name: current.attributes.name,
|
||||||
|
iban: current.attributes.iban,
|
||||||
|
type: current.attributes.type,
|
||||||
|
};
|
||||||
|
this.rows.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
228
frontend/src/pages/accounts/Reconcile.vue
Normal file
228
frontend/src/pages/accounts/Reconcile.vue
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row q-mx-md" v-if="!canReconcile">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
This account cannot be reconciled :(
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-9 q-pr-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Reconcilliation range</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3 q-pr-xs">
|
||||||
|
<q-input outlined v-model="startDate" hint="Start date" type="date" dense>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="far fa-calendar"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 q-px-xs">
|
||||||
|
<q-input outlined v-model="startBalance" hint="Start balance" step="0.00" type="number" dense>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="fas fa-coins"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<q-input outlined v-model="endDate" hint="End date" type="date" dense>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="far fa-calendar"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 q-px-xs">
|
||||||
|
<q-input outlined v-model="endBalance" hint="End Balance" step="0.00" type="number" dense>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="fas fa-coins"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-9 q-px-xs">
|
||||||
|
Match the amounts and dates above to your bank statement, and press "Start reconciling"
|
||||||
|
</div>
|
||||||
|
<div class="col-3 q-px-xs">
|
||||||
|
<q-btn @click="initReconciliation">Start reconciling</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 q-pl-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Options</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
EUR {{ balanceDiff }}
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
Actions
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-ma-md">
|
||||||
|
<div class="col">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">
|
||||||
|
First verify the date-range and balances. Then press "Start reconciling"
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-page-scroller position="bottom-right" :offset="[16,16]" scroll-offset="120" v-if="canReconcile">
|
||||||
|
<div class="bg-primary text-white q-px-xl q-pa-md rounded-borders">EUR {{ balanceDiff }}</div>
|
||||||
|
</q-page-scroller>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import startOfMonth from "date-fns/startOfMonth";
|
||||||
|
import endOfMonth from "date-fns/endOfMonth";
|
||||||
|
import subDays from 'date-fns/subDays';
|
||||||
|
import format from "date-fns/format";
|
||||||
|
import Get from "../../api/accounts/get";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Reconcile",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
startDate: '',
|
||||||
|
startBalance: '0',
|
||||||
|
endDate: '',
|
||||||
|
endBalance: '0',
|
||||||
|
id: 0,
|
||||||
|
canReconcile: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
balanceDiff: function () {
|
||||||
|
return parseFloat(this.startBalance) - parseFloat(this.endBalance);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setDates();
|
||||||
|
this.collectBalances();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initReconciliation: function() {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Todo',
|
||||||
|
message: 'This function does not work yet.',
|
||||||
|
cancel: false,
|
||||||
|
persistent: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setDates: function () {
|
||||||
|
let today = new Date;
|
||||||
|
// TODO depends on view range.
|
||||||
|
let start = subDays(startOfMonth(today), 1);
|
||||||
|
let end = endOfMonth(today);
|
||||||
|
this.startDate = format(start, 'yyyy-MM-dd');
|
||||||
|
this.endDate = format(end, 'yyyy-MM-dd');
|
||||||
|
},
|
||||||
|
collectBalances: function () {
|
||||||
|
let getter = new Get;
|
||||||
|
getter.get(this.id, this.startDate).then((response) => {
|
||||||
|
if ('asset' !== response.data.data.attributes.type) {
|
||||||
|
this.canReconcile = false;
|
||||||
|
}
|
||||||
|
this.startBalance = response.data.data.attributes.current_balance;
|
||||||
|
});
|
||||||
|
|
||||||
|
getter.get(this.id, this.endDate).then((response) => {
|
||||||
|
this.endBalance = response.data.data.attributes.current_balance;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
121
frontend/src/pages/accounts/Show.vue
Normal file
121
frontend/src/pages/accounts/Show.vue
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<!--
|
||||||
|
- Show.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">
|
||||||
|
<!-- Balance chart -->
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">{{ account.name }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
Name: {{ account.name }}<br>
|
||||||
|
IBAN: {{ account.iban }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mt-sm">
|
||||||
|
<div class="col-12">
|
||||||
|
<LargeTable ref="table"
|
||||||
|
title="Transactions"
|
||||||
|
:rows="rows"
|
||||||
|
:loading="loading"
|
||||||
|
v-on:on-request="onRequest"
|
||||||
|
:rows-number="rowsNumber"
|
||||||
|
:rows-per-page="rowsPerPage"
|
||||||
|
:page="page"
|
||||||
|
>
|
||||||
|
</LargeTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/accounts/get";
|
||||||
|
import LargeTable from "../../components/transactions/LargeTable";
|
||||||
|
import Parser from "../../api/transactions/parser";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Show",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
account: {},
|
||||||
|
rows: [],
|
||||||
|
rowsNumber: 1,
|
||||||
|
rowsPerPage: 10,
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.getAccount();
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
//this.getAccount();
|
||||||
|
},
|
||||||
|
components: {LargeTable},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (payload) {
|
||||||
|
this.page = payload.page;
|
||||||
|
this.getAccount();
|
||||||
|
},
|
||||||
|
getAccount: function () {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseAccount(response));
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
const parser = new Parser;
|
||||||
|
this.rows = [];
|
||||||
|
|
||||||
|
get.transactions(this.id, this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
let resp = parser.parseResponse(response);
|
||||||
|
|
||||||
|
this.rowsPerPage = resp.rowsPerPage;
|
||||||
|
this.rowsNumber = resp.rowsNumber;
|
||||||
|
this.rows = resp.rows;
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parseAccount: function (response) {
|
||||||
|
this.account = {
|
||||||
|
name: response.data.data.attributes.name,
|
||||||
|
iban: response.data.data.attributes.iban
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
191
frontend/src/pages/admin/Index.vue
Normal file
191
frontend/src/pages/admin/Index.vue
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<q-card bordered class="q-mx-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Firefly III administration</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- TODO cloned from Preferences -->
|
||||||
|
configuration.permission_update_check
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-card bordered class="q-mx-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Firefly III information</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
Firefly III: {{ version }}<br>
|
||||||
|
API: {{ api }}<br>
|
||||||
|
OS: {{ os }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-xl-4 col-lg-6 col-md-12 q-pa-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Is demo site?
|
||||||
|
<span class="text-secondary" v-if="true === isOk.is_demo_site"><span
|
||||||
|
class="far fa-check-circle"></span></span>
|
||||||
|
<span class="text-blue" v-if="true === isLoading.is_demo_site"><span
|
||||||
|
class="fas fa-spinner fa-spin"></span></span>
|
||||||
|
<span class="text-red" v-if="true === isFailure.is_demo_site"><span
|
||||||
|
class="fas fa-skull-crossbones"></span> <small>Please refresh the page...</small></span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-checkbox v-model="isDemoSite" label="Is Demo Site?"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-4 col-lg-6 col-md-12 q-pa-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Single user mode?
|
||||||
|
<span class="text-secondary" v-if="true === isOk.single_user_mode"><span
|
||||||
|
class="far fa-check-circle"></span></span>
|
||||||
|
<span class="text-blue" v-if="true === isLoading.single_user_mode"><span
|
||||||
|
class="fas fa-spinner fa-spin"></span></span>
|
||||||
|
<span class="text-red" v-if="true === isFailure.single_user_mode"><span
|
||||||
|
class="fas fa-skull-crossbones"></span> <small>Please refresh the page...</small></span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-checkbox v-model="singleUserMode" label="Single user mode?"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-4 col-lg-6 col-md-12 q-pa-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Check for updates?
|
||||||
|
<span class="text-secondary" v-if="true === isOk.update_check"><span
|
||||||
|
class="far fa-check-circle"></span></span>
|
||||||
|
<span class="text-blue" v-if="true === isLoading.update_check"><span
|
||||||
|
class="fas fa-spinner fa-spin"></span></span>
|
||||||
|
<span class="text-red" v-if="true === isFailure.update_check"><span
|
||||||
|
class="fas fa-skull-crossbones"></span> <small>Please refresh the page...</small></span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-select
|
||||||
|
bottom-slots
|
||||||
|
outlined
|
||||||
|
v-model="permissionUpdateCheck" emit-value
|
||||||
|
map-options :options="permissions" label="Check for updates"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import About from "../../api/system/about";
|
||||||
|
import Configuration from "../../api/system/configuration";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
created() {
|
||||||
|
this.getInfo();
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.isOk = {
|
||||||
|
is_demo_site: true,
|
||||||
|
single_user_mode: true,
|
||||||
|
update_check: true,
|
||||||
|
};
|
||||||
|
this.isLoading = {
|
||||||
|
is_demo_site: false,
|
||||||
|
single_user_mode: false,
|
||||||
|
update_check: false,
|
||||||
|
};
|
||||||
|
this.isFailure = {
|
||||||
|
is_demo_site: false,
|
||||||
|
single_user_mode: false,
|
||||||
|
update_check: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// todo these methods PUT on the first load, but shouldn't.
|
||||||
|
isDemoSite: function (newValue, oldValue) {
|
||||||
|
if (oldValue !== newValue) {
|
||||||
|
let value = newValue;
|
||||||
|
(new Configuration()).put('configuration.is_demo_site', {value});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
singleUserMode: function (newValue, oldValue) {
|
||||||
|
if (oldValue !== newValue) {
|
||||||
|
let value = newValue;
|
||||||
|
(new Configuration()).put('configuration.single_user_mode', {value});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
permissionUpdateCheck: function (newValue, oldValue) {
|
||||||
|
if (oldValue !== newValue) {
|
||||||
|
let value = newValue;
|
||||||
|
(new Configuration()).put('configuration.permission_update_check', {value});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
version: '',
|
||||||
|
api: '',
|
||||||
|
os: '',
|
||||||
|
|
||||||
|
// settings
|
||||||
|
isDemoSite: false,
|
||||||
|
singleUserMode: true,
|
||||||
|
permissionUpdateCheck: -1,
|
||||||
|
|
||||||
|
// options
|
||||||
|
permissions: [
|
||||||
|
{value: -1, label: 'Ask me later'},
|
||||||
|
{value: 0, label: 'Lol no'},
|
||||||
|
{value: 1, label: 'Yes plz'},
|
||||||
|
],
|
||||||
|
|
||||||
|
// info for live update:
|
||||||
|
isOk: {},
|
||||||
|
isLoading: {},
|
||||||
|
isFailure: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getInfo: function () {
|
||||||
|
(new About).list().then((response) => {
|
||||||
|
this.version = response.data.data.version;
|
||||||
|
this.api = response.data.data.api_version;
|
||||||
|
this.os = response.data.data.os + ' with php ' + response.data.data.php_version;
|
||||||
|
});
|
||||||
|
(new Configuration).get('configuration.is_demo_site').then((response) => {
|
||||||
|
this.isDemoSite = response.data.data.value;
|
||||||
|
});
|
||||||
|
(new Configuration).get('configuration.single_user_mode').then((response) => {
|
||||||
|
this.singleUserMode = response.data.data.value;
|
||||||
|
});
|
||||||
|
(new Configuration).get('configuration.permission_update_check').then((response) => {
|
||||||
|
this.permissionUpdateCheck = response.data.data.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
170
frontend/src/pages/budgets/Create.vue
Normal file
170
frontend/src/pages/budgets/Create.vue
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Info for new budget</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.name"
|
||||||
|
:error="hasSubmissionErrors.name"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="name" :label="$t('form.name')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="submitBudget"/>
|
||||||
|
</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/budgets/post";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Create',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
type: '',
|
||||||
|
// budget fields:
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.resetForm();
|
||||||
|
this.type = this.$route.params.type;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetForm: function () {
|
||||||
|
this.name = '';
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
name: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitBudget: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build budget array
|
||||||
|
const submission = this.buildBudget();
|
||||||
|
|
||||||
|
let budgets = new Post();
|
||||||
|
budgets
|
||||||
|
.post(submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildBudget: function () {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'I am new budget',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to budget',
|
||||||
|
link: {name: 'budgets.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
177
frontend/src/pages/budgets/Edit.vue
Normal file
177
frontend/src/pages/budgets/Edit.vue
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Edit budget</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.name"
|
||||||
|
:error="hasSubmissionErrors.name"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="name" :label="$t('form.name')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="Update" @click="submitBudget"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
<q-checkbox :disable="disabledInput" v-model="doReturnHere" left-label label="Return here"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/budgets/get";
|
||||||
|
import Put from "../../api/budgets/put";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Edit",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
type: '',
|
||||||
|
// budget fields:
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.collectBudget();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
collectBudget: function() {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseBudget(response));
|
||||||
|
},
|
||||||
|
parseBudget: function(response) {
|
||||||
|
this.name = response.data.data.attributes.name;
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
name: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitBudget: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build account array
|
||||||
|
const submission = this.buildBudget();
|
||||||
|
|
||||||
|
let budgets = new Put();
|
||||||
|
budgets
|
||||||
|
.post(this.id, submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildBudget: function () {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'Budget is updated',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to budget',
|
||||||
|
link: {name: 'budgets.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
186
frontend/src/pages/budgets/Index.vue
Normal file
186
frontend/src/pages/budgets/Index.vue
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<q-table
|
||||||
|
:title="$t('firefly.budgets')"
|
||||||
|
:rows="rows"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="id"
|
||||||
|
@request="onRequest"
|
||||||
|
v-model:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
class="q-ma-md"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="name" :props="props">
|
||||||
|
<router-link :to="{ name: 'budgets.show', params: {id: props.row.id} }" class="text-primary">
|
||||||
|
{{ props.row.name }}
|
||||||
|
</router-link>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="menu" :props="props">
|
||||||
|
<q-btn-dropdown color="primary" label="Actions" size="sm">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-close-popup :to="{name: 'budgets.edit', params: {id: props.row.id}}">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Edit</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="deleteBudget(props.row.id, props.row.name)">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Delete</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<p>
|
||||||
|
<q-btn :to="{name: 'budgets.show', params: {id: 0}}">Transactions without a budget</q-btn>
|
||||||
|
</p>
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up"
|
||||||
|
>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'budgets.create'}" icon="fas fa-exchange-alt" label="New budget"/>
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, useStore} from "vuex";
|
||||||
|
import Destroy from "../../api/budgets/destroy";
|
||||||
|
import List from "../../api/budgets/list";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
watch: {
|
||||||
|
$route(to) {
|
||||||
|
// react to route changes...
|
||||||
|
if ('budgets.index' === to.name) {
|
||||||
|
this.page = 1;
|
||||||
|
this.updateBreadcrumbs();
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'desc',
|
||||||
|
descending: false,
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 5,
|
||||||
|
rowsNumber: 100
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
columns: [
|
||||||
|
{name: 'name', label: 'Name', field: 'name', align: 'left'},
|
||||||
|
{name: 'menu', label: ' ', field: 'menu', align: 'right'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getRange', 'getCacheKey', 'getListPageSize']),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.pagination.rowsPerPage = this.getListPageSize;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.type = this.$route.params.type;
|
||||||
|
if (null === this.getRange.start || null === this.getRange.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
const $store = useStore();
|
||||||
|
$store.subscribe((mutation, state) => {
|
||||||
|
if ('fireflyiii/setRange' === mutation.type) {
|
||||||
|
this.range = {start: mutation.payload.start, end: mutation.payload.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
this.range = {start: this.getRange.start, end: this.getRange.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteBudget: function (id, name) {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Do you want to delete budget "' + name + '"? Any and all transactions linked to this budget will be spared.',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
this.destroyBudget(id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyBudget: function (id) {
|
||||||
|
let destr = new Destroy;
|
||||||
|
destr.destroy(id).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.triggerUpdate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateBreadcrumbs: function () {
|
||||||
|
this.$route.meta.pageTitle = 'firefly.budgets';
|
||||||
|
this.$route.meta.breadcrumbs = [{title: 'budgets'}];
|
||||||
|
|
||||||
|
},
|
||||||
|
onRequest: function (props) {
|
||||||
|
this.page = props.pagination.page;
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (null === this.range.start || null === this.range.end) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
const list = new List();
|
||||||
|
this.rows = [];
|
||||||
|
list.list(this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
this.pagination.rowsPerPage = response.data.meta.pagination.per_page;
|
||||||
|
this.pagination.rowsNumber = response.data.meta.pagination.total;
|
||||||
|
this.pagination.page = this.page;
|
||||||
|
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
let account = {
|
||||||
|
id: current.id,
|
||||||
|
name: current.attributes.name,
|
||||||
|
};
|
||||||
|
this.rows.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
121
frontend/src/pages/budgets/Show.vue
Normal file
121
frontend/src/pages/budgets/Show.vue
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Balance chart -->
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">{{ budget.name }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
Name: {{ budget.name }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mt-sm">
|
||||||
|
<div class="col-12">
|
||||||
|
<LargeTable ref="table"
|
||||||
|
title="Transactions"
|
||||||
|
:rows="rows"
|
||||||
|
:loading="loading"
|
||||||
|
v-on:on-request="onRequest"
|
||||||
|
:rows-number="rowsNumber"
|
||||||
|
:rows-per-page="rowsPerPage"
|
||||||
|
:page="page"
|
||||||
|
>
|
||||||
|
</LargeTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LargeTable from "../../components/transactions/LargeTable";
|
||||||
|
import Get from "../../api/budgets/get";
|
||||||
|
import Parser from "../../api/transactions/parser";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Show",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
budget: {},
|
||||||
|
rows: [],
|
||||||
|
rowsNumber: 1,
|
||||||
|
rowsPerPage: 10,
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if ('no-budget' === this.$route.params.id) {
|
||||||
|
this.id = 0;
|
||||||
|
this.getWithoutBudget();
|
||||||
|
}
|
||||||
|
if ('no-budget' !== this.$route.params.id) {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.getBudget();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {LargeTable},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (payload) {
|
||||||
|
this.page = payload.page;
|
||||||
|
this.getBudget();
|
||||||
|
},
|
||||||
|
getWithoutBudget: function () {
|
||||||
|
this.budget = {name: '(without budget)'};
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
const parser = new Parser;
|
||||||
|
this.rows = [];
|
||||||
|
let get = new Get;
|
||||||
|
get.transactionsWithoutBudget(this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
let resp = parser.parseResponse(response);
|
||||||
|
|
||||||
|
this.rowsPerPage = resp.rowsPerPage;
|
||||||
|
this.rowsNumber = resp.rowsNumber;
|
||||||
|
this.rows = resp.rows;
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
},
|
||||||
|
getBudget: function () {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseBudget(response));
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
const parser = new Parser;
|
||||||
|
this.rows = [];
|
||||||
|
|
||||||
|
get.transactions(this.id, this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
let resp = parser.parseResponse(response);
|
||||||
|
|
||||||
|
this.rowsPerPage = resp.rowsPerPage;
|
||||||
|
this.rowsNumber = resp.rowsNumber;
|
||||||
|
this.rows = resp.rows;
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parseBudget: function (response) {
|
||||||
|
this.budget = {
|
||||||
|
name: response.data.data.attributes.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
170
frontend/src/pages/categories/Create.vue
Normal file
170
frontend/src/pages/categories/Create.vue
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Info for new category</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.name"
|
||||||
|
:error="hasSubmissionErrors.name"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="name" :label="$t('form.name')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="submitCategory"/>
|
||||||
|
</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/categories/post";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Create',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
type: '',
|
||||||
|
// category fields:
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.resetForm();
|
||||||
|
this.type = this.$route.params.type;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetForm: function () {
|
||||||
|
this.name = '';
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
name: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitCategory: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build category array
|
||||||
|
const submission = this.buildCategory();
|
||||||
|
|
||||||
|
let categories = new Post();
|
||||||
|
categories
|
||||||
|
.post(submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildCategory: function () {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'I am new category',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to category',
|
||||||
|
link: {name: 'categories.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
177
frontend/src/pages/categories/Edit.vue
Normal file
177
frontend/src/pages/categories/Edit.vue
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Edit category</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.name"
|
||||||
|
:error="hasSubmissionErrors.name"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="name" :label="$t('form.name')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="Update" @click="submitCategory"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
<q-checkbox :disable="disabledInput" v-model="doReturnHere" left-label label="Return here"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/categories/get";
|
||||||
|
import Put from "../../api/categories/put";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Edit",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
type: '',
|
||||||
|
// category fields:
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.collectCategory();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
collectCategory: function() {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseCategory(response));
|
||||||
|
},
|
||||||
|
parseCategory: function(response) {
|
||||||
|
this.name = response.data.data.attributes.name;
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
name: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitCategory: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build account array
|
||||||
|
const submission = this.buildCategory();
|
||||||
|
|
||||||
|
let categories = new Put();
|
||||||
|
categories
|
||||||
|
.post(this.id, submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildCategory: function () {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'Category is updated',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to category',
|
||||||
|
link: {name: 'categories.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
186
frontend/src/pages/categories/Index.vue
Normal file
186
frontend/src/pages/categories/Index.vue
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<q-table
|
||||||
|
:title="$t('firefly.categories')"
|
||||||
|
:rows="rows"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="id"
|
||||||
|
@request="onRequest"
|
||||||
|
v-model:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
class="q-ma-md"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="name" :props="props">
|
||||||
|
<router-link :to="{ name: 'categories.show', params: {id: props.row.id} }" class="text-primary">
|
||||||
|
{{ props.row.name }}
|
||||||
|
</router-link>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="menu" :props="props">
|
||||||
|
<q-btn-dropdown color="primary" label="Actions" size="sm">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-close-popup :to="{name: 'categories.edit', params: {id: props.row.id}}">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Edit</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="deleteCategory(props.row.id, props.row.name)">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Delete</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<p>
|
||||||
|
<q-btn :to="{name: 'categories.show', params: {id: 0}}">Transactions without a category</q-btn>
|
||||||
|
</p>
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up"
|
||||||
|
>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'categories.create'}" icon="fas fa-exchange-alt" label="New category"/>
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, useStore} from "vuex";
|
||||||
|
import Destroy from "../../api/categories/destroy";
|
||||||
|
import List from "../../api/categories/list";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
watch: {
|
||||||
|
$route(to) {
|
||||||
|
// react to route changes...
|
||||||
|
if ('categories.index' === to.name) {
|
||||||
|
this.page = 1;
|
||||||
|
this.updateBreadcrumbs();
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'desc',
|
||||||
|
descending: false,
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 5,
|
||||||
|
rowsNumber: 100
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
columns: [
|
||||||
|
{name: 'name', label: 'Name', field: 'name', align: 'left'},
|
||||||
|
{name: 'menu', label: ' ', field: 'menu', align: 'right'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getRange', 'getCacheKey', 'getListPageSize']),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.pagination.rowsPerPage = this.getListPageSize;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.type = this.$route.params.type;
|
||||||
|
if (null === this.getRange.start || null === this.getRange.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
const $store = useStore();
|
||||||
|
$store.subscribe((mutation, state) => {
|
||||||
|
if ('fireflyiii/setRange' === mutation.type) {
|
||||||
|
this.range = {start: mutation.payload.start, end: mutation.payload.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
this.range = {start: this.getRange.start, end: this.getRange.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteCategory: function (id, name) {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Do you want to delete category "' + name + '"? Any and all transactions linked to this category will be spared.',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
this.destroyCategory(id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyCategory: function (id) {
|
||||||
|
let destr = new Destroy;
|
||||||
|
destr.destroy(id).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.triggerUpdate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateBreadcrumbs: function () {
|
||||||
|
this.$route.meta.pageTitle = 'firefly.categories';
|
||||||
|
this.$route.meta.breadcrumbs = [{title: 'categories'}];
|
||||||
|
|
||||||
|
},
|
||||||
|
onRequest: function (props) {
|
||||||
|
this.page = props.pagination.page;
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (null === this.range.start || null === this.range.end) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
const list = new List();
|
||||||
|
this.rows = [];
|
||||||
|
list.list(this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
this.pagination.rowsPerPage = response.data.meta.pagination.per_page;
|
||||||
|
this.pagination.rowsNumber = response.data.meta.pagination.total;
|
||||||
|
this.pagination.page = this.page;
|
||||||
|
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
let account = {
|
||||||
|
id: current.id,
|
||||||
|
name: current.attributes.name,
|
||||||
|
};
|
||||||
|
this.rows.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
122
frontend/src/pages/categories/Show.vue
Normal file
122
frontend/src/pages/categories/Show.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Balance chart -->
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">{{ category.name }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
Name: {{ category.name }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mt-sm">
|
||||||
|
<div class="col-12">
|
||||||
|
<LargeTable ref="table"
|
||||||
|
title="Transactions"
|
||||||
|
:rows="rows"
|
||||||
|
:loading="loading"
|
||||||
|
v-on:on-request="onRequest"
|
||||||
|
:rows-number="rowsNumber"
|
||||||
|
:rows-per-page="rowsPerPage"
|
||||||
|
:page="page"
|
||||||
|
>
|
||||||
|
</LargeTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LargeTable from "../../components/transactions/LargeTable";
|
||||||
|
import Get from "../../api/categories/get";
|
||||||
|
import Parser from "../../api/transactions/parser";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Show",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
category: {},
|
||||||
|
rows: [],
|
||||||
|
rowsNumber: 1,
|
||||||
|
rowsPerPage: 10,
|
||||||
|
page: 1,
|
||||||
|
id: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if ('no-category' === this.$route.params.id) {
|
||||||
|
this.id = 0;
|
||||||
|
this.getWithoutCategory();
|
||||||
|
}
|
||||||
|
if ('no-category' !== this.$route.params.id) {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.getCategory();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {LargeTable},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (payload) {
|
||||||
|
this.page = payload.page;
|
||||||
|
this.getCategory();
|
||||||
|
},
|
||||||
|
getWithoutCategory: function () {
|
||||||
|
this.category = {name: '(without category)'};
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
const parser = new Parser;
|
||||||
|
this.rows = [];
|
||||||
|
let get = new Get;
|
||||||
|
get.transactionsWithoutCategory(this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
let resp = parser.parseResponse(response);
|
||||||
|
|
||||||
|
this.rowsPerPage = resp.rowsPerPage;
|
||||||
|
this.rowsNumber = resp.rowsNumber;
|
||||||
|
this.rows = resp.rows;
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
},
|
||||||
|
getCategory: function () {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseCategory(response));
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
const parser = new Parser;
|
||||||
|
this.rows = [];
|
||||||
|
|
||||||
|
get.transactions(this.id, this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
let resp = parser.parseResponse(response);
|
||||||
|
|
||||||
|
this.rowsPerPage = resp.rowsPerPage;
|
||||||
|
this.rowsNumber = resp.rowsNumber;
|
||||||
|
this.rows = resp.rows;
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parseCategory: function (response) {
|
||||||
|
this.category = {
|
||||||
|
name: response.data.data.attributes.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
201
frontend/src/pages/currencies/Create.vue
Normal file
201
frontend/src/pages/currencies/Create.vue
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Info for new currency</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.name"
|
||||||
|
:error="hasSubmissionErrors.name"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="name" :label="$t('form.name')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.code"
|
||||||
|
:error="hasSubmissionErrors.code"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="code" :label="$t('form.code')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.symbol"
|
||||||
|
:error="hasSubmissionErrors.symbol"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="symbol" :label="$t('form.symbol')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="submitCurrency"/>
|
||||||
|
</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/currencies/post";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Create',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
type: '',
|
||||||
|
// currency fields:
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
symbol: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.resetForm();
|
||||||
|
this.type = this.$route.params.type;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetForm: function () {
|
||||||
|
this.name = '';
|
||||||
|
this.code = '';
|
||||||
|
this.symbol = '';
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
symbol: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
name: false,
|
||||||
|
code: false,
|
||||||
|
symbol: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitCurrency: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build currency array
|
||||||
|
const submission = this.buildCurrency();
|
||||||
|
|
||||||
|
let currencies = new Post();
|
||||||
|
currencies
|
||||||
|
.post(submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildCurrency: function () {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
code: this.code,
|
||||||
|
symbol: this.symbol,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'I am new currency',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to currency',
|
||||||
|
link: {name: 'currencies.show', params: {code: parseInt(response.data.data.attributes.code)}}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
205
frontend/src/pages/currencies/Edit.vue
Normal file
205
frontend/src/pages/currencies/Edit.vue
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Edit currency</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.name"
|
||||||
|
:error="hasSubmissionErrors.name"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="name" :label="$t('form.name')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.code"
|
||||||
|
:error="hasSubmissionErrors.code"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="code" :label="$t('form.code')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.symbol"
|
||||||
|
:error="hasSubmissionErrors.symbol"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="symbol" :label="$t('form.symbol')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="Update" @click="submitCurrency"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
<q-checkbox :disable="disabledInput" v-model="doReturnHere" left-label label="Return here"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/currencies/get";
|
||||||
|
import Put from "../../api/currencies/put";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Edit",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
type: '',
|
||||||
|
// currency fields:
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
symbol: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.code = this.$route.params.code;
|
||||||
|
this.collectCurrency();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
collectCurrency: function() {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.code).then((response) => this.parseCurrency(response));
|
||||||
|
},
|
||||||
|
parseCurrency: function(response) {
|
||||||
|
this.name = response.data.data.attributes.name;
|
||||||
|
this.symbol = response.data.data.attributes.symbol;
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
symbol: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
name: false,
|
||||||
|
code: false,
|
||||||
|
symbol: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitCurrency: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build account array
|
||||||
|
const submission = this.buildCurrency();
|
||||||
|
|
||||||
|
let currencies = new Put();
|
||||||
|
currencies
|
||||||
|
.post(this.code, submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildCurrency: function () {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
code: this.code,
|
||||||
|
symbol: this.symbol
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'Currency is updated',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to currency',
|
||||||
|
link: {name: 'currencies.show', params: {code: response.data.data.code}}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
189
frontend/src/pages/currencies/Index.vue
Normal file
189
frontend/src/pages/currencies/Index.vue
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<q-table
|
||||||
|
:title="$t('firefly.currencies')"
|
||||||
|
:rows="rows"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="id"
|
||||||
|
@request="onRequest"
|
||||||
|
v-model:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
class="q-ma-md"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="name" :props="props">
|
||||||
|
<router-link :to="{ name: 'currencies.show', params: {code: props.row.code} }" class="text-primary">
|
||||||
|
{{ props.row.name }}
|
||||||
|
</router-link>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="name" :props="props">
|
||||||
|
{{ props.row.code }}
|
||||||
|
</q-td>
|
||||||
|
<q-td key="menu" :props="props">
|
||||||
|
<q-btn-dropdown color="primary" label="Actions" size="sm">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-close-popup :to="{name: 'currencies.edit', params: {code: props.row.code}}">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Edit</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="deleteCurrency(props.row.code, props.row.name)">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Delete</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up"
|
||||||
|
>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'currencies.create'}" icon="fas fa-exchange-alt" label="New currency"/>
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, useStore} from "vuex";
|
||||||
|
import Destroy from "../../api/currencies/destroy";
|
||||||
|
import List from "../../api/currencies/list";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
watch: {
|
||||||
|
$route(to) {
|
||||||
|
// react to route changes...
|
||||||
|
if ('currencies.index' === to.name) {
|
||||||
|
this.page = 1;
|
||||||
|
this.updateBreadcrumbs();
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'desc',
|
||||||
|
descending: false,
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 5,
|
||||||
|
rowsNumber: 100
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
columns: [
|
||||||
|
{name: 'name', label: 'Name', field: 'name', align: 'left'},
|
||||||
|
{name: 'name', label: 'Code', field: 'code', align: 'left'},
|
||||||
|
{name: 'menu', label: ' ', field: 'menu', align: 'right'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getRange', 'getCacheKey', 'getListPageSize']),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.pagination.rowsPerPage = this.getListPageSize;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.type = this.$route.params.type;
|
||||||
|
if (null === this.getRange.start || null === this.getRange.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
const $store = useStore();
|
||||||
|
$store.subscribe((mutation, state) => {
|
||||||
|
if ('fireflyiii/setRange' === mutation.type) {
|
||||||
|
this.range = {start: mutation.payload.start, end: mutation.payload.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
this.range = {start: this.getRange.start, end: this.getRange.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteCurrency: function (code, name) {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Do you want to delete currency "' + name + '"? Any and all transactions linked to this currency will be deleted as well.',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
this.destroyCurrency(code);
|
||||||
|
// TODO needs error catch.
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyCurrency: function (code) {
|
||||||
|
let destr = new Destroy;
|
||||||
|
destr.destroy(code).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.triggerUpdate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateBreadcrumbs: function () {
|
||||||
|
this.$route.meta.pageTitle = 'firefly.currencies';
|
||||||
|
this.$route.meta.breadcrumbs = [{title: 'currencies'}];
|
||||||
|
|
||||||
|
},
|
||||||
|
onRequest: function (props) {
|
||||||
|
this.page = props.pagination.page;
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (null === this.range.start || null === this.range.end) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
const list = new List();
|
||||||
|
this.rows = [];
|
||||||
|
list.list(this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
this.pagination.rowsPerPage = response.data.meta.pagination.per_page;
|
||||||
|
this.pagination.rowsNumber = response.data.meta.pagination.total;
|
||||||
|
this.pagination.page = this.page;
|
||||||
|
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
let account = {
|
||||||
|
id: current.id,
|
||||||
|
name: current.attributes.name,
|
||||||
|
code: current.attributes.code,
|
||||||
|
};
|
||||||
|
this.rows.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
99
frontend/src/pages/currencies/Show.vue
Normal file
99
frontend/src/pages/currencies/Show.vue
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Balance chart -->
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">{{ currency.name }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
Name: {{ currency.name }}<br>
|
||||||
|
Code: {{ currency.code }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mt-sm">
|
||||||
|
<div class="col-12">
|
||||||
|
<LargeTable ref="table"
|
||||||
|
title="Transactions"
|
||||||
|
:rows="rows"
|
||||||
|
:loading="loading"
|
||||||
|
v-on:on-request="onRequest"
|
||||||
|
:rows-number="rowsNumber"
|
||||||
|
:rows-per-page="rowsPerPage"
|
||||||
|
:page="page"
|
||||||
|
>
|
||||||
|
</LargeTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LargeTable from "../../components/transactions/LargeTable";
|
||||||
|
import Get from "../../api/currencies/get";
|
||||||
|
import Parser from "../../api/transactions/parser";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Show",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currency: {},
|
||||||
|
rows: [],
|
||||||
|
rowsNumber: 1,
|
||||||
|
rowsPerPage: 10,
|
||||||
|
page: 1,
|
||||||
|
code: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.code = this.$route.params.code;
|
||||||
|
this.getCurrency();
|
||||||
|
},
|
||||||
|
components: {LargeTable},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (payload) {
|
||||||
|
this.page = payload.page;
|
||||||
|
this.getCurrency();
|
||||||
|
},
|
||||||
|
getCurrency: function () {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.code).then((response) => this.parseCurrency(response));
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
const parser = new Parser;
|
||||||
|
this.rows = [];
|
||||||
|
|
||||||
|
get.transactions(this.code, this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
let resp = parser.parseResponse(response);
|
||||||
|
|
||||||
|
this.rowsPerPage = resp.rowsPerPage;
|
||||||
|
this.rowsNumber = resp.rowsNumber;
|
||||||
|
this.rows = resp.rows;
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parseCurrency: function (response) {
|
||||||
|
this.currency = {
|
||||||
|
name: response.data.data.attributes.name,
|
||||||
|
code: response.data.data.attributes.code,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
202
frontend/src/pages/dashboard/Boxes.vue
Normal file
202
frontend/src/pages/dashboard/Boxes.vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<!--
|
||||||
|
- Boxes.vue
|
||||||
|
- Copyright (c) 2021 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>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4 q-pr-sm q-pr-sm">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section class="q-pt-xs">
|
||||||
|
<div class="text-overline">
|
||||||
|
{{ $t('firefly.bills_to_pay') }}
|
||||||
|
<span class="float-right">
|
||||||
|
<span class="text-grey-4 fas fa-redo-alt" style="cursor: pointer;" @click="triggerForcedUpgrade"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pt-xs">
|
||||||
|
<span v-for="balance in prefBillsUnpaid">{{ balance.value_parsed }}</span>
|
||||||
|
<span v-for="(bill, index) in notPrefBillsUnpaid">
|
||||||
|
{{ bill.value_parsed }}<span v-if="index+1 !== notPrefBillsUnpaid.length">, </span>
|
||||||
|
</span>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 q-pr-sm q-pl-sm">
|
||||||
|
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section class="q-pt-xs">
|
||||||
|
<div class="text-overline">
|
||||||
|
{{ $t('firefly.left_to_spend') }}
|
||||||
|
<span class="float-right">
|
||||||
|
<span class="text-grey-4 fas fa-redo-alt" style="cursor: pointer;" @click="triggerForcedUpgrade"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pt-xs">
|
||||||
|
<!-- left to spend in preferred currency -->
|
||||||
|
<span v-for="left in prefLeftToSpend" :title="left.sub_title">{{ left.value_parsed }}</span>
|
||||||
|
<span v-for="(left, index) in notPrefLeftToSpend">
|
||||||
|
{{ left.value_parsed }}<span v-if="index+1 !== notPrefLeftToSpend.length">, </span>
|
||||||
|
</span>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 q-pl-sm">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section class="q-pt-xs">
|
||||||
|
<div class="text-overline">
|
||||||
|
{{ $t('firefly.net_worth') }}
|
||||||
|
<span class="float-right">
|
||||||
|
<span class="text-grey-4 fas fa-redo-alt" style="cursor: pointer;" @click="triggerForcedUpgrade"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pt-xs">
|
||||||
|
<span v-for="nw in prefNetWorth" :title="nw.sub_title">{{ nw.value_parsed }}</span>
|
||||||
|
<span v-for="(nw, index) in notPrefNetWorth">
|
||||||
|
{{ nw.value_parsed }}<span v-if="index+1 !== notPrefNetWorth.length">, </span>
|
||||||
|
</span>
|
||||||
|
<span v-if="0===notPrefNetWorth.length"> </span>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Basic from "src/api/summary/basic";
|
||||||
|
import {mapGetters, useStore} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Boxes',
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getCurrencyCode', 'getCurrencyId', 'getRange','getCacheKey']),
|
||||||
|
prefBillsUnpaid: function () {
|
||||||
|
return this.filterOnCurrency(this.billsUnpaid);
|
||||||
|
},
|
||||||
|
notPrefBillsUnpaid: function () {
|
||||||
|
return this.filterOnNotCurrency(this.billsUnpaid);
|
||||||
|
},
|
||||||
|
prefLeftToSpend: function () {
|
||||||
|
return this.filterOnCurrency(this.leftToSpend);
|
||||||
|
},
|
||||||
|
notPrefLeftToSpend: function () {
|
||||||
|
return this.filterOnNotCurrency(this.leftToSpend);
|
||||||
|
},
|
||||||
|
prefNetWorth: function () {
|
||||||
|
return this.filterOnCurrency(this.netWorth);
|
||||||
|
},
|
||||||
|
notPrefNetWorth: function () {
|
||||||
|
return this.filterOnNotCurrency(this.netWorth);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
summary: [],
|
||||||
|
billsPaid: [],
|
||||||
|
billsUnpaid: [],
|
||||||
|
leftToSpend: [],
|
||||||
|
netWorth: [],
|
||||||
|
range: {
|
||||||
|
start: null,
|
||||||
|
end: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
if (null === this.range.start || null === this.range.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
const $store = useStore();
|
||||||
|
$store.subscribe((mutation) => {
|
||||||
|
if ('fireflyiii/setRange' === mutation.type) {
|
||||||
|
this.range = mutation.payload;
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
this.start = this.getRange.start;
|
||||||
|
this.end = this.getRange.end;
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
triggerForcedUpgrade: function() {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
const basic = new Basic;
|
||||||
|
basic.list({start: this.getRange.start, end: this.getRange.end}, this.getCacheKey).then(data => {
|
||||||
|
this.netWorth = this.getKeyedEntries(data.data, 'net-worth-in-');
|
||||||
|
this.leftToSpend = this.getKeyedEntries(data.data, 'left-to-spend-in-');
|
||||||
|
this.billsPaid = this.getKeyedEntries(data.data, 'bills-paid-in-');
|
||||||
|
this.billsUnpaid = this.getKeyedEntries(data.data, 'bills-unpaid-in-');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getKeyedEntries(array, expected) {
|
||||||
|
let result = [];
|
||||||
|
for (const key in array) {
|
||||||
|
if (array.hasOwnProperty(key)) {
|
||||||
|
if (expected === key.substr(0, expected.length)) {
|
||||||
|
result.push(array[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
filterOnCurrency(array) {
|
||||||
|
let ret = [];
|
||||||
|
for (const key in array) {
|
||||||
|
if (array.hasOwnProperty(key)) {
|
||||||
|
if (array[key].currency_id === this.getCurrencyId) {
|
||||||
|
ret.push(array[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// or just the first one:
|
||||||
|
if (0 === ret.length && array.hasOwnProperty(0)) {
|
||||||
|
ret.push(array[0]);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
filterOnNotCurrency(array) {
|
||||||
|
let ret = [];
|
||||||
|
for (const key in array) {
|
||||||
|
if (array.hasOwnProperty(key)) {
|
||||||
|
if (array[key].currency_id !== this.getCurrencyId) {
|
||||||
|
ret.push(array[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
169
frontend/src/pages/dashboard/HomeChart.vue
Normal file
169
frontend/src/pages/dashboard/HomeChart.vue
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<!--
|
||||||
|
- HomeChart.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>
|
||||||
|
<div>
|
||||||
|
<ApexChart width="100%" ref="chart" height="350" type="line" :options="options" :series="series"></ApexChart>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {defineAsyncComponent} from "vue";
|
||||||
|
import Overview from '../../api/chart/account/overview';
|
||||||
|
import {mapGetters, useStore} from "vuex";
|
||||||
|
import format from "date-fns/format";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "HomeChart",
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getRange', 'getCacheKey']),
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
range: {
|
||||||
|
start: null,
|
||||||
|
end: null
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
currencies: [],
|
||||||
|
options: {
|
||||||
|
theme: {
|
||||||
|
mode: 'dark'
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
noData: {
|
||||||
|
text: 'Loading...'
|
||||||
|
},
|
||||||
|
chart: {
|
||||||
|
id: 'vuechart-home',
|
||||||
|
toolbar: {
|
||||||
|
show: true,
|
||||||
|
tools: {
|
||||||
|
download: false,
|
||||||
|
selection: false,
|
||||||
|
pan: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
formatter: this.numberFormatter
|
||||||
|
}
|
||||||
|
},
|
||||||
|
labels: [],
|
||||||
|
xaxis: {
|
||||||
|
categories: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [],
|
||||||
|
locale: 'en-US',
|
||||||
|
dateFormat: 'MMMM d, y',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
const $q = useQuasar();
|
||||||
|
this.locale = $q.lang.getLocale();
|
||||||
|
this.dateFormat = this.$t('config.month_and_day_fns');
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const $q = useQuasar();
|
||||||
|
this.options.theme.mode = $q.dark.isActive ? 'dark' : 'light';
|
||||||
|
if (null === this.range.start || null === this.range.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
const $store = useStore();
|
||||||
|
$store.subscribe((mutation, state) => {
|
||||||
|
if ('fireflyiii/setRange' === mutation.type) {
|
||||||
|
this.range = mutation.payload;
|
||||||
|
this.buildChart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
this.buildChart();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
numberFormatter: function (value, index) {
|
||||||
|
let currencyCode = this.currencies[index] ?? 'EUR';
|
||||||
|
return Intl.NumberFormat(this.locale, {style: 'currency', currency: currencyCode}).format(value);
|
||||||
|
},
|
||||||
|
buildChart: function () {
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
let start = this.getRange.start;
|
||||||
|
let end = this.getRange.end;
|
||||||
|
if (false === this.loading) {
|
||||||
|
this.loading = true;
|
||||||
|
const overview = new Overview();
|
||||||
|
// generate labels:
|
||||||
|
this.generateStaticLabels({start: start, end: end});
|
||||||
|
overview.overview({start: start, end: end}, this.getCacheKey).then(data => {
|
||||||
|
this.generateSeries(data.data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
generateSeries: function (data) {
|
||||||
|
this.series = [];
|
||||||
|
let series;
|
||||||
|
for (let i in data) {
|
||||||
|
if (data.hasOwnProperty(i)) {
|
||||||
|
series = {};
|
||||||
|
series.name = data[i].label;
|
||||||
|
series.data = [];
|
||||||
|
this.currencies.push(data[i].currency_code);
|
||||||
|
for (let ii in data[i].entries) {
|
||||||
|
series.data.push(data[i].entries[ii]);
|
||||||
|
}
|
||||||
|
this.series.push(series);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
generateStaticLabels: function (range) {
|
||||||
|
let loop = new Date(range.start);
|
||||||
|
let newDate;
|
||||||
|
let labels = [];
|
||||||
|
while (loop <= range.end) {
|
||||||
|
labels.push(format(loop, this.dateFormat));
|
||||||
|
newDate = loop.setDate(loop.getDate() + 1);
|
||||||
|
loop = new Date(newDate);
|
||||||
|
}
|
||||||
|
this.options = {
|
||||||
|
...this.options,
|
||||||
|
...{
|
||||||
|
labels: labels
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ApexChart: defineAsyncComponent(() => import('vue3-apexcharts')),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
79
frontend/src/pages/development/Index.vue
Normal file
79
frontend/src/pages/development/Index.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<!--
|
||||||
|
- Index.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-7">
|
||||||
|
<p>
|
||||||
|
Hi! With your active support and feedback I'm capable of building this fancy new layout. So thank you for testing and playing around.
|
||||||
|
I'm grateful for your help.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The <strong>v2</strong> layout was built to be perfect for each page. This new <strong>v3</strong> layout has a different approach. I'm
|
||||||
|
building a "minimum viable product", where each page has <em>minimal</em> functionality. But any functionality that's there should work. It
|
||||||
|
may not do everything you need and stuff may be missing. The things that you see are things that work.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you spot problems, feel free to report them. Here are some known issues.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li class="text-negative">You will lose data when you edit certain objects;</li>
|
||||||
|
<li>Caching is fairly aggressive and a page refresh may be necessary to get new information. This is especially obvious when you make new transactions
|
||||||
|
or accounts;
|
||||||
|
</li>
|
||||||
|
<li>Not all menu's are (un)folded correctly for all pages;</li>
|
||||||
|
<li>Breadcrumbs are missing or incorrect;</li>
|
||||||
|
<li>You can't make transaction splits;</li>
|
||||||
|
<li>Accounts, budgets, transactions, etc. have only limited fields available in the edit, create and view screens;</li>
|
||||||
|
<li>Occasionally, you may spot a "TODO". I've limited their presence, but sometimes I just need a placeholder;</li>
|
||||||
|
<li>Missing translations, <code>firefly.abc</code> references, or transactions formatted in another locale;</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
If you need to visit a <strong>v1</strong> alternative for the page you are seeing, please change the URL to
|
||||||
|
<code>*/profile</code> (where <code>*</code> is your Firefly III URL). From there, you can navigate to any <strong>v1</strong> page.
|
||||||
|
You may not be able to visit the v1 dashboard.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Tickets on GitHub that concern v3 will be <em class="text-negative">closed</em>. This rule may change in the future. Until then, please leave your feedback here:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://github.com/firefly-iii/firefly-iii/discussions/5589">GitHub discussion</a></li>
|
||||||
|
<li><a href="https://gitter.im/firefly-iii/firefly-iii">Gitter.im chat</a></li>
|
||||||
|
<li><a href="mailto:james@firefly-iii.org">james@firefly-iii.org</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Thanks again,<br>
|
||||||
|
James
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Index"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
54
frontend/src/pages/export/Index.vue
Normal file
54
frontend/src/pages/export/Index.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Export page</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<p>
|
||||||
|
Just to see if this works. Button defaults to this year.
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<p>
|
||||||
|
<q-btn @click="downloadTransactions">Download transactions</q-btn>
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Export from "../../api/data/export";
|
||||||
|
import startOfYear from "date-fns/startOfYear";
|
||||||
|
import endOfYear from "date-fns/endOfYear";
|
||||||
|
import format from "date-fns/format";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Index",
|
||||||
|
methods: {
|
||||||
|
downloadTransactions: function () {
|
||||||
|
let exp = new Export;
|
||||||
|
let start = format(startOfYear(new Date), 'yyyy-MM-dd');
|
||||||
|
let end = format(endOfYear(new Date), 'yyyy-MM-dd');
|
||||||
|
exp.transactions(start, end).then((response) => {
|
||||||
|
let label = 'export-transactions.csv';
|
||||||
|
const blob = new Blob([response.data], {type: 'application/octet-stream'})
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.download = label;
|
||||||
|
link.click()
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
177
frontend/src/pages/groups/Edit.vue
Normal file
177
frontend/src/pages/groups/Edit.vue
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Edit group</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>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="Update" @click="submitGroup"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
<q-checkbox :disable="disabledInput" v-model="doReturnHere" left-label label="Return here"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/groups/get";
|
||||||
|
import Put from "../../api/groups/put";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Edit",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
type: '',
|
||||||
|
// group fields:
|
||||||
|
id: 0,
|
||||||
|
title: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.collectGroup();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
collectGroup: function() {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseGroup(response));
|
||||||
|
},
|
||||||
|
parseGroup: function(response) {
|
||||||
|
this.title = response.data.data.attributes.title;
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
title: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitGroup: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build account array
|
||||||
|
const submission = this.buildGroup();
|
||||||
|
|
||||||
|
let groups = new Put();
|
||||||
|
groups
|
||||||
|
.post(this.id, submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildGroup: function () {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'Group is updated',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to group',
|
||||||
|
link: {name: 'groups.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
171
frontend/src/pages/groups/Index.vue
Normal file
171
frontend/src/pages/groups/Index.vue
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<q-table
|
||||||
|
:title="$t('firefly.object_groups')"
|
||||||
|
:rows="rows"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="id"
|
||||||
|
@request="onRequest"
|
||||||
|
v-model:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
class="q-ma-md"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="title" :props="props">
|
||||||
|
<router-link :to="{ name: 'groups.show', params: {id: props.row.id} }" class="text-primary">
|
||||||
|
{{ props.row.title }}
|
||||||
|
</router-link>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="menu" :props="props">
|
||||||
|
<q-btn-dropdown color="primary" label="Actions" size="sm">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-close-popup :to="{name: 'groups.edit', params: {id: props.row.id}}">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Edit</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="deleteGroup(props.row.id, props.row.title)">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Delete</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, useStore} from "vuex";
|
||||||
|
import Destroy from "../../api/groups/destroy";
|
||||||
|
import List from "../../api/groups/list";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
watch: {
|
||||||
|
$route(to) {
|
||||||
|
// react to route changes...
|
||||||
|
if ('groups.index' === to.name) {
|
||||||
|
this.page = 1;
|
||||||
|
this.updateBreadcrumbs();
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'desc',
|
||||||
|
descending: false,
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 5,
|
||||||
|
rowsNumber: 100
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
columns: [
|
||||||
|
{name: 'title', label: 'Title', field: 'title', align: 'left'},
|
||||||
|
{name: 'menu', label: ' ', field: 'menu', align: 'right'},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getRange', 'getCacheKey', 'getListPageSize']),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.pagination.rowsPerPage = this.getListPageSize;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.type = this.$route.params.type;
|
||||||
|
if (null === this.getRange.start || null === this.getRange.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
const $store = useStore();
|
||||||
|
$store.subscribe((mutation, state) => {
|
||||||
|
if ('fireflyiii/setRange' === mutation.type) {
|
||||||
|
this.range = {start: mutation.payload.start, end: mutation.payload.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
this.range = {start: this.getRange.start, end: this.getRange.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteGroup: function (code, title) {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Do you want to delete group "' + title + '"? Any resources in this group will be saved.',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
this.destroyGroup(code);
|
||||||
|
// TODO needs error catch.
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyGroup: function (code) {
|
||||||
|
let destr = new Destroy;
|
||||||
|
destr.destroy(code).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.triggerUpdate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateBreadcrumbs: function () {
|
||||||
|
this.$route.meta.pageTitle = 'firefly.groups';
|
||||||
|
this.$route.meta.breadcrumbs = [{title: 'groups'}];
|
||||||
|
|
||||||
|
},
|
||||||
|
onRequest: function (props) {
|
||||||
|
this.page = props.pagination.page;
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (null === this.range.start || null === this.range.end) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
const list = new List();
|
||||||
|
this.rows = [];
|
||||||
|
list.list(this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
this.pagination.rowsPerPage = response.data.meta.pagination.per_page;
|
||||||
|
this.pagination.rowsNumber = response.data.meta.pagination.total;
|
||||||
|
this.pagination.page = this.page;
|
||||||
|
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
let group = {
|
||||||
|
id: current.id,
|
||||||
|
title: current.attributes.title,
|
||||||
|
};
|
||||||
|
this.rows.push(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
58
frontend/src/pages/groups/Show.vue
Normal file
58
frontend/src/pages/groups/Show.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">{{ group.title }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
Title: {{ group.title }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/groups/get";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Show",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
group: {},
|
||||||
|
id: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.getGroup();
|
||||||
|
},
|
||||||
|
components: {},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (payload) {
|
||||||
|
this.page = payload.page;
|
||||||
|
this.getGroup();
|
||||||
|
},
|
||||||
|
getGroup: function () {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseGroup(response));
|
||||||
|
},
|
||||||
|
parseGroup: function (response) {
|
||||||
|
this.group = {
|
||||||
|
title: response.data.data.attributes.title,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
252
frontend/src/pages/piggy-banks/Create.vue
Normal file
252
frontend/src/pages/piggy-banks/Create.vue
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Info for new piggy bank</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.name"
|
||||||
|
:error="hasSubmissionErrors.name"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="name" :label="$t('form.name')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.account_id"
|
||||||
|
:error="hasSubmissionErrors.account_id"
|
||||||
|
bottom-slots
|
||||||
|
:disable="disabledInput"
|
||||||
|
outlined
|
||||||
|
v-model="account_id"
|
||||||
|
emit-value class="q-pr-xs"
|
||||||
|
map-options :options="accounts" label="Asset account"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.target_amount"
|
||||||
|
:error="hasSubmissionErrors.target_amount"
|
||||||
|
bottom-slots :disable="disabledInput" clearable :mask="balance_input_mask" reverse-fill-mask
|
||||||
|
hint="Expects #.##" fill-mask="0"
|
||||||
|
v-model="target_amount"
|
||||||
|
:label="$t('firefly.target_amount')" outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="submitPiggyBank"/>
|
||||||
|
</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/piggy-banks/post";
|
||||||
|
import List from "../../api/accounts/list";
|
||||||
|
import {mapGetters} from "vuex";
|
||||||
|
import {getCacheKey} from "../../store/fireflyiii/getters";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Create',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
balance_input_mask: '#.##',
|
||||||
|
|
||||||
|
// accounts
|
||||||
|
accounts: [],
|
||||||
|
|
||||||
|
// piggy bank fields:
|
||||||
|
name: '',
|
||||||
|
account_id: null,
|
||||||
|
target_amount: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
account_id: function (value) {
|
||||||
|
for (let key in this.accounts) {
|
||||||
|
if (this.accounts.hasOwnProperty(key)) {
|
||||||
|
let account = this.accounts[key];
|
||||||
|
if (account.value === value) {
|
||||||
|
let hash = '#';
|
||||||
|
this.balance_input_mask = '#.' + hash.repeat(account.decimal_places);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getCacheKey']),
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.resetForm();
|
||||||
|
this.getAccounts();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetForm: function () {
|
||||||
|
this.name = '';
|
||||||
|
this.account_id = '';
|
||||||
|
this.target_amount = '';
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
},
|
||||||
|
getAccounts: function () {
|
||||||
|
this.getAccountPage(1);
|
||||||
|
},
|
||||||
|
getAccountPage: function (page) {
|
||||||
|
|
||||||
|
(new List).list('asset', page, this.getCacheKey).then((response) => {
|
||||||
|
let totalPages = parseInt(response.data.meta.pagination.total_pages);
|
||||||
|
// get next page:
|
||||||
|
if (page < totalPages) {
|
||||||
|
this.getAccountPage(page + 1);
|
||||||
|
}
|
||||||
|
// 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.name,
|
||||||
|
decimal_places: parseInt(account.attributes.currency_decimal_places)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
account_id: '',
|
||||||
|
target_amount: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
name: false,
|
||||||
|
account_id: false,
|
||||||
|
target_amount: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitPiggyBank: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build category array
|
||||||
|
const submission = this.buildPiggyBank();
|
||||||
|
|
||||||
|
(new Post())
|
||||||
|
.post(submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildPiggyBank: function () {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
account_id: this.account_id,
|
||||||
|
target_amount: this.target_amount
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'I am new piggy',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to piggy',
|
||||||
|
link: {name: 'piggy-banks.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
176
frontend/src/pages/piggy-banks/Edit.vue
Normal file
176
frontend/src/pages/piggy-banks/Edit.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Edit piggy bank</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.name"
|
||||||
|
:error="hasSubmissionErrors.name"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="name" :label="$t('form.name')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="Update" @click="submitPiggyBank"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
<q-checkbox :disable="disabledInput" v-model="doReturnHere" left-label label="Return here"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/piggy-banks/get";
|
||||||
|
import Put from "../../api/piggy-banks/put";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Edit",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
|
||||||
|
// piggy bank fields:
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.collectPiggyBank();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
collectPiggyBank: function() {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parsePiggyBank(response));
|
||||||
|
},
|
||||||
|
parsePiggyBank: function(response) {
|
||||||
|
this.name = response.data.data.attributes.name;
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
name: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitPiggyBank: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build account array
|
||||||
|
const submission = this.buildPiggyBank();
|
||||||
|
|
||||||
|
(new Put())
|
||||||
|
.post(this.id, submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildPiggyBank: function () {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'Piggy is updated',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to piggy',
|
||||||
|
link: {name: 'piggy-banks.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
182
frontend/src/pages/piggy-banks/Index.vue
Normal file
182
frontend/src/pages/piggy-banks/Index.vue
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<q-table
|
||||||
|
:title="$t('firefly.piggy-banks')"
|
||||||
|
:rows="rows"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="id"
|
||||||
|
@request="onRequest"
|
||||||
|
v-model:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
class="q-ma-md"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="name" :props="props">
|
||||||
|
<router-link :to="{ name: 'piggy-banks.show', params: {id: props.row.id} }" class="text-primary">
|
||||||
|
{{ props.row.name }}
|
||||||
|
</router-link>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="menu" :props="props">
|
||||||
|
<q-btn-dropdown color="primary" label="Actions" size="sm">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-close-popup :to="{name: 'piggy-banks.edit', params: {id: props.row.id}}">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Edit</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="deletePiggyBank(props.row.id, props.row.name)">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Delete</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up"
|
||||||
|
>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'piggy-banks.create'}" icon="fas fa-exchange-alt" label="New piggy bank"/>
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, useStore} from "vuex";
|
||||||
|
import Destroy from "../../api/piggy-banks/destroy";
|
||||||
|
import List from "../../api/piggy-banks/list";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
watch: {
|
||||||
|
$route(to) {
|
||||||
|
// react to route changes...
|
||||||
|
if ('piggy-banks.index' === to.name) {
|
||||||
|
this.page = 1;
|
||||||
|
this.updateBreadcrumbs();
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'desc',
|
||||||
|
descending: false,
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 5,
|
||||||
|
rowsNumber: 100
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
columns: [
|
||||||
|
{name: 'name', label: 'Name', field: 'name', align: 'left'},
|
||||||
|
{name: 'menu', label: ' ', field: 'menu', align: 'right'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getRange', 'getCacheKey', 'getListPageSize']),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.pagination.rowsPerPage = this.getListPageSize;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (null === this.getRange.start || null === this.getRange.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
const $store = useStore();
|
||||||
|
$store.subscribe((mutation, state) => {
|
||||||
|
if ('fireflyiii/setRange' === mutation.type) {
|
||||||
|
this.range = {start: mutation.payload.start, end: mutation.payload.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
this.range = {start: this.getRange.start, end: this.getRange.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deletePiggyBank: function (id, name) {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Do you want to delete piggy bank "' + name + '"?',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
this.destroyPiggyBank(id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyPiggyBank: function (id) {
|
||||||
|
let destr = new Destroy;
|
||||||
|
destr.destroy(id).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.triggerUpdate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateBreadcrumbs: function () {
|
||||||
|
this.$route.meta.pageTitle = 'firefly.piggy-banks';
|
||||||
|
this.$route.meta.breadcrumbs = [{title: 'piggy-banks'}];
|
||||||
|
|
||||||
|
},
|
||||||
|
onRequest: function (props) {
|
||||||
|
this.page = props.pagination.page;
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (null === this.range.start || null === this.range.end) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
const list = new List();
|
||||||
|
this.rows = [];
|
||||||
|
list.list(this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
this.pagination.rowsPerPage = response.data.meta.pagination.per_page;
|
||||||
|
this.pagination.rowsNumber = response.data.meta.pagination.total;
|
||||||
|
this.pagination.page = this.page;
|
||||||
|
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
let account = {
|
||||||
|
id: current.id,
|
||||||
|
name: current.attributes.name,
|
||||||
|
};
|
||||||
|
this.rows.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
57
frontend/src/pages/piggy-banks/Show.vue
Normal file
57
frontend/src/pages/piggy-banks/Show.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Balance chart -->
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">{{ piggyBank.name }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
Name: {{ piggyBank.name }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/piggy-banks/get";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Show",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
piggyBank: {},
|
||||||
|
id: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.getPiggyBank();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (payload) {
|
||||||
|
this.page = payload.page;
|
||||||
|
this.getPiggyBank();
|
||||||
|
},
|
||||||
|
getPiggyBank: function () {
|
||||||
|
(new Get).get(this.id).then((response) => this.parsePiggyBank(response));
|
||||||
|
},
|
||||||
|
parsePiggyBank: function (response) {
|
||||||
|
this.piggyBank = {
|
||||||
|
name: response.data.data.attributes.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
390
frontend/src/pages/preferences/Index.vue
Normal file
390
frontend/src/pages/preferences/Index.vue
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-xl-4 col-lg-6 col-md-12 q-pa-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Language and locale
|
||||||
|
<span class="text-secondary" v-if="true === isOk.language"><span
|
||||||
|
class="far fa-check-circle"></span></span>
|
||||||
|
<span class="text-blue" v-if="true === isLoading.language"><span
|
||||||
|
class="fas fa-spinner fa-spin"></span></span>
|
||||||
|
<span class="text-red" v-if="true === isFailure.language"><span
|
||||||
|
class="fas fa-skull-crossbones"></span> <small>Please refresh the page...</small></span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
bottom-slots
|
||||||
|
outlined
|
||||||
|
v-model="language" emit-value
|
||||||
|
map-options :options="languages" label="I prefer the following language"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-4 col-lg-6 col-md-12 q-pa-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Accounts on the home screen
|
||||||
|
|
||||||
|
<span class="text-secondary" v-if="true === isOk.accounts"><span
|
||||||
|
class="far fa-check-circle"></span></span>
|
||||||
|
<span class="text-blue" v-if="true === isLoading.accounts"><span
|
||||||
|
class="fas fa-spinner fa-spin"></span></span>
|
||||||
|
<span class="text-red" v-if="true === isFailure.accounts"><span
|
||||||
|
class="fas fa-skull-crossbones"></span> <small>Please refresh the page...</small></span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-select
|
||||||
|
bottom-slots
|
||||||
|
outlined
|
||||||
|
multiple
|
||||||
|
use-chips
|
||||||
|
v-model="accounts" emit-value
|
||||||
|
map-options :options="allAccounts" label="I want to see these accounts on the dashboard"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-4 col-lg-6 col-md-12 q-pa-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">View range and list size
|
||||||
|
|
||||||
|
<span class="text-secondary" v-if="true === isOk.pageSize"><span
|
||||||
|
class="far fa-check-circle"></span></span>
|
||||||
|
<span class="text-blue" v-if="true === isLoading.pageSize"><span
|
||||||
|
class="fas fa-spinner fa-spin"></span></span>
|
||||||
|
<span class="text-red" v-if="true === isFailure.pageSize"><span
|
||||||
|
class="fas fa-skull-crossbones"></span> <small>Please refresh the page...</small></span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-input outlined v-model="pageSize" type="number" step="1" label="Page size"/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-select
|
||||||
|
bottom-slots
|
||||||
|
outlined
|
||||||
|
v-model="viewRange"
|
||||||
|
emit-value
|
||||||
|
map-options :options="viewRanges" label="Default period and view range"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-4 col-lg-6 col-md-12 q-pa-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Optional transaction fields
|
||||||
|
|
||||||
|
<span class="text-secondary" v-if="true === isOk.transactionFields"><span
|
||||||
|
class="far fa-check-circle"></span></span>
|
||||||
|
<span class="text-blue" v-if="true === isLoading.transactionFields"><span
|
||||||
|
class="fas fa-spinner fa-spin"></span></span>
|
||||||
|
<span class="text-red" v-if="true === isFailure.transactionFields"><span
|
||||||
|
class="fas fa-skull-crossbones"></span> <small>Please refresh the page...</small></span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-tabs
|
||||||
|
v-model="tab" dense
|
||||||
|
>
|
||||||
|
<q-tab name="date" label="Date fields"/>
|
||||||
|
<q-tab name="meta" label="Meta data fields"/>
|
||||||
|
<q-tab name="ref" label="Reference fields"/>
|
||||||
|
</q-tabs>
|
||||||
|
<q-tab-panels v-model="tab" animated swipeable>
|
||||||
|
<q-tab-panel name="date">
|
||||||
|
<q-option-group
|
||||||
|
:options="allTransactionFields.date"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="transactionFields.date"
|
||||||
|
/>
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="meta">
|
||||||
|
<q-option-group
|
||||||
|
:options="allTransactionFields.meta"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="transactionFields.meta"
|
||||||
|
/>
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="ref">
|
||||||
|
<q-option-group
|
||||||
|
:options="allTransactionFields.ref"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="transactionFields.ref"
|
||||||
|
/>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Configuration from "../../api/system/configuration";
|
||||||
|
import Put from "../../api/preferences/put";
|
||||||
|
import Preferences from "../../api/preferences";
|
||||||
|
import List from "../../api/accounts/list";
|
||||||
|
import {mapGetters} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
mounted() {
|
||||||
|
this.isOk = {
|
||||||
|
language: true,
|
||||||
|
accounts: true,
|
||||||
|
pageSize: true,
|
||||||
|
transactionFields: true,
|
||||||
|
};
|
||||||
|
this.isLoading = {
|
||||||
|
language: false,
|
||||||
|
accounts: false,
|
||||||
|
pageSize: false,
|
||||||
|
transactionFields: false,
|
||||||
|
};
|
||||||
|
this.isFailure = {
|
||||||
|
language: false,
|
||||||
|
accounts: false,
|
||||||
|
pageSize: false,
|
||||||
|
transactionFields: false,
|
||||||
|
};
|
||||||
|
// get select lists for certain preferences
|
||||||
|
this.getLanguages();
|
||||||
|
this.getLanguage();
|
||||||
|
this.getAssetAccounts().then(() => {
|
||||||
|
this.getPreferredAccounts()
|
||||||
|
});
|
||||||
|
this.getViewRanges().then(() => {
|
||||||
|
this.getPreferredViewRange()
|
||||||
|
});
|
||||||
|
this.getPageSize();
|
||||||
|
this.getOptionalFields();
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// data for select lists
|
||||||
|
languages: [],
|
||||||
|
allAccounts: [],
|
||||||
|
tab: 'date',
|
||||||
|
allTransactionFields: {
|
||||||
|
date: [
|
||||||
|
{label: 'Interest date', value: 'interest_date'},
|
||||||
|
{label: 'Book date', value: 'book_date'},
|
||||||
|
{label: 'Processing date', value: 'process_date'},
|
||||||
|
{label: 'Due date', value: 'due_date'},
|
||||||
|
{label: 'Payment date', value: 'payment_date'},
|
||||||
|
{label: 'Invoice date', value: 'invoice_date'},
|
||||||
|
],
|
||||||
|
meta: [
|
||||||
|
{label: 'Notes', value: 'notes'},
|
||||||
|
{label: 'Location', value: 'location'},
|
||||||
|
{label: 'Attachments', value: 'attachments'},
|
||||||
|
],
|
||||||
|
ref: [
|
||||||
|
{label: 'Internal reference', value: 'internal_reference'},
|
||||||
|
{label: 'Transaction links', value: 'links'},
|
||||||
|
{label: 'External URL', value: 'external_url'},
|
||||||
|
{label: 'External ID', value: 'external_id'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
viewRanges: [],
|
||||||
|
|
||||||
|
// is loading:
|
||||||
|
isOk: {},
|
||||||
|
isLoading: {},
|
||||||
|
isFailure: {},
|
||||||
|
|
||||||
|
// preferences:
|
||||||
|
language: 'en_US',
|
||||||
|
viewRange: '1M',
|
||||||
|
pageSize: 50,
|
||||||
|
accounts: [],
|
||||||
|
transactionFields: {
|
||||||
|
date: [],
|
||||||
|
meta: [],
|
||||||
|
ref: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
pageSize: function (value) {
|
||||||
|
this.isOk.language = false;
|
||||||
|
this.isLoading.language = true;
|
||||||
|
(new Put).put('listPageSize', value).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.isOk.pageSize = true;
|
||||||
|
this.isLoading.pageSize = false;
|
||||||
|
this.isFailure.pageSize = false;
|
||||||
|
}).catch(() => {
|
||||||
|
this.isOk.pageSize = false;
|
||||||
|
this.isLoading.pageSize = false;
|
||||||
|
this.isFailure.pageSize = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'transactionFields.date': function () {
|
||||||
|
this.submitTransactionFields();
|
||||||
|
},
|
||||||
|
'transactionFields.meta': function () {
|
||||||
|
this.submitTransactionFields();
|
||||||
|
},
|
||||||
|
'transactionFields.ref': function () {
|
||||||
|
this.submitTransactionFields();
|
||||||
|
},
|
||||||
|
language: function (value) {
|
||||||
|
this.isOk.language = false;
|
||||||
|
this.isLoading.language = true;
|
||||||
|
(new Put).put('language', value).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.isOk.language = true;
|
||||||
|
this.isLoading.language = false;
|
||||||
|
this.isFailure.language = false;
|
||||||
|
}).catch(() => {
|
||||||
|
this.isOk.language = false;
|
||||||
|
this.isLoading.language = false;
|
||||||
|
this.isFailure.language = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
accounts: function (value) {
|
||||||
|
(new Put).put('frontpageAccounts', value).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.isOk.accounts = true;
|
||||||
|
this.isLoading.accounts = false;
|
||||||
|
this.isFailure.accounts = false;
|
||||||
|
}).catch(() => {
|
||||||
|
this.isOk.accounts = false;
|
||||||
|
this.isLoading.accounts = false;
|
||||||
|
this.isFailure.accounts = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
viewRange: function (value) {
|
||||||
|
(new Put).put('viewRange', value).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.isOk.pageSize = true;
|
||||||
|
this.isLoading.pageSize = false;
|
||||||
|
this.isFailure.pageSize = false;
|
||||||
|
}).catch(() => {
|
||||||
|
this.isOk.pageSize = false;
|
||||||
|
this.isLoading.pageSize = false;
|
||||||
|
this.isFailure.pageSize = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getCacheKey']),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getAssetAccounts: function () {
|
||||||
|
return this.getAssetAccountPage(1);
|
||||||
|
},
|
||||||
|
getAssetAccountPage: function (page) {
|
||||||
|
return (new List).list('asset', page, this.getCacheKey).then((response) => {
|
||||||
|
let totalPages = parseInt(response.data.meta.pagination.total_pages);
|
||||||
|
|
||||||
|
// parse accounts:
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
this.allAccounts.push({value: parseInt(current.id), label: current.attributes.name});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (totalPages > page) {
|
||||||
|
this.getAssetAccountPage(page + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
submitTransactionFields: function() {
|
||||||
|
let submission = {};
|
||||||
|
for(let i in this.transactionFields) {
|
||||||
|
if(this.transactionFields.hasOwnProperty(i)) {
|
||||||
|
let set = this.transactionFields[i];
|
||||||
|
for(let ii in set) {
|
||||||
|
if(set.hasOwnProperty(ii)) {
|
||||||
|
let value = set[ii];
|
||||||
|
submission[value] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(new Put).put('transaction_journal_optional_fields', submission).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.isOk.transactionFields = true;
|
||||||
|
this.isLoading.transactionFields = false;
|
||||||
|
this.isFailure.transactionFields = false;
|
||||||
|
}).catch(() => {
|
||||||
|
this.isOk.transactionFields = false;
|
||||||
|
this.isLoading.transactionFields = false;
|
||||||
|
this.isFailure.transactionFields = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getOptionalFields: function () {
|
||||||
|
(new Preferences).getByName('transaction_journal_optional_fields').then((response) => {
|
||||||
|
let preferences = response.data.data.attributes.data;
|
||||||
|
for (let i in preferences) {
|
||||||
|
// loop over allTransactionFields
|
||||||
|
for (let ii in this.allTransactionFields) {
|
||||||
|
if (this.allTransactionFields.hasOwnProperty(ii)) {
|
||||||
|
let set = this.allTransactionFields[ii];
|
||||||
|
for (let iii in set) {
|
||||||
|
if (set.hasOwnProperty(iii)) {
|
||||||
|
let field = set[iii];
|
||||||
|
if (i === field.value && true === preferences[i]) {
|
||||||
|
this.transactionFields[ii].push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getLanguage: function () {
|
||||||
|
(new Preferences).getByName('language').then((response) => {
|
||||||
|
this.language = response.data.data.attributes.data;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getPageSize: function () {
|
||||||
|
(new Preferences).getByName('listPageSize').then((response) => {
|
||||||
|
this.pageSize = response.data.data.attributes.data;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getPreferredAccounts: function () {
|
||||||
|
(new Preferences).getByName('frontpageAccounts').then((response) => {
|
||||||
|
this.accounts = response.data.data.attributes.data;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getPreferredViewRange: function () {
|
||||||
|
(new Preferences).getByName('viewRange').then((response) => {
|
||||||
|
this.viewRange = response.data.data.attributes.data;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getLanguages: function () {
|
||||||
|
// get languages
|
||||||
|
let config = new Configuration();
|
||||||
|
config.get('firefly.languages').then((response) => {
|
||||||
|
let obj = response.data.data.value;
|
||||||
|
for (let key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
let lang = obj[key];
|
||||||
|
this.languages.push({value: key, label: lang.name_locale + ' (' + lang.name_english + ')'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getViewRanges: function () {
|
||||||
|
// get languages
|
||||||
|
let config = new Configuration();
|
||||||
|
return config.get('firefly.valid_view_ranges').then((response) => {
|
||||||
|
let obj = response.data.data.value;
|
||||||
|
for (let key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
let lang = obj[key];
|
||||||
|
this.viewRanges.push({value: lang, label: this.$t('firefly.pref_' + lang)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
26
frontend/src/pages/profile/Data.vue
Normal file
26
frontend/src/pages/profile/Data.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<!-- TODO Authentication different page -->
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-xl-4 col-lg-6 col-md-12 q-pa-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
Empty / TODO
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Data',
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
148
frontend/src/pages/profile/Index.vue
Normal file
148
frontend/src/pages/profile/Index.vue
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<!-- TODO Authentication different page -->
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-xl-4 col-lg-6 col-md-12 q-pa-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Email address</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-input outlined type="email" required v-model="emailAddress" label="Email address">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="fas fa-envelope"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
<p class="text-primary">
|
||||||
|
If you change your email address you will be logged out. You must confirm your address change before you can login again.
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions v-if="emailTouched">
|
||||||
|
<q-btn flat @click="confirmAddressChange">Change address</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="col-xl-4 col-lg-6 col-md-12 q-pa-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Password</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<p>
|
||||||
|
(input) (input)
|
||||||
|
</p>
|
||||||
|
<p class="text-primary">
|
||||||
|
Change password instructions here. Also needs logout. Button does not work.
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<div class="col-xl-4 col-lg-6 col-md-12 q-pa-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">2FA</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<p class="text-primary">
|
||||||
|
Here
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<div class="col-xl-4 col-lg-6 col-md-12 q-pa-xs">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Session management</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<p class="text-primary">
|
||||||
|
Explanation here
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
Logout one / Logout all
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up">
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'profile.data' }" icon="fas fa-database" label="Manage data"/>
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AboutUser from "../../api/system/user";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tab: 'mails',
|
||||||
|
id: 0,
|
||||||
|
emailAddress: '',
|
||||||
|
emailOriginal: '',
|
||||||
|
emailTouched: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
emailAddress: function (value) {
|
||||||
|
this.emailTouched = false;
|
||||||
|
if (this.emailOriginal !== value) {
|
||||||
|
this.emailTouched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getUserInfo();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getUserInfo: function () {
|
||||||
|
(new AboutUser).get().then((response) => {
|
||||||
|
this.emailAddress = response.data.data.attributes.email;
|
||||||
|
this.emailOriginal = response.data.data.attributes.email;
|
||||||
|
this.id = parseInt(response.data.data.id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
confirmAddressChange: function () {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Are you sure?',
|
||||||
|
cancel: true,
|
||||||
|
persistent: false
|
||||||
|
}).onOk(() => {
|
||||||
|
this.submitAddressChange();
|
||||||
|
}).onCancel(() => {
|
||||||
|
// console.log('>>>> Cancel')
|
||||||
|
}).onDismiss(() => {
|
||||||
|
// console.log('I am triggered on both OK and Cancel')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitAddressChange: function () {
|
||||||
|
(new AboutUser).put(this.id, {email: this.emailAddress})
|
||||||
|
.then((response) => {
|
||||||
|
(new AboutUser).logout();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
570
frontend/src/pages/recurring/Create.vue
Normal file
570
frontend/src/pages/recurring/Create.vue
Normal file
@@ -0,0 +1,570 @@
|
|||||||
|
<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>
|
||||||
583
frontend/src/pages/recurring/Edit.vue
Normal file
583
frontend/src/pages/recurring/Edit.vue
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
<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="disabledInput"
|
||||||
|
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
|
||||||
|
:disable="disabledInput"
|
||||||
|
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('form.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="submitRecurringTransaction"/>
|
||||||
|
</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 Get from "../../api/recurring/get";
|
||||||
|
import Put from "../../api/recurring/put";
|
||||||
|
import {parseISO} from "date-fns";
|
||||||
|
import format from "date-fns/format";
|
||||||
|
import List from "../../api/accounts/list";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Edit",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
loading: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
index: 0,
|
||||||
|
accounts: [],
|
||||||
|
balance_input_mask: '#.##', // shared with lots of methods.
|
||||||
|
|
||||||
|
// shared with Create
|
||||||
|
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: [],
|
||||||
|
|
||||||
|
|
||||||
|
// recurring transaction fields:
|
||||||
|
id: 0,
|
||||||
|
type: '',
|
||||||
|
title: '',
|
||||||
|
first_date: null,
|
||||||
|
nr_of_repetitions: 0,
|
||||||
|
repeat_until: '',
|
||||||
|
repetitions: {},
|
||||||
|
transactions: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'first_date': function () {
|
||||||
|
// update actual single repetition value
|
||||||
|
this.recalculateRepetitions();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting || this.loading;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// TODO some forms use 'loading' others use 'submitting' or 'disabledInput', needs to be the same.
|
||||||
|
created() {
|
||||||
|
this.loading = true;
|
||||||
|
this.resetErrors();
|
||||||
|
this.resetForm();
|
||||||
|
this.getAccounts().then(() => {
|
||||||
|
this.collectRecurringTransaction().then(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetForm: function () {
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// default repetition:
|
||||||
|
this.repetitions = [
|
||||||
|
{
|
||||||
|
type: 'daily',
|
||||||
|
moment: '',
|
||||||
|
skip: null,
|
||||||
|
weekend: 1,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
recalculateRepetitions: function () {
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
collectRecurringTransaction: function () {
|
||||||
|
let get = new Get;
|
||||||
|
return get.get(this.id).then((response) => this.parseRecurringTransaction(response));
|
||||||
|
},
|
||||||
|
parseRecurringTransaction: function (response) {
|
||||||
|
//this.name = response.data.data.attributes.name;
|
||||||
|
let info = response.data.data;
|
||||||
|
let attributes = info.attributes;
|
||||||
|
this.id = parseInt(info.id);
|
||||||
|
this.title = attributes.title;
|
||||||
|
this.type = attributes.type;
|
||||||
|
this.first_date = attributes.first_date.substr(0, 10);
|
||||||
|
this.nr_of_repetitions = attributes.nr_of_repetitions;
|
||||||
|
this.repeat_until = attributes.repeat_until ? attributes.repeat_until.substr(0, 10) : null;
|
||||||
|
|
||||||
|
// for the time being, only parse first transaction.
|
||||||
|
let ft = attributes.transactions[0];
|
||||||
|
this.transactions[0].description = ft.description;
|
||||||
|
this.transactions[0].amount = ft.amount;
|
||||||
|
this.transactions[0].source_id = parseInt(ft.source_id);
|
||||||
|
this.transactions[0].destination_id = parseInt(ft.destination_id);
|
||||||
|
|
||||||
|
// for the time being, only parse first repetition
|
||||||
|
let fr = attributes.repetitions[0];
|
||||||
|
this.repetitions[0].type = fr.type;
|
||||||
|
this.repetitions[0].weekend = fr.weekend;
|
||||||
|
this.repetitions[0].skip = fr.skip;
|
||||||
|
},
|
||||||
|
// same function as Create
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitRecurringTransaction: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build account array
|
||||||
|
const submission = this.buildRecurringTransaction();
|
||||||
|
|
||||||
|
(new Put())
|
||||||
|
.post(this.id, submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildRecurringTransaction: 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) {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'Recurrence is updated',
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
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)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
// same as Create
|
||||||
|
getAccounts: function () {
|
||||||
|
return this.getPage(1);
|
||||||
|
},
|
||||||
|
getPage: function (page) {
|
||||||
|
return (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) {
|
||||||
|
return 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>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
183
frontend/src/pages/recurring/Index.vue
Normal file
183
frontend/src/pages/recurring/Index.vue
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<q-table
|
||||||
|
:title="$t('firefly.recurring')"
|
||||||
|
:rows="rows"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="id"
|
||||||
|
@request="onRequest"
|
||||||
|
v-model:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
class="q-ma-md"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="name" :props="props">
|
||||||
|
<router-link :to="{ name: 'recurring.show', params: {id: props.row.id} }" class="text-primary">
|
||||||
|
{{ props.row.name }}
|
||||||
|
</router-link>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="menu" :props="props">
|
||||||
|
<q-btn-dropdown color="primary" label="Actions" size="sm">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-close-popup :to="{name: 'recurring.edit', params: {id: props.row.id}}">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Edit</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="deleteRecurring(props.row.id, props.row.name)">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Delete</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up"
|
||||||
|
>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'recurring.create'}" icon="fas fa-exchange-alt" label="New recurring transaction"/>
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, useStore} from "vuex";
|
||||||
|
import Destroy from "../../api/recurring/destroy";
|
||||||
|
import List from "../../api/recurring/list";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
watch: {
|
||||||
|
$route(to) {
|
||||||
|
// react to route changes...
|
||||||
|
if ('recurring.index' === to.name) {
|
||||||
|
this.page = 1;
|
||||||
|
this.updateBreadcrumbs();
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'desc',
|
||||||
|
descending: false,
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 5,
|
||||||
|
rowsNumber: 100
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
columns: [
|
||||||
|
{name: 'name', label: 'Name', field: 'name', align: 'left'},
|
||||||
|
{name: 'menu', label: ' ', field: 'menu', align: 'right'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getRange', 'getCacheKey', 'getListPageSize']),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.pagination.rowsPerPage = this.getListPageSize;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.type = this.$route.params.type;
|
||||||
|
if (null === this.getRange.start || null === this.getRange.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
const $store = useStore();
|
||||||
|
$store.subscribe((mutation, state) => {
|
||||||
|
if ('fireflyiii/setRange' === mutation.type) {
|
||||||
|
this.range = {start: mutation.payload.start, end: mutation.payload.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
this.range = {start: this.getRange.start, end: this.getRange.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteRecurring: function (id, name) {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Do you want to delete recurring transaction "' + name + '"?',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
this.destroyRecurring(id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyRecurring: function (id) {
|
||||||
|
let destr = new Destroy;
|
||||||
|
destr.destroy(id).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.triggerUpdate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateBreadcrumbs: function () {
|
||||||
|
this.$route.meta.pageTitle = 'firefly.Recurring';
|
||||||
|
this.$route.meta.breadcrumbs = [{title: 'Recurring'}];
|
||||||
|
|
||||||
|
},
|
||||||
|
onRequest: function (props) {
|
||||||
|
this.page = props.pagination.page;
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (null === this.range.start || null === this.range.end) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
const list = new List();
|
||||||
|
this.rows = [];
|
||||||
|
list.list(this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
this.pagination.rowsPerPage = response.data.meta.pagination.per_page;
|
||||||
|
this.pagination.rowsNumber = response.data.meta.pagination.total;
|
||||||
|
this.pagination.page = this.page;
|
||||||
|
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
let account = {
|
||||||
|
id: current.id,
|
||||||
|
name: current.attributes.title,
|
||||||
|
};
|
||||||
|
this.rows.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
57
frontend/src/pages/recurring/Show.vue
Normal file
57
frontend/src/pages/recurring/Show.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Balance chart -->
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">{{ recurrence.title }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
Title: {{ recurrence.title }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/recurring/get";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Show",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
recurrence: {},
|
||||||
|
id: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.getRecurring();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (payload) {
|
||||||
|
this.page = payload.page;
|
||||||
|
this.getRecurring();
|
||||||
|
},
|
||||||
|
getRecurring: function () {
|
||||||
|
(new Get).get(this.id).then((response) => this.parseRecurring(response));
|
||||||
|
},
|
||||||
|
parseRecurring: function (response) {
|
||||||
|
this.recurrence = {
|
||||||
|
title: response.data.data.attributes.title,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
15
frontend/src/pages/reports/Default.vue
Normal file
15
frontend/src/pages/reports/Default.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
Here be default report.
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Default"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
136
frontend/src/pages/reports/Index.vue
Normal file
136
frontend/src/pages/reports/Index.vue
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-4">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Reports</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-select
|
||||||
|
bottom-slots
|
||||||
|
outlined
|
||||||
|
v-model="type"
|
||||||
|
emit-value class="q-pr-xs"
|
||||||
|
map-options :options="types" label="Report type"/>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
bottom-slots
|
||||||
|
outlined
|
||||||
|
:disable="loading"
|
||||||
|
v-model="selectedAccounts"
|
||||||
|
class="q-pr-xs"
|
||||||
|
multiple
|
||||||
|
emit-value
|
||||||
|
use-chips
|
||||||
|
map-options :options="accounts" label="Included accounts"/>
|
||||||
|
<q-input
|
||||||
|
bottom-slots
|
||||||
|
type="date" v-model="start_date" :label="$t('form.start_date')"
|
||||||
|
hint="Start date"
|
||||||
|
outlined/>
|
||||||
|
<q-input
|
||||||
|
bottom-slots
|
||||||
|
type="date" v-model="end_date" :label="$t('form.start_date')"
|
||||||
|
hint="Start date"
|
||||||
|
outlined/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn :disable="loading || selectedAccounts.length < 1" @click="submit" color="primary" label="View report"/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import List from "../../api/accounts/list";
|
||||||
|
import {startOfMonth} from "date-fns";
|
||||||
|
import {format} from "date-fns";
|
||||||
|
import {endOfMonth} from "date-fns";
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
created() {
|
||||||
|
this.getAccounts();
|
||||||
|
this.start_date = format(startOfMonth(new Date), 'yyyy-MM-dd');
|
||||||
|
this.end_date = format(endOfMonth(new Date), 'yyyy-MM-dd');
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// is loading:
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
// report settings
|
||||||
|
type: 'default',
|
||||||
|
selectedAccounts: [],
|
||||||
|
accounts: [],
|
||||||
|
start_date: '',
|
||||||
|
end_date: '',
|
||||||
|
|
||||||
|
types: [
|
||||||
|
{value: 'default', label: 'Default financial report'},
|
||||||
|
// value="audit">Transaction history overview (audit)
|
||||||
|
// value="budget">Budget report</option>
|
||||||
|
// value="category">Category report</option>
|
||||||
|
// value="tag">Tag report</option>
|
||||||
|
// value="double">Expense/revenue account report</option> // to be dropped
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submit: function() {
|
||||||
|
let start = this.start_date.replace('-','');
|
||||||
|
let end = this.end_date.replace('-','');
|
||||||
|
let accounts = this.selectedAccounts.join(',');
|
||||||
|
if('default' === this.type) {
|
||||||
|
this.$router.push(
|
||||||
|
{name: 'reports.default',
|
||||||
|
params:
|
||||||
|
{
|
||||||
|
accounts: accounts,
|
||||||
|
start: start,
|
||||||
|
end: end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// duplicate function
|
||||||
|
getAccounts: function () {
|
||||||
|
this.loading = true;
|
||||||
|
this.getPage(1);
|
||||||
|
},
|
||||||
|
// duplicate function
|
||||||
|
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];
|
||||||
|
if ('liabilities' === account.attributes.type || 'asset' === account.attributes.type) {
|
||||||
|
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>
|
||||||
169
frontend/src/pages/rule-groups/Create.vue
Normal file
169
frontend/src/pages/rule-groups/Create.vue
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Info for new rule group</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.title"
|
||||||
|
:error="hasSubmissionErrors.title"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="title" :label="$t('form.title')"
|
||||||
|
outlined/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="submitRuleGroup"/>
|
||||||
|
</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/rule-groups/post";
|
||||||
|
import {mapGetters} from "vuex";
|
||||||
|
import {getCacheKey} from "../../store/fireflyiii/getters";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Create',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
|
||||||
|
// rule group fields:
|
||||||
|
title: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getCacheKey']),
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.resetForm();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetForm: function () {
|
||||||
|
this.title = '';
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
title: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitRuleGroup: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build category array
|
||||||
|
const submission = this.buildRuleGroup();
|
||||||
|
|
||||||
|
(new Post())
|
||||||
|
.post(submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildRuleGroup: function () {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'I am new rule group',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to piggy',
|
||||||
|
link: {name: 'rule-groups.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
176
frontend/src/pages/rule-groups/Edit.vue
Normal file
176
frontend/src/pages/rule-groups/Edit.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Edit rule group</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>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="Update" @click="submitRuleGroup"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
<q-checkbox :disable="disabledInput" v-model="doReturnHere" left-label label="Return here"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/rule-groups/get";
|
||||||
|
import Put from "../../api/rule-groups/put";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Edit",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
|
||||||
|
// rule group fields:
|
||||||
|
id: 0,
|
||||||
|
title: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.collectRuleGroup();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
collectRuleGroup: function() {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseRuleGroup(response));
|
||||||
|
},
|
||||||
|
parseRuleGroup: function(response) {
|
||||||
|
this.title = response.data.data.attributes.title;
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
title: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitRuleGroup: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build account array
|
||||||
|
const submission = this.buildRuleGroup();
|
||||||
|
|
||||||
|
(new Put())
|
||||||
|
.post(this.id, submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildRuleGroup: function () {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'Rule group is is updated',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to rule group', // todo
|
||||||
|
link: {name: 'rule.index'}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
573
frontend/src/pages/rules/Create.vue
Normal file
573
frontend/src/pages/rules/Create.vue
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Info for new rule</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.title"
|
||||||
|
:error="hasSubmissionErrors.title"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="title" :label="$t('form.title')"
|
||||||
|
outlined/>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.rule_group_id"
|
||||||
|
:error="hasSubmissionErrors.rule_group_id"
|
||||||
|
bottom-slots
|
||||||
|
:disable="disabledInput"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
v-model="rule_group_id"
|
||||||
|
class="q-pr-xs"
|
||||||
|
map-options :options="ruleGroups" label="Rule group"/>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.trigger"
|
||||||
|
:error="hasSubmissionErrors.trigger"
|
||||||
|
bottom-slots
|
||||||
|
:disable="disabledInput"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="trigger"
|
||||||
|
class="q-pr-xs"
|
||||||
|
map-options :options="initialTriggers" label="What fires a rule?"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md q-mt-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Triggers</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<strong>Trigger</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<strong>Trigger on value</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<strong>Active?</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<strong>Stop processing after a hit</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
del
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="(trigger, index) in triggers" class="row" :key="index">
|
||||||
|
<div class="col">
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.triggers[index].type"
|
||||||
|
:error="hasSubmissionErrors.triggers[index].type"
|
||||||
|
bottom-slots
|
||||||
|
:disable="disabledInput"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
v-model="trigger.type"
|
||||||
|
class="q-pr-xs"
|
||||||
|
map-options :options="availableTriggers" label="Trigger type"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.triggers[index].value"
|
||||||
|
:error="hasSubmissionErrors.triggers[index].value"
|
||||||
|
bottom-slots
|
||||||
|
dense
|
||||||
|
:disable="disabledInput"
|
||||||
|
v-if="trigger.type.needs_context"
|
||||||
|
type="text" clearable v-model="trigger.value" label="Trigger value"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-checkbox v-model="trigger.active"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-checkbox v-model="trigger.stop_processing"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-btn color="secondary" @click="removeTrigger(index)">Del</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn color="primary" @click="addTrigger">Add trigger</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md q-mt-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Actions</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<strong>Action</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<strong>Value</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<strong>Active?</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<strong>Stop processing other actions</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
del
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="(action, index) in actions" class="row" :key="index">
|
||||||
|
<div class="col">
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.actions[index].type"
|
||||||
|
:error="hasSubmissionErrors.actions[index].type"
|
||||||
|
bottom-slots
|
||||||
|
:disable="disabledInput"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
v-model="action.type"
|
||||||
|
class="q-pr-xs"
|
||||||
|
map-options :options="availableActions" label="Action type"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.actions[index].value"
|
||||||
|
:error="hasSubmissionErrors.actions[index].value"
|
||||||
|
bottom-slots
|
||||||
|
dense
|
||||||
|
:disable="disabledInput"
|
||||||
|
v-if="action.type.needs_context"
|
||||||
|
type="text" clearable v-model="action.value" label="Action value"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-checkbox v-model="action.active"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-checkbox v-model="action.stop_processing"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-btn color="secondary" @click="removeAction(index)">Del</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn color="primary" @click="addAction">Add action</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="submitRule"/>
|
||||||
|
</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/rules/post";
|
||||||
|
import {mapGetters} from "vuex";
|
||||||
|
import {getCacheKey} from "../../store/fireflyiii/getters";
|
||||||
|
import Configuration from "../../api/system/configuration";
|
||||||
|
import List from "../../api/rule-groups/list";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Create',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {
|
||||||
|
triggers: [],
|
||||||
|
actions: [],
|
||||||
|
},
|
||||||
|
hasSubmissionErrors: {
|
||||||
|
triggers: [],
|
||||||
|
actions: []
|
||||||
|
},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
|
||||||
|
// rule settings things:
|
||||||
|
ruleGroups: [],
|
||||||
|
availableTriggers: [],
|
||||||
|
availableActions: [],
|
||||||
|
initialTriggers: [],
|
||||||
|
|
||||||
|
// rule group fields:
|
||||||
|
title: '',
|
||||||
|
rule_group_id: null,
|
||||||
|
trigger: 'store-journal',
|
||||||
|
|
||||||
|
triggers: [],
|
||||||
|
actions: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getCacheKey']),
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.resetForm();
|
||||||
|
this.getRuleGroups();
|
||||||
|
this.getRuleTriggers();
|
||||||
|
this.getRuleActions();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addTrigger: function () {
|
||||||
|
this.triggers.push(
|
||||||
|
this.getDefaultTrigger()
|
||||||
|
);
|
||||||
|
this.submissionErrors.triggers.push(
|
||||||
|
this.getDefaultTriggerError()
|
||||||
|
);
|
||||||
|
this.hasSubmissionErrors.triggers.push(
|
||||||
|
this.getDefaultHasTriggerError()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
addAction: function () {
|
||||||
|
this.actions.push(
|
||||||
|
this.getDefaultAction()
|
||||||
|
);
|
||||||
|
this.submissionErrors.actions.push(
|
||||||
|
this.getDefaultActionError()
|
||||||
|
);
|
||||||
|
this.hasSubmissionErrors.actions.push(
|
||||||
|
this.getDefaultHasActionError()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getDefaultTriggerError: function () {
|
||||||
|
return {
|
||||||
|
type: '',
|
||||||
|
value: '',
|
||||||
|
stop_processing: '',
|
||||||
|
active: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getDefaultActionError: function () {
|
||||||
|
return {
|
||||||
|
type: '',
|
||||||
|
value: '',
|
||||||
|
stop_processing: '',
|
||||||
|
active: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getDefaultHasTriggerError: function () {
|
||||||
|
return {
|
||||||
|
type: false,
|
||||||
|
value: false,
|
||||||
|
stop_processing: false,
|
||||||
|
active: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getDefaultHasActionError: function () {
|
||||||
|
return {
|
||||||
|
type: false,
|
||||||
|
value: false,
|
||||||
|
stop_processing: false,
|
||||||
|
active: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
removeTrigger: function (index) {
|
||||||
|
this.triggers.splice(index, 1);
|
||||||
|
this.submissionErrors.triggers.splice(index, 1);
|
||||||
|
this.hasSubmissionErrors.triggers.splice(index, 1);
|
||||||
|
},
|
||||||
|
removeAction: function (index) {
|
||||||
|
this.actions.splice(index, 1);
|
||||||
|
this.submissionErrors.actions.splice(index, 1);
|
||||||
|
this.hasSubmissionErrors.actions.splice(index, 1);
|
||||||
|
},
|
||||||
|
getDefaultTrigger: function () {
|
||||||
|
return {
|
||||||
|
type: {
|
||||||
|
value: 'description_is',
|
||||||
|
needs_context: true,
|
||||||
|
label: this.$t('firefly.rule_trigger_description_is_choice')
|
||||||
|
},
|
||||||
|
value: '',
|
||||||
|
stop_processing: false,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getDefaultAction: function () {
|
||||||
|
return {
|
||||||
|
type: {
|
||||||
|
value: 'add_tag',
|
||||||
|
needs_context: true,
|
||||||
|
label: this.$t('firefly.rule_action_add_tag_choice')
|
||||||
|
},
|
||||||
|
value: '',
|
||||||
|
stop_processing: false,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getRuleTriggers: function () {
|
||||||
|
let config = new Configuration;
|
||||||
|
config.get('firefly.search.operators').then((response) => {
|
||||||
|
for (let i in response.data.data.value) {
|
||||||
|
if (response.data.data.value.hasOwnProperty(i)) {
|
||||||
|
let trigger = response.data.data.value[i];
|
||||||
|
if (false === trigger.alias && i !== 'user_action') {
|
||||||
|
this.availableTriggers.push(
|
||||||
|
{
|
||||||
|
value: i,
|
||||||
|
needs_context: trigger.needs_context,
|
||||||
|
label: this.$t('firefly.rule_trigger_' + i + '_choice')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getRuleActions: function () {
|
||||||
|
let config = new Configuration;
|
||||||
|
config.get('firefly.rule-actions').then((response) => {
|
||||||
|
for (let i in response.data.data.value) {
|
||||||
|
if (response.data.data.value.hasOwnProperty(i)) {
|
||||||
|
this.availableActions.push(
|
||||||
|
{
|
||||||
|
value: i,
|
||||||
|
needs_context: false,
|
||||||
|
label: this.$t('firefly.rule_action_' + i + '_choice')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
// get actions that require context:
|
||||||
|
config.get('firefly.context-rule-actions').then((response) => {
|
||||||
|
let contextActions = response.data.data.value;
|
||||||
|
for (let i in contextActions) {
|
||||||
|
let current = contextActions[i];
|
||||||
|
// find it in availableActions and set to true:
|
||||||
|
for (let ii in this.availableActions) {
|
||||||
|
let action = this.availableActions[ii];
|
||||||
|
if (action.value === current) {
|
||||||
|
this.availableActions[ii].needs_context = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetForm: function () {
|
||||||
|
|
||||||
|
this.initialTriggers = [
|
||||||
|
{
|
||||||
|
value: 'store-journal',
|
||||||
|
label: 'When a transaction is stored'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'update-journal',
|
||||||
|
label: 'When a transaction is updated'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
this.title = '';
|
||||||
|
this.rule_group_id = null;
|
||||||
|
this.trigger = 'store-journal';
|
||||||
|
// add new (single) trigger:
|
||||||
|
this.triggers.push(this.getDefaultTrigger());
|
||||||
|
this.actions.push(this.getDefaultAction());
|
||||||
|
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
},
|
||||||
|
getRuleGroups: function () {
|
||||||
|
this.getGroupPage(1);
|
||||||
|
},
|
||||||
|
getGroupPage: function (page) {
|
||||||
|
let list = new List();
|
||||||
|
list.list(page, this.getCacheKey).then((response) => {
|
||||||
|
if (page < parseInt(response.data.meta.pagination.total_pages)) {
|
||||||
|
this.getGroupPage(page + 1);
|
||||||
|
}
|
||||||
|
let groups = response.data.data;
|
||||||
|
for (let i in groups) {
|
||||||
|
if (groups.hasOwnProperty(i)) {
|
||||||
|
let group = groups[i];
|
||||||
|
this.ruleGroups.push(
|
||||||
|
{
|
||||||
|
value: parseInt(group.id),
|
||||||
|
label: group.attributes.title,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
rule_group_id: '',
|
||||||
|
triggers: [this.getDefaultTriggerError()],
|
||||||
|
actions: [this.getDefaultActionError()],
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
title: false,
|
||||||
|
rule_group_id: false,
|
||||||
|
triggers: [this.getDefaultHasTriggerError()],
|
||||||
|
actions: [this.getDefaultHasActionError()],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitRule: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build category array
|
||||||
|
const submission = this.buildRule();
|
||||||
|
|
||||||
|
(new Post())
|
||||||
|
.post(submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildRule: function () {
|
||||||
|
let rule = {
|
||||||
|
title: this.title,
|
||||||
|
rule_group_id: this.rule_group_id,
|
||||||
|
trigger: this.trigger,
|
||||||
|
triggers: [],
|
||||||
|
actions: [],
|
||||||
|
};
|
||||||
|
for (let i in this.triggers) {
|
||||||
|
// todo leaves room for filtering.
|
||||||
|
rule.triggers.push(
|
||||||
|
{
|
||||||
|
type: this.triggers[i].type.value,
|
||||||
|
value: this.triggers[i].value,
|
||||||
|
stop_processing: this.triggers[i].stop_processing,
|
||||||
|
active: this.triggers[i].active,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (let i in this.actions) {
|
||||||
|
let action = this.actions[i];
|
||||||
|
console.log(action);
|
||||||
|
rule.actions.push(
|
||||||
|
{
|
||||||
|
type: this.actions[i].type.value,
|
||||||
|
value: this.actions[i].value,
|
||||||
|
stop_processing: this.actions[i].stop_processing,
|
||||||
|
active: this.actions[i].active,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rule;
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'I am new rule',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to piggy',
|
||||||
|
link: {name: 'rules.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
176
frontend/src/pages/rules/Edit.vue
Normal file
176
frontend/src/pages/rules/Edit.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Edit rule</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>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="Update" @click="submitRule"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
<q-checkbox :disable="disabledInput" v-model="doReturnHere" left-label label="Return here"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/rules/get";
|
||||||
|
import Put from "../../api/rules/put";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Edit",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
|
||||||
|
// rule fields:
|
||||||
|
id: 0,
|
||||||
|
title: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.collectRule();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
collectRule: function() {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseRule(response));
|
||||||
|
},
|
||||||
|
parseRule: function(response) {
|
||||||
|
this.title = response.data.data.attributes.title;
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
title: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitRule: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build account array
|
||||||
|
const submission = this.buildRule();
|
||||||
|
|
||||||
|
(new Put())
|
||||||
|
.post(this.id, submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildRule: function () {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'Rule is updated',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to rule',
|
||||||
|
link: {name: 'rules.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
212
frontend/src/pages/rules/Index.vue
Normal file
212
frontend/src/pages/rules/Index.vue
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<q-card v-for="ruleGroup in ruleGroups" class="q-ma-md">
|
||||||
|
<q-table
|
||||||
|
:title="ruleGroup.title"
|
||||||
|
:rows="ruleGroup.rules"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="id"
|
||||||
|
:pagination="pagination"
|
||||||
|
:dense="$q.screen.lt.md"
|
||||||
|
:loading="ruleGroup.loading"
|
||||||
|
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="name" :props="props">
|
||||||
|
<router-link :to="{ name: 'rules.show', params: {id: props.row.id} }" class="text-primary">
|
||||||
|
{{ props.row.title }}
|
||||||
|
</router-link>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="menu" :props="props">
|
||||||
|
<q-btn-dropdown color="primary" label="Actions" size="sm">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-close-popup :to="{name: 'rules.edit', params: {id: props.row.id}}">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Edit</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="deleteRule(props.row.id, props.row.title)">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Delete</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn-group>
|
||||||
|
<q-btn size="sm" :to="{name: 'rule-groups.edit', params: {id: ruleGroup.id}}" color="primary">Edit group
|
||||||
|
</q-btn>
|
||||||
|
<q-btn size="sm" color="primary" @click="deleteRuleGroup(ruleGroup.id, ruleGroup.title)">Delete group
|
||||||
|
</q-btn>
|
||||||
|
</q-btn-group>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up"
|
||||||
|
>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'rule-groups.create'}" icon="fas fa-exchange-alt"
|
||||||
|
label="New rule group"/>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'rules.create'}" icon="fas fa-exchange-alt" label="New rule"/>
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters} from "vuex";
|
||||||
|
import List from "../../api/rule-groups/list";
|
||||||
|
import Get from "../../api/rule-groups/get";
|
||||||
|
import Destroy from "../../api/rule-groups/destroy";
|
||||||
|
import DestroyRule from "../../api/rules/destroy";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
watch: {
|
||||||
|
$route(to) {
|
||||||
|
// react to route changes...
|
||||||
|
if ('rules.index' === to.name) {
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 0
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{name: 'name', label: 'Name', field: 'name', align: 'left'},
|
||||||
|
{name: 'menu', label: ' ', field: 'menu', align: 'right'},
|
||||||
|
],
|
||||||
|
ruleGroups: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getRange', 'getCacheKey']),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
this.ruleGroups = {};
|
||||||
|
this.getPage(1);
|
||||||
|
},
|
||||||
|
deleteRule: function (id, title) {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Do you want to delete rule "' + title + '"?',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
this.destroyRule(id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteRuleGroup: function (id, title) {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Do you want to delete rule group "' + title + '"?',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
this.destroyRuleGroup(id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyRuleGroup: function (id) {
|
||||||
|
let destr = new Destroy;
|
||||||
|
destr.destroy(id).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.triggerUpdate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyRule: function (id) {
|
||||||
|
let destr = new DestroyRule;
|
||||||
|
destr.destroy(id).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.triggerUpdate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getPage: function (page) {
|
||||||
|
const list = new List();
|
||||||
|
this.rows = [];
|
||||||
|
list.list(page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
if (page < parseInt(response.data.meta.pagination.total_pages)) {
|
||||||
|
this.getPage(page + 1);
|
||||||
|
}
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
let identifier = parseInt(current.id);
|
||||||
|
this.ruleGroups[identifier] = {
|
||||||
|
id: identifier,
|
||||||
|
title: current.attributes.title,
|
||||||
|
rules: [],
|
||||||
|
loading: true
|
||||||
|
};
|
||||||
|
this.getRules(identifier, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (page === parseInt(response.data.meta.pagination.total_pages)) {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getRules: function (identifier, page) {
|
||||||
|
const get = new Get;
|
||||||
|
this.rows = [];
|
||||||
|
get.rules(identifier, page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
if (page < parseInt(response.data.meta.pagination.total_pages)) {
|
||||||
|
this.getRules(identifier, page + 1);
|
||||||
|
}
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
let ruleId = parseInt(current.id);
|
||||||
|
let rule = {
|
||||||
|
id: ruleId,
|
||||||
|
title: current.attributes.title,
|
||||||
|
};
|
||||||
|
this.ruleGroups[identifier].rules.push(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (page === parseInt(response.data.meta.pagination.total_pages)) {
|
||||||
|
this.ruleGroups[identifier].loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
57
frontend/src/pages/rules/Show.vue
Normal file
57
frontend/src/pages/rules/Show.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Balance chart -->
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">{{ rule.title }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
Rule: {{ rule.title }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/rules/get";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Show",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rule: {},
|
||||||
|
id: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.getRule();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (payload) {
|
||||||
|
this.page = payload.page;
|
||||||
|
this.getRule();
|
||||||
|
},
|
||||||
|
getRule: function () {
|
||||||
|
(new Get).get(this.id).then((response) => this.parseRule(response));
|
||||||
|
},
|
||||||
|
parseRule: function (response) {
|
||||||
|
this.rule = {
|
||||||
|
title: response.data.data.attributes.title,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
276
frontend/src/pages/subscriptions/Create.vue
Normal file
276
frontend/src/pages/subscriptions/Create.vue
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<!--
|
||||||
|
- 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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Info for new subscription</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.name"
|
||||||
|
:error="hasSubmissionErrors.name"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="name" :label="$t('form.name')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.date"
|
||||||
|
:error="hasSubmissionErrors.date"
|
||||||
|
bottom-slots :disable="disabledInput" type="date" v-model="date" :label="$t('form.date')"
|
||||||
|
hint="The next date you expect the subscription to hit"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 q-mb-xs q-pr-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.amount_min"
|
||||||
|
:error="hasSubmissionErrors.amount_min"
|
||||||
|
bottom-slots :disable="disabledInput" type="number" v-model="amount_min" :label="$t('form.amount_min')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 q-mb-xs q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.amount_max"
|
||||||
|
:error="hasSubmissionErrors.amount_max"
|
||||||
|
:rules="[ val => parseFloat(val) >= parseFloat(amount_min) || 'Must be more than minimum amount']"
|
||||||
|
bottom-slots :disable="disabledInput" type="number" v-model="amount_max" :label="$t('form.amount_max')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.repeat_freq"
|
||||||
|
:error="hasSubmissionErrors.repeat_freq"
|
||||||
|
outlined v-model="repeat_freq" :options="repeatFrequencies" label="Outlined"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="submitSubscription"/>
|
||||||
|
</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/subscriptions/post";
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Create",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
repeatFrequencies: [],
|
||||||
|
// subscription fields:
|
||||||
|
name: '',
|
||||||
|
date: '',
|
||||||
|
repeat_freq: 'monthly',
|
||||||
|
amount_min: '',
|
||||||
|
amount_max: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.date = format(new Date, 'y-MM-dd');
|
||||||
|
this.repeatFrequencies = [
|
||||||
|
{
|
||||||
|
label: this.$t('firefly.repeat_freq_weekly'),
|
||||||
|
value: 'weekly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('firefly.repeat_freq_monthly'),
|
||||||
|
value: 'monthly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('firefly.repeat_freq_quarterly'),
|
||||||
|
value: 'quarterly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('firefly.repeat_freq_half-year'),
|
||||||
|
value: 'half-year',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('firefly.repeat_freq_yearly'),
|
||||||
|
value: 'yearly',
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
this.resetForm();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetForm: function () {
|
||||||
|
this.name = '';
|
||||||
|
this.date = format(new Date, 'y-MM-dd');
|
||||||
|
this.repeat_freq = 'monthly';
|
||||||
|
this.amount_min = '';
|
||||||
|
this.amount_max = '';
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
date: '',
|
||||||
|
repeat_freq: '',
|
||||||
|
amount_min: '',
|
||||||
|
amount_max: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
name: false,
|
||||||
|
date: false,
|
||||||
|
repeat_freq: false,
|
||||||
|
amount_min: false,
|
||||||
|
amount_max: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitSubscription: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build account array
|
||||||
|
const submission = this.buildSubscription();
|
||||||
|
|
||||||
|
let subscriptions = new Post();
|
||||||
|
subscriptions
|
||||||
|
.post(submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildSubscription: function () {
|
||||||
|
let subscription = {
|
||||||
|
name: this.name,
|
||||||
|
date: this.date,
|
||||||
|
repeat_freq: this.repeat_freq,
|
||||||
|
amount_min: this.amount_min,
|
||||||
|
amount_max: this.amount_max,
|
||||||
|
};
|
||||||
|
return subscription;
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'I am new subscription lol',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to account',
|
||||||
|
link: {name: 'subscriptions.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
281
frontend/src/pages/subscriptions/Edit.vue
Normal file
281
frontend/src/pages/subscriptions/Edit.vue
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
<!--
|
||||||
|
- 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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Edit subscription {{ name }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.name"
|
||||||
|
:error="hasSubmissionErrors.name"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="name" :label="$t('form.name')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.date"
|
||||||
|
:error="hasSubmissionErrors.date"
|
||||||
|
bottom-slots :disable="disabledInput" type="date" v-model="date" :label="$t('form.date')"
|
||||||
|
hint="The next date you expect the subscription to hit"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 q-mb-xs q-pr-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.amount_min"
|
||||||
|
:error="hasSubmissionErrors.amount_min"
|
||||||
|
bottom-slots :disable="disabledInput" type="number" v-model="amount_min" :label="$t('form.amount_min')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 q-mb-xs q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors.amount_max"
|
||||||
|
:error="hasSubmissionErrors.amount_max"
|
||||||
|
:rules="[ val => parseFloat(val) >= parseFloat(amount_min) || 'Must be more than minimum amount']"
|
||||||
|
bottom-slots :disable="disabledInput" type="number" v-model="amount_max" :label="$t('form.amount_max')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.repeat_freq"
|
||||||
|
:error="hasSubmissionErrors.repeat_freq"
|
||||||
|
outlined v-model="repeat_freq" :options="repeatFrequencies" label="Outlined"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="submitSubscription"/>
|
||||||
|
</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"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Put from "../../api/subscriptions/put";
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import Get from "../../api/subscriptions/get";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Edit",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tab: 'split-0',
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
repeatFrequencies: [],
|
||||||
|
// subscription fields:
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
date: '',
|
||||||
|
repeat_freq: 'monthly',
|
||||||
|
amount_min: '',
|
||||||
|
amount_max: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.date = format(new Date, 'y-MM-dd');
|
||||||
|
this.repeatFrequencies = [
|
||||||
|
{
|
||||||
|
label: this.$t('firefly.repeat_freq_weekly'),
|
||||||
|
value: 'weekly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('firefly.repeat_freq_monthly'),
|
||||||
|
value: 'monthly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('firefly.repeat_freq_quarterly'),
|
||||||
|
value: 'quarterly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('firefly.repeat_freq_half-year'),
|
||||||
|
value: 'half-year',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('firefly.repeat_freq_yearly'),
|
||||||
|
value: 'yearly',
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.collectSubscription();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
date: '',
|
||||||
|
repeat_freq: '',
|
||||||
|
amount_min: '',
|
||||||
|
amount_max: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
name: false,
|
||||||
|
date: false,
|
||||||
|
repeat_freq: false,
|
||||||
|
amount_min: false,
|
||||||
|
amount_max: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
collectSubscription: function() {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseSubscription(response));
|
||||||
|
},
|
||||||
|
submitSubscription: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build subscription array
|
||||||
|
const submission = this.buildSubscription();
|
||||||
|
|
||||||
|
let subscriptions = new Put();
|
||||||
|
subscriptions
|
||||||
|
.put(this.id, submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
parseSubscription: function(response) {
|
||||||
|
this.name = response.data.data.attributes.name;
|
||||||
|
this.date = response.data.data.attributes.date.substr(0,10);
|
||||||
|
console.log(this.date);
|
||||||
|
this.repeat_freq = response.data.data.attributes.repeat_freq;
|
||||||
|
this.amount_min = response.data.data.attributes.amount_min;
|
||||||
|
this.amount_max = response.data.data.attributes.amount_max;
|
||||||
|
},
|
||||||
|
buildSubscription: function () {
|
||||||
|
let subscription = {
|
||||||
|
name: this.name,
|
||||||
|
date: this.date,
|
||||||
|
repeat_freq: this.repeat_freq,
|
||||||
|
amount_min: this.amount_min,
|
||||||
|
amount_max: this.amount_max,
|
||||||
|
};
|
||||||
|
return subscription;
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'I am updated subscription ',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to subscription',
|
||||||
|
link: {name: 'subscriptions.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
170
frontend/src/pages/subscriptions/Index.vue
Normal file
170
frontend/src/pages/subscriptions/Index.vue
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<q-table
|
||||||
|
:title="$t('firefly.subscriptions')"
|
||||||
|
:rows="rows"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="id"
|
||||||
|
@request="onRequest"
|
||||||
|
v-model:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
class="q-ma-md"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="name" :props="props">
|
||||||
|
<router-link :to="{ name: 'subscriptions.show', params: {id: props.row.id} }" class="text-primary">
|
||||||
|
{{ props.row.name }}
|
||||||
|
</router-link>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="menu" :props="props">
|
||||||
|
<q-btn-dropdown color="primary" label="Actions" size="sm">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-close-popup :to="{name: 'subscriptions.edit', params: {id: props.row.id}}">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Edit</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="deleteSubscription(props.row.id, props.row.name)">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Delete</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up"
|
||||||
|
>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'subscriptions.create', params: {type: 'asset'} }" icon="fas fa-exchange-alt"
|
||||||
|
label="New subscription"/>
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, useStore} from "vuex";
|
||||||
|
import List from "../../api/subscriptions/list";
|
||||||
|
import Destroy from "../../api/subscriptions/destroy";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getRange', 'getCacheKey', 'getListPageSize']),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.pagination.rowsPerPage = this.getListPageSize;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.type = this.$route.params.type;
|
||||||
|
if (null === this.getRange.start || null === this.getRange.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
const $store = useStore();
|
||||||
|
$store.subscribe((mutation, state) => {
|
||||||
|
if ('fireflyiii/setRange' === mutation.type) {
|
||||||
|
this.range = {start: mutation.payload.start, end: mutation.payload.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
this.range = {start: this.getRange.start, end: this.getRange.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'desc',
|
||||||
|
descending: false,
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 5,
|
||||||
|
rowsNumber: 100
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
columns: [
|
||||||
|
{name: 'name', label: 'Name', field: 'name', align: 'left'},
|
||||||
|
{name: 'menu', label: ' ', field: 'menu', align: 'right'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (props) {
|
||||||
|
this.page = props.pagination.page;
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
deleteSubscription: function (id, name) {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Do you want to delete subscriptions "' + name + '"? Transactions linked to this subscription will not be deleted.',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
this.destroySubscription(id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroySubscription: function (id) {
|
||||||
|
let destr = new Destroy;
|
||||||
|
destr.destroy(id).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.triggerUpdate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (null === this.range.start || null === this.range.end) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
const list = new List();
|
||||||
|
this.rows = [];
|
||||||
|
list.list(this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
this.pagination.rowsPerPage = response.data.meta.pagination.per_page;
|
||||||
|
this.pagination.rowsNumber = response.data.meta.pagination.total;
|
||||||
|
this.pagination.page = this.page;
|
||||||
|
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
let account = {
|
||||||
|
id: current.id,
|
||||||
|
name: current.attributes.name,
|
||||||
|
};
|
||||||
|
this.rows.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
116
frontend/src/pages/subscriptions/Show.vue
Normal file
116
frontend/src/pages/subscriptions/Show.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<!--
|
||||||
|
- Show.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-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">{{ subscription.name }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
Name: {{ subscription.name }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mt-sm">
|
||||||
|
<div class="col-12">
|
||||||
|
<LargeTable ref="table"
|
||||||
|
title="Transactions"
|
||||||
|
:rows="rows"
|
||||||
|
:loading="loading"
|
||||||
|
v-on:on-request="onRequest"
|
||||||
|
:rows-number="rowsNumber"
|
||||||
|
:rows-per-page="rowsPerPage"
|
||||||
|
:page="page"
|
||||||
|
>
|
||||||
|
</LargeTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LargeTable from "../../components/transactions/LargeTable";
|
||||||
|
import Get from "../../api/subscriptions/get";
|
||||||
|
import Parser from "../../api/transactions/parser";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Show",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
subscription: {},
|
||||||
|
rows: [],
|
||||||
|
rowsNumber: 1,
|
||||||
|
rowsPerPage: 10,
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.getSubscription();
|
||||||
|
},
|
||||||
|
components: {LargeTable},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (payload) {
|
||||||
|
this.page = payload.page;
|
||||||
|
this.getSubscription();
|
||||||
|
},
|
||||||
|
getSubscription: function () {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseSubscription(response));
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
const parser = new Parser;
|
||||||
|
this.rows = [];
|
||||||
|
|
||||||
|
get.transactions(this.id, this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
let resp = parser.parseResponse(response);
|
||||||
|
|
||||||
|
this.rowsPerPage = resp.rowsPerPage;
|
||||||
|
this.rowsNumber = resp.rowsNumber;
|
||||||
|
this.rows = resp.rows;
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parseSubscription: function (response) {
|
||||||
|
this.subscription = {
|
||||||
|
name: response.data.data.attributes.name,
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
114
frontend/src/pages/tags/Index.vue
Normal file
114
frontend/src/pages/tags/Index.vue
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<span v-for="tag in tags">
|
||||||
|
<q-badge outline class="q-ma-xs" color="blue">
|
||||||
|
<router-link :to="{ name: 'tags.show', params: {id: tag.id} }">
|
||||||
|
{{ tag.attributes.tag }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
</q-badge>
|
||||||
|
</span>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up"
|
||||||
|
>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'tags.create'}" icon="fas fa-exchange-alt" label="New tag"/>
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, useStore} from "vuex";
|
||||||
|
import List from "../../api/tags/list";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
watch: {
|
||||||
|
$route(to) {
|
||||||
|
// react to route changes...
|
||||||
|
if ('tags.index' === to.name) {
|
||||||
|
this.page = 1;
|
||||||
|
this.updateBreadcrumbs();
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tags: [],
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getRange', 'getCacheKey']),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (null === this.getRange.start || null === this.getRange.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
const $store = useStore();
|
||||||
|
$store.subscribe((mutation, state) => {
|
||||||
|
if ('fireflyiii/setRange' === mutation.type) {
|
||||||
|
this.range = {start: mutation.payload.start, end: mutation.payload.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
this.range = {start: this.getRange.start, end: this.getRange.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateBreadcrumbs: function () {
|
||||||
|
this.$route.meta.pageTitle = 'firefly.tags';
|
||||||
|
this.$route.meta.breadcrumbs = [{title: 'tags'}];
|
||||||
|
|
||||||
|
},
|
||||||
|
onRequest: function (props) {
|
||||||
|
this.page = props.pagination.page;
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
this.getPage(1);
|
||||||
|
},
|
||||||
|
getPage: function (page) {
|
||||||
|
const list = new List();
|
||||||
|
this.rows = [];
|
||||||
|
list.list(page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
this.tags.push(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// get next page:
|
||||||
|
if (page < parseInt(response.data.meta.pagination.total_pages)) {
|
||||||
|
this.getPage(page + 1);
|
||||||
|
}
|
||||||
|
if (page === parseInt(response.data.meta.pagination.total_pages)) {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
118
frontend/src/pages/tags/Show.vue
Normal file
118
frontend/src/pages/tags/Show.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<!--
|
||||||
|
- Show.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">
|
||||||
|
<!-- Balance chart -->
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">{{ tag.tag }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
Tag: {{ tag.tag }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mt-sm">
|
||||||
|
<div class="col-12">
|
||||||
|
<LargeTable ref="table"
|
||||||
|
title="Transactions"
|
||||||
|
:rows="rows"
|
||||||
|
:loading="loading"
|
||||||
|
v-on:on-request="onRequest"
|
||||||
|
:rows-number="rowsNumber"
|
||||||
|
:rows-per-page="rowsPerPage"
|
||||||
|
:page="page"
|
||||||
|
>
|
||||||
|
</LargeTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/tags/get";
|
||||||
|
import LargeTable from "../../components/transactions/LargeTable";
|
||||||
|
import Parser from "../../api/transactions/parser";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Show",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tag: {},
|
||||||
|
rows: [],
|
||||||
|
rowsNumber: 1,
|
||||||
|
rowsPerPage: 10,
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.getTag();
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
components: {LargeTable},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (payload) {
|
||||||
|
this.page = payload.page;
|
||||||
|
this.getTag();
|
||||||
|
},
|
||||||
|
getTag: function () {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseTag(response));
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
const parser = new Parser;
|
||||||
|
this.rows = [];
|
||||||
|
|
||||||
|
get.transactions(this.id, this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
let resp = parser.parseResponse(response);
|
||||||
|
|
||||||
|
this.rowsPerPage = resp.rowsPerPage;
|
||||||
|
this.rowsNumber = resp.rowsNumber;
|
||||||
|
this.rows = resp.rows;
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parseTag: function (response) {
|
||||||
|
this.tag = {
|
||||||
|
tag: response.data.data.attributes.tag,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
512
frontend/src/pages/transactions/Create.vue
Normal file
512
frontend/src/pages/transactions/Create.vue
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
<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-ma-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<q-tabs
|
||||||
|
v-model="tab"
|
||||||
|
inline-label
|
||||||
|
dense
|
||||||
|
align="left"
|
||||||
|
class="text-teal col"
|
||||||
|
>
|
||||||
|
<q-tab v-for="(transaction,index) in transactions" :name="'split-' + index" :label="getSplitLabel(index)"/>
|
||||||
|
<q-btn @click="addTransaction" flat label="Add split" icon="fas fa-plus-circle" class="text-orange"></q-btn>
|
||||||
|
</q-tabs>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-tab-panels v-model="tab" animated>
|
||||||
|
<q-tab-panel v-for="(transaction,index) in transactions" :key="index" :name="'split-' + index">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Info for {{ $route.params.type }} {{ index }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors[index].description"
|
||||||
|
:error="hasSubmissionErrors[index].description"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="transaction.description" :label="$t('firefly.description')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4 q-mb-xs q-pr-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors[index].source"
|
||||||
|
:error="hasSubmissionErrors[index].source"
|
||||||
|
bottom-slots :disable="disabledInput" clearable v-model="transaction.source" :label="$t('firefly.source_account')" outlined/>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 q-px-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors[index].amount"
|
||||||
|
:error="hasSubmissionErrors[index].amount"
|
||||||
|
bottom-slots :disable="disabledInput" clearable mask="#.##" reverse-fill-mask hint="Expects #.##" fill-mask="0"
|
||||||
|
v-model="transaction.amount"
|
||||||
|
:label="$t('firefly.amount')" outlined/>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors[index].destination"
|
||||||
|
:error="hasSubmissionErrors[index].destination"
|
||||||
|
bottom-slots :disable="disabledInput" clearable v-model="transaction.destination" :label="$t('firefly.destination_account')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4 offset-4">
|
||||||
|
Foreign
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors[index].date"
|
||||||
|
:error="hasSubmissionErrors[index].date"
|
||||||
|
bottom-slots :disable="disabledInput" v-model="transaction.date" outlined type="date" :hint="$t('firefly.date')"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-input bottom-slots :disable="disabledInput" v-model="transaction.time" outlined type="time" :hint="$t('firefly.time')"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="col-4 offset-4">
|
||||||
|
<q-input v-model="transaction.interest_date" filled type="date" hint="Interest date"/>
|
||||||
|
<q-input v-model="transaction.book_date" filled type="date" hint="Book date"/>
|
||||||
|
<q-input v-model="transaction.process_date" filled type="date" hint="Processing date"/>
|
||||||
|
<q-input v-model="transaction.due_date" filled type="date" hint="Due date"/>
|
||||||
|
<q-input v-model="transaction.payment_date" filled type="date" hint="Payment date"/>
|
||||||
|
<q-input v-model="transaction.invoice_date" filled type="date" hint="Invoice date"/>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
<!--
|
||||||
|
<q-card bordered class="q-mt-md">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Meta for {{ $route.params.type }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<q-select filled v-model="transaction.budget" :options="tempBudgets" label="Budget"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-input filled clearable v-model="transaction.category" :label="$t('firefly.category')" outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<q-select filled v-model="transaction.subscription" :options="tempSubscriptions" label="Subscription"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
Tags
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
Bill
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
???
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
<q-card bordered class="q-mt-md">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Extr for {{ $route.params.type }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
Notes
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
attachments
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
Links
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
reference
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
url
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
location
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
-->
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<q-tab-panel name="split-1">
|
||||||
|
<div class="text-h6">Alarms1</div>
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit.
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel name="split-2">
|
||||||
|
<div class="text-h6">Movies1</div>
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit.
|
||||||
|
</q-tab-panel>
|
||||||
|
-->
|
||||||
|
</q-tab-panels>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="submitTransaction"/>
|
||||||
|
</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 format from 'date-fns/format';
|
||||||
|
import formatISO from 'date-fns/formatISO';
|
||||||
|
import Post from "../../api/transactions/post";
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Create',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tab: 'split-0',
|
||||||
|
transactions: [],
|
||||||
|
submissionErrors: [],
|
||||||
|
hasSubmissionErrors: [],
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
group_title: '',
|
||||||
|
//tempModels: ['A', 'B', 'C'],
|
||||||
|
//tempBudgets: [{label: 'Budget A', value: 1}, {label: 'Budget B', value: 2}, {label: 'Budget C', value: 3}],
|
||||||
|
//tempSubscriptions: [{label: 'Sub A', value: 1}, {label: 'Sub B', value: 2}, {label: 'Sub C', value: 3}]
|
||||||
|
|
||||||
|
errorMessage: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.resetForm();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetForm: function () {
|
||||||
|
this.transactions = [];
|
||||||
|
const info = this.getDefaultTransaction();
|
||||||
|
this.transactions.push(info.transaction);
|
||||||
|
this.submissionErrors.push(info.submissionError);
|
||||||
|
this.hasSubmissionErrors.push(info.hasSubmissionError);
|
||||||
|
},
|
||||||
|
addTransaction: function () {
|
||||||
|
const transaction = this.getDefaultTransaction();
|
||||||
|
this.transactions.push(transaction);
|
||||||
|
this.tab = 'split-' + (parseInt(this.transactions.length) - 1);
|
||||||
|
},
|
||||||
|
getSplitLabel: function (index) {
|
||||||
|
if (this.transactions.hasOwnProperty(index) && null !== this.transactions[index].description && this.transactions[index].description.length > 0) {
|
||||||
|
return this.transactions[index].description
|
||||||
|
}
|
||||||
|
return this.$t('firefly.single_split') + ' ' + (index + 1);
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
submitTransaction: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build transaction array
|
||||||
|
const submission = this.buildTransaction();
|
||||||
|
|
||||||
|
let transactions = new Post();
|
||||||
|
transactions
|
||||||
|
.post(submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'I am text',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to transaction',
|
||||||
|
link: { name: 'transactions.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
let length = this.transactions.length;
|
||||||
|
let transaction = this.getDefaultTransaction();
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
this.submissionErrors[i] = transaction.submissionError;
|
||||||
|
this.hasSubmissionErrors[i] = transaction.hasSubmissionError;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)) {
|
||||||
|
this.processSingleError(i, errors.errors[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
processSingleError: function (key, errors) {
|
||||||
|
// lol dumbest way to explode "transactions.0.something" ever.
|
||||||
|
let index = parseInt(key.split('.')[1]);
|
||||||
|
let fieldName = key.split('.')[2];
|
||||||
|
switch (fieldName) {
|
||||||
|
case 'amount':
|
||||||
|
case 'date':
|
||||||
|
case 'description':
|
||||||
|
this.submissionErrors[index][fieldName] = errors[0];
|
||||||
|
this.hasSubmissionErrors[index][fieldName] = true;
|
||||||
|
break;
|
||||||
|
case 'source_id':
|
||||||
|
case 'source_name':
|
||||||
|
this.submissionErrors[index].source = errors[0];
|
||||||
|
this.hasSubmissionErrors[index].source = true;
|
||||||
|
break;
|
||||||
|
case 'destination_id':
|
||||||
|
case 'destination_name':
|
||||||
|
this.submissionErrors[index].source = errors[0];
|
||||||
|
this.hasSubmissionErrors[index].source = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildTransaction: function () {
|
||||||
|
const obj = {
|
||||||
|
transactions: []
|
||||||
|
};
|
||||||
|
this.transactions.forEach(element => {
|
||||||
|
let dateStr = formatISO(new Date(element.date + ' ' + element.time));
|
||||||
|
let row = {
|
||||||
|
type: this.$route.params.type,
|
||||||
|
description: element.description,
|
||||||
|
source_name: element.source,
|
||||||
|
destination_name: element.destination,
|
||||||
|
amount: element.amount,
|
||||||
|
date: dateStr
|
||||||
|
};
|
||||||
|
obj.transactions.push(row);
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
getDefaultTransaction: function () {
|
||||||
|
let date = '';
|
||||||
|
let time = '00:00';
|
||||||
|
|
||||||
|
if (0 === this.transactions.length) {
|
||||||
|
date = format(new Date, 'yyyy-MM-dd');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
submissionError: {
|
||||||
|
description: '',
|
||||||
|
amount: '',
|
||||||
|
date: '',
|
||||||
|
source: '',
|
||||||
|
destination: '',
|
||||||
|
},
|
||||||
|
hasSubmissionError: {
|
||||||
|
description: false,
|
||||||
|
amount: false,
|
||||||
|
date: false,
|
||||||
|
source: false,
|
||||||
|
destination: false,
|
||||||
|
},
|
||||||
|
transaction: {
|
||||||
|
description: '',
|
||||||
|
date: date,
|
||||||
|
time: time,
|
||||||
|
amount: 0,
|
||||||
|
|
||||||
|
// source and destination
|
||||||
|
source: '',
|
||||||
|
destination: '',
|
||||||
|
|
||||||
|
// categorisation
|
||||||
|
budget: '',
|
||||||
|
category: '',
|
||||||
|
subscription: '',
|
||||||
|
|
||||||
|
// custom dates
|
||||||
|
interest_date: '',
|
||||||
|
book_date: '',
|
||||||
|
process_date: '',
|
||||||
|
due_date: '',
|
||||||
|
payment_date: '',
|
||||||
|
invoice_date: '',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// date: "",
|
||||||
|
// amount: "",
|
||||||
|
// category: "",
|
||||||
|
// piggy_bank: 0,
|
||||||
|
// errors: {
|
||||||
|
// source_account: [],
|
||||||
|
// destination_account: [],
|
||||||
|
// description: [],
|
||||||
|
// amount: [],
|
||||||
|
// date: [],
|
||||||
|
// budget_id: [],
|
||||||
|
// bill_id: [],
|
||||||
|
// foreign_amount: [],
|
||||||
|
// category: [],
|
||||||
|
// piggy_bank: [],
|
||||||
|
// tags: [],
|
||||||
|
// custom fields:
|
||||||
|
// custom_errors: {
|
||||||
|
// interest_date: [],
|
||||||
|
// book_date: [],
|
||||||
|
// process_date: [],
|
||||||
|
// due_date: [],
|
||||||
|
// payment_date: [],
|
||||||
|
// invoice_date: [],
|
||||||
|
// internal_reference: [],
|
||||||
|
// notes: [],
|
||||||
|
// attachments: [],
|
||||||
|
// external_uri: [],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// budget: 0,
|
||||||
|
// bill: 0,
|
||||||
|
// tags: [],
|
||||||
|
// custom_fields: {
|
||||||
|
// "interest_date": "",
|
||||||
|
// "book_date": "",
|
||||||
|
// "process_date": "",
|
||||||
|
// "due_date": "",
|
||||||
|
// "payment_date": "",
|
||||||
|
// "invoice_date": "",
|
||||||
|
// "internal_reference": "",
|
||||||
|
// "notes": "",
|
||||||
|
// "attachments": [],
|
||||||
|
// "external_uri": "",
|
||||||
|
// },
|
||||||
|
// foreign_amount: {
|
||||||
|
// amount: "",
|
||||||
|
// currency_id: 0
|
||||||
|
// },
|
||||||
|
// source_account: {
|
||||||
|
// id: 0,
|
||||||
|
// name: "",
|
||||||
|
// type: "",
|
||||||
|
// currency_id: 0,
|
||||||
|
// currency_name: '',
|
||||||
|
// currency_code: '',
|
||||||
|
// currency_decimal_places: 2,
|
||||||
|
// allowed_types: ['Asset account', 'Revenue account', 'Loan', 'Debt', 'Mortgage'],
|
||||||
|
// default_allowed_types: ['Asset account', 'Revenue account', 'Loan', 'Debt', 'Mortgage']
|
||||||
|
// },
|
||||||
|
// destination_account: {
|
||||||
|
// id: 0,
|
||||||
|
// name: "",
|
||||||
|
// type: "",
|
||||||
|
// currency_id: 0,
|
||||||
|
// currency_name: '',
|
||||||
|
// currency_code: '',
|
||||||
|
// currency_decimal_places: 2,
|
||||||
|
// allowed_types: ['Asset account', 'Expense account', 'Loan', 'Debt', 'Mortgage'],
|
||||||
|
// default_allowed_types: ['Asset account', 'Expense account', 'Loan', 'Debt', 'Mortgage']
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// if (this.transactions.length === 1) {
|
||||||
|
// // console.log('Length == 1, set date to today.');
|
||||||
|
// // set first date.
|
||||||
|
// let today = new Date();
|
||||||
|
// this.transactions[0].date = today.getFullYear() + '-' + ("0" + (today.getMonth() + 1)).slice(-2) + '-' + ("0" + today.getDate()).slice(-2);
|
||||||
|
// // call for extra clear thing:
|
||||||
|
// // this.clearSource(0);
|
||||||
|
// //this.clearDestination(0);
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
// };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preFetch() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
419
frontend/src/pages/transactions/Edit.vue
Normal file
419
frontend/src/pages/transactions/Edit.vue
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
<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">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-tab-panels v-model="tab" animated>
|
||||||
|
<q-tab-panel v-for="(transaction, index) in transactions" :key="index" :name="'split-' + index">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Info for {{ $route.params.type }} {{ index }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors[index].description"
|
||||||
|
:error="hasSubmissionErrors[index].description"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="transaction.description" :label="$t('firefly.description')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4 q-mb-xs q-pr-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors[index].source"
|
||||||
|
:error="hasSubmissionErrors[index].source"
|
||||||
|
bottom-slots :disable="disabledInput" clearable v-model="transaction.source" :label="$t('firefly.source_account')" outlined/>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 q-px-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors[index].amount"
|
||||||
|
:error="hasSubmissionErrors[index].amount"
|
||||||
|
bottom-slots :disable="disabledInput" clearable mask="#.##" reverse-fill-mask hint="Expects #.##" fill-mask="0"
|
||||||
|
v-model="transaction.amount"
|
||||||
|
:label="$t('firefly.amount')" outlined/>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 q-pl-xs">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors[index].destination"
|
||||||
|
:error="hasSubmissionErrors[index].destination"
|
||||||
|
bottom-slots :disable="disabledInput" clearable v-model="transaction.destination" :label="$t('firefly.destination_account')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
:error-message="submissionErrors[index].date"
|
||||||
|
:error="hasSubmissionErrors[index].date"
|
||||||
|
bottom-slots :disable="disabledInput" v-model="transaction.date" outlined type="date" :hint="$t('firefly.date')"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-input bottom-slots :disable="disabledInput" v-model="transaction.time" outlined type="time" :hint="$t('firefly.time')"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="submitTransaction"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
<q-checkbox :disable="disabledInput" v-model="doReturnHere" left-label label="Return here"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import formatISO from 'date-fns/formatISO';
|
||||||
|
import Put from "../../api/transactions/put";
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import Get from "../../api/transactions/get";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Edit',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tab: 'split-0',
|
||||||
|
transactions: [],
|
||||||
|
submissionErrors: [],
|
||||||
|
hasSubmissionErrors: [],
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
index: 0,
|
||||||
|
doResetForm: false,
|
||||||
|
group_title: '',
|
||||||
|
errorMessage: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.resetForm();
|
||||||
|
this.collectTransaction();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
collectTransaction: function() {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseTransaction(response));
|
||||||
|
},
|
||||||
|
parseTransaction: function(response) {
|
||||||
|
this.group_title = response.data.data.attributes.group_title;
|
||||||
|
// parse transactions:
|
||||||
|
let transactions = response.data.data.attributes.transactions;
|
||||||
|
transactions.reverse();
|
||||||
|
for(let i in transactions) {
|
||||||
|
if(transactions.hasOwnProperty(i)) {
|
||||||
|
let transaction = transactions[i];
|
||||||
|
let index = parseInt(i);
|
||||||
|
// parse first transaction only:
|
||||||
|
if(0 === index) {
|
||||||
|
let parts = transaction.date.split('T');
|
||||||
|
let date = parts[0];
|
||||||
|
let time = parts[1].substr(0,8);
|
||||||
|
this.transactions.push(
|
||||||
|
{
|
||||||
|
description: transaction.description,
|
||||||
|
type: transaction.type,
|
||||||
|
date: date,
|
||||||
|
time: time,
|
||||||
|
amount: parseFloat(transaction.amount).toFixed(transaction.currency_decimal_places),
|
||||||
|
|
||||||
|
// source and destination
|
||||||
|
source: transaction.source_name,
|
||||||
|
destination: transaction.destination_name,
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetForm: function () {
|
||||||
|
this.transactions = [];
|
||||||
|
const info = this.getDefaultTransaction();
|
||||||
|
this.transactions = [];
|
||||||
|
this.submissionErrors.push(info.submissionError);
|
||||||
|
this.hasSubmissionErrors.push(info.hasSubmissionError);
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
submitTransaction: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build transaction array
|
||||||
|
const submission = this.buildTransaction();
|
||||||
|
|
||||||
|
let transactions = new Put();
|
||||||
|
transactions
|
||||||
|
.put(this.id, submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
this.submitting = false;
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'Updated transaction',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to transaction',
|
||||||
|
link: { name: 'transactions.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
let length = this.transactions.length;
|
||||||
|
let transaction = this.getDefaultTransaction();
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
this.submissionErrors[i] = transaction.submissionError;
|
||||||
|
this.hasSubmissionErrors[i] = transaction.hasSubmissionError;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
// TODO rule and recurring have similar code
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.processSingleError(i, errors.errors[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
processSingleError: function (key, errors) {
|
||||||
|
// lol dumbest way to explode "transactions.0.something" ever.
|
||||||
|
let index = parseInt(key.split('.')[1]);
|
||||||
|
let fieldName = key.split('.')[2];
|
||||||
|
switch (fieldName) {
|
||||||
|
case 'amount':
|
||||||
|
case 'date':
|
||||||
|
case 'description':
|
||||||
|
this.submissionErrors[index][fieldName] = errors[0];
|
||||||
|
this.hasSubmissionErrors[index][fieldName] = true;
|
||||||
|
break;
|
||||||
|
case 'source_id':
|
||||||
|
case 'source_name':
|
||||||
|
this.submissionErrors[index].source = errors[0];
|
||||||
|
this.hasSubmissionErrors[index].source = true;
|
||||||
|
break;
|
||||||
|
case 'destination_id':
|
||||||
|
case 'destination_name':
|
||||||
|
this.submissionErrors[index].source = errors[0];
|
||||||
|
this.hasSubmissionErrors[index].source = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildTransaction: function () {
|
||||||
|
const obj = {
|
||||||
|
transactions: []
|
||||||
|
};
|
||||||
|
this.transactions.forEach(element => {
|
||||||
|
let dateStr = formatISO(new Date(element.date + ' ' + element.time));
|
||||||
|
let row = {
|
||||||
|
type: element.type,
|
||||||
|
description: element.description,
|
||||||
|
source_name: element.source,
|
||||||
|
destination_name: element.destination,
|
||||||
|
amount: element.amount,
|
||||||
|
date: dateStr
|
||||||
|
};
|
||||||
|
obj.transactions.push(row);
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
getDefaultTransaction: function () {
|
||||||
|
let date = '';
|
||||||
|
let time = '00:00';
|
||||||
|
|
||||||
|
if (0 === this.transactions.length) {
|
||||||
|
date = format(new Date, 'yyyy-MM-dd');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
submissionError: {
|
||||||
|
description: '',
|
||||||
|
amount: '',
|
||||||
|
date: '',
|
||||||
|
source: '',
|
||||||
|
destination: '',
|
||||||
|
},
|
||||||
|
hasSubmissionError: {
|
||||||
|
description: false,
|
||||||
|
amount: false,
|
||||||
|
date: false,
|
||||||
|
source: false,
|
||||||
|
destination: false,
|
||||||
|
},
|
||||||
|
transaction: {
|
||||||
|
description: '',
|
||||||
|
date: date,
|
||||||
|
time: time,
|
||||||
|
amount: 0,
|
||||||
|
|
||||||
|
// source and destination
|
||||||
|
source: '',
|
||||||
|
destination: '',
|
||||||
|
|
||||||
|
// categorisation
|
||||||
|
budget: '',
|
||||||
|
category: '',
|
||||||
|
subscription: '',
|
||||||
|
|
||||||
|
// custom dates
|
||||||
|
interest_date: '',
|
||||||
|
book_date: '',
|
||||||
|
process_date: '',
|
||||||
|
due_date: '',
|
||||||
|
payment_date: '',
|
||||||
|
invoice_date: '',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// date: "",
|
||||||
|
// amount: "",
|
||||||
|
// category: "",
|
||||||
|
// piggy_bank: 0,
|
||||||
|
// errors: {
|
||||||
|
// source_account: [],
|
||||||
|
// destination_account: [],
|
||||||
|
// description: [],
|
||||||
|
// amount: [],
|
||||||
|
// date: [],
|
||||||
|
// budget_id: [],
|
||||||
|
// bill_id: [],
|
||||||
|
// foreign_amount: [],
|
||||||
|
// category: [],
|
||||||
|
// piggy_bank: [],
|
||||||
|
// tags: [],
|
||||||
|
// custom fields:
|
||||||
|
// custom_errors: {
|
||||||
|
// interest_date: [],
|
||||||
|
// book_date: [],
|
||||||
|
// process_date: [],
|
||||||
|
// due_date: [],
|
||||||
|
// payment_date: [],
|
||||||
|
// invoice_date: [],
|
||||||
|
// internal_reference: [],
|
||||||
|
// notes: [],
|
||||||
|
// attachments: [],
|
||||||
|
// external_uri: [],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// budget: 0,
|
||||||
|
// bill: 0,
|
||||||
|
// tags: [],
|
||||||
|
// custom_fields: {
|
||||||
|
// "interest_date": "",
|
||||||
|
// "book_date": "",
|
||||||
|
// "process_date": "",
|
||||||
|
// "due_date": "",
|
||||||
|
// "payment_date": "",
|
||||||
|
// "invoice_date": "",
|
||||||
|
// "internal_reference": "",
|
||||||
|
// "notes": "",
|
||||||
|
// "attachments": [],
|
||||||
|
// "external_uri": "",
|
||||||
|
// },
|
||||||
|
// foreign_amount: {
|
||||||
|
// amount: "",
|
||||||
|
// currency_id: 0
|
||||||
|
// },
|
||||||
|
// source_account: {
|
||||||
|
// id: 0,
|
||||||
|
// name: "",
|
||||||
|
// type: "",
|
||||||
|
// currency_id: 0,
|
||||||
|
// currency_name: '',
|
||||||
|
// currency_code: '',
|
||||||
|
// currency_decimal_places: 2,
|
||||||
|
// allowed_types: ['Asset account', 'Revenue account', 'Loan', 'Debt', 'Mortgage'],
|
||||||
|
// default_allowed_types: ['Asset account', 'Revenue account', 'Loan', 'Debt', 'Mortgage']
|
||||||
|
// },
|
||||||
|
// destination_account: {
|
||||||
|
// id: 0,
|
||||||
|
// name: "",
|
||||||
|
// type: "",
|
||||||
|
// currency_id: 0,
|
||||||
|
// currency_name: '',
|
||||||
|
// currency_code: '',
|
||||||
|
// currency_decimal_places: 2,
|
||||||
|
// allowed_types: ['Asset account', 'Expense account', 'Loan', 'Debt', 'Mortgage'],
|
||||||
|
// default_allowed_types: ['Asset account', 'Expense account', 'Loan', 'Debt', 'Mortgage']
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// if (this.transactions.length === 1) {
|
||||||
|
// // console.log('Length == 1, set date to today.');
|
||||||
|
// // set first date.
|
||||||
|
// let today = new Date();
|
||||||
|
// this.transactions[0].date = today.getFullYear() + '-' + ("0" + (today.getMonth() + 1)).slice(-2) + '-' + ("0" + today.getDate()).slice(-2);
|
||||||
|
// // call for extra clear thing:
|
||||||
|
// // this.clearSource(0);
|
||||||
|
// //this.clearDestination(0);
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
// };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preFetch() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
154
frontend/src/pages/transactions/Index.vue
Normal file
154
frontend/src/pages/transactions/Index.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
|
||||||
|
<!-- insert LargeTable -->
|
||||||
|
<LargeTable ref="table"
|
||||||
|
:title="$t('firefly.title_' + this.type)"
|
||||||
|
:rows="rows"
|
||||||
|
:loading="loading"
|
||||||
|
v-on:on-request="onRequest"
|
||||||
|
:rows-number="rowsNumber"
|
||||||
|
:rows-per-page="rowsPerPage"
|
||||||
|
:page="page"
|
||||||
|
>
|
||||||
|
|
||||||
|
</LargeTable>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up"
|
||||||
|
>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'transactions.create', params: {type: 'transfer'} }" icon="fas fa-exchange-alt" label="New transfer"/>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'transactions.create', params: {type: 'deposit'} }" icon="fas fa-long-arrow-alt-right"
|
||||||
|
label="New deposit"/>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'transactions.create', params: {type: 'withdrawal'} }" icon="fas fa-long-arrow-alt-left"
|
||||||
|
label="New withdrawal"/>
|
||||||
|
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, useStore} from "vuex";
|
||||||
|
import List from "../../api/transactions/list";
|
||||||
|
import LargeTable from "../../components/transactions/LargeTable";
|
||||||
|
import Parser from "../../api/transactions/parser";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
components: {LargeTable},
|
||||||
|
watch: {
|
||||||
|
$route(to) {
|
||||||
|
// react to route changes...
|
||||||
|
if ('transactions.index' === to.name) {
|
||||||
|
this.type = to.params.type;
|
||||||
|
this.page = 1;
|
||||||
|
|
||||||
|
// update meta for breadcrumbs and page title:
|
||||||
|
//this.$route.meta.pageTitle = 'firefly.title_' + this.type;
|
||||||
|
//this.$route.meta.breadcrumbs = [{title: 'title_' + this.type}];
|
||||||
|
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
rows: [],
|
||||||
|
columns: [
|
||||||
|
{name: 'type', label: ' ', field: 'type', style: 'width: 30px'},
|
||||||
|
{name: 'description', label: 'Description', field: 'description', align: 'left'},
|
||||||
|
{
|
||||||
|
name: 'amount', label: 'Amount', field: 'amount'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'date', label: 'Date', field: 'date',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{name: 'source', label: 'Source', field: 'source', align: 'left'},
|
||||||
|
{name: 'destination', label: 'Destination', field: 'destination', align: 'left'},
|
||||||
|
{name: 'category', label: 'Category', field: 'category', align: 'left'},
|
||||||
|
{name: 'budget', label: 'Budget', field: 'budget', align: 'left'},
|
||||||
|
{name: 'menu', label: ' ', field: 'menu', align: 'left'},
|
||||||
|
],
|
||||||
|
type: 'withdrawal',
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 50,
|
||||||
|
rowsNumber: 100,
|
||||||
|
range: {
|
||||||
|
start: null,
|
||||||
|
end: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getRange', 'getCacheKey', 'getListPageSize']),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.rowsPerPage = this.getListPageSize;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.type = this.$route.params.type;
|
||||||
|
if (null === this.getRange.start || null === this.getRange.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
const $store = useStore();
|
||||||
|
$store.subscribe((mutation, state) => {
|
||||||
|
if ('fireflyiii/setRange' === mutation.type) {
|
||||||
|
this.range = {start: mutation.payload.start, end: mutation.payload.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (null !== this.getRange.start && null !== this.getRange.end) {
|
||||||
|
this.range = {start: this.getRange.start, end: this.getRange.end};
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (payload) {
|
||||||
|
this.page = payload.page;
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
formatAmount: function (currencyCode, amount) {
|
||||||
|
return Intl.NumberFormat('en-US', {style: 'currency', currency: currencyCode}).format(amount);
|
||||||
|
},
|
||||||
|
gotoTransaction: function (event, row) {
|
||||||
|
this.$router.push({name: 'transactions.show', params: {id: 1}});
|
||||||
|
},
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (null === this.range.start || null === this.range.end) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
const list = new List();
|
||||||
|
const parser = new Parser;
|
||||||
|
this.rows = [];
|
||||||
|
|
||||||
|
list.list(this.type, this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
let resp = parser.parseResponse(response);
|
||||||
|
|
||||||
|
this.rowsPerPage = resp.rowsPerPage;
|
||||||
|
this.rowsNumber = resp.rowsNumber;
|
||||||
|
this.rows = resp.rows;
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
108
frontend/src/pages/transactions/Show.vue
Normal file
108
frontend/src/pages/transactions/Show.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<!--
|
||||||
|
- Show.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">
|
||||||
|
<!-- Balance chart -->
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Transaction: {{ title }}
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row" v-for="(transaction, index) in group.transactions">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<strong>index {{ index }}</strong><br>
|
||||||
|
{{ transaction.description }}<br>
|
||||||
|
{{ transaction.amount }}<br>
|
||||||
|
{{ transaction.source_name }} --> {{ transaction.destination_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/transactions/get";
|
||||||
|
import LargeTable from "../../components/transactions/LargeTable";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Show",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: '',
|
||||||
|
group: {
|
||||||
|
transactions: []
|
||||||
|
},
|
||||||
|
rows: [],
|
||||||
|
rowsNumber: 1,
|
||||||
|
rowsPerPage: 10,
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.getTransaction();
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
components: {LargeTable},
|
||||||
|
methods: {
|
||||||
|
onRequest: function (payload) {
|
||||||
|
this.page = payload.page;
|
||||||
|
this.getTag();
|
||||||
|
},
|
||||||
|
getTransaction: function () {
|
||||||
|
let get = new Get;
|
||||||
|
this.loading = true;
|
||||||
|
get.get(this.id).then((response) => this.parseTransaction(response.data.data));
|
||||||
|
},
|
||||||
|
parseTransaction: function (data) {
|
||||||
|
this.group = {
|
||||||
|
group_title: data.attributes.group_title,
|
||||||
|
transactions: [],
|
||||||
|
};
|
||||||
|
if(null !== data.attributes.group_title) {
|
||||||
|
this.title = data.attributes.group_title;
|
||||||
|
}
|
||||||
|
for(let i in data.attributes.transactions) {
|
||||||
|
if(data.attributes.transactions.hasOwnProperty(i)) {
|
||||||
|
let transaction = data.attributes.transactions[i];
|
||||||
|
this.group.transactions.push(transaction);
|
||||||
|
|
||||||
|
if(0 === parseInt(i) && null === data.attributes.group_title) {
|
||||||
|
this.title = transaction.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
260
frontend/src/pages/webhooks/Create.vue
Normal file
260
frontend/src/pages/webhooks/Create.vue
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Info for new webhook</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-input
|
||||||
|
:error-message="submissionErrors.url"
|
||||||
|
:error="hasSubmissionErrors.url"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="url" :label="$t('form.url')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.response"
|
||||||
|
:error="hasSubmissionErrors.response"
|
||||||
|
bottom-slots
|
||||||
|
:disable="disabledInput"
|
||||||
|
outlined
|
||||||
|
v-model="response"
|
||||||
|
emit-value class="q-pr-xs"
|
||||||
|
map-options :options="responses" label="Response"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.delivery"
|
||||||
|
:error="hasSubmissionErrors.delivery"
|
||||||
|
bottom-slots
|
||||||
|
:disable="disabledInput"
|
||||||
|
outlined
|
||||||
|
v-model="delivery"
|
||||||
|
emit-value class="q-pr-xs"
|
||||||
|
map-options :options="deliveries" label="Delivery"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.trigger"
|
||||||
|
:error="hasSubmissionErrors.trigger"
|
||||||
|
bottom-slots
|
||||||
|
:disable="disabledInput"
|
||||||
|
outlined
|
||||||
|
v-model="trigger"
|
||||||
|
emit-value class="q-pr-xs"
|
||||||
|
map-options :options="triggers" label="Triggers"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="submitWebhook"/>
|
||||||
|
</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/webhooks/post";
|
||||||
|
import {mapGetters} from "vuex";
|
||||||
|
import {getCacheKey} from "../../store/fireflyiii/getters";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Create',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
balance_input_mask: '#.##',
|
||||||
|
|
||||||
|
// values:
|
||||||
|
triggers: [
|
||||||
|
{value: 'TRIGGER_STORE_TRANSACTION', label: 'When transaction stored'},
|
||||||
|
{value: 'TRIGGER_UPDATE_TRANSACTION', label: 'When transaction updated'},
|
||||||
|
{value: 'TRIGGER_DESTROY_TRANSACTION', label: 'When transaction deleted'}
|
||||||
|
],
|
||||||
|
|
||||||
|
responses: [
|
||||||
|
{value: 'RESPONSE_TRANSACTIONS', label: 'Send transaction'},
|
||||||
|
{value: 'RESPONSE_ACCOUNTS', label: 'Send accounts'},
|
||||||
|
{value: 'RESPONSE_NONE', label: 'Send nothing'},
|
||||||
|
],
|
||||||
|
deliveries: [
|
||||||
|
{value: 'DELIVERY_JSON', label: 'JSON'}
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
// webhook fields:
|
||||||
|
title: '',
|
||||||
|
url: '',
|
||||||
|
response: 'RESPONSE_TRANSACTIONS',
|
||||||
|
delivery: 'DELIVERY_JSON',
|
||||||
|
trigger: 'TRIGGER_STORE_TRANSACTION',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getCacheKey']),
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.resetForm();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetForm: function () {
|
||||||
|
this.title = '';
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
url: '',
|
||||||
|
response: '',
|
||||||
|
delivery: '',
|
||||||
|
trigger: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
title: false,
|
||||||
|
url: false,
|
||||||
|
response: false,
|
||||||
|
delivery: false,
|
||||||
|
trigger: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitWebhook: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build category array
|
||||||
|
const submission = this.buildWebhook();
|
||||||
|
|
||||||
|
(new Post())
|
||||||
|
.post(submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildWebhook: function () {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
url: this.url,
|
||||||
|
response: this.response,
|
||||||
|
delivery: this.delivery,
|
||||||
|
trigger: this.trigger,
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'I am new webhook',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to webhook',
|
||||||
|
link: {name: 'webhooks.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
266
frontend/src/pages/webhooks/Edit.vue
Normal file
266
frontend/src/pages/webhooks/Edit.vue
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
<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-12">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Edit webhook</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-input
|
||||||
|
:error-message="submissionErrors.url"
|
||||||
|
:error="hasSubmissionErrors.url"
|
||||||
|
bottom-slots :disable="disabledInput" type="text" clearable v-model="url" :label="$t('form.url')"
|
||||||
|
outlined/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.response"
|
||||||
|
:error="hasSubmissionErrors.response"
|
||||||
|
bottom-slots
|
||||||
|
:disable="disabledInput"
|
||||||
|
outlined
|
||||||
|
v-model="response"
|
||||||
|
emit-value class="q-pr-xs"
|
||||||
|
map-options :options="responses" label="Response"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.delivery"
|
||||||
|
:error="hasSubmissionErrors.delivery"
|
||||||
|
bottom-slots
|
||||||
|
:disable="disabledInput"
|
||||||
|
outlined
|
||||||
|
v-model="delivery"
|
||||||
|
emit-value class="q-pr-xs"
|
||||||
|
map-options :options="deliveries" label="Delivery"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
<q-select
|
||||||
|
:error-message="submissionErrors.trigger"
|
||||||
|
:error="hasSubmissionErrors.trigger"
|
||||||
|
bottom-slots
|
||||||
|
:disable="disabledInput"
|
||||||
|
outlined
|
||||||
|
v-model="trigger"
|
||||||
|
emit-value class="q-pr-xs"
|
||||||
|
map-options :options="triggers" label="Triggers"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<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="Update" @click="submitWebhook"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
<q-checkbox :disable="disabledInput" v-model="doReturnHere" left-label label="Return here"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/webhooks/get";
|
||||||
|
import Put from "../../api/webhooks/put";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Edit",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submissionErrors: {},
|
||||||
|
hasSubmissionErrors: {},
|
||||||
|
submitting: false,
|
||||||
|
doReturnHere: false,
|
||||||
|
doResetForm: false,
|
||||||
|
errorMessage: '',
|
||||||
|
|
||||||
|
// webhook options
|
||||||
|
triggers: [
|
||||||
|
{value: 'TRIGGER_STORE_TRANSACTION', label: 'When transaction stored'},
|
||||||
|
{value: 'TRIGGER_UPDATE_TRANSACTION', label: 'When transaction updated'},
|
||||||
|
{value: 'TRIGGER_DESTROY_TRANSACTION', label: 'When transaction deleted'}
|
||||||
|
],
|
||||||
|
|
||||||
|
responses: [
|
||||||
|
{value: 'RESPONSE_TRANSACTIONS', label: 'Send transaction'},
|
||||||
|
{value: 'RESPONSE_ACCOUNTS', label: 'Send accounts'},
|
||||||
|
{value: 'RESPONSE_NONE', label: 'Send nothing'},
|
||||||
|
],
|
||||||
|
deliveries: [
|
||||||
|
{value: 'DELIVERY_JSON', label: 'JSON'}
|
||||||
|
],
|
||||||
|
|
||||||
|
// webhook fields:
|
||||||
|
id: 0,
|
||||||
|
title: '',
|
||||||
|
url: '',
|
||||||
|
response: '',
|
||||||
|
delivery: '',
|
||||||
|
trigger: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabledInput: function () {
|
||||||
|
return this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.collectWebhook();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
collectWebhook: function () {
|
||||||
|
let get = new Get;
|
||||||
|
get.get(this.id).then((response) => this.parseWebhook(response));
|
||||||
|
},
|
||||||
|
parseWebhook: function (response) {
|
||||||
|
this.title = response.data.data.attributes.title;
|
||||||
|
this.url = response.data.data.attributes.url;
|
||||||
|
this.response = response.data.data.attributes.response;
|
||||||
|
this.delivery = response.data.data.attributes.delivery;
|
||||||
|
this.trigger = response.data.data.attributes.trigger;
|
||||||
|
},
|
||||||
|
resetErrors: function () {
|
||||||
|
this.submissionErrors =
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
url: '',
|
||||||
|
response: '',
|
||||||
|
delivery: '',
|
||||||
|
trigger: '',
|
||||||
|
};
|
||||||
|
this.hasSubmissionErrors = {
|
||||||
|
title: false,
|
||||||
|
url: false,
|
||||||
|
response: false,
|
||||||
|
delivery: false,
|
||||||
|
trigger: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
submitWebhook: function () {
|
||||||
|
this.submitting = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// reset errors:
|
||||||
|
this.resetErrors();
|
||||||
|
|
||||||
|
// build account array
|
||||||
|
const submission = this.buildWebhook();
|
||||||
|
|
||||||
|
(new Put())
|
||||||
|
.put(this.id, submission)
|
||||||
|
.catch(this.processErrors)
|
||||||
|
.then(this.processSuccess);
|
||||||
|
},
|
||||||
|
buildWebhook: function () {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
url: this.url,
|
||||||
|
response: this.response,
|
||||||
|
delivery: this.delivery,
|
||||||
|
trigger: this.trigger
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dismissBanner: function () {
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
processSuccess: function (response) {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
let message = {
|
||||||
|
level: 'success',
|
||||||
|
text: 'Webhook is updated',
|
||||||
|
show: true,
|
||||||
|
action: {
|
||||||
|
show: true,
|
||||||
|
text: 'Go to webhook',
|
||||||
|
link: {name: 'webhooks.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
processErrors: function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
let errors = error.response.data; // => the response payload
|
||||||
|
this.errorMessage = errors.message;
|
||||||
|
console.log(errors);
|
||||||
|
for (let i in errors.errors) {
|
||||||
|
if (errors.errors.hasOwnProperty(i)) {
|
||||||
|
this.submissionErrors[i] = errors.errors[i][0];
|
||||||
|
this.hasSubmissionErrors[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
166
frontend/src/pages/webhooks/Index.vue
Normal file
166
frontend/src/pages/webhooks/Index.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<q-table
|
||||||
|
:title="$t('firefly.webhooks')"
|
||||||
|
:rows="rows"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="id"
|
||||||
|
@request="onRequest"
|
||||||
|
v-model:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
class="q-ma-md"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="title" :props="props">
|
||||||
|
<router-link :to="{ name: 'webhooks.show', params: {id: props.row.id} }" class="text-primary">
|
||||||
|
{{ props.row.title }}
|
||||||
|
</router-link>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="menu" :props="props">
|
||||||
|
<q-btn-dropdown color="primary" label="Actions" size="sm">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-close-popup :to="{name: 'webhooks.edit', params: {id: props.row.id}}">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Edit</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="deleteWebhook(props.row.id, props.row.title)">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Delete</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
||||||
|
<q-fab
|
||||||
|
label="Actions"
|
||||||
|
square
|
||||||
|
vertical-actions-align="right"
|
||||||
|
label-position="left"
|
||||||
|
color="green"
|
||||||
|
icon="fas fa-chevron-up"
|
||||||
|
direction="up"
|
||||||
|
>
|
||||||
|
<q-fab-action color="primary" square :to="{ name: 'webhooks.create'}" icon="fas fa-exchange-alt"
|
||||||
|
label="New webhook"/>
|
||||||
|
</q-fab>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters} from "vuex";
|
||||||
|
import Destroy from "../../api/webhooks/destroy";
|
||||||
|
import List from "../../api/webhooks/list";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
watch: {
|
||||||
|
$route(to) {
|
||||||
|
// react to route changes...
|
||||||
|
if ('webhooks.index' === to.name) {
|
||||||
|
this.page = 1;
|
||||||
|
this.updateBreadcrumbs();
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'desc',
|
||||||
|
descending: false,
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 5,
|
||||||
|
rowsNumber: 100
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
columns: [
|
||||||
|
{name: 'title', label: 'Title', field: 'title', align: 'left'},
|
||||||
|
{name: 'menu', label: ' ', field: 'menu', align: 'right'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('fireflyiii', ['getCacheKey', 'getListPageSize']),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.pagination.rowsPerPage = this.getListPageSize;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteWebhook: function (id, name) {
|
||||||
|
this.$q.dialog({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Do you want to delete webhook "' + name + '"?',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
this.destroyWebhook(id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyWebhook: function (id) {
|
||||||
|
let destr = new Destroy;
|
||||||
|
destr.destroy(id).then(() => {
|
||||||
|
this.$store.dispatch('fireflyiii/refreshCacheKey');
|
||||||
|
this.triggerUpdate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateBreadcrumbs: function () {
|
||||||
|
this.$route.meta.pageTitle = 'firefly.webhooks';
|
||||||
|
this.$route.meta.breadcrumbs = [{title: 'webhooks'}];
|
||||||
|
},
|
||||||
|
onRequest: function (props) {
|
||||||
|
this.page = props.pagination.page;
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
const list = new List();
|
||||||
|
this.rows = [];
|
||||||
|
list.list(this.page, this.getCacheKey).then(
|
||||||
|
(response) => {
|
||||||
|
this.pagination.rowsPerPage = response.data.meta.pagination.per_page;
|
||||||
|
this.pagination.rowsNumber = response.data.meta.pagination.total;
|
||||||
|
this.pagination.page = this.page;
|
||||||
|
|
||||||
|
for (let i in response.data.data) {
|
||||||
|
if (response.data.data.hasOwnProperty(i)) {
|
||||||
|
let current = response.data.data[i];
|
||||||
|
let account = {
|
||||||
|
id: current.id,
|
||||||
|
title: current.attributes.title,
|
||||||
|
};
|
||||||
|
this.rows.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
53
frontend/src/pages/webhooks/Show.vue
Normal file
53
frontend/src/pages/webhooks/Show.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Balance chart -->
|
||||||
|
<q-card bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">{{ webhook.title }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-xs">
|
||||||
|
Name: {{ webhook.title }}<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Get from "../../api/webhooks/get";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Show",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
webhook: {},
|
||||||
|
id: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.id = parseInt(this.$route.params.id);
|
||||||
|
this.getWebhook();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getWebhook: function () {
|
||||||
|
(new Get).get(this.id).then((response) => this.parseWebhook(response));
|
||||||
|
},
|
||||||
|
parseWebhook: function (response) {
|
||||||
|
this.webhook = {
|
||||||
|
title: response.data.data.attributes.title,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user