Add budget overview
This commit is contained in:
parent
1194e36e6e
commit
6eb231f4d0
9 changed files with 117 additions and 18 deletions
|
@ -168,7 +168,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Modal } from 'bootstrap'
|
import { Modal } from 'bootstrap'
|
||||||
|
|
||||||
const startingBalanceAcc = '00000000-0000-0000-0000-000000000002'
|
import { unallocatedMoneyAcc } from '../constants'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -224,7 +224,7 @@ export default {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
account: account.id,
|
account: account.id,
|
||||||
amount: this.modals.addAccount.startingBalance,
|
amount: this.modals.addAccount.startingBalance,
|
||||||
category: startingBalanceAcc,
|
category: unallocatedMoneyAcc,
|
||||||
cleared: true,
|
cleared: true,
|
||||||
description: 'Starting Balance',
|
description: 'Starting Balance',
|
||||||
time: new Date(),
|
time: new Date(),
|
||||||
|
@ -249,7 +249,7 @@ export default {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
})
|
})
|
||||||
} else if (account.type === 'category') {
|
} else if (account.type === 'category') {
|
||||||
return fetch(`/api/accounts/${startingBalanceAcc}/transfer/${account.id}?amount=${this.modals.addAccount.startingBalance}`, {
|
return fetch(`/api/accounts/${unallocatedMoneyAcc}/transfer/${account.id}?amount=${this.modals.addAccount.startingBalance}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
@updateAccounts="fetchAccounts"
|
@updateAccounts="fetchAccounts"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-column flex-grow-1 p-3">
|
<div class="d-flex flex-column flex-grow-1 py-3">
|
||||||
<router-view />
|
<router-view :accounts="accounts" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
90
frontend/components/budgetDashboard.vue
Normal file
90
frontend/components/budgetDashboard.vue
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<template>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<!-- TODO: Add time-selector -->
|
||||||
|
</div>
|
||||||
|
<div class="col d-flex align-items-center justify-content-end">
|
||||||
|
<div :class="unallocatedMoneyClass">
|
||||||
|
<span class="fs-4">{{ unallocatedMoney.toFixed(2) }} €</span>
|
||||||
|
<span class="small">Unallocated</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col">
|
||||||
|
<table class="table table-striped small">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Category</th>
|
||||||
|
<th class="text-end">
|
||||||
|
Activity
|
||||||
|
</th>
|
||||||
|
<th class="text-end">
|
||||||
|
Available
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="cat in categories"
|
||||||
|
:key="cat.id"
|
||||||
|
>
|
||||||
|
<td>{{ cat.name }}</td>
|
||||||
|
<td> </td>
|
||||||
|
<td :class="{'text-end': true, 'text-danger': cat.balance < 0}">
|
||||||
|
{{ cat.balance.toFixed(2) }} €
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { unallocatedMoneyAcc } from '../constants'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
categories() {
|
||||||
|
const accounts = this.accounts
|
||||||
|
.filter(acc => acc.type === 'category')
|
||||||
|
.filter(acc => acc.id !== unallocatedMoneyAcc)
|
||||||
|
accounts.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
return accounts
|
||||||
|
},
|
||||||
|
|
||||||
|
unallocatedMoney() {
|
||||||
|
const acc = this.accounts.filter(acc => acc.id === unallocatedMoneyAcc)[0] || null
|
||||||
|
if (acc === null) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return acc.balance
|
||||||
|
},
|
||||||
|
|
||||||
|
unallocatedMoneyClass() {
|
||||||
|
const classes = ['d-inline-flex', 'flex-column', 'text-center', 'ms-auto', 'p-2', 'rounded']
|
||||||
|
if (this.unallocatedMoney < 0) {
|
||||||
|
classes.push('bg-danger')
|
||||||
|
} else if (this.unallocatedMoney === 0) {
|
||||||
|
classes.push('bg-warning')
|
||||||
|
} else {
|
||||||
|
classes.push('bg-success')
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
name: 'AccountingAppBudgetDashboard',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
accounts: {
|
||||||
|
required: true,
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
1
frontend/constants.js
Normal file
1
frontend/constants.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const unallocatedMoneyAcc = '00000000-0000-0000-0000-000000000001'
|
|
@ -1,7 +1,9 @@
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
import budgetDashboard from './components/budgetDashboard.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ component: null, name: 'budget', path: '/' },
|
{ component: budgetDashboard, name: 'budget', path: '/' },
|
||||||
{ component: null, name: 'account-transactions', path: '/accounts/:id' },
|
{ component: null, name: 'account-transactions', path: '/accounts/:id' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -136,11 +136,11 @@ func (a apiServer) handleTransferMoney(w http.ResponseWriter, r *http.Request) {
|
||||||
a.errorResponse(w, err, "transferring money", http.StatusInternalServerError)
|
a.errorResponse(w, err, "transferring money", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
if err = a.dbc.TransferMoneyWithCategory(from, to, amount, category); err != nil {
|
||||||
if err = a.dbc.TransferMoneyWithCategory(from, to, amount, category); err != nil {
|
a.errorResponse(w, err, "transferring money", http.StatusInternalServerError)
|
||||||
a.errorResponse(w, err, "transferring money", http.StatusInternalServerError)
|
return
|
||||||
return
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
|
|
@ -82,12 +82,18 @@ func (a apiServer) handleListTransactionsByAccount(w http.ResponseWriter, r *htt
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var since time.Time
|
var (
|
||||||
|
since time.Time
|
||||||
|
until = time.Now()
|
||||||
|
)
|
||||||
if v, err := time.Parse(time.RFC3339, r.URL.Query().Get("since")); err == nil {
|
if v, err := time.Parse(time.RFC3339, r.URL.Query().Get("since")); err == nil {
|
||||||
since = v
|
since = v
|
||||||
}
|
}
|
||||||
|
if v, err := time.Parse(time.RFC3339, r.URL.Query().Get("until")); err == nil {
|
||||||
|
until = v
|
||||||
|
}
|
||||||
|
|
||||||
txs, err := a.dbc.ListTransactionsByAccount(accid, since)
|
txs, err := a.dbc.ListTransactionsByAccount(accid, since, until)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, err, "getting transactions", http.StatusInternalServerError)
|
a.errorResponse(w, err, "getting transactions", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
@ -226,10 +226,10 @@ func (c *Client) ListAccountsByType(at AccountType, showHidden bool) (a []Accoun
|
||||||
|
|
||||||
// ListTransactionsByAccount retrieves all transactions for an account
|
// ListTransactionsByAccount retrieves all transactions for an account
|
||||||
// or category
|
// or category
|
||||||
func (c *Client) ListTransactionsByAccount(acc uuid.UUID, since time.Time) (txs []Transaction, err error) {
|
func (c *Client) ListTransactionsByAccount(acc uuid.UUID, since, until time.Time) (txs []Transaction, err error) {
|
||||||
if err = c.retryRead(func(db *gorm.DB) error {
|
if err = c.retryRead(func(db *gorm.DB) error {
|
||||||
return db.
|
return db.
|
||||||
Where("time >= ?", since).
|
Where("time >= ? and time <= ?", since, until).
|
||||||
Find(&txs, "account = ? OR category = ?", acc, acc).
|
Find(&txs, "account = ? OR category = ?", acc, acc).
|
||||||
Error
|
Error
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|
|
@ -187,11 +187,11 @@ func TestTransactions(t *testing.T) {
|
||||||
checkAcctBal(bals, UnallocatedMoney, 500)
|
checkAcctBal(bals, UnallocatedMoney, 500)
|
||||||
|
|
||||||
// List transactions
|
// List transactions
|
||||||
txs, err := dbc.ListTransactionsByAccount(tb1.ID, time.Time{})
|
txs, err := dbc.ListTransactionsByAccount(tb1.ID, time.Time{}, time.Now())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, txs, 4)
|
assert.Len(t, txs, 4)
|
||||||
|
|
||||||
txs, err = dbc.ListTransactionsByAccount(UnallocatedMoney, time.Time{})
|
txs, err = dbc.ListTransactionsByAccount(UnallocatedMoney, time.Time{}, time.Now())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, txs, 2)
|
assert.Len(t, txs, 2)
|
||||||
|
|
||||||
|
@ -232,7 +232,7 @@ func TestTransactions(t *testing.T) {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
// List transactions
|
// List transactions
|
||||||
txs, err = dbc.ListTransactionsByAccount(tb1.ID, time.Time{})
|
txs, err = dbc.ListTransactionsByAccount(tb1.ID, time.Time{}, time.Now())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, txs, 3)
|
assert.Len(t, txs, 3)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue