Reformatted source code.

This commit is contained in:
andrewlalis 2025-09-24 20:59:25 -04:00
parent 110eb2c912
commit 44ead22c4f
8 changed files with 177 additions and 64 deletions

View File

@ -12,7 +12,7 @@ defineEmits<{ categorySelected: [TransactionCategoryTree | null] }>()
const categories: Ref<TransactionCategoryTree[]> = ref([]) const categories: Ref<TransactionCategoryTree[]> = ref([])
const selectedCategory: Ref<TransactionCategoryTree | null> = ref(null) const selectedCategory: Ref<TransactionCategoryTree | null> = ref(null)
const options: Ref<Option<TransactionCategoryTree>[]> = computed(() => { const options: Ref<Option<TransactionCategoryTree>[]> = computed(() => {
return categories.value.map(c => { return categories.value.map((c) => {
return { label: c.name, value: c } return { label: c.name, value: c }
}) })
}) })
@ -37,8 +37,14 @@ function getCategoryById(id: number): TransactionCategoryTree | null {
} }
</script> </script>
<template> <template>
<VueSelect class="category-select" v-model="selectedCategory" :options="options" placeholder="Select a category" <VueSelect
@option-selected="model = selectedCategory?.id ?? null" @option-deselected="model = null"> class="category-select"
v-model="selectedCategory"
:options="options"
placeholder="Select a category"
@option-selected="model = selectedCategory?.id ?? null"
@option-deselected="model = null"
>
<template #option="{ option }"> <template #option="{ option }">
{{ '&nbsp;'.repeat(option.value.depth * 4) }} {{ option.label }} {{ '&nbsp;'.repeat(option.value.depth * 4) }} {{ option.label }}
</template> </template>

View File

@ -32,10 +32,7 @@ function canSave() {
if (!inputValid) return false if (!inputValid) return false
if (props.vendor) { if (props.vendor) {
const newDesc = description.value.trim().length === 0 ? null : description.value.trim() const newDesc = description.value.trim().length === 0 ? null : description.value.trim()
return ( return props.vendor.name.trim() !== name.value.trim() || props.vendor.description !== newDesc
props.vendor.name.trim() !== name.value.trim() ||
props.vendor.description !== newDesc
)
} }
return true return true
} }
@ -70,17 +67,31 @@ defineExpose({ show })
<AppForm> <AppForm>
<FormGroup> <FormGroup>
<FormControl label="Name"> <FormControl label="Name">
<input type="text" v-model="name" /> <input
type="text"
v-model="name"
/>
</FormControl> </FormControl>
<FormControl label="Description" style="min-width: 300px"> <FormControl
label="Description"
style="min-width: 300px"
>
<textarea v-model="description"></textarea> <textarea v-model="description"></textarea>
</FormControl> </FormControl>
</FormGroup> </FormGroup>
</AppForm> </AppForm>
</template> </template>
<template v-slot:buttons> <template v-slot:buttons>
<AppButton :disabled="!canSave()" @click="doSave()">Save</AppButton> <AppButton
<AppButton button-style="secondary" @click="modal?.close()">Cancel</AppButton> :disabled="!canSave()"
@click="doSave()"
>Save</AppButton
>
<AppButton
button-style="secondary"
@click="modal?.close()"
>Cancel</AppButton
>
</template> </template>
</ModalWrapper> </ModalWrapper>
</template> </template>

View File

@ -8,8 +8,12 @@ defineEmits<{ deleted: void }>()
<AppBadge> <AppBadge>
<span class="tag-label-hashtag">#</span> <span class="tag-label-hashtag">#</span>
{{ tag }} {{ tag }}
<font-awesome-icon v-if="deletable" icon="fa-x" class="tag-label-delete" <font-awesome-icon
@click="$emit('deleted')"></font-awesome-icon> v-if="deletable"
icon="fa-x"
class="tag-label-delete"
@click="$emit('deleted')"
></font-awesome-icon>
</AppBadge> </AppBadge>
</template> </template>
<style lang="css"> <style lang="css">

View File

@ -1,20 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { getSelectedProfile } from '@/api/profile'; import { getSelectedProfile } from '@/api/profile'
import { TransactionApiClient } from '@/api/transaction'; import { TransactionApiClient } from '@/api/transaction'
import { computed, onMounted, ref, type Ref } from 'vue'; import { computed, onMounted, ref, type Ref } from 'vue'
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router'
import TagLabel from './TagLabel.vue'; import TagLabel from './TagLabel.vue'
import AppButton from './common/AppButton.vue'; import AppButton from './common/AppButton.vue'
const route = useRoute() const route = useRoute()
const model = defineModel<string[]>({ required: true }) const model = defineModel<string[]>({ required: true })
const existingTags: Ref<string[]> = ref([]) const existingTags: Ref<string[]> = ref([])
const availableTags = computed(() => { const availableTags = computed(() => {
return existingTags.value.filter(t => !model.value.includes(t)) return existingTags.value.filter((t) => !model.value.includes(t))
}) })
const tagSelect: Ref<string | null> = ref(null) const tagSelect: Ref<string | null> = ref(null)
const enteringCustomTag = computed(() => tagSelect.value == "--CUSTOM_TAG--") const enteringCustomTag = computed(() => tagSelect.value == '--CUSTOM_TAG--')
const customTagInput: Ref<string> = ref('') const customTagInput: Ref<string> = ref('')
const customTagInputValid = computed(() => { const customTagInputValid = computed(() => {
const result = customTagInput.value.match('^[a-z0-9-_]{3,32}$') const result = customTagInput.value.match('^[a-z0-9-_]{3,32}$')
@ -44,21 +44,38 @@ function addTag() {
} }
</script> </script>
<template> <template>
<div style="margin-top: 0.5rem; margin-bottom: 0.5rem; font-weight: normal;"> <div style="margin-top: 0.5rem; margin-bottom: 0.5rem; font-weight: normal">
<TagLabel v-for="t in model" :key="t" :tag="t" deletable @deleted="model = model.filter((tg) => tg !== t)" /> <TagLabel
v-for="t in model"
:key="t"
:tag="t"
deletable
@deleted="model = model.filter((tg) => tg !== t)"
/>
</div> </div>
<div> <div>
<select v-model="tagSelect"> <select v-model="tagSelect">
<option v-for="tag in availableTags" :key="tag" :value="tag"> <option
v-for="tag in availableTags"
:key="tag"
:value="tag"
>
{{ tag }} {{ tag }}
</option> </option>
<option value="--CUSTOM_TAG--"> <option value="--CUSTOM_TAG--">Add a new tag</option>
Add a new tag
</option>
</select> </select>
<input style="margin-left: 0.25rem;" v-model="customTagInput" v-if="enteringCustomTag" <input
placeholder="Custom tag..." /> style="margin-left: 0.25rem"
<AppButton size="sm" style="margin-left: 0.25rem;" @click="addTag()" :disabled="!canAddTag"> v-model="customTagInput"
v-if="enteringCustomTag"
placeholder="Custom tag..."
/>
<AppButton
size="sm"
style="margin-left: 0.25rem"
@click="addTag()"
:disabled="!canAddTag"
>
Add Tag Add Tag
</AppButton> </AppButton>
</div> </div>

View File

@ -1,23 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TransactionVendor } from '@/api/transaction'; import type { TransactionVendor } from '@/api/transaction'
import AppButton from './common/AppButton.vue'; import AppButton from './common/AppButton.vue'
defineProps<{ vendor: TransactionVendor }>() defineProps<{ vendor: TransactionVendor }>()
defineEmits<{ 'edit': void, 'delete': void }>() defineEmits<{ edit: void; delete: void }>()
</script> </script>
<template> <template>
<div class="vendor-card"> <div class="vendor-card">
<div style="flex-shrink: 1;"> <div style="flex-shrink: 1">
<div> <div>
{{ vendor.name }} {{ vendor.name }}
</div> </div>
<div v-if="vendor.description !== null" class="font-size-small text-muted"> <div
v-if="vendor.description !== null"
class="font-size-small text-muted"
>
{{ vendor.description }} {{ vendor.description }}
</div> </div>
</div> </div>
<div> <div>
<AppButton icon="wrench" size="sm" @click="$emit('edit')"></AppButton> <AppButton
<AppButton icon="trash" size="sm" @click="$emit('delete')"></AppButton> icon="wrench"
size="sm"
@click="$emit('edit')"
></AppButton>
<AppButton
icon="trash"
size="sm"
@click="$emit('delete')"
></AppButton>
</div> </div>
</div> </div>
</template> </template>

View File

@ -56,11 +56,25 @@ async function deleteVendor(vendor: TransactionVendor) {
Vendors are businesses and other entities with which you exchange money. Adding a vendor to Vendors are businesses and other entities with which you exchange money. Adding a vendor to
Finnow allows you to track when you interact with that vendor on a transaction. Finnow allows you to track when you interact with that vendor on a transaction.
</p> </p>
<VendorCard v-for="v in vendors" :key="v.id" :vendor="v" @edit="editVendor(v)" @delete="deleteVendor(v)" /> <VendorCard
v-for="v in vendors"
:key="v.id"
:vendor="v"
@edit="editVendor(v)"
@delete="deleteVendor(v)"
/>
<ButtonBar> <ButtonBar>
<AppButton button-type="button" icon="plus" @click="addVendor()">Add Vendor</AppButton> <AppButton
button-type="button"
icon="plus"
@click="addVendor()"
>Add Vendor</AppButton
>
</ButtonBar> </ButtonBar>
<EditVendorModal ref="editVendorModal" :vendor="editedVendor" /> <EditVendorModal
ref="editVendorModal"
:vendor="editedVendor"
/>
</AppPage> </AppPage>
</template> </template>

View File

@ -55,12 +55,11 @@ const formValid = computed(() => {
) )
}) })
const unsavedEdits = computed(() => { const unsavedEdits = computed(() => {
console.log("Checking 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 tx = existingTransaction.value
const tagsEqual = const tagsEqual =
tags.value.every((t) => tx.tags.includes(t)) && tags.value.every((t) => tx.tags.includes(t)) && tx.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 === tx.lineItems.length) { if (lineItems.value.length === tx.lineItems.length) {
lineItemsEqual = true lineItemsEqual = true
@ -82,7 +81,8 @@ 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 timestampChanged = new Date(timestamp.value).toISOString() !== tx.timestamp
const amountChanged = amount.value * Math.pow(10, currency.value?.fractionalDigits ?? 0) !== tx.amount const amountChanged =
amount.value * Math.pow(10, currency.value?.fractionalDigits ?? 0) !== tx.amount
const currencyChanged = currency.value?.code !== tx.currency.code const currencyChanged = currency.value?.code !== tx.currency.code
const descriptionChanged = description.value !== tx.description const descriptionChanged = description.value !== tx.description
const vendorChanged = vendor.value?.id !== tx.vendor?.id const vendorChanged = vendor.value?.id !== tx.vendor?.id
@ -281,20 +281,46 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
<FormGroup> <FormGroup>
<!-- Basic properties --> <!-- Basic properties -->
<FormControl label="Timestamp"> <FormControl label="Timestamp">
<input type="datetime-local" v-model="timestamp" step="1" :disabled="loading" style="min-width: 250px" /> <input
type="datetime-local"
v-model="timestamp"
step="1"
:disabled="loading"
style="min-width: 250px"
/>
</FormControl> </FormControl>
<FormControl label="Amount"> <FormControl label="Amount">
<input type="number" v-model="amount" step="0.01" min="0.01" :disabled="loading" style="max-width: 100px" /> <input
type="number"
v-model="amount"
step="0.01"
min="0.01"
:disabled="loading"
style="max-width: 100px"
/>
</FormControl> </FormControl>
<FormControl label="Currency"> <FormControl label="Currency">
<select v-model="currency" :disabled="loading || availableCurrencies.length === 1"> <select
<option v-for="currency in availableCurrencies" :key="currency.code" :value="currency"> v-model="currency"
:disabled="loading || availableCurrencies.length === 1"
>
<option
v-for="currency in availableCurrencies"
:key="currency.code"
:value="currency"
>
{{ currency.code }} {{ currency.code }}
</option> </option>
</select> </select>
</FormControl> </FormControl>
<FormControl label="Description" style="min-width: 200px"> <FormControl
<textarea v-model="description" :disabled="loading"></textarea> label="Description"
style="min-width: 200px"
>
<textarea
v-model="description"
:disabled="loading"
></textarea>
</FormControl> </FormControl>
</FormGroup> </FormGroup>
@ -311,16 +337,30 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
<FormGroup> <FormGroup>
<!-- Accounts --> <!-- Accounts -->
<FormControl label="Credited Account"> <FormControl label="Credited Account">
<select v-model="creditedAccountId" :disabled="loading"> <select
<option v-for="account in availableAccounts" :key="account.id" :value="account.id"> v-model="creditedAccountId"
:disabled="loading"
>
<option
v-for="account in availableAccounts"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.numberSuffix }}) {{ account.name }} ({{ account.numberSuffix }})
</option> </option>
<option :value="null">None</option> <option :value="null">None</option>
</select> </select>
</FormControl> </FormControl>
<FormControl label="Debited Account"> <FormControl label="Debited Account">
<select v-model="debitedAccountId" :disabled="loading"> <select
<option v-for="account in availableAccounts" :key="account.id" :value="account.id"> v-model="debitedAccountId"
:disabled="loading"
>
<option
v-for="account in availableAccounts"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.numberSuffix }}) {{ account.name }} ({{ account.numberSuffix }})
</option> </option>
<option :value="null">None</option> <option :value="null">None</option>
@ -328,8 +368,12 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
</FormControl> </FormControl>
</FormGroup> </FormGroup>
<LineItemsEditor v-if="currency" v-model="lineItems" :currency="currency" <LineItemsEditor
:transaction-amount="floatMoneyToInteger(amount, currency)" /> v-if="currency"
v-model="lineItems"
:currency="currency"
:transaction-amount="floatMoneyToInteger(amount, currency)"
/>
<FormGroup> <FormGroup>
<!-- Tags --> <!-- Tags -->
@ -340,12 +384,18 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
<FormGroup> <FormGroup>
<h5>Attachments</h5> <h5>Attachments</h5>
<FileSelector :initial-files="existingTransaction?.attachments ?? []" <FileSelector
v-model:uploaded-files="attachmentsToUpload" v-model:removed-files="removedAttachmentIds" /> :initial-files="existingTransaction?.attachments ?? []"
v-model:uploaded-files="attachmentsToUpload"
v-model:removed-files="removedAttachmentIds"
/>
</FormGroup> </FormGroup>
<FormActions @cancel="doCancel()" :disabled="loading || !formValid || !unsavedEdits" <FormActions
:submit-text="editing ? 'Save' : 'Add'" /> @cancel="doCancel()"
:disabled="loading || !formValid || !unsavedEdits"
:submit-text="editing ? 'Save' : 'Add'"
/>
</AppForm> </AppForm>
</AppPage> </AppPage>
</template> </template>