3. Zwischenstand

This commit is contained in:
2026-03-22 13:53:29 +01:00
parent 03bcc1a939
commit 9f665fc3b8
26 changed files with 2037 additions and 583 deletions

View File

@@ -1,6 +1,7 @@
<script setup>
import InputGroup from "~/components/InputGroup.vue";
import dayjs from "dayjs";
import { parseDate } from "@internationalized/date"
import { useDraggable } from '@vueuse/core'
// --- Standard Setup & Data ---
@@ -44,6 +45,9 @@ const costcentres = ref([])
const vendors = ref([])
const accounts = ref([])
const loadedFileId = ref(null)
const invoiceFiles = ref([])
const paymentTypeItems = ['Überweisung', 'Lastschrift', 'Kreditkarte', 'PayPal', 'Bar', 'Sonstiges']
const files = useFiles()
const setup = async () => {
// 1. Daten laden
@@ -67,7 +71,9 @@ const setup = async () => {
// Datei laden
if (itemInfo.value.files && itemInfo.value.files.length > 0) {
loadedFileId.value = itemInfo.value.files[itemInfo.value.files.length-1].id
invoiceFiles.value = await files.selectSomeDocuments(itemInfo.value.files.map((file) => file.id))
const latestPdf = [...invoiceFiles.value].reverse().find((file) => file?.path?.toLowerCase().includes('.pdf'))
loadedFileId.value = latestPdf?.id || null
}
if(itemInfo.value.date && !itemInfo.value.dueDate) itemInfo.value.dueDate = itemInfo.value.date
@@ -98,6 +104,23 @@ const taxOptions = ref([
{ label: "Keine USt", percentage: 0, key: "null" },
])
const getCalendarValue = (value) => {
if (!value) return undefined
const formatted = dayjs(value).format('YYYY-MM-DD')
return formatted ? parseDate(formatted) : undefined
}
const setDateField = (field, value) => {
itemInfo.value[field] = value ? dayjs(value.toString()).toDate() : null
}
const setDateFieldToToday = (field) => {
itemInfo.value[field] = dayjs().toDate()
}
const getDateButtonLabel = (value, emptyLabel = "Kein Datum") => value ? dayjs(value).format('DD.MM.YYYY') : emptyLabel
const totalCalculated = computed(() => {
let totalNet = 0
let totalAmount19Tax = 0
@@ -335,18 +358,18 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<USelectMenu
class="w-full"
v-model="itemInfo.vendor"
:options="vendors"
option-attribute="name"
value-attribute="id"
searchable
:items="vendors"
label-key="name"
value-key="id"
:search-input="{ placeholder: 'Lieferant suchen...' }"
:disabled="mode === 'show'"
:search-attributes="['name', 'vendorNumber']"
placeholder="Lieferant suchen..."
:filter-fields="['name', 'vendorNumber']"
:color="itemInfo.vendor ? 'primary' : 'error'"
>
<template #label>
<template #default>
{{ vendors.find(v => v.id === itemInfo.vendor)?.name || 'Bitte wählen' }}
</template>
<template #option="{ option }">
<template #item="{ item: option }">
<span class="font-mono text-xs opacity-75 mr-2">{{ option.vendorNumber }}</span> {{ option.name }}
</template>
</USelectMenu>
@@ -368,33 +391,81 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
</UFormField>
<UFormField label="Rechnungsnummer">
<UInput v-model="itemInfo.reference" icon="i-heroicons-hashtag" :disabled="mode === 'show'" />
<UInput class="w-full" v-model="itemInfo.reference" icon="i-heroicons-hashtag" :disabled="mode === 'show'" :color="itemInfo.reference ? 'primary' : 'error'" />
</UFormField>
<UFormField label="Zahlart">
<USelectMenu v-model="itemInfo.paymentType" :options="['Überweisung', 'Lastschrift', 'Kreditkarte', 'PayPal', 'Bar', 'Sonstiges']" :disabled="mode === 'show'" />
<USelectMenu v-model="itemInfo.paymentType" :items="paymentTypeItems" :disabled="mode === 'show'" class="w-full" />
</UFormField>
<UFormField label="Rechnungsdatum">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton block color="white" icon="i-heroicons-calendar" :label="itemInfo.date ? dayjs(itemInfo.date).format('DD.MM.YYYY') : '-'" :disabled="mode === 'show'" />
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.date" @close="() => { if(!itemInfo.dueDate) itemInfo.dueDate = itemInfo.date; close() }" />
<UPopover class="w-full" :content="{ side: 'bottom', align: 'start' }">
<UButton
block
icon="i-heroicons-calendar"
:label="getDateButtonLabel(itemInfo.date)"
:disabled="mode === 'show'"
:color="itemInfo.date ? 'neutral' : 'error'"
variant="outline"
class="w-full justify-start"
/>
<template #content>
<div class="p-2">
<UCalendar
:model-value="getCalendarValue(itemInfo.date)"
@update:model-value="(value) => { setDateField('date', value); if (!itemInfo.dueDate) itemInfo.dueDate = itemInfo.date }"
:week-starts-on="1"
/>
<div class="flex justify-end px-2 pb-2">
<UButton
size="xs"
color="gray"
variant="soft"
icon="i-heroicons-calendar-days"
label="Heute"
@click="() => { setDateFieldToToday('date'); if (!itemInfo.dueDate) itemInfo.dueDate = itemInfo.date }"
/>
</div>
</div>
</template>
</UPopover>
</UFormField>
<UFormField label="Fälligkeitsdatum">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton block color="white" icon="i-heroicons-calendar" :label="itemInfo.dueDate ? dayjs(itemInfo.dueDate).format('DD.MM.YYYY') : '-'" :disabled="mode === 'show'" />
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.dueDate" @close="close" />
<UPopover class="w-full" :content="{ side: 'bottom', align: 'start' }">
<UButton
block
icon="i-heroicons-calendar"
:label="getDateButtonLabel(itemInfo.dueDate)"
:disabled="mode === 'show'"
:color="itemInfo.dueDate ? 'neutral' : 'error'"
variant="outline"
class="w-full justify-start"
/>
<template #content>
<div class="p-2">
<UCalendar
:model-value="getCalendarValue(itemInfo.dueDate)"
@update:model-value="(value) => setDateField('dueDate', value)"
:week-starts-on="1"
/>
<div class="flex justify-end px-2 pb-2">
<UButton
size="xs"
color="gray"
variant="soft"
icon="i-heroicons-calendar-days"
label="Heute"
@click="setDateFieldToToday('dueDate')"
/>
</div>
</div>
</template>
</UPopover>
</UFormField>
<UFormField label="Beschreibung / Notiz" class="md:col-span-2">
<UTextarea v-model="itemInfo.description" :rows="2" autoresize :disabled="mode === 'show'" />
<UTextarea class="w-full" v-model="itemInfo.description" :rows="2" autoresize :disabled="mode === 'show'" />
</UFormField>
</div>
</UCard>
@@ -430,19 +501,20 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<div class="col-span-12 md:col-span-6">
<UFormField label="Konto / Kategorie">
<USelectMenu
class="w-full"
v-model="item.account"
:options="accounts"
searchable
placeholder="Kategorie wählen"
option-attribute="label"
value-attribute="id"
:items="accounts"
:search-input="{ placeholder: 'Kategorie wählen' }"
label-key="label"
value-key="id"
:disabled="mode === 'show'"
:search-attributes="['label', 'number']"
:filter-fields="['label', 'number']"
:color="item.account ? 'primary' : 'error'"
>
<template #option="{ option }">
<template #item="{ item: option }">
<span class="font-mono text-xs text-gray-500 mr-2">{{ option.number }}</span> {{ option.label }}
</template>
<template #label>
<template #default>
{{ accounts.find(a => a.id === item.account)?.label || 'Auswählen' }}
</template>
</USelectMenu>
@@ -452,15 +524,15 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<div class="col-span-12 md:col-span-6">
<UFormField label="Kostenstelle">
<USelectMenu
class="w-full"
v-model="item.costCentre"
:options="costcentres"
searchable
option-attribute="name"
value-attribute="id"
placeholder="Optional"
:items="costcentres"
:search-input="{ placeholder: 'Optional' }"
label-key="name"
value-key="id"
:disabled="mode === 'show'"
>
<template #label>
<template #default>
{{ costcentres.find(c => c.id === item.costCentre)?.name || 'Keine' }}
</template>
</USelectMenu>
@@ -470,6 +542,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<div class="col-span-12 md:col-span-3">
<UFormField label="Betrag (Netto)">
<UInput
class="w-full"
type="number"
step="0.01"
:disabled="mode === 'show' || !useNetMode"
@@ -484,6 +557,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<div class="col-span-12 md:col-span-3">
<UFormField label="Betrag (Brutto)">
<UInput
class="w-full"
type="number"
step="0.01"
:disabled="mode === 'show' || useNetMode"
@@ -498,19 +572,21 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<div class="col-span-6 md:col-span-3">
<UFormField label="Steuerschlüssel">
<USelectMenu
class="w-full"
v-model="item.taxType"
:options="taxOptions"
value-attribute="key"
option-attribute="label"
:items="taxOptions"
value-key="key"
label-key="label"
:disabled="mode === 'show'"
@change="recalculateItem(item, 'taxType')"
@update:model-value="recalculateItem(item, 'taxType')"
:color="item.taxType ? 'primary' : 'error'"
/>
</UFormField>
</div>
<div class="col-span-6 md:col-span-3">
<UFormField label="Steuerbetrag" help="Automatisch berechnet">
<UInput :model-value="item.amountTax" disabled color="gray" >
<UInput class="w-full" :model-value="item.amountTax" disabled color="gray" >
<template #trailing>€</template>
</UInput>
</UFormField>
@@ -538,7 +614,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
</div>
<div class="col-span-12">
<UInput v-model="item.description" :disabled="mode === 'show'" placeholder="Positionstext (optional)" icon="i-heroicons-bars-3-bottom-left" variant="none" class="border-b border-gray-100 dark:border-gray-800" />
<UInput v-model="item.description" :disabled="mode === 'show'" placeholder="Positionstext (optional)" icon="i-heroicons-bars-3-bottom-left" variant="none" class="w-full border-b border-gray-100 dark:border-gray-800" />
</div>
</div>
</UCard>

View File

@@ -148,7 +148,15 @@ const isPaid = (item) => {
return Math.abs(amountPaid) === Math.abs(Number(getInvoiceSum(item)))
}
const selectIncomingInvoice = (invoice) => {
const unwrapInvoiceRow = (invoiceLike) => invoiceLike?.original || invoiceLike
const selectIncomingInvoice = (invoiceLike) => {
const invoice = unwrapInvoiceRow(invoiceLike)
if (!invoice?.id) {
return
}
if (invoice.state === "Gebucht") {
router.push(`/incomingInvoices/show/${invoice.id}`)
} else {
@@ -254,7 +262,7 @@ const selectIncomingInvoice = (invoice) => {
:columns="normalizeTableColumns(columns)"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
:on-select="(i) => selectIncomingInvoice(i) "
:on-select="selectIncomingInvoice"
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
>
<template #reference-cell="{row}">