Made login auto-redirect to first profile. Fixes #31
Build and Deploy Web App / build-and-deploy (push) Successful in 19s Details

This commit is contained in:
andrewlalis 2025-09-27 08:16:59 -04:00
parent c1193d96fd
commit 92a3dc4c62
8 changed files with 215 additions and 49 deletions

View File

@ -48,3 +48,21 @@ export function getSelectedProfile(route: RouteLocation): string {
} }
return name return name
} }
const LAST_PROFILE_USED_KEY = 'last-profile-used'
export function saveLastProfileUsed(route: RouteLocation) {
try {
const profileName = getSelectedProfile(route)
const existing = localStorage.getItem(LAST_PROFILE_USED_KEY)
if (existing === null || existing !== profileName) {
localStorage.setItem(LAST_PROFILE_USED_KEY, profileName)
}
} catch {
localStorage.removeItem(LAST_PROFILE_USED_KEY)
}
}
export function getLastProfileUsed(): string | null {
return localStorage.getItem(LAST_PROFILE_USED_KEY)
}

View File

@ -34,18 +34,28 @@ function goToAccount() {
} }
</script> </script>
<template> <template>
<div class="account-card" @click="goToAccount()"> <div
class="account-card"
@click="goToAccount()"
>
<!-- A top row for the name on the left, and balance on the right. --> <!-- A top row for the name on the left, and balance on the right. -->
<div class="account-card-top-row"> <div class="account-card-top-row">
<div> <div>
<span class="font-bold" style="margin-right: 0.5rem">{{ account.name }}</span> <span
class="font-bold"
style="margin-right: 0.5rem"
>{{ account.name }}</span
>
<span class="font-mono font-size-xsmall">#{{ account.numberSuffix }}</span> <span class="font-mono font-size-xsmall">#{{ account.numberSuffix }}</span>
</div> </div>
<div class="font-mono font-size-small" :class="{ <div
class="font-mono font-size-small"
:class="{
'text-positive': isBalancePositive, 'text-positive': isBalancePositive,
'text-negative': isBalanceNegative, 'text-negative': isBalanceNegative,
}"> }"
>
<span v-if="account.currentBalance !== null"> <span v-if="account.currentBalance !== null">
{{ formatMoney(account.currentBalance, account.currency) }} {{ formatMoney(account.currentBalance, account.currency) }}
</span> </span>

View File

