KI-AGENT: Kassenbuch ins Bankportal integrieren
This commit is contained in:
@@ -285,6 +285,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
const insertedStatements = await tx.insert(bankstatements).values({
|
||||
account: cashbookId,
|
||||
date: dayjs(body.date).format("YYYY-MM-DD"),
|
||||
valueDate: dayjs(body.date).format("YYYY-MM-DD"),
|
||||
amount: signedAmount,
|
||||
tenant: req.user.tenant_id,
|
||||
text: description,
|
||||
@@ -687,7 +688,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
|
||||
if (matchesBankAccountId && matchesIban) {
|
||||
score = 100
|
||||
reason = "IBAN und hinterlegte Bankverbindung stimmen ueberein"
|
||||
reason = "IBAN und hinterlegte Bankverbindung stimmen überein"
|
||||
} else if (matchesBankAccountId) {
|
||||
score = 95
|
||||
reason = "Hinterlegte Bankverbindung passt zur IBAN"
|
||||
@@ -699,7 +700,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
reason = "Name passt exakt zur Buchung"
|
||||
} else if (partialNameMatch) {
|
||||
score = 45
|
||||
reason = "Name aehnelt der Buchung"
|
||||
reason = "Name ähnelt der Buchung"
|
||||
}
|
||||
|
||||
if (!score) continue
|
||||
@@ -743,7 +744,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
|
||||
if (matchesBankAccountId && matchesIban) {
|
||||
score = 100
|
||||
reason = "IBAN und hinterlegte Bankverbindung stimmen ueberein"
|
||||
reason = "IBAN und hinterlegte Bankverbindung stimmen überein"
|
||||
} else if (matchesBankAccountId) {
|
||||
score = 95
|
||||
reason = "Hinterlegte Bankverbindung passt zur IBAN"
|
||||
@@ -755,7 +756,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
reason = "Name passt exakt zur Buchung"
|
||||
} else if (partialNameMatch) {
|
||||
score = 45
|
||||
reason = "Name aehnelt der Buchung"
|
||||
reason = "Name ähnelt der Buchung"
|
||||
}
|
||||
|
||||
if (!score) continue
|
||||
|
||||
@@ -14,8 +14,10 @@ const route = useRoute()
|
||||
|
||||
const bankstatements = ref([])
|
||||
const bankaccounts = ref([])
|
||||
const accounts = ref([])
|
||||
const customers = ref([])
|
||||
const vendors = ref([])
|
||||
const ownaccounts = ref([])
|
||||
const entitybankaccounts = ref([])
|
||||
const createddocuments = ref([])
|
||||
const incominginvoices = ref([])
|
||||
@@ -23,9 +25,15 @@ const openDocuments = ref([])
|
||||
const openIncomingInvoices = ref([])
|
||||
const filterAccount = ref([])
|
||||
const isSyncing = ref(false)
|
||||
const cashbookBookingModalOpen = ref(false)
|
||||
const savingCashbookBooking = ref(false)
|
||||
const loadingDocs = ref(true) // Startet im Ladezustand
|
||||
const suggestionsModalOpen = ref(false)
|
||||
const selectedSuggestionRowId = ref(null)
|
||||
const counterSearch = ref("")
|
||||
const expandedGroups = ref([])
|
||||
|
||||
const CASHBOOK_BANK_ID = "fedeo-cashbook"
|
||||
|
||||
// Zeitraum-Optionen
|
||||
const periodOptions = [
|
||||
@@ -52,6 +60,23 @@ const dateRange = ref({
|
||||
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) => {
|
||||
if (!value) return undefined
|
||||
|
||||
@@ -72,20 +97,27 @@ const setDateRangeFieldToToday = (field) => {
|
||||
const setupPage = async () => {
|
||||
loadingDocs.value = true
|
||||
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("bankaccounts").select(),
|
||||
useEntities("accounts").selectSpecial("*", "number", true),
|
||||
useEntities("customers").select(),
|
||||
useEntities("vendors").select(),
|
||||
useEntities("ownaccounts").select(),
|
||||
useEntities("entitybankaccounts").select(),
|
||||
useEntities("createddocuments").select("*, statementallocations(*), customer(id,name)"),
|
||||
useEntities("incominginvoices").select("*, statementallocations(*), vendor(*)")
|
||||
])
|
||||
|
||||
bankstatements.value = statements
|
||||
bankaccounts.value = accounts
|
||||
bankaccounts.value = (bankAccountItems || []).map((account) => ({
|
||||
...account,
|
||||
displayLabel: getBankAccountLabel(account)
|
||||
}))
|
||||
accounts.value = accountItems
|
||||
customers.value = customerItems
|
||||
vendors.value = vendorItems
|
||||
ownaccounts.value = ownAccountItems
|
||||
entitybankaccounts.value = entityBankItems
|
||||
createddocuments.value = documentItems
|
||||
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) => {
|
||||
if (index === 0) return true;
|
||||
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) => {
|
||||
@@ -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 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))]
|
||||
@@ -223,7 +398,7 @@ const getAmountMatchLabel = (openSum, remaining) => {
|
||||
const difference = Math.abs(Math.abs(Number(openSum)) - Math.abs(Number(remaining)))
|
||||
if (difference < 0.01) return "Betrag exakt"
|
||||
if (difference <= 1) return "Betrag fast passend"
|
||||
if (difference <= 5) return "Betrag aehnlich"
|
||||
if (difference <= 5) return "Betrag ähnlich"
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -276,7 +451,7 @@ const getTopEntitySuggestion = (statement) => {
|
||||
reason = "Name passt"
|
||||
} else if (partialNameMatch) {
|
||||
score = 45
|
||||
reason = "Name aehnlich"
|
||||
reason = "Name ähnlich"
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -401,10 +576,10 @@ const filteredRows = computed(() => {
|
||||
|
||||
// Filterung nach Datum
|
||||
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) {
|
||||
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
|
||||
@@ -419,13 +594,13 @@ const filteredRows = computed(() => {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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(".", ",")} €`
|
||||
@@ -492,13 +667,22 @@ onMounted(() => {
|
||||
<template>
|
||||
<UDashboardNavbar title="Bankbuchungen" :badge="filteredRows.length">
|
||||
<template #right>
|
||||
<UButton
|
||||
v-if="selectedCashbookAccount"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
icon="i-heroicons-plus"
|
||||
@click="cashbookBookingModalOpen = true"
|
||||
>
|
||||
Eintrag hinzufügen
|
||||
</UButton>
|
||||
<UButton
|
||||
color="primary"
|
||||
variant="soft"
|
||||
icon="i-heroicons-sparkles"
|
||||
@click="suggestionsModalOpen = true"
|
||||
>
|
||||
Vorschlaege
|
||||
Vorschläge
|
||||
<UBadge color="primary" variant="solid" size="xs" class="ml-2">{{ suggestionCount }}</UBadge>
|
||||
</UButton>
|
||||
<UButton
|
||||
@@ -525,11 +709,11 @@ onMounted(() => {
|
||||
:items="bankaccounts"
|
||||
v-model="filterAccount"
|
||||
value-key="id"
|
||||
label-key="iban"
|
||||
label-key="displayLabel"
|
||||
multiple
|
||||
by="id"
|
||||
placeholder="Konten"
|
||||
class="w-48"
|
||||
class="w-64"
|
||||
>
|
||||
<template #default>
|
||||
{{ 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">
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-calendar" class="w-4 h-4"/>
|
||||
{{ $dayjs(row.valueDate).format('MMMM YYYY') }}
|
||||
{{ $dayjs(getStatementDate(row)).format('MMMM YYYY') }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -648,9 +832,9 @@ onMounted(() => {
|
||||
@click="router.push(`/banking/statements/edit/${row.id}`)"
|
||||
>
|
||||
<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 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">
|
||||
<span :class="row.amount >= 0 ? 'text-green-600 dark:text-green-400' : 'text-rose-600 dark:text-rose-400'">
|
||||
{{ displayCurrency(row.amount) }}
|
||||
@@ -680,13 +864,148 @@ onMounted(() => {
|
||||
</div>
|
||||
<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' }">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<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>
|
||||
<UBadge color="primary" variant="subtle">{{ suggestionCount }}</UBadge>
|
||||
@@ -724,7 +1043,7 @@ onMounted(() => {
|
||||
<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-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 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>
|
||||
@@ -783,7 +1102,7 @@ onMounted(() => {
|
||||
|
||||
<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"/>
|
||||
<p>Keine Vorschlaege fuer die aktuelle Filterung vorhanden.</p>
|
||||
<p>Keine Vorschläge für die aktuelle Filterung vorhanden.</p>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user