Updated category select and transaction edit page.
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
bbe3e2aab5
commit
7666c0f450
|
|
@ -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 }">
|
||||||
>
|
{{ ' '.repeat(option.value.depth * 4) }} {{ option.label }}
|
||||||
<option
|
</template>
|
||||||
v-for="category in categories"
|
</VueSelect>
|
||||||
:key="category.id"
|
|
||||||
:value="category.id"
|
|
||||||
>
|
|
||||||
{{ ' '.repeat(4 * category.depth) + category.name }}
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
v-if="required !== true"
|
|
||||||
:value="null"
|
|
||||||
:selected="model === null"
|
|
||||||
>
|
|
||||||
None
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue