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 sameId = (left: any, right: any) => String(left ?? "") === String(right ?? "")
|
||||||
|
|
||||||
const getStatementDate = (allocation: any) => {
|
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) => {
|
const matchesSelectedPeriod = (dateValue: any) => {
|
||||||
@@ -154,21 +159,12 @@ const filteredStatementAllocations = computed(() => {
|
|||||||
return statementAllocations.value.filter((allocation) => matchesSelectedPeriod(getStatementDate(allocation)))
|
return statementAllocations.value.filter((allocation) => matchesSelectedPeriod(getStatementDate(allocation)))
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredDirectAccountStatementAllocations = computed(() => {
|
const filteredAccountStatementAllocations = computed(() => {
|
||||||
return filteredStatementAllocations.value.filter((allocation) => {
|
return filteredStatementAllocations.value.filter((allocation) => {
|
||||||
if (allocation.account === null || allocation.account === undefined) {
|
if (allocation.account === null || allocation.account === undefined) {
|
||||||
return false
|
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
|
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))
|
.filter((allocation) => sameId(allocation.account?.id || allocation.account, account.id))
|
||||||
.map((allocation) => {
|
.map((allocation) => {
|
||||||
const amount = Number(allocation.amount || 0)
|
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) {
|
if (bookings.length === 0) {
|
||||||
return null
|
return null
|
||||||
@@ -350,23 +359,29 @@ const accountRows = computed(() => {
|
|||||||
const ownAccountRows = computed(() => {
|
const ownAccountRows = computed(() => {
|
||||||
return ownAccounts.value
|
return ownAccounts.value
|
||||||
.map((account) => {
|
.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) {
|
if (bookings.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const income = bookings.reduce((sum, booking) => {
|
const income = bookings.reduce((sum, amount) => {
|
||||||
const amount = Number(booking.amount || 0)
|
|
||||||
return amount > 0 ? sum + amount : sum
|
return amount > 0 ? sum + amount : sum
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
const expenses = bookings.reduce((sum, booking) => {
|
const expenses = bookings.reduce((sum, amount) => {
|
||||||
const amount = Number(booking.amount || 0)
|
|
||||||
return amount < 0 ? sum + Math.abs(amount) : sum
|
return amount < 0 ? sum + Math.abs(amount) : sum
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
const balance = bookings.reduce((sum, booking) => sum + Number(booking.amount || 0), 0)
|
const balance = bookings.reduce((sum, amount) => sum + amount, 0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: account.id,
|
id: account.id,
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const dataStore = useDataStore()
|
|
||||||
const profileStore = useProfileStore()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -22,6 +20,10 @@ const loadedDocuments = ref([])
|
|||||||
const loaded = ref(false)
|
const loaded = ref(false)
|
||||||
const noAccountsPresent = ref(false)
|
const noAccountsPresent = ref(false)
|
||||||
const sourceMessage = ref(null)
|
const sourceMessage = ref(null)
|
||||||
|
const recipientOptions = ref([])
|
||||||
|
const selectedRecipient = ref(null)
|
||||||
|
const loadingRecipients = ref(false)
|
||||||
|
const preferredCustomerId = ref(null)
|
||||||
const composeMode = computed(() => {
|
const composeMode = computed(() => {
|
||||||
const mode = String(route.query.mode || "")
|
const mode = String(route.query.mode || "")
|
||||||
return ["reply", "replyAll", "forward"].includes(mode) ? mode : "new"
|
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 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 prefixedSubject = (prefix, subject = "") => {
|
||||||
const cleanSubject = String(subject || "")
|
const cleanSubject = String(subject || "")
|
||||||
return cleanSubject.toLowerCase().startsWith(prefix.toLowerCase()) ? cleanSubject : `${prefix} ${cleanSubject}`.trim()
|
return cleanSubject.toLowerCase().startsWith(prefix.toLowerCase()) ? cleanSubject : `${prefix} ${cleanSubject}`.trim()
|
||||||
@@ -124,38 +280,50 @@ const setupPage = async () => {
|
|||||||
|
|
||||||
|
|
||||||
if(route.query.loadDocuments) {
|
if(route.query.loadDocuments) {
|
||||||
console.log(JSON.parse(route.query.loadDocuments))
|
|
||||||
const data = await useFiles().selectSomeDocuments(JSON.parse(route.query.loadDocuments))
|
const data = await useFiles().selectSomeDocuments(JSON.parse(route.query.loadDocuments))
|
||||||
console.log(data)
|
|
||||||
|
|
||||||
if(data) loadedDocuments.value = data
|
if(data) loadedDocuments.value = data
|
||||||
|
|
||||||
loadedDocuments.value = await Promise.all(loadedDocuments.value.map(async doc => {
|
loadedDocuments.value = await Promise.all(loadedDocuments.value.map(async doc => {
|
||||||
|
|
||||||
const document = await useEntities("createddocuments").selectSingle(doc.createddocument)
|
const document = await useEntities("createddocuments").selectSingle(doc.createddocument)
|
||||||
console.log(document)
|
|
||||||
return {
|
return {
|
||||||
...doc,
|
...doc,
|
||||||
createddocument: document
|
createddocument: document
|
||||||
}}))
|
}}))
|
||||||
|
|
||||||
//console.log(loadedDocuments.value)
|
|
||||||
|
|
||||||
if(loadedDocuments.value.length > 0) {
|
if(loadedDocuments.value.length > 0) {
|
||||||
console.log(loadedDocuments.value[0])
|
const createdDocument = loadedDocuments.value[0].createddocument
|
||||||
emailData.value.subject = `${loadedDocuments.value[0].createddocument.title} von ${auth.activeTenantData.businessInfo.name}`
|
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) {
|
preferredCustomerId.value = customerId || Number(contact?.customer?.id || contact?.customer || 0) || null
|
||||||
console.log("Contact")
|
emailData.value.subject = `${createdDocument.title} von ${auth.activeTenantData.businessInfo.name}`
|
||||||
emailData.value.to = loadedDocuments.value[0].createddocument.contact.email
|
|
||||||
} else if(loadedDocuments.value[0].createddocument.customer && loadedDocuments.value[0].createddocument.customer.infoData.invoiceEmail) {
|
if(contact?.email) {
|
||||||
emailData.value.to = loadedDocuments.value[0].createddocument.customer.infoData.invoiceEmail
|
emailData.value.to = formatAddress({
|
||||||
} else if(loadedDocuments.value[0].createddocument.customer && loadedDocuments.value[0].createddocument.customer.infoData.email) {
|
name: contact.fullName,
|
||||||
emailData.value.to = loadedDocuments.value[0].createddocument.customer.infoData.email
|
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
|
loaded.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,10 +479,28 @@ const sendEmail = async () => {
|
|||||||
<UFormField
|
<UFormField
|
||||||
label="Empfänger"
|
label="Empfänger"
|
||||||
>
|
>
|
||||||
<UInput
|
<div class="grid gap-2 md:grid-cols-[minmax(260px,0.9fr)_minmax(320px,1.1fr)]">
|
||||||
class="w-full my-1"
|
<USelectMenu
|
||||||
v-model="emailData.to"
|
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>
|
||||||
<UFormField
|
<UFormField
|
||||||
label="Kopie"
|
label="Kopie"
|
||||||
|
|||||||
Reference in New Issue
Block a user