@ -10,7 +10,11 @@ defineProps<{
{{ label }} {{ label }}
<slot></slot> <slot></slot>
</label> </label>
<p v-if="hint" style="margin-top: 0.1rem; margin-bottom: 0;" class="font-size-small text-muted"> <p
v-if="hint"
style="margin-top: 0.1rem; margin-bottom: 0"
class="font-size-small text-muted"
>
{{ hint }} {{ hint }}
</p> </p>
</div> </div>

View File

@ -60,13 +60,28 @@ async function addValueRecord() {
} }
</script> </script>
<template> <template>
<AppPage :title="account?.name ?? ''" v-if="account"> <AppPage
:title="account?.name ?? ''"
v-if="account"
>
<div> <div>
<AppBadge size="lg" v-if="account.currentBalance !== null" class="font-mono"> <AppBadge
size="lg"
v-if="account.currentBalance !== null"
class="font-mono"
>
{{ account.currency.code }} {{ formatMoney(account.currentBalance, account.currency) }} {{ account.currency.code }} {{ formatMoney(account.currentBalance, account.currency) }}
</AppBadge> </AppBadge>
<AppBadge size="lg" class="font-mono">#{{ account.numberSuffix }}</AppBadge> <AppBadge
<AppBadge size="lg" v-if="accountType" class="font-mono"> size="lg"
class="font-mono"
>#{{ account.numberSuffix }}</AppBadge
>
<AppBadge
size="lg"
v-if="accountType"
class="font-mono"
>
{{ accountType.emoji }} {{ accountType.name }} {{ accountType.emoji }} {{ accountType.name }}
</AppBadge> </AppBadge>
</div> </div>
@ -81,15 +96,36 @@ async function addValueRecord() {
</PropertiesTable> </PropertiesTable>
<ButtonBar> <ButtonBar>
<AppButton @click="addValueRecord()" :disabled="account.archived">Record Value</AppButton> <AppButton
<AppButton icon="wrench" :disabled="account.archived" @click="addValueRecord()"
@click="router.push(`/profiles/${getSelectedProfile(route)}/accounts/${account?.id}/edit`)"> :disabled="account.archived"
Edit</AppButton> >Record Value</AppButton
<AppButton icon="trash" @click="deleteAccount()" :disabled="account.archived">Delete</AppButton> >
<AppButton
icon="wrench"
:disabled="account.archived"
@click="router.push(`/profiles/${getSelectedProfile(route)}/accounts/${account?.id}/edit`)"
>
Edit</AppButton
>
<AppButton
icon="trash"
@click="deleteAccount()"
:disabled="account.archived"
>Delete</AppButton
>
</ButtonBar> </ButtonBar>
<AccountHistory :account="account" v-if="account" ref="history" /> <AccountHistory
:account="account"
v-if="account"
ref="history"
/>
<AddValueRecordModal v-if="account" :account="account" ref="addValueRecordModal" /> <AddValueRecordModal
v-if="account"
:account="account"
ref="addValueRecordModal"
/>
</AppPage> </AppPage>
</template> </template>

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { AuthApiClient } from '@/api/auth' import { AuthApiClient } from '@/api/auth'
import { ApiError } from '@/api/base' import { ApiError } from '@/api/base'
import { getLastProfileUsed, ProfileApiClient } from '@/api/profile'
import AppButton from '@/components/common/AppButton.vue' import AppButton from '@/components/common/AppButton.vue'
import AppForm from '@/components/common/form/AppForm.vue' import AppForm from '@/components/common/form/AppForm.vue'
import FormControl from '@/components/common/form/FormControl.vue' import FormControl from '@/components/common/form/FormControl.vue'
@ -27,11 +28,20 @@ async function doLogin() {
try { try {
const token = await apiClient.login(username.value, password.value) const token = await apiClient.login(username.value, password.value)
authStore.onUserLoggedIn(username.value, token) authStore.onUserLoggedIn(username.value, token)
const profiles = await new ProfileApiClient().getProfiles()
const lastProfileUsed = getLastProfileUsed()
hideLoader() hideLoader()
if ('next' in route.query && typeof route.query.next === 'string') { if ('next' in route.query && typeof route.query.next === 'string') {
await router.replace(route.query.next) await router.replace(route.query.next)
} else if (profiles.length === 1) {
// If there's only one available profile, go right to it.
await router.replace(`/profiles/${profiles[0].name}`)
} else if (lastProfileUsed !== null && profiles.map((p) => p.name).includes(lastProfileUsed)) {
// If we know what profile the user was last using, go to that.
await router.replace(`/profiles/${lastProfileUsed}`)
} else { } else {
await router.replace('/') // Otherwise, go to the profiles page so the user can choose.
await router.replace('/profiles')
} }
} catch (err) { } catch (err) {
hideLoader() hideLoader()

View File

@ -80,11 +80,22 @@ function onCancel() {
<AppPage :title="editing ? 'Edit Account' : 'Add Account'"> <AppPage :title="editing ? 'Edit Account' : 'Add Account'">
<AppForm @submit="doSubmit()"> <AppForm @submit="doSubmit()">
<FormGroup> <FormGroup>
<FormControl label="Account Name" style="max-width: 200px"> <FormControl
<input v-model="accountName" :disabled="loading" /> label="Account Name"
style="max-width: 200px"
>
<input
v-model="accountName"
:disabled="loading"
/>
</FormControl> </FormControl>
<FormControl label="Account Type"> <FormControl label="Account Type">
<select id="account-type-select" v-model="accountType" :disabled="loading" required> <select
id="account-type-select"
v-model="accountType"
:disabled="loading"
required
>
<option :value="AccountTypes.CHECKING">{{ AccountTypes.CHECKING.name }}</option> <option :value="AccountTypes.CHECKING">{{ AccountTypes.CHECKING.name }}</option>
<option :value="AccountTypes.SAVINGS">{{ AccountTypes.SAVINGS.name }}</option> <option :value="AccountTypes.SAVINGS">{{ AccountTypes.SAVINGS.name }}</option>
<option :value="AccountTypes.CREDIT_CARD">{{ AccountTypes.CREDIT_CARD.name }}</option> <option :value="AccountTypes.CREDIT_CARD">{{ AccountTypes.CREDIT_CARD.name }}</option>
@ -92,24 +103,46 @@ function onCancel() {
</select> </select>
</FormControl> </FormControl>
<FormControl label="Currency"> <FormControl label="Currency">
<select id="currency-select" v-model="currency" :disabled="loading" required> <select
id="currency-select"
v-model="currency"
:disabled="loading"
required
>
<option value="USD">USD</option> <option value="USD">USD</option>
<option value="EUR">EUR</option> <option value="EUR">EUR</option>
<option value="GBP">GBP</option> <option value="GBP">GBP</option>
</select> </select>
</FormControl> </FormControl>
<FormControl label="Account Number Suffix" style="max-width: 200px"> <FormControl
<input id="account-number-suffix-input" v-model="accountNumberSuffix" minlength="4" maxlength="4" label="Account Number Suffix"
:disabled="loading" required /> style="max-width: 200px"
>
<input
id="account-number-suffix-input"
v-model="accountNumberSuffix"
minlength="4"
maxlength="4"
:disabled="loading"
required
/>
</FormControl> </FormControl>
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormControl label="Description"> <FormControl label="Description">
<textarea id="description-textarea" v-model="description" :disabled="loading"></textarea> <textarea
id="description-textarea"
v-model="description"
:disabled="loading"
></textarea>
</FormControl> </FormControl>
</FormGroup> </FormGroup>
<FormActions @cancel="onCancel" :disabled="loading" :submit-text="editing ? 'Save' : 'Add'" /> <FormActions
@cancel="onCancel"
:disabled="loading"
:submit-text="editing ? 'Save' : 'Add'"
/>
</AppForm> </AppForm>
</AppPage> </AppPage>
</template> </template>

View File

@ -281,20 +281,46 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
<FormGroup> <FormGroup>
<!-- Basic properties --> <!-- Basic properties -->
<FormControl label="Timestamp"> <FormControl label="Timestamp">
<input type="datetime-local" v-model="timestamp" step="1" :disabled="loading" style="min-width: 250px" /> <input
type="datetime-local"
v-model="timestamp"
step="1"
:disabled="loading"
style="min-width: 250px"
/>
</FormControl> </FormControl>
<FormControl label="Amount"> <FormControl label="Amount">
<input type="number" v-model="amount" step="0.01" min="0.01" :disabled="loading" style="max-width: 100px" /> <input
type="number"
v-model="amount"
step="0.01"
min="0.01"
:disabled="loading"
style="max-width: 100px"
/>
</FormControl> </FormControl>
<FormControl label="Currency"> <FormControl label="Currency">
<select v-model="currency" :disabled="loading || availableCurrencies.length === 1"> <select
<option v-for="currency in availableCurrencies" :key="currency.code" :value="currency"> v-model="currency"
:disabled="loading || availableCurrencies.length === 1"
>
<option
v-for="currency in availableCurrencies"
:key="currency.code"
:value="currency"
>
{{ currency.code }} {{ currency.code }}
</option> </option>
</select> </select>
</FormControl> </FormControl>
<FormControl label="Description" style="min-width: 200px"> <FormControl
<textarea v-model="description" :disabled="loading"></textarea> label="Description"
style="min-width: 200px"
>
<textarea
v-model="description"
:disabled="loading"
></textarea>
</FormControl> </FormControl>
</FormGroup> </FormGroup>
@ -311,16 +337,30 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
<FormGroup> <FormGroup>
<!-- Accounts --> <!-- Accounts -->
<FormControl label="Credited Account"> <FormControl label="Credited Account">
<select v-model="creditedAccountId" :disabled="loading"> <select
<option v-for="account in availableAccounts" :key="account.id" :value="account.id"> v-model="creditedAccountId"
:disabled="loading"
>
<option
v-for="account in availableAccounts"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.numberSuffix }}) {{ account.name }} ({{ account.numberSuffix }})
</option> </option>
<option :value="null">None</option> <option :value="null">None</option>
</select> </select>
</FormControl> </FormControl>
<FormControl label="Debited Account"> <FormControl label="Debited Account">
<select v-model="debitedAccountId" :disabled="loading"> <select
<option v-for="account in availableAccounts" :key="account.id" :value="account.id"> v-model="debitedAccountId"
:disabled="loading"
>
<option
v-for="account in availableAccounts"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.numberSuffix }}) {{ account.name }} ({{ account.numberSuffix }})
</option> </option>
<option :value="null">None</option> <option :value="null">None</option>
@ -328,8 +368,12 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
</FormControl> </FormControl>
</FormGroup> </FormGroup>
<LineItemsEditor v-if="currency" v-model="lineItems" :currency="currency" <LineItemsEditor
:transaction-amount="floatMoneyToInteger(amount, currency)" /> v-if="currency"
v-model="lineItems"
:currency="currency"
:transaction-amount="floatMoneyToInteger(amount, currency)"
/>
<FormGroup> <FormGroup>
<!-- Tags --> <!-- Tags -->
@ -340,12 +384,18 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
<FormGroup> <FormGroup>
<h5>Attachments</h5> <h5>Attachments</h5>
<FileSelector :initial-files="existingTransaction?.attachments ?? []" <FileSelector
v-model:uploaded-files="attachmentsToUpload" v-model:removed-files="removedAttachmentIds" /> :initial-files="existingTransaction?.attachments ?? []"
v-model:uploaded-files="attachmentsToUpload"
v-model:removed-files="removedAttachmentIds"
/>
</FormGroup> </FormGroup>
<FormActions @cancel="doCancel()" :disabled="loading || !formValid || !unsavedEdits" <FormActions
:submit-text="editing ? 'Save' : 'Add'" /> @cancel="doCancel()"
:disabled="loading || !formValid || !unsavedEdits"
:submit-text="editing ? 'Save' : 'Add'"
/>
</AppForm> </AppForm>
</AppPage> </AppPage>
</template> </template>

View File

@ -1,4 +1,4 @@
import { getSelectedProfile } from '@/api/profile' import { getSelectedProfile, saveLastProfileUsed } from '@/api/profile'
import { useAuthStore } from '@/stores/auth-store' import { useAuthStore } from '@/stores/auth-store'
import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vue-router' import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vue-router'
@ -104,6 +104,11 @@ router.beforeEach((to, _, next) => {
next() next()
}) })
// After navigation, save the last used profile to keep track of what the user was last using.
router.afterEach((to) => {
saveLastProfileUsed(to)
})
/** /**
* Guard to ensure a route can only be accessed by authenticated users. * Guard to ensure a route can only be accessed by authenticated users.
* @param to The route to guard. * @param to The route to guard.