KI-AGENT: Mailantworten und Entitätsverknüpfungen ergänzen

This commit is contained in:
2026-09-07 19:00:08 +02:00
parent 4e4466ff11
commit f3384cfc74
10 changed files with 774 additions and 30 deletions

View File

@@ -366,6 +366,11 @@ const invitePortalUser = async () => {
v-else-if="tab.label === 'Zeiten'"
:platform="platform"
/>
<EmailEntityMessages
v-else-if="tab.label === 'E-Mails'"
:entity-type="type"
:entity-id="props.item.id"
/>
<div v-else-if="tab.label === 'Wiki'" class="h-[600px] w-full overflow-hidden">
<WikiEntityWidget
:entity-type="type"
@@ -410,6 +415,11 @@ const invitePortalUser = async () => {
@updateNeeded="emit('updateNeeded')"
:platform="platform"
/>
<EmailEntityMessages
v-else-if="sub.label === 'E-Mails'"
:entity-type="type"
:entity-id="props.item.id"
/>
<!--<EntityShowSubPhases
:item="props.item"
:top-level-type="type"

View File

@@ -0,0 +1,169 @@
<script setup lang="ts">
type EntityLink = {
id: string
entityType: string
entityId: number
entityName: string
entityTypeLabel: string
}
const props = defineProps<{
messageId: string
entityLinks?: EntityLink[]
}>()
const emit = defineEmits<{
updated: [links: EntityLink[]]
}>()
const toast = useToast()
const links = ref<EntityLink[]>([])
const selectedEntityType = ref("customers")
const selectedEntityId = ref<number | null>(null)
const entityOptions = ref<Array<{ label: string; value: number }>>([])
const loadingOptions = ref(false)
const saving = ref(false)
const entityTypes = [
{ label: "Kunde", value: "customers" },
{ label: "Lieferant", value: "vendors" },
{ label: "Projekt", value: "projects" },
{ label: "Objekt", value: "plants" },
]
watch(() => props.entityLinks, (value) => {
links.value = [...(value || [])]
}, { immediate: true, deep: true })
async function loadEntityOptions() {
loadingOptions.value = true
selectedEntityId.value = null
try {
const rows = await useEntities(selectedEntityType.value).select("*")
entityOptions.value = rows.map((row: any) => {
const number = row.customerNumber || row.vendorNumber || row.projectNumber
return {
label: number ? `${number} · ${row.name}` : row.name,
value: Number(row.id),
}
})
} catch (err: any) {
entityOptions.value = []
toast.add({
title: "Entitäten konnten nicht geladen werden",
description: err?.data?.error || err?.message,
color: "error",
})
} finally {
loadingOptions.value = false
}
}
async function addLink() {
if (!selectedEntityId.value) return
saving.value = true
try {
const response = await useNuxtApp().$api(`/api/email/messages/${props.messageId}/entity-links`, {
method: "POST",
body: {
entityType: selectedEntityType.value,
entityId: selectedEntityId.value,
},
})
links.value = response.entityLinks || []
emit("updated", links.value)
selectedEntityId.value = null
toast.add({ title: "E-Mail verknüpft", color: "success" })
} catch (err: any) {
toast.add({
title: "Verknüpfen fehlgeschlagen",
description: err?.data?.error || err?.message,
color: "error",
})
} finally {
saving.value = false
}
}
async function removeLink(link: EntityLink) {
saving.value = true
try {
const response = await useNuxtApp().$api(
`/api/email/messages/${props.messageId}/entity-links/${link.entityType}/${link.entityId}`,
{ method: "DELETE" },
)
links.value = response.entityLinks || []
emit("updated", links.value)
toast.add({ title: "Verknüpfung entfernt", color: "success" })
} catch (err: any) {
toast.add({
title: "Entfernen fehlgeschlagen",
description: err?.data?.error || err?.message,
color: "error",
})
} finally {
saving.value = false
}
}
watch(selectedEntityType, loadEntityOptions, { immediate: true })
</script>
<template>
<div class="mt-4 rounded-lg border border-(--ui-border) bg-(--ui-bg-muted) p-3">
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-medium">Verknüpft mit</span>
<UBadge
v-for="link in links"
:key="link.id"
color="primary"
variant="soft"
class="gap-1"
>
{{ link.entityTypeLabel }}: {{ link.entityName }}
<button
type="button"
class="ml-1 rounded hover:text-error"
:disabled="saving"
:aria-label="`Verknüpfung mit ${link.entityName} entfernen`"
@click="removeLink(link)"
>
<UIcon name="i-heroicons-x-mark" class="size-3.5" />
</button>
</UBadge>
<span v-if="!links.length" class="text-sm text-dimmed">Noch keine Zuordnung</span>
</div>
<div class="mt-3 flex flex-wrap items-center gap-2">
<USelectMenu
v-model="selectedEntityType"
:items="entityTypes"
value-key="value"
label-key="label"
size="sm"
class="w-36"
/>
<USelectMenu
v-model="selectedEntityId"
:items="entityOptions"
value-key="value"
label-key="label"
size="sm"
class="min-w-64 flex-1"
placeholder="Entität auswählen"
:loading="loadingOptions"
/>
<UButton
icon="i-heroicons-link"
size="sm"
:loading="saving"
:disabled="!selectedEntityId"
@click="addLink"
>
Verknüpfen
</UButton>
</div>
</div>
</template>

View File

@@ -0,0 +1,129 @@
<script setup lang="ts">
type EmailAddress = {
name?: string | null
address?: string | null
}
type LinkedMessage = {
id: string
accountId: string
mailboxPath: string
subject?: string | null
from?: EmailAddress[] | null
to?: EmailAddress[] | null
preview?: string | null
receivedAt?: string | null
sentAt?: string | null
linkedAt: string
hasAttachments: boolean
canOpen: boolean
}
const props = defineProps<{
entityType: string
entityId: string | number
}>()
const messages = ref<LinkedMessage[]>([])
const loading = ref(true)
const errorMessage = ref("")
const formatAddress = (addresses?: EmailAddress[] | null) => {
const address = addresses?.[0]
if (!address) return "Unbekannt"
return address.name || address.address || "Unbekannt"
}
const formatDate = (value?: string | null) => {
if (!value) return ""
return new Intl.DateTimeFormat("de-DE", {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(value))
}
const openMessage = (message: LinkedMessage) => navigateTo({
path: "/email",
query: {
account: message.accountId,
mailbox: message.mailboxPath,
message: message.id,
},
})
async function loadMessages() {
loading.value = true
errorMessage.value = ""
try {
messages.value = await useNuxtApp().$api(
`/api/email/entity-links/${props.entityType}/${props.entityId}`,
)
} catch (err: any) {
errorMessage.value = err?.data?.error || err?.message || "Verknüpfte E-Mails konnten nicht geladen werden."
} finally {
loading.value = false
}
}
watch(() => [props.entityType, props.entityId], loadMessages, { immediate: true })
</script>
<template>
<div class="space-y-3">
<div v-if="loading" class="space-y-3">
<USkeleton v-for="index in 4" :key="index" class="h-28" />
</div>
<UAlert
v-else-if="errorMessage"
color="error"
icon="i-heroicons-exclamation-triangle"
title="E-Mails konnten nicht geladen werden"
:description="errorMessage"
/>
<TableEmptyState
v-else-if="!messages.length"
label="Noch keine E-Mails mit dieser Entität verknüpft"
/>
<article
v-for="message in messages"
v-else
:key="message.id"
class="rounded-lg border border-(--ui-border) bg-(--ui-bg) p-4"
>
<div class="flex items-start justify-between gap-4">
<div class="min-w-0">
<h3 class="truncate font-medium text-highlighted">
{{ message.subject || '(kein Betreff)' }}
</h3>
<p class="mt-1 text-sm text-dimmed">
Von {{ formatAddress(message.from) }} · {{ formatDate(message.receivedAt || message.sentAt) }}
</p>
</div>
<UButton
v-if="message.canOpen"
icon="i-heroicons-arrow-top-right-on-square"
color="neutral"
variant="soft"
size="sm"
@click="openMessage(message)"
>
Im Postfach öffnen
</UButton>
</div>
<p class="mt-3 line-clamp-2 text-sm text-muted">
{{ message.preview || 'Keine Vorschau verfügbar' }}
</p>
<div class="mt-3 flex items-center gap-3 text-xs text-dimmed">
<span>Verknüpft am {{ formatDate(message.linkedAt) }}</span>
<span v-if="message.hasAttachments" class="flex items-center gap-1">
<UIcon name="i-heroicons-paper-clip" class="size-3.5" />
Anhänge
</span>
</div>
</article>
</div>
</template>