Updated category select and transaction edit page.
Build and Deploy Web App / build-and-deploy (push) Successful in 19s Details

This commit is contained in:
andrewlalis 2025-09-21 15:36:07 -04:00
parent bbe3e2aab5
commit 7666c0f450
2 changed files with 78 additions and 124 deletions

View File

@ -1,18 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
import { getSelectedProfile } from '@/api/profile' 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 { computed, onMounted, ref, watch, type Ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import VueSelect, { type Option } from 'vue3-select-component'
const route = useRoute() 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([])
const selectedCategory: Ref<TransactionCategoryTree | null> = ref(null)
const options: Ref<Option<TransactionCategoryTree>[]> = computed(() => {
return categories.value.map(c => {
return { label: c.name, value: c }
})
})
onMounted(() => { watch(model, (newValue) => {
selectedCategory.value = newValue === null ? null : getCategoryById(newValue)
})
onMounted(async () => {
const api = new TransactionApiClient(getSelectedProfile(route)) const api = new TransactionApiClient(getSelectedProfile(route))
api.getCategoriesFlattened().then((c) => (categories.value = c)) categories.value = await api.getCategoriesFlattened()
if (model.value !== null) {
selectedCategory.value = getCategoryById(model.value)
}
}) })
function getCategoryById(id: number): TransactionCategoryTree | null { function getCategoryById(id: number): TransactionCategoryTree | null {
@ -23,23 +37,10 @@ function getCategoryById(id: number): TransactionCategoryTree | null {
} }
</script> </script>
<template> <template>
<select <VueSelect class="category-select" v-model="selectedCategory" :options="options" placeholder="Select a category"
v-model="model" @option-selected="model = selectedCategory?.id ?? null" @option-deselected="model = null">
@change="$emit('categorySelected', model === null ? null : getCategoryById(model))" <template #option="{ option }">
> {{ '&nbsp;'.repeat(option.value.depth * 4) }} {{ option.label }}
<option </template>
v-for="category in categories" </VueSelect>
:key="category.id"
:value="category.id"
>
{{ '&nbsp;'.repeat(4 * category.depth) + category.name }}
</option>
<option
v-if="required !== true"
:value="null"
:selected="model === null"
>
None
</option>
</select>
</template> </template>

View File

@ -45,7 +45,6 @@ const editing = computed(() => {
return existingTransaction.value !== null || route.meta.title === 'Edit Transaction' return existingTransaction.value !== null || route.meta.title === 'Edit Transaction'
}) })
const formValid = computed(() => { const formValid = computed(() => {
console.log('Computing if for is valid...')
return ( return (
timestamp.value.length > 0 && timestamp.value.length > 0 &&
amount.value > 0 && amount.value > 0 &&
@ -56,17 +55,18 @@ const formValid = computed(() => {
) )
}) })
const unsavedEdits = computed(() => { const unsavedEdits = computed(() => {
console.log('Computing if there are unsaved edits...') console.log("Checking if there are unsaved edits...")
if (!existingTransaction.value) return true if (!existingTransaction.value) return true
const tx = existingTransaction.value
const tagsEqual = const tagsEqual =
tags.value.every((t) => existingTransaction.value?.tags.includes(t)) && tags.value.every((t) => tx.tags.includes(t)) &&
existingTransaction.value.tags.every((t) => tags.value.includes(t)) tx.tags.every((t) => tags.value.includes(t))
let lineItemsEqual = false let lineItemsEqual = false
if (lineItems.value.length === existingTransaction.value.lineItems.length) { if (lineItems.value.length === tx.lineItems.length) {
lineItemsEqual = true lineItemsEqual = true
for (let i = 0; i < lineItems.value.length; i++) { for (let i = 0; i < lineItems.value.length; i++) {
const i1 = lineItems.value[i] const i1 = lineItems.value[i]
const i2 = existingTransaction.value.lineItems[i] const i2 = tx.lineItems[i]
if ( if (
i1.idx !== i2.idx || i1.idx !== i2.idx ||
i1.quantity !== i2.quantity || i1.quantity !== i2.quantity ||
@ -81,17 +81,37 @@ const unsavedEdits = computed(() => {
} }
const attachmentsChanged = const attachmentsChanged =
attachmentsToUpload.value.length > 0 || removedAttachmentIds.value.length > 0 attachmentsToUpload.value.length > 0 || removedAttachmentIds.value.length > 0
const timestampChanged = new Date(timestamp.value).toISOString() !== tx.timestamp
const amountChanged = amount.value * Math.pow(10, currency.value?.fractionalDigits ?? 0) !== tx.amount
const currencyChanged = currency.value?.code !== tx.currency.code
const descriptionChanged = description.value !== tx.description
const vendorChanged = vendor.value?.id !== tx.vendor?.id
const categoryChanged = categoryId.value !== (tx.category?.id ?? null)
const creditedAccountChanged = creditedAccountId.value !== (tx.creditedAccount?.id ?? null)
const debitedAccountChanged = debitedAccountId.value !== (tx.debitedAccount?.id ?? null)
console.log(`
Timestamp changed: ${timestampChanged}
Amount changed: ${amountChanged}
Currency changed: ${currencyChanged}
Description changed: ${descriptionChanged}
Vendor changed: ${vendorChanged}
Category changed: ${categoryChanged}
Credited account changed: ${creditedAccountChanged}
Debited account changed: ${debitedAccountChanged}
Tags changed: ${!tagsEqual}
Line items changed: ${!lineItemsEqual}
Attachments changed: ${attachmentsChanged}
`)
return ( return (
new Date(timestamp.value).toISOString() !== existingTransaction.value.timestamp || timestampChanged ||
amount.value * Math.pow(10, currency.value?.fractionalDigits ?? 0) !== amountChanged ||
existingTransaction.value.amount || currencyChanged ||
currency.value !== existingTransaction.value.currency || descriptionChanged ||
description.value !== existingTransaction.value.description || vendorChanged ||
(vendor.value?.id ?? null) !== (existingTransaction.value.vendor?.id ?? null) || categoryChanged ||
categoryId.value !== (existingTransaction.value.category?.id ?? null) || creditedAccountChanged ||
creditedAccountId.value !== (existingTransaction.value.creditedAccount?.id ?? null) || debitedAccountChanged ||
debitedAccountId.value !== (existingTransaction.value.debitedAccount?.id ?? null) ||
!tagsEqual || !tagsEqual ||
!lineItemsEqual || !lineItemsEqual ||
attachmentsChanged attachmentsChanged
@ -279,46 +299,20 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
<FormGroup> <FormGroup>
<!-- Basic properties --> <!-- Basic properties -->
<FormControl label="Timestamp"> <FormControl label="Timestamp">
<input <input type="datetime-local" v-model="timestamp" step="1" :disabled="loading" style="min-width: 250px" />
type="datetime-local"
v-model="timestamp"
step="1"
:disabled="loading"
style="min-width: 250px"
/>
</FormControl> </FormControl>
<FormControl label="Amount"> <FormControl label="Amount">
<input <input type="number" v-model="amount" step="0.01" min="0.01" :disabled="loading" style="max-width: 100px" />
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 <select v-model="currency" :disabled="loading || availableCurrencies.length === 1">
v-model="currency" <option v-for="currency in availableCurrencies" :key="currency.code" :value="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 <FormControl label="Description" style="min-width: 200px">
label="Description" <textarea v-model="description" :disabled="loading"></textarea>
style="min-width: 200px"
>
<textarea
v-model="description"
:disabled="loading"
></textarea>
</FormControl> </FormControl>
</FormGroup> </FormGroup>
@ -335,30 +329,16 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
<FormGroup> <FormGroup>
<!-- Accounts --> <!-- Accounts -->
<FormControl label="Credited Account"> <FormControl label="Credited Account">
<select <select v-model="creditedAccountId" :disabled="loading">
v-model="creditedAccountId" <option v-for="account in availableAccounts" :key="account.id" :value="account.id">
: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 <select v-model="debitedAccountId" :disabled="loading">
v-model="debitedAccountId" <option v-for="account in availableAccounts" :key="account.id" :value="account.id">
: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>
@ -366,44 +346,23 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
</FormControl> </FormControl>
</FormGroup> </FormGroup>
<LineItemsEditor <LineItemsEditor v-if="currency" v-model="lineItems" :currency="currency"
v-if="currency" :transaction-amount="floatMoneyToInteger(amount, currency)" />
v-model="lineItems"
:currency="currency"
:transaction-amount="floatMoneyToInteger(amount, currency)"
/>
<FormGroup> <FormGroup>
<!-- Tags --> <!-- Tags -->
<FormControl label="Tags"> <FormControl label="Tags">
<div style="margin-top: 0.5rem; margin-bottom: 0.5rem"> <div style="margin-top: 0.5rem; margin-bottom: 0.5rem">
<TagLabel <TagLabel v-for="t in tags" :key="t" :tag="t" deletable @deleted="tags = tags.filter((tg) => tg !== t)" />
v-for="t in tags"
:key="t"
:tag="t"
deletable
@deleted="tags = tags.filter((tg) => tg !== t)"
/>
</div> </div>
<div> <div>
<select v-model="selectedTagToAdd"> <select v-model="selectedTagToAdd">
<option <option v-for="tag in availableTags" :key="tag" :value="tag">
v-for="tag in availableTags"
:key="tag"
:value="tag"
>
{{ tag }} {{ tag }}
</option> </option>
</select> </select>
<input <input v-model="customTagInput" placeholder="Custom tag..." />
v-model="customTagInput" <button type="button" @click="addTag()" :disabled="selectedTagToAdd === null && !customTagInputValid">
placeholder="Custom tag..."
/>
<button
type="button"
@click="addTag()"
:disabled="selectedTagToAdd === null && !customTagInputValid"
>
Add Tag Add Tag
</button> </button>
</div> </div>
@ -412,18 +371,12 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
<FormGroup> <FormGroup>
<h5>Attachments</h5> <h5>Attachments</h5>
<FileSelector <FileSelector :initial-files="existingTransaction?.attachments ?? []"
:initial-files="existingTransaction?.attachments ?? []" v-model:uploaded-files="attachmentsToUpload" v-model:removed-files="removedAttachmentIds" />
v-model:uploaded-files="attachmentsToUpload"
v-model:removed-files="removedAttachmentIds"
/>
</FormGroup> </FormGroup>
<FormActions <FormActions @cancel="doCancel()" :disabled="loading || !formValid || !unsavedEdits"
@cancel="doCancel()" :submit-text="editing ? 'Save' : 'Add'" />
:disabled="loading || !formValid || !unsavedEdits"
:submit-text="editing ? 'Save' : 'Add'"
/>
</AppForm> </AppForm>
</AppPage> </AppPage>
</template> </template>