finnow/web-app/src/pages/transaction-editor/EditTransactionPage.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>