Fixed category balance calculations.
Build and Deploy Web App / build-and-deploy (push) Successful in 32s Details
Build and Deploy API / build-and-deploy (push) Successful in 1m48s Details

This commit is contained in:
Andrew Lalis 2026-02-17 20:24:19 -05:00
parent 1e35494f9d
commit 78ebbac9ca
3 changed files with 29 additions and 67 deletions

View File

@ -190,7 +190,7 @@ class SqliteTransactionCategoryRepository : TransactionCategoryRepository {
stmt.bind(idx++, beforeTimestamp.value.toISOExtString()); stmt.bind(idx++, beforeTimestamp.value.toISOExtString());
}); });
} }
string query = qb.build() ~ " ORDER BY je.currency ASC, je.type ASC"; string query = qb.build() ~ " GROUP BY je.currency, je.type ORDER BY je.currency ASC, je.type ASC";
Statement stmt = db.prepare(query); Statement stmt = db.prepare(query);
qb.applyArgBindings(stmt); qb.applyArgBindings(stmt);
ResultRange result = stmt.execute(); ResultRange result = stmt.execute();
@ -201,7 +201,6 @@ class SqliteTransactionCategoryRepository : TransactionCategoryRepository {
Currency currency = Currency.ofCode(row.peek!(string, PeekMode.slice)(0)); Currency currency = Currency.ofCode(row.peek!(string, PeekMode.slice)(0));
string journalEntryType = row.peek!(string, PeekMode.slice)(1); string journalEntryType = row.peek!(string, PeekMode.slice)(1);
ulong amountSum = row.peek!ulong(2); ulong amountSum = row.peek!ulong(2);
TransactionCategoryBalance balance; TransactionCategoryBalance balance;
if (currency.numericCode in balancesGroupedByCurrency) { if (currency.numericCode in balancesGroupedByCurrency) {

View File

@ -187,8 +187,10 @@ export class TransactionApiClient extends ApiClient {
return super.getJson(this.path + '/categories/' + parentId + '/children') return super.getJson(this.path + '/categories/' + parentId + '/children')
} }
getCategoryBalances(id: number): Promise<TransactionCategoryBalance[]> { getCategoryBalances(id: number, includeChildren: boolean): Promise<TransactionCategoryBalance[]> {
return super.getJson(this.path + '/categories/' + id + '/balances') return super.getJson(
this.path + '/categories/' + id + '/balances?includeChildren=' + includeChildren,
)
} }
createCategory(data: CreateCategoryPayload): Promise<TransactionCategory> { createCategory(data: CreateCategoryPayload): Promise<TransactionCategory> {

View File

@ -37,6 +37,7 @@ watch(balancesIncludeSubcategories, async () => {
if (!category.value) return if (!category.value) return
balances.value = await new TransactionApiClient(getSelectedProfile(route)).getCategoryBalances( balances.value = await new TransactionApiClient(getSelectedProfile(route)).getCategoryBalances(
category.value.id, category.value.id,
balancesIncludeSubcategories.value
) )
}) })
@ -49,6 +50,8 @@ async function loadCategory(id: number) {
category.value = undefined category.value = undefined
parentCategory.value = undefined parentCategory.value = undefined
childCategories.value = [] childCategories.value = []
balances.value = []
balancesIncludeSubcategories.value = true
relatedTransactionsPage.value = defaultPage() relatedTransactionsPage.value = defaultPage()
try { try {
const api = new TransactionApiClient(getSelectedProfile(route)) const api = new TransactionApiClient(getSelectedProfile(route))
@ -57,7 +60,7 @@ async function loadCategory(id: number) {
parentCategory.value = await api.getCategory(category.value.parentId) parentCategory.value = await api.getCategory(category.value.parentId)
} }
childCategories.value = await api.getChildCategories(category.value.id) childCategories.value = await api.getChildCategories(category.value.id)
balances.value = await api.getCategoryBalances(category.value.id) balances.value = await api.getCategoryBalances(category.value.id, balancesIncludeSubcategories.value)
await fetchPage(1) await fetchPage(1)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
@ -84,32 +87,16 @@ async function fetchPage(pg: number) {
} }
</script> </script>
<template> <template>
<AppPage <AppPage v-if="category" :title="'Category - ' + category.name">
v-if="category"
:title="'Category - ' + category.name"
>
<!-- Initial subtext with color & parent (if available). --> <!-- Initial subtext with color & parent (if available). -->
<div> <div>
<span <span class="category-color-indicator" :style="{ 'background-color': '#' + category.color }"></span>
class="category-color-indicator" <span v-if="parentCategory" class="text-muted" style="vertical-align: middle">
:style="{ 'background-color': '#' + category.color }"
></span>
<span
v-if="parentCategory"
class="text-muted"
style="vertical-align: middle"
>
A subcategory of A subcategory of
<RouterLink <RouterLink :to="`/profiles/${getSelectedProfile(route)}/categories/${parentCategory.id}`">{{
:to="`/profiles/${getSelectedProfile(route)}/categories/${parentCategory.id}`" parentCategory.name }}</RouterLink>.
>{{ parentCategory.name }}</RouterLink
>.
</span> </span>
<span <span v-if="!parentCategory" class="text-muted" style="vertical-align: middle">
v-if="!parentCategory"
class="text-muted"
style="vertical-align: middle"
>
This is a top-level category. This is a top-level category.
</span> </span>
</div> </div>
@ -121,12 +108,8 @@ async function fetchPage(pg: number) {
<div v-if="childCategories.length > 0"> <div v-if="childCategories.length > 0">
<h3>Subcategories</h3> <h3>Subcategories</h3>
<ul> <ul>
<li <li v-for="child in childCategories" :key="child.id">
v-for="child in childCategories" <RouterLink :to="`/profiles/${getSelectedProfile(route)}/categories/${child.id}`">{{ child.name }}
:key="child.id"
>
<RouterLink :to="`/profiles/${getSelectedProfile(route)}/categories/${child.id}`"
>{{ child.name }}
</RouterLink> </RouterLink>
</li> </li>
</ul> </ul>
@ -135,33 +118,22 @@ async function fetchPage(pg: number) {
<!-- Display total balances. --> <!-- Display total balances. -->
<div v-if="balances.length > 0"> <div v-if="balances.length > 0">
<h3>Balances</h3> <h3>Balances</h3>
<div <div v-for="balance in balances" :key="balance.currency.code">
v-for="balance in balances"
:key="balance.currency.code"
>
USD: USD:
<AppBadge <AppBadge>Debits:
>Debits:
<span class="text-positive">{{ formatMoney(balance.debits, balance.currency) }}</span> <span class="text-positive">{{ formatMoney(balance.debits, balance.currency) }}</span>
</AppBadge> </AppBadge>
<AppBadge <AppBadge>Credits:
>Credits:
<span class="text-negative">{{ formatMoney(balance.credits, balance.currency) }}</span> <span class="text-negative">{{ formatMoney(balance.credits, balance.currency) }}</span>
</AppBadge> </AppBadge>
<AppBadge <AppBadge>Balance:
>Balance: <span :class="{ 'text-positive': balance.balance > 0, 'text-negative': balance.balance < 0 }">{{
<span formatMoney(balance.balance, balance.currency) }}</span>
:class="{ 'text-positive': balance.balance > 0, 'text-negative': balance.balance < 0 }" </AppBadge>
>{{ formatMoney(balance.balance, balance.currency) }}</span
></AppBadge
>
</div> </div>
<FormGroup> <FormGroup>
<FormControl label="Include Subcategories"> <FormControl label="Include Subcategories" v-if="childCategories.length > 0">
<input <input type="checkbox" v-model="balancesIncludeSubcategories" />
type="checkbox"
v-model="balancesIncludeSubcategories"
/>
</FormControl> </FormControl>
<!-- <!--
<FormControl label="After"> <FormControl label="After">
@ -179,20 +151,9 @@ async function fetchPage(pg: number) {
<p class="text-muted font-size-small mt-0"> <p class="text-muted font-size-small mt-0">
Below is a list of all transactions recorded with this category, or any subcategory. Below is a list of all transactions recorded with this category, or any subcategory.
</p> </p>
<PaginationControls <PaginationControls :page="relatedTransactionsPage" @update="(pr) => fetchPage(pr.page)" class="align-right" />
:page="relatedTransactionsPage" <TransactionCard v-for="txn in relatedTransactionsPage.items" :key="txn.id" :tx="txn" />
@update="(pr) => fetchPage(pr.page)" <p v-if="relatedTransactionsPage.totalElements === 0" class="text-muted font-italic">
class="align-right"
/>
<TransactionCard
v-for="txn in relatedTransactionsPage.items"
:key="txn.id"
:tx="txn"
/>
<p
v-if="relatedTransactionsPage.totalElements === 0"
class="text-muted font-italic"
>
There are no transactions linked to this category. There are no transactions linked to this category.
</p> </p>
</div> </div>