KI-AGENT: Nebenstellen und Standardroute für Telefonie ergänzen
This commit is contained in:
@@ -122,6 +122,20 @@ const itemInfo = ref({
|
||||
projectTypes: []
|
||||
})
|
||||
|
||||
const planningBoardTimeGridOptions = [
|
||||
{ label: "15 Minuten", value: 15 },
|
||||
{ label: "30 Minuten", value: 30 },
|
||||
{ label: "60 Minuten", value: 60 },
|
||||
{ label: "120 Minuten", value: 120 },
|
||||
{ label: "180 Minuten", value: 180 }
|
||||
]
|
||||
|
||||
const planningBoardConfig = reactive({
|
||||
startTime: auth.activeTenantData?.calendarConfig?.planningBoard?.startTime || "06:00",
|
||||
endTime: auth.activeTenantData?.calendarConfig?.planningBoard?.endTime || "21:00",
|
||||
slotMinutes: auth.activeTenantData?.calendarConfig?.planningBoard?.slotMinutes || 180
|
||||
})
|
||||
|
||||
const canManageMcpTokens = computed(() => Boolean(auth.user?.is_admin || auth.hasPermission("mcp.tokens.write")))
|
||||
const mcpTokens = ref([])
|
||||
const mcpTokensLoading = ref(false)
|
||||
@@ -135,10 +149,18 @@ const mcpTokenForm = reactive({
|
||||
const telephonyTrunkLoading = ref(false)
|
||||
const telephonyTrunkSaving = ref(false)
|
||||
const telephonyTrunkApplying = ref(false)
|
||||
const telephonyExtensionsLoading = ref(false)
|
||||
const telephonyExtensionSaving = ref(false)
|
||||
const telephonyExtensionDeletingId = ref(null)
|
||||
const telephonyProviderOptions = [
|
||||
{ label: "Easybell", value: "easybell" },
|
||||
{ label: "Telekom", value: "telekom" }
|
||||
]
|
||||
const telephonyExtensionTargetTypes = [
|
||||
{ label: "Benutzer", value: "user" },
|
||||
{ label: "Team", value: "team" },
|
||||
{ label: "Niederlassung", value: "branch" }
|
||||
]
|
||||
const telephonyProviderDefaults = {
|
||||
easybell: {
|
||||
registrar: "voip.easybell.de",
|
||||
@@ -162,16 +184,43 @@ const telephonyTrunkForm = reactive({
|
||||
clearPassword: false,
|
||||
callerId: "",
|
||||
inboundExtension: "1001",
|
||||
defaultRouteExtensionId: null,
|
||||
outboundPrefix: "0",
|
||||
externalSignalingAddress: "",
|
||||
externalMediaAddress: "",
|
||||
localNetworks: "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8"
|
||||
})
|
||||
const telephonyExtensions = ref([])
|
||||
const telephonyExtensionOptions = ref({ users: [], teams: [], branches: [] })
|
||||
const telephonyExtensionForm = reactive({
|
||||
id: null,
|
||||
targetType: "user",
|
||||
targetUserId: null,
|
||||
targetTeamId: null,
|
||||
targetBranchId: null,
|
||||
extension: "",
|
||||
displayName: "",
|
||||
sipUsername: "",
|
||||
sipPassword: "",
|
||||
enabled: true
|
||||
})
|
||||
const activeTelephonyProvider = computed(() => telephonyProviderDefaults[telephonyTrunkForm.provider] || telephonyProviderDefaults.easybell)
|
||||
const telephonyRouteOptions = computed(() => telephonyExtensions.value.map((extension) => ({
|
||||
label: `${extension.extension} - ${extension.displayName || extension.targetType}`,
|
||||
value: extension.id
|
||||
})))
|
||||
const activeTelephonyTargetOptions = computed(() => {
|
||||
if (telephonyExtensionForm.targetType === "team") return telephonyExtensionOptions.value.teams || []
|
||||
if (telephonyExtensionForm.targetType === "branch") return telephonyExtensionOptions.value.branches || []
|
||||
return telephonyExtensionOptions.value.users || []
|
||||
})
|
||||
|
||||
const setupPage = async () => {
|
||||
itemInfo.value = auth.activeTenantData
|
||||
console.log(itemInfo.value)
|
||||
planningBoardConfig.startTime = auth.activeTenantData?.calendarConfig?.planningBoard?.startTime || "06:00"
|
||||
planningBoardConfig.endTime = auth.activeTenantData?.calendarConfig?.planningBoard?.endTime || "21:00"
|
||||
planningBoardConfig.slotMinutes = auth.activeTenantData?.calendarConfig?.planningBoard?.slotMinutes || 180
|
||||
}
|
||||
|
||||
const features = ref({ ...defaultFeatures, ...(auth.activeTenantData?.features || {}) })
|
||||
@@ -244,6 +293,7 @@ const loadTelephonyTrunk = async () => {
|
||||
telephonyTrunkForm.clearPassword = false
|
||||
telephonyTrunkForm.callerId = res?.callerId || ""
|
||||
telephonyTrunkForm.inboundExtension = res?.inboundExtension || "1001"
|
||||
telephonyTrunkForm.defaultRouteExtensionId = res?.defaultRouteExtensionId || null
|
||||
telephonyTrunkForm.outboundPrefix = res?.outboundPrefix || "0"
|
||||
telephonyTrunkForm.externalSignalingAddress = res?.externalSignalingAddress || ""
|
||||
telephonyTrunkForm.externalMediaAddress = res?.externalMediaAddress || ""
|
||||
@@ -280,6 +330,7 @@ const saveTelephonyTrunk = async () => {
|
||||
clearPassword: telephonyTrunkForm.clearPassword,
|
||||
callerId: telephonyTrunkForm.callerId,
|
||||
inboundExtension: telephonyTrunkForm.inboundExtension,
|
||||
defaultRouteExtensionId: telephonyTrunkForm.defaultRouteExtensionId,
|
||||
outboundPrefix: telephonyTrunkForm.outboundPrefix,
|
||||
externalSignalingAddress: telephonyTrunkForm.externalSignalingAddress,
|
||||
externalMediaAddress: telephonyTrunkForm.externalMediaAddress,
|
||||
@@ -302,6 +353,111 @@ const saveTelephonyTrunk = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadTelephonyExtensionOptions = async () => {
|
||||
try {
|
||||
telephonyExtensionOptions.value = await useNuxtApp().$api("/api/telephony/extensions/options")
|
||||
} catch (error) {
|
||||
toast.add({ title: "Nebenstellen-Ziele konnten nicht geladen werden", color: "error" })
|
||||
}
|
||||
}
|
||||
|
||||
const loadTelephonyExtensions = async () => {
|
||||
telephonyExtensionsLoading.value = true
|
||||
|
||||
try {
|
||||
const res = await useNuxtApp().$api("/api/telephony/extensions")
|
||||
telephonyExtensions.value = res?.rows || []
|
||||
} catch (error) {
|
||||
toast.add({ title: "Nebenstellen konnten nicht geladen werden", color: "error" })
|
||||
} finally {
|
||||
telephonyExtensionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetTelephonyExtensionForm = () => {
|
||||
telephonyExtensionForm.id = null
|
||||
telephonyExtensionForm.targetType = "user"
|
||||
telephonyExtensionForm.targetUserId = null
|
||||
telephonyExtensionForm.targetTeamId = null
|
||||
telephonyExtensionForm.targetBranchId = null
|
||||
telephonyExtensionForm.extension = ""
|
||||
telephonyExtensionForm.displayName = ""
|
||||
telephonyExtensionForm.sipUsername = ""
|
||||
telephonyExtensionForm.sipPassword = ""
|
||||
telephonyExtensionForm.enabled = true
|
||||
}
|
||||
|
||||
const editTelephonyExtension = (extension) => {
|
||||
telephonyExtensionForm.id = extension.id
|
||||
telephonyExtensionForm.targetType = extension.targetType || "user"
|
||||
telephonyExtensionForm.targetUserId = extension.targetUserId || null
|
||||
telephonyExtensionForm.targetTeamId = extension.targetTeamId || null
|
||||
telephonyExtensionForm.targetBranchId = extension.targetBranchId || null
|
||||
telephonyExtensionForm.extension = extension.extension || ""
|
||||
telephonyExtensionForm.displayName = extension.displayName || ""
|
||||
telephonyExtensionForm.sipUsername = extension.sipUsername || extension.extension || ""
|
||||
telephonyExtensionForm.sipPassword = ""
|
||||
telephonyExtensionForm.enabled = extension.enabled !== false
|
||||
}
|
||||
|
||||
const telephonyExtensionPayload = () => ({
|
||||
targetType: telephonyExtensionForm.targetType,
|
||||
targetUserId: telephonyExtensionForm.targetType === "user" ? telephonyExtensionForm.targetUserId : null,
|
||||
targetTeamId: telephonyExtensionForm.targetType === "team" ? telephonyExtensionForm.targetTeamId : null,
|
||||
targetBranchId: telephonyExtensionForm.targetType === "branch" ? telephonyExtensionForm.targetBranchId : null,
|
||||
extension: telephonyExtensionForm.extension,
|
||||
displayName: telephonyExtensionForm.displayName,
|
||||
sipUsername: telephonyExtensionForm.sipUsername,
|
||||
sipPassword: telephonyExtensionForm.sipPassword,
|
||||
enabled: telephonyExtensionForm.enabled
|
||||
})
|
||||
|
||||
const saveTelephonyExtension = async () => {
|
||||
if (!telephonyExtensionForm.extension?.trim()) {
|
||||
toast.add({ title: "Nebenstelle fehlt", color: "orange" })
|
||||
return
|
||||
}
|
||||
|
||||
telephonyExtensionSaving.value = true
|
||||
|
||||
try {
|
||||
await useNuxtApp().$api(
|
||||
telephonyExtensionForm.id ? `/api/telephony/extensions/${telephonyExtensionForm.id}` : "/api/telephony/extensions",
|
||||
{
|
||||
method: telephonyExtensionForm.id ? "PUT" : "POST",
|
||||
body: telephonyExtensionPayload()
|
||||
}
|
||||
)
|
||||
|
||||
toast.add({ title: "Nebenstelle gespeichert", color: "success" })
|
||||
resetTelephonyExtensionForm()
|
||||
await loadTelephonyExtensions()
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: "Nebenstelle konnte nicht gespeichert werden",
|
||||
description: error?.data?.error || error?.message,
|
||||
color: "error"
|
||||
})
|
||||
} finally {
|
||||
telephonyExtensionSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteTelephonyExtension = async (extension) => {
|
||||
if (!extension?.id || !confirm(`Nebenstelle ${extension.extension} löschen?`)) return
|
||||
|
||||
telephonyExtensionDeletingId.value = extension.id
|
||||
try {
|
||||
await useNuxtApp().$api(`/api/telephony/extensions/${extension.id}`, { method: "DELETE" })
|
||||
toast.add({ title: "Nebenstelle gelöscht", color: "success" })
|
||||
await loadTelephonyExtensions()
|
||||
} catch (error) {
|
||||
toast.add({ title: "Nebenstelle konnte nicht gelöscht werden", color: "error" })
|
||||
} finally {
|
||||
telephonyExtensionDeletingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const applyTelephonyTrunk = async () => {
|
||||
telephonyTrunkApplying.value = true
|
||||
|
||||
@@ -378,9 +534,36 @@ const deactivateMcpToken = async (token) => {
|
||||
}
|
||||
}
|
||||
|
||||
const savePlanningBoardConfig = async () => {
|
||||
if (!planningBoardConfig.startTime || !planningBoardConfig.endTime) {
|
||||
toast.add({ title: "Rahmenzeiten fehlen", description: "Bitte Start- und Endzeit angeben.", color: "orange" })
|
||||
return
|
||||
}
|
||||
|
||||
if (planningBoardConfig.startTime >= planningBoardConfig.endTime) {
|
||||
toast.add({ title: "Rahmenzeiten ungültig", description: "Die Endzeit muss nach der Startzeit liegen.", color: "orange" })
|
||||
return
|
||||
}
|
||||
|
||||
const currentCalendarConfig = auth.activeTenantData?.calendarConfig || {}
|
||||
|
||||
await updateTenant({
|
||||
calendarConfig: {
|
||||
...currentCalendarConfig,
|
||||
planningBoard: {
|
||||
startTime: planningBoardConfig.startTime || "06:00",
|
||||
endTime: planningBoardConfig.endTime || "21:00",
|
||||
slotMinutes: Number(planningBoardConfig.slotMinutes) || 180
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setupPage()
|
||||
onMounted(() => {
|
||||
loadMcpTokens()
|
||||
loadTelephonyExtensionOptions()
|
||||
loadTelephonyExtensions()
|
||||
loadTelephonyTrunk()
|
||||
})
|
||||
|
||||
@@ -404,6 +587,8 @@ watch(() => telephonyTrunkForm.provider, async (provider) => {
|
||||
label: 'Dokubox'
|
||||
},{
|
||||
label: 'Rechnung & Kontakt'
|
||||
},{
|
||||
label: 'Plantafel'
|
||||
},{
|
||||
label: 'Integrationen'
|
||||
},{
|
||||
@@ -498,6 +683,48 @@ watch(() => telephonyTrunkForm.provider, async (provider) => {
|
||||
</UCard>
|
||||
|
||||
</div>
|
||||
<div v-else-if="item.label === 'Plantafel'">
|
||||
<UCard class="mt-5">
|
||||
<UForm class="w-1/2">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-highlighted">Plantafel</h3>
|
||||
<p class="text-sm text-muted">Lege hier die Rahmenzeiten und den Rasterabstand für die Plantafel fest.</p>
|
||||
</div>
|
||||
|
||||
<UFormField
|
||||
label="Rahmenzeit von:"
|
||||
>
|
||||
<UInput type="time" v-model="planningBoardConfig.startTime" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Rahmenzeit bis:"
|
||||
>
|
||||
<UInput type="time" v-model="planningBoardConfig.endTime" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Rasterabstand:"
|
||||
>
|
||||
<USelectMenu
|
||||
v-model="planningBoardConfig.slotMinutes"
|
||||
:items="planningBoardTimeGridOptions"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UButton
|
||||
class="mt-1"
|
||||
@click="savePlanningBoardConfig"
|
||||
>
|
||||
Plantafel speichern
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
</UCard>
|
||||
</div>
|
||||
<div v-else-if="item.label === 'Integrationen'">
|
||||
<UCard class="mt-5">
|
||||
<div class="mb-8 space-y-6">
|
||||
@@ -563,6 +790,15 @@ watch(() => telephonyTrunkForm.provider, async (provider) => {
|
||||
<UFormField label="Eingehende Nebenstelle">
|
||||
<UInput v-model="telephonyTrunkForm.inboundExtension" placeholder="1001" />
|
||||
</UFormField>
|
||||
<UFormField label="Standardroute">
|
||||
<USelectMenu
|
||||
v-model="telephonyTrunkForm.defaultRouteExtensionId"
|
||||
:items="telephonyRouteOptions"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
placeholder="Nebenstelle auswählen"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="Ausgehender Prefix">
|
||||
<UInput v-model="telephonyTrunkForm.outboundPrefix" placeholder="0" />
|
||||
</UFormField>
|
||||
@@ -603,6 +839,133 @@ watch(() => telephonyTrunkForm.provider, async (provider) => {
|
||||
In Asterisk anwenden
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<USeparator />
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-highlighted">Nebenstellen</h4>
|
||||
<p class="text-sm text-muted">Ordne Benutzer, Teams und Niederlassungen routbaren Nebenstellen zu.</p>
|
||||
</div>
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-path"
|
||||
variant="outline"
|
||||
:loading="telephonyExtensionsLoading"
|
||||
@click="loadTelephonyExtensions"
|
||||
>
|
||||
Aktualisieren
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<UFormField label="Zieltyp">
|
||||
<USelectMenu
|
||||
v-model="telephonyExtensionForm.targetType"
|
||||
:items="telephonyExtensionTargetTypes"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField v-if="telephonyExtensionForm.targetType === 'user'" label="Benutzer">
|
||||
<USelectMenu
|
||||
v-model="telephonyExtensionForm.targetUserId"
|
||||
:items="activeTelephonyTargetOptions"
|
||||
label-key="label"
|
||||
value-key="id"
|
||||
placeholder="Benutzer auswählen"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField v-else-if="telephonyExtensionForm.targetType === 'team'" label="Team">
|
||||
<USelectMenu
|
||||
v-model="telephonyExtensionForm.targetTeamId"
|
||||
:items="activeTelephonyTargetOptions"
|
||||
label-key="label"
|
||||
value-key="id"
|
||||
placeholder="Team auswählen"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField v-else label="Niederlassung">
|
||||
<USelectMenu
|
||||
v-model="telephonyExtensionForm.targetBranchId"
|
||||
:items="activeTelephonyTargetOptions"
|
||||
label-key="label"
|
||||
value-key="id"
|
||||
placeholder="Niederlassung auswählen"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="Nebenstelle">
|
||||
<UInput v-model="telephonyExtensionForm.extension" placeholder="1001" />
|
||||
</UFormField>
|
||||
<UFormField label="Anzeigename">
|
||||
<UInput v-model="telephonyExtensionForm.displayName" placeholder="Optional" />
|
||||
</UFormField>
|
||||
<UFormField label="SIP-Benutzername">
|
||||
<UInput v-model="telephonyExtensionForm.sipUsername" placeholder="Leer = Nebenstelle" />
|
||||
</UFormField>
|
||||
<UFormField label="SIP-Kennwort">
|
||||
<UInput v-model="telephonyExtensionForm.sipPassword" type="password" placeholder="Leer = automatisch" />
|
||||
</UFormField>
|
||||
<UFormField label="Aktiv">
|
||||
<USwitch v-model="telephonyExtensionForm.enabled" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<UButton
|
||||
icon="i-heroicons-check"
|
||||
:loading="telephonyExtensionSaving"
|
||||
@click="saveTelephonyExtension"
|
||||
>
|
||||
{{ telephonyExtensionForm.id ? "Nebenstelle speichern" : "Nebenstelle anlegen" }}
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="telephonyExtensionForm.id"
|
||||
variant="outline"
|
||||
@click="resetTelephonyExtensionForm"
|
||||
>
|
||||
Abbrechen
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-default rounded-md border border-default">
|
||||
<div
|
||||
v-for="extension in telephonyExtensions"
|
||||
:key="extension.id"
|
||||
class="flex flex-col gap-3 p-3 md:flex-row md:items-center md:justify-between"
|
||||
>
|
||||
<div>
|
||||
<div class="font-medium text-highlighted">
|
||||
{{ extension.extension }} - {{ extension.displayName || extension.targetType }}
|
||||
</div>
|
||||
<div class="text-sm text-muted">
|
||||
{{ extension.targetType }} · Ziele: {{ extension.dialTargets?.length ? extension.dialTargets.join(", ") : "noch keine Benutzer-Nebenstelle" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<UButton
|
||||
icon="i-heroicons-pencil-square"
|
||||
variant="outline"
|
||||
@click="editTelephonyExtension(extension)"
|
||||
>
|
||||
Bearbeiten
|
||||
</UButton>
|
||||
<UButton
|
||||
icon="i-heroicons-trash"
|
||||
color="error"
|
||||
variant="soft"
|
||||
:loading="telephonyExtensionDeletingId === extension.id"
|
||||
@click="deleteTelephonyExtension(extension)"
|
||||
>
|
||||
Löschen
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!telephonyExtensions.length" class="p-3 text-sm text-muted">
|
||||
Noch keine Nebenstellen angelegt.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<USeparator class="mb-8" />
|
||||
|
||||
Reference in New Issue
Block a user