222 lines
6.1 KiB
Vue
222 lines
6.1 KiB
Vue
<script setup lang="ts">
|
|
import dayjs from "dayjs"
|
|
import {
|
|
getCreatedDocumentTaxBreakdown,
|
|
getIncomingInvoiceTaxBreakdown
|
|
} from "~/composables/useTaxEvaluation"
|
|
|
|
const loading = ref(true)
|
|
const summary = ref({
|
|
label: "",
|
|
income: 0,
|
|
expenses: 0,
|
|
result: 0,
|
|
taxBalance: 0,
|
|
incomeCount: 0,
|
|
expenseCount: 0
|
|
})
|
|
|
|
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 loadSummary = async () => {
|
|
loading.value = true
|
|
|
|
try {
|
|
const bounds = {
|
|
start: dayjs().startOf("month"),
|
|
end: dayjs().endOf("month")
|
|
}
|
|
|
|
const [docs, incoming, allocations] = await Promise.all([
|
|
useEntities("createddocuments").select(),
|
|
useEntities("incominginvoices").select(),
|
|
useEntities("statementallocations").select("*, bankstatement(*)")
|
|
])
|
|
|
|
const outputDocs = (docs || []).filter((doc: any) => {
|
|
if (!isRelevantOutputDocument(doc)) {
|
|
return false
|
|
}
|
|
|
|
const date = dayjs(doc.documentDate)
|
|
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
|
})
|
|
|
|
const inputDocs = (incoming || []).filter((invoice: any) => {
|
|
if (!isRelevantInputInvoice(invoice)) {
|
|
return false
|
|
}
|
|
|
|
const date = dayjs(invoice.date)
|
|
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
|
})
|
|
|
|
const directExpenses = (allocations || []).filter((allocation: any) => {
|
|
if (allocation?.account === null || typeof allocation?.account === "undefined") {
|
|
return false
|
|
}
|
|
|
|
const statementDate = allocation?.bankstatement?.date || allocation?.bankstatement?.valueDate || allocation?.date || allocation?.created_at
|
|
const date = dayjs(statementDate)
|
|
const amount = Number(allocation?.amount || 0)
|
|
|
|
return amount < 0 && date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
|
})
|
|
|
|
const income = outputDocs.reduce((sum: number, doc: any) => {
|
|
return sum + (doc.rows || []).reduce((rowSum: number, row: any) => {
|
|
if (!row || ["pagebreak", "title", "text"].includes(row.mode)) {
|
|
return rowSum
|
|
}
|
|
|
|
const quantity = Number(row.quantity || 0)
|
|
const price = Number(row.price || 0)
|
|
const discountPercent = Number(row.discountPercent || 0)
|
|
|
|
return rowSum + (quantity * price * (1 - discountPercent / 100))
|
|
}, 0)
|
|
}, 0)
|
|
|
|
const invoiceExpenses = inputDocs.reduce((sum: number, invoice: any) => {
|
|
return sum + (invoice.accounts || []).reduce((accountSum: number, account: any) => accountSum + Number(account.amountNet || 0), 0)
|
|
}, 0)
|
|
|
|
const directAccountExpenses = directExpenses.reduce((sum: number, allocation: any) => {
|
|
return sum + Math.abs(Number(allocation.amount || 0))
|
|
}, 0)
|
|
|
|
const outputTax = outputDocs.reduce((sum: number, doc: any) => {
|
|
const breakdown = getCreatedDocumentTaxBreakdown(doc)
|
|
return sum + breakdown.tax19 + breakdown.tax7
|
|
}, 0)
|
|
|
|
const inputTax = inputDocs.reduce((sum: number, invoice: any) => {
|
|
const breakdown = getIncomingInvoiceTaxBreakdown(invoice)
|
|
return sum + breakdown.tax19 + breakdown.tax7
|
|
}, 0)
|
|
|
|
const expenses = invoiceExpenses + directAccountExpenses
|
|
|
|
summary.value = {
|
|
label: dayjs().format("MMMM YYYY"),
|
|
income: Number(income.toFixed(2)),
|
|
expenses: Number(expenses.toFixed(2)),
|
|
result: Number((income - expenses).toFixed(2)),
|
|
taxBalance: Number((outputTax - inputTax).toFixed(2)),
|
|
incomeCount: outputDocs.length,
|
|
expenseCount: inputDocs.length + directExpenses.length
|
|
}
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(loadSummary)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-3">
|
|
<div class="bwa-summary-top">
|
|
<div>
|
|
<p class="bwa-summary-period">{{ summary.label }}</p>
|
|
<p class="bwa-summary-range">Aktueller Monat</p>
|
|
</div>
|
|
<UButton
|
|
size="xs"
|
|
variant="soft"
|
|
color="gray"
|
|
icon="i-heroicons-arrow-top-right-on-square"
|
|
@click="navigateTo('/accounting/bwa')"
|
|
>
|
|
Details
|
|
</UButton>
|
|
</div>
|
|
|
|
<div class="bwa-summary-row">
|
|
<span class="bwa-summary-label">Einnahmen</span>
|
|
<span class="bwa-summary-value text-primary-500">
|
|
{{ loading ? "..." : formatCurrency(summary.income) }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="bwa-summary-row">
|
|
<span class="bwa-summary-label">Ausgaben</span>
|
|
<span class="bwa-summary-value text-error">
|
|
{{ loading ? "..." : formatCurrency(summary.expenses) }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="bwa-summary-row">
|
|
<span class="bwa-summary-label">Ergebnis</span>
|
|
<span class="bwa-summary-value" :class="summary.result >= 0 ? 'text-primary-500' : 'text-error'">
|
|
{{ loading ? "..." : formatCurrency(summary.result) }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="bwa-summary-meta">
|
|
{{ summary.incomeCount }} Einnahmenbelege | {{ summary.expenseCount }} Ausgabenbelege | USt-Saldo {{ formatCurrency(summary.taxBalance) }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.bwa-summary-top {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.bwa-summary-period {
|
|
margin: 0;
|
|
font-weight: 700;
|
|
color: rgb(17 24 39);
|
|
}
|
|
|
|
.bwa-summary-range,
|
|
.bwa-summary-meta {
|
|
margin: 0;
|
|
font-size: 0.875rem;
|
|
color: rgb(107 114 128);
|
|
}
|
|
|
|
.bwa-summary-row {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
gap: 0.75rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.bwa-summary-label {
|
|
color: rgb(55 65 81);
|
|
}
|
|
|
|
.bwa-summary-value {
|
|
font-weight: 700;
|
|
text-align: right;
|
|
}
|
|
|
|
:deep(.dark) .bwa-summary-period {
|
|
color: rgb(243 244 246);
|
|
}
|
|
|
|
:deep(.dark) .bwa-summary-range,
|
|
:deep(.dark) .bwa-summary-meta,
|
|
:deep(.dark) .bwa-summary-label {
|
|
color: rgb(156 163 175);
|
|
}
|
|
</style>
|