Files
FEDEO/frontend/pages/banking/statements/[mode]/[[id]].vue
florianfederspiel af4a283eb5 feat(accounting): Skonto bei Zahlungen auf Ausgangs- und Eingangsbelege buchen
Skonto wird an der Bankzuordnung gespeichert und gleicht den Beleg zusammen
mit der Zahlung aus. Der Betrag wird anteilig nach Steuersätzen auf die
Skontokonten verteilt (Standard SKR03, je Mandant einstellbar), im
DATEV-Export als eigene Buchung je Steuersatz ausgegeben und in der
USt-Auswertung im Zeitraum der Zahlung berücksichtigt. Schlussrechnungen
verteilen den Skonto auf die Restforderung nach Abzug der Abschläge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 11:31:47 +01:00

1388 lines
61 KiB
Vue

<script setup>
import dayjs from "dayjs";
import { BANK_ALLOCATION_TAX_ITEMS, getBankAllocationAmounts } from "~/utils/bankAllocationTax"
import { sumSettledAllocations } from "~/utils/cashDiscount"
import {
DEPRECIATION_METHOD_ITEMS,
EXPENSE_BOOKING_MODE_ITEMS,
isDepreciationBookingMode,
normalizeExpenseBookingMode
} from "~/composables/useDepreciation"
// import {filter} from "vuedraggable/dist/vuedraggable.common.js"; // Scheint nicht genutzt zu werden, auskommentiert
defineShortcuts({
'backspace': () => {
router.push("/banking")
}
})
const dataStore = useDataStore()
const tempStore = useTempStore()
const route = useRoute()
const router = useRouter()
const mode = ref(route.params.mode || "show")
const itemInfo = ref({statementallocations: []})
const oldItemInfo = ref({})
const openDocuments = ref([])
const allocatedDocuments = ref([])
const openIncomingInvoices = ref([])
const allocatedIncomingInvoices = ref([])
const statementSuggestions = ref({ suggestions: [] })
const customers = ref([])
const vendors = ref([])
const createddocuments = ref([])
const incominginvoices = ref([])
const accounts = ref([])
const ownaccounts = ref([])
const costcentres = ref([])
const loading = ref(true)
const loadingDocuments = ref(true)
const savingNotes = ref(false)
const notesSaveDelay = 800
let notesSaveTimer = null
let notesSaveQueued = false
const rebuildDocumentLists = () => {
const documents = createddocuments.value.filter(i => i.type === "invoices" || i.type === "advanceInvoices")
openDocuments.value = documents.filter(i => useSum().isOpenCreatedDocument(i, createddocuments.value))
openDocuments.value = openDocuments.value.map(i => {
return {
...i,
docTotal: useSum().getCreatedDocumentSum(i, createddocuments.value),
statementTotal: Number(sumSettledAllocations(i.statementallocations)),
openSum: useSum().getCreatedDocumentOpenAmount(i, createddocuments.value).toFixed(2)
}
})
allocatedDocuments.value = documents.filter(i => i.statementallocations.find(x => x.bankstatement === itemInfo.value.id))
allocatedIncomingInvoices.value = incominginvoices.value.filter(i => i.statementallocations.find(x => x.bankstatement === itemInfo.value.id))
openIncomingInvoices.value = incominginvoices.value.filter(i =>
!i.archived && sumSettledAllocations(i.statementallocations).toFixed(2) !== getInvoiceSum(i, false)
)
}
const setup = async () => {
loading.value = true
loadingDocuments.value = true
const supportingDataPromise = Promise.all([
useEntities("createddocuments").select("*, statementallocations(*), customer(id,name)"),
useEntities("incominginvoices").select("*, statementallocations(*), vendor(*)"),
useEntities("ownaccounts").select(),
useEntities("customers").select(),
useEntities("vendors").select()
])
const [statement, accountItems, costCentreItems] = await Promise.all([
route.params.id
? useEntities("bankstatements").selectSingle(route.params.id, "*, statementallocations(*, cd_id(*), ii_id(*))")
: Promise.resolve(itemInfo.value),
useEntities("accounts").selectSpecial("*", "number", true),
useEntities("costcentres").select()
])
itemInfo.value = statement
accounts.value = accountItems
costcentres.value = costCentreItems
if (itemInfo.value) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
manualAllocationSum.value = calculateOpenSum.value
allocationDepreciationStartDate.value = dayjs(itemInfo.value?.date || new Date()).format("YYYY-MM-DD")
loading.value = false
const suggestionsPromise = itemInfo.value?.id
? useFunctions().useBankingStatementSuggestions(itemInfo.value.id)
: Promise.resolve({ suggestions: [] })
try {
const [documentItems, invoiceItems, ownAccountItems, customerItems, vendorItems] = await supportingDataPromise
createddocuments.value = documentItems
incominginvoices.value = invoiceItems.filter(i => i.state === "Gebucht")
ownaccounts.value = ownAccountItems
customers.value = customerItems
vendors.value = vendorItems
rebuildDocumentLists()
} catch (error) {
console.error("Offene Belege konnten nicht geladen werden:", error)
} finally {
loadingDocuments.value = false
}
try {
statementSuggestions.value = await suggestionsPromise
} catch (error) {
console.error("Buchungsvorschläge konnten nicht geladen werden:", error)
statementSuggestions.value = { suggestions: [] }
}
}
const displayCurrency = (value, currency = "€") => {
return `${Number(value).toFixed(2).replace(".", ",")} ${currency}`
}
// Angepasst: Prüft nun auch auf null/undefined, nicht nur auf leeren String
const separateIBAN = (input) => {
if (!input) return ""
const separates = input.toString().match(/.{1,4}/g)
return separates ? separates.join(" ") : input
}
const getInvoiceSum = (invoice, onlyOpenSum) => {
let sum = Number(useSum().getIncomingInvoiceSum(invoice))
if (onlyOpenSum) sum = sum + Number(sumSettledAllocations(invoice.statementallocations))
if (invoice.expense) {
return (sum * -1).toFixed(2)
} else {
return sum.toFixed(2)
}
}
const calculateOpenSum = computed(() => {
let startingAmount = 0
if (itemInfo.value.statementallocations) {
itemInfo.value.statementallocations.forEach(item => {
startingAmount += item.amount
})
}
return (itemInfo.value.amount - startingAmount).toFixed(2)
})
const showAccountSelection = ref(false)
const accountToSave = ref("")
const ownAccountToSave = ref("")
const customerAccountToSave = ref("")
const vendorAccountToSave = ref("")
const selectAccount = (id) => {
accountToSave.value = id
showAccountSelection.value = false
}
const manualAllocationSum = ref(itemInfo.value.amount || 0)
const allocationDescription = ref("")
const allocationCostcentre = ref(null)
const allocationTaxKey = ref("40")
const allocationAmounts = computed(() => getBankAllocationAmounts({
bankstatement: itemInfo.value.id,
account: accountToSave.value,
ownaccount: ownAccountToSave.value,
amount: manualAllocationSum.value,
datevTaxKey: allocationTaxKey.value,
}))
const getCostCentreLabel = (id) => {
const centre = costcentres.value.find(item => item.id === id)
return centre ? `${centre.number} - ${centre.name}` : id
}
const getAllocationTaxLabel = (item) => BANK_ALLOCATION_TAX_ITEMS.find(tax => tax.value === item.datevTaxKey)?.label
|| (item.datevTaxKey ? `Steuerschlüssel ${item.datevTaxKey}` : "")
const allocationBookingMode = ref("expense")
const allocationDepreciationMonths = ref(36)
const allocationDepreciationStartDate = ref(dayjs().format("YYYY-MM-DD"))
const allocationDepreciationMethod = ref("linear")
const allocationDepreciationLabel = ref("")
const allocationDepreciationGroup = ref("")
const allocationResidualValue = ref(0)
const showMoreWithoutRecipe = ref(false)
const showMoreText = ref(false)
const isDepreciationAllocation = computed(() => isDepreciationBookingMode(allocationBookingMode.value))
const buildAllocationPayload = (payload) => {
const base = {
...payload,
...((payload.account || payload.ownaccount) ? {
costcentre: allocationCostcentre.value || null,
datevTaxKey: allocationTaxKey.value || null,
} : {}),
bookingMode: normalizeExpenseBookingMode(allocationBookingMode.value)
}
if (!isDepreciationAllocation.value) {
return {
...base,
depreciationMonths: null,
depreciationStartDate: null,
depreciationLabel: null,
depreciationGroup: null
}
}
return {
...base,
depreciationMonths: Number(allocationDepreciationMonths.value || 0),
depreciationStartDate: allocationDepreciationStartDate.value || itemInfo.value?.date || null,
depreciationMethod: allocationDepreciationMethod.value,
depreciationLabel: allocationDepreciationLabel.value || allocationDescription.value || "Direkte Abschreibung",
depreciationGroup: allocationBookingMode.value === "depreciation_bundle" ? allocationDepreciationGroup.value || null : null,
residualValue: Number(allocationResidualValue.value || 0)
}
}
watch(allocationBookingMode, (value) => {
if (!isDepreciationBookingMode(value)) return
if (!allocationDepreciationStartDate.value) {
allocationDepreciationStartDate.value = dayjs(itemInfo.value?.date || new Date()).format("YYYY-MM-DD")
}
})
const saveAllocation = async (allocation) => {
const res = await useNuxtApp().$api("/api/banking/statements", {
method: "POST",
body: {data: buildAllocationPayload(allocation)}
})
if (res) {
itemInfo.value.statementallocations.push(res)
if (res.createddocument) {
const document = createddocuments.value.find(item => item.id === res.createddocument)
document?.statementallocations.push(res)
}
if (res.incominginvoice) {
const invoice = incominginvoices.value.find(item => item.id === res.incominginvoice)
invoice?.statementallocations.push(res)
}
rebuildDocumentLists()
manualAllocationSum.value = calculateOpenSum.value
accountToSave.value = null
vendorAccountToSave.value = null
customerAccountToSave.value = null
ownAccountToSave.value = null
allocationCostcentre.value = null
allocationTaxKey.value = "40"
allocationBookingMode.value = "expense"
allocationDepreciationMonths.value = 36
allocationDepreciationStartDate.value = dayjs(itemInfo.value?.date || new Date()).format("YYYY-MM-DD")
allocationDepreciationMethod.value = "linear"
allocationDepreciationLabel.value = ""
allocationDepreciationGroup.value = ""
allocationResidualValue.value = 0
// allocationDescription.value = null // Optional: Beschreibung behalten für nächste Buchung?
}
}
const removeAllocation = async (allocationId) => {
await useNuxtApp().$api(`/api/banking/statements/${allocationId}`, {
method: "DELETE"
})
itemInfo.value.statementallocations = itemInfo.value.statementallocations.filter(item => item.id !== allocationId)
createddocuments.value.forEach(document => {
document.statementallocations = document.statementallocations.filter(item => item.id !== allocationId)
})
incominginvoices.value.forEach(invoice => {
invoice.statementallocations = invoice.statementallocations.filter(item => item.id !== allocationId)
})
rebuildDocumentLists()
manualAllocationSum.value = calculateOpenSum.value
}
const saveNotes = async () => {
if (!itemInfo.value?.id || !notesChanged.value) return
if (savingNotes.value) {
notesSaveQueued = true
return
}
const notes = String(itemInfo.value.notes || "").trim() || null
savingNotes.value = true
try {
const updated = await useEntities("bankstatements").update(itemInfo.value.id, {notes}, true)
oldItemInfo.value.notes = updated.notes
if (String(itemInfo.value.notes || "").trim() === String(notes || "")) {
itemInfo.value.notes = updated.notes
}
} finally {
savingNotes.value = false
if (notesSaveQueued || notesChanged.value) {
notesSaveQueued = false
scheduleNotesSave()
}
}
}
const notesChanged = computed(() =>
String(itemInfo.value?.notes || "").trim() !== String(oldItemInfo.value?.notes || "").trim()
)
const scheduleNotesSave = () => {
if (notesSaveTimer) clearTimeout(notesSaveTimer)
notesSaveTimer = setTimeout(() => {
notesSaveTimer = null
void saveNotes()
}, notesSaveDelay)
}
watch(() => itemInfo.value?.notes, () => {
if (!loading.value && notesChanged.value) scheduleNotesSave()
})
onBeforeRouteLeave(async () => {
if (notesSaveTimer) {
clearTimeout(notesSaveTimer)
notesSaveTimer = null
}
if (notesChanged.value) await saveNotes()
})
const searchString = ref(tempStore.searchStrings["bankstatementsedit"] || '')
const clearSearchString = () => {
searchString.value = ''
tempStore.clearSearchString("bankstatementsedit")
}
const filteredDocuments = computed(() => {
return useSearch(searchString.value, openDocuments.value.filter(i => i.state === "Gebucht"))
})
const filteredIncomingInvoices = computed(() => {
return useSearch(searchString.value, openIncomingInvoices.value.filter(i => i.state === "Gebucht"))
})
const dismissedSuggestions = computed(() => tempStore.settings?.banking?.dismissedSuggestions || {})
const getDismissedSuggestionKeys = (statementId) => {
const key = String(statementId || "")
const values = dismissedSuggestions.value?.[key]
return Array.isArray(values) ? values : []
}
const isSuggestionDismissed = (statementId, suggestionKey) => {
return getDismissedSuggestionKeys(statementId).includes(suggestionKey)
}
const dismissSuggestion = (statementId, suggestionKey) => {
const bankingSettings = tempStore.settings?.banking || {}
const dismissed = { ...(bankingSettings.dismissedSuggestions || {}) }
const rowKey = String(statementId || "")
const existing = Array.isArray(dismissed[rowKey]) ? dismissed[rowKey] : []
if (!existing.includes(suggestionKey)) dismissed[rowKey] = [...existing, suggestionKey]
tempStore.modifySettings("banking", {
...bankingSettings,
dismissedSuggestions: dismissed
})
}
const topEntitySuggestion = computed(() => {
const suggestions = Array.isArray(statementSuggestions.value?.suggestions) ? statementSuggestions.value.suggestions : []
return suggestions[0] || null
})
const normalizeSuggestionText = (value) => {
return String(value || "")
.toLowerCase()
.replace(/[^a-z0-9äöüß]+/gi, " ")
.replace(/\s+/g, " ")
.trim()
}
const getSuggestionTokens = (value) => {
return [...new Set(
normalizeSuggestionText(value)
.split(" ")
.filter((token) => token.length >= 4)
)]
}
const getMatchingPurposeParts = (value, candidates = []) => {
const haystack = normalizeSuggestionText(value)
if (!haystack) return []
return [...new Set(
candidates
.flatMap(candidate => getSuggestionTokens(candidate))
.filter(token => haystack.includes(token))
)].slice(0, 3)
}
const getAmountMatchLabel = (openSum, remaining) => {
const difference = Math.abs(Math.abs(Number(openSum)) - Math.abs(Number(remaining)))
if (difference < 0.01) return "Betrag exakt passend"
if (difference <= 1) return `Betrag fast passend (${displayCurrency(difference)} Abweichung)`
if (difference <= 5) return `Betrag aehnlich (${displayCurrency(difference)} Abweichung)`
return null
}
const isWithinAmountTolerance = (candidateSum, remaining) => {
const reference = Math.abs(Number(remaining))
const candidate = Math.abs(Number(candidateSum))
if (!reference || !candidate) return false
const difference = Math.abs(candidate - reference)
return difference / reference <= 0.1
}
const getDocumentSuggestionMeta = (document, suggestion) => {
let score = 0
const remaining = Math.abs(Number(calculateOpenSum.value))
const openSum = Math.abs(Number(document.openSum))
const purposeMatches = getMatchingPurposeParts(itemInfo.value.text, [
document.documentNumber,
document.title,
document.description
])
const amountLabel = getAmountMatchLabel(openSum, remaining)
const withinTolerance = isWithinAmountTolerance(openSum, remaining)
if (!withinTolerance) {
return {
score: 0,
purposeMatches: [],
amountLabel: null
}
}
if (openSum === remaining) score += 100
else score += Math.max(0, 60 - Math.abs(openSum - remaining))
if (suggestion && document.customer?.id === suggestion.id) score += 80
if (purposeMatches.length > 0) score += 50 + (purposeMatches.length * 10)
return {
score,
purposeMatches,
amountLabel
}
}
const getInvoiceSuggestionMeta = (invoice, suggestion) => {
let score = 0
const remaining = Math.abs(Number(calculateOpenSum.value))
const openSum = Math.abs(Number(getInvoiceSum(invoice, true)))
const purposeMatches = getMatchingPurposeParts(itemInfo.value.text, [
invoice.reference,
invoice.description
])
const amountLabel = getAmountMatchLabel(openSum, remaining)
const withinTolerance = isWithinAmountTolerance(openSum, remaining)
if (!withinTolerance) {
return {
score: 0,
purposeMatches: [],
amountLabel: null
}
}
if (openSum === remaining) score += 100
else score += Math.max(0, 60 - Math.abs(openSum - remaining))
if (suggestion && invoice.vendor?.id === suggestion.id) score += 80
if (purposeMatches.length > 0) score += 50 + (purposeMatches.length * 10)
return {
score,
purposeMatches,
amountLabel
}
}
const suggestedDocuments = computed(() => {
if (topEntitySuggestion.value?.type !== "customer") return []
return openDocuments.value
.filter((document) => document.customer?.id === topEntitySuggestion.value.id)
.map((document) => {
const meta = getDocumentSuggestionMeta(document, topEntitySuggestion.value)
return {
...document,
suggestionScore: meta.score,
suggestionPurposeMatches: meta.purposeMatches,
suggestionAmountLabel: meta.amountLabel,
suggestionKey: `document:${document.id}`
}
})
.filter((document) => !isSuggestionDismissed(itemInfo.value?.id, document.suggestionKey))
.sort((a, b) => b.suggestionScore - a.suggestionScore)
.slice(0, 3)
})
const suggestedIncomingInvoices = computed(() => {
if (topEntitySuggestion.value?.type !== "vendor") return []
return openIncomingInvoices.value
.filter((invoice) => invoice.vendor?.id === topEntitySuggestion.value.id)
.map((invoice) => {
const meta = getInvoiceSuggestionMeta(invoice, topEntitySuggestion.value)
return {
...invoice,
suggestionScore: meta.score,
suggestionPurposeMatches: meta.purposeMatches,
suggestionAmountLabel: meta.amountLabel,
suggestionKey: `invoice:${invoice.id}`
}
})
.filter((invoice) => !isSuggestionDismissed(itemInfo.value?.id, invoice.suggestionKey))
.sort((a, b) => b.suggestionScore - a.suggestionScore)
.slice(0, 3)
})
const fallbackSuggestedDocuments = computed(() => {
if (topEntitySuggestion.value) return []
return openDocuments.value
.map((document) => {
const meta = getDocumentSuggestionMeta(document, null)
return {
...document,
suggestionScore: meta.score,
suggestionPurposeMatches: meta.purposeMatches,
suggestionAmountLabel: meta.amountLabel,
suggestionKey: `document:${document.id}`
}
})
.filter((document) => !isSuggestionDismissed(itemInfo.value?.id, document.suggestionKey))
.filter((document) => document.suggestionScore >= 80 || document.suggestionPurposeMatches?.length > 0)
.sort((a, b) => b.suggestionScore - a.suggestionScore)
.slice(0, 3)
})
const fallbackSuggestedIncomingInvoices = computed(() => {
if (topEntitySuggestion.value) return []
return openIncomingInvoices.value
.map((invoice) => {
const meta = getInvoiceSuggestionMeta(invoice, null)
return {
...invoice,
suggestionScore: meta.score,
suggestionPurposeMatches: meta.purposeMatches,
suggestionAmountLabel: meta.amountLabel,
suggestionKey: `invoice:${invoice.id}`
}
})
.filter((invoice) => !isSuggestionDismissed(itemInfo.value?.id, invoice.suggestionKey))
.filter((invoice) => invoice.suggestionScore >= 80 || invoice.suggestionPurposeMatches?.length > 0)
.sort((a, b) => b.suggestionScore - a.suggestionScore)
.slice(0, 3)
})
const archiveStatement = async () => {
let temp = {...itemInfo.value}
delete temp.statementallocations
await useEntities("bankstatements").archive(temp.id)
}
// Helpers for the allocation list display
const getAllocationLabel = (item) => {
if (item.account) return `${accounts.value.find(i => i.id === item.account)?.number} - ${accounts.value.find(i => i.id === item.account)?.label}`
if (item.ownaccount) return `${ownaccounts.value.find(i => i.id === item.ownaccount)?.number} - ${ownaccounts.value.find(i => i.id === item.ownaccount)?.name}`
if (item.customer) return `${customers.value.find(i => i.id === item.customer)?.customerNumber} - ${customers.value.find(i => i.id === item.customer)?.name}`
if (item.vendor) return `${vendors.value.find(i => i.id === item.vendor)?.vendorNumber} - ${vendors.value.find(i => i.id === item.vendor)?.name}`
if (item.incominginvoice) {
const inv = incominginvoices.value.find(i => i.id === item.incominginvoice)
return `Eingangsrechnung: ${inv?.reference} (${inv?.vendor?.name})`
}
if (item.createddocument) {
const doc = createddocuments.value.find(i => i.id === item.createddocument)
return `${doc?.documentNumber} (${doc?.customer?.name})`
}
return "Unbekannte Zuordnung"
}
const getAllocationIcon = (item) => {
if (item.account || item.ownaccount) return 'i-heroicons-book-open'
if (item.customer || item.createddocument) return 'i-heroicons-arrow-up-right' // Einnahme / Kunde
if (item.vendor || item.incominginvoice) return 'i-heroicons-arrow-down-left' // Ausgabe / Lieferant
return 'i-heroicons-question-mark-circle'
}
const getAllocationBookingModeLabel = (item) => {
const mode = normalizeExpenseBookingMode(item?.bookingMode)
if (mode === "depreciation_bundle") return "Sammelabschreibung"
if (mode === "depreciation_single") return "Einzelabschreibung"
return "Sofortaufwand"
}
// Skonto: Die Zahlung gleicht zusammen mit dem Skonto den offenen Betrag des Belegs aus.
const cashDiscountModalOpen = ref(false)
const cashDiscountTarget = ref(null)
const cashDiscountInput = ref(0)
const savingCashDiscount = ref(false)
const cashDiscountError = ref("")
const getCashDiscountCandidate = (kind, entity) => {
const open = Number(kind === "createddocument" ? entity.openSum : getInvoiceSum(entity, true))
const payment = Number(manualAllocationSum.value)
if (!open || !payment || Math.sign(open) !== Math.sign(payment)) return null
if (Math.abs(payment) >= Math.abs(open) - 0.004) return null
return { open, payment, discount: Number((open - payment).toFixed(2)) }
}
const canBookCashDiscount = (kind, entity) => Boolean(getCashDiscountCandidate(kind, entity))
const openCashDiscountModal = (kind, entity) => {
const candidate = getCashDiscountCandidate(kind, entity)
if (!candidate) return
cashDiscountTarget.value = {
kind,
id: entity.id,
label: kind === "createddocument"
? `${entity.documentNumber} (${entity.customer?.name || "Ohne Kunde"})`
: `${entity.reference || "Eingangsbeleg"} (${entity.vendor?.name || "Ohne Lieferant"})`,
...candidate
}
cashDiscountInput.value = Math.abs(candidate.discount)
cashDiscountError.value = ""
cashDiscountModalOpen.value = true
}
const cashDiscountPercent = computed(() => {
const target = cashDiscountTarget.value
if (!target || !target.open) return 0
return Math.abs(Number(cashDiscountInput.value || 0)) / Math.abs(target.open) * 100
})
const cashDiscountRemaining = computed(() => {
const target = cashDiscountTarget.value
if (!target) return 0
return Number((Math.abs(target.open) - Math.abs(target.payment) - Math.abs(Number(cashDiscountInput.value || 0))).toFixed(2))
})
const confirmCashDiscount = async () => {
const target = cashDiscountTarget.value
if (!target || savingCashDiscount.value) return
savingCashDiscount.value = true
try {
await saveAllocation({
[target.kind]: target.id,
bankstatement: itemInfo.value.id,
amount: target.payment,
cashDiscountAmount: Math.abs(Number(cashDiscountInput.value || 0)),
description: allocationDescription.value || "Zahlung mit Skonto"
})
cashDiscountModalOpen.value = false
cashDiscountTarget.value = null
} catch (err) {
cashDiscountError.value = err?.data?.error || "Zahlung und Skonto konnten nicht gebucht werden."
} finally {
savingCashDiscount.value = false
}
}
const getCashDiscountSplitLabel = (item) => (item.cashDiscountSplit || [])
.map(split => `${split.account} (${split.rate} %): ${displayCurrency(split.gross)}`)
.join(" · ")
setup()
</script>
<template>
<UDashboardNavbar :ui="{center: 'flex items-stretch gap-1.5 min-w-0'}">
<template #left>
<UButton icon="i-heroicons-chevron-left" variant="outline" @click="router.back()">Zurück</UButton>
<UButton icon="i-heroicons-building-library" variant="ghost" @click="router.push(`/banking`)">Kontobewegungen
</UButton>
</template>
<template #center>
<h1 class="text-xl font-medium">Kontobewegung bearbeiten</h1>
</template>
<template #badge v-if="itemInfo">
<UBadge v-if="calculateOpenSum == 0" color="green" variant="subtle">Zugewiesen</UBadge>
<UBadge v-else color="amber" variant="subtle">Offen</UBadge>
</template>
<template #right>
<ArchiveButton color="error" variant="outline" type="bankstatements" @confirmed="archiveStatement"/>
</template>
</UDashboardNavbar>
<UDashboardPanelContent class="p-0 bg-gray-50 dark:bg-gray-900 overflow-hidden" v-if="!loading">
<div class="flex flex-col lg:flex-row h-[calc(100vh-4rem)]">
<div
class="w-full lg:w-5/12 xl:w-4/12 flex flex-col border-r border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 h-full overflow-hidden">
<div class="p-4 border-b border-gray-100 dark:border-gray-800 shrink-0">
<div class="flex justify-between items-start mb-2">
<div>
<h2 class="text-lg font-bold text-gray-900 dark:text-white line-clamp-1">
{{ itemInfo.amount > 0 ? itemInfo.debName : itemInfo.credName || 'Unbekannter Partner' }}
</h2>
<div class="text-xs text-gray-500 flex gap-2 mt-1">
<span>{{ dayjs(itemInfo.date).format("DD.MM.YYYY") }}</span>
<template v-if="itemInfo.debIban || itemInfo.credIban">
<span>&bull;</span>
<span class="font-mono">{{ separateIBAN(itemInfo.debIban || itemInfo.credIban) }}</span>
</template>
<template v-else>
<span>&bull;</span>
<span class="italic text-gray-400">Sammelbuchung / Keine IBAN</span>
</template>
</div>
</div>
<div class="text-right">
<div class="text-xl font-bold font-mono"
:class="itemInfo.amount > 0 ? 'text-emerald-600' : 'text-rose-600'">
{{ displayCurrency(itemInfo.amount) }}
</div>
</div>
</div>
<div
class="text-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 p-2 rounded-md cursor-pointer hover:bg-gray-100 transition-colors"
@click="showMoreText = !showMoreText">
<p :class="{'line-clamp-2': !showMoreText}">{{ itemInfo.text }}</p>
<div class="flex justify-center mt-1" v-if="!showMoreText">
<UIcon name="i-heroicons-chevron-down" class="w-3 h-3 text-gray-400"/>
</div>
</div>
<div class="mt-3">
<UFormField label="Notiz" size="sm">
<UTextarea
v-model="itemInfo.notes"
class="w-full"
:rows="3"
autoresize
placeholder="Interne Notiz zu dieser Bankbuchung hinzufügen …"
/>
</UFormField>
</div>
<div class="mt-4">
<div class="flex justify-between text-xs mb-1 font-medium">
<span :class="calculateOpenSum != 0 ? 'text-amber-600' : 'text-green-600'">
{{
calculateOpenSum != 0 ? `${displayCurrency(calculateOpenSum)} offen` : 'Vollständig zugewiesen'
}}
</span>
<span class="text-gray-400">{{
((Math.abs(itemInfo.amount) - Math.abs(calculateOpenSum)) / Math.abs(itemInfo.amount) * 100).toFixed(0)
}}%</span>
</div>
<UProgress
:value="Math.abs(itemInfo.amount) - Math.abs(calculateOpenSum)"
:max="Math.abs(itemInfo.amount)"
:color="calculateOpenSum != 0 ? 'amber' : 'green'"
size="sm"
/>
</div>
</div>
<div class="flex-1 overflow-y-auto min-h-0 bg-gray-50/50 dark:bg-gray-900">
<div v-if="itemInfo.statementallocations.length === 0" class="p-8 text-center text-gray-400">
<UIcon name="i-heroicons-banknotes" class="w-12 h-12 mb-2 opacity-20"/>
<p class="text-sm">Noch keine Buchungen zugeordnet.</p>
</div>
<ul class="divide-y divide-gray-100 dark:divide-gray-800" v-else>
<li
v-for="item in itemInfo.statementallocations"
:key="item.id"
class="bg-white dark:bg-gray-900 p-3 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors group"
>
<div class="flex justify-between items-start">
<div class="flex gap-3">
<div class="mt-1">
<UAvatar :icon="getAllocationIcon(item)" size="xs" :ui="{ rounded: 'rounded-md' }"
class="bg-gray-100 dark:bg-gray-800 text-gray-500"/>
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-white">
{{ getAllocationLabel(item) }}
</div>
<div class="text-xs text-gray-500 mt-0.5" v-if="item.description">{{ item.description }}</div>
<div v-if="item.costcentre" class="text-xs text-gray-500 mt-1">Kostenstelle: {{ getCostCentreLabel(item.costcentre) }}</div>
<div v-if="item.datevTaxKey" class="text-xs text-gray-500 mt-1">
{{ getAllocationTaxLabel(item) }} · Netto {{ displayCurrency(getBankAllocationAmounts(item).amountNet) }}
· Steuer {{ displayCurrency(getBankAllocationAmounts(item).amountTax) }}
</div>
<div v-if="Number(item.cashDiscountAmount)" class="text-xs text-amber-700 dark:text-amber-400 mt-1">
Skonto {{ displayCurrency(item.cashDiscountAmount) }}<span v-if="item.cashDiscountSplit?.length"> · {{ getCashDiscountSplitLabel(item) }}</span>
</div>
<div class="mt-1 flex flex-wrap gap-1" v-if="item.bookingMode && item.bookingMode !== 'expense'">
<UBadge size="xs" color="warning" variant="soft">{{ getAllocationBookingModeLabel(item) }}</UBadge>
<UBadge size="xs" color="neutral" variant="soft" v-if="item.depreciationGroup">{{ item.depreciationGroup }}</UBadge>
<UBadge size="xs" color="neutral" variant="soft" v-if="item.depreciationMonths">{{ item.depreciationMonths }} Monate</UBadge>
<UBadge size="xs" color="neutral" variant="soft" v-if="item.depreciationMethod">{{ item.depreciationMethod === 'degressive' ? 'Degressiv' : 'Linear' }}</UBadge>
</div>
<div class="flex gap-2 mt-1" v-if="item.createddocument || item.incominginvoice">
<UButton
v-if="item.createddocument"
size="2xs"
variant="link"
:padded="false"
icon="i-heroicons-eye"
@click="navigateTo(`/createDocument/show/${item.createddocument}`)"
>Beleg anzeigen
</UButton>
<UButton
v-if="item.incominginvoice"
size="2xs"
variant="link"
:padded="false"
icon="i-heroicons-eye"
@click="navigateTo(`/incominginvoices/show/${item.incominginvoice}`)"
>Beleg anzeigen
</UButton>
</div>
</div>
</div>
<div class="text-right">
<div class="font-mono text-sm font-semibold">{{ displayCurrency(item.amount) }}</div>
<div v-if="Number(item.cashDiscountAmount)" class="font-mono text-xs text-amber-600">
+ {{ displayCurrency(item.cashDiscountAmount) }} Skonto
</div>
<UButton
icon="i-heroicons-trash"
color="error"
variant="ghost"
size="xs"
class="opacity-0 group-hover:opacity-100 transition-opacity"
@click="removeAllocation(item.id)"
/>
</div>
</div>
</li>
</ul>
</div>
</div>
<div class="w-full lg:w-7/12 xl:w-8/12 flex flex-col h-full overflow-hidden bg-gray-50 dark:bg-gray-950">
<div
class="p-4 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 shadow-sm shrink-0 z-10">
<div class="grid grid-cols-12 gap-4 items-end">
<div class="col-span-12 md:col-span-3">
<UFormField label="Bruttobetrag" size="sm">
<UInput v-model="manualAllocationSum" type="number" step="0.01">
<template #trailing><span class="text-gray-500 text-xs">EUR</span></template>
</UInput>
</UFormField>
</div>
<div class="col-span-12 md:col-span-5">
<UFormField label="Konto / Manuelle Buchung" size="sm">
<div class="flex gap-1">
<USelectMenu
class="w-full"
:items="accounts"
value-key="id"
label-key="label"
v-model="accountToSave"
:search-input="{ placeholder: 'Konto suchen...' }"
:filter-fields="['number','label']"
placeholder="Konto suchen..."
>
<template #default>
<span v-if="accountToSave"
class="truncate">{{ accounts.find(i => i.id === accountToSave).number }} - {{ accounts.find(i => i.id === accountToSave).label }}</span>
<span v-else>Direkt verbuchen...</span>
</template>
<template #item-label="{ item: option }">
<span class="font-mono text-xs text-gray-500 mr-2">{{ option.number }}</span> {{ option.label }}
</template>
</USelectMenu>
<UButton
icon="i-heroicons-check"
label="Buchen"
:disabled="!accountToSave || !allocationTaxKey"
@click="saveAllocation({bankstatement: itemInfo.id, amount: manualAllocationSum, account: accountToSave, description: allocationDescription })"
/>
</div>
</UFormField>
</div>
<div class="col-span-12 md:col-span-4 flex justify-end gap-2 pb-0.5">
<UButton variant="soft" color="gray" icon="i-heroicons-adjustments-horizontal"
@click="showMoreWithoutRecipe = !showMoreWithoutRecipe" label="Erweitert"/>
<UButton variant="soft" color="gray" icon="i-heroicons-pencil-square"
@click="allocationDescription = allocationDescription ? '' : 'Manuelle Buchung'"
:color="allocationDescription ? 'primary' : 'gray'"/>
</div>
</div>
<div class="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
<UFormField label="Umsatzsteuer / Vorsteuer" size="sm">
<USelectMenu v-model="allocationTaxKey" :items="BANK_ALLOCATION_TAX_ITEMS" value-key="value" label-key="label"
placeholder="Steuer auswählen …" class="w-full" />
</UFormField>
<UFormField label="Kostenstelle (optional)" size="sm">
<div class="flex gap-1">
<USelectMenu v-model="allocationCostcentre" :items="costcentres" value-key="id" label-key="name"
:filter-fields="['number', 'name']" placeholder="Keine Kostenstelle" class="w-full">
<template #default>{{ allocationCostcentre ? getCostCentreLabel(allocationCostcentre) : 'Keine Kostenstelle' }}</template>
<template #item-label="{ item }">{{ item.number }} - {{ item.name }}</template>
</USelectMenu>
<UButton v-if="allocationCostcentre" icon="i-heroicons-x-mark" variant="ghost" aria-label="Kostenstelle entfernen"
@click="allocationCostcentre = null" />
</div>
</UFormField>
<p v-if="allocationTaxKey && (accountToSave || ownAccountToSave)" class="md:col-span-2 text-xs text-gray-500">
Netto {{ displayCurrency(allocationAmounts.amountNet) }} · Steuer {{ displayCurrency(allocationAmounts.amountTax) }}
· Brutto {{ displayCurrency(allocationAmounts.amountGross) }}
</p>
</div>
<div v-if="showMoreWithoutRecipe"
class="mt-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<UFormField label="Aufwandsart" size="sm">
<USelectMenu
v-model="allocationBookingMode"
:items="EXPENSE_BOOKING_MODE_ITEMS"
value-key="value"
label-key="label"
/>
</UFormField>
<UFormField label="Abschreibungsdauer (Monate)" size="sm" v-if="isDepreciationAllocation">
<UInput v-model="allocationDepreciationMonths" type="number" min="1" step="1" />
</UFormField>
<UFormField label="Methode" size="sm" v-if="isDepreciationAllocation">
<USelectMenu
v-model="allocationDepreciationMethod"
:items="DEPRECIATION_METHOD_ITEMS"
value-key="value"
label-key="label"
/>
</UFormField>
</div>
<div class="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3" v-if="isDepreciationAllocation">
<UFormField label="Start Abschreibung" size="sm">
<UInput v-model="allocationDepreciationStartDate" type="date" />
</UFormField>
<UFormField label="Restwert" size="sm">
<UInput v-model="allocationResidualValue" type="number" min="0" step="0.01" />
</UFormField>
</div>
<div class="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3" v-if="isDepreciationAllocation">
<UFormField v-if="allocationBookingMode === 'depreciation_bundle'" label="Sammelposten" size="sm">
<UInput v-model="allocationDepreciationGroup" placeholder="z. B. Betriebsausstattung 2026" />
</UFormField>
<UFormField v-else label="Bezeichnung Abschreibung" size="sm">
<UInput v-model="allocationDepreciationLabel" placeholder="z. B. Werkzeugkoffer" />
</UFormField>
<UAlert
color="warning"
variant="soft"
icon="i-heroicons-calendar-days"
title="BWA bucht nur die Abschreibungsrate"
description="Die Auszahlung erscheint dann nicht sofort als Aufwand, sondern periodisiert über die gewählte Laufzeit."
/>
</div>
<div class="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
<USelectMenu :items="ownaccounts" value-key="id" label-key="name" v-model="ownAccountToSave"
:search-input="{ placeholder: 'Eigenes Konto' }" :filter-fields="['number','name']" placeholder="Eigenes Konto">
<template #default>
{{ ownAccountToSave ? ownaccounts.find(i => i.id === ownAccountToSave).name : 'Eigenes Konto' }}
</template>
</USelectMenu>
<USelectMenu :items="customers" value-key="id" label-key="name"
v-model="customerAccountToSave" :search-input="{ placeholder: 'Kunde (Guthaben)' }" :filter-fields="['name','customerNumber']" placeholder="Kunde (Guthaben)">
<template #default>
{{ customerAccountToSave ? customers.find(i => i.id === customerAccountToSave).name : 'Kunde' }}
</template>
</USelectMenu>
<USelectMenu :items="vendors" value-key="id" label-key="name" v-model="vendorAccountToSave"
:search-input="{ placeholder: 'Lieferant (Guthaben)' }" :filter-fields="['name','vendorNumber']" placeholder="Lieferant (Guthaben)">
<template #default>
{{ vendorAccountToSave ? vendors.find(i => i.id === vendorAccountToSave).name : 'Lieferant' }}
</template>
</USelectMenu>
<div class="md:col-span-3 flex justify-end">
<UButton size="xs" label="Zuweisen"
:disabled="!!ownAccountToSave && !allocationTaxKey"
@click="saveAllocation({bankstatement: itemInfo.id, amount: manualAllocationSum, ownaccount: ownAccountToSave ? ownAccountToSave : null, customer: customerAccountToSave ? customerAccountToSave : null, vendor: vendorAccountToSave ? vendorAccountToSave : null, description: allocationDescription })"/>
</div>
</div>
</div>
<div v-if="allocationDescription" class="mt-2">
<UInput v-model="allocationDescription" placeholder="Beschreibung für Buchung..." icon="i-heroicons-pencil"
size="sm"/>
</div>
</div>
<div class="flex-1 flex flex-col min-h-0">
<div class="p-2 border-b border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900">
<UInput
v-model="searchString"
icon="i-heroicons-magnifying-glass"
placeholder="Belege suchen (Nr, Name, Referenz)..."
:ui="{ icon: { trailing: { pointer: '' } } }"
@change="tempStore.modifySearchString('bankstatementsedit',searchString)"
>
<template #trailing>
<UButton v-if="searchString" color="gray" variant="link" icon="i-heroicons-x-mark" :padded="false"
@click="clearSearchString"/>
</template>
</UInput>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-6">
<div v-if="loadingDocuments" class="flex items-center justify-center gap-2 py-10 text-sm text-gray-500">
<UIcon name="i-heroicons-arrow-path" class="h-5 w-5 animate-spin"/>
Offene Belege werden geladen …
</div>
<div v-if="Number(calculateOpenSum) !== 0 && (topEntitySuggestion || fallbackSuggestedDocuments.length > 0 || fallbackSuggestedIncomingInvoices.length > 0)" class="rounded-xl border border-primary-200 bg-primary-50/70 dark:bg-primary-900/10 dark:border-primary-800 p-4">
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div v-if="topEntitySuggestion">
<div class="text-xs font-bold uppercase tracking-wide text-primary-700 dark:text-primary-300">Automatischer Vorschlag</div>
<div class="mt-1 text-sm font-semibold text-gray-900 dark:text-white">
{{ topEntitySuggestion.type === 'customer' ? 'Kunde' : 'Lieferant' }}:
{{ topEntitySuggestion.number }} - {{ topEntitySuggestion.name }}
</div>
<div class="text-xs text-gray-600 dark:text-gray-300 mt-1">
{{ topEntitySuggestion.reason }}
</div>
<div class="text-xs text-gray-500 mt-1" v-if="statementSuggestions.partnerIban || statementSuggestions.partnerName">
{{ statementSuggestions.partnerName || 'Unbekannter Partner' }}
<span v-if="statementSuggestions.partnerIban">| {{ separateIBAN(statementSuggestions.partnerIban) }}</span>
</div>
</div>
</div>
<div v-if="!topEntitySuggestion" class="mb-3">
<div class="text-xs font-bold uppercase tracking-wide text-primary-700 dark:text-primary-300">Automatische Belegvorschläge</div>
<div class="mt-1 text-xs text-gray-600 dark:text-gray-300">
Kein eindeutiger Kunde oder Lieferant erkannt. Vorschläge basieren auf Betrag und Verwendungszweck.
</div>
</div>
<div v-if="suggestedDocuments.length > 0 || suggestedIncomingInvoices.length > 0 || fallbackSuggestedDocuments.length > 0 || fallbackSuggestedIncomingInvoices.length > 0" class="mt-4 grid gap-2">
<div
v-for="document in (topEntitySuggestion ? suggestedDocuments : fallbackSuggestedDocuments)"
:key="`suggested-document-${document.id}`"
class="flex items-center justify-between rounded-lg border border-white/70 dark:border-gray-800 bg-white dark:bg-gray-900 px-3 py-2"
>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ document.documentNumber }}</div>
<div class="text-xs text-gray-500">{{ document.customer?.name }} | Offen {{ displayCurrency(document.openSum) }}</div>
<div class="mt-1 flex flex-wrap gap-1" v-if="document.suggestionAmountLabel || document.suggestionPurposeMatches?.length">
<UBadge v-if="document.suggestionAmountLabel" size="xs" color="emerald" variant="subtle">
{{ document.suggestionAmountLabel }}
</UBadge>
<UBadge v-for="match in document.suggestionPurposeMatches" :key="`doc-match-${document.id}-${match}`" size="xs" color="amber" variant="subtle">
VWZ: {{ match }}
</UBadge>
</div>
</div>
<UButton
size="xs"
color="primary"
@click="saveAllocation({createddocument: document.id, bankstatement: itemInfo.id, amount: Number(Number(document.openSum) < manualAllocationSum ? document.openSum : manualAllocationSum), description: allocationDescription || 'Automatischer Vorschlag'})"
>
Rechnung zuweisen
</UButton>
<UButton
v-if="canBookCashDiscount('createddocument', document)"
size="xs"
color="warning"
variant="soft"
@click="openCashDiscountModal('createddocument', document)"
>
Mit Skonto
</UButton>
<UButton
size="xs"
color="gray"
variant="ghost"
@click="dismissSuggestion(itemInfo.id, document.suggestionKey)"
>
Ablehnen
</UButton>
</div>
<div
v-for="invoice in (topEntitySuggestion ? suggestedIncomingInvoices : fallbackSuggestedIncomingInvoices)"
:key="`suggested-invoice-${invoice.id}`"
class="flex items-center justify-between rounded-lg border border-white/70 dark:border-gray-800 bg-white dark:bg-gray-900 px-3 py-2"
>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ invoice.vendor?.name }}</div>
<div class="text-xs text-gray-500">Ref: {{ invoice.reference || '-' }} | Offen {{ displayCurrency(getInvoiceSum(invoice, true)) }}</div>
<div class="mt-1 flex flex-wrap gap-1" v-if="invoice.suggestionAmountLabel || invoice.suggestionPurposeMatches?.length">
<UBadge v-if="invoice.suggestionAmountLabel" size="xs" color="emerald" variant="subtle">
{{ invoice.suggestionAmountLabel }}
</UBadge>
<UBadge v-for="match in invoice.suggestionPurposeMatches" :key="`invoice-match-${invoice.id}-${match}`" size="xs" color="amber" variant="subtle">
VWZ: {{ match }}
</UBadge>
</div>
</div>
<UButton
size="xs"
color="error"
@click="saveAllocation({incominginvoice: invoice.id, bankstatement: itemInfo.id, amount: Number(Math.abs(getInvoiceSum(invoice,true)) > Math.abs(manualAllocationSum) ? manualAllocationSum : getInvoiceSum(invoice,true)), description: allocationDescription || 'Automatischer Vorschlag'})"
>
Beleg zuweisen
</UButton>
<UButton
v-if="canBookCashDiscount('incominginvoice', invoice)"
size="xs"
color="warning"
variant="soft"
@click="openCashDiscountModal('incominginvoice', invoice)"
>
Mit Skonto
</UButton>
<UButton
size="xs"
color="gray"
variant="ghost"
@click="dismissSuggestion(itemInfo.id, invoice.suggestionKey)"
>
Ablehnen
</UButton>
</div>
</div>
</div>
<div v-if="filteredDocuments.length > 0">
<h3 class="text-xs font-bold text-gray-500 uppercase mb-2 pl-1 flex items-center gap-2">
<UIcon name="i-heroicons-document-arrow-up"/>
Ausgangsrechnungen
</h3>
<div
class="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
<div
v-for="(document, index) in filteredDocuments"
:key="document.id"
class="flex items-center justify-between p-3 border-b border-gray-100 dark:border-gray-800 last:border-0 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div class="flex items-center gap-3 overflow-hidden">
<div class="bg-primary-50 dark:bg-primary-900/20 text-primary-600 rounded p-1.5 shrink-0">
<UIcon name="i-heroicons-document-text" class="w-5 h-5"/>
</div>
<div class="min-w-0">
<div class="font-medium text-sm text-gray-900 dark:text-white truncate">
{{ document.documentNumber }} <span class="text-gray-400 mx-1">|</span>
{{ document.customer ? document.customer.name : "Ohne Kunde" }}
</div>
<div class="text-xs text-gray-500 flex gap-3 mt-0.5">
<span class="flex items-center gap-1">
<UIcon name="i-heroicons-calendar" class="w-3 h-3"/>
{{ dayjs(document.documentDate || document.date).format("DD.MM.YYYY") }}
</span>
<span class="text-primary-600 font-medium"
v-if="Number(document.openSum) < Number(document.total)">
Teilzahlung
</span>
</div>
</div>
</div>
<div class="flex items-center gap-3 pl-2">
<div class="text-right">
<div class="font-mono font-bold text-sm">{{ displayCurrency(document.openSum) }}</div>
<div class="text-xs text-gray-400">Offen</div>
</div>
<UTooltip text="Zuweisen">
<UButton
v-if="!itemInfo.statementallocations.find(i => i.createddocument === document.id)"
icon="i-heroicons-check"
size="sm"
color="primary"
variant="soft"
@click="saveAllocation({createddocument: document.id, bankstatement: itemInfo.id, amount: Number(Number(document.openSum) < manualAllocationSum ? document.openSum : manualAllocationSum), description: allocationDescription})"
/>
</UTooltip>
<UTooltip text="Mit Skonto ausgleichen">
<UButton
v-if="!itemInfo.statementallocations.find(i => i.createddocument === document.id) && canBookCashDiscount('createddocument', document)"
icon="i-heroicons-receipt-percent"
size="sm"
color="warning"
variant="soft"
@click="openCashDiscountModal('createddocument', document)"
/>
</UTooltip>
<UButton
icon="i-heroicons-eye"
color="gray"
variant="ghost"
size="sm"
@click="router.push(`/createDocument/show/${document.id}`)"
/>
</div>
</div>
</div>
</div>
<div v-if="filteredIncomingInvoices.length > 0">
<h3 class="text-xs font-bold text-gray-500 uppercase mb-2 pl-1 flex items-center gap-2">
<UIcon name="i-heroicons-document-arrow-down"/>
Eingangsbelege
</h3>
<div
class="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
<div
v-for="(item, index) in filteredIncomingInvoices"
:key="item.id"
class="flex items-center justify-between p-3 border-b border-gray-100 dark:border-gray-800 last:border-0 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div class="flex items-center gap-3 overflow-hidden">
<div class="bg-rose-50 dark:bg-rose-900/20 text-rose-600 rounded p-1.5 shrink-0">
<UIcon name="i-heroicons-receipt-refund" class="w-5 h-5"/>
</div>
<div class="min-w-0">
<div class="font-medium text-sm text-gray-900 dark:text-white truncate">
{{ item.vendor ? item.vendor.name : 'Ohne Lieferant' }}
</div>
<div class="text-xs text-gray-500 flex gap-3 mt-0.5">
<span class="flex items-center gap-1">
<UIcon name="i-heroicons-calendar" class="w-3 h-3"/>
{{ dayjs(item.date).format("DD.MM.YYYY") }}
</span>
<span class="truncate max-w-[150px]" :title="item.reference">Ref: {{ item.reference }}</span>
</div>
</div>
</div>
<div class="flex items-center gap-3 pl-2">
<div class="text-right">
<div class="font-mono font-bold text-sm text-rose-600">
{{ displayCurrency(getInvoiceSum(item, true)) }}
</div>
<div class="text-xs text-gray-400">Offen</div>
</div>
<UTooltip text="Zuweisen">
<UButton
v-if="!itemInfo.statementallocations.find(i => i.incominginvoice === item.id)"
icon="i-heroicons-check"
size="sm"
color="error"
variant="soft"
@click="saveAllocation({incominginvoice: item.id, bankstatement: itemInfo.id, amount: Number(Math.abs(getInvoiceSum(item,true)) > Math.abs(manualAllocationSum) ? manualAllocationSum : getInvoiceSum(item,true)), description: allocationDescription})"
/>
</UTooltip>
<UTooltip text="Mit Skonto ausgleichen">
<UButton
v-if="!itemInfo.statementallocations.find(i => i.incominginvoice === item.id) && canBookCashDiscount('incominginvoice', item)"
icon="i-heroicons-receipt-percent"
size="sm"
color="warning"
variant="soft"
@click="openCashDiscountModal('incominginvoice', item)"
/>
</UTooltip>
<UButton
icon="i-heroicons-eye"
color="gray"
variant="ghost"
size="sm"
@click="router.push(`/incominginvoices/show/${item.id}`)"
/>
</div>
</div>
</div>
</div>
<div v-if="!loadingDocuments && filteredDocuments.length === 0 && filteredIncomingInvoices.length === 0"
class="text-center py-10 text-gray-400">
<UIcon name="i-heroicons-magnifying-glass" class="w-8 h-8 mx-auto mb-2 opacity-50"/>
<p>Keine passenden offenen Belege gefunden.</p>
</div>
</div>
</div>
</div>
</div>
</UDashboardPanelContent>
<UModal v-model:open="cashDiscountModalOpen" :ui="{ width: 'sm:max-w-lg' }">
<template #content>
<UCard v-if="cashDiscountTarget">
<template #header>
<div class="text-lg font-semibold">Mit Skonto ausgleichen</div>
<div class="text-sm text-gray-500 mt-0.5">{{ cashDiscountTarget.label }}</div>
</template>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-2 text-sm">
<div class="text-gray-500">Offener Betrag</div>
<div class="text-right font-mono">{{ displayCurrency(Math.abs(cashDiscountTarget.open)) }}</div>
<div class="text-gray-500">Zahlung aus diesem Umsatz</div>
<div class="text-right font-mono">{{ displayCurrency(Math.abs(cashDiscountTarget.payment)) }}</div>
</div>
<UFormField label="Skonto (brutto)" :help="`${cashDiscountPercent.toFixed(2).replace('.', ',')} % des offenen Betrags`">
<UInput v-model="cashDiscountInput" type="number" step="0.01" min="0">
<template #trailing><span class="text-gray-500 text-xs">EUR</span></template>
</UInput>
</UFormField>
<UAlert
v-if="cashDiscountPercent > 5"
color="warning"
variant="subtle"
icon="i-heroicons-exclamation-triangle"
title="Ungewöhnlich hoher Skonto"
description="Bitte prüfen, ob es sich wirklich um Skonto handelt und nicht um eine Teilzahlung."
/>
<UAlert
v-if="cashDiscountError"
color="error"
variant="subtle"
icon="i-heroicons-x-circle"
:description="cashDiscountError"
/>
<div v-if="Math.abs(cashDiscountRemaining) >= 0.01" class="text-sm text-amber-600">
Danach bleiben {{ displayCurrency(cashDiscountRemaining) }} offen.
</div>
<p class="text-xs text-gray-500">
Der Skonto wird anteilig nach Steuersätzen auf die Skontokonten verteilt
({{ cashDiscountTarget.kind === 'createddocument' ? 'gewährte' : 'erhaltene' }} Skonti) und korrigiert die
{{ cashDiscountTarget.kind === 'createddocument' ? 'Umsatzsteuer' : 'Vorsteuer' }} im Monat der Zahlung.
</p>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<UButton color="neutral" variant="ghost" @click="cashDiscountModalOpen = false">Abbrechen</UButton>
<UButton
color="primary"
:loading="savingCashDiscount"
:disabled="!(Number(cashDiscountInput) > 0) || cashDiscountRemaining < -0.009"
@click="confirmCashDiscount"
>
Zahlung und Skonto buchen
</UButton>
</div>
</template>
</UCard>
</template>
</UModal>
</template>
<style scoped>
/* Custom scrollbar styling just in case, though usually handled by Tailwind/System */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
.dark ::-webkit-scrollbar-thumb {
background: #475569;
}
</style>