Files
Caramel-CRM/resources/js/components/documents/InvoiceDialog.vue
T

736 lines
36 KiB
Vue
Raw Normal View History

2025-10-20 08:57:51 +02:00
<!-- 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">
2025-11-18 10:27:49 +01:00
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'
2025-10-20 08:57:51 +02:00
import axios from 'axios'
2025-11-18 10:27:49 +01:00
import { type DateValue, getLocalTimeZone, fromDate } from "@internationalized/date"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"
2025-10-20 08:57:51 +02:00
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
2025-11-18 10:27:49 +01:00
import { Table, TableBody, TableCell, TableHead, TableRow, } from '@/components/ui/table'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'
2025-10-20 08:57:51 +02:00
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input';
2025-11-18 10:27:49 +01:00
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
import { StatusBadge, statusBadgeLabels } from '@/components/ui/status-badge'
2025-10-20 08:57:51 +02:00
import LineItemTable from '@/components/documents/LineItemTable.vue'
2025-11-18 10:27:49 +01:00
import { Eye, FileText, Trash2, BookUser, User, CodeXml, MessageCircleQuestion, Loader2, Ellipsis, Check, FileCheck, Ban } from "lucide-vue-next"
2025-10-29 18:04:09 +01:00
import { alertStore } from "@/stores/alertStore"
import { GrowingTextarea } from '../ui/growing-textarea'
2025-10-30 10:15:02 +01:00
import { toast } from "vue-sonner"
2025-11-14 17:45:57 +01:00
import { Kbd, KbdGroup } from '@/components/ui/kbd';
import DialogClose from "../ui/dialog/DialogClose.vue"
2025-11-18 10:27:49 +01:00
import DialogCloseButton from "../DialogCloseButton/DialogCloseButton.vue";
import SendMailDialog from "../ui/send-mail-dialog/SendMailDialog.vue"
import Skeleton from "../ui/skeleton/Skeleton.vue"
2025-10-20 08:57:51 +02:00
const props = defineProps<{
2025-11-18 10:27:49 +01:00
invoiceData?: Invoice,
2025-10-20 08:57:51 +02:00
modelValue: boolean
}>()
2025-11-18 10:27:49 +01:00
const invoice = ref<Invoice>()
const customers = ref([] as Customer[])
2025-10-20 08:57:51 +02:00
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)
2025-10-29 18:04:09 +01:00
const alert = alertStore()
2025-11-11 11:29:17 +01:00
const reminderDialogOpen = ref(false)
2025-10-30 10:15:02 +01:00
const reminderLoading = ref(false)
2025-10-20 08:57:51 +02:00
2025-11-18 10:27:49 +01:00
const emit = defineEmits(['update:modelValue', 'save', 'cancel', 'delete'])
2025-10-20 08:57:51 +02:00
onMounted(async () => {
2025-11-18 10:27:49 +01:00
// 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
}
2025-10-20 08:57:51 +02:00
})
const isOpen = computed({
get: () => props.modelValue,
set: (value) => {
emit('update:modelValue', value)
}
})
2025-11-18 10:27:49 +01:00
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'
}
})
2025-10-20 08:57:51 +02:00
2025-11-18 10:27:49 +01:00
const value = ref<DateValue>()
2025-10-20 08:57:51 +02:00
// watch changes on local invoice date
watch(invoice,
(newValue, oldValue) => {
2025-11-18 10:27:49 +01:00
console.group('watch invoice')
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
2025-10-20 08:57:51 +02:00
if (isLoading.value) {
2025-11-04 13:51:31 +01:00
if (!invoice.value) return;
2025-11-18 10:27:49 +01:00
if (invoice.value.id === 0) {
isLoading.value = false;
}
2025-10-20 08:57:51 +02:00
// Initial load of invoice data
2025-11-04 13:51:31 +01:00
if (!invoice.value.billingData) {
2025-10-20 08:57:51 +02:00
// 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 || "",
},
2025-10-30 10:15:02 +01:00
contactSalutation: invoice.value.customer?.contacts && invoice.value.customer.contacts.length > 0 ? invoice.value.customer.contacts[0].salutation : "",
2025-10-20 08:57:51 +02:00
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,
}
}
2025-11-04 13:51:31 +01:00
if (invoice.value.customer?.id !== 0) {
2025-10-20 08:57:51 +02:00
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
}
2025-11-18 10:27:49 +01:00
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
console.groupEnd()
2025-10-20 08:57:51 +02:00
},
{ deep: true }
)
watch(importCustomer,
(newValue, oldValue) => {
if (!invoice.value) return
2025-11-18 10:27:49 +01:00
console.group('watch importCustomer')
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
2025-10-20 08:57:51 +02:00
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
2025-10-22 16:13:52 +02:00
invoice.value.customer = newValue
2025-10-20 08:57:51 +02:00
}
2025-10-22 16:13:52 +02:00
2025-11-18 10:27:49 +01:00
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
console.groupEnd()
2025-10-20 08:57:51 +02:00
},
{ deep: true }
)
watch(importContact,
(newValue, oldValue) => {
if (!invoice.value) return
2025-11-18 10:27:49 +01:00
console.group('watch importContact')
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
2025-10-20 08:57:51 +02:00
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;
2025-11-18 10:27:49 +01:00
} else {
isLoading.value = false;
2025-10-20 08:57:51 +02:00
}
2025-11-18 10:27:49 +01:00
console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
console.groupEnd()
2025-10-20 08:57:51 +02:00
},
{ 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 ""
})
2025-10-20 08:57:51 +02:00
const saveChanges = () => {
if (invoice.value) {
emit('save', invoice.value)
// isOpen.value = false
2025-10-20 08:57:51 +02:00
}
}
const cancelChanges = (event: Event | null) => {
2025-11-18 10:27:49 +01:00
if (!event) return
event.preventDefault()
event.returnValue = true
2025-10-20 08:57:51 +02:00
if (isDirty.value) {
2025-10-30 10:15:02 +01:00
alert.show(
"Wirklich schließen?",
"Es gibt ungespeicherte Änderungen, die dann verloren gehen.",
{
actionText: "Änderungen verwerfen",
onAction: () => {
emit('cancel')
isOpen.value = false
}
}
)
2025-10-20 08:57:51 +02:00
} 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'
}
2025-10-20 08:57:51 +02:00
const deleteInvoice = function () {
2025-10-30 10:15:02 +01:00
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
}
}
)
2025-10-20 08:57:51 +02:00
}
const cancelInvoice = function () {
if (!invoice.value) return;
invoice.value.paymentStatus = 'cancelled'
}
2025-11-11 11:29:17 +01:00
const openReminderDialog = function () {
if (!invoice.value) return
reminderDialogOpen.value = true
}
2025-10-30 10:15:02 +01:00
2025-11-11 11:29:17 +01:00
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
}
2025-10-22 11:58:18 +02:00
}
2025-10-20 08:57:51 +02:00
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"
2025-10-20 08:57:51 +02:00
@escapeKeyDown="cancelChanges" @interactOutside="cancelChanges">
2025-11-18 10:27:49 +01:00
<!-- <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">
2025-11-18 10:27:49 +01:00
<DialogTitle class="text-primary-foreground font-bold text-left">
<h1>{{ title }}</h1>
</DialogTitle>
<DialogDescription>
2025-11-18 10:27:49 +01:00
<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"
2025-11-18 10:27:49 +01:00
type="text" placeholder="Titel" />
</DialogDescription>
</div>
<div class="flex gap-2 items-center">
2025-10-20 08:57:51 +02:00
<TooltipProvider>
<!-- Save -->
2025-11-18 10:27:49 +01:00
<Button v-if="invoice && isDirty" class="grow md:grow-0" size="sm" @click="saveChanges">
<Check stroke-width="1.5" />
Speichern
</Button>
<!-- Issue -->
2025-11-18 10:27:49 +01:00
<!-- TODO: validate complete data -->
<Tooltip v-if="invoice && invoice.paymentStatus == 'draft'">
2025-10-20 08:57:51 +02:00
<TooltipTrigger>
<Button size="sm" variant="action" @click="issueInvoice">
Rechnung stellen
2025-10-20 08:57:51 +02:00
</Button>
</TooltipTrigger>
<TooltipContent>
Bearbeitung sperren und Rechnung erstellen
2025-10-20 08:57:51 +02:00
</TooltipContent>
</Tooltip>
<!-- Paid -->
<Tooltip v-if="invoice && ['issued', 'due', 'reminded'].includes(invoice.paymentStatus)">
2025-10-20 08:57:51 +02:00
<TooltipTrigger>
<Button size="sm" variant="success" @click="invoice.paymentStatus = 'paid'">
<FileCheck stroke-width="1.5" /> Bezahlt
2025-10-20 08:57:51 +02:00
</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
2025-10-20 08:57:51 +02:00
</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">
2025-11-14 17:45:57 +01:00
<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 -->
2025-11-18 10:27:49 +01:00
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'"
class="flex justify-between" @click="downloadPdf">
2025-11-14 17:45:57 +01:00
<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 -->
2025-11-18 10:27:49 +01:00
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'"
class="flex justify-between" @click="downloadXml">
2025-11-14 17:45:57 +01:00
<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">
2025-11-14 17:45:57 +01:00
<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">
2025-11-14 17:45:57 +01:00
<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>
2025-10-20 08:57:51 +02:00
</div>
</DialogHeader>
<div class="overflow-y-auto p-4 md:p-6 lg:p-12 pt-0!" v-if="invoice">
2025-10-20 08:57:51 +02:00
<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>
2025-10-20 08:57:51 +02:00
</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>
2025-10-20 08:57:51 +02:00
</div>
2025-10-20 08:57:51 +02:00
</div>
<div id="document-meta"
class="flex-none md:flex gap-12 2 p-6 bg-slate-100 dark:bg-neutral-900 rounded-lg">
2025-10-20 08:57:51 +02:00
2025-10-22 11:58:18 +02:00
<div class="flex flex-col gap-1">
<div class="flex">
2025-10-20 08:57:51 +02:00
<Input type="text" v-model="invoice.billingData.companyName" placeholder="Firma"
2025-10-22 11:58:18 +02:00
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" />
2025-10-20 08:57:51 +02:00
<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"
2025-10-22 11:58:18 +02:00
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" />
2025-10-20 08:57:51 +02:00
<Input type="text" v-model="invoice.billingData.contactLastName" placeholder="Nachname"
2025-10-22 11:58:18 +02:00
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" />
2025-10-20 08:57:51 +02:00
<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"
2025-10-22 11:58:18 +02:00
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" />
2025-10-20 08:57:51 +02:00
<Input type="text" v-model="invoice.billingData.billingAddress.lineTwo"
placeholder="Zeile 2"
2025-10-22 11:58:18 +02:00
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" />
2025-10-20 08:57:51 +02:00
<div class="flex gap-4">
<Input type="text" v-model="invoice.billingData.billingAddress.postalCode"
placeholder="PLZ"
2025-10-22 11:58:18 +02:00
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" />
2025-10-20 08:57:51 +02:00
<Input type="text" v-model="invoice.billingData.billingAddress.city" placeholder="Stadt"
2025-10-22 11:58:18 +02:00
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" />
2025-10-20 08:57:51 +02:00
</div>
<Input type="text" v-model="invoice.billingData.vatId" placeholder="USt-Id-Nr."
2025-10-22 11:58:18 +02:00
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" />
2025-10-20 08:57:51 +02:00
</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>
2025-11-18 10:27:49 +01:00
<Select v-if="invoice && invoice.billingData"
v-model="invoice.billingData.paymentTerms" by="id">
2025-10-20 08:57:51 +02:00
<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">
2025-10-22 16:13:52 +02:00
<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" />
2025-10-20 08:57:51 +02:00
</div>
</div>
</DialogContent>
</Dialog>
2025-11-18 10:27:49 +01:00
2025-11-11 11:29:17 +01:00
<SendMailDialog v-model:open="reminderDialogOpen" title="Zahlungserinnerung senden?" description=""
2025-11-18 10:27:49 +01:00
:recipient="billingContactEmail" @send="(recipient: string | undefined) => sendReminder(recipient)" />
2025-10-20 08:57:51 +02:00
</template>
2025-11-18 10:27:49 +01:00
<style></style>