3. Zwischenstand
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import * as Sentry from "@sentry/browser"
|
import * as Sentry from "@sentry/browser"
|
||||||
|
import { de as germanLocale } from "@nuxt/ui/locale"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ useSeoMeta({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UApp>
|
<UApp :locale="germanLocale">
|
||||||
<div class="safearea">
|
<div class="safearea">
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<NuxtPage/>
|
<NuxtPage/>
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ const modal = useModal()
|
|||||||
<template>
|
<template>
|
||||||
<UButton
|
<UButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="w-25 ml-2"
|
size="sm"
|
||||||
|
square
|
||||||
|
class="ml-2 shrink-0"
|
||||||
v-if="props.id && props.buttonShow"
|
v-if="props.id && props.buttonShow"
|
||||||
icon="i-heroicons-eye"
|
icon="i-heroicons-eye"
|
||||||
@click="modal.open(StandardEntityModal, {
|
@click="modal.open(StandardEntityModal, {
|
||||||
@@ -50,7 +52,9 @@ const modal = useModal()
|
|||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="w-25 ml-2"
|
size="sm"
|
||||||
|
square
|
||||||
|
class="ml-2 shrink-0"
|
||||||
v-if="props.id && props.buttonEdit"
|
v-if="props.id && props.buttonEdit"
|
||||||
icon="i-heroicons-pencil-solid"
|
icon="i-heroicons-pencil-solid"
|
||||||
@click="modal.open(StandardEntityModal, {
|
@click="modal.open(StandardEntityModal, {
|
||||||
@@ -64,7 +68,9 @@ const modal = useModal()
|
|||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="w-25 ml-2"
|
size="sm"
|
||||||
|
square
|
||||||
|
class="ml-2 shrink-0"
|
||||||
v-if="!props.id && props.buttonCreate"
|
v-if="!props.id && props.buttonCreate"
|
||||||
icon="i-heroicons-plus"
|
icon="i-heroicons-plus"
|
||||||
@click="modal.open(StandardEntityModal, {
|
@click="modal.open(StandardEntityModal, {
|
||||||
|
|||||||
@@ -255,9 +255,14 @@ const selectItem = (item) => {
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||||
:on-select="(row) => selectItem(row.original)"
|
:on-select="(row) => selectItem(row.original)"
|
||||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
|
||||||
style="height: 70vh"
|
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 }">
|
<template #type-cell="{ row }">
|
||||||
{{ dataStore.documentTypesForCreation[row.original.type].labelSingle }}
|
{{ dataStore.documentTypesForCreation[row.original.type].labelSingle }}
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs"
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
queryStringData: {
|
queryStringData: {
|
||||||
@@ -21,83 +21,260 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const statementallocations = ref([])
|
const loading = ref(true)
|
||||||
const incominginvoices = ref([])
|
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 () => {
|
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()
|
setup()
|
||||||
|
|
||||||
const selectAllocation = (allocation) => {
|
const unwrapAllocationRow = (allocationLike) => allocationLike?.original || allocationLike
|
||||||
if(allocation.type === "statementallocation") {
|
|
||||||
router.push(`/banking/statements/edit/${allocation.bs_id.id}`)
|
const selectAllocation = (allocationLike) => {
|
||||||
} else if(allocation.type === "incominginvoice") {
|
const allocation = unwrapAllocationRow(allocationLike)
|
||||||
router.push(`/incominginvoices/show/${allocation.incominginvoiceid}`)
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UCard class="mt-5">
|
<UCard class="mt-5">
|
||||||
<UTable
|
<div class="space-y-4">
|
||||||
v-if="props.item.statementallocations"
|
<div class="flex flex-col gap-3 md:flex-row md:items-end">
|
||||||
:data="renderedAllocations"
|
<UFormField label="Jahr" class="w-full md:w-48">
|
||||||
:columns="normalizeTableColumns([{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}])"
|
<USelectMenu
|
||||||
:on-select="(i) => selectAllocation(i)"
|
v-model="selectedYear"
|
||||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
|
:items="yearItems"
|
||||||
>
|
value-key="value"
|
||||||
<template #amount-cell="{row}">
|
label-key="label"
|
||||||
<span class="text-right text-rose-600" v-if="row.original.amount < 0 || row.original.color === 'red'">{{useCurrency(row.original.amount)}}</span>
|
class="w-full"
|
||||||
<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>
|
</UFormField>
|
||||||
</template>
|
|
||||||
<template #date-cell="{row}">
|
<UFormField label="Monat" class="w-full md:w-56">
|
||||||
{{row.original.date ? dayjs(row.original.date).format('DD.MM.YYYY') : ''}}
|
<USelectMenu
|
||||||
</template>
|
v-model="selectedMonth"
|
||||||
<template #description-cell="{row}">
|
:items="monthItems"
|
||||||
{{row.original.description ? row.original.description : ''}}
|
value-key="value"
|
||||||
</template>
|
label-key="label"
|
||||||
</UTable>
|
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>
|
</UCard>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -69,8 +69,13 @@ const columns = [
|
|||||||
class="mt-3"
|
class="mt-3"
|
||||||
:columns="normalizeTableColumns(columns)"
|
:columns="normalizeTableColumns(columns)"
|
||||||
:data="props.item.times"
|
: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 }">
|
<template #state-cell="{ row }">
|
||||||
<span
|
<span
|
||||||
v-if="row.original.state === 'Entwurf'"
|
v-if="row.original.state === 'Entwurf'"
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const handleClick = async () => {
|
|||||||
:icon="labelPrinter.connected ? 'i-heroicons-printer' : 'i-heroicons-printer'"
|
:icon="labelPrinter.connected ? 'i-heroicons-printer' : 'i-heroicons-printer'"
|
||||||
:color="labelPrinter.connected ? 'green' : ''"
|
:color="labelPrinter.connected ? 'green' : ''"
|
||||||
variant="soft"
|
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"
|
:loading="labelPrinter.connectLoading"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -141,6 +141,11 @@ const links = computed(() => {
|
|||||||
to: "/accounting/tax",
|
to: "/accounting/tax",
|
||||||
icon: "i-heroicons-calculator",
|
icon: "i-heroicons-calculator",
|
||||||
} : null,
|
} : null,
|
||||||
|
(featureEnabled("createDocument") || featureEnabled("incomingInvoices") || featureEnabled("accounts") || featureEnabled("ownaccounts")) ? {
|
||||||
|
label: "BWA",
|
||||||
|
to: "/accounting/bwa",
|
||||||
|
icon: "i-heroicons-chart-bar-square",
|
||||||
|
} : null,
|
||||||
featureEnabled("costcentres") ? {
|
featureEnabled("costcentres") ? {
|
||||||
label: "Kostenstellen",
|
label: "Kostenstellen",
|
||||||
to: "/standardEntity/costcentres",
|
to: "/standardEntity/costcentres",
|
||||||
|
|||||||
166
frontend/components/UCalendar.vue
Normal file
166
frontend/components/UCalendar.vue
Normal 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>
|
||||||
94
frontend/components/UDashboardNavbar.vue
Normal file
94
frontend/components/UDashboardNavbar.vue
Normal 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>
|
||||||
@@ -28,16 +28,16 @@ const userItems = computed(() => [[
|
|||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="ghost"
|
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']"
|
: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 }}
|
{{ auth.user.email }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<template #trailing>
|
|
||||||
<UIcon name="i-heroicons-ellipsis-vertical" class="h-5 w-5 shrink-0" />
|
<UIcon name="i-heroicons-ellipsis-vertical" class="h-5 w-5 shrink-0" />
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
</UButton>
|
</UButton>
|
||||||
</template>
|
</template>
|
||||||
</UDropdownMenu>
|
</UDropdownMenu>
|
||||||
|
|||||||
@@ -1,26 +1,205 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
item: {
|
item: {
|
||||||
required: true,
|
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 () => {
|
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()
|
setupPage()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{props.item}}
|
<div class="space-y-4">
|
||||||
{{incomingInvoices}}
|
<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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -72,18 +72,26 @@ const setRowData = (row) => {
|
|||||||
+ Artikel
|
+ Artikel
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
||||||
<table class="w-full mt-3">
|
<div class="mt-3 overflow-x-auto">
|
||||||
<tr>
|
<table class="w-full min-w-[44rem] table-fixed">
|
||||||
<th>Artikel</th>
|
<thead>
|
||||||
<th>Menge</th>
|
<tr class="border-b border-gray-200 dark:border-gray-800">
|
||||||
<th>Einheit</th>
|
<th class="px-2 py-2 text-left font-medium">Artikel</th>
|
||||||
<th>Verkaufspreis</th>
|
<th class="px-2 py-2 text-left font-medium">Menge</th>
|
||||||
</tr>
|
<th class="px-2 py-2 text-left font-medium">Einheit</th>
|
||||||
<tr
|
<th class="px-2 py-2 text-left font-medium">Verkaufspreis</th>
|
||||||
v-for="product in props.item.materialComposition"
|
<th class="w-12 px-2 py-2"></th>
|
||||||
>
|
</tr>
|
||||||
<td>
|
</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
|
<USelectMenu
|
||||||
|
class="w-full"
|
||||||
:items="products"
|
:items="products"
|
||||||
label-key="name"
|
label-key="name"
|
||||||
value-key="id"
|
value-key="id"
|
||||||
@@ -91,38 +99,45 @@ const setRowData = (row) => {
|
|||||||
:filter-fields="['name']"
|
:filter-fields="['name']"
|
||||||
v-model="product.product"
|
v-model="product.product"
|
||||||
:color="product.product ? 'primary' : 'error'"
|
:color="product.product ? 'primary' : 'error'"
|
||||||
@change="setRowData(product)"
|
@update:model-value="setRowData(product)"
|
||||||
>
|
>
|
||||||
<template #default>
|
<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>
|
</template>
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="px-2 py-2">
|
||||||
<UInput
|
<UInput
|
||||||
|
class="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
v-model="product.quantity"
|
v-model="product.quantity"
|
||||||
:step="units.find(i => i.id === product.unit) ? units.find(i => i.id === product.unit).step : 1"
|
:step="units.find(i => i.id === product.unit) ? units.find(i => i.id === product.unit).step : 1"
|
||||||
@change="calculateTotalMaterialPrice"
|
@change="calculateTotalMaterialPrice"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="px-2 py-2">
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
|
class="w-full"
|
||||||
:items="units"
|
:items="units"
|
||||||
label-key="name"
|
label-key="name"
|
||||||
value-key="id"
|
value-key="id"
|
||||||
v-model="product.unit"
|
v-model="product.unit"
|
||||||
></USelectMenu>
|
>
|
||||||
|
<template #default>
|
||||||
|
{{ units.find(i => i.id === product.unit)?.name || 'Einheit wählen' }}
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="px-2 py-2">
|
||||||
<UInput
|
<UInput
|
||||||
|
class="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
v-model="product.price"
|
v-model="product.price"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
@change="calculateTotalMaterialPrice"
|
@change="calculateTotalMaterialPrice"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="px-2 py-2">
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-x-mark"
|
icon="i-heroicons-x-mark"
|
||||||
@click="removeProductFromMaterialComposition(product.id)"
|
@click="removeProductFromMaterialComposition(product.id)"
|
||||||
@@ -130,8 +145,10 @@ const setRowData = (row) => {
|
|||||||
color="error"
|
color="error"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -73,19 +73,27 @@ const setRowData = (row) => {
|
|||||||
+ Stundensatz
|
+ Stundensatz
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
||||||
<table class="w-full mt-3">
|
<div class="mt-3 overflow-x-auto">
|
||||||
<tr>
|
<table class="w-full min-w-[52rem] table-fixed">
|
||||||
<th>Name</th>
|
<thead>
|
||||||
<th>Menge</th>
|
<tr class="border-b border-gray-200 dark:border-gray-800">
|
||||||
<th>Einheit</th>
|
<th class="px-2 py-2 text-left font-medium">Name</th>
|
||||||
<th>Einkaufpreis</th>
|
<th class="px-2 py-2 text-left font-medium">Menge</th>
|
||||||
<th>Verkaufspreis</th>
|
<th class="px-2 py-2 text-left font-medium">Einheit</th>
|
||||||
</tr>
|
<th class="px-2 py-2 text-left font-medium">Einkaufspreis</th>
|
||||||
<tr
|
<th class="px-2 py-2 text-left font-medium">Verkaufspreis</th>
|
||||||
v-for="row in props.item.personalComposition"
|
<th class="w-12 px-2 py-2"></th>
|
||||||
>
|
</tr>
|
||||||
<td>
|
</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
|
<USelectMenu
|
||||||
|
class="w-full"
|
||||||
:items="hourrates"
|
:items="hourrates"
|
||||||
label-key="name"
|
label-key="name"
|
||||||
value-key="id"
|
value-key="id"
|
||||||
@@ -93,47 +101,55 @@ const setRowData = (row) => {
|
|||||||
:filter-fields="['name']"
|
:filter-fields="['name']"
|
||||||
v-model="row.hourrate"
|
v-model="row.hourrate"
|
||||||
:color="row.hourrate ? 'primary' : 'error'"
|
:color="row.hourrate ? 'primary' : 'error'"
|
||||||
@change="setRowData(row)"
|
@update:model-value="setRowData(row)"
|
||||||
>
|
>
|
||||||
<!-- <template #label>
|
<template #default>
|
||||||
{{products.find(i => i.id === product.product) ? products.find(i => i.id === product.product).name : 'Kein Artikel ausgewählt'}}
|
{{ hourrates.find(i => i.id === row.hourrate)?.name || 'Kein Stundensatz ausgewählt' }}
|
||||||
</template>-->
|
</template>
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="px-2 py-2">
|
||||||
<UInput
|
<UInput
|
||||||
|
class="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
v-model="row.quantity"
|
v-model="row.quantity"
|
||||||
:step="units.find(i => i.id === row.unit) ? units.find(i => i.id === row.unit).step : 1"
|
:step="units.find(i => i.id === row.unit) ? units.find(i => i.id === row.unit).step : 1"
|
||||||
@change="calculateTotalPersonalPrice"
|
@change="calculateTotalPersonalPrice"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="px-2 py-2">
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
|
class="w-full"
|
||||||
:items="units"
|
:items="units"
|
||||||
disabled
|
disabled
|
||||||
label-key="name"
|
label-key="name"
|
||||||
value-key="id"
|
value-key="id"
|
||||||
v-model="row.unit"
|
v-model="row.unit"
|
||||||
></USelectMenu>
|
>
|
||||||
|
<template #default>
|
||||||
|
{{ units.find(i => i.id === row.unit)?.name || 'Einheit' }}
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="px-2 py-2">
|
||||||
<UInput
|
<UInput
|
||||||
|
class="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
v-model="row.purchasePrice"
|
v-model="row.purchasePrice"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
@change="calculateTotalPersonalPrice"
|
@change="calculateTotalPersonalPrice"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="px-2 py-2">
|
||||||
<UInput
|
<UInput
|
||||||
|
class="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
v-model="row.price"
|
v-model="row.price"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
@change="calculateTotalPersonalPrice"
|
@change="calculateTotalPersonalPrice"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="px-2 py-2">
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-x-mark"
|
icon="i-heroicons-x-mark"
|
||||||
@click="removeRowFromPersonalComposition(row.id)"
|
@click="removeRowFromPersonalComposition(row.id)"
|
||||||
@@ -141,8 +157,10 @@ const setRowData = (row) => {
|
|||||||
color="error"
|
color="error"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer="{ collapsed }">
|
<template #footer="{ collapsed }">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3 w-full">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<UColorModeSwitch :class="[collapsed ? 'mx-auto' : 'ml-3']"/>
|
<UColorModeSwitch :class="[collapsed ? 'mx-auto' : 'ml-3']"/>
|
||||||
<UDashboardSidebarCollapse v-if="collapsed" class="mx-auto" />
|
<UDashboardSidebarCollapse v-if="collapsed" class="mx-auto" />
|
||||||
@@ -284,7 +284,7 @@ onMounted(() => {
|
|||||||
:key="item.label"
|
:key="item.label"
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="w-full"
|
class="w-full min-w-0 justify-start rounded-lg px-2.5 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
@click="item.click ? item.click() : null"
|
@click="item.click ? item.click() : null"
|
||||||
>
|
>
|
||||||
@@ -305,10 +305,10 @@ onMounted(() => {
|
|||||||
</UDashboardSidebar>
|
</UDashboardSidebar>
|
||||||
|
|
||||||
|
|
||||||
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
<UDashboardPanel class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||||
<slot/>
|
<slot/>
|
||||||
|
|
||||||
</div>
|
</UDashboardPanel>
|
||||||
</UDashboardGroup>
|
</UDashboardGroup>
|
||||||
|
|
||||||
<HelpSlideover/>
|
<HelpSlideover/>
|
||||||
|
|||||||
545
frontend/pages/accounting/bwa.vue
Normal file
545
frontend/pages/accounting/bwa.vue
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import {
|
||||||
|
getCreatedDocumentTaxBreakdown,
|
||||||
|
getIncomingInvoiceTaxBreakdown
|
||||||
|
} from "~/composables/useTaxEvaluation"
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const createdDocuments = ref<any[]>([])
|
||||||
|
const incomingInvoices = ref<any[]>([])
|
||||||
|
const accounts = ref<any[]>([])
|
||||||
|
const ownAccounts = ref<any[]>([])
|
||||||
|
const statementAllocations = ref<any[]>([])
|
||||||
|
|
||||||
|
const selectedYear = ref(String(dayjs().year()))
|
||||||
|
const selectedMonth = ref("all")
|
||||||
|
|
||||||
|
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 accountColumns = [
|
||||||
|
{ accessorKey: "number", header: "Nummer" },
|
||||||
|
{ accessorKey: "label", header: "Konto" },
|
||||||
|
{ accessorKey: "bookings", header: "Buchungen" },
|
||||||
|
{ accessorKey: "net", header: "Netto" },
|
||||||
|
{ accessorKey: "tax", header: "Steuer" },
|
||||||
|
{ accessorKey: "gross", header: "Brutto" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const ownAccountColumns = [
|
||||||
|
{ accessorKey: "number", header: "Nummer" },
|
||||||
|
{ accessorKey: "label", header: "Konto" },
|
||||||
|
{ accessorKey: "bookings", header: "Buchungen" },
|
||||||
|
{ accessorKey: "income", header: "Einnahmen" },
|
||||||
|
{ accessorKey: "expenses", header: "Ausgaben" },
|
||||||
|
{ accessorKey: "balance", header: "Saldo" }
|
||||||
|
]
|
||||||
|
|
||||||
|
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 sameId = (left: any, right: any) => String(left ?? "") === String(right ?? "")
|
||||||
|
|
||||||
|
const getStatementDate = (allocation: any) => {
|
||||||
|
return allocation?.bankstatement?.date || allocation?.bankstatement?.valueDate || allocation?.date || allocation?.created_at || null
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchesSelectedPeriod = (dateValue: any) => {
|
||||||
|
const parsed = dayjs(dateValue)
|
||||||
|
|
||||||
|
if (!parsed.isValid()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(parsed.year()) !== selectedYear.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedMonth.value !== "all" && parsed.month() + 1 !== Number(selectedMonth.value)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const computeDocumentNet = (doc: any) => {
|
||||||
|
return Number((doc?.rows || []).reduce((sum: number, row: any) => {
|
||||||
|
if (!row || ["pagebreak", "title", "text"].includes(row.mode)) {
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantity = Number(row.quantity || 0)
|
||||||
|
const price = Number(row.price || 0)
|
||||||
|
const discountPercent = Number(row.discountPercent || 0)
|
||||||
|
|
||||||
|
return sum + (quantity * price * (1 - discountPercent / 100))
|
||||||
|
}, 0).toFixed(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
const computeIncomingInvoiceGross = (invoice: any) => {
|
||||||
|
return Number((invoice?.accounts || []).reduce((sum: number, account: any) => {
|
||||||
|
const amountNet = Number(account?.amountNet || 0)
|
||||||
|
const amountTax = Number(account?.amountTax || 0)
|
||||||
|
const amountGross = Number(account?.amountGross)
|
||||||
|
|
||||||
|
return sum + (Number.isFinite(amountGross) ? amountGross : amountNet + amountTax)
|
||||||
|
}, 0).toFixed(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearItems = computed(() => {
|
||||||
|
const years = new Set<string>([String(dayjs().year())])
|
||||||
|
|
||||||
|
createdDocuments.value.forEach((doc) => {
|
||||||
|
const parsed = dayjs(doc.documentDate)
|
||||||
|
if (parsed.isValid()) {
|
||||||
|
years.add(String(parsed.year()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
incomingInvoices.value.forEach((invoice) => {
|
||||||
|
const parsed = dayjs(invoice.date)
|
||||||
|
if (parsed.isValid()) {
|
||||||
|
years.add(String(parsed.year()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
statementAllocations.value.forEach((allocation) => {
|
||||||
|
const parsed = dayjs(getStatementDate(allocation))
|
||||||
|
if (parsed.isValid()) {
|
||||||
|
years.add(String(parsed.year()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(years)
|
||||||
|
.sort((a, b) => Number(b) - Number(a))
|
||||||
|
.map((year) => ({ label: year, value: year }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredDocuments = computed(() => {
|
||||||
|
return createdDocuments.value.filter((doc) => matchesSelectedPeriod(doc.documentDate))
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredIncomingInvoices = computed(() => {
|
||||||
|
return incomingInvoices.value.filter((invoice) => matchesSelectedPeriod(invoice.date))
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredStatementAllocations = computed(() => {
|
||||||
|
return statementAllocations.value.filter((allocation) => matchesSelectedPeriod(getStatementDate(allocation)))
|
||||||
|
})
|
||||||
|
|
||||||
|
const incomeTotal = computed(() => {
|
||||||
|
return Number(filteredDocuments.value.reduce((sum, doc) => sum + computeDocumentNet(doc), 0).toFixed(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
const expenseNetTotal = computed(() => {
|
||||||
|
return Number(filteredIncomingInvoices.value.reduce((sum, invoice) => {
|
||||||
|
return sum + (invoice.accounts || []).reduce((accountSum: number, account: any) => accountSum + Number(account.amountNet || 0), 0)
|
||||||
|
}, 0).toFixed(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
const expenseGrossTotal = computed(() => {
|
||||||
|
return Number(filteredIncomingInvoices.value.reduce((sum, invoice) => sum + computeIncomingInvoiceGross(invoice), 0).toFixed(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
const taxSummary = computed(() => {
|
||||||
|
const output = filteredDocuments.value.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 = filteredIncomingInvoices.value.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))
|
||||||
|
|
||||||
|
return {
|
||||||
|
output,
|
||||||
|
input,
|
||||||
|
outputTax,
|
||||||
|
inputTax,
|
||||||
|
balance: Number((outputTax - inputTax).toFixed(2))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const operatingResult = computed(() => {
|
||||||
|
return Number((incomeTotal.value - expenseNetTotal.value).toFixed(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
const accountRows = computed(() => {
|
||||||
|
return accounts.value
|
||||||
|
.map((account) => {
|
||||||
|
const bookings = filteredIncomingInvoices.value.flatMap((invoice) => {
|
||||||
|
return (invoice.accounts || [])
|
||||||
|
.filter((invoiceAccount: any) => sameId(invoiceAccount.account?.id || invoiceAccount.account, account.id))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (bookings.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const net = bookings.reduce((sum, booking: any) => sum + Number(booking.amountNet || 0), 0)
|
||||||
|
const tax = bookings.reduce((sum, booking: any) => sum + Number(booking.amountTax || 0), 0)
|
||||||
|
const gross = bookings.reduce((sum, booking: any) => {
|
||||||
|
const amountGross = Number(booking.amountGross)
|
||||||
|
return sum + (Number.isFinite(amountGross) ? amountGross : Number(booking.amountNet || 0) + Number(booking.amountTax || 0))
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: account.id,
|
||||||
|
number: account.number || "-",
|
||||||
|
label: account.label || account.name || "-",
|
||||||
|
bookings: bookings.length,
|
||||||
|
net: Number(net.toFixed(2)),
|
||||||
|
tax: Number(tax.toFixed(2)),
|
||||||
|
gross: Number(gross.toFixed(2))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((left: any, right: any) => Number(right.gross) - Number(left.gross))
|
||||||
|
})
|
||||||
|
|
||||||
|
const ownAccountRows = computed(() => {
|
||||||
|
return ownAccounts.value
|
||||||
|
.map((account) => {
|
||||||
|
const bookings = filteredStatementAllocations.value.filter((allocation) => sameId(allocation.ownaccount?.id || allocation.ownaccount, account.id))
|
||||||
|
|
||||||
|
if (bookings.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const income = bookings.reduce((sum, booking) => {
|
||||||
|
const amount = Number(booking.amount || 0)
|
||||||
|
return amount > 0 ? sum + amount : sum
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const expenses = bookings.reduce((sum, booking) => {
|
||||||
|
const amount = Number(booking.amount || 0)
|
||||||
|
return amount < 0 ? sum + Math.abs(amount) : sum
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const balance = bookings.reduce((sum, booking) => sum + Number(booking.amount || 0), 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: account.id,
|
||||||
|
number: account.number || "-",
|
||||||
|
label: account.name || account.label || "-",
|
||||||
|
bookings: bookings.length,
|
||||||
|
income: Number(income.toFixed(2)),
|
||||||
|
expenses: Number(expenses.toFixed(2)),
|
||||||
|
balance: Number(balance.toFixed(2))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((left: any, right: any) => Math.abs(Number(right.balance)) - Math.abs(Number(left.balance)))
|
||||||
|
})
|
||||||
|
|
||||||
|
const setupPage = async () => {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [docs, invoices, accountItems, ownAccountItems, allocationItems] = await Promise.all([
|
||||||
|
useEntities("createddocuments").select(),
|
||||||
|
useEntities("incominginvoices").select("*, vendor(*)"),
|
||||||
|
useEntities("accounts").selectSpecial(),
|
||||||
|
useEntities("ownaccounts").select(),
|
||||||
|
useEntities("statementallocations").select("*, bankstatement(*), createddocument(*), incominginvoice(*)")
|
||||||
|
])
|
||||||
|
|
||||||
|
createdDocuments.value = (docs || []).filter(isRelevantOutputDocument)
|
||||||
|
incomingInvoices.value = (invoices || []).filter(isRelevantInputInvoice)
|
||||||
|
accounts.value = accountItems || []
|
||||||
|
ownAccounts.value = ownAccountItems || []
|
||||||
|
statementAllocations.value = allocationItems || []
|
||||||
|
|
||||||
|
const firstYear = yearItems.value[0]?.value
|
||||||
|
if (firstYear && !yearItems.value.some((item) => item.value === selectedYear.value)) {
|
||||||
|
selectedYear.value = firstYear
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAccount = (rowLike: any) => {
|
||||||
|
const row = rowLike?.original || rowLike
|
||||||
|
if (row?.id) {
|
||||||
|
router.push(`/accounts/show/${row.id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openOwnAccount = (rowLike: any) => {
|
||||||
|
const row = rowLike?.original || rowLike
|
||||||
|
if (row?.id) {
|
||||||
|
router.push(`/standardEntity/ownaccounts/show/${row.id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(setupPage)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardNavbar title="BWA">
|
||||||
|
<template #right>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-arrow-path"
|
||||||
|
variant="outline"
|
||||||
|
:loading="loading"
|
||||||
|
@click="setupPage"
|
||||||
|
>
|
||||||
|
Aktualisieren
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardPanelContent class="min-w-0 space-y-6 overflow-x-hidden overflow-y-auto p-4 md:p-6">
|
||||||
|
<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 min-w-0 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<UCard class="min-w-0">
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">Einnahmen netto</div>
|
||||||
|
<div class="mt-2 text-2xl font-semibold">{{ useCurrency(incomeTotal) }}</div>
|
||||||
|
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ filteredDocuments.length }} gebuchte Ausgangsbelege
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="min-w-0">
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">Ausgaben netto</div>
|
||||||
|
<div class="mt-2 text-2xl font-semibold">{{ useCurrency(expenseNetTotal) }}</div>
|
||||||
|
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Brutto: {{ useCurrency(expenseGrossTotal) }}
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="min-w-0">
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">Betriebsergebnis</div>
|
||||||
|
<div class="mt-2 text-2xl font-semibold" :class="operatingResult >= 0 ? 'text-primary-500' : 'text-error'">
|
||||||
|
{{ useCurrency(operatingResult) }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Einnahmen minus Ausgaben netto
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="min-w-0">
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">USt-Saldo</div>
|
||||||
|
<div class="mt-2 text-2xl font-semibold" :class="taxSummary.balance >= 0 ? 'text-amber-600 dark:text-amber-400' : 'text-primary-500'">
|
||||||
|
{{ useCurrency(taxSummary.balance) }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
USt {{ useCurrency(taxSummary.outputTax) }} | Vorsteuer {{ useCurrency(taxSummary.inputTax) }}
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid min-w-0 gap-4 xl:grid-cols-2">
|
||||||
|
<UCard class="min-w-0">
|
||||||
|
<template #header>
|
||||||
|
<div class="font-semibold">USt-Details</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-3 text-sm">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>Netto 19% Ausgangsbelege</span>
|
||||||
|
<span>{{ useCurrency(taxSummary.output.net19) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>USt 19%</span>
|
||||||
|
<span>{{ useCurrency(taxSummary.output.tax19) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>Netto 7% Ausgangsbelege</span>
|
||||||
|
<span>{{ useCurrency(taxSummary.output.net7) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>USt 7%</span>
|
||||||
|
<span>{{ useCurrency(taxSummary.output.tax7) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>Steuerfrei</span>
|
||||||
|
<span>{{ useCurrency(taxSummary.output.net0 + taxSummary.input.net0) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="min-w-0">
|
||||||
|
<template #header>
|
||||||
|
<div class="font-semibold">Vorsteuer-Details</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-3 text-sm">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>Netto 19% Eingangsbelege</span>
|
||||||
|
<span>{{ useCurrency(taxSummary.input.net19) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>Vorsteuer 19%</span>
|
||||||
|
<span>{{ useCurrency(taxSummary.input.tax19) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>Netto 7% Eingangsbelege</span>
|
||||||
|
<span>{{ useCurrency(taxSummary.input.net7) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>Vorsteuer 7%</span>
|
||||||
|
<span>{{ useCurrency(taxSummary.input.tax7) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>Steuerfrei</span>
|
||||||
|
<span>{{ useCurrency(taxSummary.input.net0) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid min-w-0 gap-4 xl:grid-cols-2">
|
||||||
|
<UCard class="min-w-0">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<span class="font-semibold">Buchungskonten</span>
|
||||||
|
<UBadge color="neutral" variant="soft">{{ accountRows.length }}</UBadge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="min-w-0">
|
||||||
|
<UTable
|
||||||
|
:data="accountRows"
|
||||||
|
:columns="normalizeTableColumns(accountColumns)"
|
||||||
|
:loading="loading"
|
||||||
|
:on-select="openAccount"
|
||||||
|
>
|
||||||
|
<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 Buchungskonten im ausgewaehlten Zeitraum</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #label-cell="{ row }">
|
||||||
|
<div class="truncate font-medium">{{ row.original.label }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #bookings-cell="{ row }">
|
||||||
|
<div class="text-right">{{ row.original.bookings }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #net-cell="{ row }">
|
||||||
|
<div class="text-right tabular-nums">{{ useCurrency(row.original.net) }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #tax-cell="{ row }">
|
||||||
|
<div class="text-right tabular-nums">{{ useCurrency(row.original.tax) }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #gross-cell="{ row }">
|
||||||
|
<div class="text-right font-medium tabular-nums">{{ useCurrency(row.original.gross) }}</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="min-w-0">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<span class="font-semibold">Eigene Buchungskonten</span>
|
||||||
|
<UBadge color="neutral" variant="soft">{{ ownAccountRows.length }}</UBadge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="min-w-0">
|
||||||
|
<UTable
|
||||||
|
:data="ownAccountRows"
|
||||||
|
:columns="normalizeTableColumns(ownAccountColumns)"
|
||||||
|
:loading="loading"
|
||||||
|
:on-select="openOwnAccount"
|
||||||
|
>
|
||||||
|
<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 eigenen Buchungen im ausgewaehlten Zeitraum</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #label-cell="{ row }">
|
||||||
|
<div class="truncate font-medium">{{ row.original.label }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #bookings-cell="{ row }">
|
||||||
|
<div class="text-right">{{ row.original.bookings }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #income-cell="{ row }">
|
||||||
|
<div class="text-right text-primary-500 tabular-nums">{{ useCurrency(row.original.income) }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #expenses-cell="{ row }">
|
||||||
|
<div class="text-right text-error tabular-nums">{{ useCurrency(row.original.expenses) }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #balance-cell="{ row }">
|
||||||
|
<div class="text-right font-medium tabular-nums" :class="row.original.balance >= 0 ? 'text-primary-500' : 'text-error'">
|
||||||
|
{{ useCurrency(row.original.balance) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</UDashboardPanelContent>
|
||||||
|
</template>
|
||||||
@@ -198,7 +198,7 @@ setupPage()
|
|||||||
:columns="normalizeTableColumns(columns)"
|
:columns="normalizeTableColumns(columns)"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||||
:on-select="(i) => router.push(`/accounts/show/${i.id}`)"
|
:on-select="(row) => router.push(`/accounts/show/${row.original.id}`)"
|
||||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
|
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
|
||||||
>
|
>
|
||||||
<template #allocations-cell="{row}">
|
<template #allocations-cell="{row}">
|
||||||
|
|||||||
@@ -1,62 +1,44 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const itemInfo = ref(null)
|
const itemInfo = ref(null)
|
||||||
const statementallocations = ref([])
|
const statementallocations = ref([])
|
||||||
const incominginvoices = ref([])
|
const incominginvoices = ref([])
|
||||||
|
const currentAccountId = computed(() => String(route.params.id))
|
||||||
|
const sameAccount = (value) => String(value ?? "") === currentAccountId.value
|
||||||
|
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
itemInfo.value = (await useEntities("accounts").selectSpecial("*")).find(i => i.id === Number(route.params.id))
|
itemInfo.value = (await useEntities("accounts").selectSpecial("*")).find(i => i.id === Number(route.params.id))
|
||||||
statementallocations.value = (await useEntities("statementallocations").select("*, bs_id(*)")).filter(i => i.account === Number(route.params.id))
|
statementallocations.value = (await useEntities("statementallocations").select("*, bankstatement(*)"))
|
||||||
incominginvoices.value = (await useEntities("incominginvoices").select("*, vendor(*)")).filter(i => i.accounts.find(x => x.account === Number(route.params.id)))
|
.filter((allocation) => sameAccount(allocation.account?.id || allocation.account))
|
||||||
|
incominginvoices.value = (await useEntities("incominginvoices").select("*, vendor(*)"))
|
||||||
|
.filter((invoice) => (invoice.accounts || []).some((account) => sameAccount(account.account?.id || account.account)))
|
||||||
}
|
}
|
||||||
|
|
||||||
setup()
|
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 renderedAllocations = computed(() => {
|
const renderedAllocations = computed(() => {
|
||||||
|
const statementRows = statementallocations.value.map((allocation) => ({
|
||||||
|
...allocation,
|
||||||
|
type: "statementallocation",
|
||||||
|
amount: Number(allocation.amount || 0)
|
||||||
|
}))
|
||||||
|
|
||||||
let tempstatementallocations = statementallocations.value.map(i => {
|
const incomingInvoiceRows = incominginvoices.value.flatMap((invoice) => {
|
||||||
return {
|
return (invoice.accounts || [])
|
||||||
...i,
|
.filter((account) => sameAccount(account.account?.id || account.account))
|
||||||
type: "statementallocation",
|
.map((account, index) => ({
|
||||||
date: i.bs_id.date,
|
id: `${invoice.id}-${index}`,
|
||||||
partner: i.bs_id ? (i.bs_id.debName ? i.bs_id.debName : (i.bs_id.credName ? i.bs_id.credName : '')) : ''
|
incominginvoiceid: invoice.id,
|
||||||
}
|
type: "incominginvoice",
|
||||||
|
amount: Number(account.amountGross || account.amountNet || 0),
|
||||||
|
expense: invoice.expense
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return [...statementRows, ...incomingInvoiceRows]
|
||||||
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",
|
|
||||||
expense: i.expense
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
return [...tempstatementallocations, ... incominginvoicesallocations]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const saldo = computed(() => {
|
const saldo = computed(() => {
|
||||||
@@ -135,25 +117,12 @@ const saldo = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
<UCard class="mt-5" v-if="item.label === 'Buchungen'">
|
<UCard class="mt-5" v-if="item.label === 'Buchungen'">
|
||||||
<UTable
|
<EntityShowSubOwnAccountsStatements
|
||||||
v-if="statementallocations"
|
v-if="itemInfo"
|
||||||
:data="renderedAllocations"
|
:item="itemInfo"
|
||||||
:columns="normalizeTableColumns([{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}])"
|
top-level-type="accounts"
|
||||||
:on-select="(i) => selectAllocation(i)"
|
platform="desktop"
|
||||||
: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>
|
|
||||||
</UCard>
|
</UCard>
|
||||||
</template>
|
</template>
|
||||||
</UTabs>
|
</UTabs>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -98,7 +98,7 @@
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||||
:on-select="(row) => router.push(`/createDocument/edit/${row.id}`)"
|
:on-select="(row) => router.push(`/createDocument/edit/${row.id}`)"
|
||||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
empty="Keine Belege anzuzeigen"
|
||||||
>
|
>
|
||||||
<template #actions-cell="{ row }">
|
<template #actions-cell="{ row }">
|
||||||
<div @click.stop>
|
<div @click.stop>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import InputGroup from "~/components/InputGroup.vue";
|
import InputGroup from "~/components/InputGroup.vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { parseDate } from "@internationalized/date"
|
||||||
import { useDraggable } from '@vueuse/core'
|
import { useDraggable } from '@vueuse/core'
|
||||||
|
|
||||||
// --- Standard Setup & Data ---
|
// --- Standard Setup & Data ---
|
||||||
@@ -44,6 +45,9 @@ const costcentres = ref([])
|
|||||||
const vendors = ref([])
|
const vendors = ref([])
|
||||||
const accounts = ref([])
|
const accounts = ref([])
|
||||||
const loadedFileId = ref(null)
|
const loadedFileId = ref(null)
|
||||||
|
const invoiceFiles = ref([])
|
||||||
|
const paymentTypeItems = ['Überweisung', 'Lastschrift', 'Kreditkarte', 'PayPal', 'Bar', 'Sonstiges']
|
||||||
|
const files = useFiles()
|
||||||
|
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
// 1. Daten laden
|
// 1. Daten laden
|
||||||
@@ -67,7 +71,9 @@ const setup = async () => {
|
|||||||
|
|
||||||
// Datei laden
|
// Datei laden
|
||||||
if (itemInfo.value.files && itemInfo.value.files.length > 0) {
|
if (itemInfo.value.files && itemInfo.value.files.length > 0) {
|
||||||
loadedFileId.value = itemInfo.value.files[itemInfo.value.files.length-1].id
|
invoiceFiles.value = await files.selectSomeDocuments(itemInfo.value.files.map((file) => file.id))
|
||||||
|
const latestPdf = [...invoiceFiles.value].reverse().find((file) => file?.path?.toLowerCase().includes('.pdf'))
|
||||||
|
loadedFileId.value = latestPdf?.id || null
|
||||||
}
|
}
|
||||||
|
|
||||||
if(itemInfo.value.date && !itemInfo.value.dueDate) itemInfo.value.dueDate = itemInfo.value.date
|
if(itemInfo.value.date && !itemInfo.value.dueDate) itemInfo.value.dueDate = itemInfo.value.date
|
||||||
@@ -98,6 +104,23 @@ const taxOptions = ref([
|
|||||||
{ label: "Keine USt", percentage: 0, key: "null" },
|
{ label: "Keine USt", percentage: 0, key: "null" },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const getCalendarValue = (value) => {
|
||||||
|
if (!value) return undefined
|
||||||
|
|
||||||
|
const formatted = dayjs(value).format('YYYY-MM-DD')
|
||||||
|
return formatted ? parseDate(formatted) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDateField = (field, value) => {
|
||||||
|
itemInfo.value[field] = value ? dayjs(value.toString()).toDate() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDateFieldToToday = (field) => {
|
||||||
|
itemInfo.value[field] = dayjs().toDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDateButtonLabel = (value, emptyLabel = "Kein Datum") => value ? dayjs(value).format('DD.MM.YYYY') : emptyLabel
|
||||||
|
|
||||||
const totalCalculated = computed(() => {
|
const totalCalculated = computed(() => {
|
||||||
let totalNet = 0
|
let totalNet = 0
|
||||||
let totalAmount19Tax = 0
|
let totalAmount19Tax = 0
|
||||||
@@ -335,18 +358,18 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
|||||||
<USelectMenu
|
<USelectMenu
|
||||||
class="w-full"
|
class="w-full"
|
||||||
v-model="itemInfo.vendor"
|
v-model="itemInfo.vendor"
|
||||||
:options="vendors"
|
:items="vendors"
|
||||||
option-attribute="name"
|
label-key="name"
|
||||||
value-attribute="id"
|
value-key="id"
|
||||||
searchable
|
:search-input="{ placeholder: 'Lieferant suchen...' }"
|
||||||
:disabled="mode === 'show'"
|
:disabled="mode === 'show'"
|
||||||
:search-attributes="['name', 'vendorNumber']"
|
:filter-fields="['name', 'vendorNumber']"
|
||||||
placeholder="Lieferant suchen..."
|
:color="itemInfo.vendor ? 'primary' : 'error'"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #default>
|
||||||
{{ vendors.find(v => v.id === itemInfo.vendor)?.name || 'Bitte wählen' }}
|
{{ vendors.find(v => v.id === itemInfo.vendor)?.name || 'Bitte wählen' }}
|
||||||
</template>
|
</template>
|
||||||
<template #option="{ option }">
|
<template #item="{ item: option }">
|
||||||
<span class="font-mono text-xs opacity-75 mr-2">{{ option.vendorNumber }}</span> {{ option.name }}
|
<span class="font-mono text-xs opacity-75 mr-2">{{ option.vendorNumber }}</span> {{ option.name }}
|
||||||
</template>
|
</template>
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
@@ -368,33 +391,81 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
|||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UFormField label="Rechnungsnummer">
|
<UFormField label="Rechnungsnummer">
|
||||||
<UInput v-model="itemInfo.reference" icon="i-heroicons-hashtag" :disabled="mode === 'show'" />
|
<UInput class="w-full" v-model="itemInfo.reference" icon="i-heroicons-hashtag" :disabled="mode === 'show'" :color="itemInfo.reference ? 'primary' : 'error'" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UFormField label="Zahlart">
|
<UFormField label="Zahlart">
|
||||||
<USelectMenu v-model="itemInfo.paymentType" :options="['Überweisung', 'Lastschrift', 'Kreditkarte', 'PayPal', 'Bar', 'Sonstiges']" :disabled="mode === 'show'" />
|
<USelectMenu v-model="itemInfo.paymentType" :items="paymentTypeItems" :disabled="mode === 'show'" class="w-full" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UFormField label="Rechnungsdatum">
|
<UFormField label="Rechnungsdatum">
|
||||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
<UPopover class="w-full" :content="{ side: 'bottom', align: 'start' }">
|
||||||
<UButton block color="white" icon="i-heroicons-calendar" :label="itemInfo.date ? dayjs(itemInfo.date).format('DD.MM.YYYY') : '-'" :disabled="mode === 'show'" />
|
<UButton
|
||||||
<template #panel="{ close }">
|
block
|
||||||
<LazyDatePicker v-model="itemInfo.date" @close="() => { if(!itemInfo.dueDate) itemInfo.dueDate = itemInfo.date; close() }" />
|
icon="i-heroicons-calendar"
|
||||||
|
:label="getDateButtonLabel(itemInfo.date)"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
|
:color="itemInfo.date ? 'neutral' : 'error'"
|
||||||
|
variant="outline"
|
||||||
|
class="w-full justify-start"
|
||||||
|
/>
|
||||||
|
<template #content>
|
||||||
|
<div class="p-2">
|
||||||
|
<UCalendar
|
||||||
|
:model-value="getCalendarValue(itemInfo.date)"
|
||||||
|
@update:model-value="(value) => { setDateField('date', value); if (!itemInfo.dueDate) itemInfo.dueDate = itemInfo.date }"
|
||||||
|
:week-starts-on="1"
|
||||||
|
/>
|
||||||
|
<div class="flex justify-end px-2 pb-2">
|
||||||
|
<UButton
|
||||||
|
size="xs"
|
||||||
|
color="gray"
|
||||||
|
variant="soft"
|
||||||
|
icon="i-heroicons-calendar-days"
|
||||||
|
label="Heute"
|
||||||
|
@click="() => { setDateFieldToToday('date'); if (!itemInfo.dueDate) itemInfo.dueDate = itemInfo.date }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UPopover>
|
</UPopover>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UFormField label="Fälligkeitsdatum">
|
<UFormField label="Fälligkeitsdatum">
|
||||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
<UPopover class="w-full" :content="{ side: 'bottom', align: 'start' }">
|
||||||
<UButton block color="white" icon="i-heroicons-calendar" :label="itemInfo.dueDate ? dayjs(itemInfo.dueDate).format('DD.MM.YYYY') : '-'" :disabled="mode === 'show'" />
|
<UButton
|
||||||
<template #panel="{ close }">
|
block
|
||||||
<LazyDatePicker v-model="itemInfo.dueDate" @close="close" />
|
icon="i-heroicons-calendar"
|
||||||
|
:label="getDateButtonLabel(itemInfo.dueDate)"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
|
:color="itemInfo.dueDate ? 'neutral' : 'error'"
|
||||||
|
variant="outline"
|
||||||
|
class="w-full justify-start"
|
||||||
|
/>
|
||||||
|
<template #content>
|
||||||
|
<div class="p-2">
|
||||||
|
<UCalendar
|
||||||
|
:model-value="getCalendarValue(itemInfo.dueDate)"
|
||||||
|
@update:model-value="(value) => setDateField('dueDate', value)"
|
||||||
|
:week-starts-on="1"
|
||||||
|
/>
|
||||||
|
<div class="flex justify-end px-2 pb-2">
|
||||||
|
<UButton
|
||||||
|
size="xs"
|
||||||
|
color="gray"
|
||||||
|
variant="soft"
|
||||||
|
icon="i-heroicons-calendar-days"
|
||||||
|
label="Heute"
|
||||||
|
@click="setDateFieldToToday('dueDate')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UPopover>
|
</UPopover>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UFormField label="Beschreibung / Notiz" class="md:col-span-2">
|
<UFormField label="Beschreibung / Notiz" class="md:col-span-2">
|
||||||
<UTextarea v-model="itemInfo.description" :rows="2" autoresize :disabled="mode === 'show'" />
|
<UTextarea class="w-full" v-model="itemInfo.description" :rows="2" autoresize :disabled="mode === 'show'" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
@@ -430,19 +501,20 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
|||||||
<div class="col-span-12 md:col-span-6">
|
<div class="col-span-12 md:col-span-6">
|
||||||
<UFormField label="Konto / Kategorie">
|
<UFormField label="Konto / Kategorie">
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
|
class="w-full"
|
||||||
v-model="item.account"
|
v-model="item.account"
|
||||||
:options="accounts"
|
:items="accounts"
|
||||||
searchable
|
:search-input="{ placeholder: 'Kategorie wählen' }"
|
||||||
placeholder="Kategorie wählen"
|
label-key="label"
|
||||||
option-attribute="label"
|
value-key="id"
|
||||||
value-attribute="id"
|
|
||||||
:disabled="mode === 'show'"
|
:disabled="mode === 'show'"
|
||||||
:search-attributes="['label', 'number']"
|
:filter-fields="['label', 'number']"
|
||||||
|
:color="item.account ? 'primary' : 'error'"
|
||||||
>
|
>
|
||||||
<template #option="{ option }">
|
<template #item="{ item: option }">
|
||||||
<span class="font-mono text-xs text-gray-500 mr-2">{{ option.number }}</span> {{ option.label }}
|
<span class="font-mono text-xs text-gray-500 mr-2">{{ option.number }}</span> {{ option.label }}
|
||||||
</template>
|
</template>
|
||||||
<template #label>
|
<template #default>
|
||||||
{{ accounts.find(a => a.id === item.account)?.label || 'Auswählen' }}
|
{{ accounts.find(a => a.id === item.account)?.label || 'Auswählen' }}
|
||||||
</template>
|
</template>
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
@@ -452,15 +524,15 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
|||||||
<div class="col-span-12 md:col-span-6">
|
<div class="col-span-12 md:col-span-6">
|
||||||
<UFormField label="Kostenstelle">
|
<UFormField label="Kostenstelle">
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
|
class="w-full"
|
||||||
v-model="item.costCentre"
|
v-model="item.costCentre"
|
||||||
:options="costcentres"
|
:items="costcentres"
|
||||||
searchable
|
:search-input="{ placeholder: 'Optional' }"
|
||||||
option-attribute="name"
|
label-key="name"
|
||||||
value-attribute="id"
|
value-key="id"
|
||||||
placeholder="Optional"
|
|
||||||
:disabled="mode === 'show'"
|
:disabled="mode === 'show'"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #default>
|
||||||
{{ costcentres.find(c => c.id === item.costCentre)?.name || 'Keine' }}
|
{{ costcentres.find(c => c.id === item.costCentre)?.name || 'Keine' }}
|
||||||
</template>
|
</template>
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
@@ -470,6 +542,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
|||||||
<div class="col-span-12 md:col-span-3">
|
<div class="col-span-12 md:col-span-3">
|
||||||
<UFormField label="Betrag (Netto)">
|
<UFormField label="Betrag (Netto)">
|
||||||
<UInput
|
<UInput
|
||||||
|
class="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
:disabled="mode === 'show' || !useNetMode"
|
:disabled="mode === 'show' || !useNetMode"
|
||||||
@@ -484,6 +557,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
|||||||
<div class="col-span-12 md:col-span-3">
|
<div class="col-span-12 md:col-span-3">
|
||||||
<UFormField label="Betrag (Brutto)">
|
<UFormField label="Betrag (Brutto)">
|
||||||
<UInput
|
<UInput
|
||||||
|
class="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
:disabled="mode === 'show' || useNetMode"
|
:disabled="mode === 'show' || useNetMode"
|
||||||
@@ -498,19 +572,21 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
|||||||
<div class="col-span-6 md:col-span-3">
|
<div class="col-span-6 md:col-span-3">
|
||||||
<UFormField label="Steuerschlüssel">
|
<UFormField label="Steuerschlüssel">
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
|
class="w-full"
|
||||||
v-model="item.taxType"
|
v-model="item.taxType"
|
||||||
:options="taxOptions"
|
:items="taxOptions"
|
||||||
value-attribute="key"
|
value-key="key"
|
||||||
option-attribute="label"
|
label-key="label"
|
||||||
:disabled="mode === 'show'"
|
:disabled="mode === 'show'"
|
||||||
@change="recalculateItem(item, 'taxType')"
|
@update:model-value="recalculateItem(item, 'taxType')"
|
||||||
|
:color="item.taxType ? 'primary' : 'error'"
|
||||||
/>
|
/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-span-6 md:col-span-3">
|
<div class="col-span-6 md:col-span-3">
|
||||||
<UFormField label="Steuerbetrag" help="Automatisch berechnet">
|
<UFormField label="Steuerbetrag" help="Automatisch berechnet">
|
||||||
<UInput :model-value="item.amountTax" disabled color="gray" >
|
<UInput class="w-full" :model-value="item.amountTax" disabled color="gray" >
|
||||||
<template #trailing>€</template>
|
<template #trailing>€</template>
|
||||||
</UInput>
|
</UInput>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
@@ -538,7 +614,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-span-12">
|
<div class="col-span-12">
|
||||||
<UInput v-model="item.description" :disabled="mode === 'show'" placeholder="Positionstext (optional)" icon="i-heroicons-bars-3-bottom-left" variant="none" class="border-b border-gray-100 dark:border-gray-800" />
|
<UInput v-model="item.description" :disabled="mode === 'show'" placeholder="Positionstext (optional)" icon="i-heroicons-bars-3-bottom-left" variant="none" class="w-full border-b border-gray-100 dark:border-gray-800" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
|
|||||||
@@ -148,7 +148,15 @@ const isPaid = (item) => {
|
|||||||
return Math.abs(amountPaid) === Math.abs(Number(getInvoiceSum(item)))
|
return Math.abs(amountPaid) === Math.abs(Number(getInvoiceSum(item)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectIncomingInvoice = (invoice) => {
|
const unwrapInvoiceRow = (invoiceLike) => invoiceLike?.original || invoiceLike
|
||||||
|
|
||||||
|
const selectIncomingInvoice = (invoiceLike) => {
|
||||||
|
const invoice = unwrapInvoiceRow(invoiceLike)
|
||||||
|
|
||||||
|
if (!invoice?.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (invoice.state === "Gebucht") {
|
if (invoice.state === "Gebucht") {
|
||||||
router.push(`/incomingInvoices/show/${invoice.id}`)
|
router.push(`/incomingInvoices/show/${invoice.id}`)
|
||||||
} else {
|
} else {
|
||||||
@@ -254,7 +262,7 @@ const selectIncomingInvoice = (invoice) => {
|
|||||||
:columns="normalizeTableColumns(columns)"
|
:columns="normalizeTableColumns(columns)"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||||
:on-select="(i) => selectIncomingInvoice(i) "
|
:on-select="selectIncomingInvoice"
|
||||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
||||||
>
|
>
|
||||||
<template #reference-cell="{row}">
|
<template #reference-cell="{row}">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { FormSubmitEvent } from '#ui/types'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "notLoggedIn"
|
layout: "notLoggedIn"
|
||||||
})
|
})
|
||||||
@@ -7,16 +9,23 @@ const auth = useAuthStore()
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const doLogin = async (data:any) => {
|
const state = reactive({
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const doLogin = async (event: FormSubmitEvent<typeof state>) => {
|
||||||
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
await auth.login(data.email, data.password)
|
await auth.login(event.data.email, event.data.password)
|
||||||
// Weiterleiten nach erfolgreichem Login
|
|
||||||
toast.add({title:"Einloggen erfolgreich"})
|
toast.add({title:"Einloggen erfolgreich"})
|
||||||
|
|
||||||
await router.push("/")
|
await router.push("/")
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.add({title:"Zugangsdaten falsch. Bitte überprüfen Sie Ihre Eingaben",color:"error"})
|
toast.add({title:"Zugangsdaten falsch. Bitte überprüfen Sie Ihre Eingaben",color:"error"})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -29,60 +38,42 @@ const doLogin = async (data:any) => {
|
|||||||
dark="/Logo_Dark.png"
|
dark="/Logo_Dark.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UAuthForm
|
<div class="mt-6 space-y-5">
|
||||||
title="Login"
|
<div class="space-y-1">
|
||||||
description="Geben Sie Ihre Anmeldedaten ein um Zugriff auf Ihren Account zu erhalten."
|
<h1 class="text-xl font-semibold">Login</h1>
|
||||||
align="bottom"
|
<p class="text-sm text-muted">
|
||||||
:fields="[{
|
Geben Sie Ihre Anmeldedaten ein um Zugriff auf Ihren Account zu erhalten.
|
||||||
name: 'email',
|
</p>
|
||||||
type: 'text',
|
</div>
|
||||||
label: 'Email',
|
|
||||||
placeholder: 'Deine E-Mail Adresse'
|
|
||||||
}, {
|
|
||||||
name: 'password',
|
|
||||||
label: 'Passwort',
|
|
||||||
type: 'password',
|
|
||||||
placeholder: 'Dein Passwort'
|
|
||||||
}]"
|
|
||||||
:loading="false"
|
|
||||||
@submit="doLogin"
|
|
||||||
:submit-button="{label: 'Weiter'}"
|
|
||||||
divider="oder"
|
|
||||||
>
|
|
||||||
<template #password-hint>
|
|
||||||
<NuxtLink to="/password-reset" class="text-primary font-medium">Passwort vergessen?</NuxtLink>
|
|
||||||
</template>
|
|
||||||
</UAuthForm>
|
|
||||||
</UCard>
|
|
||||||
<!-- <div v-else class="mt-20 m-2 p-2">
|
|
||||||
<UColorModeImage
|
|
||||||
light="/Logo.png"
|
|
||||||
dark="/Logo_Dark.png"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UAuthForm
|
<UForm :state="state" class="space-y-4" @submit="doLogin">
|
||||||
title="Login"
|
<UFormField label="E-Mail" name="email">
|
||||||
description="Geben Sie Ihre Anmeldedaten ein um Zugriff auf Ihren Account zu erhalten."
|
<UInput
|
||||||
align="bottom"
|
v-model="state.email"
|
||||||
:fields="[{
|
type="email"
|
||||||
name: 'email',
|
class="w-full"
|
||||||
type: 'text',
|
placeholder="Deine E-Mail Adresse"
|
||||||
label: 'Email',
|
autocomplete="email"
|
||||||
placeholder: 'Deine E-Mail Adresse'
|
/>
|
||||||
}, {
|
</UFormField>
|
||||||
name: 'password',
|
|
||||||
label: 'Passwort',
|
<UFormField label="Passwort" name="password">
|
||||||
type: 'password',
|
<template #hint>
|
||||||
placeholder: 'Dein Passwort'
|
<NuxtLink to="/password-reset" class="text-primary font-medium">Passwort vergessen?</NuxtLink>
|
||||||
}]"
|
</template>
|
||||||
:loading="false"
|
<UInput
|
||||||
@submit="doLogin"
|
v-model="state.password"
|
||||||
:submit-button="{label: 'Weiter'}"
|
type="password"
|
||||||
divider="oder"
|
class="w-full"
|
||||||
>
|
placeholder="Dein Passwort"
|
||||||
<template #password-hint>
|
autocomplete="current-password"
|
||||||
<NuxtLink to="/password-reset" class="text-primary font-medium">Passwort vergessen?</NuxtLink>
|
/>
|
||||||
</template>
|
</UFormField>
|
||||||
</UAuthForm>
|
|
||||||
</div>-->
|
<UButton type="submit" block class="w-full" :loading="loading">
|
||||||
|
Weiter
|
||||||
|
</UButton>
|
||||||
|
</UForm>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { FormSubmitEvent } from '#ui/types'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "notLoggedIn"
|
layout: "notLoggedIn"
|
||||||
})
|
})
|
||||||
@@ -6,25 +8,31 @@ definePageMeta({
|
|||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
oldPassword: '',
|
||||||
|
newPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const doChange = async (event: FormSubmitEvent<typeof state>) => {
|
||||||
const doChange = async (data:any) => {
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await useNuxtApp().$api("/api/auth/password/change", {
|
await useNuxtApp().$api("/api/auth/password/change", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
old_password: data.oldPassword,
|
old_password: event.data.oldPassword,
|
||||||
new_password: data.newPassword,
|
new_password: event.data.newPassword,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Weiterleiten nach erfolgreichem Login
|
|
||||||
toast.add({title:"Ändern erfolgreich"})
|
toast.add({title:"Ändern erfolgreich"})
|
||||||
await auth.logout()
|
await auth.logout()
|
||||||
return navigateTo("/login")
|
return navigateTo("/login")
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.add({title:"Es gab ein Problem beim ändern",color:"error"})
|
toast.add({title:"Es gab ein Problem beim ändern",color:"error"})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -37,26 +45,39 @@ const doChange = async (data:any) => {
|
|||||||
dark="/Logo_Dark.png"
|
dark="/Logo_Dark.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UAuthForm
|
<div class="mt-6 space-y-5">
|
||||||
title="Passwort zurücksetzen"
|
<div class="space-y-1">
|
||||||
description="Geben Sie Ihre E-Mail ein um ein neues Passwort per E-Mail zu erhalten."
|
<h1 class="text-xl font-semibold">Passwort ändern</h1>
|
||||||
align="bottom"
|
<p class="text-sm text-muted">
|
||||||
:fields="[{
|
Geben Sie Ihr aktuelles und Ihr neues Passwort ein.
|
||||||
name: 'oldPassword',
|
</p>
|
||||||
label: 'Altes Passwort',
|
</div>
|
||||||
type: 'password',
|
|
||||||
placeholder: 'Dein altes Passwort'
|
<UForm :state="state" class="space-y-4" @submit="doChange">
|
||||||
},{
|
<UFormField label="Altes Passwort" name="oldPassword">
|
||||||
name: 'newPassword',
|
<UInput
|
||||||
label: 'Neues Passwort',
|
v-model="state.oldPassword"
|
||||||
type: 'password',
|
type="password"
|
||||||
placeholder: 'Dein neues Passwort'
|
class="w-full"
|
||||||
}]"
|
placeholder="Dein altes Passwort"
|
||||||
:loading="false"
|
autocomplete="current-password"
|
||||||
@submit="doChange"
|
/>
|
||||||
:submit-button="{label: 'Ändern'}"
|
</UFormField>
|
||||||
divider="oder"
|
|
||||||
>
|
<UFormField label="Neues Passwort" name="newPassword">
|
||||||
</UAuthForm>
|
<UInput
|
||||||
|
v-model="state.newPassword"
|
||||||
|
type="password"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Dein neues Passwort"
|
||||||
|
autocomplete="new-password"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UButton type="submit" block class="w-full" :loading="loading">
|
||||||
|
Ändern
|
||||||
|
</UButton>
|
||||||
|
</UForm>
|
||||||
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,28 +1,34 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { FormSubmitEvent } from '#ui/types'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "notLoggedIn"
|
layout: "notLoggedIn"
|
||||||
})
|
})
|
||||||
|
|
||||||
const auth = useAuthStore()
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
email: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const doReset = async (event: FormSubmitEvent<typeof state>) => {
|
||||||
const doReset = async (data:any) => {
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await useNuxtApp().$api("/auth/password/reset", {
|
await useNuxtApp().$api("/auth/password/reset", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
email: data.email
|
email: event.data.email
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Weiterleiten nach erfolgreichem Login
|
|
||||||
toast.add({title:"Zurücksetzen erfolgreich"})
|
toast.add({title:"Zurücksetzen erfolgreich"})
|
||||||
return navigateTo("/login")
|
return navigateTo("/login")
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.add({title:"Problem beim zurücksetzen",color:"error"})
|
toast.add({title:"Problem beim zurücksetzen",color:"error"})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -35,21 +41,29 @@ const doReset = async (data:any) => {
|
|||||||
dark="/Logo_Dark.png"
|
dark="/Logo_Dark.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UAuthForm
|
<div class="mt-6 space-y-5">
|
||||||
title="Passwort zurücksetzen"
|
<div class="space-y-1">
|
||||||
description="Geben Sie Ihre E-Mail ein um ein neues Passwort per E-Mail zu erhalten."
|
<h1 class="text-xl font-semibold">Passwort zurücksetzen</h1>
|
||||||
align="bottom"
|
<p class="text-sm text-muted">
|
||||||
:fields="[{
|
Geben Sie Ihre E-Mail ein um ein neues Passwort per E-Mail zu erhalten.
|
||||||
name: 'email',
|
</p>
|
||||||
type: 'text',
|
</div>
|
||||||
label: 'Email',
|
|
||||||
placeholder: 'Deine E-Mail Adresse'
|
<UForm :state="state" class="space-y-4" @submit="doReset">
|
||||||
}]"
|
<UFormField label="E-Mail" name="email">
|
||||||
:loading="false"
|
<UInput
|
||||||
@submit="doReset"
|
v-model="state.email"
|
||||||
:submit-button="{label: 'Zurücksetzen'}"
|
type="email"
|
||||||
divider="oder"
|
class="w-full"
|
||||||
>
|
placeholder="Deine E-Mail Adresse"
|
||||||
</UAuthForm>
|
autocomplete="email"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UButton type="submit" block class="w-full" :loading="loading">
|
||||||
|
Zurücksetzen
|
||||||
|
</UButton>
|
||||||
|
</UForm>
|
||||||
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</template>
|
</template>
|
||||||
@@ -173,6 +173,8 @@ setupPage()
|
|||||||
<UAlert
|
<UAlert
|
||||||
class="mt-5"
|
class="mt-5"
|
||||||
title="DOKUBOX"
|
title="DOKUBOX"
|
||||||
|
color="neutral"
|
||||||
|
variant="outline"
|
||||||
>
|
>
|
||||||
<template #description>
|
<template #description>
|
||||||
<p>Die Dokubox ist eine E-Mail Inbox um deine Anhänge direkt als Dokumente zu importieren. Leite Deine E-Mails einfach an die folgende E-Mail Adresse weiter, diese ist für dein Unternehmen einzigartig. Die E-Mails werden dann alle 5 min abgerufen.</p>
|
<p>Die Dokubox ist eine E-Mail Inbox um deine Anhänge direkt als Dokumente zu importieren. Leite Deine E-Mails einfach an die folgende E-Mail Adresse weiter, diese ist für dein Unternehmen einzigartig. Die E-Mails werden dann alle 5 min abgerufen.</p>
|
||||||
|
|||||||
@@ -313,6 +313,20 @@ const truncateValue = (value, maxLength) => {
|
|||||||
return `${stringValue.substring(0, maxLength)}...`
|
return `${stringValue.substring(0, maxLength)}...`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDistinctFilterItems = (columnKey) => {
|
||||||
|
return (itemsMeta.value?.distinctValues?.[columnKey] || []).map((value) => ({
|
||||||
|
label: String(value),
|
||||||
|
value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDistinctFilterActive = (columnKey) => {
|
||||||
|
const available = itemsMeta.value?.distinctValues?.[columnKey] || []
|
||||||
|
const selected = columnsToFilter.value[columnKey] || []
|
||||||
|
|
||||||
|
return selected.length > 0 && selected.length !== available.length
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -371,7 +385,7 @@ const truncateValue = (value, maxLength) => {
|
|||||||
v-model="pageLimit"
|
v-model="pageLimit"
|
||||||
value-key="value"
|
value-key="value"
|
||||||
label-key="value"
|
label-key="value"
|
||||||
@change="setupPage"
|
@update:model-value="setupPage"
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
<UPagination
|
<UPagination
|
||||||
@@ -400,7 +414,7 @@ const truncateValue = (value, maxLength) => {
|
|||||||
by="key"
|
by="key"
|
||||||
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'"
|
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'"
|
||||||
:content="{ width: 'min-w-max' }"
|
:content="{ width: 'min-w-max' }"
|
||||||
@change="tempStore.modifyColumns(type,selectedColumns)"
|
@update:model-value="tempStore.modifyColumns(type, selectedColumns)"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
Spalten
|
Spalten
|
||||||
@@ -442,32 +456,26 @@ const truncateValue = (value, maxLength) => {
|
|||||||
:text="!columnsToFilter[column.key]?.length > 0 ? `Keine Einträge für ${column.label} verfügbar` : `${column.label} Spalte nach Einträgen filtern`"
|
:text="!columnsToFilter[column.key]?.length > 0 ? `Keine Einträge für ${column.label} verfügbar` : `${column.label} Spalte nach Einträgen filtern`"
|
||||||
>
|
>
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
:items="(itemsMeta?.distinctValues?.[column.key] || []).map(value => ({ label: value, value }))"
|
class="min-w-0"
|
||||||
|
:items="getDistinctFilterItems(column.key)"
|
||||||
v-model="columnsToFilter[column.key]"
|
v-model="columnsToFilter[column.key]"
|
||||||
multiple
|
multiple
|
||||||
@change="handleFilterChange('change', column.key)"
|
@update:model-value="handleFilterChange('change', column.key)"
|
||||||
:search-input="{ placeholder: 'Suche...' }"
|
:search-input="{ placeholder: 'Suche...' }"
|
||||||
value-key="value"
|
value-key="value"
|
||||||
label-key="label"
|
label-key="label"
|
||||||
:content="{ width: 'min-w-max' }"
|
:content="{ width: 'min-w-max' }"
|
||||||
|
:disabled="getDistinctFilterItems(column.key).length === 0"
|
||||||
>
|
>
|
||||||
|
|
||||||
<template #empty>
|
<template #empty>
|
||||||
Keine Einträge in der Spalte {{column.label}}
|
Keine Einträge in der Spalte {{column.label}}
|
||||||
</template>
|
</template>
|
||||||
<template #default="slotProps">
|
<template #default="slotProps">
|
||||||
<UButton
|
<span class="inline-flex min-w-0 items-center">
|
||||||
:disabled="!columnsToFilter[column.key]?.length > 0"
|
|
||||||
:variant="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length ? 'outline' : 'solid'"
|
|
||||||
:color="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length ? 'primary' : 'white'"
|
|
||||||
>
|
|
||||||
<span class="truncate">{{ column.label }}</span>
|
<span class="truncate">{{ column.label }}</span>
|
||||||
|
</span>
|
||||||
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :class="[slotProps?.open && 'transform rotate-90']" />
|
|
||||||
</UButton>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
<UButton
|
<UButton
|
||||||
@@ -700,12 +708,15 @@ const truncateValue = (value, maxLength) => {
|
|||||||
|
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-model="columnsToFilter[column.key]"
|
v-model="columnsToFilter[column.key]"
|
||||||
:options="itemsMeta?.distinctValues?.[column.key]"
|
:items="getDistinctFilterItems(column.key)"
|
||||||
multiple
|
multiple
|
||||||
searchable
|
value-key="value"
|
||||||
:search-attributes="[column.key]"
|
label-key="label"
|
||||||
|
:search-input="{ placeholder: `${column.label} filtern...` }"
|
||||||
|
:filter-fields="['label']"
|
||||||
placeholder="Auswählen…"
|
placeholder="Auswählen…"
|
||||||
:ui-menu="{ width: '100%' }"
|
:content="{ width: 'w-full' }"
|
||||||
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user