Remove profileStore and access selected profile only with route.

This commit is contained in:
andrewlalis 2025-08-31 16:00:39 -04:00
parent b5b92a9af7
commit a674421337
20 changed files with 96 additions and 143 deletions

View File

@ -1,7 +1,7 @@
import { ApiClient } from './base' import { ApiClient } from './base'
import type { Currency } from './data' import type { Currency } from './data'
import type { Page, PageRequest } from './pagination' import type { Page, PageRequest } from './pagination'
import type { Profile } from './profile' import { getSelectedProfile } from './profile'
export interface AccountType { export interface AccountType {
id: string id: string
@ -125,9 +125,9 @@ export interface AccountHistoryJournalEntryItem extends AccountHistoryItem {
export class AccountApiClient extends ApiClient { export class AccountApiClient extends ApiClient {
readonly path: string readonly path: string
constructor(profile: Profile) { constructor() {
super() super()
this.path = `/profiles/${profile.name}/accounts` this.path = `/profiles/${getSelectedProfile()}/accounts`
} }
getAccounts(): Promise<Account[]> { getAccounts(): Promise<Account[]> {

View File

@ -1,3 +1,4 @@
import { useRoute, type RouteLocation } from 'vue-router'
import { ApiClient } from './base' import { ApiClient } from './base'
export interface Profile { export interface Profile {
@ -30,3 +31,21 @@ export class ProfileApiClient extends ApiClient {
return super.getJson(`/profiles/${profileName}/properties`) return super.getJson(`/profiles/${profileName}/properties`)
} }
} }
/**
* Gets the currently selected profile. Throws an error in any case where
* the route doesn't contain profile name information.
* @param route The route to get the profile from. Defaults to getting it from
* Vue's `useRoute()` which is available in component contexts.
* @returns The currently selected profile name, via the current route.
*/
export function getSelectedProfile(route: RouteLocation = useRoute()): string {
if (!('profileName' in route.params)) {
throw new Error('No "profileName" route property available.')
}
const name = route.params.profileName
if (Array.isArray(name)) {
throw new Error('"profileName" route property is an array. Expected a single string.')
}
return name
}

View File

@ -1,7 +1,7 @@
import { useProfileStore } from '@/stores/profile-store'
import { ApiClient } from './base' import { ApiClient } from './base'
import type { Currency } from './data' import type { Currency } from './data'
import { type Page, type PageRequest } from './pagination' import { type Page, type PageRequest } from './pagination'
import { getSelectedProfile } from './profile'
export interface TransactionVendor { export interface TransactionVendor {
id: number id: number
@ -146,9 +146,7 @@ export class TransactionApiClient extends ApiClient {
constructor() { constructor() {
super() super()
const profileStore = useProfileStore() this.path = `/profiles/${getSelectedProfile()}`
if (!profileStore.state) throw new Error('No profile state!')
this.path = `/profiles/${profileStore.state.name}`
} }
getVendors(): Promise<TransactionVendor[]> { getVendors(): Promise<TransactionVendor[]> {

View File

@ -6,12 +6,10 @@ import FormGroup from './form/FormGroup.vue';
import ModalWrapper from './ModalWrapper.vue'; import ModalWrapper from './ModalWrapper.vue';
import AppButton from './AppButton.vue'; import AppButton from './AppButton.vue';
import { AccountApiClient, AccountValueRecordType, type Account, type AccountValueRecord, type AccountValueRecordCreationPayload } from '@/api/account'; import { AccountApiClient, AccountValueRecordType, type Account, type AccountValueRecord, type AccountValueRecordCreationPayload } from '@/api/account';
import { useProfileStore } from '@/stores/profile-store';
import { datetimeLocalToISO, getDatetimeLocalValueForNow } from '@/util/time'; import { datetimeLocalToISO, getDatetimeLocalValueForNow } from '@/util/time';
import FileSelector from './FileSelector.vue'; import FileSelector from './FileSelector.vue';
const props = defineProps<{ account: Account }>() const props = defineProps<{ account: Account }>()
const profileStore = useProfileStore()
const modal = useTemplateRef('modal') const modal = useTemplateRef('modal')
const savedValueRecord: Ref<AccountValueRecord | undefined> = ref(undefined) const savedValueRecord: Ref<AccountValueRecord | undefined> = ref(undefined)
@ -33,13 +31,12 @@ async function show(): Promise<AccountValueRecord | undefined> {
} }
async function addValueRecord() { async function addValueRecord() {
if (!profileStore.state) return
const payload: AccountValueRecordCreationPayload = { const payload: AccountValueRecordCreationPayload = {
timestamp: datetimeLocalToISO(timestamp.value), timestamp: datetimeLocalToISO(timestamp.value),
type: AccountValueRecordType.BALANCE, type: AccountValueRecordType.BALANCE,
value: amount.value value: amount.value
} }
const api = new AccountApiClient(profileStore.state) const api = new AccountApiClient()
try { try {
savedValueRecord.value = await api.createValueRecord(props.account.id, payload, attachments.value) savedValueRecord.value = await api.createValueRecord(props.account.id, payload, attachments.value)
modal.value?.close('saved') modal.value?.close('saved')

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { getSelectedProfile } from '@/api/profile';
import type { TransactionCategory } from '@/api/transaction'; import type { TransactionCategory } from '@/api/transaction';
import { useProfileStore } from '@/stores/profile-store';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
const router = useRouter() const router = useRouter()
@ -8,8 +8,7 @@ const props = defineProps<{ category: TransactionCategory, clickable?: boolean }
function onClicked() { function onClicked() {
if (props.clickable) { if (props.clickable) {
const profileStore = useProfileStore() router.push(`/profiles/${getSelectedProfile()}/categories`)
router.push(`/profiles/${profileStore.state?.name}/categories`)
} }
} }
</script> </script>

View File

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { AccountApiClient, AccountHistoryItemType, type AccountHistoryItem, type AccountHistoryJournalEntryItem, type AccountHistoryValueRecordItem } from '@/api/account'; import { AccountApiClient, AccountHistoryItemType, type AccountHistoryItem, type AccountHistoryJournalEntryItem, type AccountHistoryValueRecordItem } from '@/api/account';
import type { PageRequest } from '@/api/pagination'; import type { PageRequest } from '@/api/pagination';
import { useProfileStore } from '@/stores/profile-store';
import { onMounted, ref, type Ref } from 'vue'; import { onMounted, ref, type Ref } from 'vue';
import ValueRecordHistoryItem from './ValueRecordHistoryItem.vue'; import ValueRecordHistoryItem from './ValueRecordHistoryItem.vue';
import JournalEntryHistoryItem from './JournalEntryHistoryItem.vue'; import JournalEntryHistoryItem from './JournalEntryHistoryItem.vue';
@ -10,10 +9,8 @@ const props = defineProps<{ accountId: number }>()
const historyItems: Ref<AccountHistoryItem[]> = ref([]) const historyItems: Ref<AccountHistoryItem[]> = ref([])
onMounted(async () => { onMounted(async () => {
const profileStore = useProfileStore()
if (!profileStore.state) return
const pageRequest: PageRequest = { page: 1, size: 10, sorts: [{ attribute: 'timestamp', dir: 'DESC' }] } const pageRequest: PageRequest = { page: 1, size: 10, sorts: [{ attribute: 'timestamp', dir: 'DESC' }] }
const api = new AccountApiClient(profileStore.state) const api = new AccountApiClient()
while (true) { while (true) {
try { try {
const page = await api.getHistory(props.accountId, pageRequest) const page = await api.getHistory(props.accountId, pageRequest)

View File

@ -1,15 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import type { AccountHistoryJournalEntryItem } from '@/api/account' import type { AccountHistoryJournalEntryItem } from '@/api/account'
import { formatMoney } from '@/api/data'; import { formatMoney } from '@/api/data';
import { useProfileStore } from '@/stores/profile-store'; import { getSelectedProfile } from '@/api/profile';
defineProps<{ item: AccountHistoryJournalEntryItem }>() defineProps<{ item: AccountHistoryJournalEntryItem }>()
const profileStore = useProfileStore()
</script> </script>
<template> <template>
<div class="history-item-content"> <div class="history-item-content">
<div> <div>
<RouterLink :to="`/profiles/${profileStore.state?.name}/transactions/${item.transactionId}`"> <RouterLink :to="`/profiles/${getSelectedProfile()}/transactions/${item.transactionId}`">
Transaction #{{ item.transactionId }} Transaction #{{ item.transactionId }}
</RouterLink> </RouterLink>
entered as a entered as a

View File

@ -2,17 +2,14 @@
import { AccountApiClient, type AccountHistoryValueRecordItem } from '@/api/account'; import { AccountApiClient, type AccountHistoryValueRecordItem } from '@/api/account';
import { formatMoney } from '@/api/data'; import { formatMoney } from '@/api/data';
import AppButton from '../AppButton.vue'; import AppButton from '../AppButton.vue';
import { useProfileStore } from '@/stores/profile-store';
import { showConfirm } from '@/util/alert'; import { showConfirm } from '@/util/alert';
const props = defineProps<{ item: AccountHistoryValueRecordItem, accountId: number }>() const props = defineProps<{ item: AccountHistoryValueRecordItem, accountId: number }>()
const profileStore = useProfileStore()
async function deleteValueRecord(id: number) { async function deleteValueRecord(id: number) {
if (!profileStore.state) return
const confirm = await showConfirm('Are you sure you want to delete this value record?') const confirm = await showConfirm('Are you sure you want to delete this value record?')
if (!confirm) return if (!confirm) return
const api = new AccountApiClient(profileStore.state) const api = new AccountApiClient()
try { try {
await api.deleteValueRecord(props.accountId, id) await api.deleteValueRecord(props.accountId, id)
} catch (err) { } catch (err) {

View File

@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { AccountApiClient, type Account } from '@/api/account'; import { AccountApiClient, type Account } from '@/api/account';
import { formatMoney } from '@/api/data'; import { formatMoney } from '@/api/data';
import { getSelectedProfile } from '@/api/profile';
import AddValueRecordModal from '@/components/AddValueRecordModal.vue'; import AddValueRecordModal from '@/components/AddValueRecordModal.vue';
import AppButton from '@/components/AppButton.vue'; import AppButton from '@/components/AppButton.vue';
import AppPage from '@/components/AppPage.vue'; import AppPage from '@/components/AppPage.vue';
import AccountHistory from '@/components/history/AccountHistory.vue'; import AccountHistory from '@/components/history/AccountHistory.vue';
import PropertiesTable from '@/components/PropertiesTable.vue'; import PropertiesTable from '@/components/PropertiesTable.vue';
import { useProfileStore } from '@/stores/profile-store';
import { showConfirm } from '@/util/alert'; import { showConfirm } from '@/util/alert';
import { onMounted, ref, useTemplateRef, type Ref } from 'vue'; import { onMounted, ref, useTemplateRef, type Ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
@ -14,19 +14,13 @@ import { useRoute, useRouter } from 'vue-router';
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const profileStore = useProfileStore()
const addValueRecordModal = useTemplateRef("addValueRecordModal") const addValueRecordModal = useTemplateRef("addValueRecordModal")
const account: Ref<Account | null> = ref(null) const account: Ref<Account | null> = ref(null)
onMounted(async () => { onMounted(async () => {
if (!profileStore.state) {
await router.replace('/')
return
}
const accountId = parseInt(route.params.id as string) const accountId = parseInt(route.params.id as string)
try { try {
const api = new AccountApiClient(profileStore.state) const api = new AccountApiClient()
account.value = await api.getAccount(accountId) account.value = await api.getAccount(accountId)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
@ -35,12 +29,12 @@ onMounted(async () => {
}) })
async function deleteAccount() { async function deleteAccount() {
if (!profileStore.state || !account.value) return if (!account.value) return
if (await showConfirm('Are you sure you want to delete this account? This will permanently remove the account and all associated transactions.')) { if (await showConfirm('Are you sure you want to delete this account? This will permanently remove the account and all associated transactions.')) {
try { try {
const api = new AccountApiClient(profileStore.state) const api = new AccountApiClient()
await api.deleteAccount(account.value.id) await api.deleteAccount(account.value.id)
await router.replace(`/profiles/${profileStore.state.name}`) await router.replace(`/profiles/${getSelectedProfile()}`)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@ -96,8 +90,8 @@ async function addValueRecord() {
</PropertiesTable> </PropertiesTable>
<div> <div>
<AppButton @click="addValueRecord()">Record Value</AppButton> <AppButton @click="addValueRecord()">Record Value</AppButton>
<AppButton icon="wrench" <AppButton icon="wrench" @click="router.push(`/profiles/${getSelectedProfile()}/accounts/${account?.id}/edit`)">
@click="router.push(`/profiles/${profileStore.state?.name}/accounts/${account?.id}/edit`)">Edit</AppButton> Edit</AppButton>
<AppButton icon="trash" @click="deleteAccount()">Delete</AppButton> <AppButton icon="trash" @click="deleteAccount()">Delete</AppButton>
</div> </div>

View File

@ -58,7 +58,7 @@ async function doChangePassword() {
<template> <template>
<AppPage title="My User"> <AppPage title="My User">
<p> <p>
You are logged in as <code>{{ authStore.state?.username }}</code>. You are logged in as <code style="font-size: 14px;">{{ authStore.state?.username }}</code>.
</p> </p>
<div style="text-align: right;"> <div style="text-align: right;">
<AppButton @click="showChangePasswordModal()">Change Password</AppButton> <AppButton @click="showChangePasswordModal()">Change Password</AppButton>

View File

@ -26,7 +26,7 @@ onMounted(async () => {
} }
try { try {
const api = new AccountApiClient(profile.value) const api = new AccountApiClient()
accounts.value = await api.getAccounts() accounts.value = await api.getAccounts()
} catch (err) { } catch (err) {
console.error('Failed to load accounts', err) console.error('Failed to load accounts', err)

View File

@ -3,12 +3,8 @@ import { ProfileApiClient, type Profile } from '@/api/profile';
import AppButton from '@/components/AppButton.vue'; import AppButton from '@/components/AppButton.vue';
import AppPage from '@/components/AppPage.vue'; import AppPage from '@/components/AppPage.vue';
import ModalWrapper from '@/components/ModalWrapper.vue'; import ModalWrapper from '@/components/ModalWrapper.vue';
import { useAuthStore } from '@/stores/auth-store';
import { useProfileStore } from '@/stores/profile-store';
import { onMounted, type Ref, ref, useTemplateRef } from 'vue'; import { onMounted, type Ref, ref, useTemplateRef } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
const authStore = useAuthStore()
const profileStore = useProfileStore()
const router = useRouter() const router = useRouter()
const profiles: Ref<Profile[]> = ref([]) const profiles: Ref<Profile[]> = ref([])
@ -16,12 +12,6 @@ const addProfileModal = useTemplateRef('addProfileModal')
const newProfileName = ref('') const newProfileName = ref('')
onMounted(async () => { onMounted(async () => {
authStore.$subscribe(async (_, state) => {
if (state.state === null) {
await router.replace('/login')
}
})
await fetchProfiles() await fetchProfiles()
}) })
@ -35,7 +25,6 @@ async function fetchProfiles() {
} }
function selectProfile(profile: Profile) { function selectProfile(profile: Profile) {
profileStore.onProfileSelected(profile)
router.push('/profiles/' + profile.name) router.push('/profiles/' + profile.name)
} }

View File

@ -1,20 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { ApiError } from '@/api/base'; import { ApiError } from '@/api/base';
import { formatMoney } from '@/api/data'; import { formatMoney } from '@/api/data';
import { getSelectedProfile } from '@/api/profile';
import { TransactionApiClient, type TransactionDetail } from '@/api/transaction'; import { TransactionApiClient, type TransactionDetail } from '@/api/transaction';
import AppButton from '@/components/AppButton.vue'; import AppButton from '@/components/AppButton.vue';
import AppPage from '@/components/AppPage.vue'; import AppPage from '@/components/AppPage.vue';
import CategoryLabel from '@/components/CategoryLabel.vue'; import CategoryLabel from '@/components/CategoryLabel.vue';
import PropertiesTable from '@/components/PropertiesTable.vue'; import PropertiesTable from '@/components/PropertiesTable.vue';
import TagLabel from '@/components/TagLabel.vue'; import TagLabel from '@/components/TagLabel.vue';
import { useProfileStore } from '@/stores/profile-store';
import { showAlert, showConfirm } from '@/util/alert'; import { showAlert, showConfirm } from '@/util/alert';
import { onMounted, ref, type Ref } from 'vue'; import { onMounted, ref, type Ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const profileStore = useProfileStore()
const transaction: Ref<TransactionDetail | undefined> = ref() const transaction: Ref<TransactionDetail | undefined> = ref()
@ -33,12 +32,12 @@ onMounted(async () => {
}) })
async function deleteTransaction() { async function deleteTransaction() {
if (!transaction.value || !profileStore.state) return if (!transaction.value) return
const conf = await showConfirm('Are you sure you want to delete this transaction? This will permanently delete all data pertaining to this transaction, and it cannot be recovered.') const conf = await showConfirm('Are you sure you want to delete this transaction? This will permanently delete all data pertaining to this transaction, and it cannot be recovered.')
if (!conf) return if (!conf) return
try { try {
await new TransactionApiClient().deleteTransaction(transaction.value.id) await new TransactionApiClient().deleteTransaction(transaction.value.id)
await router.replace(`/profiles/${profileStore.state.name}`) await router.replace(`/profiles/${getSelectedProfile()}`)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@ -120,7 +119,8 @@ async function deleteTransaction() {
</div> </div>
<div> <div>
<AppButton icon="wrench" <AppButton icon="wrench"
@click="router.push(`/profiles/${profileStore.state?.name}/transactions/${transaction.id}/edit`)">Edit @click="router.push(`/profiles/${getSelectedProfile()}/transactions/${transaction.id}/edit`)">
Edit
</AppButton> </AppButton>
<AppButton icon="trash" @click="deleteTransaction()">Delete</AppButton> <AppButton icon="trash" @click="deleteTransaction()">Delete</AppButton>
</div> </div>

View File

@ -1,17 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { AccountApiClient, AccountTypes, type Account, type AccountType } from '@/api/account'; import { AccountApiClient, AccountTypes, type Account, type AccountType } from '@/api/account';
import { getSelectedProfile } from '@/api/profile';
import AppPage from '@/components/AppPage.vue'; import AppPage from '@/components/AppPage.vue';
import AppForm from '@/components/form/AppForm.vue'; import AppForm from '@/components/form/AppForm.vue';
import FormActions from '@/components/form/FormActions.vue'; import FormActions from '@/components/form/FormActions.vue';
import FormControl from '@/components/form/FormControl.vue'; import FormControl from '@/components/form/FormControl.vue';
import FormGroup from '@/components/form/FormGroup.vue'; import FormGroup from '@/components/form/FormGroup.vue';
import { useProfileStore } from '@/stores/profile-store';
import { computed, onMounted, ref, type Ref } from 'vue'; import { computed, onMounted, ref, type Ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const profileStore = useProfileStore()
const existingAccount: Ref<Account | null> = ref(null) const existingAccount: Ref<Account | null> = ref(null)
const editing = computed(() => { const editing = computed(() => {
@ -26,14 +25,12 @@ const currency = ref('USD')
const description = ref('') const description = ref('')
onMounted(async () => { onMounted(async () => {
if (!profileStore.state) return
const accountIdStr = route.params.id const accountIdStr = route.params.id
if (accountIdStr && typeof (accountIdStr) === 'string') { if (accountIdStr && typeof (accountIdStr) === 'string') {
const accountId = parseInt(accountIdStr) const accountId = parseInt(accountIdStr)
try { try {
loading.value = true loading.value = true
const api = new AccountApiClient(profileStore.state) const api = new AccountApiClient()
existingAccount.value = await api.getAccount(accountId) existingAccount.value = await api.getAccount(accountId)
accountName.value = existingAccount.value.name accountName.value = existingAccount.value.name
accountType.value = AccountTypes.of(existingAccount.value.type) accountType.value = AccountTypes.of(existingAccount.value.type)
@ -49,7 +46,6 @@ onMounted(async () => {
}) })
async function doSubmit() { async function doSubmit() {
if (!profileStore.state) return
const payload = { const payload = {
name: accountName.value, name: accountName.value,
description: description.value, description: description.value,
@ -59,12 +55,12 @@ async function doSubmit() {
} }
try { try {
const api = new AccountApiClient(profileStore.state) const api = new AccountApiClient()
loading.value = true loading.value = true
const account = editing.value const account = editing.value
? await api.updateAccount(existingAccount.value?.id ?? 0, payload) ? await api.updateAccount(existingAccount.value?.id ?? 0, payload)
: await api.createAccount(payload) : await api.createAccount(payload)
await router.replace(`/profiles/${profileStore.state.name}/accounts/${account.id}`) await router.replace(`/profiles/${getSelectedProfile()}/accounts/${account.id}`)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} finally { } finally {
@ -105,7 +101,7 @@ async function doSubmit() {
</FormControl> </FormControl>
</FormGroup> </FormGroup>
<FormActions @cancel="router.replace(`/profiles/${profileStore.state?.name}`)" :disabled="loading" <FormActions @cancel="router.replace(`/profiles/${getSelectedProfile()}`)" :disabled="loading"
:submit-text="editing ? 'Save' : 'Add'" /> :submit-text="editing ? 'Save' : 'Add'" />
</AppForm> </AppForm>
</AppPage> </AppPage>

View File

@ -12,6 +12,7 @@ The form consists of a few main sections:
<script setup lang="ts"> <script setup lang="ts">
import { AccountApiClient, type Account } from '@/api/account'; import { AccountApiClient, type Account } from '@/api/account';
import { DataApiClient, type Currency } from '@/api/data'; import { DataApiClient, type Currency } from '@/api/data';
import { getSelectedProfile } from '@/api/profile';
import { TransactionApiClient, type AddTransactionPayload, type TransactionDetail, type TransactionDetailLineItem, type TransactionVendor } from '@/api/transaction'; import { TransactionApiClient, type AddTransactionPayload, type TransactionDetail, type TransactionDetailLineItem, type TransactionVendor } from '@/api/transaction';
import AppPage from '@/components/AppPage.vue'; import AppPage from '@/components/AppPage.vue';
import CategorySelect from '@/components/CategorySelect.vue'; import CategorySelect from '@/components/CategorySelect.vue';
@ -22,14 +23,12 @@ import FormControl from '@/components/form/FormControl.vue';
import FormGroup from '@/components/form/FormGroup.vue'; import FormGroup from '@/components/form/FormGroup.vue';
import LineItemsEditor from '@/components/LineItemsEditor.vue'; import LineItemsEditor from '@/components/LineItemsEditor.vue';
import TagLabel from '@/components/TagLabel.vue'; import TagLabel from '@/components/TagLabel.vue';
import { useProfileStore } from '@/stores/profile-store';
import { getDatetimeLocalValueForNow } from '@/util/time'; import { getDatetimeLocalValueForNow } from '@/util/time';
import { computed, onMounted, ref, watch, type Ref } from 'vue'; import { computed, onMounted, ref, watch, type Ref } from 'vue';
import { useRoute, useRouter, } from 'vue-router'; import { useRoute, useRouter, } from 'vue-router';
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const profileStore = useProfileStore()
const existingTransaction: Ref<TransactionDetail | null> = ref(null) const existingTransaction: Ref<TransactionDetail | null> = ref(null)
const editing = computed(() => { const editing = computed(() => {
@ -80,10 +79,9 @@ watch(availableCurrencies, (newValue: Currency[]) => {
}) })
onMounted(async () => { onMounted(async () => {
if (!profileStore.state) return
const dataClient = new DataApiClient() const dataClient = new DataApiClient()
const transactionClient = new TransactionApiClient() const transactionClient = new TransactionApiClient()
const accountClient = new AccountApiClient(profileStore.state) const accountClient = new AccountApiClient()
// Fetch various collections of data needed for different user choices. // Fetch various collections of data needed for different user choices.
dataClient.getCurrencies().then(currencies => allCurrencies.value = currencies) dataClient.getCurrencies().then(currencies => allCurrencies.value = currencies)
@ -117,8 +115,6 @@ onMounted(async () => {
* created. * created.
*/ */
async function doSubmit() { async function doSubmit() {
if (!profileStore.state) return
const localDate = new Date(timestamp.value) const localDate = new Date(timestamp.value)
const scaledAmount = amount.value * Math.pow(10, currency.value?.fractionalDigits ?? 0) const scaledAmount = amount.value * Math.pow(10, currency.value?.fractionalDigits ?? 0)
const payload: AddTransactionPayload = { const payload: AddTransactionPayload = {
@ -146,7 +142,7 @@ async function doSubmit() {
} else { } else {
savedTransaction = await transactionApi.addTransaction(payload, attachmentsToUpload.value) savedTransaction = await transactionApi.addTransaction(payload, attachmentsToUpload.value)
} }
await router.replace(`/profiles/${profileStore.state.name}/transactions/${savedTransaction.id}`) await router.replace(`/profiles/${getSelectedProfile()}/transactions/${savedTransaction.id}`)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} finally { } finally {
@ -159,11 +155,10 @@ async function doSubmit() {
* profile's homepage. * profile's homepage.
*/ */
function doCancel() { function doCancel() {
if (!profileStore.state) return
if (editing.value) { if (editing.value) {
router.replace(`/profiles/${profileStore.state.name}/transactions/${existingTransaction.value?.id}`) router.replace(`/profiles/${getSelectedProfile()}/transactions/${existingTransaction.value?.id}`)
} else { } else {
router.replace(`/profiles/${profileStore.state.name}`) router.replace(`/profiles/${getSelectedProfile()}`)
} }
} }

View File

@ -1,24 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { AccountApiClient, type Account } from '@/api/account' import { AccountApiClient, type Account } from '@/api/account'
import { formatMoney } from '@/api/data' import { formatMoney } from '@/api/data'
import { getSelectedProfile } from '@/api/profile'
import AppButton from '@/components/AppButton.vue' import AppButton from '@/components/AppButton.vue'
import HomeModule from '@/components/HomeModule.vue' import HomeModule from '@/components/HomeModule.vue'
import { useProfileStore } from '@/stores/profile-store'
import { onMounted, ref, type Ref } from 'vue' import { onMounted, ref, type Ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
const profileStore = useProfileStore()
const accounts: Ref<Account[]> = ref([]) const accounts: Ref<Account[]> = ref([])
onMounted(async () => { onMounted(async () => {
if (!profileStore.state) { const accountApi = new AccountApiClient()
console.warn('No profile is selected.')
return
}
const accountApi = new AccountApiClient(profileStore.state)
accountApi.getAccounts().then(result => accounts.value = result) accountApi.getAccounts().then(result => accounts.value = result)
.catch(err => console.error(err)) .catch(err => console.error(err))
}) })
@ -39,7 +33,7 @@ onMounted(async () => {
<tbody> <tbody>
<tr v-for="account in accounts" :key="account.id"> <tr v-for="account in accounts" :key="account.id">
<td> <td>
<RouterLink :to="`/profiles/${profileStore.state?.name}/accounts/${account.id}`">{{ account.name }} <RouterLink :to="`/profiles/${getSelectedProfile()}/accounts/${account.id}`">{{ account.name }}
</RouterLink> </RouterLink>
</td> </td>
<td>{{ account.currency.code }}</td> <td>{{ account.currency.code }}</td>
@ -54,7 +48,7 @@ onMounted(async () => {
</table> </table>
</template> </template>
<template v-slot:actions> <template v-slot:actions>
<AppButton icon="plus" @click="router.push(`/profiles/${profileStore.state?.name}/add-account`)">Add Account <AppButton icon="plus" @click="router.push(`/profiles/${getSelectedProfile()}/add-account`)">Add Account
</AppButton> </AppButton>
</template> </template>
</HomeModule> </HomeModule>

View File

@ -1,21 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import { ProfileApiClient } from '@/api/profile'; import { getSelectedProfile, ProfileApiClient, type Profile } from '@/api/profile';
import AppButton from '@/components/AppButton.vue'; import AppButton from '@/components/AppButton.vue';
import ConfirmModal from '@/components/ConfirmModal.vue'; import ConfirmModal from '@/components/ConfirmModal.vue';
import HomeModule from '@/components/HomeModule.vue'; import HomeModule from '@/components/HomeModule.vue';
import { useProfileStore } from '@/stores/profile-store'; import { showAlert } from '@/util/alert';
import { useTemplateRef } from 'vue'; import { onMounted, ref, useTemplateRef, type Ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
const profileStore = useProfileStore()
const router = useRouter() const router = useRouter()
const confirmDeleteModal = useTemplateRef('confirmDeleteModal') const confirmDeleteModal = useTemplateRef('confirmDeleteModal')
const profile: Ref<Profile | undefined> = ref()
onMounted(async () => {
try {
profile.value = await new ProfileApiClient().getProfile(getSelectedProfile())
} catch (err) {
console.error(err)
await showAlert("Failed to get profile.")
await router.replace("/profiles")
}
})
async function deleteProfile() { async function deleteProfile() {
if (profileStore.state && await confirmDeleteModal.value?.confirm()) { const currentProfileName = getSelectedProfile()
if (await confirmDeleteModal.value?.confirm()) {
const api = new ProfileApiClient() const api = new ProfileApiClient()
try { try {
await api.deleteProfile(profileStore.state.name) await api.deleteProfile(currentProfileName)
await router.replace('/profiles') await router.replace('/profiles')
} catch (err) { } catch (err) {
console.error(err) console.error(err)
@ -24,14 +36,14 @@ async function deleteProfile() {
} }
</script> </script>
<template> <template>
<HomeModule title="Profile"> <HomeModule title="Profile" v-if="profile">
<template v-slot:default> <template v-slot:default>
<p>Your currently selected profile is: {{ profileStore.state?.name }}</p> <p>Your currently selected profile is: {{ profile.name }}</p>
<p> <p>
<RouterLink :to="`/profiles/${profileStore.state?.name}/vendors`">View all vendors here.</RouterLink> <RouterLink :to="`/profiles/${profile.name}/vendors`">View all vendors here.</RouterLink>
</p> </p>
<p> <p>
<RouterLink :to="`/profiles/${profileStore.state?.name}/categories`">View all categories here.</RouterLink> <RouterLink :to="`/profiles/${profile.name}/categories`">View all categories here.</RouterLink>
</p> </p>
<ConfirmModal ref="confirmDeleteModal"> <ConfirmModal ref="confirmDeleteModal">

View File

@ -1,16 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { formatMoney } from '@/api/data'; import { formatMoney } from '@/api/data';
import type { Page, PageRequest } from '@/api/pagination'; import type { Page, PageRequest } from '@/api/pagination';
import { getSelectedProfile } from '@/api/profile';
import { TransactionApiClient, type TransactionsListItem } from '@/api/transaction'; import { TransactionApiClient, type TransactionsListItem } from '@/api/transaction';
import AppButton from '@/components/AppButton.vue'; import AppButton from '@/components/AppButton.vue';
import HomeModule from '@/components/HomeModule.vue'; import HomeModule from '@/components/HomeModule.vue';
import PaginationControls from '@/components/PaginationControls.vue'; import PaginationControls from '@/components/PaginationControls.vue';
import { useProfileStore } from '@/stores/profile-store';
import { onMounted, ref, type Ref } from 'vue'; import { onMounted, ref, type Ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
const router = useRouter() const router = useRouter()
const profileStore = useProfileStore()
const transactions: Ref<Page<TransactionsListItem>> = ref({ items: [], pageRequest: { page: 1, size: 10, sorts: [] }, totalElements: 0, totalPages: 0, isFirst: true, isLast: true }) const transactions: Ref<Page<TransactionsListItem>> = ref({ items: [], pageRequest: { page: 1, size: 10, sorts: [] }, totalElements: 0, totalPages: 0, isFirst: true, isLast: true })
onMounted(async () => { onMounted(async () => {
@ -51,18 +50,18 @@ async function fetchPage(pageRequest: PageRequest) {
<td>{{ tx.description }}</td> <td>{{ tx.description }}</td>
<td> <td>
<RouterLink v-if="tx.creditedAccount" <RouterLink v-if="tx.creditedAccount"
:to="`/profiles/${profileStore.state?.name}/accounts/${tx.creditedAccount.id}`"> :to="`/profiles/${getSelectedProfile()}/accounts/${tx.creditedAccount.id}`">
{{ tx.creditedAccount?.name }} {{ tx.creditedAccount?.name }}
</RouterLink> </RouterLink>
</td> </td>
<td> <td>
<RouterLink v-if="tx.debitedAccount" <RouterLink v-if="tx.debitedAccount"
:to="`/profiles/${profileStore.state?.name}/accounts/${tx.debitedAccount.id}`"> :to="`/profiles/${getSelectedProfile()}/accounts/${tx.debitedAccount.id}`">
{{ tx.debitedAccount?.name }} {{ tx.debitedAccount?.name }}
</RouterLink> </RouterLink>
</td> </td>
<td> <td>
<RouterLink :to="`/profiles/${profileStore.state?.name}/transactions/${tx.id}`">View</RouterLink> <RouterLink :to="`/profiles/${getSelectedProfile()}/transactions/${tx.id}`">View</RouterLink>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -70,7 +69,7 @@ async function fetchPage(pageRequest: PageRequest) {
<PaginationControls :page="transactions" @update="pr => fetchPage(pr)"></PaginationControls> <PaginationControls :page="transactions" @update="pr => fetchPage(pr)"></PaginationControls>
</template> </template>
<template v-slot:actions> <template v-slot:actions>
<AppButton icon="plus" @click="router.push(`/profiles/${profileStore.state?.name}/add-transaction`)">Add <AppButton icon="plus" @click="router.push(`/profiles/${getSelectedProfile()}/add-transaction`)">Add
Transaction</AppButton> Transaction</AppButton>
</template> </template>
</HomeModule> </HomeModule>

View File

@ -1,6 +1,6 @@
import { getSelectedProfile } 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'
import { useProfileStore } from '@/stores/profile-store'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -30,13 +30,12 @@ const router = createRouter({
meta: { title: 'Profiles' }, meta: { title: 'Profiles' },
}, },
{ {
path: 'profiles/:name', path: 'profiles/:profileName',
beforeEnter: profileSelected,
children: [ children: [
{ {
path: '', path: '',
component: () => import('@/pages/UserHomePage.vue'), component: () => import('@/pages/UserHomePage.vue'),
meta: { title: (to: RouteLocationNormalized) => 'Profile ' + to.params.name }, meta: { title: (to: RouteLocationNormalized) => 'Profile ' + getSelectedProfile(to) },
}, },
{ {
path: 'accounts/:id', path: 'accounts/:id',
@ -85,6 +84,7 @@ const router = createRouter({
], ],
}) })
// Adds a webpage title to each route based on the "meta.title" attribute.
router.beforeEach((to, _, next) => { router.beforeEach((to, _, next) => {
if (to.meta.title !== undefined && typeof to.meta.title === 'string') { if (to.meta.title !== undefined && typeof to.meta.title === 'string') {
document.title = 'Finnow - ' + to.meta.title document.title = 'Finnow - ' + to.meta.title
@ -99,6 +99,12 @@ router.beforeEach((to, _, next) => {
next() next()
}) })
/**
* Guard to ensure a route can only be accessed by authenticated users.
* @param to The route to guard.
* @returns True if the user is authenticated, or a redirect to the `/login`
* page if the user isn't logged in.
*/
function onlyAuthenticated(to: RouteLocationNormalized) { function onlyAuthenticated(to: RouteLocationNormalized) {
const authStore = useAuthStore() const authStore = useAuthStore()
if (authStore.state) return true if (authStore.state) return true
@ -106,10 +112,4 @@ function onlyAuthenticated(to: RouteLocationNormalized) {
return '/login?next=' + encodeURIComponent(to.path) return '/login?next=' + encodeURIComponent(to.path)
} }
function profileSelected() {
const profileStore = useProfileStore()
if (profileStore.state) return true
return '/profiles' // Send the user to /profiles to select one before continuing.
}
export default router export default router

View File

@ -1,32 +0,0 @@
import type { Profile } from '@/api/profile'
import { defineStore } from 'pinia'
import { ref, type Ref } from 'vue'
const LOCAL_STORAGE_KEY = 'profile'
export const useProfileStore = defineStore('profile', () => {
const state: Ref<Profile | null> = ref(getStateFromLocalStorage())
function onProfileSelected(profile: Profile) {
state.value = profile
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(profile))
}
function onProfileSelectionCleared() {
state.value = null
localStorage.removeItem(LOCAL_STORAGE_KEY)
}
return { state, onProfileSelected, onProfileSelectionCleared }
})
function getStateFromLocalStorage(): Profile | null {
const jsonText = localStorage.getItem(LOCAL_STORAGE_KEY)
if (jsonText === null || jsonText.length === 0) {
localStorage.removeItem(LOCAL_STORAGE_KEY)
return null
}
const profile = JSON.parse(jsonText) as Profile
console.info('Loaded profile', profile)
return profile
}