Add title sections to invoices backend and frontend, #41
This commit is contained in:
@@ -296,6 +296,7 @@ const save = async () => {
|
||||
items: invoice.value.items.map(item => ({
|
||||
id: item.id, // Include ID for existing items
|
||||
position: item.position,
|
||||
isSection: item.isSection,
|
||||
type: item.type,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
@@ -323,6 +324,7 @@ const save = async () => {
|
||||
} finally {
|
||||
// remove spinner from save button
|
||||
isSaving.value = false
|
||||
setTimeout(() => { isDirty.value = false }, 1000)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -47,13 +47,23 @@ watch(() => props.lineItems, (newLineItems) => {
|
||||
items.value = (newLineItems ?? []).slice().sort(function (a, b) { return a.position - b.position })
|
||||
}, { deep: true, once: true })
|
||||
|
||||
const newItem = () => {
|
||||
const position = items.value.length + 1
|
||||
let item = newLineItem()
|
||||
item.position = position
|
||||
const newItem = (isSection: boolean) => {
|
||||
let item = newLineItem(isSection)
|
||||
item.position = getNextPosition(items.value.length, isSection)
|
||||
items.value.push(item)
|
||||
}
|
||||
|
||||
const getNextPosition = (index: number, isSection: boolean) => {
|
||||
let lastPosition = (index == 0) ? 0 : items.value.at(index - 1)?.position ?? 0
|
||||
let nextInteger = (lastPosition == Math.ceil(lastPosition)) ? lastPosition + 1 : Math.ceil(lastPosition)
|
||||
|
||||
// item: next integer
|
||||
// sections: middle between last element and next integer
|
||||
return isSection ?
|
||||
lastPosition + (nextInteger - lastPosition) / 2
|
||||
: nextInteger
|
||||
}
|
||||
|
||||
const deleteItem = (lineItem: LineItem) => {
|
||||
const index = items.value.indexOf(lineItem)
|
||||
if (index > -1) {
|
||||
@@ -64,7 +74,7 @@ const deleteItem = (lineItem: LineItem) => {
|
||||
|
||||
const recalculatePositions = () => {
|
||||
for (let i = 0; i < items.value.length; i++) {
|
||||
items.value[i].position = i + 1
|
||||
items.value[i].position = getNextPosition(i, items.value[i].isSection)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +109,32 @@ const recalculatePositions = () => {
|
||||
@end="recalculatePositions">
|
||||
<template #item="{ element }">
|
||||
|
||||
<TableRow>
|
||||
<TableCell class="px-0 handle px-1 cursor-move w-6">
|
||||
<GripVertical :size="18" :strokeWidth="1.5" class="text-muted-foreground" />
|
||||
<TableRow v-if="element.isSection">
|
||||
<TableCell class="handle px-1 cursor-move w-6">
|
||||
<GripVertical :size="18" stroke-width="1.5" class="text-muted-foreground" />
|
||||
</TableCell>
|
||||
|
||||
<!-- Posten -->
|
||||
<TableCell colspan="6" class="pt-6">
|
||||
<Input v-model="element.title" placeholder="Titel"
|
||||
class="font-bold text-base! p-1 h-7! m-0 -mb-1 bg-transparent dark:bg-transparent hover:bg-background/66 dark:hover:bg-background/66 border-none hover:border-1 dark:hover:border-1 placeholder:text-muted-foreground/30 shadow-none" />
|
||||
<GrowingTextarea v-model="element.description" placeholder="Text"
|
||||
class="font-light bg-transparent dark:bg-transparent hover:bg-background/66 dark:hover:bg-background/66 py-0 px-1 m-0 border-none shadow-none" />
|
||||
</TableCell>
|
||||
|
||||
|
||||
<!-- Buttons -->
|
||||
<TableCell class="w-8 text-right px-1">
|
||||
<Button variant="ghost" size="sm" @click="deleteItem(element)"
|
||||
class="has-[>svg]:px-1 text-muted-foreground hover:text-destructive">
|
||||
<Trash2 :size="18" stroke-width="1.5"/>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow v-else>
|
||||
<TableCell class="handle px-1 cursor-move w-6">
|
||||
<GripVertical :size="18" stroke-width="1.5" class="text-muted-foreground" />
|
||||
</TableCell>
|
||||
|
||||
<!-- Pos. -->
|
||||
@@ -159,14 +192,11 @@ const recalculatePositions = () => {
|
||||
</TableCell>
|
||||
|
||||
<!-- Buttons -->
|
||||
<TableCell class="w-8 text-right">
|
||||
<TableCell class="w-8 text-right px-1">
|
||||
<Button variant="ghost" size="sm" @click="deleteItem(element)"
|
||||
class="has-[>svg]:px-1 text-muted-foreground hover:text-destructive">
|
||||
<Trash2 :size="18" />
|
||||
<Trash2 :size="18" stroke-width="1.5"/>
|
||||
</Button>
|
||||
<!-- <Button :variant="'ghost'" :size="'sm'" @click="" class="has-[>svg]:px-1 text-muted-foreground">
|
||||
<CirclePlus />
|
||||
</Button> -->
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -175,12 +205,15 @@ const recalculatePositions = () => {
|
||||
|
||||
<TableFooter v-if="items.length >= 1" class="bg-transparent">
|
||||
<TableRow class="hover:bg-transparent dark:hover:bg-transparent">
|
||||
<TableCell colspan="8" class="text-center">
|
||||
|
||||
<Button class="mt-4" variant="ghost" @click="newItem">
|
||||
<Plus /> Neue Zeile
|
||||
</Button>
|
||||
|
||||
<TableCell colspan="8">
|
||||
<div class="flex gap-2 justify-center">
|
||||
<Button class="mt-4" variant="ghost" @click="newItem(true)">
|
||||
<Plus /> Neuer Abschnitt
|
||||
</Button>
|
||||
<Button class="mt-4" variant="ghost" @click="newItem(false)">
|
||||
<Plus /> Neue Zeile
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
@@ -197,9 +230,14 @@ const recalculatePositions = () => {
|
||||
<EmptyTitle>Diese Rechnung ist leer</EmptyTitle>
|
||||
<EmptyDescription>Erstelle hier deinen ersten Posten</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<Button variant="action" @click="newItem">
|
||||
<Plus /> Neue Zeile
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="action" @click="newItem(true)">
|
||||
<Plus stroke-width="1.5" /> Neuer Abschnitt
|
||||
</Button>
|
||||
<Button variant="action" @click="newItem(false)">
|
||||
<Plus stroke-width="1.5" /> Neue Zeile
|
||||
</Button>
|
||||
</div>
|
||||
</Empty>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user