Cleaned up some backend logging, refactored module styles, pagination, and transaction cards.
This commit is contained in:
parent
be3d554b7f
commit
df6e4cc81f
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue