265 lines
8.4 KiB
Vue
265 lines
8.4 KiB
Vue
<!--
|
|
This page is quite large, and handles the form in which users can create and
|
|
edit transactions. It's accessed through two routes:
|
|
- /profiles/:profileName/transactions/:transactionId for editing
|
|
- /profiles/:profileName/add-transaction for creating a new transaction
|
|
|
|
The form consists of a few main sections:
|
|
- Standard form controls for various fields like timestamp, amount, description, etc.
|
|
- Line items table for editing the list of line items.
|
|
- Tags editor for editing the set of tags.
|
|
-->
|
|
<script setup lang="ts">
|
|
import { AccountApiClient, type Account } from '@/api/account'
|
|
import { DataApiClient, floatMoneyToInteger, type Currency } from '@/api/data'
|
|
import AppPage from '@/components/common/AppPage.vue'
|
|
import CategorySelect from '@/components/CategorySelect.vue'
|
|
import FileSelector from '@/components/common/FileSelector.vue'
|
|
import AppForm from '@/components/common/form/AppForm.vue'
|
|
import FormControl from '@/components/common/form/FormControl.vue'
|
|
import FormGroup from '@/components/common/form/FormGroup.vue'
|
|
import LineItemsEditor from '@/components/LineItemsEditor.vue'
|
|
import { computed, onMounted, ref, watch, type Ref } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import VendorSelect from '@/components/VendorSelect.vue'
|
|
import TagsSelect from '@/components/TagsSelect.vue'
|
|
import {
|
|
defaultEmptyFormFields,
|
|
DraftEditorContext,
|
|
loadEditorContextFromRoute,
|
|
NewTransactionEditorContext,
|
|
TransactionEditorContext,
|
|
type TransactionEditorAction,
|
|
type TransactionEditorContextBase,
|
|
type TransactionEditorFormFields,
|
|
} from './util'
|
|
import ButtonBar from '@/components/common/ButtonBar.vue'
|
|
import AppButton from '@/components/common/AppButton.vue'
|
|
import { showAlert } from '@/util/alert'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const accountApi = new AccountApiClient(route)
|
|
|
|
// General data used to populate form controls.
|
|
const allCurrencies: Ref<Currency[]> = ref([])
|
|
const availableCurrencies = computed(() => {
|
|
return allCurrencies.value.filter((c) =>
|
|
allAccounts.value.some((a) => a.currency.code === c.code),
|
|
)
|
|
})
|
|
const allAccounts: Ref<Account[]> = ref([])
|
|
const availableAccounts = computed(() => {
|
|
return allAccounts.value.filter((a) => a.currency.code === formData.value.currency?.code)
|
|
})
|
|
|
|
// Reactive form data:
|
|
const loading = ref(false)
|
|
const formData: Ref<TransactionEditorFormFields> = ref(defaultEmptyFormFields())
|
|
const editorContext: Ref<TransactionEditorContextBase> = ref(new NewTransactionEditorContext())
|
|
const pageTitle = computed(() => {
|
|
if (editorContext.value instanceof NewTransactionEditorContext) {
|
|
return 'Add Transaction'
|
|
} else if (editorContext.value instanceof DraftEditorContext) {
|
|
return 'Edit Draft Transaction'
|
|
} else if (editorContext.value instanceof TransactionEditorContext) {
|
|
return 'Edit Transaction'
|
|
}
|
|
return 'Edit Transaction'
|
|
})
|
|
|
|
watch(availableCurrencies, (newValue: Currency[]) => {
|
|
if (newValue.length === 1) {
|
|
formData.value.currency = newValue[0]
|
|
}
|
|
})
|
|
|
|
onMounted(async () => {
|
|
const dataClient = new DataApiClient()
|
|
|
|
// Fetch various collections of data needed for different user choices.
|
|
dataClient.getCurrencies().then((currencies) => (allCurrencies.value = currencies))
|
|
accountApi.getAccounts().then((accounts) => (allAccounts.value = accounts))
|
|
|
|
try {
|
|
editorContext.value = await loadEditorContextFromRoute(route)
|
|
} catch (err) {
|
|
console.error(err)
|
|
}
|
|
formData.value = editorContext.value.initializeFormFields(route.query)
|
|
})
|
|
|
|
function onActionClicked(action: TransactionEditorAction) {
|
|
loading.value = true
|
|
action
|
|
.callback(formData.value, route, router)
|
|
.catch((err) => {
|
|
showAlert(err)
|
|
})
|
|
.finally(() => {
|
|
loading.value = false
|
|
})
|
|
}
|
|
</script>
|
|
<template>
|
|
<AppPage :title="pageTitle">
|
|
<AppForm>
|
|
<!-- Initial draft-only form group: -->
|
|
<FormGroup v-if="editorContext instanceof DraftEditorContext">
|
|
<FormControl
|
|
label="Template Name"
|
|
hint="Add a name to this draft to turn it into a Template, which you can use when creating new transactions or scheduled transactions."
|
|
>
|
|
<input
|
|
type="text"
|
|
v-model="formData.templateName"
|
|
:disabled="loading"
|
|
style="max-width: 200px"
|
|
maxlength="32"
|
|
/>
|
|
</FormControl>
|
|
</FormGroup>
|
|
|
|
<FormGroup>
|
|
<!-- Basic properties -->
|
|
<FormControl label="Timestamp">
|
|
<input
|
|
type="datetime-local"
|
|
v-model="formData.timestamp"
|
|
step="1"
|
|
:disabled="loading"
|
|
style="min-width: 250px"
|
|
/>
|
|
</FormControl>
|
|
<FormControl label="Amount">
|
|
<input
|
|
type="number"
|
|
v-model="formData.amount"
|
|
step="0.01"
|
|
min="0.01"
|
|
:disabled="loading"
|
|
style="max-width: 100px"
|
|
/>
|
|
</FormControl>
|
|
<FormControl label="Currency">
|
|
<select
|
|
v-model="formData.currency"
|
|
:disabled="loading || availableCurrencies.length === 1"
|
|
>
|
|
<option
|
|
v-for="currency in availableCurrencies"
|
|
:key="currency.code"
|
|
:value="currency"
|
|
>
|
|
{{ currency.code }}
|
|
</option>
|
|
</select>
|
|
</FormControl>
|
|
<FormControl
|
|
label="Description"
|
|
style="min-width: 200px"
|
|
>
|
|
<textarea
|
|
v-model="formData.description"
|
|
:disabled="loading"
|
|
></textarea>
|
|
</FormControl>
|
|
</FormGroup>
|
|
|
|
<FormGroup>
|
|
<!-- Vendor & Category -->
|
|
<FormControl label="Vendor">
|
|
<VendorSelect v-model="formData.vendor" />
|
|
</FormControl>
|
|
<FormControl label="Category">
|
|
<CategorySelect v-model="formData.categoryId" />
|
|
</FormControl>
|
|
</FormGroup>
|
|
|
|
<FormGroup>
|
|
<!-- Accounts -->
|
|
<FormControl label="Credited Account">
|
|
<select
|
|
v-model="formData.creditedAccountId"
|
|
:disabled="loading"
|
|
>
|
|
<option
|
|
v-for="account in availableAccounts"
|
|
:key="account.id"
|
|
:value="account.id"
|
|
>
|
|
{{ account.name }} ({{ account.numberSuffix }})
|
|
</option>
|
|
<option :value="null">None</option>
|
|
</select>
|
|
</FormControl>
|
|
<FormControl label="Debited Account">
|
|
<select
|
|
v-model="formData.debitedAccountId"
|
|
:disabled="loading"
|
|
>
|
|
<option
|
|
v-for="account in availableAccounts"
|
|
:key="account.id"
|
|
:value="account.id"
|
|
>
|
|
{{ account.name }} ({{ account.numberSuffix }})
|
|
</option>
|
|
<option :value="null">None</option>
|
|
</select>
|
|
</FormControl>
|
|
</FormGroup>
|
|
|
|
<LineItemsEditor
|
|
v-if="formData.currency"
|
|
v-model="formData.lineItems"
|
|
:currency="formData.currency"
|
|
:transaction-amount="floatMoneyToInteger(formData.amount ?? 0, formData.currency)"
|
|
/>
|
|
|
|
<FormGroup>
|
|
<!-- Tags -->
|
|
<FormControl label="Tags">
|
|
<TagsSelect v-model="formData.tags" />
|
|
</FormControl>
|
|
</FormGroup>
|
|
|
|
<FormGroup>
|
|
<h5>Attachments</h5>
|
|
<FileSelector
|
|
:initial-files="formData.existingAttachments"
|
|
v-model:uploaded-files="formData.attachmentsToUpload"
|
|
v-model:removed-files="formData.removedAttachmentIds"
|
|
/>
|
|
</FormGroup>
|
|
|
|
<!-- One last group for less-often used fields: -->
|
|
<FormGroup>
|
|
<FormControl
|
|
label="Internal Transfer"
|
|
hint="Mark this transaction as an internal transfer to ignore it in analytics. Useful for things like credit card payments."
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
v-model="formData.internalTransfer"
|
|
:disabled="loading"
|
|
/>
|
|
</FormControl>
|
|
</FormGroup>
|
|
|
|
<!-- The set of available actions is defined by the current editor context. -->
|
|
<ButtonBar>
|
|
<AppButton
|
|
v-for="action in editorContext.getActions(formData)"
|
|
:key="action.name"
|
|
:disabled="action.disabled"
|
|
@click="onActionClicked(action)"
|
|
>
|
|
{{ action.name }}
|
|
</AppButton>
|
|
</ButtonBar>
|
|
</AppForm>
|
|
</AppPage>
|
|
</template>
|