658 lines
21 KiB
Vue
658 lines
21 KiB
Vue
<script setup>
|
|
const toast = useToast()
|
|
const { $api } = useNuxtApp()
|
|
const runtimeConfig = useRuntimeConfig()
|
|
|
|
const status = ref(null)
|
|
const identity = ref(null)
|
|
const tenantSpace = ref(null)
|
|
const generalRoom = ref(null)
|
|
const provisionResult = ref(null)
|
|
const tenantSpaceProvisionResult = ref(null)
|
|
const generalRoomProvisionResult = ref(null)
|
|
const matrixMessages = ref([])
|
|
const matrixMessageDraft = ref("")
|
|
const loading = ref(false)
|
|
const provisioning = ref(false)
|
|
const tenantSpaceProvisioning = ref(false)
|
|
const generalRoomProvisioning = ref(false)
|
|
const matrixMessagesLoading = ref(false)
|
|
const matrixMessageSending = ref(false)
|
|
const lastUpdated = ref(null)
|
|
|
|
const statusItems = computed(() => [
|
|
{
|
|
label: "Konfiguration",
|
|
value: status.value?.configured ? "Aktiv" : "Nicht aktiv",
|
|
icon: status.value?.configured ? "i-heroicons-check-circle" : "i-heroicons-x-circle",
|
|
color: status.value?.configured ? "success" : "error"
|
|
},
|
|
{
|
|
label: "Homeserver",
|
|
value: status.value?.homeserverUrl || "-",
|
|
icon: "i-heroicons-server-stack",
|
|
color: "neutral"
|
|
},
|
|
{
|
|
label: "Servername",
|
|
value: status.value?.serverName || "-",
|
|
icon: "i-heroicons-identification",
|
|
color: "neutral"
|
|
},
|
|
{
|
|
label: "Provisionierung",
|
|
value: status.value?.provisioningConfigured ? "Bereit" : "Nicht eingerichtet",
|
|
icon: status.value?.provisioningConfigured ? "i-heroicons-key" : "i-heroicons-exclamation-triangle",
|
|
color: status.value?.provisioningConfigured ? "success" : "warning"
|
|
},
|
|
{
|
|
label: "Erreichbarkeit",
|
|
value: status.value?.reachable ? "Erreichbar" : "Nicht erreichbar",
|
|
icon: status.value?.reachable ? "i-heroicons-signal" : "i-heroicons-signal-slash",
|
|
color: status.value?.reachable ? "success" : "error"
|
|
}
|
|
])
|
|
|
|
const elementBaseUrl = computed(() =>
|
|
String(runtimeConfig.public.matrixElementUrl || "http://localhost:8080").replace(/\/+$/, "")
|
|
)
|
|
|
|
const activeMatrixTarget = computed(() => {
|
|
if (generalRoom.value?.exists) return generalRoom.value
|
|
if (tenantSpace.value?.exists) return tenantSpace.value
|
|
return null
|
|
})
|
|
|
|
const activeMatrixTargetId = computed(() =>
|
|
activeMatrixTarget.value?.roomId || activeMatrixTarget.value?.alias || ""
|
|
)
|
|
|
|
const embeddedElementUrl = computed(() => {
|
|
if (!activeMatrixTargetId.value) return `${elementBaseUrl.value}/`
|
|
return `${elementBaseUrl.value}/#/room/${encodeURIComponent(activeMatrixTargetId.value)}`
|
|
})
|
|
|
|
const loadMatrixInfo = async () => {
|
|
loading.value = true
|
|
try {
|
|
const [statusRes, identityRes, tenantSpaceRes, generalRoomRes] = await Promise.all([
|
|
$api("/api/communication/matrix/status"),
|
|
$api("/api/communication/matrix/me"),
|
|
$api("/api/communication/matrix/tenant-space"),
|
|
$api("/api/communication/matrix/rooms/general")
|
|
])
|
|
|
|
status.value = statusRes
|
|
identity.value = identityRes
|
|
tenantSpace.value = tenantSpaceRes
|
|
generalRoom.value = generalRoomRes
|
|
lastUpdated.value = new Date()
|
|
|
|
if (generalRoomRes?.exists) {
|
|
await loadGeneralMessages()
|
|
}
|
|
} catch (error) {
|
|
toast.add({
|
|
title: "Matrix-Status konnte nicht geladen werden",
|
|
color: "error"
|
|
})
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const provisionMatrixAccount = async () => {
|
|
provisioning.value = true
|
|
try {
|
|
const res = await $api("/api/communication/matrix/me/provision", {
|
|
method: "POST"
|
|
})
|
|
|
|
provisionResult.value = res
|
|
identity.value = {
|
|
...identity.value,
|
|
matrixUserId: res.matrixUserId,
|
|
displayName: res.displayName
|
|
}
|
|
|
|
toast.add({
|
|
title: res.alreadyExisted ? "Matrix-Konto ist bereits vorhanden" : "Matrix-Konto erstellt",
|
|
color: "success"
|
|
})
|
|
} catch (error) {
|
|
toast.add({
|
|
title: "Matrix-Konto konnte nicht erstellt werden",
|
|
color: "error"
|
|
})
|
|
} finally {
|
|
provisioning.value = false
|
|
}
|
|
}
|
|
|
|
const provisionTenantSpace = async () => {
|
|
tenantSpaceProvisioning.value = true
|
|
try {
|
|
const res = await $api("/api/communication/matrix/tenant-space/provision", {
|
|
method: "POST"
|
|
})
|
|
|
|
tenantSpaceProvisionResult.value = res
|
|
tenantSpace.value = {
|
|
tenantId: res.tenantId,
|
|
tenantName: res.tenantName,
|
|
alias: res.alias,
|
|
exists: true,
|
|
roomId: res.roomId,
|
|
servers: res.servers || []
|
|
}
|
|
|
|
toast.add({
|
|
title: res.alreadyExisted ? "Mandanten-Space ist bereits vorhanden" : "Mandanten-Space erstellt",
|
|
color: "success"
|
|
})
|
|
} catch (error) {
|
|
toast.add({
|
|
title: "Mandanten-Space konnte nicht erstellt werden",
|
|
color: "error"
|
|
})
|
|
} finally {
|
|
tenantSpaceProvisioning.value = false
|
|
}
|
|
}
|
|
|
|
const provisionGeneralRoom = async () => {
|
|
generalRoomProvisioning.value = true
|
|
try {
|
|
const res = await $api("/api/communication/matrix/rooms/general/provision", {
|
|
method: "POST"
|
|
})
|
|
|
|
generalRoomProvisionResult.value = res
|
|
generalRoom.value = {
|
|
tenantId: res.tenantId,
|
|
tenantName: res.tenantName,
|
|
key: res.key,
|
|
name: res.name,
|
|
alias: res.alias,
|
|
exists: true,
|
|
roomId: res.roomId,
|
|
servers: res.servers || []
|
|
}
|
|
tenantSpace.value = {
|
|
...tenantSpace.value,
|
|
exists: true,
|
|
roomId: res.parentSpaceRoomId || tenantSpace.value?.roomId
|
|
}
|
|
|
|
toast.add({
|
|
title: res.alreadyExisted ? "Allgemeiner Chat ist bereits vorhanden" : "Allgemeiner Chat erstellt",
|
|
color: "success"
|
|
})
|
|
|
|
await loadGeneralMessages()
|
|
} catch (error) {
|
|
toast.add({
|
|
title: "Allgemeiner Chat konnte nicht erstellt werden",
|
|
color: "error"
|
|
})
|
|
} finally {
|
|
generalRoomProvisioning.value = false
|
|
}
|
|
}
|
|
|
|
const loadGeneralMessages = async () => {
|
|
matrixMessagesLoading.value = true
|
|
try {
|
|
const res = await $api("/api/communication/matrix/rooms/general/messages")
|
|
matrixMessages.value = res.messages || []
|
|
generalRoom.value = {
|
|
...generalRoom.value,
|
|
alias: res.alias || generalRoom.value?.alias,
|
|
exists: true,
|
|
roomId: res.roomId || generalRoom.value?.roomId
|
|
}
|
|
} catch (error) {
|
|
toast.add({
|
|
title: "Matrix-Nachrichten konnten nicht geladen werden",
|
|
color: "error"
|
|
})
|
|
} finally {
|
|
matrixMessagesLoading.value = false
|
|
}
|
|
}
|
|
|
|
const sendMatrixMessage = async () => {
|
|
const text = matrixMessageDraft.value.trim()
|
|
if (!text) return
|
|
|
|
matrixMessageSending.value = true
|
|
try {
|
|
const message = await $api("/api/communication/matrix/rooms/general/messages", {
|
|
method: "POST",
|
|
body: { text }
|
|
})
|
|
|
|
matrixMessages.value = [
|
|
...matrixMessages.value,
|
|
message
|
|
]
|
|
matrixMessageDraft.value = ""
|
|
} catch (error) {
|
|
toast.add({
|
|
title: "Matrix-Nachricht konnte nicht gesendet werden",
|
|
color: "error"
|
|
})
|
|
} finally {
|
|
matrixMessageSending.value = false
|
|
}
|
|
}
|
|
|
|
const formatDateTime = (value) => {
|
|
if (!value) return "-"
|
|
|
|
return new Intl.DateTimeFormat("de-DE", {
|
|
dateStyle: "short",
|
|
timeStyle: "short"
|
|
}).format(value)
|
|
}
|
|
|
|
const formatMessageTime = (timestamp) => {
|
|
if (!timestamp) return ""
|
|
|
|
return new Intl.DateTimeFormat("de-DE", {
|
|
dateStyle: "short",
|
|
timeStyle: "short"
|
|
}).format(new Date(timestamp))
|
|
}
|
|
|
|
onMounted(loadMatrixInfo)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="min-h-0 flex-1 overflow-y-auto">
|
|
<div class="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6">
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-semibold text-highlighted">
|
|
Kommunikation
|
|
</h1>
|
|
<p class="mt-1 text-sm text-muted">
|
|
Matrix-Verbindung und persönliche Kommunikationsidentität.
|
|
</p>
|
|
</div>
|
|
|
|
<UButton
|
|
icon="i-heroicons-arrow-path"
|
|
color="neutral"
|
|
variant="outline"
|
|
:loading="loading"
|
|
@click="loadMatrixInfo"
|
|
>
|
|
Aktualisieren
|
|
</UButton>
|
|
</div>
|
|
|
|
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
<UCard
|
|
v-for="item in statusItems"
|
|
:key="item.label"
|
|
:ui="{ root: 'rounded-lg', body: 'p-4 sm:p-4' }"
|
|
>
|
|
<div class="flex items-start gap-3">
|
|
<UIcon
|
|
:name="item.icon"
|
|
class="mt-0.5 size-5 shrink-0"
|
|
:class="{
|
|
'text-success': item.color === 'success',
|
|
'text-error': item.color === 'error',
|
|
'text-warning': item.color === 'warning',
|
|
'text-muted': item.color === 'neutral'
|
|
}"
|
|
/>
|
|
<div class="min-w-0">
|
|
<p class="text-xs font-medium uppercase text-muted">
|
|
{{ item.label }}
|
|
</p>
|
|
<p class="mt-1 break-words text-sm font-medium text-highlighted">
|
|
{{ item.value }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
|
|
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_360px]">
|
|
<div class="space-y-4">
|
|
<UCard :ui="{ root: 'rounded-lg' }">
|
|
<template #header>
|
|
<div class="flex items-center gap-2">
|
|
<UIcon name="i-heroicons-user-circle" class="size-5 text-primary" />
|
|
<h2 class="text-base font-semibold text-highlighted">
|
|
Eigene Matrix-Identität
|
|
</h2>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="space-y-4">
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
<div>
|
|
<p class="text-xs font-medium uppercase text-muted">
|
|
Matrix-ID
|
|
</p>
|
|
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
|
{{ identity?.matrixUserId || "-" }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium uppercase text-muted">
|
|
Anzeigename
|
|
</p>
|
|
<p class="mt-1 text-sm text-highlighted">
|
|
{{ identity?.displayName || "-" }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<UAlert
|
|
v-if="provisionResult"
|
|
icon="i-heroicons-check-circle"
|
|
color="success"
|
|
variant="soft"
|
|
:title="provisionResult.alreadyExisted ? 'Matrix-Konto vorhanden' : 'Matrix-Konto erstellt'"
|
|
:description="provisionResult.matrixUserId"
|
|
/>
|
|
|
|
<UAlert
|
|
v-if="status && !status.reachable"
|
|
icon="i-heroicons-exclamation-triangle"
|
|
color="error"
|
|
variant="soft"
|
|
title="Matrix-Homeserver nicht erreichbar"
|
|
:description="status.error || 'Bitte prüfe den lokalen Matrix-Stack und die Backend-Konfiguration.'"
|
|
/>
|
|
|
|
<UAlert
|
|
v-else-if="status && !status.provisioningConfigured"
|
|
icon="i-heroicons-key"
|
|
color="warning"
|
|
variant="soft"
|
|
title="Matrix-Provisionierung nicht eingerichtet"
|
|
description="Bitte setze MATRIX_REGISTRATION_SHARED_SECRET in der Backend-Umgebung."
|
|
/>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<p class="text-xs text-muted">
|
|
Aktualisiert: {{ formatDateTime(lastUpdated) }}
|
|
</p>
|
|
<UButton
|
|
icon="i-heroicons-user-plus"
|
|
:loading="provisioning"
|
|
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
|
@click="provisionMatrixAccount"
|
|
>
|
|
Matrix-Konto erstellen
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
</UCard>
|
|
|
|
<UCard :ui="{ root: 'rounded-lg' }">
|
|
<template #header>
|
|
<div class="flex items-center gap-2">
|
|
<UIcon name="i-heroicons-building-office-2" class="size-5 text-primary" />
|
|
<h2 class="text-base font-semibold text-highlighted">
|
|
Mandanten-Space
|
|
</h2>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="space-y-4">
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
<div>
|
|
<p class="text-xs font-medium uppercase text-muted">
|
|
Alias
|
|
</p>
|
|
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
|
{{ tenantSpace?.alias || "-" }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium uppercase text-muted">
|
|
Status
|
|
</p>
|
|
<UBadge
|
|
class="mt-1"
|
|
:color="tenantSpace?.exists ? 'success' : 'neutral'"
|
|
variant="soft"
|
|
>
|
|
{{ tenantSpace?.exists ? "Vorhanden" : "Noch nicht erstellt" }}
|
|
</UBadge>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="text-xs font-medium uppercase text-muted">
|
|
Raum-ID
|
|
</p>
|
|
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
|
{{ tenantSpace?.roomId || "-" }}
|
|
</p>
|
|
</div>
|
|
|
|
<UAlert
|
|
v-if="tenantSpaceProvisionResult"
|
|
icon="i-heroicons-check-circle"
|
|
color="success"
|
|
variant="soft"
|
|
:title="tenantSpaceProvisionResult.alreadyExisted ? 'Mandanten-Space vorhanden' : 'Mandanten-Space erstellt'"
|
|
:description="tenantSpaceProvisionResult.alias"
|
|
/>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-end">
|
|
<UButton
|
|
icon="i-heroicons-plus"
|
|
:loading="tenantSpaceProvisioning"
|
|
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
|
@click="provisionTenantSpace"
|
|
>
|
|
Mandanten-Space erstellen
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
</UCard>
|
|
|
|
<UCard :ui="{ root: 'rounded-lg' }">
|
|
<template #header>
|
|
<div class="flex items-center gap-2">
|
|
<UIcon name="i-heroicons-chat-bubble-left-right" class="size-5 text-primary" />
|
|
<h2 class="text-base font-semibold text-highlighted">
|
|
Allgemeiner Chat
|
|
</h2>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="space-y-4">
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
<div>
|
|
<p class="text-xs font-medium uppercase text-muted">
|
|
Alias
|
|
</p>
|
|
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
|
{{ generalRoom?.alias || "-" }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium uppercase text-muted">
|
|
Status
|
|
</p>
|
|
<UBadge
|
|
class="mt-1"
|
|
:color="generalRoom?.exists ? 'success' : 'neutral'"
|
|
variant="soft"
|
|
>
|
|
{{ generalRoom?.exists ? "Vorhanden" : "Noch nicht erstellt" }}
|
|
</UBadge>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="text-xs font-medium uppercase text-muted">
|
|
Raum-ID
|
|
</p>
|
|
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
|
{{ generalRoom?.roomId || "-" }}
|
|
</p>
|
|
</div>
|
|
|
|
<UAlert
|
|
v-if="generalRoomProvisionResult"
|
|
icon="i-heroicons-check-circle"
|
|
color="success"
|
|
variant="soft"
|
|
:title="generalRoomProvisionResult.alreadyExisted ? 'Allgemeiner Chat vorhanden' : 'Allgemeiner Chat erstellt'"
|
|
:description="generalRoomProvisionResult.alias"
|
|
/>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-end">
|
|
<UButton
|
|
icon="i-heroicons-plus"
|
|
:loading="generalRoomProvisioning"
|
|
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
|
@click="provisionGeneralRoom"
|
|
>
|
|
Allgemeinen Chat erstellen
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
</UCard>
|
|
</div>
|
|
|
|
<UCard :ui="{ root: 'rounded-lg' }">
|
|
<template #header>
|
|
<div class="flex items-center gap-2">
|
|
<UIcon name="i-heroicons-video-camera" class="size-5 text-primary" />
|
|
<h2 class="text-base font-semibold text-highlighted">
|
|
Nächste Ausbaustufe
|
|
</h2>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="space-y-3 text-sm text-muted">
|
|
<div class="flex gap-2">
|
|
<UIcon name="i-heroicons-building-office-2" class="mt-0.5 size-4 shrink-0" />
|
|
<span>Team- und Projekt-Räume im Mandanten-Space anlegen.</span>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<UIcon name="i-heroicons-users" class="mt-0.5 size-4 shrink-0" />
|
|
<span>FEDEO-Nutzer in Matrix-Räume synchronisieren.</span>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<UIcon name="i-heroicons-chat-bubble-left-right" class="mt-0.5 size-4 shrink-0" />
|
|
<span>Nativen FEDEO-Chat schrittweise an Matrix-Sync anbinden.</span>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
|
|
<UCard :ui="{ root: 'rounded-lg overflow-hidden', body: 'p-0 sm:p-0' }">
|
|
<template #header>
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="flex min-w-0 items-center gap-2">
|
|
<UIcon name="i-heroicons-chat-bubble-left-right" class="size-5 shrink-0 text-primary" />
|
|
<div class="min-w-0">
|
|
<h2 class="text-base font-semibold text-highlighted">
|
|
Matrix-Kommunikation
|
|
</h2>
|
|
<p class="mt-1 truncate text-xs text-muted">
|
|
{{ generalRoom?.name || generalRoom?.alias || "Allgemeiner Chat" }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
<UButton
|
|
icon="i-heroicons-arrow-path"
|
|
color="neutral"
|
|
variant="outline"
|
|
:loading="matrixMessagesLoading"
|
|
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
|
@click="loadGeneralMessages"
|
|
>
|
|
Nachrichten laden
|
|
</UButton>
|
|
<UButton
|
|
icon="i-heroicons-arrow-top-right-on-square"
|
|
color="neutral"
|
|
variant="outline"
|
|
:to="embeddedElementUrl"
|
|
target="_blank"
|
|
>
|
|
Element öffnen
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="flex h-[640px] min-h-[520px] flex-col bg-muted">
|
|
<div class="flex-1 space-y-3 overflow-y-auto p-4 sm:p-5">
|
|
<div
|
|
v-if="!matrixMessages.length && !matrixMessagesLoading"
|
|
class="flex h-full min-h-64 items-center justify-center text-sm text-muted"
|
|
>
|
|
Noch keine Nachrichten im allgemeinen Chat.
|
|
</div>
|
|
|
|
<div
|
|
v-for="message in matrixMessages"
|
|
:key="message.id"
|
|
class="flex"
|
|
:class="message.own ? 'justify-end' : 'justify-start'"
|
|
>
|
|
<div
|
|
class="max-w-[78%] rounded-lg px-3 py-2 shadow-sm"
|
|
:class="message.own ? 'bg-primary text-inverted' : 'bg-default text-highlighted'"
|
|
>
|
|
<div class="mb-1 flex items-center gap-2 text-[11px] opacity-75">
|
|
<span class="truncate font-medium">
|
|
{{ message.own ? "Du" : message.sender }}
|
|
</span>
|
|
<span>{{ formatMessageTime(message.timestamp) }}</span>
|
|
</div>
|
|
<p class="whitespace-pre-wrap break-words text-sm">
|
|
{{ message.body }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form
|
|
class="flex gap-2 border-t border-default bg-default p-3"
|
|
@submit.prevent="sendMatrixMessage"
|
|
>
|
|
<UInput
|
|
v-model="matrixMessageDraft"
|
|
class="min-w-0 flex-1"
|
|
placeholder="Nachricht schreiben"
|
|
:disabled="matrixMessageSending || !status?.reachable || !status?.provisioningConfigured"
|
|
/>
|
|
<UButton
|
|
type="submit"
|
|
icon="i-heroicons-paper-airplane"
|
|
:loading="matrixMessageSending"
|
|
:disabled="!matrixMessageDraft.trim() || !status?.reachable || !status?.provisioningConfigured"
|
|
>
|
|
Senden
|
|
</UButton>
|
|
</form>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
</div>
|
|
</template>
|