262 lines
7.9 KiB
Vue
262 lines
7.9 KiB
Vue
<script setup>
|
|
import dayjs from "dayjs"
|
|
|
|
const props = defineProps({
|
|
item: {
|
|
required: true,
|
|
type: Object
|
|
}
|
|
})
|
|
|
|
const loading = ref(true)
|
|
const incomingInvoices = ref([])
|
|
const costcentres = ref([])
|
|
const selectedYear = ref(String(dayjs().year()))
|
|
const selectedMonth = ref("all")
|
|
|
|
const currency = (value) => `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`
|
|
|
|
const costCentreMap = computed(() => {
|
|
return new Map(costcentres.value.map((costcentre) => [costcentre.id, costcentre]))
|
|
})
|
|
|
|
const relevantCostCentreIds = computed(() => {
|
|
const rootId = props.item?.id
|
|
|
|
if (!rootId) {
|
|
return new Set()
|
|
}
|
|
|
|
const childrenByParent = new Map()
|
|
|
|
costcentres.value.forEach((costcentre) => {
|
|
if (!costcentre.parentCostcentre) {
|
|
return
|
|
}
|
|
|
|
if (!childrenByParent.has(costcentre.parentCostcentre)) {
|
|
childrenByParent.set(costcentre.parentCostcentre, [])
|
|
}
|
|
|
|
childrenByParent.get(costcentre.parentCostcentre).push(costcentre.id)
|
|
})
|
|
|
|
const collectedIds = new Set([rootId])
|
|
const queue = [rootId]
|
|
|
|
while (queue.length > 0) {
|
|
const currentId = queue.shift()
|
|
const childIds = childrenByParent.get(currentId) || []
|
|
|
|
childIds.forEach((childId) => {
|
|
if (collectedIds.has(childId)) {
|
|
return
|
|
}
|
|
|
|
collectedIds.add(childId)
|
|
queue.push(childId)
|
|
})
|
|
}
|
|
|
|
return collectedIds
|
|
})
|
|
|
|
const yearItems = computed(() => {
|
|
const years = [...new Set(
|
|
incomingInvoices.value
|
|
.map((invoice) => invoice.date ? String(dayjs(invoice.date).year()) : null)
|
|
.filter(Boolean)
|
|
)].sort((a, b) => Number(b) - Number(a))
|
|
|
|
return years.length > 0 ? years.map((year) => ({ label: year, value: year })) : [{ label: String(dayjs().year()), value: String(dayjs().year()) }]
|
|
})
|
|
|
|
const monthItems = [
|
|
{ label: "Ganzes Jahr", value: "all" },
|
|
{ label: "Januar", value: "1" },
|
|
{ label: "Februar", value: "2" },
|
|
{ label: "März", value: "3" },
|
|
{ label: "April", value: "4" },
|
|
{ label: "Mai", value: "5" },
|
|
{ label: "Juni", value: "6" },
|
|
{ label: "Juli", value: "7" },
|
|
{ label: "August", value: "8" },
|
|
{ label: "September", value: "9" },
|
|
{ label: "Oktober", value: "10" },
|
|
{ label: "November", value: "11" },
|
|
{ label: "Dezember", value: "12" }
|
|
]
|
|
|
|
const reportRows = computed(() => {
|
|
return incomingInvoices.value.flatMap((invoice) => {
|
|
const invoiceDate = invoice.date ? dayjs(invoice.date) : null
|
|
|
|
if (invoiceDate && invoiceDate.year().toString() !== selectedYear.value) {
|
|
return []
|
|
}
|
|
|
|
if (invoiceDate && selectedMonth.value !== "all" && invoiceDate.month() + 1 !== Number(selectedMonth.value)) {
|
|
return []
|
|
}
|
|
|
|
const matchingAccounts = (invoice.accounts || []).filter((account) =>
|
|
relevantCostCentreIds.value.has(account.costCentre)
|
|
)
|
|
|
|
return matchingAccounts.map((account, index) => {
|
|
const amountNet = Number(account.amountNet || 0)
|
|
const amountTax = Number(account.amountTax || 0)
|
|
const amountGross = Number(account.amountGross || amountNet + amountTax || 0)
|
|
const accountCostCentre = costCentreMap.value.get(account.costCentre)
|
|
|
|
return {
|
|
id: `${invoice.id}-${index}`,
|
|
invoiceId: invoice.id,
|
|
reference: invoice.reference || "-",
|
|
date: invoice.date,
|
|
state: invoice.state || "-",
|
|
vendorName: invoice.vendor?.name || "-",
|
|
accountLabel: account.account?.label || account.accountLabel || "-",
|
|
costCentreName: accountCostCentre ? `${accountCostCentre.number} - ${accountCostCentre.name}` : "-",
|
|
description: account.description || invoice.description || "-",
|
|
amountNet,
|
|
amountTax,
|
|
amountGross
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
const totals = computed(() => {
|
|
return reportRows.value.reduce((acc, row) => {
|
|
acc.net += row.amountNet
|
|
acc.tax += row.amountTax
|
|
acc.gross += row.amountGross
|
|
return acc
|
|
}, { net: 0, tax: 0, gross: 0 })
|
|
})
|
|
|
|
const columns = [
|
|
{ accessorKey: "reference", header: "Beleg" },
|
|
{ accessorKey: "date", header: "Datum" },
|
|
{ accessorKey: "vendorName", header: "Lieferant" },
|
|
{ accessorKey: "accountLabel", header: "Konto" },
|
|
{ accessorKey: "costCentreName", header: "Kostenstelle" },
|
|
{ accessorKey: "description", header: "Beschreibung" },
|
|
{ accessorKey: "amountNet", header: "Netto" },
|
|
{ accessorKey: "amountTax", header: "Steuer" },
|
|
{ accessorKey: "amountGross", header: "Brutto" }
|
|
]
|
|
|
|
const setupPage = async () => {
|
|
loading.value = true
|
|
|
|
costcentres.value = await useEntities("costcentres").select("*", null, false, true)
|
|
const invoices = await useEntities("incominginvoices").select("*, vendor(id,name)")
|
|
|
|
incomingInvoices.value = invoices.filter((invoice) =>
|
|
(invoice.accounts || []).some((account) => relevantCostCentreIds.value.has(account.costCentre))
|
|
)
|
|
|
|
const firstYear = yearItems.value[0]?.value
|
|
if (firstYear && !yearItems.value.some((item) => item.value === selectedYear.value)) {
|
|
selectedYear.value = firstYear
|
|
}
|
|
|
|
loading.value = false
|
|
}
|
|
|
|
setupPage()
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-4">
|
|
<div class="flex flex-col gap-3 md:flex-row md:items-end">
|
|
<UFormField label="Jahr" class="w-full md:w-48">
|
|
<USelectMenu
|
|
v-model="selectedYear"
|
|
:items="yearItems"
|
|
value-key="value"
|
|
label-key="label"
|
|
class="w-full"
|
|
/>
|
|
</UFormField>
|
|
|
|
<UFormField label="Monat" class="w-full md:w-56">
|
|
<USelectMenu
|
|
v-model="selectedMonth"
|
|
:items="monthItems"
|
|
value-key="value"
|
|
label-key="label"
|
|
class="w-full"
|
|
/>
|
|
</UFormField>
|
|
</div>
|
|
|
|
<div class="grid gap-3 md:grid-cols-3">
|
|
<UCard>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Netto gesamt</div>
|
|
<div class="mt-1 text-xl font-semibold">{{ currency(totals.net) }}</div>
|
|
</UCard>
|
|
|
|
<UCard>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Steuer gesamt</div>
|
|
<div class="mt-1 text-xl font-semibold">{{ currency(totals.tax) }}</div>
|
|
</UCard>
|
|
|
|
<UCard>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Brutto gesamt</div>
|
|
<div class="mt-1 text-xl font-semibold">{{ currency(totals.gross) }}</div>
|
|
</UCard>
|
|
</div>
|
|
|
|
<UTable
|
|
v-if="!loading"
|
|
:data="reportRows"
|
|
:columns="columns"
|
|
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Eingangsbelege mit dieser Kostenstelle oder ihren Unterkostenstellen gefunden' }"
|
|
class="w-full"
|
|
>
|
|
<template #reference-cell="{ row }">
|
|
<div class="truncate font-medium">{{ row.original.reference }}</div>
|
|
</template>
|
|
|
|
<template #date-cell="{ row }">
|
|
<div class="truncate">{{ row.original.date ? dayjs(row.original.date).format("DD.MM.YYYY") : "-" }}</div>
|
|
</template>
|
|
|
|
<template #vendorName-cell="{ row }">
|
|
<div class="truncate">{{ row.original.vendorName }}</div>
|
|
</template>
|
|
|
|
<template #accountLabel-cell="{ row }">
|
|
<div class="truncate">{{ row.original.accountLabel }}</div>
|
|
</template>
|
|
|
|
<template #costCentreName-cell="{ row }">
|
|
<div class="truncate">{{ row.original.costCentreName }}</div>
|
|
</template>
|
|
|
|
<template #description-cell="{ row }">
|
|
<UTooltip :text="row.original.description">
|
|
<div class="max-w-[18rem] truncate">{{ row.original.description }}</div>
|
|
</UTooltip>
|
|
</template>
|
|
|
|
<template #amountNet-cell="{ row }">
|
|
<div class="text-right tabular-nums">{{ currency(row.original.amountNet) }}</div>
|
|
</template>
|
|
|
|
<template #amountTax-cell="{ row }">
|
|
<div class="text-right tabular-nums">{{ currency(row.original.amountTax) }}</div>
|
|
</template>
|
|
|
|
<template #amountGross-cell="{ row }">
|
|
<div class="text-right font-medium tabular-nums">{{ currency(row.original.amountGross) }}</div>
|
|
</template>
|
|
</UTable>
|
|
|
|
<UProgress v-else animation="carousel" class="w-3/4 mx-auto" />
|
|
</div>
|
|
</template>
|