Two month of work
This commit is contained in:
@@ -6,18 +6,16 @@
|
||||
<!-- TODO: Steuersatz in LineItem -->
|
||||
<!-- TODO: Stunden und Tagessatz aus Settings -->
|
||||
<!-- TODO: Client-side validation -->
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref, computed, watch, onMounted, onUpdated, toRaw } from "vue"
|
||||
import { ref, computed, watch, onMounted, onUpdated, toRaw, nextTick } from "vue"
|
||||
import { Customer, Invoice, Contact, PaymentTerms, Address, LineItem, PaymentStatus, Unit } from "@/types"
|
||||
import { 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 { toCurrency, toLocalDate, toShortISOString, calcDueDate, toFixedRounded, hotkey } from '@/lib/utils'
|
||||
import axios from "axios"
|
||||
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 { Table, TableBody, TableCell, TableHead, TableRow, } from '@/components/ui/crm-table'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'
|
||||
import { Button } from '@/components/ui/crm-button'
|
||||
import { Input } from '@/components/ui/crm-input';
|
||||
@@ -28,35 +26,21 @@ import { Eye, FileText, Trash2, BookUser, User, CodeXml, MessageCircleQuestion,
|
||||
import { alertStore } from "@/stores/alertStore"
|
||||
import { GrowingTextarea } from '../ui/growing-textarea'
|
||||
import { toast } from "vue-sonner"
|
||||
import { Kbd, KbdGroup } from '@/components/ui/kbd';
|
||||
import { Kbd, KbdGroup } from '@/components/ui/kbd'
|
||||
import DialogClose from "../ui/dialog/DialogClose.vue"
|
||||
import DialogCloseButton from "../DialogCloseButton/DialogCloseButton.vue";
|
||||
import DialogCloseButton from "../DialogCloseButton/DialogCloseButton.vue"
|
||||
import SendMailDialog from "../ui/send-mail-dialog/SendMailDialog.vue"
|
||||
|
||||
const DEBUG = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
invoiceData?: Invoice,
|
||||
invoiceData: Invoice | undefined,
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
|
||||
const invoice = ref<Invoice>()
|
||||
const units = ref([] as Unit[])
|
||||
const customers = ref([] as Customer[])
|
||||
const paymentTermsData = ref([] as PaymentTerms[])
|
||||
const isDirty = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const itemsLoading = 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 value = ref<DateValue>() // TODO: name properly
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'cancel', 'delete'])
|
||||
|
||||
// Dialog state
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
@@ -64,7 +48,21 @@ const isOpen = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const title = computed<string>(() => {
|
||||
const invoice = ref<Invoice>()
|
||||
const units = ref([] as Unit[])
|
||||
const customers = ref([] as Customer[])
|
||||
const paymentTerms = ref([] as PaymentTerms[])
|
||||
const isDirty = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const itemsLoading = 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 fileInput = ref<HTMLInputElement | null>(null);
|
||||
const dataLoaded = ref(false)
|
||||
const title = computed<string>(_ => {
|
||||
if (invoice.value && invoice.value.id !== 0) {
|
||||
return `Rechnung ${invoice.value.nr || ''}`
|
||||
} else {
|
||||
@@ -72,83 +70,99 @@ const title = computed<string>(() => {
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
// Load customers and payment terms
|
||||
// Load additional data that wasn’t provided by parent invoices view
|
||||
try {
|
||||
const promises = [];
|
||||
promises.push(axios.get('/api/customers'))
|
||||
promises.push(axios.get('/api/paymentterms'))
|
||||
promises.push(axios.get('/api/units'))
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
// Create an array of promises for each data request
|
||||
promises.push(axios.get("/api/customers"))
|
||||
promises.push(axios.get("/api/paymentterms"))
|
||||
promises.push(axios.get("/api/units"))
|
||||
|
||||
// Wait for all promises to resolve
|
||||
const responses = await Promise.all(promises)
|
||||
let responseIndex = 0
|
||||
customers.value = responses[responseIndex].data as Customer[]
|
||||
paymentTermsData.value = responses[responseIndex + 1].data as PaymentTerms[]
|
||||
units.value = responses[responseIndex + 2].data as Unit[]
|
||||
|
||||
// Process each response
|
||||
customers.value = responses[0].data
|
||||
paymentTerms.value = responses[0].data
|
||||
units.value = responses[0].data
|
||||
|
||||
} catch (error) {
|
||||
toast.error('Fehler beim Laden der Daten', error || String(error))
|
||||
}
|
||||
|
||||
// Register hotkeys
|
||||
hotkey('mod+i', importLineItems, null, () => isOpen.value)
|
||||
hotkey('mod+e', exportPdf, null, () => isOpen.value)
|
||||
hotkey('mod+p', preview, null, () => isOpen.value)
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
if (isLoading.value) isLoading.value = false
|
||||
// console.group('onUpdated')
|
||||
// console.error(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
|
||||
// console.groupEnd()
|
||||
})
|
||||
// Initial data from parent view
|
||||
watch(() => props.invoiceData, async () => {
|
||||
// Reset state flag
|
||||
isDirty.value = false
|
||||
dataLoaded.value = false
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
// on open
|
||||
if (open) {
|
||||
// console.group('on open')
|
||||
// console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
|
||||
if (DEBUG.value) {
|
||||
console.group('on parent data')
|
||||
console.log(`isDirty: ${isDirty.value}`, `dataLoaded: ${dataLoaded.value}`)
|
||||
}
|
||||
|
||||
// Reset state flags
|
||||
isDirty.value = false;
|
||||
isLoading.value = true;
|
||||
// Get invoice data from props
|
||||
invoice.value = props.invoiceData
|
||||
|
||||
// Get invoice data from props
|
||||
// console.warn('trigger invoice watcher')
|
||||
invoice.value = props.invoiceData
|
||||
// Load line items
|
||||
if (invoice.value && invoice.value.id !== 0) {
|
||||
itemsLoading.value = true
|
||||
|
||||
// Load line items
|
||||
if (invoice.value && invoice.value.id !== 0) {
|
||||
itemsLoading.value = true
|
||||
|
||||
try {
|
||||
itemsLoading.value = true
|
||||
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))
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await 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))
|
||||
} finally {
|
||||
await nextTick()
|
||||
itemsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
|
||||
// console.groupEnd()
|
||||
|
||||
/* wait until next tick to reset loading state
|
||||
*
|
||||
* as changes made to customer and contacts here will probably
|
||||
* change the invoice and would then trigger the invoice watcher agai
|
||||
*/
|
||||
await nextTick()
|
||||
dataLoaded.value = true
|
||||
if (DEBUG.value) {
|
||||
console.log(`isDirty: ${isDirty.value}`, `dataLoaded: ${dataLoaded.value}`)
|
||||
console.groupEnd()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// watch changes on local invoice date
|
||||
watch(invoice,
|
||||
(newValue, oldValue) => {
|
||||
async (newValue, oldValue) => {
|
||||
if (newValue == oldValue) return
|
||||
|
||||
// console.group('watch invoice')
|
||||
// console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
|
||||
if (DEBUG.value) {
|
||||
console.group('watch invoice')
|
||||
console.log(`isDirty: ${isDirty.value}`, `dataLoaded: ${dataLoaded.value}`)
|
||||
}
|
||||
|
||||
if (isLoading.value) {
|
||||
if (dataLoaded.value) {
|
||||
isDirty.value = true
|
||||
} else {
|
||||
if (!newValue) {
|
||||
// console.groupEnd()
|
||||
console.groupEnd()
|
||||
return;
|
||||
}
|
||||
|
||||
// Set default billing data from customer
|
||||
// If no billing data is store in the invoice, generat ot from customer
|
||||
if (!newValue.billingData) {
|
||||
// console.warn('trigger invoice watcher')
|
||||
newValue.billingData = {
|
||||
companyName: newValue.customer?.companyName || "",
|
||||
vatId: newValue.customer?.vatId || "",
|
||||
@@ -167,8 +181,6 @@ watch(invoice,
|
||||
}
|
||||
|
||||
if (newValue.customerId && newValue.customerId !== 0) {
|
||||
// if (importCustomer.value != newValue.customer)
|
||||
// console.warn('trigger importCustomer watcher')
|
||||
customers.value.find(customer => {
|
||||
if (customer.id === newValue.customerId) {
|
||||
if (invoice.value) invoice.value.customer = customer as Customer
|
||||
@@ -177,8 +189,6 @@ watch(invoice,
|
||||
customer.contacts.find(contact => {
|
||||
if (contact.firstName === newValue?.billingData?.contactFirstName &&
|
||||
contact.lastName === newValue?.billingData?.contactLastName) {
|
||||
// if (importContact.value != contact)
|
||||
// console.warn('trigger importContact watcher')
|
||||
importContact.value = contact
|
||||
return true
|
||||
}
|
||||
@@ -186,15 +196,12 @@ watch(invoice,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
value.value = fromDate(new Date(newValue.invoiceDate), getLocalTimeZone())
|
||||
}
|
||||
else {
|
||||
isDirty.value = true
|
||||
}
|
||||
|
||||
// console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
|
||||
// console.groupEnd()
|
||||
if (DEBUG.value) {
|
||||
console.log(`isDirty: ${isDirty.value}`, `dataLoaded: ${dataLoaded.value}`)
|
||||
console.groupEnd()
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
@@ -204,13 +211,14 @@ watch(importCustomer,
|
||||
if (newValue == oldValue) return
|
||||
|
||||
if (!invoice.value) return
|
||||
|
||||
// console.group('watch importCustomer')
|
||||
// console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
|
||||
if (DEBUG.value) {
|
||||
console.group('watch importCustomer')
|
||||
console.log(`isDirty: ${isDirty.value}`)
|
||||
}
|
||||
|
||||
// Don't overwrite these values during loading
|
||||
// they can intentionally be different from customer data
|
||||
if (!isLoading.value) {
|
||||
if (dataLoaded.value) {
|
||||
if (!invoice.value.billingData) invoice.value.billingData = newBillingData()
|
||||
|
||||
// console.warn('trigger invoice watcher')
|
||||
@@ -232,8 +240,10 @@ watch(importCustomer,
|
||||
isDirty.value = true;
|
||||
}
|
||||
|
||||
// console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
|
||||
// console.groupEnd()
|
||||
if (DEBUG.value) {
|
||||
console.log(`isDirty: ${isDirty.value}`)
|
||||
console.groupEnd()
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
@@ -243,10 +253,12 @@ watch(importContact,
|
||||
if (newValue == oldValue) return
|
||||
if (!invoice.value) return
|
||||
|
||||
// console.group('watch importContact')
|
||||
// console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
|
||||
if (DEBUG.value) {
|
||||
console.group('watch importContact')
|
||||
console.log(`isDirty: ${isDirty.value}`)
|
||||
}
|
||||
|
||||
if (!isLoading.value) {
|
||||
if (dataLoaded.value) {
|
||||
if (newValue.id !== 0) {
|
||||
invoice.value.billingData!.contactFirstName = newValue.firstName
|
||||
invoice.value.billingData!.contactLastName = newValue.lastName
|
||||
@@ -258,8 +270,10 @@ watch(importContact,
|
||||
isDirty.value = true;
|
||||
}
|
||||
|
||||
// console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`)
|
||||
// console.groupEnd()
|
||||
if (DEBUG.value) {
|
||||
console.log(`isDirty: ${isDirty.value}`)
|
||||
console.groupEnd()
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
@@ -278,7 +292,7 @@ const save = async () => {
|
||||
isSaving.value = true
|
||||
|
||||
try {
|
||||
// Prepare the invoice data for API request
|
||||
// Prepare the invoice data API request
|
||||
const invoiceToSave = {
|
||||
nr: invoice.value.nr,
|
||||
invoiceDate: invoice.value.invoiceDate,
|
||||
@@ -322,11 +336,10 @@ const save = async () => {
|
||||
invoice.value = response.data;
|
||||
} else {
|
||||
// Update existing invoice
|
||||
const response = await axios.put(`/api/invoices/${invoice.value.id}`, invoiceToSave);
|
||||
await axios.put(`/api/invoices/${invoice.value.id}`, invoiceToSave);
|
||||
}
|
||||
|
||||
emit('save', invoice.value)
|
||||
// isOpen.value = false
|
||||
} catch (error) {
|
||||
toast.error("Rechnung konnte nicht gespeichert werden", {
|
||||
description: (error as Error).message,
|
||||
@@ -334,7 +347,7 @@ const save = async () => {
|
||||
} finally {
|
||||
// remove spinner from save button
|
||||
isSaving.value = false
|
||||
setTimeout(() => { isDirty.value = false }, 1000)
|
||||
isDirty.value = false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -369,12 +382,12 @@ const preview = function () {
|
||||
window?.open('/invoice/' + invoice.value.id, '_blank')?.focus();
|
||||
}
|
||||
|
||||
const downloadPdf = function () {
|
||||
const exportPdf = function () {
|
||||
if (!invoice.value) return;
|
||||
window?.open('/invoice/' + invoice.value.id + '/pdf');
|
||||
}
|
||||
|
||||
const downloadXml = function () {
|
||||
const exportXml = function () {
|
||||
if (!invoice.value) return;
|
||||
window?.open('/invoice/' + invoice.value.id + '/xml');
|
||||
}
|
||||
@@ -444,11 +457,11 @@ const sendReminder = async function (to: string | undefined, cc: string | undefi
|
||||
}
|
||||
|
||||
const updateLineItems = (newItems: LineItem[]) => {
|
||||
if (isLoading.value) return;
|
||||
if (!dataLoaded.value) return;
|
||||
if (!invoice.value) return;
|
||||
|
||||
// console.group('updateLineItems');
|
||||
// console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`);
|
||||
console.group('updateLineItems');
|
||||
console.log(`isDirty: ${isDirty.value}`);
|
||||
|
||||
// Konvertiere die neuen Items in normale Objekte
|
||||
const rawItems = toRaw(newItems) || [];
|
||||
@@ -456,13 +469,13 @@ const updateLineItems = (newItems: LineItem[]) => {
|
||||
// Sortiere die Items nach position
|
||||
const sortedItems = [...rawItems].sort((a, b) => a.position - b.position);
|
||||
|
||||
// Erstellen Sie eine tiefe Kopie der neuen Items
|
||||
// Create a deep copy of the new items
|
||||
const updatedItems = JSON.parse(JSON.stringify(sortedItems));
|
||||
|
||||
// Aktualisieren Sie die Items in der Rechnung
|
||||
// Update the invoice items
|
||||
invoice.value.items = updatedItems;
|
||||
|
||||
// Berechnen Sie den neuen Gesamtbetrag
|
||||
// Calculate the new total amount
|
||||
let total = 0;
|
||||
updatedItems.forEach(item => {
|
||||
total += item.quantity * item.price;
|
||||
@@ -472,11 +485,13 @@ const updateLineItems = (newItems: LineItem[]) => {
|
||||
// Erzwingen Sie eine Aktualisierung der Benutzeroberfläche
|
||||
invoice.value = { ...invoice.value };
|
||||
|
||||
// console.log(`isDirty: ${isDirty.value}\tisLoading: ${isLoading.value}`);
|
||||
// console.groupEnd();
|
||||
console.log(`isDirty: ${isDirty.value}`);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
const importLineItems = () => {
|
||||
if (!isOpen) return
|
||||
|
||||
if (!fileInput.value) {
|
||||
fileInput.value = document.createElement('input');
|
||||
fileInput.value.type = 'file';
|
||||
@@ -495,18 +510,14 @@ const handleFileUpload = async (event: Event) => {
|
||||
formData.append('csv', file);
|
||||
|
||||
try {
|
||||
if (!invoice.value || !invoice.value.id) {
|
||||
throw new Error('Rechnung muss gespeichert werden, bevor Positionen importiert werden können');
|
||||
}
|
||||
|
||||
const response = await axios.post(`/api/lineitems/import/${invoice.value.id}`, formData, {
|
||||
const response = await axios.post(`/api/lineitems/import`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
if (invoice.value) {
|
||||
invoice.value.items = response.data;
|
||||
invoice.value.items = invoice.value.items.concat(response.data as LineItem[]);
|
||||
isDirty.value = true;
|
||||
}
|
||||
|
||||
@@ -539,7 +550,7 @@ const handleFileUpload = async (event: Event) => {
|
||||
<h1>{{ title }}</h1>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Input v-if="invoice" v-model="invoice.title"
|
||||
<Input v-if="invoice" v-model="invoice.title" :id="'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>
|
||||
@@ -550,8 +561,8 @@ const handleFileUpload = async (event: Event) => {
|
||||
<!-- Save -->
|
||||
<Button v-if="invoice && isDirty" class="grow md:grow-0" size="sm" @click="save"
|
||||
:disabled="isSaving">
|
||||
<Loader2 v-if="isSaving" stroke-width="1.5" class="animate-spin" />
|
||||
<Check v-else stroke-width="1.5" />
|
||||
<Loader2 v-if="isSaving" class="animate-spin" />
|
||||
<Check v-else />
|
||||
Speichern
|
||||
</Button>
|
||||
|
||||
@@ -573,7 +584,7 @@ const handleFileUpload = async (event: Event) => {
|
||||
<Tooltip v-if="invoice && ['issued', 'due', 'reminded'].includes(invoice.paymentStatus)">
|
||||
<TooltipTrigger>
|
||||
<Button size="sm" variant="success" @click="updateStatus('paid')">
|
||||
<FileCheck stroke-width="1.5" /> Bezahlt
|
||||
<FileCheck /> Bezahlt
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -587,7 +598,6 @@ const handleFileUpload = async (event: Event) => {
|
||||
<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>
|
||||
@@ -599,10 +609,10 @@ const handleFileUpload = async (event: Event) => {
|
||||
|
||||
|
||||
<!-- Ellipsis menu -->
|
||||
<DropdownMenu v-if="invoice && invoice.id > 0">
|
||||
<DropdownMenu v-if="invoice">
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="ghost" size="sm" class="px-0! w-7 ml-2">
|
||||
<Ellipsis class="size-4" stroke-width="1.5" />
|
||||
<Ellipsis class="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -612,7 +622,7 @@ const handleFileUpload = async (event: Event) => {
|
||||
<!-- Import Items -->
|
||||
<DropdownMenuItem class="flex items-center justify-between" @click="importLineItems">
|
||||
<div class="flex items-center gap-3">
|
||||
<Import :strokeWidth="1.5" class="text-muted-foreground" />
|
||||
<Import class="text-muted-foreground" />
|
||||
<div class="mr-4 flex flex-col">
|
||||
<span>Posten importieren</span>
|
||||
<span class="text-xs text-muted-foreground">(CSV)</span>
|
||||
@@ -628,10 +638,9 @@ const handleFileUpload = async (event: Event) => {
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<!-- Preview -->
|
||||
<DropdownMenuItem v-if="invoice?.paymentStatus == 'draft'"
|
||||
class="flex items-center justify-between" @click="preview">
|
||||
<DropdownMenuItem class="flex items-center justify-between" @click="preview">
|
||||
<div class="flex items-center gap-3">
|
||||
<Eye :strokeWidth="1.5" class="text-muted-foreground" />
|
||||
<Eye class="text-muted-foreground" />
|
||||
<span class="mr-4">Vorschau</span>
|
||||
</div>
|
||||
<KbdGroup>
|
||||
@@ -643,9 +652,9 @@ const handleFileUpload = async (event: Event) => {
|
||||
|
||||
<!-- PDF -->
|
||||
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'"
|
||||
class="flex justify-between" @click="downloadPdf">
|
||||
class="flex justify-between" @click="exportPdf">
|
||||
<div class="flex items-center gap-3">
|
||||
<FileText stroke-width="1.5" class="text-muted-foreground" />
|
||||
<FileText class="text-muted-foreground" />
|
||||
<div class="mr-4 flex flex-col">
|
||||
<span>PDF exportieren</span>
|
||||
<span class="text-xs text-muted-foreground">(ZUGFeRD)</span>
|
||||
@@ -660,9 +669,9 @@ const handleFileUpload = async (event: Event) => {
|
||||
|
||||
<!-- XML -->
|
||||
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'"
|
||||
class="flex justify-between" @click="downloadXml">
|
||||
class="flex justify-between" @click="exportXml">
|
||||
<div class="flex items-center gap-3">
|
||||
<CodeXml stroke-width="1.5" class="text-muted-foreground" />
|
||||
<CodeXml class="text-muted-foreground" />
|
||||
<div class="mr-4 flex flex-col">
|
||||
<span>XML exportieren</span>
|
||||
<span class="text-xs text-muted-foreground">(XRechnung)</span>
|
||||
@@ -676,7 +685,7 @@ const handleFileUpload = async (event: Event) => {
|
||||
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'"
|
||||
class="flex justify-between" @click="" disabled>
|
||||
<div class="flex items-center gap-3">
|
||||
<Logs stroke-width="1.5" class="text-muted-foreground" />
|
||||
<Logs class="text-muted-foreground" />
|
||||
<span>Audit</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
@@ -688,8 +697,8 @@ const handleFileUpload = async (event: Event) => {
|
||||
v-if="invoice && ['issued', 'due', 'reminded'].includes(invoice.paymentStatus)"
|
||||
class="flex justify-between" @click="updateStatus('cancelled')">
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- <FileX stroke-width="1.5" class="text-muted-foreground"/> -->
|
||||
<Ban :strokeWidth="1.5" class="text-current" />
|
||||
<!-- <FileX class="text-muted-foreground"/> -->
|
||||
<Ban class="text-current" />
|
||||
<span class="mr-2">Stornieren</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
@@ -699,7 +708,7 @@ const handleFileUpload = async (event: Event) => {
|
||||
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" />
|
||||
<Trash2 class="text-current" />
|
||||
<span class="mr-2">Löschen</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
@@ -832,7 +841,7 @@ const handleFileUpload = async (event: Event) => {
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<MessageCircleQuestion strokeWidth="1.5"
|
||||
<MessageCircleQuestion
|
||||
class="h-[16px] w-[16px] ml-[1px] mb-2" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -851,7 +860,7 @@ const handleFileUpload = async (event: Event) => {
|
||||
<SelectValue placeholder="Zahlungsziel" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="term in paymentTermsData" :value="term">
|
||||
<SelectItem v-for="term in paymentTerms" :value="term">
|
||||
<span v-if="term.isFixed">{{ term.description }}</span>
|
||||
<span v-else>{{ term.days }} Tage</span>
|
||||
</SelectItem>
|
||||
|
||||
Reference in New Issue
Block a user