Files
FEDEO/frontend/pages/createDocument/show/[id].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>