Ersetzt ungültige UTable-Empty-Props durch einen gemeinsamen Empty-State-Slot, damit leere Tabellen keine Objekt-/JSON-Ausgabe mehr anzeigen.
290 lines
10 KiB
Vue
290 lines
10 KiB
Vue
<script setup lang="ts">
|
|
import dayjs from "dayjs"
|
|
import customParseFormat from "dayjs/plugin/customParseFormat"
|
|
import {
|
|
formatTaxEvaluationPeriodLabel,
|
|
formatTaxEvaluationPeriodRange,
|
|
getCreatedDocumentTaxBreakdown,
|
|
getIncomingInvoiceTaxBreakdown,
|
|
getTaxEvaluationPeriodBounds,
|
|
normalizeTaxEvaluationPeriod,
|
|
shiftTaxEvaluationPeriodStart
|
|
} from "~/composables/useTaxEvaluation"
|
|
|
|
dayjs.extend(customParseFormat)
|
|
|
|
const auth = useAuthStore()
|
|
|
|
const loading = ref(true)
|
|
const createdDocuments = ref<any[]>([])
|
|
const incomingInvoices = ref<any[]>([])
|
|
|
|
const periodType = computed(() => normalizeTaxEvaluationPeriod(auth.activeTenantData?.taxEvaluationPeriod))
|
|
|
|
const formatCurrency = (value: number) => {
|
|
return new Intl.NumberFormat("de-DE", {
|
|
style: "currency",
|
|
currency: "EUR"
|
|
}).format(Number(value || 0))
|
|
}
|
|
|
|
const isRelevantOutputDocument = (doc: any) => {
|
|
return doc?.state === "Gebucht" && ["invoices", "advanceInvoices", "cancellationInvoices"].includes(doc?.type)
|
|
}
|
|
|
|
const isRelevantInputInvoice = (invoice: any) => {
|
|
return invoice?.state === "Gebucht" && !!invoice?.date
|
|
}
|
|
|
|
const loadData = async () => {
|
|
loading.value = true
|
|
|
|
try {
|
|
const [docs, incoming] = await Promise.all([
|
|
useEntities("createddocuments").select(),
|
|
useEntities("incominginvoices").select()
|
|
])
|
|
|
|
createdDocuments.value = (docs || []).filter(isRelevantOutputDocument)
|
|
incomingInvoices.value = (incoming || []).filter(isRelevantInputInvoice)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const periods = computed(() => {
|
|
const currentBounds = getTaxEvaluationPeriodBounds(dayjs(), periodType.value)
|
|
|
|
return Array.from({ length: 8 }, (_, index) => {
|
|
const start = shiftTaxEvaluationPeriodStart(currentBounds.start, periodType.value, -index)
|
|
const bounds = getTaxEvaluationPeriodBounds(start, periodType.value)
|
|
|
|
const outputDocs = createdDocuments.value.filter((doc) => {
|
|
const date = dayjs(doc.documentDate)
|
|
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
|
})
|
|
|
|
const inputDocs = incomingInvoices.value.filter((invoice) => {
|
|
const date = dayjs(invoice.date)
|
|
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
|
})
|
|
|
|
const output = outputDocs.reduce((sum, doc) => {
|
|
const breakdown = getCreatedDocumentTaxBreakdown(doc)
|
|
return {
|
|
net19: sum.net19 + breakdown.net19,
|
|
tax19: sum.tax19 + breakdown.tax19,
|
|
net7: sum.net7 + breakdown.net7,
|
|
tax7: sum.tax7 + breakdown.tax7,
|
|
net0: sum.net0 + breakdown.net0,
|
|
}
|
|
}, { net19: 0, tax19: 0, net7: 0, tax7: 0, net0: 0 })
|
|
|
|
const input = inputDocs.reduce((sum, invoice) => {
|
|
const breakdown = getIncomingInvoiceTaxBreakdown(invoice)
|
|
return {
|
|
net19: sum.net19 + breakdown.net19,
|
|
tax19: sum.tax19 + breakdown.tax19,
|
|
net7: sum.net7 + breakdown.net7,
|
|
tax7: sum.tax7 + breakdown.tax7,
|
|
net0: sum.net0 + breakdown.net0,
|
|
}
|
|
}, { net19: 0, tax19: 0, net7: 0, tax7: 0, net0: 0 })
|
|
|
|
const outputTax = Number((output.tax19 + output.tax7).toFixed(2))
|
|
const inputTax = Number((input.tax19 + input.tax7).toFixed(2))
|
|
const balance = Number((outputTax - inputTax).toFixed(2))
|
|
|
|
return {
|
|
key: bounds.start.format("YYYY-MM-DD"),
|
|
label: formatTaxEvaluationPeriodLabel(bounds.start, periodType.value),
|
|
range: formatTaxEvaluationPeriodRange(bounds.start, periodType.value),
|
|
isCurrent: index === 0,
|
|
outputTax,
|
|
inputTax,
|
|
balance,
|
|
output,
|
|
input,
|
|
outputCount: outputDocs.length,
|
|
inputCount: inputDocs.length,
|
|
}
|
|
})
|
|
})
|
|
|
|
const currentPeriod = computed(() => periods.value[0] || null)
|
|
|
|
const columns = [
|
|
{ key: "label", label: "Zeitraum" },
|
|
{ key: "range", label: "Datumsbereich" },
|
|
{ key: "outputTax", label: "USt Rechnungen" },
|
|
{ key: "inputTax", label: "Vorsteuer" },
|
|
{ key: "balance", label: "Ergebnis" },
|
|
{ key: "documents", label: "Belege" },
|
|
]
|
|
|
|
onMounted(loadData)
|
|
</script>
|
|
|
|
<template>
|
|
<UDashboardNavbar title="USt-Auswertung">
|
|
<template #right>
|
|
<UButton
|
|
icon="i-heroicons-arrow-path"
|
|
variant="outline"
|
|
@click="loadData"
|
|
:loading="loading"
|
|
>
|
|
Aktualisieren
|
|
</UButton>
|
|
</template>
|
|
</UDashboardNavbar>
|
|
|
|
<UDashboardPanelContent class="p-4 md:p-6">
|
|
<div class="mb-6 flex flex-col gap-2">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
Aktueller Zeitraum: {{ currentPeriod?.label }}
|
|
</h2>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
Intervall: {{ periodType === "monthly" ? "monatlich" : periodType === "quarterly" ? "quartalsweise" : "jährlich" }}.
|
|
Berücksichtigt werden gebuchte Ausgangsrechnungen, Abschlags- und Stornorechnungen sowie gebuchte Eingangsbelege mit Datum.
|
|
</p>
|
|
<p v-if="currentPeriod" class="text-sm text-gray-500 dark:text-gray-400">
|
|
{{ currentPeriod.range }}
|
|
</p>
|
|
</div>
|
|
|
|
<div v-if="currentPeriod" class="grid gap-4 md:grid-cols-3">
|
|
<UCard>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Berechnete USt</div>
|
|
<div class="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
|
{{ formatCurrency(currentPeriod.outputTax) }}
|
|
</div>
|
|
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
|
19%: {{ formatCurrency(currentPeriod.output.tax19) }} | 7%: {{ formatCurrency(currentPeriod.output.tax7) }}
|
|
</div>
|
|
</UCard>
|
|
|
|
<UCard>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Vorsteuer</div>
|
|
<div class="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
|
{{ formatCurrency(currentPeriod.inputTax) }}
|
|
</div>
|
|
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
|
19%: {{ formatCurrency(currentPeriod.input.tax19) }} | 7%: {{ formatCurrency(currentPeriod.input.tax7) }}
|
|
</div>
|
|
</UCard>
|
|
|
|
<UCard>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Verrechnetes Ergebnis</div>
|
|
<div
|
|
class="mt-2 text-2xl font-semibold"
|
|
:class="currentPeriod.balance >= 0 ? 'text-amber-600 dark:text-amber-400' : 'text-emerald-600 dark:text-emerald-400'"
|
|
>
|
|
{{ formatCurrency(currentPeriod.balance) }}
|
|
</div>
|
|
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
|
{{ currentPeriod.outputCount }} Ausgangsbelege | {{ currentPeriod.inputCount }} Eingangsbelege
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
|
|
<div v-if="currentPeriod" class="mt-6 grid gap-4 xl:grid-cols-2">
|
|
<UCard>
|
|
<template #header>
|
|
<div class="font-semibold">Ausgangsrechnungen</div>
|
|
</template>
|
|
<div class="space-y-3 text-sm">
|
|
<div class="flex items-center justify-between">
|
|
<span>Netto 19%</span>
|
|
<span>{{ formatCurrency(currentPeriod.output.net19) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>USt 19%</span>
|
|
<span>{{ formatCurrency(currentPeriod.output.tax19) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>Netto 7%</span>
|
|
<span>{{ formatCurrency(currentPeriod.output.net7) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>USt 7%</span>
|
|
<span>{{ formatCurrency(currentPeriod.output.tax7) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>Netto 0%</span>
|
|
<span>{{ formatCurrency(currentPeriod.output.net0) }}</span>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<UCard>
|
|
<template #header>
|
|
<div class="font-semibold">Eingangsbelege</div>
|
|
</template>
|
|
<div class="space-y-3 text-sm">
|
|
<div class="flex items-center justify-between">
|
|
<span>Netto 19%</span>
|
|
<span>{{ formatCurrency(currentPeriod.input.net19) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>Vorsteuer 19%</span>
|
|
<span>{{ formatCurrency(currentPeriod.input.tax19) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>Netto 7%</span>
|
|
<span>{{ formatCurrency(currentPeriod.input.net7) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>Vorsteuer 7%</span>
|
|
<span>{{ formatCurrency(currentPeriod.input.tax7) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>Netto 0%</span>
|
|
<span>{{ formatCurrency(currentPeriod.input.net0) }}</span>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
|
|
<UCard class="mt-6">
|
|
<template #header>
|
|
<div class="font-semibold">Aktueller und vorherige Zeiträume</div>
|
|
</template>
|
|
|
|
<UTable
|
|
:columns="normalizeTableColumns(columns)"
|
|
:data="periods"
|
|
:loading="loading"
|
|
>
|
|
<template #label-cell="{ row }">
|
|
<div class="flex items-center gap-2">
|
|
<span>{{ row.original.label }}</span>
|
|
<UBadge v-if="row.original.isCurrent" color="primary" variant="soft">Aktuell</UBadge>
|
|
</div>
|
|
</template>
|
|
|
|
<template #outputTax-cell="{ row }">
|
|
{{ formatCurrency(row.original.outputTax) }}
|
|
</template>
|
|
|
|
<template #inputTax-cell="{ row }">
|
|
{{ formatCurrency(row.original.inputTax) }}
|
|
</template>
|
|
|
|
<template #balance-cell="{ row }">
|
|
<span :class="row.original.balance >= 0 ? 'text-amber-600 dark:text-amber-400 font-medium' : 'text-emerald-600 dark:text-emerald-400 font-medium'">
|
|
{{ formatCurrency(row.original.balance) }}
|
|
</span>
|
|
</template>
|
|
|
|
<template #documents-cell="{ row }">
|
|
{{ row.original.outputCount }} / {{ row.original.inputCount }}
|
|
</template>
|
|
<template #empty>
|
|
<TableEmptyState label="Keine Daten für die USt-Auswertung vorhanden" icon="i-heroicons-calculator" />
|
|
</template>
|
|
</UTable>
|
|
</UCard>
|
|
</UDashboardPanelContent>
|
|
</template>
|