Cleaned up some backend logging, refactored module styles, pagination, and transaction cards.
Build and Deploy Web App / build-and-deploy (push) Successful in 21s Details
Build and Deploy API / build-and-deploy (push) Successful in 1m47s Details

This commit is contained in:
andrewlalis 2026-01-15 20:50:34 -05:00
parent be3d554b7f
commit df6e4cc81f
9 changed files with 98 additions and 192 deletions

View File

@ -43,4 +43,5 @@ void changeMyPassword(ref ServerHttpRequest request, ref ServerHttpResponse resp
AuthContext auth = getAuthContext(request); AuthContext auth = getAuthContext(request);
PasswordChangeRequest data = readJsonBodyAs!PasswordChangeRequest(request); PasswordChangeRequest data = readJsonBodyAs!PasswordChangeRequest(request);
changePassword(auth.user, new FileSystemUserRepository(), data.currentPassword, data.newPassword); changePassword(auth.user, new FileSystemUserRepository(), data.currentPassword, data.newPassword);
infoF!"User \"%s\" changed their password."(auth.user.username);
} }

View File

@ -19,7 +19,7 @@ void postLogin(ref ServerHttpRequest request, ref ServerHttpResponse response) {
LoginData data = readJsonBodyAs!LoginData(request); LoginData data = readJsonBodyAs!LoginData(request);
string token = generateTokenForLogin(data.username, data.password); string token = generateTokenForLogin(data.username, data.password);
response.writeBodyString(token); response.writeBodyString(token);
debugF!"Generated token for user: %s"(data.username); infoF!"User \"%s\" logged in."(data.username);
} }
struct UsernameAvailabilityResponse { struct UsernameAvailabilityResponse {
@ -71,6 +71,6 @@ void postRegister(ref ServerHttpRequest request, ref ServerHttpResponse response
} }
User user = createNewUser(userRepo, registrationData.username, registrationData.password); User user = createNewUser(userRepo, registrationData.username, registrationData.password);
infoF!"Created user: %s"(registrationData.username); infoF!"User \"%s\" registered."(registrationData.username);
response.writeBodyString(user.username); response.writeBodyString(user.username);
} }

View File

