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>
|
||||
import { Modal } from 'bootstrap'
|
||||
|
||||
const startingBalanceAcc = '00000000-0000-0000-0000-000000000002'
|
||||
import { unallocatedMoneyAcc } from '../constants'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
|
@ -224,7 +224,7 @@ export default {
|
|||
body: JSON.stringify({
|
||||
account: account.id,
|
||||
amount: this.modals.addAccount.startingBalance,
|
||||
category: startingBalanceAcc,
|
||||
category: unallocatedMoneyAcc,
|
||||
cleared: true,
|
||||
description: 'Starting Balance',
|
||||
time: new Date(),
|
||||
|
@ -249,7 +249,7 @@ export default {
|
|||
method: 'POST',
|
||||
})
|
||||
} 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',
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
@updateAccounts="fetchAccounts"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-grow-1 p-3">
|
||||
<router-view />
|
||||
<div class="d-flex flex-column flex-grow-1 py-3">
|
||||
<router-view :accounts="accounts" />
|
||||
</div>
|
||||
</div>
|
||||
</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 budgetDashboard from './components/budgetDashboard.vue'
|
||||
|
||||
const routes = [
|
||||
{ component: null, name: 'budget', path: '/' },
|
||||
{ component: budgetDashboard, name: 'budget', path: '/' },
|
||||
{ component: null, name: 'account-transactions', path: '/accounts/:id' },
|
||||
]
|
||||
|
||||
|
|
|
@ -136,12 +136,12 @@ func (a apiServer) handleTransferMoney(w http.ResponseWriter, r *http.Request) {
|
|||
a.errorResponse(w, err, "transferring money", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if err = a.dbc.TransferMoneyWithCategory(from, to, amount, category); err != nil {
|
||||
a.errorResponse(w, err, "transferring money", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -82,12 +82,18 @@ func (a apiServer) handleListTransactionsByAccount(w http.ResponseWriter, r *htt
|
|||
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 {
|
||||
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 {
|
||||
a.errorResponse(w, err, "getting transactions", http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -226,10 +226,10 @@ func (c *Client) ListAccountsByType(at AccountType, showHidden bool) (a []Accoun
|
|||
|
||||
// ListTransactionsByAccount retrieves all transactions for an account
|
||||
// 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 {
|
||||
return db.
|
||||
Where("time >= ?", since).
|
||||
Where("time >= ? and time <= ?", since, until).
|
||||
Find(&txs, "account = ? OR category = ?", acc, acc).
|
||||
Error
|
||||
}); err != nil {
|
||||
|
|
|
@ -187,11 +187,11 @@ func TestTransactions(t *testing.T) {
|
|||
checkAcctBal(bals, UnallocatedMoney, 500)
|
||||
|
||||
// List transactions
|
||||
txs, err := dbc.ListTransactionsByAccount(tb1.ID, time.Time{})
|
||||
txs, err := dbc.ListTransactionsByAccount(tb1.ID, time.Time{}, time.Now())
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
assert.Len(t, txs, 2)
|
||||
|
||||
|
@ -232,7 +232,7 @@ func TestTransactions(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
|
||||
// List transactions
|
||||
txs, err = dbc.ListTransactionsByAccount(tb1.ID, time.Time{})
|
||||
txs, err = dbc.ListTransactionsByAccount(tb1.ID, time.Time{}, time.Now())
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, txs, 3)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue