453 lines
16 KiB
Vue
453 lines
16 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)
|
|
}
|
|
|
|
linkedDocument.value = await useFiles().selectDocument(itemInfo.value.files[0].id)
|
|
}
|
|
}
|
|
|
|
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
|
|
: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>
|