mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-09-22 12:04:00 +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