Added account diffs and cleaned up transaction page.
Build and Deploy Web App / build-and-deploy (push) Successful in 17s Details
Build and Deploy API / build-and-deploy (push) Successful in 1m30s Details

This commit is contained in:
andrewlalis 2025-10-19 12:14:09 -04:00
parent 30715947a3
commit 534071cbe0
5 changed files with 93 additions and 4 deletions

View File

@ -79,6 +79,23 @@ void handleDeleteAccount(ref ServerHttpRequest request, ref ServerHttpResponse r
ds.getAccountRepository().deleteById(accountId); ds.getAccountRepository().deleteById(accountId);
} }
void handleGetAccountBalance(ref ServerHttpRequest request, ref ServerHttpResponse response) {
ulong accountId = request.getPathParamAs!ulong("accountId");
auto ds = getProfileDataSource(request);
SysTime timestamp = Clock.currTime(UTC());
string providedTimestamp = request.getParamAs!string("timestamp");
if (providedTimestamp != null && providedTimestamp.length > 0) {
timestamp = SysTime.fromISOExtString(providedTimestamp);
}
Optional!long balance = getBalance(ds, accountId, timestamp);
if (balance.isNull) {
response.writeBodyString("{\"balance\": null}", ContentTypes.APPLICATION_JSON);
} else {
import std.conv : to;
response.writeBodyString("{\"balance\": " ~ balance.value.to!string ~ "}", ContentTypes.APPLICATION_JSON);
}
}
void handleGetAccountHistory(ref ServerHttpRequest request, ref ServerHttpResponse response) { void handleGetAccountHistory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
ulong accountId = request.getPathParamOrThrow!ulong("accountId"); ulong accountId = request.getPathParamOrThrow!ulong("accountId");
PageRequest pagination = PageRequest.parse(request, PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)])); PageRequest pagination = PageRequest.parse(request, PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]));

View File

@ -59,6 +59,7 @@ HttpRequestHandler mapApiHandlers(string webOrigin) {
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);
a.map(HttpMethod.DELETE, ACCOUNT_PATH, &handleDeleteAccount); a.map(HttpMethod.DELETE, ACCOUNT_PATH, &handleDeleteAccount);
a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/balance", &handleGetAccountBalance);
a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/history", &handleGetAccountHistory); a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/history", &handleGetAccountHistory);
a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records", &handleGetValueRecords); a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records", &handleGetValueRecords);
a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong", &handleGetValueRecord); a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong", &handleGetValueRecord);

View File

@ -173,6 +173,14 @@ export class AccountApiClient extends ApiClient {
return super.delete(this.path + '/' + id) return super.delete(this.path + '/' + id)
} }
async getBalance(id: number, timestamp: Date): Promise<number | undefined> {
const result: { balance: number | null } = await super.getJson(
`${this.path}/${id}/balance?timestamp=${timestamp.toISOString()}`,
)
if (result.balance === null) return undefined
return result.balance
}
getHistory(id: number, pageRequest: PageRequest): Promise<Page<AccountHistoryItem>> { getHistory(id: number, pageRequest: PageRequest): Promise<Page<AccountHistoryItem>> {
return super.getJsonPage(`${this.path}/${id}/history`, pageRequest) return super.getJsonPage(`${this.path}/${id}/history`, pageRequest)
} }

View File

@ -14,3 +14,8 @@
margin-left: 0.5rem; margin-left: 0.5rem;
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.my-1 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}

View File

@ -15,17 +15,26 @@ import AttachmentRow from '@/components/common/AttachmentRow.vue'
import LineItemCard from '@/components/LineItemCard.vue' import LineItemCard from '@/components/LineItemCard.vue'
import AppBadge from '@/components/common/AppBadge.vue' import AppBadge from '@/components/common/AppBadge.vue'
import ButtonBar from '@/components/common/ButtonBar.vue' import ButtonBar from '@/components/common/ButtonBar.vue'
import { AccountApiClient } from '@/api/account'
interface BalanceDiff {
before: number
after: number
}
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const transactionApi = new TransactionApiClient(getSelectedProfile(route)) const transactionApi = new TransactionApiClient(getSelectedProfile(route))
const transaction: Ref<TransactionDetail | undefined> = ref() const transaction: Ref<TransactionDetail | undefined> = ref()
const creditedAccountBalanceDiff = ref<BalanceDiff | undefined>()
const debitedAccountBalanceDiff = ref<BalanceDiff | undefined>()
onMounted(async () => { onMounted(async () => {
const transactionId = parseInt(route.params.id as string) const transactionId = parseInt(route.params.id as string)
try { try {
transaction.value = await transactionApi.getTransaction(transactionId) transaction.value = await transactionApi.getTransaction(transactionId)
fetchBalanceDiffs()
} catch (err) { } catch (err) {
console.error(err) console.error(err)
await router.replace('/') await router.replace('/')
@ -35,6 +44,34 @@ onMounted(async () => {
} }
}) })
async function fetchBalanceDiffs() {
const txn = transaction.value
if (!txn) return
const txnTime = new Date(txn.timestamp).getTime()
const beforeTs = new Date(txnTime - 1000)
const afterTs = new Date(txnTime + 1000)
const accountApi = new AccountApiClient(route)
const p1: Promise<number | undefined> = txn.creditedAccount
? accountApi.getBalance(txn.creditedAccount.id, beforeTs)
: Promise.resolve(undefined)
const p2: Promise<number | undefined> = txn.creditedAccount
? accountApi.getBalance(txn.creditedAccount.id, afterTs)
: Promise.resolve(undefined)
const p3: Promise<number | undefined> = txn.debitedAccount
? accountApi.getBalance(txn.debitedAccount.id, beforeTs)
: Promise.resolve(undefined)
const p4: Promise<number | undefined> = txn.debitedAccount
? accountApi.getBalance(txn.debitedAccount.id, afterTs)
: Promise.resolve(undefined)
const results = await Promise.all([p1, p2, p3, p4])
if (results[0] !== undefined && results[1] !== undefined) {
creditedAccountBalanceDiff.value = { before: results[0], after: results[1] }
}
if (results[2] !== undefined && results[3] !== undefined) {
debitedAccountBalanceDiff.value = { before: results[2], after: results[3] }
}
}
async function deleteTransaction() { async function deleteTransaction() {
if (!transaction.value) return if (!transaction.value) return
const conf = await showConfirm( const conf = await showConfirm(
@ -68,18 +105,39 @@ async function deleteTransaction() {
<p>{{ transaction.description }}</p> <p>{{ transaction.description }}</p>
<p v-if="transaction.creditedAccount"> <div v-if="transaction.creditedAccount" class="my-1">
<strong class="text-negative">Credited</strong> from <strong class="text-negative">Credited</strong> from
<RouterLink :to="`/profiles/${getSelectedProfile(route)}/accounts/${transaction.creditedAccount.id}`"> <RouterLink :to="`/profiles/${getSelectedProfile(route)}/accounts/${transaction.creditedAccount.id}`">
{{ transaction.creditedAccount.name }} (#{{ transaction.creditedAccount.numberSuffix }}) {{ transaction.creditedAccount.name }} (#{{ transaction.creditedAccount.numberSuffix }})
</RouterLink> </RouterLink>
</p> <div v-if="creditedAccountBalanceDiff" class="font-size-xsmall">
<p v-if="transaction.debitedAccount"> Balance Before:
<span class="font-mono">
{{ formatMoney(creditedAccountBalanceDiff.before, transaction.currency) }}
</span>
/ After:
<span class="font-mono">
{{ formatMoney(creditedAccountBalanceDiff.after, transaction.currency) }}
</span>
</div>
</div>
<div v-if="transaction.debitedAccount" class="my-1">
<strong class="text-positive">Debited</strong> to <strong class="text-positive">Debited</strong> to
<RouterLink :to="`/profiles/${getSelectedProfile(route)}/accounts/${transaction.debitedAccount.id}`"> <RouterLink :to="`/profiles/${getSelectedProfile(route)}/accounts/${transaction.debitedAccount.id}`">
{{ transaction.debitedAccount.name }} (#{{ transaction.debitedAccount.numberSuffix }}) {{ transaction.debitedAccount.name }} (#{{ transaction.debitedAccount.numberSuffix }})
</RouterLink> </RouterLink>
</p> <div v-if="debitedAccountBalanceDiff" class="font-size-xsmall">
Balance Before:
<span class="font-mono">
{{ formatMoney(debitedAccountBalanceDiff.before, transaction.currency) }}
</span>
/ After:
<span class="font-mono">
{{ formatMoney(debitedAccountBalanceDiff.after, transaction.currency) }}
</span>
</div>
</div>
<!-- All remaining properties are put in this table. --> <!-- All remaining properties are put in this table. -->
<PropertiesTable> <PropertiesTable>