3. Zwischenstand
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}">
|
||||
|
||||
Reference in New Issue
Block a user