Added category select.
This commit is contained in:
parent
ae53d7423c
commit
ca87d2723c
|
|
@ -165,6 +165,20 @@ export class TransactionApiClient extends ApiClient {
|
|||
return super.getJson(this.path + '/categories')
|
||||
}
|
||||
|
||||
async getCategoriesFlattened(): Promise<TransactionCategoryTree[]> {
|
||||
const categories = await this.getCategories()
|
||||
const flat: TransactionCategoryTree[] = []
|
||||
await this.flattenCategories(flat, categories)
|
||||
return flat
|
||||
}
|
||||
|
||||
private flattenCategories(arr: TransactionCategoryTree[], tree: TransactionCategoryTree[]) {
|
||||
for (const category of tree) {
|
||||
arr.push(category)
|
||||
this.flattenCategories(arr, category.children)
|
||||
}
|
||||
}
|
||||
|
||||
getCategory(id: number): Promise<TransactionCategory> {
|
||||
return super.getJson(this.path + '/categories/' + id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
import { TransactionApiClient, type TransactionCategoryTree } from '@/api/transaction';
|
||||
import { onMounted, ref, type Ref } from 'vue';
|
||||
|
||||
|
||||
const model = defineModel<number | null>({ required: true })
|
||||
defineProps<{ required?: boolean }>()
|
||||
defineEmits<{ categorySelected: [TransactionCategoryTree | null] }>()
|
||||
const categories: Ref<TransactionCategoryTree[]> = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
const api = new TransactionApiClient()
|
||||
api.getCategoriesFlattened()
|
||||
.then(c => categories.value = c)
|
||||
})
|
||||
|
||||
function getCategoryById(id: number): TransactionCategoryTree | null {
|
||||
for (const c of categories.value) {
|
||||
if (c.id === id) return c
|
||||
}
|
||||
return null
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<select v-model="model" @change="$emit('categorySelected', model === null ? null : getCategoryById(model))">
|
||||
<option v-for="category in categories" :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>
|
||||
|
|
@ -6,6 +6,7 @@ import AppForm from './form/AppForm.vue';
|
|||
import FormGroup from './form/FormGroup.vue';
|
||||
import FormControl from './form/FormControl.vue';
|
||||
import AppButton from './AppButton.vue';
|
||||
import CategorySelect from './CategorySelect.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
category?: TransactionCategory
|
||||
|
|
@ -24,11 +25,16 @@ function show(): Promise<string | undefined> {
|
|||
name.value = props.category?.name ?? ''
|
||||
description.value = props.category?.description ?? ''
|
||||
if (props.category) {
|
||||
name.value = props.category.name
|
||||
description.value = props.category.description
|
||||
color.value = '#' + props.category.color
|
||||
parentId.value = props.category.parentId
|
||||
} else {
|
||||
name.value = ''
|
||||
description.value = ''
|
||||
color.value = '#ffffff'
|
||||
parentId.value = null
|
||||
}
|
||||
parentId.value = props.category?.parentId ?? null
|
||||
return modal.value.show()
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +91,9 @@ defineExpose({ show })
|
|||
<FormControl label="Description" style="min-width: 300px;">
|
||||
<textarea v-model="description"></textarea>
|
||||
</FormControl>
|
||||
<FormControl label="Parent Category">
|
||||
<CategorySelect v-model="parentId"></CategorySelect>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</AppForm>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -11,20 +11,20 @@ import { formatMoney, type Currency } from '@/api/data';
|
|||
import ModalWrapper from './ModalWrapper.vue';
|
||||
import FormControl from './form/FormControl.vue';
|
||||
import { ref, type Ref, useTemplateRef } from 'vue';
|
||||
import CategorySelect from './CategorySelect.vue';
|
||||
|
||||
const model = defineModel<TransactionDetailLineItem[]>({ required: true })
|
||||
defineProps<{
|
||||
currency: Currency | null,
|
||||
categories: TransactionCategoryTree[]
|
||||
currency: Currency | null
|
||||
}>()
|
||||
|
||||
const addLineItemDescription = ref('')
|
||||
const addLineItemValuePerItem = ref(0)
|
||||
const addLineItemQuantity = ref(0)
|
||||
const addLineItemCategory: Ref<TransactionCategoryTree | null> = ref(null)
|
||||
const addLineItemCategoryId: Ref<number | null> = ref(null)
|
||||
const selectedCategory: Ref<TransactionCategoryTree | null> = ref(null)
|
||||
const addLineItemModal = useTemplateRef('addLineItemModal')
|
||||
|
||||
|
||||
function canAddLineItem() {
|
||||
return addLineItemDescription.value.length > 0 &&
|
||||
addLineItemQuantity.value > 0
|
||||
|
|
@ -34,11 +34,11 @@ function showAddLineItemModal() {
|
|||
addLineItemDescription.value = ''
|
||||
addLineItemValuePerItem.value = 1.00
|
||||
addLineItemQuantity.value = 1
|
||||
addLineItemCategory.value = null
|
||||
addLineItemCategoryId.value = null
|
||||
addLineItemModal.value?.show()
|
||||
}
|
||||
|
||||
function addLineItem() {
|
||||
async function addLineItem() {
|
||||
const idxs: number[] = model.value.map(i => i.idx)
|
||||
const newIdx = Math.max(...idxs)
|
||||
model.value.push({
|
||||
|
|
@ -46,7 +46,7 @@ function addLineItem() {
|
|||
description: addLineItemDescription.value,
|
||||
quantity: addLineItemQuantity.value,
|
||||
valuePerItem: addLineItemValuePerItem.value,
|
||||
category: addLineItemCategory.value
|
||||
category: selectedCategory.value
|
||||
})
|
||||
addLineItemModal.value?.close()
|
||||
}
|
||||
|
|
@ -104,12 +104,7 @@ function removeLineItem(idx: number) {
|
|||
<input type="number" step="1" min="1" v-model="addLineItemQuantity" />
|
||||
</FormControl>
|
||||
<FormControl label="Category">
|
||||
<select v-model="addLineItemCategory">
|
||||
<option v-for="category in categories" :key="category.id" :value="category">
|
||||
{{ " ".repeat(4 * category.depth) + category.name }}
|
||||
</option>
|
||||
<option :value="null">None</option>
|
||||
</select>
|
||||
<CategorySelect v-model="addLineItemCategoryId" @category-selected="c => selectedCategory = c" />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ async function deleteCategory(categoryId: number) {
|
|||
|
||||
async function addCategory() {
|
||||
editedCategory.value = undefined
|
||||
await nextTick()
|
||||
const result = await editCategoryModal.value?.show()
|
||||
if (result === 'saved') {
|
||||
await loadCategories()
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ The form consists of a few main sections:
|
|||
<script setup lang="ts">
|
||||
import { AccountApiClient, type Account } from '@/api/account';
|
||||
import { DataApiClient, type Currency } from '@/api/data';
|
||||
import { TransactionApiClient, type AddTransactionPayload, type TransactionCategoryTree, type TransactionDetail, type TransactionDetailLineItem, type TransactionVendor } from '@/api/transaction';
|
||||
import { TransactionApiClient, type AddTransactionPayload, type TransactionDetail, type TransactionDetailLineItem, type TransactionVendor } from '@/api/transaction';
|
||||
import AppPage from '@/components/AppPage.vue';
|
||||
import CategorySelect from '@/components/CategorySelect.vue';
|
||||
import AppForm from '@/components/form/AppForm.vue';
|
||||
import FormActions from '@/components/form/FormActions.vue';
|
||||
import FormControl from '@/components/form/FormControl.vue';
|
||||
|
|
@ -38,7 +39,6 @@ const availableCurrencies = computed(() => {
|
|||
return allCurrencies.value.filter(c => allAccounts.value.some(a => a.currency === c.code))
|
||||
})
|
||||
const availableVendors: Ref<TransactionVendor[]> = ref([])
|
||||
const availableCategories: Ref<TransactionCategoryTree[]> = ref([])
|
||||
const allAccounts: Ref<Account[]> = ref([])
|
||||
const availableAccounts = computed(() => {
|
||||
return allAccounts.value.filter(a => a.currency === currency.value?.code)
|
||||
|
|
@ -77,19 +77,13 @@ watch(availableCurrencies, (newValue: Currency[]) => {
|
|||
onMounted(async () => {
|
||||
if (!profileStore.state) return
|
||||
const dataClient = new DataApiClient()
|
||||
const transactionClient = new TransactionApiClient(profileStore.state)
|
||||
const transactionClient = new TransactionApiClient()
|
||||
const accountClient = new AccountApiClient(profileStore.state)
|
||||
|
||||
// Fetch various collections of data needed for different user choices.
|
||||
dataClient.getCurrencies().then(currencies => allCurrencies.value = currencies)
|
||||
transactionClient.getVendors().then(vendors => availableVendors.value = vendors)
|
||||
transactionClient.getAllTags().then(t => allTags.value = t)
|
||||
transactionClient.getCategories().then(categories => {
|
||||
// Flatten the recursive list of categories.
|
||||
const flattened: TransactionCategoryTree[] = []
|
||||
flattenCategories(flattened, categories)
|
||||
availableCategories.value = flattened
|
||||
})
|
||||
accountClient.getAccounts().then(accounts => allAccounts.value = accounts)
|
||||
|
||||
|
||||
|
|
@ -108,13 +102,6 @@ onMounted(async () => {
|
|||
}
|
||||
})
|
||||
|
||||
function flattenCategories(arr: TransactionCategoryTree[], tree: TransactionCategoryTree[]) {
|
||||
for (const category of tree) {
|
||||
arr.push(category)
|
||||
flattenCategories(arr, category.children)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the transaction. If the user is editing an existing transaction,
|
||||
* then that transaction will be updated. Otherwise, a new transaction is
|
||||
|
|
@ -140,7 +127,7 @@ async function doSubmit() {
|
|||
})
|
||||
}
|
||||
|
||||
const transactionApi = new TransactionApiClient(profileStore.state)
|
||||
const transactionApi = new TransactionApiClient()
|
||||
let savedTransaction = null
|
||||
try {
|
||||
loading.value = true
|
||||
|
|
@ -291,12 +278,7 @@ function isEdited() {
|
|||
</select>
|
||||
</FormControl>
|
||||
<FormControl label="Category">
|
||||
<select v-model="categoryId" :disabled="loading">
|
||||
<option v-for="category in availableCategories" :key="category.id" :value="category.id">
|
||||
{{ " ".repeat(4 * category.depth) + category.name }}
|
||||
</option>
|
||||
<option :value="null" :selected="categoryId === null">None</option>
|
||||
</select>
|
||||
<CategorySelect v-model="categoryId" />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
|
|
@ -320,7 +302,7 @@ function isEdited() {
|
|||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<LineItemsEditor v-model="lineItems" :currency="currency" :categories="availableCategories" />
|
||||
<LineItemsEditor v-model="lineItems" :currency="currency" />
|
||||
|
||||
<FormGroup>
|
||||
<!-- Tags -->
|
||||
|
|
|
|||
Loading…
Reference in New Issue