Compare commits

..

2 Commits

Author SHA1 Message Date
60327fe928 KI-AGENT: Gegenkonten in der BWA vollständig saldieren
Some checks failed
Build and Push Docker Images / build-backend (push) Successful in 21s
Build and Push Docker Images / build-website (push) Has been cancelled
Build and Push Docker Images / build-central-services-api (push) Has been cancelled
Build and Push Docker Images / build-central-services-admin (push) Has been cancelled
Build and Push Docker Images / build-docs (push) Has been cancelled
Build and Push Docker Images / build-frontend (push) Has been cancelled
2026-09-07 20:38:10 +02:00
8d8b07ce0d KI-AGENT: Empfängerauswahl beim Mailversand erweitern 2026-09-07 20:31:20 +02:00
2 changed files with 240 additions and 39 deletions

View File

@@ -78,7 +78,12 @@ const isRelevantInputInvoice = (invoice: any) => {
const sameId = (left: any, right: any) => String(left ?? "") === String(right ?? "")
const getStatementDate = (allocation: any) => {
return allocation?.bankstatement?.date || allocation?.bankstatement?.valueDate || allocation?.date || allocation?.created_at || null
return allocation?.bankstatement?.date
|| allocation?.bankstatement?.valueDate
|| allocation?.date
|| allocation?.manualBookingDate
|| allocation?.created_at
|| null
}
const matchesSelectedPeriod = (dateValue: any) => {
@@ -154,21 +159,12 @@ const filteredStatementAllocations = computed(() => {
return statementAllocations.value.filter((allocation) => matchesSelectedPeriod(getStatementDate(allocation)))
})
const filteredDirectAccountStatementAllocations = computed(() => {
const filteredAccountStatementAllocations = computed(() => {
return filteredStatementAllocations.value.filter((allocation) => {
if (allocation.account === null || allocation.account === undefined) {
return false
}
return !allocation?.incominginvoice
&& !allocation?.createddocument
&& !allocation?.ii_id
&& !allocation?.cd_id
})
})
const filteredAccountStatementAllocations = computed(() => {
return filteredDirectAccountStatementAllocations.value.filter((allocation) => {
return getStatementAllocationImmediateExpenseAmount(allocation) > 0
})
})
@@ -307,7 +303,7 @@ const accountRows = computed(() => {
}))
})
const directBookings = filteredDirectAccountStatementAllocations.value
const debitBookings = filteredStatementAllocations.value
.filter((allocation) => sameId(allocation.account?.id || allocation.account, account.id))
.map((allocation) => {
const amount = Number(allocation.amount || 0)
@@ -320,7 +316,20 @@ const accountRows = computed(() => {
}
})
const bookings = [...invoiceBookings, ...directBookings]
const creditBookings = filteredStatementAllocations.value
.filter((allocation) => sameId(allocation.contraAccount?.id || allocation.contraAccount, account.id))
.map((allocation) => {
const amount = -Number(allocation.amount || 0)
return {
type: "statementallocation",
amountNet: amount,
amountTax: 0,
amountGross: amount
}
})
const bookings = [...invoiceBookings, ...debitBookings, ...creditBookings]
if (bookings.length === 0) {
return null
@@ -350,23 +359,29 @@ const accountRows = computed(() => {
const ownAccountRows = computed(() => {
return ownAccounts.value
.map((account) => {
const bookings = filteredStatementAllocations.value.filter((allocation) => sameId(allocation.ownaccount?.id || allocation.ownaccount, account.id))
const debitBookings = filteredStatementAllocations.value
.filter((allocation) => sameId(allocation.ownaccount?.id || allocation.ownaccount, account.id))
.map((allocation) => Number(allocation.amount || 0))
const creditBookings = filteredStatementAllocations.value
.filter((allocation) => sameId(allocation.contraOwnaccount?.id || allocation.contraOwnaccount, account.id))
.map((allocation) => -Number(allocation.amount || 0))
const bookings = [...debitBookings, ...creditBookings]
if (bookings.length === 0) {
return null
}
const income = bookings.reduce((sum, booking) => {
const amount = Number(booking.amount || 0)
const income = bookings.reduce((sum, amount) => {
return amount > 0 ? sum + amount : sum
}, 0)
const expenses = bookings.reduce((sum, booking) => {
const amount = Number(booking.amount || 0)
const expenses = bookings.reduce((sum, amount) => {
return amount < 0 ? sum + Math.abs(amount) : sum
}, 0)
const balance = bookings.reduce((sum, booking) => sum + Number(booking.amount || 0), 0)
const balance = bookings.reduce((sum, amount) => sum + amount, 0)
return {
id: account.id,

View File

@@ -1,6 +1,4 @@
<script setup>
const dataStore = useDataStore()
const profileStore = useProfileStore()
const route = useRoute()
const router = useRouter()
const toast = useToast()
@@ -22,6 +20,10 @@ const loadedDocuments = ref([])
const loaded = ref(false)
const noAccountsPresent = ref(false)
const sourceMessage = ref(null)
const recipientOptions = ref([])
const selectedRecipient = ref(null)
const loadingRecipients = ref(false)
const preferredCustomerId = ref(null)
const composeMode = computed(() => {
const mode = String(route.query.mode || "")
return ["reply", "replyAll", "forward"].includes(mode) ? mode : "new"
@@ -46,6 +48,160 @@ const formatAddress = (address) => {
const formatAddressList = (addresses = []) => addresses.map(formatAddress).filter(Boolean).join(", ")
const normalizeEmail = (value = "") => {
const text = String(value).trim()
const bracketMatch = text.match(/<([^<>]+)>/)
return String(bracketMatch?.[1] || text.split(/[;,]/)[0] || "").trim().toLowerCase()
}
const parseInfoData = (value) => {
if (!value) return {}
if (typeof value === "object") return value
try {
return JSON.parse(value)
} catch {
return {}
}
}
const addRecipientOption = (target, option) => {
const email = normalizeEmail(option.email)
if (!email) return
const existing = target.find(item => item.email === email)
if (existing) {
if (option.priority < existing.priority) Object.assign(existing, option, { email })
return
}
target.push({ ...option, email })
}
const syncSelectedRecipient = () => {
const currentValue = String(emailData.value.to || "").trim()
if (!currentValue) {
selectedRecipient.value = null
return
}
const currentEmail = normalizeEmail(currentValue)
const matchingOption = recipientOptions.value.find(option => option.email === currentEmail)
if (matchingOption && (currentValue.match(/@/g) || []).length === 1) {
selectedRecipient.value = matchingOption.value
return
}
const currentOption = {
label: `Aktuelle Empfänger · ${currentValue}`,
value: currentValue,
email: currentEmail,
priority: -2,
}
recipientOptions.value = [
currentOption,
...recipientOptions.value.filter(option => option.value !== currentValue),
]
selectedRecipient.value = currentValue
}
const loadRecipientOptions = async () => {
loadingRecipients.value = true
try {
const [customers, contacts] = await Promise.all([
useEntities("customers").select("*", "name", true),
useEntities("contacts").select("*, customer(id,name)", "fullName", true),
])
const options = []
const customerNames = new Map(customers.map(customer => [Number(customer.id), customer.name]))
customers
.filter(customer => customer.active !== false)
.forEach(customer => {
const infoData = parseInfoData(customer.infoData)
const isPreferred = Number(customer.id) === Number(preferredCustomerId.value)
const priority = isPreferred ? 0 : 20
addRecipientOption(options, {
email: infoData.email,
value: infoData.email,
label: `${isPreferred ? "Passender Kunde" : "Kunde"} · ${customer.name} · ${infoData.email || ""}`,
priority,
customerId: Number(customer.id),
kind: "customer",
})
addRecipientOption(options, {
email: infoData.invoiceEmail,
value: infoData.invoiceEmail,
label: `${isPreferred ? "Passender Kunde" : "Kunde"} · ${customer.name} · Rechnung · ${infoData.invoiceEmail || ""}`,
priority: priority + 1,
customerId: Number(customer.id),
kind: "invoice",
})
})
contacts
.filter(contact => contact.active !== false && contact.email)
.forEach(contact => {
const customerId = Number(contact.customer?.id || contact.customer || 0)
const customerName = contact.customer?.name || customerNames.get(customerId)
const contactName = contact.fullName
|| [contact.firstName, contact.lastName].filter(Boolean).join(" ")
|| contact.email
const isPreferred = customerId && customerId === Number(preferredCustomerId.value)
addRecipientOption(options, {
email: contact.email,
value: formatAddress({ name: contactName, address: contact.email }),
label: [
isPreferred ? "Passender Ansprechpartner" : "Ansprechpartner",
contactName,
customerName,
contact.email,
].filter(Boolean).join(" · "),
priority: isPreferred ? 2 : 30,
customerId,
kind: "contact",
})
})
if (!preferredCustomerId.value) {
const currentEmail = normalizeEmail(emailData.value.to)
preferredCustomerId.value = options.find(option => option.email === currentEmail)?.customerId || null
}
if (preferredCustomerId.value) {
options.forEach(option => {
if (Number(option.customerId) !== Number(preferredCustomerId.value)) return
option.priority = option.kind === "customer" ? 0 : option.kind === "invoice" ? 1 : 2
option.label = option.label
.replace(/^Kunde ·/, "Passender Kunde ·")
.replace(/^Ansprechpartner ·/, "Passender Ansprechpartner ·")
})
}
recipientOptions.value = options.sort((first, second) =>
first.priority - second.priority || first.label.localeCompare(second.label, "de")
)
syncSelectedRecipient()
} catch (err) {
recipientOptions.value = []
syncSelectedRecipient()
toast.add({
title: "Adressbuch konnte nicht geladen werden",
description: err?.data?.error || err?.message,
color: "warning",
})
} finally {
loadingRecipients.value = false
}
}
const selectRecipient = (value) => {
if (!value) return
emailData.value.to = value
}
const prefixedSubject = (prefix, subject = "") => {
const cleanSubject = String(subject || "")
return cleanSubject.toLowerCase().startsWith(prefix.toLowerCase()) ? cleanSubject : `${prefix} ${cleanSubject}`.trim()
@@ -124,38 +280,50 @@ const setupPage = async () => {
if(route.query.loadDocuments) {
console.log(JSON.parse(route.query.loadDocuments))
const data = await useFiles().selectSomeDocuments(JSON.parse(route.query.loadDocuments))
console.log(data)
if(data) loadedDocuments.value = data
loadedDocuments.value = await Promise.all(loadedDocuments.value.map(async doc => {
const document = await useEntities("createddocuments").selectSingle(doc.createddocument)
console.log(document)
return {
...doc,
createddocument: document
}}))
//console.log(loadedDocuments.value)
if(loadedDocuments.value.length > 0) {
console.log(loadedDocuments.value[0])
emailData.value.subject = `${loadedDocuments.value[0].createddocument.title} von ${auth.activeTenantData.businessInfo.name}`
const createdDocument = loadedDocuments.value[0].createddocument
const customerId = Number(createdDocument.customer?.id || createdDocument.customer || 0) || null
const contactId = Number(createdDocument.contact?.id || createdDocument.contact || 0) || null
const customer = createdDocument.customer && typeof createdDocument.customer === "object"
? createdDocument.customer
: customerId
? await useEntities("customers").selectSingle(customerId)
: null
const contact = createdDocument.contact && typeof createdDocument.contact === "object"
? createdDocument.contact
: contactId
? await useEntities("contacts").selectSingle(contactId)
: null
if(loadedDocuments.value[0].createddocument.contact && loadedDocuments.value[0].createddocument.contact.email) {
console.log("Contact")
emailData.value.to = loadedDocuments.value[0].createddocument.contact.email
} else if(loadedDocuments.value[0].createddocument.customer && loadedDocuments.value[0].createddocument.customer.infoData.invoiceEmail) {
emailData.value.to = loadedDocuments.value[0].createddocument.customer.infoData.invoiceEmail
} else if(loadedDocuments.value[0].createddocument.customer && loadedDocuments.value[0].createddocument.customer.infoData.email) {
emailData.value.to = loadedDocuments.value[0].createddocument.customer.infoData.email
preferredCustomerId.value = customerId || Number(contact?.customer?.id || contact?.customer || 0) || null
emailData.value.subject = `${createdDocument.title} von ${auth.activeTenantData.businessInfo.name}`
if(contact?.email) {
emailData.value.to = formatAddress({
name: contact.fullName,
address: contact.email,
})
} else if(parseInfoData(customer?.infoData).invoiceEmail) {
emailData.value.to = parseInfoData(customer.infoData).invoiceEmail
} else if(parseInfoData(customer?.infoData).email) {
emailData.value.to = parseInfoData(customer.infoData).email
}
}
}
await loadRecipientOptions()
loaded.value = true
}
@@ -311,10 +479,28 @@ const sendEmail = async () => {
<UFormField
label="Empfänger"
>
<UInput
class="w-full my-1"
v-model="emailData.to"
/>
<div class="grid gap-2 md:grid-cols-[minmax(260px,0.9fr)_minmax(320px,1.1fr)]">
<USelectMenu
v-model="selectedRecipient"
:items="recipientOptions"
value-key="value"
label-key="label"
:loading="loadingRecipients"
:search-input="{ placeholder: 'Kunde, Ansprechpartner oder E-Mail suchen' }"
placeholder="Aus Kunden und Ansprechpartnern wählen"
class="w-full"
@update:model-value="selectRecipient"
/>
<UInput
class="w-full"
v-model="emailData.to"
placeholder="E-Mail-Adresse oder mehrere Adressen"
@update:model-value="selectedRecipient = null"
/>
</div>
<p class="mt-1 text-xs text-dimmed">
Passende Adressen zum vorausgefüllten Kunden werden zuerst angezeigt. Freie Eingaben bleiben möglich.
</p>
</UFormField>
<UFormField
label="Kopie"