finnow/web-app/src/components/LineItemsEditor.vue

166 lines
4.9 KiB
Vue

<!--
Part of the EditTransactionPage which controls the list of line items for the
transaction. This editor shows a table of current line items, and includes a
modal for adding a new one.
-->
<script setup lang="ts">
import { type TransactionCategoryTree, type TransactionLineItemResponse } from '@/api/transaction'
import AppButton from '@/components/common/AppButton.vue'
import FormGroup from '@/components/common/form/FormGroup.vue'
import { floatMoneyToInteger, formatMoney, type Currency } from '@/api/data'
import ModalWrapper from '@/components/common/ModalWrapper.vue'
import FormControl from '@/components/common/form/FormControl.vue'
import { computed, ref, type Ref, useTemplateRef } from 'vue'
import CategorySelect from './CategorySelect.vue'
import LineItemCard from './LineItemCard.vue'
import AppBadge from './common/AppBadge.vue'
const model = defineModel<TransactionLineItemResponse[]>({ required: true })
const props = defineProps<{
transactionAmount: number
currency: Currency
}>()
const computedTotal = computed(() => {
let sum = 0
for (const item of model.value) {
sum += item.quantity * item.valuePerItem
}
return sum
})
const addLineItemDescription = ref('')
const addLineItemValuePerItem = ref(0)
const addLineItemQuantity = ref(0)
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
}
function showAddLineItemModal() {
addLineItemDescription.value = ''
addLineItemValuePerItem.value = 1.0
addLineItemQuantity.value = 1
addLineItemCategoryId.value = null
addLineItemModal.value?.show()
}
async function addLineItem() {
const newIdx = model.value.length === 0 ? 0 : Math.max(...model.value.map((i) => i.idx)) + 1
model.value.push({
idx: newIdx,
description: addLineItemDescription.value,
quantity: addLineItemQuantity.value,
valuePerItem: floatMoneyToInteger(addLineItemValuePerItem.value, props.currency),
category: selectedCategory.value,
})
addLineItemModal.value?.close()
}
function removeLineItem(idx: number) {
model.value = model.value.filter((i) => i.idx !== idx)
for (let i = 0; i < model.value.length; i++) {
model.value[i].idx = i
}
}
function moveItemUp(idx: number) {
if (idx <= 0) return
const item = model.value[idx]
model.value[idx] = model.value[idx - 1]
model.value[idx - 1] = item
for (let i = 0; i < model.value.length; i++) {
model.value[i].idx = i
}
}
function moveItemDown(idx: number) {
if (idx >= model.value.length - 1) return
const item = model.value[idx]
model.value[idx] = model.value[idx + 1]
model.value[idx + 1] = item
for (let i = 0; i < model.value.length; i++) {
model.value[i].idx = i
}
}
</script>
<template>
<div>
<LineItemCard
v-for="item in model"
:key="item.idx"
:line-item="item"
:currency="currency"
:total-count="model.length"
editable
@deleted="removeLineItem(item.idx)"
@moved-up="moveItemUp(item.idx)"
@moved-down="moveItemDown(item.idx)"
/>
<div>
<AppButton
icon="plus"
@click="showAddLineItemModal()"
>Add Line Item</AppButton
>
<AppBadge
v-if="model.length > 0"
:class="{
'text-positive': computedTotal === transactionAmount,
'text-negative': computedTotal !== transactionAmount,
}"
>
Items Total: {{ formatMoney(computedTotal, currency) }}
</AppBadge>
</div>
<!-- Modal for adding a new line item. -->
<ModalWrapper ref="addLineItemModal">
<template v-slot:default>
<h3>Add Line Item</h3>
<FormGroup>
<FormControl label="Description">
<textarea v-model="addLineItemDescription"></textarea>
</FormControl>
<FormControl label="Value Per Item">
<input
type="number"
step="0.01"
v-model="addLineItemValuePerItem"
/>
</FormControl>
<FormControl label="Quantity">
<input
type="number"
step="1"
min="1"
v-model="addLineItemQuantity"
/>
</FormControl>
<FormControl label="Category">
<CategorySelect
v-model="addLineItemCategoryId"
@category-selected="(c) => (selectedCategory = c)"
/>
</FormControl>
</FormGroup>
</template>
<template v-slot:buttons>
<AppButton
@click="addLineItem()"
:disabled="!canAddLineItem()"
>Add</AppButton
>
<AppButton
button-style="secondary"
@click="addLineItemModal?.close()"
>Cancel</AppButton
>
</template>
</ModalWrapper>
</div>
</template>