Added total balances API endpoint.
This commit is contained in:
parent
df4460d2ca
commit
25b715156b
|
|
@ -115,6 +115,12 @@ private void writeHistoryResponse(ref ServerHttpResponse response, in Page!Accou
|
||||||
response.writeBodyString(jsonStr, ContentTypes.APPLICATION_JSON);
|
response.writeBodyString(jsonStr, ContentTypes.APPLICATION_JSON);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void handleGetTotalBalances(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
|
auto ds = getProfileDataSource(request);
|
||||||
|
auto balances = getTotalBalanceForAllAccounts(ds);
|
||||||
|
writeJsonBody(response, balances);
|
||||||
|
}
|
||||||
|
|
||||||
// Value records:
|
// Value records:
|
||||||
|
|
||||||
const PageRequest VALUE_RECORD_DEFAULT_PAGE_REQUEST = PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]);
|
const PageRequest VALUE_RECORD_DEFAULT_PAGE_REQUEST = PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import std.datetime;
|
||||||
import account.model;
|
import account.model;
|
||||||
import account.data;
|
import account.data;
|
||||||
import profile.data;
|
import profile.data;
|
||||||
|
import util.money;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the balance for an account, at a given point in time.
|
* Gets the balance for an account, at a given point in time.
|
||||||
|
|
@ -46,6 +47,39 @@ Optional!long getBalance(ProfileDataSource ds, ulong accountId, SysTime timestam
|
||||||
return Optional!long.empty;
|
return Optional!long.empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct CurrencyBalance {
|
||||||
|
Currency currency;
|
||||||
|
long balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrencyBalance[] getTotalBalanceForAllAccounts(ProfileDataSource ds, SysTime timestamp = Clock.currTime(UTC())) {
|
||||||
|
auto accountRepo = ds.getAccountRepository();
|
||||||
|
CurrencyBalance[] balances;
|
||||||
|
foreach (Account account; accountRepo.findAll()) {
|
||||||
|
Optional!long accountBalance = getBalance(ds, account.id, timestamp);
|
||||||
|
if (!accountBalance.isNull) {
|
||||||
|
long value = accountBalance.value;
|
||||||
|
if (!account.type.debitsPositive) {
|
||||||
|
value = -value;
|
||||||
|
}
|
||||||
|
// Add the balance to the relevant currency balance:
|
||||||
|
bool added = false;
|
||||||
|
foreach (ref cb; balances) {
|
||||||
|
if (cb.currency.code == account.currency.code) {
|
||||||
|
cb.balance += value;
|
||||||
|
added = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!added) {
|
||||||
|
balances ~= CurrencyBalance(account.currency, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return balances;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method that derives a balance for an account, by using the nearest
|
* Helper method that derives a balance for an account, by using the nearest
|
||||||
* value record, and all journal entries between that record and the desired
|
* value record, and all journal entries between that record and the desired
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ HttpRequestHandler mapApiHandlers(string webOrigin) {
|
||||||
import account.api;
|
import account.api;
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/accounts", &handleGetAccounts);
|
a.map(HttpMethod.GET, PROFILE_PATH ~ "/accounts", &handleGetAccounts);
|
||||||
a.map(HttpMethod.POST, PROFILE_PATH ~ "/accounts", &handleCreateAccount);
|
a.map(HttpMethod.POST, PROFILE_PATH ~ "/accounts", &handleCreateAccount);
|
||||||
|
a.map(HttpMethod.GET, PROFILE_PATH ~ "/account-balances", &handleGetTotalBalances);
|
||||||
const ACCOUNT_PATH = PROFILE_PATH ~ "/accounts/:accountId:ulong";
|
const ACCOUNT_PATH = PROFILE_PATH ~ "/accounts/:accountId:ulong";
|
||||||
a.map(HttpMethod.GET, ACCOUNT_PATH, &handleGetAccount);
|
a.map(HttpMethod.GET, ACCOUNT_PATH, &handleGetAccount);
|
||||||
a.map(HttpMethod.PUT, ACCOUNT_PATH, &handleUpdateAccount);
|
a.map(HttpMethod.PUT, ACCOUNT_PATH, &handleUpdateAccount);
|
||||||
|
|
|
||||||
|
|
@ -123,11 +123,18 @@ export interface AccountHistoryJournalEntryItem extends AccountHistoryItem {
|
||||||
transactionDescription: string
|
transactionDescription: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CurrencyBalance {
|
||||||
|
currency: Currency
|
||||||
|
balance: number
|
||||||
|
}
|
||||||
|
|
||||||
export class AccountApiClient extends ApiClient {
|
export class AccountApiClient extends ApiClient {
|
||||||
readonly path: string
|
readonly path: string
|
||||||
|
readonly profileName: string
|
||||||
|
|
||||||
constructor(route: RouteLocation) {
|
constructor(route: RouteLocation) {
|
||||||
super()
|
super()
|
||||||
|
this.profileName = getSelectedProfile(route)
|
||||||
this.path = `/profiles/${getSelectedProfile(route)}/accounts`
|
this.path = `/profiles/${getSelectedProfile(route)}/accounts`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,6 +162,10 @@ export class AccountApiClient extends ApiClient {
|
||||||
return super.getJsonPage(`${this.path}/${id}/history`, pageRequest)
|
return super.getJsonPage(`${this.path}/${id}/history`, pageRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTotalBalances(): Promise<CurrencyBalance[]> {
|
||||||
|
return super.getJson(`/profiles/${this.profileName}/account-balances`)
|
||||||
|
}
|
||||||
|
|
||||||
getValueRecords(accountId: number, pageRequest: PageRequest): Promise<Page<AccountValueRecord>> {
|
getValueRecords(accountId: number, pageRequest: PageRequest): Promise<Page<AccountValueRecord>> {
|
||||||
return super.getJsonPage(this.path + '/' + accountId + '/value-records', pageRequest)
|
return super.getJsonPage(this.path + '/' + accountId + '/value-records', pageRequest)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AccountApiClient, type Account } from '@/api/account'
|
import { AccountApiClient, type Account, type CurrencyBalance } from '@/api/account'
|
||||||
import { formatMoney } from '@/api/data'
|
import { formatMoney } from '@/api/data'
|
||||||
import { getSelectedProfile } from '@/api/profile'
|
import { getSelectedProfile } from '@/api/profile'
|
||||||
import AppButton from '@/components/AppButton.vue'
|
import AppButton from '@/components/AppButton.vue'
|
||||||
|
|
@ -11,11 +11,16 @@ const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const accounts: Ref<Account[]> = ref([])
|
const accounts: Ref<Account[]> = ref([])
|
||||||
|
const totalBalances: Ref<CurrencyBalance[]> = ref([])
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const accountApi = new AccountApiClient(route)
|
const accountApi = new AccountApiClient(route)
|
||||||
accountApi.getAccounts().then(result => accounts.value = result)
|
accountApi.getAccounts().then(result => accounts.value = result)
|
||||||
.catch(err => console.error(err))
|
.catch(err => console.error(err))
|
||||||
|
accountApi.getTotalBalances().then(result => {
|
||||||
|
totalBalances.value = result
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -51,6 +56,16 @@ onMounted(async () => {
|
||||||
<p v-if="accounts.length === 0">
|
<p v-if="accounts.length === 0">
|
||||||
You haven't added any accounts. Add one to start tracking your finances.
|
You haven't added any accounts. Add one to start tracking your finances.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li v-for="bal in totalBalances" :key="bal.currency.code">
|
||||||
|
<span>Total in {{ bal.currency.code }}</span>
|
||||||
|
=
|
||||||
|
<span>{{ formatMoney(bal.balance, bal.currency) }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:actions>
|
<template v-slot:actions>
|
||||||
<AppButton icon="plus" @click="router.push(`/profiles/${getSelectedProfile(route)}/add-account`)">Add Account
|
<AppButton icon="plus" @click="router.push(`/profiles/${getSelectedProfile(route)}/add-account`)">Add Account
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue