This repository has been archived on 2025-12-04. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Caramel-CRM-Backup/resources/js/components/documents/InvoiceDialog.vue
T

736 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- Rechnungsart: Wiederkehren, einmalig, Abschlag (siehe Chat, wirkt sich auf Leistungszeitraum aus) -->
<!-- TODO: Leistungsdatum -->
<!-- TODO: Leistungsstart -->
<!-- TODO: Leistungsende -->
<!-- TODO: Steuersatz in LineItem -->
<!-- TODO: Stunden und Tagessatz aus Settings -->
<!-- TODO: Client-side validation -->
<script setup lang="ts">
import { ref, computed, watch, onMounted } from "vue"
import { Customer, Invoice, Contact, PaymentTerms, Address, LineItem } from "@/types"
import { newInvoice, newCustomer, newContact, newBillingData } from '@/types/index.d'
import { toCurrency, toLocalDate, toShortISOString, calcDueDate, toFixedRounded } from '@/lib/utils'
import axios from 'axios'
import { type DateValue, getLocalTimeZone, fromDate } from "@internationalized/date"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Table, TableBody, TableCell, TableHead, TableRow, } from '@/components/ui/table'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
import { StatusBadge, statusBadgeLabels } from '@/components/ui/status-badge'
import LineItemTable from '@/components/documents/LineItemTable.vue'
import { Eye, FileText, Trash2, BookUser, User, CodeXml, MessageCircleQuestion, Loader2, Ellipsis, Check, FileCheck, Ban } from "lucide-vue-next"
import { alertStore } from "@/stores/alertStore"
import { GrowingTextarea } from '../ui/growing-textarea'
import { toast } from "vue-sonner"
import { Kbd, KbdGroup } from '@/components/ui/kbd';
import DialogClose from "../ui/dialog/DialogClose.vue"
import DialogCloseButton from "../DialogCloseButton/DialogCloseButton.vue";
import SendMailDialog from "../ui/send-mail-dialog/SendMailDialog.vue"
import Skeleton from "../ui/skeleton/Skeleton.vue"
const props = defineProps<{
invoiceData?: Invoice,
modelValue: boolean
}>()
const invoice = ref<Invoice>()
const customers = ref([] as Customer[])
const paymentTermsData = ref([] as PaymentTerms[])
const isDirty = ref(false);
const isLoading = ref(false);
const importContact = ref(newContact() as Contact)
const importCustomer = ref(newCustomer() as Customer)
const alert = alertStore()
const reminderDialogOpen = ref(false)
const reminderLoading = ref(false)
const emit = defineEmits(['update:modelValue', 'save', 'cancel', 'delete'])
onMounted(async () => {
// Load customers and payment terms
try {
const promises = [];
promises.push(axios.get('/api/customers'))
promises.push(axios.get('/api/paymentterms'))
const responses = await Promise.all(promises)
let responseIndex = 0
customers.value = responses[responseIndex].data as Customer[]
paymentTermsData.value = responses[responseIndex + 1].data as PaymentTerms[]
} catch (error) {
toast.error('Fehler beim Laden der Daten', error || String(error))
}
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
console.groupEnd()
})
watch(() => props.modelValue, (open) => {
// on open
if (open) {
console.group('watch props.modelValue')
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
isDirty.value = false;
isLoading.value = true;
// Get invoice data from props
invoice.value = props.invoiceData
// Load line items
try {
axios.get('/api/lineitems/' + invoice.value.id).then(response => {
if (invoice.value) invoice.value.items = response.data as LineItem[]
})
} catch (error) {
toast.error('Fehler beim Laden der Positionen', error || String(error))
}
}
// on close
else {
invoice.value = undefined
}
})
const isOpen = computed({
get: () => props.modelValue,
set: (value) => {
emit('update:modelValue', value)
}
})
const title = computed<string>(() => {
if (isLoading.value) {
return 'Rechnung'
} else if (invoice.value && invoice.value.id > 0) {
return `Rechnung ${invoice.value.nr}`
} else {
return 'Neue Rechnung'
}
})
const value = ref<DateValue>()
// watch changes on local invoice date
watch(invoice,
(newValue, oldValue) => {
console.group('watch invoice')
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
if (isLoading.value) {
if (!invoice.value) return;
if (invoice.value.id === 0) {
isLoading.value = false;
}
// Initial load of invoice data
if (!invoice.value.billingData) {
// Set default billing data from customer
invoice.value.billingData = {
companyName: invoice.value.customer?.companyName || "",
vatId: invoice.value.customer?.vatId || "",
billingAddress: {
lineOne: invoice.value.customer?.billingAddress?.lineOne || "",
lineTwo: invoice.value.customer?.billingAddress?.lineTwo || "",
city: invoice.value.customer?.billingAddress?.city || "",
postalCode: invoice.value.customer?.billingAddress?.postalCode || "",
countryCode: invoice.value.customer?.billingAddress?.countryCode || "",
},
contactSalutation: invoice.value.customer?.contacts && invoice.value.customer.contacts.length > 0 ? invoice.value.customer.contacts[0].salutation : "",
contactFirstName: invoice.value.customer?.contacts && invoice.value.customer.contacts.length > 0 ? invoice.value.customer.contacts[0].firstName : "",
contactLastName: invoice.value.customer?.contacts && invoice.value.customer.contacts.length > 0 ? invoice.value.customer.contacts[0].lastName : "",
paymentTerms: invoice.value.customer?.paymentTerms || paymentTermsData.value.length > 0 ? paymentTermsData.value[2] : null,
}
}
if (invoice.value.customer?.id !== 0) {
importCustomer.value = invoice.value.customer as Customer
// console.log("billingData contact", invoice.value?.billingData?.contactFirstName, invoice.value?.billingData?.contactLastName)
invoice.value.customer?.contacts.find(contact => {
if (
contact.firstName === invoice.value?.billingData?.contactFirstName &&
contact.lastName === invoice.value?.billingData?.contactLastName
) {
importContact.value = contact
return true
}
})
}
value.value = fromDate(new Date(invoice.value.invoiceDate), getLocalTimeZone())
}
else {
isDirty.value = true
}
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
console.groupEnd()
},
{ deep: true }
)
watch(importCustomer,
(newValue, oldValue) => {
if (!invoice.value) return
console.group('watch importCustomer')
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
if (!isLoading.value) {
if (!invoice.value.billingData) invoice.value.billingData = newBillingData()
invoice.value.billingData.companyName = newValue.companyName
invoice.value.billingData.vatId = newValue.vatId
// Don't overwrite these values during loading
// they can intentionally be different from customer data
if (!invoice.value.billingData.billingAddress || !isLoading.value)
invoice.value.billingData.billingAddress = newValue.billingAddress as Address
if (!invoice.value.billingData.contactFirstName || !isLoading.value)
invoice.value.billingData.contactFirstName = newValue.contacts && newValue.contacts.length > 0 ? newValue.contacts[0].firstName : ''
if (!invoice.value.billingData.contactLastName || !isLoading.value)
invoice.value.billingData.contactLastName = newValue.contacts && newValue.contacts.length > 0 ? newValue.contacts[0].lastName : ''
if (!invoice.value.billingData.paymentTerms || !isLoading.value)
invoice.value.billingData.paymentTerms = newValue.paymentTerms as PaymentTerms
invoice.value.customer = newValue
}
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
console.groupEnd()
},
{ deep: true }
)
watch(importContact,
(newValue, oldValue) => {
if (!invoice.value) return
console.group('watch importContact')
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
if (!isLoading.value) {
if (newValue.id !== 0) {
invoice.value.billingData!.contactFirstName = newValue.firstName
invoice.value.billingData!.contactLastName = newValue.lastName
} else {
invoice.value.billingData!.contactFirstName = ''
invoice.value.billingData!.contactLastName = ''
}
isDirty.value = true;
} else {
isLoading.value = false;
}
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
console.groupEnd()
},
{ deep: true }
)
const billingContactEmail = computed<string | undefined>(() => {
// TODO: use e-mail from billing data if set
// and fallback to primary contact email
if (invoice.value?.customer && invoice.value?.customer.contacts[0])
return invoice.value?.customer?.contacts[0].email
else return ""
})
const saveChanges = () => {
if (invoice.value) {
emit('save', invoice.value)
// isOpen.value = false
}
}
const cancelChanges = (event: Event | null) => {
if (!event) return
event.preventDefault()
event.returnValue = true
if (isDirty.value) {
alert.show(
"Wirklich schließen?",
"Es gibt ungespeicherte Änderungen, die dann verloren gehen.",
{
actionText: "Änderungen verwerfen",
onAction: () => {
emit('cancel')
isOpen.value = false
}
}
)
} else {
emit('cancel')
isOpen.value = false
}
}
const preview = function () {
if (!invoice.value) return;
window?.open('/invoice/' + invoice.value.id, '_blank')?.focus();
}
const downloadPdf = function () {
if (!invoice.value) return;
window?.open('/invoice/' + invoice.value.id + '/pdf');
}
const downloadXml = function () {
if (!invoice.value) return;
window?.open('/invoice/' + invoice.value.id + '/xml');
}
const issueInvoice = function () {
if (!invoice.value) return;
invoice.value.paymentStatus = 'issued'
}
const deleteInvoice = function () {
alert.show(
"Möchtest Du diese Rechnung wirklich löschen?",
(invoice.value?.paymentStatus == "draft") ? null : "Nach GoBD musst Du alle Belege und Daten in unveränderter Form aufbewahren",
{
actionText: "Löschen",
actionVariant: "destructive",
onAction: () => {
emit('delete', invoice.value?.id)
isOpen.value = false
}
}
)
}
const cancelInvoice = function () {
if (!invoice.value) return;
invoice.value.paymentStatus = 'cancelled'
}
const openReminderDialog = function () {
if (!invoice.value) return
reminderDialogOpen.value = true
}
const sendReminder = async function (recipient: string | undefined) {
if (!recipient) return
if (!invoice.value || !invoice.value.id) return
// Close dialog
reminderDialogOpen.value = false
// make button spin and disable button
reminderLoading.value = true
try {
// await axios call
await axios.get('/api/invoices/' + invoice.value.id + '/remind/' + recipient)
toast.success("Zahlungserinnerung gesendet", { description: recipient })
} catch (error: any) {
toast.error(error?.title || 'Fehler', { description: error?.message || String(error) })
} finally {
reminderLoading.value = false
}
}
const updateTotalAmount = () => {
if (!invoice.value) return;
let total = 0;
invoice.value.items.forEach(item => {
total += item.quantity * item.price;
});
invoice.value.totalAmount = total;
}
</script>
<template>
<Dialog id="invoice-dialog" v-model:open="isOpen">
<DialogContent
class="sm:max-w-[min((100%-2rem),1152px)] grid-rows-[auto_minmax(0,1fr)_auto] h-[calc(100dvh-2rem)] gap-0 p-0 outline-none"
@escapeKeyDown="cancelChanges" @interactOutside="cancelChanges">
<!-- <div :class="{ 'opacity-100': isLoading }"
class="absolute inset-[1rem_0_0_0] flex justify-center items-center z-10 pointer-events-none bg-background opacity-0 transition-opacity rounded-lg">
<div class="bg-sidebar rounded-lg p-6">
<Loader2 class="h-6 w-6 animate-spin text-muted-foreground" stroke-width="1.5" />
</div>
</div> -->
<DialogHeader class="p-4 md:p-6 lg:p-12 pb-0 md:pb-2 lg:pb-8 flex flex-row items-start gap-6">
<div class="flex flex-col grow">
<DialogTitle class="text-primary-foreground font-bold text-left">
<h1>{{ title }}</h1>
</DialogTitle>
<DialogDescription>
<Input v-if="invoice" v-model="invoice.title"
class="text-foreground md:text-base text-ellipsis px-0 bg-transparent dark:bg-transparent hover:bg-accent dark:hover:bg-accent/30 border-none shadow-none"
type="text" placeholder="Titel" />
</DialogDescription>
</div>
<div class="flex gap-2 items-center">
<TooltipProvider>
<!-- Save -->
<Button v-if="invoice && isDirty" class="grow md:grow-0" size="sm" @click="saveChanges">
<Check stroke-width="1.5" />
Speichern
</Button>
<!-- Issue -->
<!-- TODO: validate complete data -->
<Tooltip v-if="invoice && invoice.paymentStatus == 'draft'">
<TooltipTrigger>
<Button size="sm" variant="action" @click="issueInvoice">
Rechnung stellen
</Button>
</TooltipTrigger>
<TooltipContent>
Bearbeitung sperren und Rechnung erstellen
</TooltipContent>
</Tooltip>
<!-- Paid -->
<Tooltip v-if="invoice && ['issued', 'due', 'reminded'].includes(invoice.paymentStatus)">
<TooltipTrigger>
<Button size="sm" variant="success" @click="invoice.paymentStatus = 'paid'">
<FileCheck stroke-width="1.5" /> Bezahlt
</Button>
</TooltipTrigger>
<TooltipContent>
Als bezahlt markieren
</TooltipContent>
</Tooltip>
<!-- Remind -->
<Tooltip v-if="invoice && ['due', 'reminded'].includes(invoice.paymentStatus)">
<TooltipTrigger>
<Button size="sm" variant="destructive" @click="openReminderDialog"
:disabled="reminderLoading" class="gap-0">
<Loader2 class="h-4 w-4 transition-[width] ease-in-out animate-spin"
stroke-width="1.5"
:class="{ 'w-0!': !reminderLoading, 'mr-2': reminderLoading }" />
Erinnern
</Button>
</TooltipTrigger>
<TooltipContent>
Zahlungserinnerung per E-Mail senden
</TooltipContent>
</Tooltip>
<!-- Ellipsis menu -->
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant="ghost" size="sm" class="px-0! w-7 ml-2">
<Ellipsis class="size-4" stroke-width="1.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<!-- Preview -->
<DropdownMenuItem v-if="invoice?.paymentStatus == 'draft'"
class="flex items-center justify-between" @click="preview">
<div class="flex items-center gap-3">
<Eye :strokeWidth="1.5" class="text-current" />
<span class="mr-4">Vorschau</span>
</div>
<KbdGroup>
<Kbd class="visible-mac"></Kbd>
<Kbd class="visible-pc">Ctrl</Kbd>
<Kbd>P</Kbd>
</KbdGroup>
</DropdownMenuItem>
<!-- PDF -->
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'"
class="flex justify-between" @click="downloadPdf">
<div class="flex items-center gap-3">
<FileText stroke-width="1.5" class="text-muted-foreground" />
<div class="mr-4 flex flex-col">
<span>PDF exportieren</span>
<span class="text-xs text-muted-foreground">(ZUGFeRD)</span>
</div>
</div>
<KbdGroup>
<Kbd class="visible-mac"></Kbd>
<Kbd class="visible-pc">Ctrl</Kbd>
<Kbd>E</Kbd>
</KbdGroup>
</DropdownMenuItem>
<!-- XML -->
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'"
class="flex justify-between" @click="downloadXml">
<div class="flex items-center gap-3">
<CodeXml stroke-width="1.5" class="text-muted-foreground" />
<div class="mr-4 flex flex-col">
<span>XML exportieren</span>
<span class="text-xs text-muted-foreground">(XRechnung)</span>
</div>
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
<!-- Cancel -->
<DropdownMenuItem
v-if="invoice && ['issued', 'due', 'reminded'].includes(invoice.paymentStatus)"
class="flex justify-between" @click="deleteInvoice">
<div class="flex items-center gap-3">
<!-- <FileX stroke-width="1.5" class="text-muted-foreground"/> -->
<Ban :strokeWidth="1.5" class="text-current" />
<span class="mr-2">Stornieren</span>
</div>
</DropdownMenuItem>
<!-- Delete -->
<DropdownMenuItem
class="flex justify-between text-destructive! hover:bg-red-100! dark:hover:bg-red-950!"
@click="deleteInvoice">
<div class="flex items-center gap-3">
<Trash2 :strokeWidth="1.5" class="text-current" />
<span class="mr-2">Löschen</span>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TooltipProvider>
<DialogClose as-child>
<DialogCloseButton />
</DialogClose>
</div>
</DialogHeader>
<div class="overflow-y-auto p-4 md:p-6 lg:p-12 pt-0!" v-if="invoice">
<div id="document">
<div id="document-header"
class="h-7 mb-12 sticky top-0 bg-background z-1 flex flex-col md:flex-row justify-between items-center">
<!-- Status -->
<div>
<StatusBadge size="lg" :variant="invoice.paymentStatus">{{
statusBadgeLabels[invoice.paymentStatus] }}
</StatusBadge>
</div>
<!-- <div class="flex gap-4 mr-6 w-33" v-if="invoice && invoice.paymentStatus == 'draft'">
</div>
<Select v-model="invoice.paymentStatus" v-else>
<SelectTrigger class="bg-transparent! shadow-none! outline-0 border-0 pr-8 w-41 pl-0">
<StatusBadge size="lg" :variant="invoice.paymentStatus">{{
statusBadgeLabels[invoice.paymentStatus] }}
</StatusBadge>
<!-- <SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="(label, value) in statusBadgeLabels" :value="value">
<SelectLabel>{{ label }}</SelectLabel>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select> -->
<!-- Betrag -->
<div class="grid grid-cols-[auto_auto_auto_auto] items-end gap-x-6 gap-y-0">
<label class="text-muted-foreground text-xs pb-[0.4rem]">Netto</label>
<span class="text-lg text-muted-foreground place-self-end">{{
toCurrency(invoice.totalAmount) }}</span>
<label class="text-muted-foreground text-xs pb-[0.4rem]">Brutto</label>
<span class="text-xl font-bold place-self-end">{{
toCurrency(toFixedRounded(Number(invoice.totalAmount *
1.19), 2)) }}</span>
</div>
</div>
<div id="document-meta"
class="flex-none md:flex gap-12 2 p-6 bg-slate-100 dark:bg-neutral-900 rounded-lg">
<div class="flex flex-col gap-1">
<div class="flex">
<Input type="text" v-model="invoice.billingData.companyName" placeholder="Firma"
class="bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 p-1 shadow-none border-0 border-b-1 border-slate-300 dark:border-neutral-800 placeholder:text-muted-foreground/50 rounded-none hover:rounded-md" />
<Select v-model="importCustomer" by="id">
<SelectTrigger
class="bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 border-none">
<SelectValue>
<BookUser />
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem :value="newCustomer()">
Manuell eingeben
</SelectItem>
<SelectItem v-for="customer in customers" :value="customer">
{{ customer.companyName }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex flex-row gap-4">
<Input type="text" v-model="invoice.billingData.contactFirstName" placeholder="Vorname"
class="bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 p-1 shadow-none border-0 border-b-1 border-slate-300 dark:border-neutral-800 placeholder:text-muted-foreground/50 rounded-none hover:rounded-md" />
<Input type="text" v-model="invoice.billingData.contactLastName" placeholder="Nachname"
class="bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 p-1 shadow-none border-0 border-b-1 border-slate-300 dark:border-neutral-800 placeholder:text-muted-foreground/50 rounded-none hover:rounded-md" />
<Select v-model="importContact" by="id">
<SelectTrigger v-bind:disabled="!importCustomer || invoice.customer.id === 0"
class="bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 border-none">
<SelectValue>
<User />
</SelectValue>
</SelectTrigger>
<SelectContent v-if="invoice.customer && invoice.customer.id !== 0">
<SelectItem :value="{ id: 0, firstName: '', lastName: '' }">
</SelectItem>
<SelectItem v-for="customer in importCustomer.contacts" :value="customer">
{{ customer.firstName + ' ' + customer.lastName }}
</SelectItem>
</SelectContent>
</Select>
</div>
<Input type="text" v-model="invoice.billingData.billingAddress.lineOne"
placeholder="Zeile 1"
class="bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 p-1 shadow-none border-0 border-b-1 border-slate-300 dark:border-neutral-800 placeholder:text-muted-foreground/50 rounded-none hover:rounded-md" />
<Input type="text" v-model="invoice.billingData.billingAddress.lineTwo"
placeholder="Zeile 2"
class="bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 p-1 shadow-none border-0 border-b-1 border-slate-300 dark:border-neutral-800 placeholder:text-muted-foreground/50 rounded-none hover:rounded-md" />
<div class="flex gap-4">
<Input type="text" v-model="invoice.billingData.billingAddress.postalCode"
placeholder="PLZ"
class="bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 p-1 w-20 shadow-none border-0 border-b-1 border-slate-300 dark:border-neutral-800 placeholder:text-muted-foreground/50 rounded-none hover:rounded-md" />
<Input type="text" v-model="invoice.billingData.billingAddress.city" placeholder="Stadt"
class="bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 p-1 shadow-none border-0 border-b-1 border-slate-300 dark:border-neutral-800 placeholder:text-muted-foreground/50 rounded-none hover:rounded-md" />
</div>
<Input type="text" v-model="invoice.billingData.vatId" placeholder="USt-Id-Nr."
class="mt-6 bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 p-1 shadow-none border-0 border-b-1 border-slate-300 dark:border-neutral-800 placeholder:text-muted-foreground/50 rounded-none hover:rounded-md" />
</div>
<div>
<Table>
<TableBody>
<!-- Rechnungsdatum -->
<TableRow>
<TableHead>Datum</TableHead>
<TableCell>
<Input type="date" :modelValue="toShortISOString(invoice.invoiceDate)"
@update:modelValue="newValue => { invoice.invoiceDate = new Date(newValue); invoice.dueDate = calcDueDate(invoice.invoiceDate, invoice.billingData?.paymentTerms?.days) }"
placeholder="Datum"
class="bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 shadow-none border-none" />
</TableCell>
</TableRow>
<!-- Zahlungsziel -->
<TableRow>
<TableHead class="flex items-center gap-1">
Zahlungsziel
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<MessageCircleQuestion strokeWidth="1.5"
class="h-[16px] w-[16px] ml-[1px] mb-2" />
</TooltipTrigger>
<TooltipContent>
Zahlungsbedingungen können<br />
beim Kunden hinterlegt werden.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableHead>
<TableCell>
<Select v-if="invoice && invoice.billingData"
v-model="invoice.billingData.paymentTerms" by="id">
<SelectTrigger
class="w-full bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 shadow-none border-none">
<SelectValue placeholder="Zahlungsziel" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="term in paymentTermsData" :value="term">
<span v-if="term.isFixed">{{ term.description }}</span>
<span v-else>{{ term.days }} Tage</span>
</SelectItem>
</SelectContent>
</Select>
</TableCell>
</TableRow>
<!-- Fällig -->
<TableRow v-if="!invoice.billingData?.paymentTerms?.isFixed">
<TableHead>Fällig</TableHead>
<TableCell class="pl-5">{{ toLocalDate(invoice.dueDate) }}</TableCell>
</TableRow>
<!-- Leistungszeitraum -->
<TableRow>
<TableHead>Leistungszeitraum</TableHead>
<TableCell>
<Input type="date" :modelValue="toShortISOString(invoice.serviceStartDate)"
@update:modelValue="newValue => invoice.serviceStartDate = new Date(newValue)"
placeholder="Datum"
class="bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 shadow-none border-none w-fit inline" />
bis
<Input type="date" :modelValue="toShortISOString(invoice.serviceEndDate)"
@update:modelValue="newValue => invoice.serviceEndDate = new Date(newValue)"
placeholder="Datum"
class="bg-transparent dark:bg-transparent hover:bg-background dark:hover:bg-background/40 shadow-none border-none w-fit inline" />
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
<div id="document-text" class="mt-6 md:mt-8 lg:mt-12">
<GrowingTextarea v-model="invoice.text" placeholder="Anschreiben"
class="font-light bg-transparent dark:bg-transparent hover:bg-accent dark:hover:bg-accent/30 border-none shadow-none" />
</div>
<LineItemTable :lineItems="invoice.items" @update:lineItems="updateTotalAmount" sticky-top="7"
class="mt-4" />
</div>
</div>
</DialogContent>
</Dialog>
<SendMailDialog v-model:open="reminderDialogOpen" title="Zahlungserinnerung senden?" description=""
:recipient="billingContactEmail" @send="(recipient: string | undefined) => sendReminder(recipient)" />
</template>
<style></style>