Build out more frontend views

Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
William Brawner 2020-03-15 20:22:27 -06:00
parent a5e349e776
commit 69c21ad37b
18 changed files with 745 additions and 79 deletions

View file

@ -1,5 +1,8 @@
module.exports = { module.exports = {
extends: [ extends: [
'nextcloud' 'nextcloud'
] ],
"rules": {
"indent": ["error", 4]
}
}; };

View file

@ -23,7 +23,7 @@ return [
[ [
'name' => 'transaction#sum', 'name' => 'transaction#sum',
'url' => '/api/v1.0/transactions/sum', 'url' => '/api/v1.0/transactions/sum',
'verb' => 'POST', 'verb' => 'GET',
] ]
] ]
]; ];

View file

@ -1,3 +1,22 @@
#hello { :root {
color: red; --good-color: green;
--warn-color: yellow;
--danger-color: red;
} }
h2, h3 {
margin: 0;
padding: 0.25em 0.5em;
}
.good {
color: var(--good-color);
}
.warn {
color: var(--warn-color);
}
.danger {
color: var(--danger-color);
}

View file

@ -176,4 +176,14 @@ class BudgetController extends Controller
$this->budgetMapper->delete($budget); $this->budgetMapper->delete($budget);
return new DataResponse($budget); return new DataResponse($budget);
} }
public function stats(int $budgetId) {
try {
$userPermission = $this->userPermissionMapper->find($id, $this->userId);
$budget = $this->budgetMapper->find($userPermission->getBudgetId());
} catch (Exception $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
}
} }

View file

@ -25,6 +25,8 @@ class TransactionController extends Controller
private $transactionMapper; private $transactionMapper;
private $userPermissionMapper; private $userPermissionMapper;
private $logger; private $logger;
private $DATE_FORMAT = DateTime::RFC3339_EXTENDED;
private const DATE_FORMAT = "Y-m-d\TH:i:s.v\Z";
public function __construct( public function __construct(
$AppName, $AppName,
@ -49,16 +51,19 @@ class TransactionController extends Controller
* @NoAdminRequired * @NoAdminRequired
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function index() public function index(?int $budgetId, ?int $categoryId, ?int $count)
{ {
$budgetId = $_GET['budgetId'];
$categoryId = $_GET['categoryId'];
if ($budgetId == null) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
try { try {
$this->userPermissionMapper->find($budgetId, $this->userId); if ($budgetId != null) {
return new DataResponse($this->transactionMapper->findAll($budgetId, $categoryId)); $this->userPermissionMapper->find($budgetId, $this->userId);
} else if ($categoryId != null) {
$category = $this->categoryMapper->find($categoryId);
$budgetId = $category->getBudgetId();
$this->userPermissionMapper->find($budgetId, $this->userId);
} else {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
return new DataResponse($this->transactionMapper->findAll($budgetId, $categoryId, $count));
} catch (Exception $e) { } catch (Exception $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND); return new DataResponse([], Http::STATUS_NOT_FOUND);
} }
@ -95,7 +100,7 @@ class TransactionController extends Controller
*/ */
public function create( public function create(
string $name, string $name,
string $description, ?string $description,
int $amount, int $amount,
string $date, string $date,
bool $expense, bool $expense,
@ -115,9 +120,9 @@ class TransactionController extends Controller
$transaction->setDescription($description); $transaction->setDescription($description);
$transaction->setAmount($amount); $transaction->setAmount($amount);
$transaction->setExpense($expense); $transaction->setExpense($expense);
$dateTime = DateTime::createFromFormat(DateTime::ATOM, $date); $dateTime = DateTime::createFromFormat($this->DATE_FORMAT, $date);
if (!$dateTime) { if (!$dateTime) {
return new DataResponse([], Http::STATUS_BAD_REQUEST); return new DataResponse(["message" => "Invalid date format: '$date'"], Http::STATUS_BAD_REQUEST);
} }
$transaction->setDate($dateTime->getTimestamp()); $transaction->setDate($dateTime->getTimestamp());
$this->logger->error("Setting category $categoryId for new transaction"); $this->logger->error("Setting category $categoryId for new transaction");
@ -168,7 +173,7 @@ class TransactionController extends Controller
$transaction->setDescription($description); $transaction->setDescription($description);
$transaction->setAmount($amount); $transaction->setAmount($amount);
$transaction->setExpense($expense); $transaction->setExpense($expense);
$dateTime = DateTime::createFromFormat(DateTime::ATOM, $date); $dateTime = DateTime::createFromFormat($this->DATE_FORMAT, $date);
if (!$dateTime) { if (!$dateTime) {
return new DataResponse([], Http::STATUS_BAD_REQUEST); return new DataResponse([], Http::STATUS_BAD_REQUEST);
} }
@ -235,7 +240,7 @@ class TransactionController extends Controller
); );
$startDateTime->setTime(0, 0, 0, 0); $startDateTime->setTime(0, 0, 0, 0);
} else { } else {
$startDateTime = DateTime::createFromFormat(DateTime::ATOM, $startDate); $startDateTime = DateTime::createFromFormat($this->DATE_FORMAT, $startDate);
} }
if (!$startDateTime) { if (!$startDateTime) {
return new DataResponse([], Http::STATUS_BAD_REQUEST); return new DataResponse([], Http::STATUS_BAD_REQUEST);
@ -250,7 +255,7 @@ class TransactionController extends Controller
); );
$endDateTime->setTime(23, 59, 59, 999); $endDateTime->setTime(23, 59, 59, 999);
} else { } else {
$endDateTime = DateTime::createFromFormat(DateTime::ATOM, $endDate); $endDateTime = DateTime::createFromFormat($this->DATE_FORMAT, $endDate);
} }
if (!$endDateTime) { if (!$endDateTime) {
return new DataResponse([], Http::STATUS_BAD_REQUEST); return new DataResponse([], Http::STATUS_BAD_REQUEST);

View file

@ -101,5 +101,5 @@ class BudgetMapper extends QBMapper
$qb->execute(); $qb->execute();
return $entity; return $entity;
} }
} }

