Files
FEDEO/frontend/components/displayBWASummary.vue
2026-04-08 18:52:16 +02:00

244 lines
7.2 KiB
Vue

<script setup lang="ts">
import dayjs from "dayjs"
import {
getCreatedDocumentTaxBreakdown,
getIncomingInvoiceTaxBreakdown
} from "~/composables/useTaxEvaluation"
import {
getIncomingInvoiceDepreciationRows,
getIncomingInvoiceImmediateExpenseNet,
getStatementAllocationImmediateExpenseAmount
} from "~/composables/useDepreciation"
const loading = ref(true)
const summary = ref({
label: "",
income: 0,
expenses: 0,
depreciations: 0,
result: 0,
taxBalance: 0,
incomeCount: 0,
expenseCount: 0,
depreciationCount: 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 = getStatementAllocationImmediateExpenseAmount(allocation)
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 + getIncomingInvoiceImmediateExpenseNet(invoice)
}, 0)
const directAccountExpenses = directExpenses.reduce((sum: number, allocation: any) => {
return sum + Number(getStatementAllocationImmediateExpenseAmount(allocation) || 0)
}, 0)
const depreciationRows = [
...inputDocs.flatMap((invoice: any) => getIncomingInvoiceDepreciationRows(invoice, bounds.start, bounds.end))
]
const depreciations = depreciationRows.reduce((sum: number, row: any) => sum + Number(row.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 + depreciations
summary.value = {
label: dayjs().format("MMMM YYYY"),
income: Number(income.toFixed(2)),
expenses: Number(expenses.toFixed(2)),
depreciations: Number(depreciations.toFixed(2)),
result: Number((income - expenses).toFixed(2)),
taxBalance: Number((outputTax - inputTax).toFixed(2)),
incomeCount: outputDocs.length,
expenseCount: inputDocs.filter((invoice: any) => getIncomingInvoiceImmediateExpenseNet(invoice) > 0).length + directExpenses.filter((allocation: any) => getStatementAllocationImmediateExpenseAmount(allocation) > 0).length,
depreciationCount: depreciationRows.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">Davon Abschreibungen</span>
<span class="bwa-summary-value text-amber-600">
{{ loading ? "..." : formatCurrency(summary.depreciations) }}
</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 | {{ summary.depreciationCount }} Abschreibungen | 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>