KI-AGENT: Mailantworten und Entitätsverknüpfungen ergänzen
This commit is contained in:
@@ -52,9 +52,17 @@ type EmailMessage = {
|
||||
contentType?: string | null
|
||||
size?: number | null
|
||||
}>
|
||||
entityLinks?: Array<{
|
||||
id: string
|
||||
entityType: string
|
||||
entityId: number
|
||||
entityName: string
|
||||
entityTypeLabel: string
|
||||
}>
|
||||
}
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
const route = useRoute()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const toast = useToast()
|
||||
|
||||
@@ -74,6 +82,7 @@ const expandedMailboxPaths = ref<string[]>([])
|
||||
const syncedMailboxPaths = ref<string[]>([])
|
||||
const actionLoading = ref("")
|
||||
const moveTargetMailboxPath = ref("")
|
||||
let deepLinkApplied = false
|
||||
|
||||
const selectedAccount = computed(() =>
|
||||
accounts.value.find((account) => account.id === selectedAccountId.value) || null
|
||||
@@ -295,7 +304,10 @@ async function loadAccounts() {
|
||||
loadingAccounts.value = true
|
||||
try {
|
||||
accounts.value = await $api("/api/email/accounts")
|
||||
selectedAccountId.value = accounts.value[0]?.id || ""
|
||||
const requestedAccountId = String(route.query.account || "")
|
||||
selectedAccountId.value = accounts.value.some((account) => account.id === requestedAccountId)
|
||||
? requestedAccountId
|
||||
: accounts.value[0]?.id || ""
|
||||
if (selectedAccountId.value) {
|
||||
await loadMailboxes()
|
||||
}
|
||||
@@ -316,7 +328,9 @@ async function loadMailboxes() {
|
||||
resetExpandedMailboxes()
|
||||
const inbox = mailboxes.value.find((mailbox) => mailbox.specialUse === "\\Inbox" || mailbox.path.toUpperCase() === "INBOX")
|
||||
const previousMailbox = mailboxes.value.find((mailbox) => mailbox.path === previousMailboxPath)
|
||||
selectedMailboxPath.value = previousMailbox?.path || inbox?.path || mailboxes.value[0]?.path || "INBOX"
|
||||
const requestedMailboxPath = String(route.query.mailbox || "")
|
||||
const requestedMailbox = mailboxes.value.find((mailbox) => mailbox.path === requestedMailboxPath)
|
||||
selectedMailboxPath.value = requestedMailbox?.path || previousMailbox?.path || inbox?.path || mailboxes.value[0]?.path || "INBOX"
|
||||
expandMailboxAncestors(selectedMailboxPath.value)
|
||||
await loadMessages()
|
||||
} finally {
|
||||
@@ -340,7 +354,10 @@ async function loadMessages(options: { syncIfEmpty?: boolean } = {}) {
|
||||
}
|
||||
|
||||
if (messages.value.length) {
|
||||
await selectMessage(messages.value[0])
|
||||
const requestedMessageId = deepLinkApplied ? "" : String(route.query.message || "")
|
||||
const requestedMessage = messages.value.find((message) => message.id === requestedMessageId)
|
||||
await selectMessage(requestedMessage || messages.value[0])
|
||||
deepLinkApplied = true
|
||||
}
|
||||
} finally {
|
||||
loadingMessages.value = false
|
||||
@@ -375,6 +392,22 @@ async function selectMessage(message: EmailMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelectedMessageEntityLinks(entityLinks: NonNullable<EmailMessage["entityLinks"]>) {
|
||||
if (!selectedMessage.value) return
|
||||
selectedMessage.value = { ...selectedMessage.value, entityLinks }
|
||||
}
|
||||
|
||||
function openComposer(mode: "reply" | "replyAll" | "forward") {
|
||||
if (!selectedMessage.value) return
|
||||
navigateTo({
|
||||
path: "/email/new",
|
||||
query: {
|
||||
mode,
|
||||
source: selectedMessage.value.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function removeMessageFromCurrentList(messageId: string) {
|
||||
const currentIndex = messages.value.findIndex((message) => message.id === messageId)
|
||||
messages.value = messages.value.filter((message) => message.id !== messageId)
|
||||
@@ -800,16 +833,26 @@ onMounted(loadAccounts)
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
size="sm"
|
||||
@click="navigateTo(`/email/new?to=${encodeURIComponent(formatAddressList(selectedMessage.from))}&subject=${encodeURIComponent(`Re: ${selectedMessage.subject || ''}`)}`)"
|
||||
@click="openComposer('reply')"
|
||||
>
|
||||
Antworten
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="selectedMessage.cc?.length || (selectedMessage.to?.length || 0) > 1"
|
||||
icon="i-heroicons-users"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="openComposer('replyAll')"
|
||||
>
|
||||
Allen antworten
|
||||
</UButton>
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-uturn-right"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="navigateTo(`/email/new?subject=${encodeURIComponent(`Fw: ${selectedMessage.subject || ''}`)}`)"
|
||||
@click="openComposer('forward')"
|
||||
>
|
||||
Weiterleiten
|
||||
</UButton>
|
||||
@@ -831,6 +874,12 @@ onMounted(loadAccounts)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<EmailEntityLinks
|
||||
:message-id="selectedMessage.id"
|
||||
:entity-links="selectedMessage.entityLinks"
|
||||
@updated="updateSelectedMessageEntityLinks"
|
||||
/>
|
||||
|
||||
<div v-if="selectedMessage.attachments?.length" class="mt-4 flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="attachment in selectedMessage.attachments"
|
||||
|
||||
@@ -21,6 +21,55 @@ const preloadedContent = ref("")
|
||||
const loadedDocuments = ref([])
|
||||
const loaded = ref(false)
|
||||
const noAccountsPresent = ref(false)
|
||||
const sourceMessage = ref(null)
|
||||
const composeMode = computed(() => {
|
||||
const mode = String(route.query.mode || "")
|
||||
return ["reply", "replyAll", "forward"].includes(mode) ? mode : "new"
|
||||
})
|
||||
const pageTitle = computed(() => ({
|
||||
reply: "Antworten",
|
||||
replyAll: "Allen antworten",
|
||||
forward: "E-Mail weiterleiten",
|
||||
}[composeMode.value] || "Neue E-Mail"))
|
||||
|
||||
const escapeHtml = (value = "") => String(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
|
||||
const formatAddress = (address) => {
|
||||
if (!address?.address) return ""
|
||||
return address.name ? `"${String(address.name).replace(/"/g, "\\\"")}" <${address.address}>` : address.address
|
||||
}
|
||||
|
||||
const formatAddressList = (addresses = []) => addresses.map(formatAddress).filter(Boolean).join(", ")
|
||||
|
||||
const prefixedSubject = (prefix, subject = "") => {
|
||||
const cleanSubject = String(subject || "")
|
||||
return cleanSubject.toLowerCase().startsWith(prefix.toLowerCase()) ? cleanSubject : `${prefix} ${cleanSubject}`.trim()
|
||||
}
|
||||
|
||||
const originalMessageBlock = (message, mode) => {
|
||||
const sentAt = message.sentAt || message.receivedAt
|
||||
const date = sentAt ? new Intl.DateTimeFormat("de-DE", {
|
||||
dateStyle: "full",
|
||||
timeStyle: "short",
|
||||
}).format(new Date(sentAt)) : ""
|
||||
const originalText = escapeHtml(message.body?.text || message.preview || "").replace(/\n/g, "<br>")
|
||||
|
||||
if (mode === "forward") {
|
||||
return `<hr><p><strong>Weitergeleitete Nachricht</strong><br>`
|
||||
+ `Von: ${escapeHtml(formatAddressList(message.from))}<br>`
|
||||
+ `Datum: ${escapeHtml(date)}<br>`
|
||||
+ `Betreff: ${escapeHtml(message.subject || "(kein Betreff)")}<br>`
|
||||
+ `An: ${escapeHtml(formatAddressList(message.to))}</p><blockquote>${originalText}</blockquote>`
|
||||
}
|
||||
|
||||
return `<hr><blockquote><p>Am ${escapeHtml(date)} schrieb ${escapeHtml(formatAddressList(message.from))}:</p>${originalText}</blockquote>`
|
||||
}
|
||||
|
||||
const setupPage = async () => {
|
||||
//emailAccounts.value = await useEntities("emailAccounts").select()
|
||||
|
||||
@@ -31,7 +80,41 @@ const setupPage = async () => {
|
||||
} else {
|
||||
emailData.value.account = emailAccounts.value[0].id
|
||||
|
||||
preloadedContent.value = `<p></p><p></p><p></p>${auth.profile.email_signature || ""}`
|
||||
let initialContent = `<p></p><p></p><p></p>${auth.profile.email_signature || ""}`
|
||||
|
||||
if (route.query.source && composeMode.value !== "new") {
|
||||
sourceMessage.value = await useNuxtApp().$api(`/api/email/messages/${route.query.source}`)
|
||||
const sourceAccount = emailAccounts.value.find(account => account.id === sourceMessage.value.accountId)
|
||||
if (sourceAccount) emailData.value.account = sourceAccount.id
|
||||
|
||||
if (composeMode.value === "forward") {
|
||||
emailData.value.subject = prefixedSubject("Fw:", sourceMessage.value.subject)
|
||||
} else {
|
||||
const ownAddress = String(sourceAccount?.email || "").toLowerCase()
|
||||
const replyRecipients = sourceMessage.value.replyTo?.length
|
||||
? sourceMessage.value.replyTo
|
||||
: sourceMessage.value.from || []
|
||||
|
||||
if (composeMode.value === "replyAll") {
|
||||
const allRecipients = [
|
||||
...replyRecipients,
|
||||
...(sourceMessage.value.to || []),
|
||||
...(sourceMessage.value.cc || []),
|
||||
].filter(address => address?.address && address.address.toLowerCase() !== ownAddress)
|
||||
const uniqueRecipients = Array.from(new Map(
|
||||
allRecipients.map(address => [address.address.toLowerCase(), address])
|
||||
).values())
|
||||
emailData.value.to = formatAddressList(uniqueRecipients)
|
||||
} else {
|
||||
emailData.value.to = formatAddressList(replyRecipients)
|
||||
}
|
||||
emailData.value.subject = prefixedSubject("Re:", sourceMessage.value.subject)
|
||||
}
|
||||
|
||||
initialContent += originalMessageBlock(sourceMessage.value, composeMode.value)
|
||||
}
|
||||
|
||||
preloadedContent.value = initialContent
|
||||
|
||||
//Check Query
|
||||
if(route.query.to) emailData.value.to = route.query.to
|
||||
@@ -120,7 +203,9 @@ const sendEmail = async () => {
|
||||
|
||||
let body = {
|
||||
...emailData.value,
|
||||
attachments: []
|
||||
attachments: [],
|
||||
sourceMessageId: sourceMessage.value?.id,
|
||||
composeMode: composeMode.value === "new" ? undefined : composeMode.value,
|
||||
}
|
||||
|
||||
|
||||
@@ -151,26 +236,28 @@ const sendEmail = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
console.log(body)
|
||||
try {
|
||||
const res = await useNuxtApp().$api("/api/email/send",{
|
||||
method: "POST",
|
||||
body: body,
|
||||
})
|
||||
|
||||
const res = await useNuxtApp().$api("/api/email/send",{
|
||||
method: "POST",
|
||||
body: body,
|
||||
})
|
||||
|
||||
console.log(res)
|
||||
|
||||
|
||||
if(!res.success) {
|
||||
toast.add({title: "Fehler beim Absenden der E-Mail", color: "error"})
|
||||
|
||||
} else {
|
||||
navigateTo("/")
|
||||
toast.add({title: "E-Mail zum Senden eingereiht"})
|
||||
if(!res.success) {
|
||||
toast.add({title: "Fehler beim Absenden der E-Mail", color: "error"})
|
||||
} else {
|
||||
await navigateTo("/email")
|
||||
toast.add({title: "E-Mail gesendet", color: "success"})
|
||||
}
|
||||
} catch (err) {
|
||||
toast.add({
|
||||
title: "Fehler beim Absenden der E-Mail",
|
||||
description: err?.data?.error || err?.message,
|
||||
color: "error",
|
||||
})
|
||||
} finally {
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
loaded.value = true
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +279,7 @@ const sendEmail = async () => {
|
||||
|
||||
<div v-else>
|
||||
<UDashboardNavbar
|
||||
title="Neue E-Mail"
|
||||
:title="pageTitle"
|
||||
>
|
||||
<template #right>
|
||||
<UButton
|
||||
@@ -280,6 +367,12 @@ const sendEmail = async () => {
|
||||
>
|
||||
<span v-if="doc.createddocument">Dokument - {{doc.createddocument.documentNumber}}</span>
|
||||
</li>
|
||||
<li
|
||||
v-if="composeMode === 'forward' && sourceMessage?.attachments?.length"
|
||||
class="list-disc"
|
||||
>
|
||||
{{ sourceMessage.attachments.length }} Originalanhang/Originalanhänge werden übernommen
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user