KI-AGENT: Kassenbuch ins Bankportal integrieren

This commit is contained in:
2026-05-28 16:35:50 +02:00
parent 29a9e2b63b
commit b91c9d0fd8
2 changed files with 342 additions and 22 deletions

View File

@@ -285,6 +285,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
const insertedStatements = await tx.insert(bankstatements).values({ const insertedStatements = await tx.insert(bankstatements).values({
account: cashbookId, account: cashbookId,
date: dayjs(body.date).format("YYYY-MM-DD"), date: dayjs(body.date).format("YYYY-MM-DD"),
valueDate: dayjs(body.date).format("YYYY-MM-DD"),
amount: signedAmount, amount: signedAmount,
tenant: req.user.tenant_id, tenant: req.user.tenant_id,
text: description, text: description,
@@ -687,7 +688,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
if (matchesBankAccountId && matchesIban) { if (matchesBankAccountId && matchesIban) {
score = 100 score = 100
reason = "IBAN und hinterlegte Bankverbindung stimmen ueberein" reason = "IBAN und hinterlegte Bankverbindung stimmen überein"
} else if (matchesBankAccountId) { } else if (matchesBankAccountId) {
score = 95 score = 95
reason = "Hinterlegte Bankverbindung passt zur IBAN" reason = "Hinterlegte Bankverbindung passt zur IBAN"
@@ -699,7 +700,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
reason = "Name passt exakt zur Buchung" reason = "Name passt exakt zur Buchung"
} else if (partialNameMatch) { } else if (partialNameMatch) {
score = 45 score = 45
reason = "Name aehnelt der Buchung" reason = "Name ähnelt der Buchung"
} }
if (!score) continue if (!score) continue
@@ -743,7 +744,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
if (matchesBankAccountId && matchesIban) { if (matchesBankAccountId && matchesIban) {
score = 100 score = 100
reason = "IBAN und hinterlegte Bankverbindung stimmen ueberein" reason = "IBAN und hinterlegte Bankverbindung stimmen überein"
} else if (matchesBankAccountId) { } else if (matchesBankAccountId) {
score = 95 score = 95
reason = "Hinterlegte Bankverbindung passt zur IBAN" reason = "Hinterlegte Bankverbindung passt zur IBAN"
@@ -755,7 +756,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
reason = "Name passt exakt zur Buchung" reason = "Name passt exakt zur Buchung"
} else if (partialNameMatch) { } else if (partialNameMatch) {
score = 45 score = 45
reason = "Name aehnelt der Buchung" reason = "Name ähnelt der Buchung"
} }
if (!score) continue if (!score) continue

View File

@@ -14,8 +14,10 @@ const route = useRoute()
const bankstatements = ref([]) const bankstatements = ref([])
const bankaccounts = ref([]) const bankaccounts = ref([])
const accounts = ref([])
const customers = ref([]) const customers = ref([])
const vendors = ref([]) const vendors = ref([])
const ownaccounts = ref([])
const entitybankaccounts = ref([]) const entitybankaccounts = ref([])
const createddocuments = ref([]) const createddocuments = ref([])
const incominginvoices = ref([]) const incominginvoices = ref([])
@@ -23,9 +25,15 @@ const openDocuments = ref([])
const openIncomingInvoices = ref([]) const openIncomingInvoices = ref([])
const filterAccount = ref([]) const filterAccount = ref([])
const isSyncing = ref(false) const isSyncing = ref(false)
const cashbookBookingModalOpen = ref(false)
const savingCashbookBooking = ref(false)
const loadingDocs = ref(true) // Startet im Ladezustand const loadingDocs = ref(true) // Startet im Ladezustand
const suggestionsModalOpen = ref(false) const suggestionsModalOpen = ref(false)
const selectedSuggestionRowId = ref(null) const selectedSuggestionRowId = ref(null)
const counterSearch = ref("")
const expandedGroups = ref([])
const CASHBOOK_BANK_ID = "fedeo-cashbook"
// Zeitraum-Optionen // Zeitraum-Optionen
const periodOptions = [ const periodOptions = [
@@ -52,6 +60,23 @@ const dateRange = ref({
end: $dayjs().endOf('month').format('YYYY-MM-DD') end: $dayjs().endOf('month').format('YYYY-MM-DD')
}) })
const cashbookForm = reactive({
date: $dayjs().format("YYYY-MM-DD"),
direction: "expense",
amount: null,
counter: "",
datevTaxKey: "__none__",
description: ""
})
const DATEV_TAX_KEY_ITEMS = [
{ value: "__none__", label: "Ohne Steuerschlüssel" },
{ value: "9", label: "9 - Vorsteuer 19 %" },
{ value: "8", label: "8 - Vorsteuer 7 %" },
{ value: "19", label: "19 - EU Vorsteuer 19 %" },
{ value: "18", label: "18 - EU Vorsteuer 7 %" }
]
const getCalendarValue = (value) => { const getCalendarValue = (value) => {
if (!value) return undefined if (!value) return undefined
@@ -72,20 +97,27 @@ const setDateRangeFieldToToday = (field) => {
const setupPage = async () => { const setupPage = async () => {
loadingDocs.value = true loadingDocs.value = true
try { try {
const [statements, accounts, customerItems, vendorItems, entityBankItems, documentItems, invoiceItems] = await Promise.all([ const [statements, bankAccountItems, accountItems, customerItems, vendorItems, ownAccountItems, entityBankItems, documentItems, invoiceItems] = await Promise.all([
useEntities("bankstatements").select("*, statementallocations(*)", "valueDate", false), useEntities("bankstatements").select("*, statementallocations(*)", "valueDate", false),
useEntities("bankaccounts").select(), useEntities("bankaccounts").select(),
useEntities("accounts").selectSpecial("*", "number", true),
useEntities("customers").select(), useEntities("customers").select(),
useEntities("vendors").select(), useEntities("vendors").select(),
useEntities("ownaccounts").select(),
useEntities("entitybankaccounts").select(), useEntities("entitybankaccounts").select(),
useEntities("createddocuments").select("*, statementallocations(*), customer(id,name)"), useEntities("createddocuments").select("*, statementallocations(*), customer(id,name)"),
useEntities("incominginvoices").select("*, statementallocations(*), vendor(*)") useEntities("incominginvoices").select("*, statementallocations(*), vendor(*)")
]) ])
bankstatements.value = statements bankstatements.value = statements
bankaccounts.value = accounts bankaccounts.value = (bankAccountItems || []).map((account) => ({
...account,
displayLabel: getBankAccountLabel(account)
}))
accounts.value = accountItems
customers.value = customerItems customers.value = customerItems
vendors.value = vendorItems vendors.value = vendorItems
ownaccounts.value = ownAccountItems
entitybankaccounts.value = entityBankItems entitybankaccounts.value = entityBankItems
createddocuments.value = documentItems createddocuments.value = documentItems
incominginvoices.value = invoiceItems.filter(i => i.state === "Gebucht") incominginvoices.value = invoiceItems.filter(i => i.state === "Gebucht")
@@ -179,7 +211,7 @@ const selectedFilters = ref(tempStore.filters?.["banking"]?.["main"] || ['Nur of
const shouldShowMonthDivider = (row, index) => { const shouldShowMonthDivider = (row, index) => {
if (index === 0) return true; if (index === 0) return true;
const prevRow = filteredRows.value[index - 1]; const prevRow = filteredRows.value[index - 1];
return $dayjs(row.valueDate).format('MMMM YYYY') !== $dayjs(prevRow.valueDate).format('MMMM YYYY'); return $dayjs(getStatementDate(row)).format('MMMM YYYY') !== $dayjs(getStatementDate(prevRow)).format('MMMM YYYY');
} }
const calculateOpenSum = (statement) => { const calculateOpenSum = (statement) => {
@@ -205,6 +237,149 @@ const getInvoiceSum = (invoice, onlyOpenSum) => {
} }
} }
const getStatementDate = (statement) => statement.valueDate || statement.date
const isCashbookAccount = (account) => account?.bankId === CASHBOOK_BANK_ID
const getBankAccountId = (account) => typeof account === "object" ? account?.id : account
const resolveBankAccount = (accountOrId) => {
if (!accountOrId) return null
if (typeof accountOrId === "object") return accountOrId
return bankaccounts.value.find((account) => account.id === accountOrId) || null
}
const getBankAccountLabel = (account) => {
if (!account) return ""
if (isCashbookAccount(account)) return `${account.name || account.ownerName || "Kassenbuch"} - Konto ${account.datevNumber || account.iban}`
return `${account.name || account.ownerName || "Bankkonto"} - ${account.iban}`
}
const selectedCashbookAccount = computed(() => {
if (filterAccount.value.length !== 1) return null
const account = resolveBankAccount(filterAccount.value[0])
return isCashbookAccount(account) ? account : null
})
const currentCashbookBalance = computed(() => {
if (!selectedCashbookAccount.value) return 0
const openingBalance = Number(selectedCashbookAccount.value.balance || 0)
const movementSum = bankstatements.value
.filter((statement) => statement.account === selectedCashbookAccount.value.id)
.reduce((sum, statement) => sum + Number(statement.amount || 0), 0)
return openingBalance + movementSum
})
const getIncomingInvoiceGross = (invoice) => Number((invoice.accounts || []).reduce((sum, account) => {
return sum + Number(account.amountNet || 0) + Number(account.amountTax || 0)
}, 0))
const getIncomingInvoiceOpenAmount = (invoice) => {
const gross = getIncomingInvoiceGross(invoice)
const allocated = Number((invoice.statementallocations || []).reduce((sum, allocation) => sum + Number(allocation.amount || 0), 0))
return Math.abs(gross) - Math.abs(allocated)
}
const buildCashbookEntries = (rows, type, labelBuilder) =>
(rows || []).map((item) => ({
key: `${type}:${item.id}`,
id: item.id,
type,
number: item.number || item.vendorNumber || item.customerNumber || item.reference || "",
name: item.label || item.name || item.vendor?.name || "",
label: labelBuilder(item),
typeLabel:
type === "account"
? "Sachkonten"
: type === "vendor"
? "Kreditoren"
: type === "customer"
? "Debitoren"
: type === "incominginvoice"
? "Eingangsbelege"
: "Zusätzliche Konten"
}))
const cashbookEntryGroups = computed(() => ([
{
key: "account",
label: "Sachkonten",
entries: buildCashbookEntries(accounts.value, "account", (item) => `${item.number} - ${item.label}`)
},
{
key: "vendor",
label: "Kreditoren",
entries: buildCashbookEntries(vendors.value, "vendor", (item) => `${item.vendorNumber || "ohne Nr."} - ${item.name}`)
},
{
key: "customer",
label: "Debitoren",
entries: buildCashbookEntries(customers.value, "customer", (item) => `${item.customerNumber || "ohne Nr."} - ${item.name}`)
},
{
key: "ownaccount",
label: "Zusätzliche Konten",
entries: buildCashbookEntries(ownaccounts.value, "ownaccount", (item) => `${item.number} - ${item.name}`)
},
{
key: "incominginvoice",
label: "Eingangsbelege",
entries: buildCashbookEntries(
incominginvoices.value.filter((invoice) => !invoice.archived && getIncomingInvoiceOpenAmount(invoice) > 0.004),
"incominginvoice",
(item) => `${item.reference || "Ohne Referenz"} - ${item.vendor?.name || "Ohne Lieferant"} - Offen ${displayCurrency(getIncomingInvoiceOpenAmount(item))}`
)
}
]))
const groupedCashbookCounterEntries = computed(() =>
cashbookEntryGroups.value.map((group) => ({
...group,
entries: group.entries.filter((entry) => {
const search = normalizeSuggestionText(counterSearch.value)
if (!search) return true
return [entry.number, entry.name, entry.label, entry.typeLabel]
.some((value) => normalizeSuggestionText(value).includes(search))
})
})).filter((group) => group.entries.length > 0)
)
const selectedCashbookCounter = computed(() => cashbookEntryGroups.value.flatMap((group) => group.entries).find((entry) => entry.key === cashbookForm.counter))
const isCashbookGroupExpanded = (groupKey) => expandedGroups.value.includes(groupKey)
const toggleCashbookGroupExpanded = (groupKey) => {
if (expandedGroups.value.includes(groupKey)) {
expandedGroups.value = expandedGroups.value.filter((item) => item !== groupKey)
return
}
expandedGroups.value = [...expandedGroups.value, groupKey]
}
const visibleCashbookEntries = (group) => {
if (group.entries.length <= 5 || isCashbookGroupExpanded(group.key)) return group.entries
return group.entries.slice(0, 5)
}
const saveCashbookBooking = async () => {
if (!selectedCashbookAccount.value || !cashbookForm.date || !cashbookForm.amount || !cashbookForm.counter) {
toast.add({ title: "Bitte Kasse, Datum, Betrag und Gegenkonto auswählen.", color: "warning" })
return
}
const [counterType, counterId] = String(cashbookForm.counter).split(":")
savingCashbookBooking.value = true
try {
await $api(`/api/banking/cashbooks/${selectedCashbookAccount.value.id}/bookings`, {
method: "POST",
body: {
date: cashbookForm.date,
direction: cashbookForm.direction,
amount: Number(cashbookForm.amount),
counterType,
counterId,
datevTaxKey: cashbookForm.datevTaxKey === "__none__" ? null : cashbookForm.datevTaxKey,
description: cashbookForm.description
}
})
toast.add({ title: "Kassenbuchung erstellt." })
cashbookForm.amount = null
cashbookForm.counter = ""
cashbookForm.datevTaxKey = "__none__"
cashbookForm.description = ""
counterSearch.value = ""
expandedGroups.value = []
cashbookBookingModalOpen.value = false
await setupPage()
} finally {
savingCashbookBooking.value = false
}
}
const normalizeIban = (value) => String(value || "").replace(/\s+/g, "").toUpperCase() const normalizeIban = (value) => String(value || "").replace(/\s+/g, "").toUpperCase()
const normalizeSuggestionText = (value) => String(value || "").toLowerCase().replace(/[^a-z0-9äöüß]+/gi, " ").replace(/\s+/g, " ").trim() const normalizeSuggestionText = (value) => String(value || "").toLowerCase().replace(/[^a-z0-9äöüß]+/gi, " ").replace(/\s+/g, " ").trim()
const getSuggestionTokens = (value) => [...new Set(normalizeSuggestionText(value).split(" ").filter((token) => token.length >= 4))] const getSuggestionTokens = (value) => [...new Set(normalizeSuggestionText(value).split(" ").filter((token) => token.length >= 4))]
@@ -223,7 +398,7 @@ const getAmountMatchLabel = (openSum, remaining) => {
const difference = Math.abs(Math.abs(Number(openSum)) - Math.abs(Number(remaining))) const difference = Math.abs(Math.abs(Number(openSum)) - Math.abs(Number(remaining)))
if (difference < 0.01) return "Betrag exakt" if (difference < 0.01) return "Betrag exakt"
if (difference <= 1) return "Betrag fast passend" if (difference <= 1) return "Betrag fast passend"
if (difference <= 5) return "Betrag aehnlich" if (difference <= 5) return "Betrag ähnlich"
return null return null
} }
@@ -276,7 +451,7 @@ const getTopEntitySuggestion = (statement) => {
reason = "Name passt" reason = "Name passt"
} else if (partialNameMatch) { } else if (partialNameMatch) {
score = 45 score = 45
reason = "Name aehnlich" reason = "Name ähnlich"
} }
return { return {
@@ -401,10 +576,10 @@ const filteredRows = computed(() => {
// Filterung nach Datum // Filterung nach Datum
if (dateRange.value.start) { if (dateRange.value.start) {
temp = temp.filter(i => $dayjs(i.valueDate).isSameOrAfter($dayjs(dateRange.value.start), 'day')) temp = temp.filter(i => $dayjs(getStatementDate(i)).isSameOrAfter($dayjs(dateRange.value.start), 'day'))
} }
if (dateRange.value.end) { if (dateRange.value.end) {
temp = temp.filter(i => $dayjs(i.valueDate).isSameOrBefore($dayjs(dateRange.value.end), 'day')) temp = temp.filter(i => $dayjs(getStatementDate(i)).isSameOrBefore($dayjs(dateRange.value.end), 'day'))
} }
// Status Filter // Status Filter
@@ -419,13 +594,13 @@ const filteredRows = computed(() => {
} }
// Konto Filter & Suche // Konto Filter & Suche
let results = temp.filter(i => filterAccount.value.find(x => x.id === i.account)) let results = temp.filter(i => filterAccount.value.find(x => getBankAccountId(x) === i.account))
if (searchString.value) { if (searchString.value) {
results = useSearch(searchString.value, results) results = useSearch(searchString.value, results)
} }
return results.sort((a, b) => $dayjs(b.valueDate).unix() - $dayjs(a.valueDate).unix()) return results.sort((a, b) => $dayjs(getStatementDate(b)).unix() - $dayjs(getStatementDate(a)).unix())
}) })
const displayCurrency = (value) => `${Number(value).toFixed(2).replace(".", ",")}` const displayCurrency = (value) => `${Number(value).toFixed(2).replace(".", ",")}`
@@ -492,13 +667,22 @@ onMounted(() => {
<template> <template>
<UDashboardNavbar title="Bankbuchungen" :badge="filteredRows.length"> <UDashboardNavbar title="Bankbuchungen" :badge="filteredRows.length">
<template #right> <template #right>
<UButton
v-if="selectedCashbookAccount"
color="primary"
variant="solid"
icon="i-heroicons-plus"
@click="cashbookBookingModalOpen = true"
>
Eintrag hinzufügen
</UButton>
<UButton <UButton
color="primary" color="primary"
variant="soft" variant="soft"
icon="i-heroicons-sparkles" icon="i-heroicons-sparkles"
@click="suggestionsModalOpen = true" @click="suggestionsModalOpen = true"
> >
Vorschlaege Vorschläge
<UBadge color="primary" variant="solid" size="xs" class="ml-2">{{ suggestionCount }}</UBadge> <UBadge color="primary" variant="solid" size="xs" class="ml-2">{{ suggestionCount }}</UBadge>
</UButton> </UButton>
<UButton <UButton
@@ -525,11 +709,11 @@ onMounted(() => {
:items="bankaccounts" :items="bankaccounts"
v-model="filterAccount" v-model="filterAccount"
value-key="id" value-key="id"
label-key="iban" label-key="displayLabel"
multiple multiple
by="id" by="id"
placeholder="Konten" placeholder="Konten"
class="w-48" class="w-64"
> >
<template #default> <template #default>
{{ filterAccount.length > 0 ? `${filterAccount.length} Kont${filterAccount.length > 1 ? 'en' : 'o'}` : 'Konten' }} {{ filterAccount.length > 0 ? `${filterAccount.length} Kont${filterAccount.length > 1 ? 'en' : 'o'}` : 'Konten' }}
@@ -639,7 +823,7 @@ onMounted(() => {
<td colspan="6" class="bg-gray-50 dark:bg-gray-800/50 p-2 pl-4 text-sm font-bold text-primary-600 border-y dark:border-gray-800"> <td colspan="6" class="bg-gray-50 dark:bg-gray-800/50 p-2 pl-4 text-sm font-bold text-primary-600 border-y dark:border-gray-800">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UIcon name="i-heroicons-calendar" class="w-4 h-4"/> <UIcon name="i-heroicons-calendar" class="w-4 h-4"/>
{{ $dayjs(row.valueDate).format('MMMM YYYY') }} {{ $dayjs(getStatementDate(row)).format('MMMM YYYY') }}
</div> </div>
</td> </td>
</tr> </tr>
@@ -648,9 +832,9 @@ onMounted(() => {
@click="router.push(`/banking/statements/edit/${row.id}`)" @click="router.push(`/banking/statements/edit/${row.id}`)"
> >
<td class="p-4 text-[10px] text-gray-400 font-mono truncate max-w-[150px]"> <td class="p-4 text-[10px] text-gray-400 font-mono truncate max-w-[150px]">
{{ row.account ? bankaccounts.find(i => i.id === row.account)?.iban : "" }} {{ row.account ? getBankAccountLabel(bankaccounts.find(i => i.id === row.account)) : "" }}
</td> </td>
<td class="p-4 whitespace-nowrap">{{ $dayjs(row.valueDate).format("DD.MM.YY") }}</td> <td class="p-4 whitespace-nowrap">{{ $dayjs(getStatementDate(row)).format("DD.MM.YY") }}</td>
<td class="p-4 font-semibold"> <td class="p-4 font-semibold">
<span :class="row.amount >= 0 ? 'text-green-600 dark:text-green-400' : 'text-rose-600 dark:text-rose-400'"> <span :class="row.amount >= 0 ? 'text-green-600 dark:text-green-400' : 'text-rose-600 dark:text-rose-400'">
{{ displayCurrency(row.amount) }} {{ displayCurrency(row.amount) }}
@@ -680,13 +864,148 @@ onMounted(() => {
</div> </div>
<PageLeaveGuard :when="isSyncing"/> <PageLeaveGuard :when="isSyncing"/>
<UModal v-model:open="cashbookBookingModalOpen" :ui="{ width: 'sm:max-w-5xl' }">
<template #content>
<UCard>
<template #header>
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<div class="text-lg font-semibold">Kassenbucheintrag hinzufügen</div>
<div class="text-sm text-gray-500">{{ selectedCashbookAccount?.name || "Kassenbuch" }}</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<UBadge color="neutral" variant="subtle">
Konto {{ selectedCashbookAccount?.datevNumber || selectedCashbookAccount?.iban }}
</UBadge>
<UBadge :color="currentCashbookBalance < 0 ? 'error' : 'success'" variant="subtle">
Bestand {{ displayCurrency(currentCashbookBalance) }}
</UBadge>
</div>
</div>
</template>
<div class="space-y-6">
<div class="grid gap-3 sm:grid-cols-3">
<UFormField label="Buchungsdatum">
<UInput v-model="cashbookForm.date" type="date" />
</UFormField>
<UFormField label="Betrag">
<UInput v-model="cashbookForm.amount" type="number" min="0" step="0.01" placeholder="0,00" />
</UFormField>
<UFormField label="DATEV-Steuerschlüssel">
<USelect
v-model="cashbookForm.datevTaxKey"
:items="DATEV_TAX_KEY_ITEMS"
value-key="value"
label-key="label"
class="w-full"
/>
</UFormField>
</div>
<div class="grid gap-6 xl:grid-cols-[220px_minmax(0,1fr)]">
<div class="space-y-3">
<button
type="button"
class="w-full rounded-lg border px-4 py-3 text-left transition"
:class="cashbookForm.direction === 'income'
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-950/30'
: 'border-gray-200 bg-white hover:border-gray-300 dark:border-gray-800 dark:bg-gray-900'"
@click="cashbookForm.direction = 'income'"
>
<div class="font-semibold text-emerald-700 dark:text-emerald-300">Einnahme</div>
<div class="text-sm text-gray-500">Bargeld kommt in die Kasse.</div>
</button>
<button
type="button"
class="w-full rounded-lg border px-4 py-3 text-left transition"
:class="cashbookForm.direction === 'expense'
? 'border-red-500 bg-red-50 dark:bg-red-950/30'
: 'border-gray-200 bg-white hover:border-gray-300 dark:border-gray-800 dark:bg-gray-900'"
@click="cashbookForm.direction = 'expense'"
>
<div class="font-semibold text-red-700 dark:text-red-300">Ausgabe</div>
<div class="text-sm text-gray-500">Bargeld wird aus der Kasse entnommen.</div>
</button>
</div>
<div class="space-y-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 class="font-semibold text-gray-900 dark:text-white">Gegenkonto</h3>
<p class="text-sm text-gray-500">Sachkonto, Kreditor, Debitor, Eingangsbeleg oder zusätzliches Konto.</p>
</div>
<UInput v-model="counterSearch" icon="i-heroicons-magnifying-glass" placeholder="Gegenkonto durchsuchen..." class="max-w-sm" />
</div>
<div class="max-h-[360px] space-y-4 overflow-y-auto pr-1">
<div v-for="group in groupedCashbookCounterEntries" :key="group.key" class="space-y-2">
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500">{{ group.label }}</div>
<div class="grid gap-2 md:grid-cols-2">
<button
v-for="entry in visibleCashbookEntries(group)"
:key="entry.key"
type="button"
class="rounded-lg border px-3 py-2 text-left transition"
:class="cashbookForm.counter === entry.key
? 'border-primary-500 bg-primary-50 dark:bg-primary-950/30'
: 'border-gray-200 bg-white hover:border-gray-300 dark:border-gray-800 dark:bg-gray-900'"
@click="cashbookForm.counter = entry.key"
>
<div class="flex min-w-0 items-center gap-2">
<span class="font-mono text-sm">{{ entry.number }}</span>
<span class="truncate text-sm">{{ entry.name }}</span>
</div>
</button>
</div>
<UButton
v-if="group.entries.length > 5"
size="xs"
color="neutral"
variant="ghost"
:label="isCashbookGroupExpanded(group.key) ? 'Weniger anzeigen' : `${group.entries.length - 5} weitere anzeigen`"
@click="toggleCashbookGroupExpanded(group.key)"
/>
</div>
</div>
<UAlert
v-if="selectedCashbookCounter"
color="primary"
variant="soft"
icon="i-heroicons-arrows-right-left"
:title="cashbookForm.direction === 'income' ? `${selectedCashbookAccount?.datevNumber} an ${selectedCashbookCounter.number}` : `${selectedCashbookCounter.number} an ${selectedCashbookAccount?.datevNumber}`"
:description="`${selectedCashbookCounter.typeLabel.slice(0, -1)} ${selectedCashbookCounter.name} wird als Gegenkonto verwendet.`"
/>
<UFormField label="Beschreibung">
<UTextarea v-model="cashbookForm.description" placeholder="z. B. Bareinkauf Büromaterial" autoresize class="w-full" />
</UFormField>
</div>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<UButton color="neutral" variant="ghost" @click="cashbookBookingModalOpen = false">
Abbrechen
</UButton>
<UButton color="primary" icon="i-heroicons-check" :loading="savingCashbookBooking" @click="saveCashbookBooking">
Eintrag speichern
</UButton>
</div>
</template>
</UCard>
</template>
</UModal>
<UModal v-model:open="suggestionsModalOpen" :ui="{ width: 'sm:max-w-6xl' }"> <UModal v-model:open="suggestionsModalOpen" :ui="{ width: 'sm:max-w-6xl' }">
<template #content> <template #content>
<UCard> <UCard>
<template #header> <template #header>
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<div> <div>
<div class="text-lg font-semibold">Vorschlaege fuer Bankbuchungen</div> <div class="text-lg font-semibold">Vorschläge für Bankbuchungen</div>
<div class="text-sm text-gray-500">Direkte Zuweisung von passenden Rechnungen und Eingangsbelegen</div> <div class="text-sm text-gray-500">Direkte Zuweisung von passenden Rechnungen und Eingangsbelegen</div>
</div> </div>
<UBadge color="primary" variant="subtle">{{ suggestionCount }}</UBadge> <UBadge color="primary" variant="subtle">{{ suggestionCount }}</UBadge>
@@ -724,7 +1043,7 @@ onMounted(() => {
<div> <div>
<div class="text-sm font-semibold">{{ selectedSuggestionRow.row.amount < 0 ? selectedSuggestionRow.row.credName : selectedSuggestionRow.row.debName }}</div> <div class="text-sm font-semibold">{{ selectedSuggestionRow.row.amount < 0 ? selectedSuggestionRow.row.credName : selectedSuggestionRow.row.debName }}</div>
<div class="text-xs text-gray-500 mt-1">{{ selectedSuggestionRow.row.text || 'Ohne Verwendungszweck' }}</div> <div class="text-xs text-gray-500 mt-1">{{ selectedSuggestionRow.row.text || 'Ohne Verwendungszweck' }}</div>
<div class="text-xs text-gray-400 mt-1">{{ $dayjs(selectedSuggestionRow.row.valueDate).format("DD.MM.YYYY") }}</div> <div class="text-xs text-gray-400 mt-1">{{ $dayjs(getStatementDate(selectedSuggestionRow.row)).format("DD.MM.YYYY") }}</div>
</div> </div>
<div class="text-right"> <div class="text-right">
<div class="font-mono font-semibold" :class="selectedSuggestionRow.row.amount >= 0 ? 'text-green-600' : 'text-rose-600'">{{ displayCurrency(selectedSuggestionRow.row.amount) }}</div> <div class="font-mono font-semibold" :class="selectedSuggestionRow.row.amount >= 0 ? 'text-green-600' : 'text-rose-600'">{{ displayCurrency(selectedSuggestionRow.row.amount) }}</div>
@@ -783,7 +1102,7 @@ onMounted(() => {
<div v-else class="py-10 text-center text-gray-400"> <div v-else class="py-10 text-center text-gray-400">
<UIcon name="i-heroicons-sparkles" class="w-10 h-10 mx-auto mb-2 opacity-30"/> <UIcon name="i-heroicons-sparkles" class="w-10 h-10 mx-auto mb-2 opacity-30"/>
<p>Keine Vorschlaege fuer die aktuelle Filterung vorhanden.</p> <p>Keine Vorschläge für die aktuelle Filterung vorhanden.</p>
</div> </div>
</UCard> </UCard>
</template> </template>