Compare commits
2 Commits
36969e272d
...
60327fe928
| Author | SHA1 | Date | |
|---|---|---|---|
| 60327fe928 | |||
| 8d8b07ce0d |
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user