3. Zwischenstand

This commit is contained in:
2026-03-22 13:53:29 +01:00
parent 03bcc1a939
commit 9f665fc3b8
26 changed files with 2037 additions and 583 deletions

View File

@@ -39,7 +39,9 @@ const modal = useModal()
<template>
<UButton
variant="outline"
class="w-25 ml-2"
size="sm"
square
class="ml-2 shrink-0"
v-if="props.id && props.buttonShow"
icon="i-heroicons-eye"
@click="modal.open(StandardEntityModal, {
@@ -50,7 +52,9 @@ const modal = useModal()
/>
<UButton
variant="outline"
class="w-25 ml-2"
size="sm"
square
class="ml-2 shrink-0"
v-if="props.id && props.buttonEdit"
icon="i-heroicons-pencil-solid"
@click="modal.open(StandardEntityModal, {
@@ -64,7 +68,9 @@ const modal = useModal()
/>
<UButton
variant="outline"
class="w-25 ml-2"
size="sm"
square
class="ml-2 shrink-0"
v-if="!props.id && props.buttonCreate"
icon="i-heroicons-plus"
@click="modal.open(StandardEntityModal, {
@@ -80,4 +86,4 @@ const modal = useModal()
<style scoped>
</style>
</style>

View File

@@ -255,9 +255,14 @@ const selectItem = (item) => {
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
:on-select="(row) => selectItem(row.original)"
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
style="height: 70vh"
>
<template #empty>
<div class="flex flex-col items-center justify-center py-8 text-center text-sm text-muted">
<UIcon name="i-heroicons-circle-stack-20-solid" class="mb-2 size-5" />
<span>Keine Belege anzuzeigen</span>
</div>
</template>
<template #type-cell="{ row }">
{{ dataStore.documentTypesForCreation[row.original.type].labelSingle }}
</template>

View File

@@ -1,7 +1,7 @@
<script setup>
import dayjs from "dayjs";
const router = useRouter()
import dayjs from "dayjs"
const router = useRouter()
const props = defineProps({
queryStringData: {
@@ -21,83 +21,260 @@ const props = defineProps({
}
})
const statementallocations = ref([])
const incominginvoices = ref([])
const loading = ref(true)
const incomingInvoices = ref([])
const statementAllocations = ref([])
const selectedYear = ref(String(dayjs().year()))
const selectedMonth = ref("all")
const currency = (value) => `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`
const currentAccountId = computed(() => String(props.item?.id ?? ""))
const sameAccount = (value) => String(value ?? "") === currentAccountId.value
const getStatementId = (allocation) => allocation?.bankstatement?.id || allocation?.bs_id?.id || allocation?.bs_id || null
const getStatementLike = (allocation) => allocation?.bankstatement || (typeof allocation?.bs_id === "object" ? allocation.bs_id : null)
const getAllocationDate = (allocation) => {
const statement = getStatementLike(allocation)
return statement?.date || statement?.valueDate || allocation?.bs_id?.date || allocation?.bs_id?.valueDate || null
}
const getAllocationPartner = (allocation) => {
const statement = getStatementLike(allocation)
return statement?.debName || statement?.credName || statement?.partner || allocation?.bs_id?.debName || allocation?.bs_id?.credName || ""
}
const getAllocationDescription = (allocation) => {
const statement = getStatementLike(allocation)
return allocation?.description || statement?.purpose || statement?.text || statement?.description || allocation?.bs_id?.purpose || allocation?.bs_id?.text || ""
}
const hasContent = (value) => value !== null && value !== undefined && String(value).trim() !== ""
const monthItems = [
{ label: "Ganzes Jahr", value: "all" },
{ label: "Januar", value: "1" },
{ label: "Februar", value: "2" },
{ label: "Maerz", 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 allAllocations = computed(() => {
const statementRows = statementAllocations.value.map((allocation) => ({
...allocation,
type: "statementallocation",
bankstatement: allocation.bankstatement || getStatementLike(allocation),
date: getAllocationDate(allocation),
partner: getAllocationPartner(allocation),
description: getAllocationDescription(allocation),
amount: Number(allocation.amount || 0)
}))
const incomingInvoiceRows = incomingInvoices.value.flatMap((invoice) => {
return (invoice.accounts || [])
.filter((account) => sameAccount(account.account?.id || account.account))
.map((account, index) => ({
id: `${invoice.id}-${index}`,
incominginvoiceid: invoice.id,
type: "incominginvoice",
amount: Number(account.amountGross || account.amountNet || 0),
date: invoice.date,
partner: invoice.vendor?.name || "",
description: account.description || invoice.description || "",
color: invoice.expense ? "red" : "green",
expense: invoice.expense,
reference: invoice.reference || "-"
}))
})
return [...statementRows, ...incomingInvoiceRows]
})
const yearItems = computed(() => {
const years = [...new Set(
allAllocations.value
.map((allocation) => allocation.bankstatement?.date || allocation.date)
.filter(Boolean)
.map((date) => String(dayjs(date).year()))
)].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 renderedAllocations = computed(() => {
return allAllocations.value.filter((allocation) => {
const allocationDateValue = allocation.bankstatement?.date || allocation.date
const allocationDate = allocationDateValue ? dayjs(allocationDateValue) : null
if (allocationDate && allocationDate.year().toString() !== selectedYear.value) {
return false
}
if (allocationDate && selectedMonth.value !== "all" && allocationDate.month() + 1 !== Number(selectedMonth.value)) {
return false
}
return true
})
})
const totals = computed(() => {
return renderedAllocations.value.reduce((acc, allocation) => {
const amount = Number(allocation.amount || 0)
if (allocation.incominginvoiceid) {
if (allocation.expense) {
acc.expenses += amount
acc.balance -= amount
} else {
acc.income += amount
acc.balance += amount
}
} else {
if (amount < 0) {
acc.expenses += Math.abs(amount)
} else {
acc.income += amount
}
acc.balance += amount
}
return acc
}, { income: 0, expenses: 0, balance: 0 })
})
const columns = [
{ accessorKey: "amount", header: "Betrag" },
{ accessorKey: "date", header: "Datum" },
{ accessorKey: "partner", header: "Partner" },
{ accessorKey: "description", header: "Beschreibung" }
]
const setup = async () => {
loading.value = true
statementAllocations.value = (await useEntities("statementallocations").select("*, bankstatement(*), createddocument(*), incominginvoice(*)"))
.filter((allocation) => sameAccount(allocation.account?.id || allocation.account) || sameAccount(allocation.ownaccount?.id || allocation.ownaccount))
incomingInvoices.value = (await useEntities("incominginvoices").select("*, vendor(*)"))
.filter((invoice) => (invoice.accounts || []).some((account) => sameAccount(account.account?.id || account.account)))
const firstYear = yearItems.value[0]?.value
if (firstYear && !yearItems.value.some((item) => item.value === selectedYear.value)) {
selectedYear.value = firstYear
}
loading.value = false
}
setup()
const selectAllocation = (allocation) => {
if(allocation.type === "statementallocation") {
router.push(`/banking/statements/edit/${allocation.bs_id.id}`)
} else if(allocation.type === "incominginvoice") {
router.push(`/incominginvoices/show/${allocation.incominginvoiceid}`)
const unwrapAllocationRow = (allocationLike) => allocationLike?.original || allocationLike
const selectAllocation = (allocationLike) => {
const allocation = unwrapAllocationRow(allocationLike)
if (!allocation) {
return
}
const statementId = getStatementId(allocation)
if (allocation.type === "statementallocation" && statementId) {
router.push(`/banking/statements/edit/${statementId}`)
} else if (allocation.type === "incominginvoice" && allocation.incominginvoiceid) {
router.push(`/incomingInvoices/show/${allocation.incominginvoiceid}`)
}
}
const renderedAllocations = computed(() => {
let tempstatementallocations = props.item.statementallocations.map(i => {
return {
...i,
type: "statementallocation",
date: i.bs_id.date,
partner: i.bs_id ? (i.bs_id.debName ? i.bs_id.debName : (i.bs_id.credName ? i.bs_id.credName : '')) : ''
}
})
/*let incominginvoicesallocations = []
incominginvoices.value.forEach(i => {
incominginvoicesallocations.push(...i.accounts.filter(x => x.account == route.params.id).map(x => {
return {
...x,
incominginvoiceid: i.id,
type: "incominginvoice",
amount: x.amountGross ? x.amountGross : x.amountNet,
date: i.date,
partner: i.vendor.name,
description: i.description,
color: i.expense ? "red" : "green"
}
}))
})*/
return [...tempstatementallocations/*, ... incominginvoicesallocations*/]
})
</script>
<template>
<UCard class="mt-5">
<UTable
v-if="props.item.statementallocations"
:data="renderedAllocations"
:columns="normalizeTableColumns([{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}])"
:on-select="(i) => selectAllocation(i)"
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
>
<template #amount-cell="{row}">
<span class="text-right text-rose-600" v-if="row.original.amount < 0 || row.original.color === 'red'">{{useCurrency(row.original.amount)}}</span>
<span class="text-right text-primary-500" v-else-if="row.original.amount > 0 || row.original.color === 'green'">{{useCurrency(row.original.amount)}}</span>
<span v-else>{{useCurrency(row.original.amount)}}</span>
</template>
<template #date-cell="{row}">
{{row.original.date ? dayjs(row.original.date).format('DD.MM.YYYY') : ''}}
</template>
<template #description-cell="{row}">
{{row.original.description ? row.original.description : ''}}
</template>
</UTable>
<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">Einnahmen</div>
<div class="mt-1 text-xl font-semibold">{{ currency(totals.income) }}</div>
</UCard>
<UCard>
<div class="text-sm text-gray-500 dark:text-gray-400">Ausgaben</div>
<div class="mt-1 text-xl font-semibold">{{ currency(totals.expenses) }}</div>
</UCard>
<UCard>
<div class="text-sm text-gray-500 dark:text-gray-400">Saldo</div>
<div class="mt-1 text-xl font-semibold">{{ currency(totals.balance) }}</div>
</UCard>
</div>
<div v-if="!loading" class="overflow-auto max-h-[60vh]">
<UTable
:data="renderedAllocations"
:columns="normalizeTableColumns(columns)"
:on-select="selectAllocation"
class="w-full"
>
<template #empty>
<div class="flex flex-col items-center justify-center py-10 text-center">
<UIcon name="i-heroicons-circle-stack-20-solid" class="mb-2 h-10 w-10 text-gray-400" />
<p class="font-medium">Keine Buchungen im ausgewaehlten Zeitraum</p>
</div>
</template>
<template #amount-cell="{ row }">
<span class="text-right text-error" v-if="row.original.amount < 0 || row.original.color === 'red'">{{ useCurrency(row.original.amount) }}</span>
<span class="text-right text-primary-500" v-else-if="row.original.amount > 0 || row.original.color === 'green'">{{ useCurrency(row.original.amount) }}</span>
<span v-else>{{ useCurrency(row.original.amount) }}</span>
</template>
<template #date-cell="{ row }">
{{ row.original.bankstatement.date && dayjs(row.original.bankstatement.date).isValid() ? dayjs(row.original.bankstatement.date).format('DD.MM.YYYY') : '-' }}
</template>
<template #partner-cell="{ row }">
<div class="truncate">{{ hasContent(row.original.partner) ? row.original.partner : '-' }}</div>
</template>
<template #description-cell="{ row }">
<UTooltip :text="hasContent(row.original.description) ? row.original.description : '-'">
<div class="max-w-[22rem] truncate">{{ hasContent(row.original.description) ? row.original.description : '-' }}</div>
</UTooltip>
</template>
</UTable>
</div>
<UProgress v-else animation="carousel" class="w-3/4 mx-auto" />
</div>
</UCard>
</template>
<style scoped>
</style>

View File

@@ -69,8 +69,13 @@ const columns = [
class="mt-3"
:columns="normalizeTableColumns(columns)"
:data="props.item.times"
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Noch keine Einträge' }"
>
<template #empty>
<div class="flex flex-col items-center justify-center py-8 text-center text-sm text-muted">
<UIcon name="i-heroicons-circle-stack-20-solid" class="mb-2 size-5" />
<span>Noch keine Einträge</span>
</div>
</template>
<template #state-cell="{ row }">
<span
v-if="row.original.state === 'Entwurf'"

View File

@@ -41,7 +41,7 @@ const handleClick = async () => {
:icon="labelPrinter.connected ? 'i-heroicons-printer' : 'i-heroicons-printer'"
:color="labelPrinter.connected ? 'green' : ''"
variant="soft"
class="w-full justify-start"
class="w-full justify-start rounded-lg px-2.5 py-2 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
:loading="labelPrinter.connectLoading"
@click="handleClick"
>

View File

@@ -141,6 +141,11 @@ const links = computed(() => {
to: "/accounting/tax",
icon: "i-heroicons-calculator",
} : null,
(featureEnabled("createDocument") || featureEnabled("incomingInvoices") || featureEnabled("accounts") || featureEnabled("ownaccounts")) ? {
label: "BWA",
to: "/accounting/bwa",
icon: "i-heroicons-chart-bar-square",
} : null,
featureEnabled("costcentres") ? {
label: "Kostenstellen",
to: "/standardEntity/costcentres",

View File

@@ -0,0 +1,166 @@
<script>
import theme from "#build/ui/calendar";
</script>
<script setup>
import { computed } from "vue";
import { useForwardPropsEmits } from "reka-ui";
import { Calendar as SingleCalendar, RangeCalendar } from "reka-ui/namespaced";
import { reactiveOmit } from "@vueuse/core";
import { useAppConfig } from "#imports";
import { useLocale } from "@nuxt/ui/composables/useLocale";
import { tv } from "@nuxt/ui/utils/tv";
import UButton from "@nuxt/ui/components/Button.vue";
const props = defineProps({
as: { type: null, required: false },
nextYearIcon: { type: String, required: false },
nextYear: { type: Object, required: false },
nextMonthIcon: { type: String, required: false },
nextMonth: { type: Object, required: false },
prevYearIcon: { type: String, required: false },
prevYear: { type: Object, required: false },
prevMonthIcon: { type: String, required: false },
prevMonth: { type: Object, required: false },
color: { type: null, required: false },
size: { type: null, required: false },
range: { type: Boolean, required: false },
multiple: { type: Boolean, required: false },
monthControls: { type: Boolean, required: false, default: true },
yearControls: { type: Boolean, required: false, default: true },
defaultValue: { type: null, required: false },
modelValue: { type: null, required: false },
class: { type: null, required: false },
ui: { type: null, required: false },
defaultPlaceholder: { type: null, required: false },
placeholder: { type: null, required: false },
allowNonContiguousRanges: { type: Boolean, required: false },
pagedNavigation: { type: Boolean, required: false },
preventDeselect: { type: Boolean, required: false },
maximumDays: { type: Number, required: false },
weekStartsOn: { type: Number, required: false, default: 1 },
weekdayFormat: { type: String, required: false },
fixedWeeks: { type: Boolean, required: false, default: true },
maxValue: { type: null, required: false },
minValue: { type: null, required: false },
numberOfMonths: { type: Number, required: false },
disabled: { type: Boolean, required: false },
readonly: { type: Boolean, required: false },
initialFocus: { type: Boolean, required: false },
isDateDisabled: { type: Function, required: false },
isDateUnavailable: { type: Function, required: false },
isDateHighlightable: { type: Function, required: false },
nextPage: { type: Function, required: false },
prevPage: { type: Function, required: false },
disableDaysOutsideCurrentView: { type: Boolean, required: false },
fixedDate: { type: String, required: false }
});
const emits = defineEmits(["update:modelValue", "update:placeholder", "update:validModelValue", "update:startValue"]);
defineSlots();
const { code: locale, dir, t } = useLocale();
const appConfig = useAppConfig();
const rootProps = useForwardPropsEmits(
reactiveOmit(props, "range", "modelValue", "defaultValue", "color", "size", "monthControls", "yearControls", "class", "ui"),
emits
);
const nextYearIcon = computed(() => props.nextYearIcon || (dir.value === "rtl" ? appConfig.ui.icons.chevronDoubleLeft : appConfig.ui.icons.chevronDoubleRight));
const nextMonthIcon = computed(() => props.nextMonthIcon || (dir.value === "rtl" ? appConfig.ui.icons.chevronLeft : appConfig.ui.icons.chevronRight));
const prevYearIcon = computed(() => props.prevYearIcon || (dir.value === "rtl" ? appConfig.ui.icons.chevronDoubleRight : appConfig.ui.icons.chevronDoubleLeft));
const prevMonthIcon = computed(() => props.prevMonthIcon || (dir.value === "rtl" ? appConfig.ui.icons.chevronRight : appConfig.ui.icons.chevronLeft));
const ui = computed(() => tv({ extend: tv(theme), ...appConfig.ui?.calendar || {} })({
color: props.color,
size: props.size
}));
const Calendar = computed(() => props.range ? RangeCalendar : SingleCalendar);
function paginateYear(date, sign) {
if (sign === -1) {
return date.subtract({ years: 1 });
}
return date.add({ years: 1 });
}
</script>
<template>
<Calendar.Root
v-slot="{ weekDays, grid }"
v-bind="rootProps"
:model-value="modelValue"
:default-value="defaultValue"
:locale="locale"
:dir="dir"
:class="ui.root({ class: [props.ui?.root, props.class] })"
>
<Calendar.Header :class="ui.header({ class: props.ui?.header })">
<Calendar.Prev v-if="props.yearControls" :prev-page="(date) => paginateYear(date, -1)" :aria-label="t('calendar.prevYear')" as-child>
<UButton :icon="prevYearIcon" :size="props.size" color="neutral" variant="ghost" v-bind="props.prevYear" />
</Calendar.Prev>
<Calendar.Prev v-if="props.monthControls" :aria-label="t('calendar.prevMonth')" as-child>
<UButton :icon="prevMonthIcon" :size="props.size" color="neutral" variant="ghost" v-bind="props.prevMonth" />
</Calendar.Prev>
<Calendar.Heading v-slot="{ headingValue }" :class="ui.heading({ class: props.ui?.heading })">
<slot name="heading" :value="headingValue">
{{ headingValue }}
</slot>
</Calendar.Heading>
<Calendar.Next v-if="props.monthControls" :aria-label="t('calendar.nextMonth')" as-child>
<UButton :icon="nextMonthIcon" :size="props.size" color="neutral" variant="ghost" v-bind="props.nextMonth" />
</Calendar.Next>
<Calendar.Next v-if="props.yearControls" :next-page="(date) => paginateYear(date, 1)" :aria-label="t('calendar.nextYear')" as-child>
<UButton :icon="nextYearIcon" :size="props.size" color="neutral" variant="ghost" v-bind="props.nextYear" />
</Calendar.Next>
</Calendar.Header>
<div :class="ui.body({ class: props.ui?.body })">
<Calendar.Grid
v-for="month in grid"
:key="month.value.toString()"
:class="ui.grid({ class: props.ui?.grid })"
>
<Calendar.GridHead>
<Calendar.GridRow :class="ui.gridWeekDaysRow({ class: props.ui?.gridWeekDaysRow })">
<Calendar.HeadCell
v-for="day in weekDays"
:key="day"
:class="ui.headCell({ class: props.ui?.headCell })"
>
<slot name="week-day" :day="day">
{{ day }}
</slot>
</Calendar.HeadCell>
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody :class="ui.gridBody({ class: props.ui?.gridBody })">
<Calendar.GridRow
v-for="(weekDates, index) in month.rows"
:key="`weekDate-${index}`"
:class="ui.gridRow({ class: props.ui?.gridRow })"
>
<Calendar.Cell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate"
:class="ui.cell({ class: props.ui?.cell })"
>
<Calendar.CellTrigger
:day="weekDate"
:month="month.value"
:class="ui.cellTrigger({ class: props.ui?.cellTrigger })"
>
<slot name="day" :day="weekDate">
{{ weekDate.day }}
</slot>
</Calendar.CellTrigger>
</Calendar.Cell>
</Calendar.GridRow>
</Calendar.GridBody>
</Calendar.Grid>
</div>
</Calendar.Root>
</template>

View File

@@ -0,0 +1,94 @@
<script setup>
import DashboardNavbarBase from "@nuxt/ui-pro/runtime/components/DashboardNavbar.vue"
import UBadge from "@nuxt/ui/components/Badge.vue"
defineOptions({ inheritAttrs: false })
const props = defineProps({
as: {
type: null,
required: false
},
icon: {
type: String,
required: false
},
title: {
type: String,
required: false
},
toggle: {
type: [Boolean, Object],
required: false,
default: true
},
toggleSide: {
type: String,
required: false,
default: "left"
},
badge: {
type: [String, Number],
required: false
},
class: {
type: null,
required: false
},
ui: {
type: null,
required: false
}
})
</script>
<template>
<DashboardNavbarBase
:as="as"
:icon="icon"
:title="title"
:toggle="toggle"
:toggle-side="toggleSide"
:class="props.class"
:ui="ui"
v-bind="$attrs"
>
<template v-if="$slots.toggle" #toggle="slotProps">
<slot name="toggle" v-bind="slotProps" />
</template>
<template v-if="$slots.left" #left="slotProps">
<slot name="left" v-bind="slotProps" />
</template>
<template v-if="$slots.leading" #leading="slotProps">
<slot name="leading" v-bind="slotProps" />
</template>
<template #title>
<slot name="title">
<span class="inline-flex min-w-0 items-center gap-2">
<span class="truncate">{{ title }}</span>
<UBadge
v-if="badge !== undefined && badge !== null && badge !== ''"
size="sm"
color="neutral"
variant="subtle"
>
{{ badge }}
</UBadge>
</span>
</slot>
</template>
<template v-if="$slots.trailing" #trailing="slotProps">
<slot name="trailing" v-bind="slotProps" />
</template>
<slot />
<template v-if="$slots.right" #right="slotProps">
<slot name="right" v-bind="slotProps" />
</template>
</DashboardNavbarBase>
</template>

View File

@@ -28,16 +28,16 @@ const userItems = computed(() => [[
<UButton
color="gray"
variant="ghost"
class="w-full min-w-0 justify-start gap-2 rounded-lg px-2.5 py-2 text-left"
class="w-full min-w-0 justify-start gap-2 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
:class="[open && 'bg-gray-100 dark:bg-gray-800']"
>
<span class="min-w-0 flex-1 truncate font-medium text-gray-900 dark:text-white">
<div class="flex items-space gap-2">
<span class="min-w-0 flex-1 truncate font-medium text-gray-900 dark:text-white">
{{ auth.user.email }}
</span>
<template #trailing>
<UIcon name="i-heroicons-ellipsis-vertical" class="h-5 w-5 shrink-0" />
</template>
</div>
</UButton>
</template>
</UDropdownMenu>

View File

@@ -1,26 +1,205 @@
<script setup>
import dayjs from "dayjs"
const props = defineProps({
item: {
required: true,
type: String
type: Object
}
})
const incomingInvoices = ref({})
const loading = ref(true)
const incomingInvoices = ref([])
const selectedYear = ref(String(dayjs().year()))
const selectedMonth = ref("all")
const currency = (value) => `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`
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: "Maerz", 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) => account.costCentre === props.item.id)
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)
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 || "-",
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: "description", header: "Beschreibung" },
{ accessorKey: "amountNet", header: "Netto" },
{ accessorKey: "amountTax", header: "Steuer" },
{ accessorKey: "amountGross", header: "Brutto" }
]
const setupPage = async () => {
incomingInvoices.value = (await useEntities("incominginvoices").select()).filter(i => i.accounts.find(x => x.costCentre === props.item.id))
loading.value = true
const invoices = await useEntities("incominginvoices").select("*, vendor(id,name)")
incomingInvoices.value = invoices.filter((invoice) =>
(invoice.accounts || []).some((account) => account.costCentre === props.item.id)
)
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>
{{props.item}}
{{incomingInvoices}}
<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 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 #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>
<style scoped>
</style>

View File

@@ -72,18 +72,26 @@ const setRowData = (row) => {
+ Artikel
</UButton>
<table class="w-full mt-3">
<tr>
<th>Artikel</th>
<th>Menge</th>
<th>Einheit</th>
<th>Verkaufspreis</th>
</tr>
<tr
v-for="product in props.item.materialComposition"
>
<td>
<div class="mt-3 overflow-x-auto">
<table class="w-full min-w-[44rem] table-fixed">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-800">
<th class="px-2 py-2 text-left font-medium">Artikel</th>
<th class="px-2 py-2 text-left font-medium">Menge</th>
<th class="px-2 py-2 text-left font-medium">Einheit</th>
<th class="px-2 py-2 text-left font-medium">Verkaufspreis</th>
<th class="w-12 px-2 py-2"></th>
</tr>
</thead>
<tbody>
<tr
v-for="product in props.item.materialComposition"
:key="product.id"
class="border-b border-gray-100 align-top dark:border-gray-800"
>
<td class="px-2 py-2">
<USelectMenu
class="w-full"
:items="products"
label-key="name"
value-key="id"
@@ -91,38 +99,45 @@ const setRowData = (row) => {
:filter-fields="['name']"
v-model="product.product"
:color="product.product ? 'primary' : 'error'"
@change="setRowData(product)"
@update:model-value="setRowData(product)"
>
<template #default>
{{products.find(i => i.id === product.product) ? products.find(i => i.id === product.product).name : 'Kein Artikel ausgewählt'}}
{{ products.find(i => i.id === product.product)?.name || 'Kein Artikel ausgewählt' }}
</template>
</USelectMenu>
</td>
<td>
<td class="px-2 py-2">
<UInput
class="w-full"
type="number"
v-model="product.quantity"
:step="units.find(i => i.id === product.unit) ? units.find(i => i.id === product.unit).step : 1"
@change="calculateTotalMaterialPrice"
/>
</td>
<td>
<td class="px-2 py-2">
<USelectMenu
class="w-full"
:items="units"
label-key="name"
value-key="id"
v-model="product.unit"
></USelectMenu>
>
<template #default>
{{ units.find(i => i.id === product.unit)?.name || 'Einheit wählen' }}
</template>
</USelectMenu>
</td>
<td>
<td class="px-2 py-2">
<UInput
class="w-full"
type="number"
v-model="product.price"
step="0.01"
@change="calculateTotalMaterialPrice"
/>
</td>
<td>
<td class="px-2 py-2">
<UButton
icon="i-heroicons-x-mark"
@click="removeProductFromMaterialComposition(product.id)"
@@ -130,8 +145,10 @@ const setRowData = (row) => {
color="error"
/>
</td>
</tr>
</table>
</tr>
</tbody>
</table>
</div>
</UCard>
</template>

View File

@@ -73,19 +73,27 @@ const setRowData = (row) => {
+ Stundensatz
</UButton>
<table class="w-full mt-3">
<tr>
<th>Name</th>
<th>Menge</th>
<th>Einheit</th>
<th>Einkaufpreis</th>
<th>Verkaufspreis</th>
</tr>
<tr
v-for="row in props.item.personalComposition"
>
<td>
<div class="mt-3 overflow-x-auto">
<table class="w-full min-w-[52rem] table-fixed">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-800">
<th class="px-2 py-2 text-left font-medium">Name</th>
<th class="px-2 py-2 text-left font-medium">Menge</th>
<th class="px-2 py-2 text-left font-medium">Einheit</th>
<th class="px-2 py-2 text-left font-medium">Einkaufspreis</th>
<th class="px-2 py-2 text-left font-medium">Verkaufspreis</th>
<th class="w-12 px-2 py-2"></th>
</tr>
</thead>
<tbody>
<tr
v-for="row in props.item.personalComposition"
:key="row.id"
class="border-b border-gray-100 align-top dark:border-gray-800"
>
<td class="px-2 py-2">
<USelectMenu
class="w-full"
:items="hourrates"
label-key="name"
value-key="id"
@@ -93,47 +101,55 @@ const setRowData = (row) => {
:filter-fields="['name']"
v-model="row.hourrate"
:color="row.hourrate ? 'primary' : 'error'"
@change="setRowData(row)"
@update:model-value="setRowData(row)"
>
<!-- <template #label>
{{products.find(i => i.id === product.product) ? products.find(i => i.id === product.product).name : 'Kein Artikel ausgewählt'}}
</template>-->
<template #default>
{{ hourrates.find(i => i.id === row.hourrate)?.name || 'Kein Stundensatz ausgewählt' }}
</template>
</USelectMenu>
</td>
<td>
<td class="px-2 py-2">
<UInput
class="w-full"
type="number"
v-model="row.quantity"
:step="units.find(i => i.id === row.unit) ? units.find(i => i.id === row.unit).step : 1"
@change="calculateTotalPersonalPrice"
/>
</td>
<td>
<td class="px-2 py-2">
<USelectMenu
class="w-full"
:items="units"
disabled
label-key="name"
value-key="id"
v-model="row.unit"
></USelectMenu>
>
<template #default>
{{ units.find(i => i.id === row.unit)?.name || 'Einheit' }}
</template>
</USelectMenu>
</td>
<td>
<td class="px-2 py-2">
<UInput
class="w-full"
type="number"
v-model="row.purchasePrice"
step="0.01"
@change="calculateTotalPersonalPrice"
/>
</td>
<td>
<td class="px-2 py-2">
<UInput
class="w-full"
type="number"
v-model="row.price"
step="0.01"
@change="calculateTotalPersonalPrice"
/>
</td>
<td>
<td class="px-2 py-2">
<UButton
icon="i-heroicons-x-mark"
@click="removeRowFromPersonalComposition(row.id)"
@@ -141,8 +157,10 @@ const setRowData = (row) => {
color="error"
/>
</td>
</tr>
</table>
</tr>
</tbody>
</table>
</div>
</UCard>
</template>