Auf der Belegansicht erscheint "PDF nacherzeugen", wenn ein fertiggestellter Beleg kein angehängtes Dokument hat. Die PDF wird mit derselben Logik wie beim Fertigstellen erzeugt und angehängt, ohne den Datensatz zu speichern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
467 lines
17 KiB
Vue
467 lines
17 KiB
Vue
<script setup>
|
|
import CopyCreatedDocumentModal from "~/components/copyCreatedDocumentModal.vue";
|
|
import dayjs from "dayjs";
|
|
|
|
|
|
|
|
defineShortcuts({
|
|
'backspace': () => {
|
|
router.push("/createDocument")
|
|
},
|
|
})
|
|
|
|
const modal = useModal()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const auth = useAuthStore()
|
|
const dataStore = useDataStore()
|
|
const toast = useToast()
|
|
|
|
const itemInfo = ref({})
|
|
const linkedDocument =ref({})
|
|
const links = ref([])
|
|
const portalReleaseLoading = ref(false)
|
|
const bankBookingModalOpen = ref(false)
|
|
const costCentresOpen = ref(false)
|
|
const costCentresLoading = ref(false)
|
|
const costCentresSaving = ref(false)
|
|
const costCentres = ref([])
|
|
const costCentreDraft = ref({ costcentre: null, rows: [] })
|
|
const costCentreId = (value) => value?.id || value || null
|
|
const costCentreLabel = (id) => {
|
|
const centre = costCentres.value.find(item => item.id === id)
|
|
return centre ? [centre.number, centre.name].filter(Boolean).join(" - ") : (id ? "Bisherige Kostenstelle" : "Keine Kostenstelle")
|
|
}
|
|
|
|
const openCostCentres = async () => {
|
|
costCentresLoading.value = true
|
|
try {
|
|
costCentres.value = await useEntities("costcentres").select("*")
|
|
costCentreDraft.value = {
|
|
costcentre: costCentreId(itemInfo.value.costcentre),
|
|
rows: (itemInfo.value.rows || [])
|
|
.filter(row => row?.id != null && row.id !== "" && !["title", "subtitle", "text", "pagebreak"].includes(row.mode))
|
|
.map((row, index) => ({
|
|
rowId: String(row.id),
|
|
label: `${row.pos || index + 1}. ${row.text || row.name || row.descriptionText || "Position"}`,
|
|
costcentre: costCentreId(row.costCentre ?? row.costcentre),
|
|
})),
|
|
}
|
|
costCentresOpen.value = true
|
|
} catch (error) {
|
|
toast.add({ title: "Kostenstellen konnten nicht geladen werden", color: "error" })
|
|
} finally {
|
|
costCentresLoading.value = false
|
|
}
|
|
}
|
|
|
|
const saveCostCentres = async () => {
|
|
if (costCentresSaving.value) return
|
|
costCentresSaving.value = true
|
|
try {
|
|
const originalRows = new Map((itemInfo.value.rows || []).map(row => [String(row.id), row]))
|
|
const body = {
|
|
rowCostCentres: costCentreDraft.value.rows
|
|
.filter(row => row.costcentre !== costCentreId(originalRows.get(row.rowId)?.costCentre ?? originalRows.get(row.rowId)?.costcentre))
|
|
.map(({ rowId, costcentre }) => ({ rowId, costcentre })),
|
|
}
|
|
if (costCentreDraft.value.costcentre !== costCentreId(itemInfo.value.costcentre)) {
|
|
body.costcentre = costCentreDraft.value.costcentre
|
|
}
|
|
if (body.rowCostCentres.length || Object.hasOwn(body, "costcentre")) {
|
|
const { document } = await useNuxtApp().$api(`/api/functions/outgoing-documents/${itemInfo.value.id}/cost-centres`, {
|
|
method: "PUT",
|
|
body,
|
|
})
|
|
itemInfo.value.costcentre = document.costcentre
|
|
itemInfo.value.rows = document.rows
|
|
toast.add({ title: "Kostenstellen gespeichert", color: "success" })
|
|
}
|
|
costCentresOpen.value = false
|
|
} catch (error) {
|
|
toast.add({ title: "Kostenstellen konnten nicht gespeichert werden", description: error?.data?.error || "Bitte versuche es erneut.", color: "error" })
|
|
} finally {
|
|
costCentresSaving.value = false
|
|
}
|
|
}
|
|
|
|
const portalEligibleTypes = ["invoices", "advanceInvoices", "cancellationInvoices"]
|
|
|
|
const loadStatementAllocations = async () => {
|
|
if (!itemInfo.value?.id) return
|
|
|
|
const statementAllocations = await useEntities("statementallocations").select("*, bankstatement(*)")
|
|
itemInfo.value.statementallocations = statementAllocations.filter((allocation) => {
|
|
const createdDocumentId = allocation.createddocument?.id || allocation.createddocument
|
|
|
|
return String(createdDocumentId) === String(itemInfo.value.id)
|
|
})
|
|
}
|
|
|
|
const setupPage = async () => {
|
|
if(route.params) {
|
|
if(route.params.id) {
|
|
itemInfo.value = await useEntities("createddocuments").selectSingle(route.params.id,"*,files(*),linkedDocument(*)")
|
|
await loadStatementAllocations()
|
|
}
|
|
|
|
if(itemInfo.value.type === "invoices"){
|
|
const createddocuments = await useEntities("createddocuments").select()
|
|
links.value = createddocuments.filter(i => i.createddocument?.id === itemInfo.value.id)
|
|
}
|
|
|
|
if (itemInfo.value.files?.length > 0) {
|
|
linkedDocument.value = await useFiles().selectDocument(itemInfo.value.files[0].id)
|
|
}
|
|
}
|
|
}
|
|
|
|
const canRegeneratePdf = computed(() => itemInfo.value.id
|
|
&& itemInfo.value.state !== 'Entwurf'
|
|
&& itemInfo.value.type !== 'serialInvoices'
|
|
&& Array.isArray(itemInfo.value.files)
|
|
&& itemInfo.value.files.length === 0)
|
|
|
|
setupPage()
|
|
|
|
const getStatementLike = (allocation) => allocation?.bankstatement || (typeof allocation?.bs_id === "object" ? allocation.bs_id : null)
|
|
const getStatementId = (allocation) => allocation?.bankstatement?.id || allocation?.bs_id?.id || allocation?.bankstatement || allocation?.bs_id || null
|
|
const getBankBookingDate = (allocation) => {
|
|
const statement = getStatementLike(allocation)
|
|
|
|
return statement?.date || statement?.valueDate || allocation?.manualBookingDate || null
|
|
}
|
|
const formatDate = (value) => value ? dayjs(value).format("DD.MM.YYYY") : "-"
|
|
const formatCurrency = (value) => `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`
|
|
const bankBookingDates = computed(() => [...new Set(
|
|
(itemInfo.value.statementallocations || [])
|
|
.map(getBankBookingDate)
|
|
.filter(Boolean)
|
|
)].sort())
|
|
const bankBookingDateLabel = computed(() => {
|
|
if (bankBookingDates.value.length === 0) return "Kein Bankbuchungsdatum"
|
|
if (bankBookingDates.value.length === 1) return formatDate(bankBookingDates.value[0])
|
|
|
|
return bankBookingDates.value.map(formatDate).join(", ")
|
|
})
|
|
const linkedDocumentLabel = (document) => {
|
|
const documentType = dataStore.documentTypesForCreation[document.type]?.labelSingle || "Dokument"
|
|
|
|
return `${documentType} - ${document.documentNumber || document.title || document.id}`
|
|
}
|
|
const linkedItems = computed(() => {
|
|
const items = []
|
|
|
|
if(itemInfo.value.project) {
|
|
items.push({
|
|
label: "Projekt",
|
|
icon: "i-heroicons-link",
|
|
onSelect: () => router.push(`/standardEntity/projects/show/${itemInfo.value.project?.id}`)
|
|
})
|
|
}
|
|
|
|
if(itemInfo.value.customer) {
|
|
items.push({
|
|
label: "Kunde",
|
|
icon: "i-heroicons-link",
|
|
onSelect: () => router.push(`/standardEntity/customers/show/${itemInfo.value.customer?.id}`)
|
|
})
|
|
}
|
|
|
|
if(itemInfo.value.plant) {
|
|
items.push({
|
|
label: "Objekt",
|
|
icon: "i-heroicons-link",
|
|
onSelect: () => router.push(`/standardEntity/plants/show/${itemInfo.value.plant?.id}`)
|
|
})
|
|
}
|
|
|
|
if(itemInfo.value.contract) {
|
|
items.push({
|
|
label: "Vertrag",
|
|
icon: "i-heroicons-link",
|
|
onSelect: () => router.push(`/standardEntity/contracts/show/${itemInfo.value.contract?.id}`)
|
|
})
|
|
}
|
|
|
|
if(itemInfo.value.contact) {
|
|
items.push({
|
|
label: "Ansprechpartner",
|
|
icon: "i-heroicons-link",
|
|
onSelect: () => router.push(`/standardEntity/contacts/show/${itemInfo.value.contact?.id}`)
|
|
})
|
|
}
|
|
|
|
if(itemInfo.value.createddocument) {
|
|
items.push({
|
|
label: linkedDocumentLabel(itemInfo.value.createddocument),
|
|
icon: "i-heroicons-document-duplicate",
|
|
onSelect: () => router.push(`/createDocument/show/${itemInfo.value.createddocument.id}`)
|
|
})
|
|
}
|
|
|
|
;(itemInfo.value.createddocuments || []).forEach((document) => {
|
|
items.push({
|
|
label: linkedDocumentLabel(document),
|
|
icon: "i-heroicons-document-duplicate",
|
|
onSelect: () => router.push(`/createDocument/show/${document.id}`)
|
|
})
|
|
})
|
|
|
|
if(itemInfo.value.statementallocations?.length > 0) {
|
|
items.push({
|
|
label: "Bankbuchungen öffnen",
|
|
icon: "i-heroicons-building-library",
|
|
onSelect: openBankstatements
|
|
})
|
|
items.push({
|
|
label: "Bankdetails anzeigen",
|
|
icon: "i-heroicons-calendar-days",
|
|
onSelect: () => { bankBookingModalOpen.value = true }
|
|
})
|
|
}
|
|
|
|
return [items]
|
|
})
|
|
|
|
const openEmail = () => {
|
|
if(["invoices","advanceInvoices"].includes(itemInfo.value.type)){
|
|
router.push(`/email/new?loadDocuments=["${linkedDocument.value.id}"]&bcc=${encodeURIComponent(auth.activeTenantData.standardEmailForInvoices || "")}`)
|
|
} else {
|
|
router.push(`/email/new?loadDocuments=["${linkedDocument.value.id}"]`)
|
|
}
|
|
}
|
|
|
|
const openBankstatements = () => {
|
|
const statementIds = (itemInfo.value.statementallocations || []).map(getStatementId).filter(Boolean)
|
|
|
|
if(statementIds.length === 0) return
|
|
|
|
if(statementIds.length > 1) {
|
|
navigateTo(`/banking/?filter=${JSON.stringify(statementIds)}`)
|
|
} else {
|
|
navigateTo(`/banking/statements/edit/${statementIds[0]}`)
|
|
}
|
|
}
|
|
|
|
const togglePortalRelease = async () => {
|
|
if (!itemInfo.value?.id) return
|
|
|
|
portalReleaseLoading.value = true
|
|
|
|
try {
|
|
const nextValue = !itemInfo.value.availableInPortal
|
|
await useEntities("createddocuments").update(itemInfo.value.id, {
|
|
availableInPortal: nextValue
|
|
}, true)
|
|
|
|
itemInfo.value = {
|
|
...itemInfo.value,
|
|
availableInPortal: nextValue
|
|
}
|
|
|
|
toast.add({
|
|
title: nextValue ? "Für Kundenportal freigegeben" : "Portal-Freigabe entfernt"
|
|
})
|
|
} finally {
|
|
portalReleaseLoading.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<UDashboardNavbar
|
|
title="Erstelltes Dokument anzeigen"
|
|
>
|
|
|
|
</UDashboardNavbar>
|
|
<UDashboardToolbar>
|
|
<template #left>
|
|
<UButton
|
|
@click="router.push(`/createDocument/edit/${itemInfo.id}`)"
|
|
v-if="itemInfo.state === 'Entwurf'"
|
|
>
|
|
Bearbeiten
|
|
</UButton>
|
|
<UButton
|
|
v-if="itemInfo.state === 'Gebucht' && itemInfo.type !== 'serialInvoices'"
|
|
icon="i-heroicons-tag"
|
|
variant="outline"
|
|
:loading="costCentresLoading"
|
|
@click="openCostCentres"
|
|
>Kostenstellen bearbeiten</UButton>
|
|
<UButton
|
|
v-if="canRegeneratePdf"
|
|
icon="i-heroicons-document-arrow-down"
|
|
variant="outline"
|
|
@click="router.push(`/createDocument/edit/${itemInfo.id}?regeneratePdf=1`)"
|
|
>PDF nacherzeugen</UButton>
|
|
<!-- <UButton
|
|
:to="dataStore.documents.find(i => i.createdDocument === itemInfo.id) ? dataStore.documents.find(i => i.createdDocument === itemInfo.id).url : ''"
|
|
target="_blank"
|
|
>In neuen Tab anzeigen</UButton>-->
|
|
<UButton
|
|
icon="i-heroicons-arrow-right-end-on-rectangle"
|
|
@click="modal.open(CopyCreatedDocumentModal, {
|
|
id: itemInfo.id,
|
|
type: itemInfo.type
|
|
})"
|
|
variant="outline"
|
|
>
|
|
Kopieren
|
|
</UButton>
|
|
<UButton
|
|
icon="i-heroicons-document-duplicate"
|
|
variant="outline"
|
|
@click="router.push(`/createDocument/edit?templateFromDocument=${itemInfo.id}&type=${itemInfo.type}`)"
|
|
>
|
|
Als Vorlage übernehmen
|
|
</UButton>
|
|
<UButton
|
|
@click="openEmail"
|
|
icon="i-heroicons-envelope"
|
|
>
|
|
E-Mail
|
|
</UButton>
|
|
<UButton
|
|
v-if="portalEligibleTypes.includes(itemInfo.type) && itemInfo.state !== 'Entwurf'"
|
|
:icon="itemInfo.availableInPortal ? 'i-heroicons-eye-slash' : 'i-heroicons-eye'"
|
|
variant="outline"
|
|
:loading="portalReleaseLoading"
|
|
@click="togglePortalRelease"
|
|
>
|
|
{{ itemInfo.availableInPortal ? "Portal-Freigabe entfernen" : "Für Kundenportal freigeben" }}
|
|
</UButton>
|
|
<UTooltip
|
|
v-if="itemInfo.type === 'invoices' || itemInfo.type === 'advanceInvoices'"
|
|
:text="links.find(i => i.type === 'cancellationInvoices') ? 'Bereits stoniert' : ''"
|
|
>
|
|
<UButton
|
|
@click="router.push(`/createDocument/edit/?createddocument=${itemInfo.id}&loadMode=storno`)"
|
|
variant="outline"
|
|
color="error"
|
|
:disabled="links.find(i => i.type === 'cancellationInvoices')"
|
|
>
|
|
Stornieren
|
|
</UButton>
|
|
</UTooltip>
|
|
|
|
<UDropdownMenu
|
|
v-if="linkedItems[0].length > 0"
|
|
:items="linkedItems"
|
|
:content="{ align: 'start' }"
|
|
>
|
|
<UButton
|
|
icon="i-heroicons-link"
|
|
variant="outline"
|
|
trailing-icon="i-heroicons-chevron-down-20-solid"
|
|
>
|
|
Verknüpfungen
|
|
</UButton>
|
|
</UDropdownMenu>
|
|
<UBadge
|
|
v-if="itemInfo.statementallocations?.length > 0"
|
|
color="primary"
|
|
variant="soft"
|
|
class="self-center"
|
|
>
|
|
Bankbuchungsdatum: {{ bankBookingDateLabel }}
|
|
</UBadge>
|
|
|
|
</template>
|
|
</UDashboardToolbar>
|
|
<UModal v-model:open="costCentresOpen" title="Kostenstellen bearbeiten" :dismissible="!costCentresSaving" :close="!costCentresSaving" :ui="{ content: 'sm:max-w-2xl' }">
|
|
<template #body>
|
|
<p class="mb-4 text-sm text-muted">Die Belegkostenstelle und abweichende Positionskostenstellen können nachträglich geändert oder entfernt werden.</p>
|
|
<div class="space-y-4">
|
|
<UFormField label="Belegkostenstelle">
|
|
<InputGroup>
|
|
<USelectMenu v-model="costCentreDraft.costcentre" :items="costCentres" value-key="id" label-key="name" :filter-fields="['name', 'number']" class="w-full" :disabled="costCentresSaving">
|
|
<template #default>{{ costCentreLabel(costCentreDraft.costcentre) }}</template>
|
|
<template #item="{ item }">{{ [item.number, item.name].filter(Boolean).join(' - ') }}</template>
|
|
</USelectMenu>
|
|
<UButton icon="i-heroicons-x-mark" variant="outline" aria-label="Belegkostenstelle entfernen" :disabled="costCentresSaving || !costCentreDraft.costcentre" @click="costCentreDraft.costcentre = null" />
|
|
</InputGroup>
|
|
</UFormField>
|
|
<UFormField v-for="row in costCentreDraft.rows" :key="row.rowId" :label="row.label">
|
|
<InputGroup>
|
|
<USelectMenu v-model="row.costcentre" :items="costCentres" value-key="id" label-key="name" :filter-fields="['name', 'number']" class="w-full" :disabled="costCentresSaving">
|
|
<template #default>{{ costCentreLabel(row.costcentre) }}</template>
|
|
<template #item="{ item }">{{ [item.number, item.name].filter(Boolean).join(' - ') }}</template>
|
|
</USelectMenu>
|
|
<UButton icon="i-heroicons-x-mark" variant="outline" :aria-label="`Kostenstelle für ${row.label} entfernen`" :disabled="costCentresSaving || !row.costcentre" @click="row.costcentre = null" />
|
|
</InputGroup>
|
|
</UFormField>
|
|
</div>
|
|
</template>
|
|
<template #footer>
|
|
<UButton variant="outline" :disabled="costCentresSaving" @click="costCentresOpen = false">Abbrechen</UButton>
|
|
<UButton :loading="costCentresSaving" @click="saveCostCentres">Kostenstellen speichern</UButton>
|
|
</template>
|
|
</UModal>
|
|
<UModal v-model:open="bankBookingModalOpen" :ui="{ content: 'sm:max-w-2xl' }">
|
|
<template #content>
|
|
<UCard>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="font-semibold">Bankbuchungen</h3>
|
|
<UButton
|
|
icon="i-heroicons-x-mark"
|
|
color="gray"
|
|
variant="ghost"
|
|
@click="bankBookingModalOpen = false"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="space-y-3">
|
|
<div
|
|
v-for="allocation in itemInfo.statementallocations"
|
|
:key="allocation.id"
|
|
class="grid grid-cols-1 gap-2 rounded-md border border-gray-200 p-3 text-sm dark:border-gray-800 md:grid-cols-3"
|
|
>
|
|
<div>
|
|
<p class="text-xs text-gray-500">Bankbuchungsdatum</p>
|
|
<p class="font-medium">{{ formatDate(getBankBookingDate(allocation)) }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs text-gray-500">Betrag</p>
|
|
<p class="font-medium">{{ formatCurrency(allocation.amount) }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs text-gray-500">Text</p>
|
|
<p class="font-medium">{{ getStatementLike(allocation)?.text || allocation.description || "-" }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</template>
|
|
</UModal>
|
|
<UDashboardPanelContent>
|
|
<div
|
|
v-if="portalEligibleTypes.includes(itemInfo.type) && itemInfo.state !== 'Entwurf'"
|
|
class="px-4 py-3"
|
|
>
|
|
<UBadge :color="itemInfo.availableInPortal ? 'primary' : 'neutral'" variant="soft">
|
|
{{ itemInfo.availableInPortal ? "Im Kundenportal sichtbar" : "Nicht im Kundenportal freigegeben" }}
|
|
</UBadge>
|
|
</div>
|
|
<!-- <object
|
|
:data="linkedDocument.url"
|
|
class="w-full previewDocumentMobile"
|
|
/>-->
|
|
<PDFViewer v-if="linkedDocument.id" :file-id="linkedDocument.id" location="show_create_document" />
|
|
|
|
|
|
|
|
|
|
</UDashboardPanelContent>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.previewDocumentMobile {
|
|
aspect-ratio: 1 / 1.414;
|
|
|
|
}
|
|
</style>
|