View file

@ -29,8 +29,11 @@ class TransactionMapper extends QBMapper
return $this->findEntity($qb); return $this->findEntity($qb);
} }
public function findAll(int $budgetId, ?int $categoryId) public function findAll(
{ int $budgetId,
?int $categoryId,
?int $count
) {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
@ -45,6 +48,12 @@ class TransactionMapper extends QBMapper
); );
} }
$qb->orderBy('date', 'desc');
if ($count) {
$qb->setMaxResults($count);
}
return $this->findEntities($qb); return $this->findEntities($qb);
} }
@ -115,4 +124,8 @@ class TransactionMapper extends QBMapper
$statement->execute(); $statement->execute();
return (int) $statement->fetch(FetchMode::COLUMN); return (int) $statement->fetch(FetchMode::COLUMN);
} }
public function countByBudgetId(int $budgetId) {
}
} }

View file

@ -24,4 +24,14 @@ export default {
BudgetList, BudgetList,
}, },
}; };
</script> </script>
<style>
.app-twigs {
flex-grow: 1;
}
.app-twigs > div {
width: 100%;
}
</style>

View file

@ -0,0 +1,100 @@
<template>
<div>
<div v-if="!loading && transaction" class="add-edit-transaction">
<h2>{{ transaction.id ? 'Edit' : 'Add' }} Transaction</h2>
<input v-model="transaction.name" type="text" placeholder="Name" title="Name" />
<textarea v-model="transaction.description" placeholder="Description" title="Description"></textarea>
<input v-model.number="transaction.amount" type="number" placeholder="Amount" title="Amount" />
<DatetimePicker :value="transaction.date" type="datetime" />
<div class="radio-container">
<input v-model="transaction.expense" type="radio" id="expense" :value="true" />
<label for="expense">Expense</label>
<input v-model="transaction.expense" type="radio" id="income" :value="false" />
<label for="income">Income</label>
</div>
<select v-model="transaction.budgetId" v-on:change="updateCategories()">
<option disabled value>Select a budget</option>
<option v-for="budget in budgets" :key="budget.id" :value="budget.id">{{ budget.name }}</option>
</select>
<select v-model="transaction.categoryId">
<option disabled value>Select a category</option>
<option
v-for="category in filteredCategories"
:key="category.id"
:value="category.id"
>{{ category.name }}</option>
</select>
<button @click="saveTransaction()">Save Transaction</button>
</div>
<div v-if="loading" class="icon-loading"></div>
</div>
</template>
<script>
import { DatetimePicker } from "@nextcloud/vue/dist/Components/DatetimePicker";
import { mapGetters } from "vuex";
export default {
name: "add-edit-transaction",
components: {
DatetimePicker
},
data: function() {
return {
transaction: Object,
loading: true
};
},
computed: {
...mapGetters(["budgets"]),
filteredCategories: function(state) {
return this.$store.getters.categories.filter(function(category) {
return category.expense === state.transaction.expense;
});
}
},
methods: {
updateCategories() {
this.$store.dispatch(
"addEditTransactionBudgetSelected",
this.transaction.budgetId
);
},
saveTransaction() {
this.loading = true
this.$store.dispatch('addEditTransactionSaveClicked', this.transaction)
}
},
mounted() {
if (this.$route.params.id) {
this.transaction = this.$store.getters.transaction(this.$route.params.id);
} else {
this.transaction = {
date: new Date(),
expense: true,
budgetId: this.$store.state.currentBudget,
categoryId: this.$store.state.currentCategory
};
}
this.loading = false;
this.updateCategories();
}
};
</script>
<style scoped>
.add-edit-transaction > * {
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,39 +1,110 @@
<template> <template>
<div v-if="budget"> <div v-if="budget">
<h1 v-text="budget.name"></h1> <div class="header">
<div class="card"> <div class="header-info">
<CategoryList v-bind:budget-id="budget.id" v-bind:expense="true"></CategoryList> <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><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>
<div class="card"> <div class="budget-details">
<CategoryList v-bind:budget-id="budget.id" v-bind:expense="false"></CategoryList> <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"></TransactionList>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import { mapGetters, mapState } from "vuex";
import { Actions } from "@nextcloud/vue/dist/Components/Actions";
import { ActionButton } from "@nextcloud/vue/dist/Components/ActionButton";
import CategoryList from "./CategoryList"; import CategoryList from "./CategoryList";
import TransactionList from "./TransactionList";
export default { export default {
name: "budget-details", name: "budget-details",
components: { components: {
CategoryList Actions,
ActionButton,
CategoryList,
TransactionList
}, },
computed: { computed: {
...mapState(["budgets", "currentBudget"]),
budget: function(state) { budget: function(state) {
const budgetId = this.$route.params.id; if (state.budgets.length === 0 || !state.currentBudget) {
const budget = this.$store.getters.budgets.find( return false;
budget => budget.id === budgetId }
); return state.budgets.find(budget => budget.id === state.currentBudget);
return budget; },
balance: function(state) {
if (!state.currentBudget) {
return 0;
}
return this.$store.getters.budgetBalance(state.currentBudget) / 100;
} }
}, },
methods: { methods: {
load() { load() {
this.$store.dispatch("budgetDetailsViewed", this.$route.params.id); this.$store.dispatch("budgetDetailsViewed", this.$route.params.id);
},
addTransaction() {
this.$store.dispatch('addTransactionClicked')
} }
}, },
mounted() { mounted() {
this.load() this.load();
} }
}; };
</script> </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

@ -1,13 +1,13 @@
<template> <template>
<div> <ul>
<AppNavigationNew text="New Budget"></AppNavigationNew>
<AppNavigationItem <AppNavigationItem
v-for="budget in budgets" v-for="budget in budgets"
:key="budget.id" :key="budget.id"
:title="budget.name" :title="budget.name"
v-on:click="view(budget.id)" :to="{ name: 'budgetDetails', params: { id: budget.id } }"
></AppNavigationItem> />
<AppNavigationNew text="New Budget"></AppNavigationNew> </ul>
</div>
</template> </template>
<script> <script>
import { AppNavigationItem } from "@nextcloud/vue/dist/Components/AppNavigationItem"; import { AppNavigationItem } from "@nextcloud/vue/dist/Components/AppNavigationItem";
@ -27,9 +27,6 @@ export default {
load: function() { load: function() {
this.$store.dispatch('budgetListViewed') this.$store.dispatch('budgetListViewed')
}, },
view: function(id) {
this.$router.push({ name: "budgetDetails", params: { id: id } })
},
new: function() {} new: function() {}
}, },
mounted() { mounted() {

View file

@ -0,0 +1,43 @@
<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>
</template>
<script>
import { mapGetters, mapState } from "vuex";
import TransactionList from './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));
},
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);
}
},
mounted() {
console.log("CategoryDetails mounted")
this.load();
}
};
</script>

