Implement budget & category creation & updating
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
e4e2f24431
commit
02947db565
10 changed files with 319 additions and 154 deletions
|
@ -13,7 +13,7 @@
|
|||
import AppNavigation from "@nextcloud/vue/dist/Components/AppNavigation";
|
||||
import AppNavigationSettings from "@nextcloud/vue/dist/Components/AppNavigationSettings";
|
||||
import AppContent from "@nextcloud/vue/dist/Components/AppContent";
|
||||
import BudgetList from "./components/BudgetList";
|
||||
import BudgetList from "./components/budget/BudgetList";
|
||||
import axios from "@nextcloud/axios";
|
||||
|
||||
export default {
|
||||
|
@ -34,4 +34,4 @@ export default {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
<template>
|
||||
<div v-if="budget">
|
||||
<div class="header">
|
||||
<div class="header-info">
|
||||
<h2>{{ budget.name }}</h2>
|
||||
<h3
|
||||
v-if="balance"
|
||||
>Balance: {{ balance.toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</h3>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button @click="addTransaction()"><span class="icon-add"></span> Add Transaction</button>
|
||||
<button @click="addCategory()"><span class="icon-add"></span> Add Category</button>
|
||||
<Actions>
|
||||
<ActionButton icon="icon-edit" text="Edit" @click="alert('Delete')">Edit</ActionButton>
|
||||
<ActionButton icon="icon-delete" text="Delete" @click="alert('Delete')">Delete</ActionButton>
|
||||
</Actions>
|
||||
</div>
|
||||
</div>
|
||||
<div class="budget-details">
|
||||
<div class="card income">
|
||||
<h3>Income</h3>
|
||||
<CategoryList v-bind:budget-id="budget.id" v-bind:expense="false"></CategoryList>
|
||||
</div>
|
||||
<div class="card expenses">
|
||||
<h3>Expenses</h3>
|
||||
<CategoryList v-bind:budget-id="budget.id" v-bind:expense="true"></CategoryList>
|
||||
</div>
|
||||
<div class="card transactions">
|
||||
<h3>Recent Transactions</h3>
|
||||
<TransactionList :budget-id="budget.id" :limit="5"></TransactionList>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters, mapState } from "vuex";
|
||||
import { Actions } from "@nextcloud/vue/dist/Components/Actions";
|
||||
import { ActionButton } from "@nextcloud/vue/dist/Components/ActionButton";
|
||||
import CategoryList from "./category/CategoryList";
|
||||
import TransactionList from "./transaction/TransactionList";
|
||||
|
||||
export default {
|
||||
name: "budget-details",
|
||||
components: {
|
||||
Actions,
|
||||
ActionButton,
|
||||
CategoryList,
|
||||
TransactionList
|
||||
},
|
||||
computed: {
|
||||
...mapState(["budgets", "currentBudget"]),
|
||||
budget: function(state) {
|
||||
if (state.budgets.length === 0 || !state.currentBudget) {
|
||||
return false;
|
||||
}
|
||||
return state.budgets.find(budget => budget.id === state.currentBudget);
|
||||
},
|
||||
balance: function(state) {
|
||||
if (!state.currentBudget) {
|
||||
return 0;
|
||||
}
|
||||
return this.$store.getters.budgetBalance(state.currentBudget) / 100;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
this.$store.dispatch("budgetDetailsViewed", this.$route.params.id);
|
||||
},
|
||||
addTransaction() {
|
||||
this.$store.dispatch('addTransactionClicked')
|
||||
},
|
||||
addCategory() {
|
||||
this.$store.dispatch('addCategoryClicked')
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.load();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.budget-details {
|
||||
display: grid;
|
||||
justify-content: space-between;
|
||||
grid-gap: 0.5em;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.card {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.income {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.expenses {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.transactions {
|
||||
grid-column: 1;
|
||||
}
|
||||
</style>
|
112
src/components/budget/BudgetDetails.vue
Normal file
112
src/components/budget/BudgetDetails.vue
Normal file
|
@ -0,0 +1,112 @@
|
|||
<template>
|
||||
<div v-if="budget">
|
||||
<div class="header">
|
||||
<div class="header-info">
|
||||
<h2>{{ budget.name }}</h2>
|
||||
<p>{{ budget.description }}</p>
|
||||
<h3
|
||||
v-if="balance"
|
||||
>Balance: {{ balance.toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</h3>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button @click="addTransaction()"><span class="icon-add"></span> Add Transaction</button>
|
||||
<button @click="addCategory()"><span class="icon-add"></span> Add Category</button>
|
||||
<Actions>
|
||||
<ActionButton icon="icon-edit" text="Edit" @click="editBudget()">Edit</ActionButton>
|
||||
<ActionButton icon="icon-delete" text="Delete" @click="alert('Delete')">Delete</ActionButton>
|
||||
</Actions>
|
||||
</div>
|
||||
</div>
|
||||
<div class="budget-details">
|
||||
<div class="card income">
|
||||
<h3>Income</h3>
|
||||
<CategoryList v-bind:budget-id="budget.id" v-bind:expense="false"></CategoryList>
|
||||
</div>
|
||||
<div class="card expenses">
|
||||
<h3>Expenses</h3>
|
||||
<CategoryList v-bind:budget-id="budget.id" v-bind:expense="true"></CategoryList>
|
||||
</div>
|
||||
<div class="card transactions">
|
||||
<h3>Recent Transactions</h3>
|
||||
<TransactionList :budget-id="budget.id" :limit="5"></TransactionList>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters, mapState } from "vuex";
|
||||
import { Actions } from "@nextcloud/vue/dist/Components/Actions";
|
||||
import { ActionButton } from "@nextcloud/vue/dist/Components/ActionButton";
|
||||
import CategoryList from "../category/CategoryList";
|
||||
import TransactionList from "../transaction/TransactionList";
|
||||
|
||||
export default {
|
||||
name: "budget-details",
|
||||
components: {
|
||||
Actions,
|
||||
ActionButton,
|
||||
CategoryList,
|
||||
TransactionList
|
||||
},
|
||||
computed: {
|
||||
...mapState(["budgets", "currentBudget"]),
|
||||
...mapGetters(["budget"]),
|
||||
balance: function(state) {
|
||||
if (!state.currentBudget) {
|
||||
return 0;
|
||||
}
|
||||
return this.$store.getters.budgetBalance(state.currentBudget) / 100;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
this.$store.dispatch("budgetDetailsViewed", this.$route.params.id);
|
||||
},
|
||||
addTransaction() {
|
||||
this.$store.dispatch('addTransactionClicked')
|
||||
},
|
||||
addCategory() {
|
||||
this.$store.dispatch('addCategoryClicked')
|
||||
},
|
||||
editBudget() {
|
||||
this.$store.dispatch('editBudgetClicked', this.$route.params.id);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.load();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.budget-details {
|
||||
display: grid;
|
||||
justify-content: space-between;
|
||||
grid-gap: 0.5em;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.card {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.income {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.expenses {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.transactions {
|
||||
grid-column: 1;
|
||||
}
|
||||
</style>
|
61
src/components/budget/BudgetForm.vue
Normal file
61
src/components/budget/BudgetForm.vue
Normal file
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="!loading" class="add-edit-budget">
|
||||
<h2>{{ budget.id ? 'Edit' : 'Add' }} Budget</h2>
|
||||
<input v-model="budget.name" type="text" placeholder="Name" title="Name" />
|
||||
<textarea v-model="budget.description" placeholder="Description" title="Description"></textarea>
|
||||
<button @click="saveBudget()">Save Budget</button>
|
||||
</div>
|
||||
<div v-if="loading" class="icon-loading"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "add-edit-budget",
|
||||
components: {
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
saving: false
|
||||
};
|
||||
},
|
||||
props: {
|
||||
budget: Object
|
||||
},
|
||||
computed: {
|
||||
loading: state => state.budget === undefined || state.saving
|
||||
},
|
||||
methods: {
|
||||
saveBudget() {
|
||||
this.saving = true;
|
||||
this.$store.dispatch("budgetFormSaveClicked", this.budget);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let budgetId;
|
||||
if (this.budget) {
|
||||
budgetId = this.budget.id;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.add-edit-budget > * {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
.radio-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radio-container label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
.icon-loading {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<ul>
|
||||
<AppNavigationNew text="New Budget"></AppNavigationNew>
|
||||
<AppNavigationNew text="New Budget" @click="newBudget()"></AppNavigationNew>
|
||||
<AppNavigationItem
|
||||
v-for="budget in budgets"
|
||||
:key="budget.id"
|
||||
|
@ -27,7 +27,9 @@ export default {
|
|||
load: function() {
|
||||
this.$store.dispatch('budgetListViewed')
|
||||
},
|
||||
new: function() {}
|
||||
newBudget: function() {
|
||||
this.$store.dispatch('addBudgetClicked')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.load()
|
20
src/components/budget/EditBudget.vue
Normal file
20
src/components/budget/EditBudget.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<BudgetForm :budget="budget" />
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import BudgetForm from './BudgetForm'
|
||||
|
||||
export default {
|
||||
name: 'edit-budget',
|
||||
components: {
|
||||
BudgetForm
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['budget'])
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch("editBudgetViewed", this.$route.params.id);
|
||||
}
|
||||
}
|
||||
</script>
|
18
src/components/budget/NewBudget.vue
Normal file
18
src/components/budget/NewBudget.vue
Normal file
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<BudgetForm :budget="budget" />
|
||||
</template>
|
||||
<script>
|
||||
import BudgetForm from "./BudgetForm";
|
||||
|
||||
export default {
|
||||
name: "new-budget",
|
||||
components: {
|
||||
BudgetForm
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
budget: {}
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,43 +1,62 @@
|
|||
<template>
|
||||
<div v-if="category" class="category-details">
|
||||
<h2>{{ category.name }}</h2>
|
||||
<h3 v-if="balance">Balance: {{ balance.toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</h3>
|
||||
<h3>Transactions</h3>
|
||||
<TransactionList :category-id="category.id"></TransactionList>
|
||||
</div>
|
||||
<div v-if="category" class="category-details">
|
||||
<div class="header-info">
|
||||
<h2>{{ category.name }}</h2>
|
||||
<h3 v-if="balance">Balance: {{ balance.toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</h3>
|
||||
<div class="actions">
|
||||
<button @click="addTransaction()"><span class="icon-add"></span> Add Transaction</button>
|
||||
<Actions>
|
||||
<ActionButton icon="icon-edit" text="Edit" @click="editCategory()">Edit</ActionButton>
|
||||
<ActionButton icon="icon-delete" text="Delete" @click="alert('Delete')">Delete</ActionButton>
|
||||
</Actions>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Transactions</h3>
|
||||
<TransactionList :category-id="category.id"></TransactionList>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters, mapState } from "vuex";
|
||||
import { Actions } from "@nextcloud/vue/dist/Components/Actions";
|
||||
import { ActionButton } from "@nextcloud/vue/dist/Components/ActionButton";
|
||||
import TransactionList from '../transaction/TransactionList'
|
||||
|
||||
export default {
|
||||
name: "category-details",
|
||||
components: {
|
||||
TransactionList
|
||||
},
|
||||
computed: {
|
||||
...mapState(["categories", "currentCategory"]),
|
||||
category: function(state) {
|
||||
if (state.categories.length === 0 || !state.currentCategory) {
|
||||
return false;
|
||||
}
|
||||
return state.categories.find((category) => category.id === Number.parseInt(state.currentCategory));
|
||||
name: "category-details",
|
||||
components: {
|
||||
Actions,
|
||||
ActionButton,
|
||||
TransactionList
|
||||
},
|
||||
balance: function(state) {
|
||||
if (!state.currentCategory) {
|
||||
return 0;
|
||||
}
|
||||
return this.$store.getters.categoryBalance(state.currentCategory) / 100;
|
||||
computed: {
|
||||
...mapState(["categories", "currentCategory"]),
|
||||
category: function(state) {
|
||||
if (state.categories.length === 0 || !state.currentCategory) {
|
||||
return false;
|
||||
}
|
||||
return state.categories.find((category) => category.id === Number.parseInt(state.currentCategory));
|
||||
},
|
||||
balance: function(state) {
|
||||
if (!state.currentCategory) {
|
||||
return 0;
|
||||
}
|
||||
return this.$store.getters.categoryBalance(state.currentCategory) / 100;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
this.$store.dispatch("categoryDetailsViewed", this.$route.params.id);
|
||||
},
|
||||
addTransaction() {
|
||||
this.$store.dispatch('addTransactionClicked')
|
||||
},
|
||||
editCategory() {
|
||||
this.$store.dispatch('editCategoryClicked', this.$route.params.id);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log("CategoryDetails mounted")
|
||||
this.load();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
this.$store.dispatch("categoryDetailsViewed", this.$route.params.id);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log("CategoryDetails mounted")
|
||||
this.load();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import VueRouter from 'vue-router'
|
||||
import Vue from 'vue'
|
||||
import BudgetDetails from '../components/BudgetDetails'
|
||||
import NewBudget from '../components/budget/NewBudget'
|
||||
import EditBudget from '../components/budget/EditBudget'
|
||||
import BudgetDetails from '../components/budget/BudgetDetails'
|
||||
import NewCategory from '../components/category/NewCategory'
|
||||
import EditCategory from '../components/category/EditCategory'
|
||||
import CategoryDetails from '../components/category/CategoryDetails'
|
||||
|
@ -11,11 +13,21 @@ import TransactionDetails from '../components/transaction/TransactionDetails'
|
|||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/budgets/new',
|
||||
name: 'newBudget',
|
||||
component: NewBudget,
|
||||
},
|
||||
{
|
||||
path: '/budgets/:id',
|
||||
name: 'budgetDetails',
|
||||
component: BudgetDetails,
|
||||
},
|
||||
{
|
||||
path: '/budgets/:id/edit',
|
||||
name: 'editBudget',
|
||||
component: EditBudget,
|
||||
},
|
||||
{
|
||||
path: '/categories/new',
|
||||
name: 'newCategory',
|
||||
|
|
|
@ -18,12 +18,10 @@ export default new Vuex.Store({
|
|||
},
|
||||
getters: {
|
||||
budgets: (state) => state.budgets,
|
||||
budget: (state) => (id) => state.budgets.find(budget => budget.id === id),
|
||||
budget: (state) => state.budgets.find(budget => budget.id === state.currentBudget),
|
||||
budgetBalance: (state) => (id) => state.budgetBalances[id],
|
||||
categories: (state) => state.categories,
|
||||
category: (state) => (id) => {
|
||||
return state.categories.find(category => category.id === id)
|
||||
},
|
||||
category: (state) => state.categories.find(category => category.id === state.currentCategory),
|
||||
categoryBalance: (state) => (categoryId) => {
|
||||
return state.categoryBalances[categoryId];
|
||||
},
|
||||
|
@ -35,6 +33,9 @@ export default new Vuex.Store({
|
|||
transaction: (state) => state.transactions.find(transaction => transaction.id === state.currentTransaction),
|
||||
},
|
||||
actions: {
|
||||
addBudgetClicked({ commit }) {
|
||||
router.push({ name: "newBudget" })
|
||||
},
|
||||
budgetListViewed({ commit }) {
|
||||
axios.get(OC.generateUrl('/apps/twigs/api/v1.0/budgets'))
|
||||
.then(function (response) {
|
||||
|
@ -53,6 +54,27 @@ export default new Vuex.Store({
|
|||
budgetClicked({ commit }, budgetId) {
|
||||
router.push({ name: "budgetDetails", params: { id: budgetId } })
|
||||
},
|
||||
editBudgetViewed({ commit, state, getters }, budgetId) {
|
||||
commit('setCurrentBudget', budgetId)
|
||||
if (budgetId !== undefined && getters.budget === undefined) {
|
||||
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/budgets/${budgetId}`))
|
||||
.then((response) => {
|
||||
commit('setBudgets', [response.data])
|
||||
})
|
||||
}
|
||||
},
|
||||
budgetFormSaveClicked({ commit }, budget) {
|
||||
let request;
|
||||
if (budget.id) {
|
||||
request = axios.put(OC.generateUrl(`/apps/twigs/api/v1.0/budgets/${budget.id}`), budget)
|
||||
} else {
|
||||
request = axios.post(OC.generateUrl(`/apps/twigs/api/v1.0/budgets`), budget)
|
||||
}
|
||||
request.then(response => {
|
||||
commit('addBudget', response.data)
|
||||
router.push({ name: "budgetDetails", params: { id: response.data.id } })
|
||||
})
|
||||
},
|
||||
budgetDetailsViewed({ commit }, budgetId) {
|
||||
commit('setCurrentBudget', budgetId)
|
||||
commit('setCategories', [])
|
||||
|
@ -74,12 +96,18 @@ export default new Vuex.Store({
|
|||
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions?budgetId=${budgetId}?count=10`))
|
||||
.then((response) => commit('setTransactions', response.data))
|
||||
},
|
||||
editBudgetClicked({ commit }, budgetId) {
|
||||
router.push({ name: "editBudget" , params: { id: budgetId } })
|
||||
},
|
||||
categoryClicked({ commit }, categoryId) {
|
||||
router.push({ name: "categoryDetails", params: { id: categoryId } })
|
||||
},
|
||||
addCategoryClicked({ commit }) {
|
||||
router.push({ name: "newCategory" })
|
||||
},
|
||||
editCategoryClicked({ commit }, categoryId) {
|
||||
router.push({ name: "editCategory" , params: { id: categoryId } })
|
||||
},
|
||||
editCategoryViewed({ commit, state, getters }, categoryId) {
|
||||
commit('setCurrentCategory', categoryId)
|
||||
if (categoryId !== undefined && getters.category === undefined) {
|
||||
|
@ -174,6 +202,12 @@ export default new Vuex.Store({
|
|||
},
|
||||
},
|
||||
mutations: {
|
||||
addBudget(state, budget) {
|
||||
state.budgets = [
|
||||
...state.budgets.filter(b => b.id !== budget.id),
|
||||
budget
|
||||
]
|
||||
},
|
||||
setCurrentBudget(state, budgetId) {
|
||||
state.currentBudget = Number.parseInt(budgetId)
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue