Fix #126
This commit is contained in:
@@ -1,239 +1,279 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { Line } from "vue-chartjs";
|
||||||
|
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
|
const tempStore = useTempStore()
|
||||||
|
|
||||||
let incomeData = ref({})
|
const amountMode = ref("net")
|
||||||
let expenseData = ref({})
|
const granularity = ref("year")
|
||||||
|
const selectedYear = ref(dayjs().year())
|
||||||
|
const selectedMonth = ref(dayjs().month() + 1)
|
||||||
|
|
||||||
const setup = async () => {
|
const incomeDocuments = ref([])
|
||||||
let incomeRawData = (await useEntities("createddocuments").select()).filter(i => i.state === "Gebucht" && ['invoices','advanceInvoices','cancellationInvoices'].includes(i.type))
|
const expenseInvoices = ref([])
|
||||||
|
|
||||||
let incomeRawFilteredData = incomeRawData.filter(x => x.state === 'Gebucht' && incomeRawData.find(i => i.linkedDocument && i.linkedDocument.id === x.id && i.type === 'cancellationInvoices') && ['invoices','advanceInvoices'].includes(row.type))
|
const granularityOptions = [
|
||||||
|
{ label: "Jahr", value: "year" },
|
||||||
|
{ label: "Monat", value: "month" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const monthOptions = [
|
||||||
|
{ 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 }
|
||||||
|
]
|
||||||
|
|
||||||
let expenseRawData =(await useEntities("incominginvoices").select())
|
const normalizeMode = (value) => value === "gross" ? "gross" : "net"
|
||||||
let withoutInvoiceRawData = (await useEntities("statementallocations").select()).filter(i => i.account)
|
const normalizeGranularity = (value) => value === "month" ? "month" : "year"
|
||||||
|
|
||||||
let withoutInvoiceRawDataExpenses = []
|
watch(
|
||||||
let withoutInvoiceRawDataIncomes = []
|
() => tempStore.settings?.dashboardIncomeExpenseView,
|
||||||
|
(storedView) => {
|
||||||
|
const legacyMode = tempStore.settings?.dashboardIncomeExpenseMode
|
||||||
|
|
||||||
withoutInvoiceRawData.forEach(i => {
|
amountMode.value = normalizeMode(storedView?.amountMode || legacyMode)
|
||||||
if(i.amount > 0) {
|
granularity.value = normalizeGranularity(storedView?.granularity)
|
||||||
withoutInvoiceRawDataIncomes.push({
|
|
||||||
id: i.id,
|
const nextYear = Number(storedView?.year)
|
||||||
date: dayjs(i.created_at).format("DD-MM-YY"),
|
const nextMonth = Number(storedView?.month)
|
||||||
amount: Math.abs(i.amount),
|
|
||||||
bs_id: i.bs_id
|
selectedYear.value = Number.isFinite(nextYear) ? nextYear : dayjs().year()
|
||||||
})
|
selectedMonth.value = Number.isFinite(nextMonth) && nextMonth >= 1 && nextMonth <= 12
|
||||||
} else if(i.amount < 0) {
|
? nextMonth
|
||||||
withoutInvoiceRawDataExpenses.push({
|
: dayjs().month() + 1
|
||||||
id: i.id,
|
},
|
||||||
date: dayjs(i.created_at).format("DD-MM-YY"),
|
{ immediate: true }
|
||||||
amount: Math.abs(i.amount),
|
)
|
||||||
bs_id: i.bs_id
|
|
||||||
})
|
watch([amountMode, granularity, selectedYear, selectedMonth], () => {
|
||||||
}
|
tempStore.modifySettings("dashboardIncomeExpenseView", {
|
||||||
|
amountMode: amountMode.value,
|
||||||
|
granularity: granularity.value,
|
||||||
|
year: selectedYear.value,
|
||||||
|
month: selectedMonth.value
|
||||||
})
|
})
|
||||||
|
|
||||||
/*withoutInvoiceRawDataExpenses.forEach(i => {
|
// Backward compatibility for any existing consumers.
|
||||||
expenseData.value[i.date] ? expenseData.value[i.date] = Number((expenseData.value[i.date] + i.amount).toFixed(2)) : expenseData.value[i.date] = i.amount
|
tempStore.modifySettings("dashboardIncomeExpenseMode", amountMode.value)
|
||||||
})
|
|
||||||
|
|
||||||
withoutInvoiceRawDataIncomes.forEach(i => {
|
|
||||||
incomeData.value[i.date] ? incomeData.value[i.date] = Number((incomeData.value[i.date] + i.amount).toFixed(2)) : incomeData.value[i.date] = i.amount
|
|
||||||
})*/
|
|
||||||
|
|
||||||
expenseRawData = expenseRawData.filter(i => i.date).map(i => {
|
|
||||||
let amount = 0
|
|
||||||
|
|
||||||
i.accounts.forEach(a => {
|
|
||||||
amount += a.amountNet
|
|
||||||
})
|
|
||||||
|
|
||||||
amount = Number(amount.toFixed(2))
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: i.id,
|
|
||||||
date: dayjs(i.date).format("DD-MM-YY"),
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
expenseRawData.forEach(i => {
|
|
||||||
expenseData.value[i.date] ? expenseData.value[i.date] = Number((expenseData.value[i.date] + i.amount).toFixed(2)) : expenseData.value[i.date] = i.amount
|
|
||||||
})
|
|
||||||
|
|
||||||
let expenseMonths = {
|
|
||||||
"01": 0,
|
|
||||||
"02": 0,
|
|
||||||
"03": 0,
|
|
||||||
"04": 0,
|
|
||||||
"05": 0,
|
|
||||||
"06": 0,
|
|
||||||
"07": 0,
|
|
||||||
"08": 0,
|
|
||||||
"09": 0,
|
|
||||||
"10": 0,
|
|
||||||
"11": 0,
|
|
||||||
"12": 0,
|
|
||||||
|
|
||||||
}
|
|
||||||
Object.keys(expenseMonths).forEach(month => {
|
|
||||||
let dates = Object.keys(expenseData.value).filter(i => i.split("-")[1] === month && i.split("-")[2] === dayjs().format("YY"))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
dates.forEach(date => {
|
|
||||||
if(expenseMonths[month]){
|
|
||||||
expenseMonths[month] = Number((expenseMonths[month] + expenseData.value[date]).toFixed(2))
|
|
||||||
} else {
|
|
||||||
expenseMonths[month] = expenseData.value[date]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
expenseData.value = expenseMonths
|
|
||||||
|
|
||||||
|
|
||||||
incomeRawData = incomeRawData.map(i => {
|
|
||||||
let amount = 0
|
|
||||||
|
|
||||||
i.rows.forEach(r => {
|
|
||||||
if(r.mode !== "pagebreak" && r.mode !== "title" && r.mode !== "text"){
|
|
||||||
amount += r.price * r.quantity * (1 - r.discountPercent/100)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
amount = Number(amount.toFixed(2))
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: i.id,
|
|
||||||
date: dayjs(i.documentDate).format("DD-MM-YY"),
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
incomeRawData.forEach(i => {
|
|
||||||
incomeData.value[i.date] ? incomeData.value[i.date] = Number((incomeData.value[i.date] + i.amount).toFixed(2)) : incomeData.value[i.date] = i.amount
|
|
||||||
})
|
|
||||||
|
|
||||||
let incomeMonths = {
|
|
||||||
"01": 0,
|
|
||||||
"02": 0,
|
|
||||||
"03": 0,
|
|
||||||
"04": 0,
|
|
||||||
"05": 0,
|
|
||||||
"06": 0,
|
|
||||||
"07": 0,
|
|
||||||
"08": 0,
|
|
||||||
"09": 0,
|
|
||||||
"10": 0,
|
|
||||||
"11": 0,
|
|
||||||
"12": 0,
|
|
||||||
|
|
||||||
}
|
|
||||||
Object.keys(incomeMonths).forEach(month => {
|
|
||||||
let dates = Object.keys(incomeData.value).filter(i => i.split("-")[1] === month && i.split("-")[2] === dayjs().format("YY"))
|
|
||||||
|
|
||||||
dates.forEach(date => {
|
|
||||||
if(incomeMonths[month]){
|
|
||||||
incomeMonths[month] = Number((incomeMonths[month] + incomeData.value[date]).toFixed(2))
|
|
||||||
} else {
|
|
||||||
incomeMonths[month] = incomeData.value[date]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
incomeData.value = incomeMonths
|
|
||||||
}
|
|
||||||
|
|
||||||
const days = computed(() => {
|
|
||||||
let days = []
|
|
||||||
|
|
||||||
days = Object.keys(incomeData.value)
|
|
||||||
|
|
||||||
let expenseDays = Object.keys(expenseData.value)
|
|
||||||
|
|
||||||
expenseDays.forEach(expenseDay => {
|
|
||||||
if(!days.find(i => i === expenseDay)){
|
|
||||||
days.push(expenseDay)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
days = days.sort(function(a, b) {
|
|
||||||
var keyA = dayjs(a, "DD-MM-YY"),
|
|
||||||
keyB = dayjs(b, "DD-MM-YY");
|
|
||||||
// Compare the 2 dates
|
|
||||||
if (keyA.isBefore(keyB,'day')) {
|
|
||||||
return -1;
|
|
||||||
} else if(keyB.isBefore(keyA, 'day')) {
|
|
||||||
return 1
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return days
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/*const chartData = computed(() => {
|
const loadData = async () => {
|
||||||
return {
|
const [docs, incoming] = await Promise.all([
|
||||||
labels: days.value,
|
useEntities("createddocuments").select(),
|
||||||
datasets: [
|
useEntities("incominginvoices").select()
|
||||||
{
|
])
|
||||||
label: 'Einnahmen',
|
|
||||||
data: [2, 1, 16, 3, 2],
|
incomeDocuments.value = (docs || []).filter((item) => item.state === "Gebucht" && ["invoices", "advanceInvoices", "cancellationInvoices"].includes(item.type))
|
||||||
backgroundColor: 'rgba(20, 255, 0, 0.3)',
|
expenseInvoices.value = (incoming || []).filter((item) => item.date)
|
||||||
borderColor: 'red',
|
}
|
||||||
borderWidth: 2,
|
|
||||||
}
|
const yearsInData = computed(() => {
|
||||||
]
|
const years = new Set([dayjs().year()])
|
||||||
|
|
||||||
|
incomeDocuments.value.forEach((item) => {
|
||||||
|
const parsed = dayjs(item.documentDate)
|
||||||
|
if (parsed.isValid()) years.add(parsed.year())
|
||||||
|
})
|
||||||
|
|
||||||
|
expenseInvoices.value.forEach((item) => {
|
||||||
|
const parsed = dayjs(item.date)
|
||||||
|
if (parsed.isValid()) years.add(parsed.year())
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(years).sort((a, b) => b - a)
|
||||||
|
})
|
||||||
|
|
||||||
|
const yearOptions = computed(() => yearsInData.value.map((year) => ({ label: String(year), value: year })))
|
||||||
|
|
||||||
|
watch(yearsInData, (years) => {
|
||||||
|
if (!years.includes(selectedYear.value) && years.length > 0) {
|
||||||
|
selectedYear.value = years[0]
|
||||||
}
|
}
|
||||||
})*/
|
}, { immediate: true })
|
||||||
|
|
||||||
|
const computeDocumentAmount = (doc) => {
|
||||||
|
let amount = 0
|
||||||
|
|
||||||
|
;(doc.rows || []).forEach((row) => {
|
||||||
|
if (["pagebreak", "title", "text"].includes(row.mode)) return
|
||||||
|
|
||||||
|
const net = Number(row.price || 0) * Number(row.quantity || 0) * (1 - Number(row.discountPercent || 0) / 100)
|
||||||
|
const taxPercent = Number(row.taxPercent)
|
||||||
|
const gross = net * (1 + (Number.isFinite(taxPercent) ? taxPercent : 0) / 100)
|
||||||
|
|
||||||
|
amount += amountMode.value === "gross" ? gross : net
|
||||||
|
})
|
||||||
|
|
||||||
|
return Number(amount.toFixed(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
const computeIncomingInvoiceAmount = (invoice) => {
|
||||||
|
let amount = 0
|
||||||
|
|
||||||
|
;(invoice.accounts || []).forEach((account) => {
|
||||||
|
const net = Number(account.amountNet || 0)
|
||||||
|
const tax = Number(account.amountTax || 0)
|
||||||
|
const grossValue = Number(account.amountGross)
|
||||||
|
const gross = Number.isFinite(grossValue) ? grossValue : (net + tax)
|
||||||
|
|
||||||
|
amount += amountMode.value === "gross" ? gross : net
|
||||||
|
})
|
||||||
|
|
||||||
|
return Number(amount.toFixed(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
const buckets = computed(() => {
|
||||||
|
const income = {}
|
||||||
|
const expense = {}
|
||||||
|
|
||||||
|
if (granularity.value === "year") {
|
||||||
|
for (let month = 1; month <= 12; month += 1) {
|
||||||
|
const key = String(month).padStart(2, "0")
|
||||||
|
income[key] = 0
|
||||||
|
expense[key] = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const daysInMonth = dayjs(`${selectedYear.value}-${String(selectedMonth.value).padStart(2, "0")}-01`).daysInMonth()
|
||||||
|
|
||||||
|
for (let day = 1; day <= daysInMonth; day += 1) {
|
||||||
|
const key = String(day).padStart(2, "0")
|
||||||
|
income[key] = 0
|
||||||
|
expense[key] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
incomeDocuments.value.forEach((doc) => {
|
||||||
|
const docDate = dayjs(doc.documentDate)
|
||||||
|
if (!docDate.isValid() || docDate.year() !== selectedYear.value) return
|
||||||
|
if (granularity.value === "month" && docDate.month() + 1 !== selectedMonth.value) return
|
||||||
|
|
||||||
|
const key = granularity.value === "year"
|
||||||
|
? String(docDate.month() + 1).padStart(2, "0")
|
||||||
|
: String(docDate.date()).padStart(2, "0")
|
||||||
|
|
||||||
|
income[key] = Number((income[key] + computeDocumentAmount(doc)).toFixed(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
expenseInvoices.value.forEach((invoice) => {
|
||||||
|
const invoiceDate = dayjs(invoice.date)
|
||||||
|
if (!invoiceDate.isValid() || invoiceDate.year() !== selectedYear.value) return
|
||||||
|
if (granularity.value === "month" && invoiceDate.month() + 1 !== selectedMonth.value) return
|
||||||
|
|
||||||
|
const key = granularity.value === "year"
|
||||||
|
? String(invoiceDate.month() + 1).padStart(2, "0")
|
||||||
|
: String(invoiceDate.date()).padStart(2, "0")
|
||||||
|
|
||||||
|
expense[key] = Number((expense[key] + computeIncomingInvoiceAmount(invoice)).toFixed(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
return { income, expense }
|
||||||
|
})
|
||||||
|
|
||||||
|
const chartLabels = computed(() => {
|
||||||
|
if (granularity.value === "year") {
|
||||||
|
return ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(buckets.value.income).map((day) => `${day}.`)
|
||||||
|
})
|
||||||
|
|
||||||
import { Line } from 'vue-chartjs'
|
|
||||||
const chartData = computed(() => {
|
const chartData = computed(() => {
|
||||||
|
const keys = Object.keys(buckets.value.income).sort()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labels: ["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],
|
labels: chartLabels.value,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Ausgaben',
|
label: "Ausgaben",
|
||||||
backgroundColor: '#f87979',
|
backgroundColor: "#f87979",
|
||||||
borderColor: '#f87979',
|
borderColor: "#f87979",
|
||||||
data: Object.keys(expenseData.value).sort().map(i => expenseData.value[i]),
|
data: keys.map((key) => buckets.value.expense[key]),
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
},{
|
},
|
||||||
label: 'Einnahmen',
|
{
|
||||||
backgroundColor: '#69c350',
|
label: "Einnahmen",
|
||||||
borderColor: '#69c350',
|
backgroundColor: "#69c350",
|
||||||
data: Object.keys(incomeData.value).sort().map(i => incomeData.value[i]),
|
borderColor: "#69c350",
|
||||||
|
data: keys.map((key) => buckets.value.income[key]),
|
||||||
tension: 0.3
|
tension: 0.3
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const chartOptions = ref({
|
const chartOptions = ref({
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
setup()
|
loadData()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Line
|
<div class="h-full flex flex-col gap-2">
|
||||||
:data="chartData"
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
:options="chartOptions"
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
/>
|
<USelectMenu
|
||||||
|
v-model="granularity"
|
||||||
|
:options="granularityOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
class="w-28"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<USelectMenu
|
||||||
|
v-model="selectedYear"
|
||||||
|
:options="yearOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
class="w-24"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<USelectMenu
|
||||||
|
v-if="granularity === 'month'"
|
||||||
|
v-model="selectedMonth"
|
||||||
|
:options="monthOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
class="w-36"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButtonGroup size="xs">
|
||||||
|
<UButton
|
||||||
|
:variant="amountMode === 'net' ? 'solid' : 'outline'"
|
||||||
|
@click="amountMode = 'net'"
|
||||||
|
>
|
||||||
|
Netto
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
:variant="amountMode === 'gross' ? 'solid' : 'outline'"
|
||||||
|
@click="amountMode = 'gross'"
|
||||||
|
>
|
||||||
|
Brutto
|
||||||
|
</UButton>
|
||||||
|
</UButtonGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 min-h-[280px]">
|
||||||
|
<Line
|
||||||
|
:data="chartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<UDashboardPanelContent>
|
<UDashboardPanelContent>
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<UDashboardCard
|
<UDashboardCard
|
||||||
title="Einnahmen und Ausgaben(netto)"
|
title="Einnahmen und Ausgaben"
|
||||||
class="mt-3"
|
class="mt-3"
|
||||||
>
|
>
|
||||||
<display-income-and-expenditure/>
|
<display-income-and-expenditure/>
|
||||||
@@ -89,4 +89,4 @@ const { isNotificationsSlideoverOpen } = useDashboard()
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user