View file

@ -1,29 +1,73 @@
<template> <template>
<ul> <ul>
<li v-for="category in categories" :key="category.id"> <li v-for="category in filteredCategories" :key="category.id">
<p>{{ category.name }}</p> <a v-on:click="view(category.id)" class="category-summary">
<div class="category-info">
<p class="category-name">{{ category.name }}</p>
<p
class="category-balance"
>{{ category.expense ? 'Remaining' : 'Pending' }}: {{ (categoryRemainingBalance(category) / 100).toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</p>
</div>
<ProgressBar
:max="category.amount"
:value="Math.abs(categoryBalance(category.id))"
:invert-colors="!category.expense"
></ProgressBar>
</a>
</li> </li>
</ul> </ul>
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import { mapGetters, mapState } from "vuex";
import ProgressBar from "./ProgressBar";
export default { export default {
name: "category-list", name: "category-list",
components: {}, components: {
ProgressBar
},
props: { props: {
budgetId: Number, budgetId: Number,
expense: Boolean expense: Boolean
}, },
computed: { computed: {
categories: function(state) { ...mapState(["categories", "currentCategory"]),
const categories = this.$store.getters.categories(this.budgetId) ...mapGetters(["categoryBalance", "categoryRemainingBalance"]),
if (categories) { filteredCategories: function(state) {
return categories.filter(category => category.expense === this.expense) return state.categories.filter(
} else { category => category.expense === this.expense
return []; );
} }
},
methods: {
view: function(id) {
this.$store.dispatch("categoryClicked", id);
} }
} }
}; };
</script> </script>
<style>
.category-summary {
padding: 0.5em;
height: 4em;
display: flex;
flex-direction: column;
justify-content: space-evenly;
cursor: pointer;
}
.category-summary * {
cursor: pointer;
}
.category-summary:hover {
background: var(--color-background-hover);
}
.category-summary .category-info {
display: flex;
justify-content: space-between;
padding-bottom: 0.5em;
}
</style>

View file

@ -0,0 +1,57 @@
<template>
<div class="progress-bar">
<div class="progress" :class="status" :style="{ width: progress + '%' }"></div>
</div>
</template>
<script>
export default {
name: "progress-bar",
props: {
value: Number,
max: Number,
invertColors: Boolean
},
computed: {
progress: function(state) {
return Math.round((this.value / this.max) * 100);
},
status: function(state) {
if (this.progress <= 33) {
return this.invertColors ? "danger" : "good";
}
if (this.progress <= 66) {
return "warn";
}
if (this.progress <= 100) {
return this.invertColors ? "good" : "danger";
}
return "";
}
}
};
</script>
<style scoped>
.progress-bar {
height: 0.5em;
background: #f1f1f1;
border-radius: 1em;
overflow: hidden;
}
.progress {
height: 100%;
width: 0%;
transition: width ease-in-out 0.5s;
}
.progress.good {
background: var(--good-color);
}
.progress.warn {
background: var(--warn-color);
}
.progress.danger {
background: var(--danger-color);
}
</style>

View file

@ -0,0 +1,64 @@
<template>
<div v-if="transaction" class="transaction-details">
<h2>{{ transaction.name }}</h2>
<h3
:class="transaction.expense ? 'danger' : 'good'"
>{{ (transaction.amount / 100).toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }} {{ transaction.expense ? 'Expense' : 'Income' }}</h3>
<p class="transaction-info date">{{ new Date(transaction.date).toLocaleDateString() }}</p>
<p class="transaction-info description">{{ transaction.description }}</p>
<p v-if="category" class="transaction-info category">Category: {{ category.name }}</p>
<p v-if="budget" class="transaction-info budget">Budget: {{ budget.name }}</p>
<p class="transaction-info registered-by">
Registered By:
<UserBubble
:user="transaction.createdBy"
:display-name="transaction.createdBy"
></UserBubble>
{{ new Date(transaction.createdDate).toLocaleDateString() }}
</p>
<p v-if="transaction.updatedBy" class="transaction-info updated-by">
Updated By:
<UserBubble
:user="transaction.updatedBy"
:display-name="transaction.updatedBy"
></UserBubble>
{{ new Date(transaction.updatedDate).toLocaleDateString() }}
</p>
</div>
</template>
<script>
import { mapGetters, mapState } from "vuex";
import { UserBubble } from "@nextcloud/vue/dist/Components/UserBubble";
export default {
name: "transaction-details",
components: {
UserBubble
},
computed: {
...mapGetters(["transaction"]),
category: function(state, getters) {
const transaction = this.$store.getters.transaction
if (!transaction || !transaction.categoryId) {
return undefined;
}
return this.$store.getters.category(transaction.categoryId)
},
budget: function(state) {
const transaction = this.$store.getters.transaction
if (!transaction || !transaction.budgetId) {
return undefined;
}
return this.$store.getters.budget(transaction.budgetId)
}
},
methods: {
load() {
this.$store.dispatch("transactionDetailsViewed", this.$route.params.id);
}
},
mounted() {
this.load();
}
};
</script>

View file

@ -0,0 +1,74 @@
<template>
<ul>
<li v-for="transaction in filteredTransactions" :key="transaction.id">
<a v-on:click="view(transaction.id)" class="transaction">
<div class="transaction-details">
<p class="transaction-name">{{ transaction.name }}</p>
<p class="transaction-date">{{ new Date(transaction.date).toLocaleDateString() }}</p>
</div>
<p
class="transaction-amount"
:class="transaction.expense ? 'danger' : 'good'"
>{{ (transaction.amount / 100).toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</p>
</a>
</li>
</ul>
</template>
<script>
import { mapGetters, mapState } from "vuex";
export default {
name: "transaction-list",
components: {
},
props: {
budgetId: Number,
categoryId: Number,
},
computed: {
...mapState(["transactions", "currentTransaction"]),
filteredTransactions: function(state) {
return state.transactions.filter(function(transaction) {
console.log(transaction.date)
if (state.budgetId) {
return transaction.budgetId === state.budgetId
}
if (state.categoryId) {
return transaction.categoryId === state.categoryId
}
return false
}
);
}
},
methods: {
view: function(id) {
this.$store.dispatch("transactionClicked", id);
}
}
};
</script>
<style>
.transaction {
padding: 0.5em;
height: 4em;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.transaction * {
cursor: pointer;
}
.transaction:hover {
background: var(--color-background-hover);
}
.transaction-details {
display: flex;
flex-direction: column;
justify-content: space-between;
}
</style>

View file

@ -1,6 +1,9 @@
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
import Vue from 'vue' import Vue from 'vue'
import BudgetDetails from '../components/BudgetDetails' import BudgetDetails from '../components/BudgetDetails'
import CategoryDetails from '../components/CategoryDetails'
import AddEditTransaction from '../components/AddEditTransaction'
import TransactionDetails from '../components/TransactionDetails'
Vue.use(VueRouter) Vue.use(VueRouter)
@ -9,7 +12,27 @@ const routes = [
path: '/budgets/:id', path: '/budgets/:id',
name: 'budgetDetails', name: 'budgetDetails',
component: BudgetDetails, component: BudgetDetails,
} },
{
path: '/categories/:id',
name: 'categoryDetails',
component: CategoryDetails,
},
{
path: '/transactions/new',
name: 'newTransaction',
component: AddEditTransaction,
},
{
path: '/transactions/:id',
name: 'transactionDetails',
component: TransactionDetails,
},
{
path: '/transactions/:id/edit',
name: 'editTransaction',
component: AddEditTransaction,
},
] ]
export default new VueRouter({ export default new VueRouter({

View file

@ -1,56 +1,189 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import router from '../router'
Vue.use(Vuex) Vue.use(Vuex)
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
budgets: [], budgets: [],
budgetBalances: {},
currentBudget: 0, currentBudget: 0,
categories: {}, categories: [],
categoryBalances: {},
currentCategory: 0, currentCategory: 0,
transactions: [], transactions: [],
currentTransaction: 0, currentTransaction: 0,
}, },
getters: { getters: {
budgets: (state) => { budgets: (state) => state.budgets,
return state.budgets budget: (state) => (id) => state.budgets.find(budget => budget.id === id),
budgetBalance: (state) => (id) => state.budgetBalances[id],
categories: (state) => state.categories,
category: (state) => (id) => {
return state.categories.find(category => category.id === id)
}, },
budget: (state) => (id) => { categoryBalance: (state) => (categoryId) => {
return state.budgets.find(budget => budget.id === id) return state.categoryBalances[categoryId];
}, },
categories: (state) => (budgetId) => { categoryRemainingBalance: (state, getters) => (category) => {
return state.categories[budgetId] const modifier = category.expense ? -1 : 1;
return category.amount - (getters.categoryBalance(category.id) * modifier);
}, },
transactions: (state) => state.transactions,
transaction: (state) => state.transactions.find(transaction => transaction.id === state.currentTransaction),
}, },
actions: { actions: {
budgetListViewed({ commit }) { budgetListViewed({ commit }) {
axios.get(OC.generateUrl('/apps/twigs/api/v1.0/budgets')) axios.get(OC.generateUrl('/apps/twigs/api/v1.0/budgets'))
.then(function (response) { .then(function (response) {
commit('setBudgets', response.data) commit('setBudgets', response.data)
}) response.data.forEach(budget => {
}, axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/sum?budgetId=${budget.id}`))
budgetDetailsViewed({ commit }, budgetId) { .then(function (response) {
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/categories?budgetId=${budgetId}`)) commit({
.then(function (response) { type: 'setBudgetBalance',
commit({ ...response.data
type: 'setCategories', })
budgetId: budgetId, })
categories: response.data
}) })
}) })
} },
budgetClicked({ commit }, budgetId) {
router.push({ name: "budgetDetails", params: { id: budgetId } })
},
budgetDetailsViewed({ commit }, budgetId) {
commit('setCurrentBudget', budgetId)
commit('setCategories', [])
commit('setTransactions', [])
commit('setCurrentCategory', undefined)
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/categories?budgetId=${budgetId}`))
.then(function (response) {
commit('setCategories', response.data)
response.data.forEach(category => {
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/sum?categoryId=${category.id}`))
.then(function (response) {
commit({
type: 'setCategoryBalance',
...response.data
})
})
});
})
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions?budgetId=${budgetId}?count=10`))
.then((response) => commit('setTransactions', response.data))
},
categoryClicked({ commit }, categoryId) {
router.push({ name: "categoryDetails", params: { id: categoryId } })
},
categoryDetailsViewed({ commit, state }, categoryId) {
commit('setCurrentCategory', categoryId)
if (state.categories.length === 0) {
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/categories/${categoryId}`))
.then((response) => {
commit('setCategories', [response.data])
})
}
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions?categoryId=${categoryId}`))
.then((response) => commit('setTransactions', response.data))
},
addTransactionClicked({ commit }) {
router.push({ name: "newTransaction" })
},
addEditTransactionViewed({ commit, state, getters }, transactionId) {
if (transactionId && getters.transaction(transactionId) === undefined) {
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/${transactionId}`))
.then((response) => {
commit('setTransactions', [response.data])
})
}
},
addEditTransactionBudgetSelected({ commit, state }, budgetId) {
commit('setCategories', [])
if (!budgetId) return;
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/categories?budgetId=${budgetId}`))
.then(function (response) {
commit('setCategories', response.data)
})
},
addEditTransactionSaveClicked({ commit }, transaction) {
let request;
if (transaction.id) {
request = axios.put(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/${transaction.id}`), transaction)
} else {
request = axios.post(OC.generateUrl(`/apps/twigs/api/v1.0/transactions`), transaction)
}
request.then(response => {
commit('addTransaction', response.data)
router.push({ name: "transactionDetails", params: { id: response.data.id } })
})
},
transactionClicked({ commit }, transactionId) {
router.push({ name: "transactionDetails", params: { id: transactionId } })
},
transactionDetailsViewed({ commit, state }, transactionId) {
commit('setCurrentTransaction', transactionId)
if (state.transactions.length === 0) {
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/${transactionId}`))
.then((response) => {
commit('setTransactions', [response.data])
if (state.categories.length === 0) {
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/categories?budgetId=${response.data.budgetId}`))
.then(function (response) {
commit('setCategories', response.data)
response.data.forEach(category => {
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/sum?categoryId=${category.id}`))
.then(function (response) {
commit({
type: 'setCategoryBalance',
...response.data
})
})
});
})
}
})
}
},
}, },
mutations: { mutations: {
setCurrentBudget(state, budgetId) {
state.currentBudget = Number.parseInt(budgetId)
},
setBudgetBalance(state, data) {
state.budgetBalances = {
...state.budgetBalances,
[data.budgetId]: data.sum
}
},
setBudgets(state, budgets) { setBudgets(state, budgets) {
state.budgets = budgets state.budgets = budgets
}, },
setCurrentCategory(state, categoryId) {
state.currentCategory = Number.parseInt(categoryId)
},
setCategories(state, data) { setCategories(state, data) {
state.categories = { state.categories = data
...state.categories, },
[data.budgetId]: data.categories setCategoryBalance(state, data) {
state.categoryBalances = {
...state.categoryBalances,
[data.categoryId]: data.sum
} }
} },
addTransaction(state, transaction) {
state.transactions = [
...state.transactions.filter(t => t.id !== transaction.id),
transaction
]
},
setTransactions(state, data) {
state.transactions = data
},
setCurrentTransaction(state, transactionId) {
state.currentTransaction = Number.parseInt(transactionId)
},
} }
}) })