@ -39,13 +39,40 @@ a:hover {
.app-module-container { .app-module-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 20px; align-items: stretch;
gap: 1rem;
padding: 1rem; padding: 1rem;
} }
@media (max-width: 600px) { .app-module {
.app-module-container { width: calc(33.33% - 1.666rem);
padding: 0.5rem; display: inline-block;
min-height: 200px;
background-color: var(--bg-lighter);
border-radius: 0.5rem;
padding: 0.25rem 0.5rem 0.5rem 0.5rem;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.app-module-full-width {
width: 100%;
}
.app-module-header {
margin: 0;
}
@media (max-width: 1600px) {
.app-module {
width: calc(50% - 1.5rem);
}
}
@media (max-width: 800px) {
.app-module {
width: 100%;
} }
} }

View File

@ -14,20 +14,3 @@ defineProps<{ title: string }>()
</ButtonBar> </ButtonBar>
</div> </div>
</template> </template>
<style lang="css">
.app-module {
min-width: 300px;
min-height: 200px;
flex-grow: 1;
background-color: var(--bg-lighter);
border-radius: 0.5rem;
padding: 0.5rem;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.app-module-header {
margin: 0;
}
</style>

View File

@ -30,88 +30,74 @@ function goToTransaction() {
} }
</script> </script>
<template> <template>
<div <div class="transaction-card" @click="goToTransaction()">
class="transaction-card" <div>
@click="goToTransaction()" <!-- Top row contains timestamp and amount. -->
> <div style="display: flex; justify-content: space-between;">
<!-- Top row contains timestamp and amount. --> <div>
<div class="transaction-card-top-row"> <div class="font-mono font-size-xsmall text-normal">Transaction #{{ tx.id }}</div>
<div> <div class="text-muted font-mono font-size-xsmall">
<div class="font-mono font-size-xsmall text-normal">Transaction #{{ tx.id }}</div> {{ new Date(tx.timestamp).toLocaleString() }}
<div class="text-muted font-mono font-size-xsmall"> </div>
{{ new Date(tx.timestamp).toLocaleString() }}
</div> </div>
</div> <div>
<div> <div class="font-mono align-right font-size-small" :class="{
<div
class="font-mono align-right font-size-small"
:class="{
'text-positive': moneyStyle === 'positive', 'text-positive': moneyStyle === 'positive',
'text-negative': moneyStyle === 'negative', 'text-negative': moneyStyle === 'negative',
}" }">
> {{ formatMoney(tx.amount, tx.currency) }}
{{ formatMoney(tx.amount, tx.currency) }} </div>
</div> <div v-if="tx.creditedAccount !== null" class="font-size-small text-muted">
<div Credited to <span class="text-normal font-bold">{{ tx.creditedAccount.name }}</span>
v-if="tx.creditedAccount !== null" </div>
class="font-size-small text-muted" <div v-if="tx.debitedAccount !== null" class="font-size-small text-muted">
> Debited to <span class="text-normal font-bold">{{ tx.debitedAccount.name }}</span>
Credited to <span class="text-normal font-bold">{{ tx.creditedAccount.name }}</span> </div>
</div>
<div
v-if="tx.debitedAccount !== null"
class="font-size-small text-muted"
>
Debited to <span class="text-normal font-bold">{{ tx.debitedAccount.name }}</span>
</div> </div>
</div> </div>
</div>
<!-- Middle row contains the description. --> <!-- Middle row contains the description. -->
<div> <div>
<p class="transaction-card-description">{{ tx.description }}</p> <p class="transaction-card-description">{{ tx.description }}</p>
</div>
</div> </div>
<!-- Bottom row contains other links. --> <!-- Bottom row contains other links. -->
<div style="display: flex; justify-content: space-between"> <div style="display: flex; justify-content: space-between">
<div> <div>
<CategoryLabel <CategoryLabel :category="tx.category" v-if="tx.category" style="margin-left: 0" />
:category="tx.category"
v-if="tx.category"
style="margin-left: 0"
/>
<AppBadge v-if="tx.vendor">{{ tx.vendor.name }}</AppBadge> <AppBadge v-if="tx.vendor">{{ tx.vendor.name }}</AppBadge>
</div> </div>
<div> <div>
<TagLabel <!-- Only show the first 3 tags, and add a "+N" badge for any more. -->
v-for="tag in tx.tags" <TagLabel v-for="tag in tx.tags.slice(0, 3)" :key="tag" :tag="tag" />
:key="tag" <AppBadge v-if="tx.tags.length > 3" class="text-muted">+{{ tx.tags.length - 3 }}</AppBadge>
:tag="tag"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style lang="css"> <style lang="css" scoped>
.transaction-card { .transaction-card {
background-color: var(--bg); background-color: var(--bg);
padding: 0.5rem; padding: 0.5rem;
border-radius: 0.5rem; border-radius: 0.5rem;
margin: 0.5rem 0; margin: 0.5rem 0;
cursor: pointer; cursor: pointer;
height: 120px;
display: flex;
flex-direction: column;
justify-content: space-between;
} }
.transaction-card:hover { .transaction-card:hover {
background-color: var(--bg-darker); background-color: var(--bg-darker);
} }
.transaction-card-top-row {
display: flex;
justify-content: space-between;
}
.transaction-card-description { .transaction-card-description {
margin: 0.25rem 0; margin: 0.25rem 0;
font-size: 0.9rem; font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
</style> </style>

View File

@ -28,44 +28,18 @@ function incrementPage(step: number) {
<template> <template>
<div> <div>
<div v-if="page && page.totalElements > 0"> <div v-if="page && page.totalElements > 0">
<AppButton <AppButton size="sm" icon="backward-step" :disabled="!page || page.isFirst" @click="updatePage(1)" />
size="sm"
:disabled="!page || page.isFirst"
@click="updatePage(1)"
>
First Page
</AppButton>
<AppButton <AppButton size="sm" icon="chevron-left" :disabled="!page || page.isFirst" @click="incrementPage(-1)" />
size="sm"
:disabled="!page || page.isFirst"
@click="incrementPage(-1)"
>
Previous Page
</AppButton>
<span <span style="min-width: 100px; text-align: center; display: inline-block" class="font-size-xsmall">
style="min-width: 100px; text-align: center; display: inline-block"
class="font-size-xsmall"
>
Page <span class="font-bold">{{ page?.pageRequest.page }}</span> of {{ page?.totalPages }} Page <span class="font-bold">{{ page?.pageRequest.page }}</span> of {{ page?.totalPages }}
</span> </span>
<AppButton <AppButton size="sm" icon="chevron-right" :disabled="!page || page.isLast" @click="incrementPage(1)" />
size="sm"
:disabled="!page || page.isLast"
@click="incrementPage(1)"
>
Next Page
</AppButton>
<AppButton <AppButton size="sm" icon="forward-step" :disabled="!page || page.isLast"
size="sm" @click="updatePage(page?.totalPages ?? 0)" />
:disabled="!page || page.isLast"
@click="updatePage(page?.totalPages ?? 0)"
>
Last Page
</AppButton>
</div> </div>
</div> </div>
</template> </template>

View File

@ -104,68 +104,37 @@ onMounted(async () => {
}) })
</script> </script>
<template> <template>
<HomeModule <HomeModule title="Analytics">
title="Analytics"
style="max-width: 800px; min-height: 200px"
>
<FormGroup> <FormGroup>
<FormControl label="Chart"> <FormControl label="Chart">
<select v-model="selectedChart"> <select v-model="selectedChart">
<option <option v-for="ct in AnalyticsChartTypes" :key="ct.id" :value="ct">
v-for="ct in AnalyticsChartTypes"
:key="ct.id"
:value="ct"
>
{{ ct.name }} {{ ct.name }}
</option> </option>
</select> </select>
</FormControl> </FormControl>
</FormGroup> </FormGroup>
<BalanceTimeSeriesChart <BalanceTimeSeriesChart v-if="currency && balanceTimeSeriesData && selectedChart.id === 'account-balances'"
v-if="currency && balanceTimeSeriesData && selectedChart.id === 'account-balances'" title="Account Balances" :currency="currency" :time-frame="timeFrame" :data="accountBalancesData" />
title="Account Balances"
:currency="currency"
:time-frame="timeFrame"
:data="accountBalancesData"
/>
<BalanceTimeSeriesChart <BalanceTimeSeriesChart v-if="currency && balanceTimeSeriesData && selectedChart.id === 'total-balances'"
v-if="currency && balanceTimeSeriesData && selectedChart.id === 'total-balances'" title="Total Balances" :currency="currency" :time-frame="timeFrame" :data="totalBalancesData" />
title="Total Balances"
:currency="currency"
:time-frame="timeFrame"
:data="totalBalancesData"
/>
<CategorySpendPieChart <CategorySpendPieChart v-if="currency && categorySpendTimeSeriesData && selectedChart.id === 'category-spend'"
v-if="currency && categorySpendTimeSeriesData && selectedChart.id === 'category-spend'" :currency="currency" :time-frame="timeFrame" :data="categorySpendTimeSeriesData" />
:currency="currency"
:time-frame="timeFrame"
:data="categorySpendTimeSeriesData"
/>
<FormGroup> <FormGroup>
<FormControl label="Currency"> <FormControl label="Currency">
<select <select v-model="currency" :disabled="availableCurrencies.length < 2">
v-model="currency" <option v-for="currency in availableCurrencies" :key="currency.code" :value="currency">
:disabled="availableCurrencies.length < 2"
>
<option
v-for="currency in availableCurrencies"
:key="currency.code"
:value="currency"
>
{{ currency.code }} {{ currency.code }}
</option> </option>
</select> </select>
</FormControl> </FormControl>
<FormControl label="Time Frame"> <FormControl label="Time Frame">
<select v-model="timeFrame"> <select v-model="timeFrame">
<option <option :value="{}" selected>
:value="{}"
selected
>
All Time All Time
</option> </option>
<option :value="{ start: sub(new Date(), { days: 30 }) }">Last 30 days</option> <option :value="{ start: sub(new Date(), { days: 30 }) }">Last 30 days</option>

View File

@ -43,19 +43,12 @@ async function downloadData() {
} }
</script> </script>
<template> <template>
<HomeModule <HomeModule title="Profile" v-if="profile">
title="Profile"
v-if="profile"
>
<template v-slot:default> <template v-slot:default>
<p>Your currently selected profile is: {{ profile.name }}</p> <p>Your currently selected profile is: <strong>{{ profile.name }}</strong></p>
<p> <p>
<RouterLink :to="`/profiles/${profile.name}/vendors`">View all vendors here.</RouterLink> <AppButton size="sm" @click="router.push(`/profiles/${profile.name}/vendors`)">Vendors</AppButton>
</p> <AppButton size="sm" @click="router.push(`/profiles/${profile.name}/categories`)">Categories</AppButton>
<p>
<RouterLink :to="`/profiles/${profile.name}/categories`"
>View all categories here.</RouterLink
>
</p> </p>
<ConfirmModal ref="confirmDeleteModal"> <ConfirmModal ref="confirmDeleteModal">
@ -67,24 +60,9 @@ async function downloadData() {
</ConfirmModal> </ConfirmModal>
</template> </template>
<template v-slot:actions> <template v-slot:actions>
<AppButton <AppButton icon="folder-open" @click="router.push('/profiles')">Choose another profile</AppButton>
icon="folder-open" <AppButton icon="download" @click="downloadData()" size="sm">Download Data</AppButton>
@click="router.push('/profiles')" <AppButton button-style="secondary" icon="trash" @click="deleteProfile()" size="sm">Delete</AppButton>
>Choose another profile</AppButton
>
<AppButton
icon="download"
@click="downloadData()"
size="sm"
>Download Data</AppButton
>
<AppButton
button-style="secondary"
icon="trash"
@click="deleteProfile()"
size="sm"
>Delete</AppButton
>
</template> </template>
</HomeModule> </HomeModule>
</template> </template>

View File

@ -40,25 +40,13 @@ function goToSearch() {
<template> <template>
<HomeModule title="Transactions"> <HomeModule title="Transactions">
<template v-slot:default> <template v-slot:default>
<PaginationControls <PaginationControls :page="transactions" @update="(pr) => fetchPage(pr)" class="align-right" />
:page="transactions" <TransactionCard v-for="tx in transactions.items" :key="tx.id" :tx="tx" />
@update="(pr) => fetchPage(pr)"
class="align-right"
/>
<TransactionCard
v-for="tx in transactions.items"
:key="tx.id"
:tx="tx"
/>
<p v-if="transactions.totalElements === 0">You haven't added any transactions.</p> <p v-if="transactions.totalElements === 0">You haven't added any transactions.</p>
</template> </template>
<template v-slot:actions> <template v-slot:actions>
<AppButton <AppButton icon="plus" @click="router.push(`/profiles/${getSelectedProfile(route)}/add-transaction`)">
icon="plus" Add Transaction</AppButton>
@click="router.push(`/profiles/${getSelectedProfile(route)}/add-transaction`)"
>
Add Transaction</AppButton
>
<AppButton @click="goToSearch()">Search</AppButton> <AppButton @click="goToSearch()">Search</AppButton>
</template> </template>
</HomeModule> </HomeModule>