Clean up profile handling again!

This commit is contained in:
andrewlalis 2025-08-31 16:43:37 -04:00
parent a674421337
commit 074b4ded1d
26 changed files with 152 additions and 96 deletions

View File

@ -1,3 +1,4 @@
import { type RouteLocation } from 'vue-router'
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'
@ -125,9 +126,9 @@ export interface AccountHistoryJournalEntryItem extends AccountHistoryItem {
export class AccountApiClient extends ApiClient { export class AccountApiClient extends ApiClient {
readonly path: string readonly path: string
constructor() { constructor(route: RouteLocation) {
super() super()
this.path = `/profiles/${getSelectedProfile()}/accounts` this.path = `/profiles/${getSelectedProfile(route)}/accounts`
} }
getAccounts(): Promise<Account[]> { getAccounts(): Promise<Account[]> {

View File

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

View File

@ -1,7 +1,6 @@
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
@ -144,9 +143,9 @@ export interface CreateCategoryPayload {
export class TransactionApiClient extends ApiClient { export class TransactionApiClient extends ApiClient {
readonly path: string readonly path: string
constructor() { constructor(profileName: string) {
super() super()
this.path = `/profiles/${getSelectedProfile()}` this.path = `/profiles/${profileName}`
} }
getVendors(): Promise<TransactionVendor[]> { getVendors(): Promise<TransactionVendor[]> {

View File

@ -8,7 +8,9 @@ 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 { datetimeLocalToISO, getDatetimeLocalValueForNow } from '@/util/time'; import { datetimeLocalToISO, getDatetimeLocalValueForNow } from '@/util/time';
import FileSelector from './FileSelector.vue'; import FileSelector from './FileSelector.vue';
import { useRoute } from 'vue-router';
const route = useRoute()
const props = defineProps<{ account: Account }>() const props = defineProps<{ account: Account }>()
const modal = useTemplateRef('modal') const modal = useTemplateRef('modal')
const savedValueRecord: Ref<AccountValueRecord | undefined> = ref(undefined) const savedValueRecord: Ref<AccountValueRecord | undefined> = ref(undefined)
@ -36,7 +38,7 @@ async function addValueRecord() {
type: AccountValueRecordType.BALANCE, type: AccountValueRecordType.BALANCE,
value: amount.value value: amount.value
} }
const api = new AccountApiClient() const api = new AccountApiClient(route)
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,17 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
defineProps<{
buttonStyle?: string, export type ButtonTheme = "primary" | "secondary"
export type ButtonType = "button" | "submit" | "reset"
export type ButtonSize = "sm" | "md" | "lg"
interface Props {
theme?: ButtonTheme,
size?: ButtonSize,
icon?: string, icon?: string,
buttonType?: "button" | "submit" | "reset" | undefined, type?: ButtonType,
disabled?: boolean disabled?: boolean
}>() }
const props = withDefaults(defineProps<Props>(), {
buttonStyle: "primary",
buttonType: "button",
size: "md",
disabled: false
})
defineEmits(['click']) defineEmits(['click'])
const buttonStyle = computed(() => ({
'app-button-theme-secondary': props.theme === "secondary",
'app-button-disabled': props.disabled,
'app-button-size-sm': props.size === "sm",
'app-button-size-lg': props.size === "lg"
}))
</script> </script>
<template> <template>
<button class="app-button" <button class="app-button" :class="buttonStyle" :type="type" :disabled="disabled" @click="$emit('click')">
:class="{ 'app-button-secondary': buttonStyle === 'secondary', 'app-button-disabled': disabled ?? false }"
@click="$emit('click')" :type="buttonType" :disabled="disabled ?? false">
<span v-if="icon"> <span v-if="icon">
<font-awesome-icon :icon="'fa-' + icon" <font-awesome-icon :icon="'fa-' + icon"
:class="{ 'app-button-icon-with-text': $slots.default !== undefined, 'app-button-icon-without-text': $slots.default === undefined }"></font-awesome-icon> :class="{ 'app-button-icon-with-text': $slots.default !== undefined, 'app-button-icon-without-text': $slots.default === undefined }"></font-awesome-icon>
@ -51,6 +69,17 @@ defineEmits(['click'])
cursor: inherit; cursor: inherit;
} }
.app-button-size-sm {
padding: 0.5rem 1rem;
font-size: 0.8rem;
font-weight: 500;
}
.app-button-size-lg {
padding: 1rem 2rem;
font-size: 1.2rem;
}
.app-button:hover:not(.app-button-disabled) { .app-button:hover:not(.app-button-disabled) {
background-color: #374151; background-color: #374151;
} }
@ -65,13 +94,7 @@ defineEmits(['click'])
background-color: #3b4968; background-color: #3b4968;
} }
@media (min-width: 768px) { .app-button-theme-secondary {
.app-button {
padding: .75rem 1.5rem;
}
}
.app-button-secondary {
background-color: #1d2330; background-color: #1d2330;
} }

View File

@ -1,14 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { getSelectedProfile } from '@/api/profile'; import { getSelectedProfile } from '@/api/profile';
import type { TransactionCategory } from '@/api/transaction'; import type { TransactionCategory } from '@/api/transaction';
import { useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
const route = useRoute()
const router = useRouter() const router = useRouter()
const props = defineProps<{ category: TransactionCategory, clickable?: boolean }>() const props = defineProps<{ category: TransactionCategory, clickable?: boolean }>()
function onClicked() { function onClicked() {
if (props.clickable) { if (props.clickable) {
router.push(`/profiles/${getSelectedProfile()}/categories`) router.push(`/profiles/${getSelectedProfile(route)}/categories`)
} }
} }
</script> </script>

View File

@ -1,15 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { getSelectedProfile } from '@/api/profile';
import { TransactionApiClient, type TransactionCategoryTree } from '@/api/transaction'; import { TransactionApiClient, type TransactionCategoryTree } from '@/api/transaction';
import { onMounted, ref, type Ref } from 'vue'; import { onMounted, ref, type Ref } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()
const model = defineModel<number | null>({ required: true }) const model = defineModel<number | null>({ required: true })
defineProps<{ required?: boolean }>() defineProps<{ required?: boolean }>()
defineEmits<{ categorySelected: [TransactionCategoryTree | null] }>() defineEmits<{ categorySelected: [TransactionCategoryTree | null] }>()
const categories: Ref<TransactionCategoryTree[]> = ref([]) const categories: Ref<TransactionCategoryTree[]> = ref([])
onMounted(() => { onMounted(() => {
const api = new TransactionApiClient() const api = new TransactionApiClient(getSelectedProfile(route))
api.getCategoriesFlattened() api.getCategoriesFlattened()
.then(c => categories.value = c) .then(c => categories.value = c)
}) })

View File

@ -7,7 +7,10 @@ import FormGroup from './form/FormGroup.vue';
import FormControl from './form/FormControl.vue'; import FormControl from './form/FormControl.vue';
import AppButton from './AppButton.vue'; import AppButton from './AppButton.vue';
import CategorySelect from './CategorySelect.vue'; import CategorySelect from './CategorySelect.vue';
import { useRoute } from 'vue-router';
import { getSelectedProfile } from '@/api/profile';
const route = useRoute()
const props = defineProps<{ const props = defineProps<{
category?: TransactionCategory category?: TransactionCategory
}>() }>()
@ -52,7 +55,7 @@ function canSave() {
} }
async function doSave() { async function doSave() {
const api = new TransactionApiClient() const api = new TransactionApiClient(getSelectedProfile(route))
const payload = { const payload = {
name: name.value.trim(), name: name.value.trim(),
description: description.value.trim(), description: description.value.trim(),

View File

@ -6,7 +6,10 @@ import FormGroup from './form/FormGroup.vue';
import ModalWrapper from './ModalWrapper.vue'; import ModalWrapper from './ModalWrapper.vue';
import { TransactionApiClient, type TransactionVendor } from '@/api/transaction'; import { TransactionApiClient, type TransactionVendor } from '@/api/transaction';
import AppButton from './AppButton.vue'; import AppButton from './AppButton.vue';
import { useRoute } from 'vue-router';
import { getSelectedProfile } from '@/api/profile';
const route = useRoute()
const props = defineProps<{ const props = defineProps<{
vendor?: TransactionVendor vendor?: TransactionVendor
}>() }>()
@ -35,7 +38,7 @@ function canSave() {
} }
async function doSave() { async function doSave() {
const api = new TransactionApiClient() const api = new TransactionApiClient(getSelectedProfile(route))
const payload = { const payload = {
name: name.value.trim(), name: name.value.trim(),
description: description.value.trim() description: description.value.trim()

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Page, PageRequest } from '@/api/pagination'; import type { Page, PageRequest } from '@/api/pagination';
import AppButton from './AppButton.vue';
const props = defineProps<{ page?: Page<unknown> }>() const props = defineProps<{ page?: Page<unknown> }>()
@ -27,22 +28,24 @@ function incrementPage(step: number) {
</script> </script>
<template> <template>
<div> <div>
<button :disabled="!page || page.isFirst" @click="updatePage(1)"> <div v-if="page && page.totalElements > 0">
<AppButton size="sm" :disabled="!page || page.isFirst" @click="updatePage(1)">
First Page First Page
</button> </AppButton>
<button :disabled="!page || page.isFirst" @click="incrementPage(-1)"> <AppButton size="sm" :disabled="!page || page.isFirst" @click="incrementPage(-1)">
Previous Page Previous Page
</button> </AppButton>
<span>Page {{ page?.pageRequest.page }} / {{ page?.totalPages }}</span> <span>Page {{ page?.pageRequest.page }} / {{ page?.totalPages }}</span>
<button :disabled="!page || page.isLast" @click="incrementPage(1)"> <AppButton size="sm" :disabled="!page || page.isLast" @click="incrementPage(1)">
Next Page Next Page
</button> </AppButton>
<button :disabled="!page || page.isLast" @click="updatePage(page?.totalPages ?? 0)"> <AppButton size="sm" :disabled="!page || page.isLast" @click="updatePage(page?.totalPages ?? 0)">
Last Page Last Page
</button> </AppButton>
</div>
</div> </div>
</template> </template>

View File

@ -4,13 +4,15 @@ import type { PageRequest } from '@/api/pagination';
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';
import { useRoute } from 'vue-router';
const route = useRoute()
const props = defineProps<{ accountId: number }>() const props = defineProps<{ accountId: number }>()
const historyItems: Ref<AccountHistoryItem[]> = ref([]) const historyItems: Ref<AccountHistoryItem[]> = ref([])
onMounted(async () => { onMounted(async () => {
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() const api = new AccountApiClient(route)
while (true) { while (true) {
try { try {
const page = await api.getHistory(props.accountId, pageRequest) const page = await api.getHistory(props.accountId, pageRequest)

View File

@ -2,13 +2,16 @@
import type { AccountHistoryJournalEntryItem } from '@/api/account' import type { AccountHistoryJournalEntryItem } from '@/api/account'
import { formatMoney } from '@/api/data'; import { formatMoney } from '@/api/data';
import { getSelectedProfile } from '@/api/profile'; import { getSelectedProfile } from '@/api/profile';
import { useRoute } from 'vue-router';
const route = useRoute()
defineProps<{ item: AccountHistoryJournalEntryItem }>() defineProps<{ item: AccountHistoryJournalEntryItem }>()
</script> </script>
<template> <template>
<div class="history-item-content"> <div class="history-item-content">
<div> <div>
<RouterLink :to="`/profiles/${getSelectedProfile()}/transactions/${item.transactionId}`"> <RouterLink :to="`/profiles/${getSelectedProfile(route)}/transactions/${item.transactionId}`">
Transaction #{{ item.transactionId }} Transaction #{{ item.transactionId }}
</RouterLink> </RouterLink>
entered as a entered as a

View File

@ -3,13 +3,16 @@ import { AccountApiClient, type AccountHistoryValueRecordItem } from '@/api/acco
import { formatMoney } from '@/api/data'; import { formatMoney } from '@/api/data';
import AppButton from '../AppButton.vue'; import AppButton from '../AppButton.vue';
import { showConfirm } from '@/util/alert'; import { showConfirm } from '@/util/alert';
import { useRoute } from 'vue-router';
const route = useRoute()
const props = defineProps<{ item: AccountHistoryValueRecordItem, accountId: number }>() const props = defineProps<{ item: AccountHistoryValueRecordItem, accountId: number }>()
async function deleteValueRecord(id: number) { async function deleteValueRecord(id: number) {
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() const api = new AccountApiClient(route)
try { try {
await api.deleteValueRecord(props.accountId, id) await api.deleteValueRecord(props.accountId, id)
} catch (err) { } catch (err) {

View File

@ -20,7 +20,7 @@ const account: Ref<Account | null> = ref(null)
onMounted(async () => { onMounted(async () => {
const accountId = parseInt(route.params.id as string) const accountId = parseInt(route.params.id as string)
try { try {
const api = new AccountApiClient() const api = new AccountApiClient(route)
account.value = await api.getAccount(accountId) account.value = await api.getAccount(accountId)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
@ -32,9 +32,9 @@ async function deleteAccount() {
if (!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() const api = new AccountApiClient(route)
await api.deleteAccount(account.value.id) await api.deleteAccount(account.value.id)
await router.replace(`/profiles/${getSelectedProfile()}`) await router.replace(`/profiles/${getSelectedProfile(route)}`)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@ -90,7 +90,8 @@ async function addValueRecord() {
</PropertiesTable> </PropertiesTable>
<div> <div>
<AppButton @click="addValueRecord()">Record Value</AppButton> <AppButton @click="addValueRecord()">Record Value</AppButton>
<AppButton icon="wrench" @click="router.push(`/profiles/${getSelectedProfile()}/accounts/${account?.id}/edit`)"> <AppButton icon="wrench"
@click="router.push(`/profiles/${getSelectedProfile(route)}/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

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { getSelectedProfile } from '@/api/profile';
import { TransactionApiClient, type TransactionCategory, type TransactionCategoryTree } from '@/api/transaction'; import { TransactionApiClient, type TransactionCategory, type TransactionCategoryTree } 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';
@ -7,6 +8,9 @@ import EditCategoryModal from '@/components/EditCategoryModal.vue';
import { showConfirm } from '@/util/alert'; import { showConfirm } from '@/util/alert';
import { hideLoader, showLoader } from '@/util/loader'; import { hideLoader, showLoader } from '@/util/loader';
import { nextTick, onMounted, ref, useTemplateRef, type Ref } from 'vue'; import { nextTick, onMounted, ref, useTemplateRef, type Ref } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()
const editCategoryModal = useTemplateRef('editCategoryModal') const editCategoryModal = useTemplateRef('editCategoryModal')
@ -18,13 +22,13 @@ onMounted(async () => {
}) })
async function loadCategories() { async function loadCategories() {
const api = new TransactionApiClient() const api = new TransactionApiClient(getSelectedProfile(route))
categories.value = await api.getCategories() categories.value = await api.getCategories()
} }
async function editCategory(categoryId: number) { async function editCategory(categoryId: number) {
try { try {
const api = new TransactionApiClient() const api = new TransactionApiClient(getSelectedProfile(route))
editedCategory.value = await api.getCategory(categoryId) editedCategory.value = await api.getCategory(categoryId)
await nextTick() await nextTick()
const result = await editCategoryModal.value?.show() const result = await editCategoryModal.value?.show()
@ -41,7 +45,7 @@ async function deleteCategory(categoryId: number) {
if (result) { if (result) {
try { try {
showLoader() showLoader()
const api = new TransactionApiClient() const api = new TransactionApiClient(getSelectedProfile(route))
await api.deleteCategory(categoryId) await api.deleteCategory(categoryId)
await loadCategories() await loadCategories()
} catch (err) { } catch (err) {

View File

@ -86,7 +86,7 @@ function generateSampleData() {
</FormControl> </FormControl>
</FormGroup> </FormGroup>
<div style="display: flex; margin-left: 1rem; margin-right: 1rem;"> <div style="display: flex; margin-left: 1rem; margin-right: 1rem;">
<AppButton button-type="submit" :disabled="disableForm || !isDataValid()" style="flex-grow: 1;">Login <AppButton type="submit" :disabled="disableForm || !isDataValid()" style="flex-grow: 1;">Login
</AppButton> </AppButton>
<AppButton button-type="button" button-style="secondary" :disabled="disableForm || !isDataValid()" <AppButton button-type="button" button-style="secondary" :disabled="disableForm || !isDataValid()"
@click="doRegister()">Register</AppButton> @click="doRegister()">Register</AppButton>

View File

@ -84,7 +84,7 @@ async function doChangePassword() {
</template> </template>
<template v-slot:buttons> <template v-slot:buttons>
<AppButton @click="doChangePassword()">Change</AppButton> <AppButton @click="doChangePassword()">Change</AppButton>
<AppButton button-style="secondary" @click="changePasswordModal?.close()">Cancel</AppButton> <AppButton theme="secondary" @click="changePasswordModal?.close()">Cancel</AppButton>
</template> </template>
</ModalWrapper> </ModalWrapper>
</AppPage> </AppPage>

View File

@ -26,7 +26,7 @@ onMounted(async () => {
} }
try { try {
const api = new AccountApiClient() const api = new AccountApiClient(route)
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

@ -31,10 +31,10 @@ function selectProfile(profile: Profile) {
async function addProfile() { async function addProfile() {
try { try {
const api = new ProfileApiClient() const api = new ProfileApiClient()
api.createProfile(newProfileName.value) const newProfile = await api.createProfile(newProfileName.value)
newProfileName.value = '' newProfileName.value = ''
addProfileModal.value?.close() addProfileModal.value?.close()
await fetchProfiles() await router.push('/profiles/' + newProfile.name)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@ -45,8 +45,8 @@ async function addProfile() {
<div class="profile-card" v-for="profile in profiles" :key="profile.name" @click="selectProfile(profile)"> <div class="profile-card" v-for="profile in profiles" :key="profile.name" @click="selectProfile(profile)">
<span>{{ profile.name }}</span> <span>{{ profile.name }}</span>
</div> </div>
<div class="profile-card" @click="addProfileModal?.show()"> <div style="text-align: right;">
<span>Add a new profile...</span> <AppButton icon="plus" @click="addProfileModal?.show()">Add a new profile</AppButton>
</div> </div>
<ModalWrapper ref="addProfileModal"> <ModalWrapper ref="addProfileModal">

View File

@ -14,14 +14,14 @@ import { useRoute, useRouter } from 'vue-router';
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const transactionApi = new TransactionApiClient(getSelectedProfile(route))
const transaction: Ref<TransactionDetail | undefined> = ref() const transaction: Ref<TransactionDetail | undefined> = ref()
onMounted(async () => { onMounted(async () => {
const transactionId = parseInt(route.params.id as string) const transactionId = parseInt(route.params.id as string)
try { try {
const api = new TransactionApiClient() transaction.value = await transactionApi.getTransaction(transactionId)
transaction.value = await api.getTransaction(transactionId)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
await router.replace('/') await router.replace('/')
@ -36,8 +36,8 @@ async function deleteTransaction() {
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 transactionApi.deleteTransaction(transaction.value.id)
await router.replace(`/profiles/${getSelectedProfile()}`) await router.replace(`/profiles/${getSelectedProfile(route)}`)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@ -119,7 +119,7 @@ async function deleteTransaction() {
</div> </div>
<div> <div>
<AppButton icon="wrench" <AppButton icon="wrench"
@click="router.push(`/profiles/${getSelectedProfile()}/transactions/${transaction.id}/edit`)"> @click="router.push(`/profiles/${getSelectedProfile(route)}/transactions/${transaction.id}/edit`)">
Edit Edit
</AppButton> </AppButton>
<AppButton icon="trash" @click="deleteTransaction()">Delete</AppButton> <AppButton icon="trash" @click="deleteTransaction()">Delete</AppButton>

View File

@ -1,10 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { getSelectedProfile } from '@/api/profile';
import { TransactionApiClient, type TransactionVendor } from '@/api/transaction'; import { TransactionApiClient, type TransactionVendor } 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 EditVendorModal from '@/components/EditVendorModal.vue'; import EditVendorModal from '@/components/EditVendorModal.vue';
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 } from 'vue-router';
const route = useRoute()
const transactionApi = new TransactionApiClient(getSelectedProfile(route))
const vendors: Ref<TransactionVendor[]> = ref([]) const vendors: Ref<TransactionVendor[]> = ref([])
const editVendorModal = useTemplateRef('editVendorModal') const editVendorModal = useTemplateRef('editVendorModal')
@ -15,8 +20,7 @@ onMounted(async () => {
}) })
async function loadVendors() { async function loadVendors() {
const api = new TransactionApiClient() vendors.value = await transactionApi.getVendors()
vendors.value = await api.getVendors()
} }
async function addVendor() { async function addVendor() {
@ -38,8 +42,7 @@ async function editVendor(vendor: TransactionVendor) {
async function deleteVendor(vendor: TransactionVendor) { async function deleteVendor(vendor: TransactionVendor) {
const confirmed = await showConfirm('Are you sure you want to delete this vendor? It will be permanently removed from all associated transactions.') const confirmed = await showConfirm('Are you sure you want to delete this vendor? It will be permanently removed from all associated transactions.')
if (!confirmed) return if (!confirmed) return
const api = new TransactionApiClient() await transactionApi.deleteVendor(vendor.id)
await api.deleteVendor(vendor.id)
await loadVendors() await loadVendors()
} }
</script> </script>

View File

@ -30,7 +30,7 @@ onMounted(async () => {
const accountId = parseInt(accountIdStr) const accountId = parseInt(accountIdStr)
try { try {
loading.value = true loading.value = true
const api = new AccountApiClient() const api = new AccountApiClient(route)
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)
@ -55,12 +55,12 @@ async function doSubmit() {
} }
try { try {
const api = new AccountApiClient() const api = new AccountApiClient(route)
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/${getSelectedProfile()}/accounts/${account.id}`) await router.replace(`/profiles/${getSelectedProfile(route)}/accounts/${account.id}`)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} finally { } finally {
@ -101,7 +101,7 @@ async function doSubmit() {
</FormControl> </FormControl>
</FormGroup> </FormGroup>
<FormActions @cancel="router.replace(`/profiles/${getSelectedProfile()}`)" :disabled="loading" <FormActions @cancel="router.replace(`/profiles/${getSelectedProfile(route)}`)" :disabled="loading"
:submit-text="editing ? 'Save' : 'Add'" /> :submit-text="editing ? 'Save' : 'Add'" />
</AppForm> </AppForm>
</AppPage> </AppPage>

View File

@ -30,6 +30,9 @@ import { useRoute, useRouter, } from 'vue-router';
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const transactionApi = new TransactionApiClient(getSelectedProfile(route))
const accountApi = new AccountApiClient(route)
const existingTransaction: Ref<TransactionDetail | null> = ref(null) const existingTransaction: Ref<TransactionDetail | null> = ref(null)
const editing = computed(() => { const editing = computed(() => {
return existingTransaction.value !== null || route.meta.title === 'Edit Transaction' return existingTransaction.value !== null || route.meta.title === 'Edit Transaction'
@ -80,14 +83,12 @@ watch(availableCurrencies, (newValue: Currency[]) => {
onMounted(async () => { onMounted(async () => {
const dataClient = new DataApiClient() const dataClient = new DataApiClient()
const transactionClient = new TransactionApiClient()
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)
transactionClient.getVendors().then(vendors => availableVendors.value = vendors) transactionApi.getVendors().then(vendors => availableVendors.value = vendors)
transactionClient.getAllTags().then(t => allTags.value = t) transactionApi.getAllTags().then(t => allTags.value = t)
accountClient.getAccounts().then(accounts => allAccounts.value = accounts) accountApi.getAccounts().then(accounts => allAccounts.value = accounts)
const transactionIdStr = route.params.id const transactionIdStr = route.params.id
@ -95,7 +96,7 @@ onMounted(async () => {
const transactionId = parseInt(transactionIdStr) const transactionId = parseInt(transactionIdStr)
try { try {
loading.value = true loading.value = true
existingTransaction.value = await transactionClient.getTransaction(transactionId) existingTransaction.value = await transactionApi.getTransaction(transactionId)
loadValuesFromExistingTransaction(existingTransaction.value) loadValuesFromExistingTransaction(existingTransaction.value)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
@ -133,7 +134,6 @@ async function doSubmit() {
attachmentIdsToRemove: removedAttachmentIds.value attachmentIdsToRemove: removedAttachmentIds.value
} }
const transactionApi = new TransactionApiClient()
let savedTransaction = null let savedTransaction = null
try { try {
loading.value = true loading.value = true
@ -142,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/${getSelectedProfile()}/transactions/${savedTransaction.id}`) await router.replace(`/profiles/${getSelectedProfile(route)}/transactions/${savedTransaction.id}`)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} finally { } finally {
@ -156,9 +156,9 @@ async function doSubmit() {
*/ */
function doCancel() { function doCancel() {
if (editing.value) { if (editing.value) {
router.replace(`/profiles/${getSelectedProfile()}/transactions/${existingTransaction.value?.id}`) router.replace(`/profiles/${getSelectedProfile(route)}/transactions/${existingTransaction.value?.id}`)
} else { } else {
router.replace(`/profiles/${getSelectedProfile()}`) router.replace(`/profiles/${getSelectedProfile(route)}`)
} }
} }

View File

@ -5,14 +5,15 @@ 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 { onMounted, ref, type Ref } from 'vue' import { onMounted, ref, type Ref } from 'vue'
import { useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
const route = useRoute()
const accounts: Ref<Account[]> = ref([]) const accounts: Ref<Account[]> = ref([])
onMounted(async () => { onMounted(async () => {
const accountApi = new AccountApiClient() const accountApi = new AccountApiClient(route)
accountApi.getAccounts().then(result => accounts.value = result) accountApi.getAccounts().then(result => accounts.value = result)
.catch(err => console.error(err)) .catch(err => console.error(err))
}) })
@ -33,7 +34,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/${getSelectedProfile()}/accounts/${account.id}`">{{ account.name }} <RouterLink :to="`/profiles/${getSelectedProfile(route)}/accounts/${account.id}`">{{ account.name }}
</RouterLink> </RouterLink>
</td> </td>
<td>{{ account.currency.code }}</td> <td>{{ account.currency.code }}</td>
@ -48,7 +49,7 @@ onMounted(async () => {
</table> </table>
</template> </template>
<template v-slot:actions> <template v-slot:actions>
<AppButton icon="plus" @click="router.push(`/profiles/${getSelectedProfile()}/add-account`)">Add Account <AppButton icon="plus" @click="router.push(`/profiles/${getSelectedProfile(route)}/add-account`)">Add Account
</AppButton> </AppButton>
</template> </template>
</HomeModule> </HomeModule>

View File

@ -5,16 +5,17 @@ import ConfirmModal from '@/components/ConfirmModal.vue';
import HomeModule from '@/components/HomeModule.vue'; import HomeModule from '@/components/HomeModule.vue';
import { showAlert } from '@/util/alert'; import { showAlert } from '@/util/alert';
import { onMounted, ref, useTemplateRef, type Ref } from 'vue'; import { onMounted, ref, useTemplateRef, type Ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
const router = useRouter() const router = useRouter()
const route = useRoute()
const confirmDeleteModal = useTemplateRef('confirmDeleteModal') const confirmDeleteModal = useTemplateRef('confirmDeleteModal')
const profile: Ref<Profile | undefined> = ref() const profile: Ref<Profile | undefined> = ref()
onMounted(async () => { onMounted(async () => {
try { try {
profile.value = await new ProfileApiClient().getProfile(getSelectedProfile()) profile.value = await new ProfileApiClient().getProfile(getSelectedProfile(route))
} catch (err) { } catch (err) {
console.error(err) console.error(err)
await showAlert("Failed to get profile.") await showAlert("Failed to get profile.")
@ -23,7 +24,7 @@ onMounted(async () => {
}) })
async function deleteProfile() { async function deleteProfile() {
const currentProfileName = getSelectedProfile() const currentProfileName = getSelectedProfile(route)
if (await confirmDeleteModal.value?.confirm()) { if (await confirmDeleteModal.value?.confirm()) {
const api = new ProfileApiClient() const api = new ProfileApiClient()
try { try {

View File

@ -7,9 +7,10 @@ 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 { onMounted, ref, type Ref } from 'vue'; import { onMounted, ref, type Ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
const router = useRouter() const router = useRouter()
const route = useRoute()
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 () => {
@ -17,7 +18,7 @@ onMounted(async () => {
}) })
async function fetchPage(pageRequest: PageRequest) { async function fetchPage(pageRequest: PageRequest) {
const api = new TransactionApiClient() const api = new TransactionApiClient(getSelectedProfile(route))
try { try {
transactions.value = await api.getTransactions(pageRequest) transactions.value = await api.getTransactions(pageRequest)
} catch (err) { } catch (err) {
@ -50,18 +51,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/${getSelectedProfile()}/accounts/${tx.creditedAccount.id}`"> :to="`/profiles/${getSelectedProfile(route)}/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/${getSelectedProfile()}/accounts/${tx.debitedAccount.id}`"> :to="`/profiles/${getSelectedProfile(route)}/accounts/${tx.debitedAccount.id}`">
{{ tx.debitedAccount?.name }} {{ tx.debitedAccount?.name }}
</RouterLink> </RouterLink>
</td> </td>
<td> <td>
<RouterLink :to="`/profiles/${getSelectedProfile()}/transactions/${tx.id}`">View</RouterLink> <RouterLink :to="`/profiles/${getSelectedProfile(route)}/transactions/${tx.id}`">View</RouterLink>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -69,7 +70,8 @@ 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/${getSelectedProfile()}/add-transaction`)">Add <AppButton size="sm" icon="plus" @click="router.push(`/profiles/${getSelectedProfile(route)}/add-transaction`)">
Add
Transaction</AppButton> Transaction</AppButton>
</template> </template>
</HomeModule> </HomeModule>