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({
|
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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user