Cleaned up line items editor with new card component.
Build and Deploy Web App / build-and-deploy (push) Successful in 18s
Details
Build and Deploy Web App / build-and-deploy (push) Successful in 18s
Details
This commit is contained in:
parent
2f1197fb92
commit
943ce13be2
|
|
@ -27,3 +27,7 @@ export function formatMoney(amount: number, currency: Currency) {
|
|||
})
|
||||
return format.format(amount / Math.pow(10, currency.fractionalDigits))
|
||||
}
|
||||
|
||||
export function floatMoneyToInteger(amount: number, currency: Currency) {
|
||||
return Math.round(amount * Math.pow(10, currency.fractionalDigits ?? 0))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
<script setup lang="ts">
|
||||
import type { TransactionDetailLineItem } from '@/api/transaction';
|
||||
import AppButton from './common/AppButton.vue';
|
||||
import { formatMoney, type Currency } from '@/api/data';
|
||||
|
||||
defineProps<{
|
||||
lineItem: TransactionDetailLineItem
|
||||
currency: Currency
|
||||
totalCount?: number
|
||||
editable?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'deleted': void,
|
||||
'movedUp': void,
|
||||
'movedDown': void
|
||||
}>()
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="line-item-card">
|
||||
<div class="line-item-card-description">
|
||||
{{ lineItem.description }}
|
||||
</div>
|
||||
<div class="line-item-card-details">
|
||||
<span class="font-mono font-size-small align-right text-muted">
|
||||
{{ lineItem.quantity }}x
|
||||
</span>
|
||||
<span class="font-mono font-size-small">
|
||||
{{ formatMoney(lineItem.valuePerItem, currency) }}
|
||||
</span>
|
||||
<AppButton icon="arrow-up" v-if="editable && lineItem.idx > 0" size="sm" @click="$emit('movedUp')" />
|
||||
<AppButton icon="arrow-down" v-if="editable && totalCount !== undefined && lineItem.idx < totalCount - 1"
|
||||
size="sm" @click="$emit('movedDown')" />
|
||||
<AppButton icon="trash" size="sm" v-if="editable" @click="$emit('deleted')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="css">
|
||||
.line-item-card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: var(--bg);
|
||||
border-radius: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.line-item-card-description {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
.line-item-card-details {
|
||||
display: inline-block;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,17 +7,28 @@ modal for adding a new one.
|
|||
import { type TransactionCategoryTree, type TransactionDetailLineItem } from '@/api/transaction';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
import FormGroup from '@/components/common/form/FormGroup.vue';
|
||||
import { formatMoney, type Currency } from '@/api/data';
|
||||
import { floatMoneyToInteger, formatMoney, type Currency } from '@/api/data';
|
||||
import ModalWrapper from '@/components/common/ModalWrapper.vue';
|
||||
import FormControl from '@/components/common/form/FormControl.vue';
|
||||
import { ref, type Ref, useTemplateRef } from '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<TransactionDetailLineItem[]>({ required: true })
|
||||
defineProps<{
|
||||
currency: Currency | null
|
||||
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)
|
||||
|
|
@ -39,13 +50,14 @@ function showAddLineItemModal() {
|
|||
}
|
||||
|
||||
async function addLineItem() {
|
||||
const idxs: number[] = model.value.map(i => i.idx)
|
||||
const newIdx = Math.max(...idxs)
|
||||
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: addLineItemValuePerItem.value,
|
||||
valuePerItem: floatMoneyToInteger(addLineItemValuePerItem.value, props.currency),
|
||||
category: selectedCategory.value
|
||||
})
|
||||
addLineItemModal.value?.close()
|
||||
|
|
@ -57,36 +69,40 @@ function removeLineItem(idx: number) {
|
|||
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>
|
||||
<FormGroup>
|
||||
<table style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Value</th>
|
||||
<th>Quantity</th>
|
||||
<th>Category</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="lineItem in model" :key="lineItem.idx">
|
||||
<td>{{ lineItem.description }}</td>
|
||||
<td style="text-align: right;">{{ currency ? formatMoney(lineItem.valuePerItem, currency) :
|
||||
lineItem.valuePerItem }}</td>
|
||||
<td style="text-align: right;">{{ lineItem.quantity }}</td>
|
||||
<td style="text-align: right;">{{ lineItem.category?.name ?? 'None' }}</td>
|
||||
<td>
|
||||
<AppButton icon="trash" @click="removeLineItem(lineItem.idx)"></AppButton>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="model.length === 0">
|
||||
<td colspan="4">No line items present.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<AppButton button-type="button" @click="showAddLineItemModal()">Add</AppButton>
|
||||
<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. -->
|
||||
|
|
@ -113,5 +129,5 @@ function removeLineItem(idx: number) {
|
|||
<AppButton button-style="secondary" @click="addLineItemModal?.close()">Cancel</AppButton>
|
||||
</template>
|
||||
</ModalWrapper>
|
||||
</FormGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const buttonStyle = computed(() => ({
|
|||
}))
|
||||
</script>
|
||||
<template>
|
||||
<button class="app-button" :class="buttonStyle" :type="type" :disabled="disabled" @click="$emit('click')">
|
||||
<button class="app-button" :class="buttonStyle" :type="type ?? 'button'" :disabled="disabled" @click="$emit('click')">
|
||||
<span v-if="icon">
|
||||
<font-awesome-icon :icon="'fa-' + icon"
|
||||
:class="{ 'app-button-icon-with-text': $slots.default !== undefined, 'app-button-icon-without-text': $slots.default === undefined }"></font-awesome-icon>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ defineProps<{ submitText?: string, cancelText?: string, disabled?: boolean }>()
|
|||
</script>
|
||||
<template>
|
||||
<div class="app-form-actions">
|
||||
<AppButton button-type="submit" :disabled="disabled ?? false">{{ submitText ?? 'Submit' }}</AppButton>
|
||||
<AppButton button-style="secondary" @click="$emit('cancel')">{{ cancelText ?? 'Cancel'
|
||||
<AppButton type="submit" :disabled="disabled ?? false">{{ submitText ?? 'Submit' }}</AppButton>
|
||||
<AppButton theme="secondary" @click="$emit('cancel')">{{ cancelText ?? 'Cancel'
|
||||
}}</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { showAlert, showConfirm } from '@/util/alert';
|
|||
import { onMounted, ref, type Ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import AttachmentRow from '@/components/common/AttachmentRow.vue';
|
||||
import LineItemCard from '@/components/LineItemCard.vue';
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
|
@ -97,26 +98,8 @@ async function deleteTransaction() {
|
|||
|
||||
<div v-if="transaction.lineItems.length > 0">
|
||||
<h3>Line Items</h3>
|
||||
<table class="app-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Amount per Item</th>
|
||||
<th>Quantity</th>
|
||||
<th>Description</th>
|
||||
<th>Category</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="i in transaction.lineItems" :key="i.idx">
|
||||
<td>{{ i.idx + 1 }}</td>
|
||||
<td>{{ formatMoney(i.valuePerItem, transaction.currency) }}</td>
|
||||
<td>{{ i.quantity }}</td>
|
||||
<td>{{ i.description }}</td>
|
||||
<td>{{ i.category?.name }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<LineItemCard v-for="item of transaction.lineItems" :key="item.idx" :line-item="item"
|
||||
:currency="transaction.currency" :total-count="transaction.lineItems.length" :editable="false" />
|
||||
</div>
|
||||
|
||||
<div v-if="transaction.attachments.length > 0">
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ 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 { DataApiClient, floatMoneyToInteger, type Currency } from '@/api/data';
|
||||
import { getSelectedProfile } from '@/api/profile';
|
||||
import { TransactionApiClient, type AddTransactionPayload, type TransactionDetail, type TransactionDetailLineItem, type TransactionVendor } from '@/api/transaction';
|
||||
import AppPage from '@/components/common/AppPage.vue';
|
||||
|
|
@ -116,11 +116,14 @@ onMounted(async () => {
|
|||
* created.
|
||||
*/
|
||||
async function doSubmit() {
|
||||
if (currency.value === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const localDate = new Date(timestamp.value)
|
||||
const scaledAmount = Math.round(amount.value * Math.pow(10, currency.value?.fractionalDigits ?? 0))
|
||||
const payload: AddTransactionPayload = {
|
||||
timestamp: localDate.toISOString(),
|
||||
amount: scaledAmount,
|
||||
amount: floatMoneyToInteger(amount.value, currency.value),
|
||||
currencyCode: currency.value?.code ?? '',
|
||||
description: description.value,
|
||||
vendorId: vendorId.value,
|
||||
|
|
@ -309,7 +312,8 @@ function isEdited() {
|
|||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<LineItemsEditor v-model="lineItems" :currency="currency" />
|
||||
<LineItemsEditor v-if="currency" v-model="lineItems" :currency="currency"
|
||||
:transaction-amount="floatMoneyToInteger(amount, currency)" />
|
||||
|
||||
<FormGroup>
|
||||
<!-- Tags -->
|
||||
|
|
|
|||
Loading…
Reference in New Issue