Made login auto-redirect to first profile. Fixes #31
Build and Deploy Web App / build-and-deploy (push) Successful in 19s
Details
Build and Deploy Web App / build-and-deploy (push) Successful in 19s
Details
This commit is contained in:
parent
c1193d96fd
commit
92a3dc4c62
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue