Implement budget & category creation & updating

Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
William Brawner 2020-03-17 21:47:53 -06:00
parent e4e2f24431
commit 02947db565
10 changed files with 319 additions and 154 deletions

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View file

@ -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()

View 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>

View 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>

View file

@ -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>

View file

@ -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',

View file

@ -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)
},