Add budget overview

This commit is contained in:
Knut Ahlers 2024-01-16 17:30:17 +01:00
parent 1194e36e6e
commit 6eb231f4d0
Signed by: luzifer
SSH key fingerprint: SHA256:/xtE5lCgiRDQr8SLxHMS92ZBlACmATUmF1crK16Ks4E
9 changed files with 117 additions and 18 deletions

View file

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

View file

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

View 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>&nbsp;</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
View file

@ -0,0 +1 @@
export const unallocatedMoneyAcc = '00000000-0000-0000-0000-000000000001'

View file

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

View file

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

View file

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

View file

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

View file

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