diff --git a/backend/src/routes/telephony.ts b/backend/src/routes/telephony.ts index 6710ba0..2546378 100644 --- a/backend/src/routes/telephony.ts +++ b/backend/src/routes/telephony.ts @@ -3,7 +3,7 @@ import { promises as fs } from "node:fs" import net from "node:net" import path from "node:path" import { randomBytes } from "node:crypto" -import { and, desc, eq, inArray } from "drizzle-orm" +import { and, desc, eq, inArray, ne } from "drizzle-orm" import { authProfileBranches, authProfiles, @@ -25,21 +25,15 @@ const telephonyEnabled = () => envFlag(process.env.TELEPHONY_ENABLED, process.env.NODE_ENV !== "production") const asteriskHttpStatusUrl = () => - process.env.TELEPHONY_ASTERISK_HTTP_URL || "http://asterisk-dev:8088/ws" + process.env.TELEPHONY_ASTERISK_HTTP_URL || "" const asteriskHttpStatusUrls = () => { const configuredUrl = asteriskHttpStatusUrl() - return Array.from(new Set([ - configuredUrl, - "http://asterisk-dev:8088/ws", - "http://host.docker.internal:8088/ws", - `http://127.0.0.1:${process.env.TELEPHONY_DEV_WS_PORT || "8088"}/ws`, - `http://localhost:${process.env.TELEPHONY_DEV_WS_PORT || "8088"}/ws`, - ])) + return configuredUrl ? [configuredUrl] : [] } const publicAsteriskWsUrl = () => - process.env.TELEPHONY_ASTERISK_WS_URL || `ws://localhost:${process.env.TELEPHONY_DEV_WS_PORT || "8088"}/ws` + process.env.TELEPHONY_ASTERISK_WS_URL || "" const defaultAsteriskGeneratedDir = () => { const cwd = process.cwd() @@ -50,7 +44,7 @@ const asteriskGeneratedDir = () => process.env.TELEPHONY_ASTERISK_GENERATED_DIR || defaultAsteriskGeneratedDir() const defaultAsteriskAmiHost = () => - process.env.NODE_ENV === "production" ? "asterisk-dev" : "127.0.0.1" + process.env.NODE_ENV === "production" ? "" : "127.0.0.1" const asteriskAmiConfig = () => ({ host: process.env.TELEPHONY_ASTERISK_AMI_HOST || defaultAsteriskAmiHost(), @@ -62,19 +56,6 @@ const asteriskAmiConfig = () => ({ const sipDomain = () => process.env.TELEPHONY_SIP_DOMAIN || "localhost" -const testAccounts = () => [ - { - extension: process.env.TELEPHONY_TEST_EXTENSION || "1001", - password: process.env.TELEPHONY_TEST_PASSWORD || "fedeo-test-1001", - displayName: "FEDEO Test 1001", - }, - { - extension: process.env.TELEPHONY_TEST_EXTENSION_2 || "1002", - password: process.env.TELEPHONY_TEST_PASSWORD_2 || "fedeo-test-1002", - displayName: "FEDEO Test 1002", - }, -] - const trunkProviders = { telekom: { key: "telekom", @@ -498,6 +479,9 @@ const writeAsteriskTrunkConfig = async (trunk: any, extensions: any[] = [], dial const runAsteriskAmiCommand = async (command: string, timeoutMs = 5000) => { const config = asteriskAmiConfig() + if (!config.host) { + throw new Error("Asterisk-AMI ist nicht konfiguriert.") + } return await new Promise<{ command: string, ok: boolean, raw: string }>((resolve, reject) => { const socket = net.createConnection(config.port, config.host) @@ -708,6 +692,59 @@ export default async function telephonyRoutes(server: FastifyInstance) { updatedAt: extension.updatedAt, }) + const extensionTargetWhere = (tenantId: number, targetType: string, targetId: string | number | null) => { + if (targetType === "team") { + return and( + eq(telephonyExtensions.tenantId, tenantId), + eq(telephonyExtensions.targetType, "team"), + eq(telephonyExtensions.targetTeamId, Number(targetId)) + ) + } + + if (targetType === "branch") { + return and( + eq(telephonyExtensions.tenantId, tenantId), + eq(telephonyExtensions.targetType, "branch"), + eq(telephonyExtensions.targetBranchId, Number(targetId)) + ) + } + + return and( + eq(telephonyExtensions.tenantId, tenantId), + eq(telephonyExtensions.targetType, "user"), + eq(telephonyExtensions.targetUserId, String(targetId)) + ) + } + + const resolveExtensionTargetId = (targetType: string, body: any) => { + if (targetType === "team") return bodyNumber(body, "targetTeamId") ?? bodyNumber(body, "targetId") + if (targetType === "branch") return bodyNumber(body, "targetBranchId") ?? bodyNumber(body, "targetId") + return bodyString(body, "targetUserId") || bodyString(body, "targetId") + } + + const validateExtensionDuplicate = async ( + tenantId: number, + extension: string, + currentExtensionId: string | null = null + ) => { + const conditions = [ + eq(telephonyExtensions.tenantId, tenantId), + eq(telephonyExtensions.extension, extension), + ] + + if (currentExtensionId) { + conditions.push(ne(telephonyExtensions.id, currentExtensionId)) + } + + const existing = await server.db + .select({ id: telephonyExtensions.id }) + .from(telephonyExtensions) + .where(and(...conditions)) + .limit(1) + + return existing[0] ? "Diese Nebenstelle ist bereits vergeben." : null + } + const externalTelephonyConfig = async (tenantId: number | null) => { const trunk = await loadActiveTenantTrunk(tenantId) if (trunk) { @@ -731,14 +768,34 @@ export default async function telephonyRoutes(server: FastifyInstance) { return envExternalTelephonyConfig() } + const loadUserSipAccounts = async (tenantId: number | null, userId: string | null | undefined) => { + if (!tenantId || !userId) return [] + + const extensions = await server.db + .select() + .from(telephonyExtensions) + .where(and( + eq(telephonyExtensions.tenantId, tenantId), + eq(telephonyExtensions.targetType, "user"), + eq(telephonyExtensions.targetUserId, userId), + eq(telephonyExtensions.enabled, true) + )) + + return extensions.map((extension) => ({ + extension: extension.extension, + password: extension.sipPassword, + displayName: extension.displayName || `Nebenstelle ${extension.extension}`, + })).filter((account) => account.extension && account.password) + } + server.get("/telephony/config", async (req) => ({ enabled: telephonyEnabled(), provider: "asterisk", - mode: "local-test", + mode: "asterisk", sipDomain: sipDomain(), sipWebSocketUrl: publicAsteriskWsUrl(), echoExtension: process.env.TELEPHONY_ECHO_EXTENSION || "600", - testAccounts: testAccounts(), + accounts: await loadUserSipAccounts(req.user?.tenant_id || null, req.user?.user_id), external: await externalTelephonyConfig(req.user?.tenant_id || null), })) @@ -787,7 +844,9 @@ export default async function telephonyRoutes(server: FastifyInstance) { reachable: false, statusUrl: urls[0], attempts, - message: lastError?.name === "AbortError" + message: !urls.length + ? "Asterisk-Status-URL ist nicht konfiguriert." + : lastError?.name === "AbortError" ? "Asterisk-Statusabfrage ist abgelaufen." : (lastError?.message || "Asterisk ist nicht erreichbar."), } @@ -1002,17 +1061,166 @@ export default async function telephonyRoutes(server: FastifyInstance) { const tenantId = requireTenant(req.user.tenant_id) const extensions = await loadTenantExtensions(tenantId) const dialTargetsById = await loadExtensionDialTargets(tenantId, extensions) + const options = await (async () => { + const tenantUsers = await server.db + .select({ + id: authUsers.id, + email: authUsers.email, + firstName: authProfiles.first_name, + lastName: authProfiles.last_name, + }) + .from(authProfiles) + .innerJoin(authUsers, eq(authUsers.id, authProfiles.user_id)) + .where(and( + eq(authProfiles.tenant_id, tenantId), + eq(authProfiles.active, true) + )) + + const tenantTeams = await server.db + .select({ id: teams.id, name: teams.name }) + .from(teams) + .where(and(eq(teams.tenant, tenantId), eq(teams.archived, false))) + + const tenantBranches = await server.db + .select({ id: branches.id, name: branches.name, number: branches.number }) + .from(branches) + .where(and(eq(branches.tenant, tenantId), eq(branches.archived, false))) + + return { + users: new Map(tenantUsers.map((user) => [ + user.id, + [user.firstName, user.lastName].filter(Boolean).join(" ") || user.email, + ])), + teams: new Map(tenantTeams.map((team) => [team.id, team.name])), + branches: new Map(tenantBranches.map((branch) => [ + branch.id, + branch.number ? `${branch.number} - ${branch.name}` : branch.name, + ])), + } + })() return { rows: extensions .sort((a, b) => a.extension.localeCompare(b.extension, "de")) .map((extension) => ({ ...sanitizeExtension(extension), + targetLabel: extension.targetType === "team" + ? options.teams.get(extension.targetTeamId) + : extension.targetType === "branch" + ? options.branches.get(extension.targetBranchId) + : options.users.get(extension.targetUserId), dialTargets: dialTargetsById.get(extension.id) || [], })), } }) + server.get("/telephony/extensions/target", async (req) => { + const tenantId = requireTenant(req.user.tenant_id) + const query = req.query as { targetType?: string; targetId?: string } + const targetType = normalizeExtensionTargetType(query.targetType) + const targetId = targetType === "user" ? query.targetId : Number(query.targetId) + + if (!targetId || (targetType !== "user" && !Number.isFinite(Number(targetId)))) { + return { extension: null } + } + + const existing = await server.db + .select() + .from(telephonyExtensions) + .where(extensionTargetWhere(tenantId, targetType, targetId)) + .limit(1) + + return { extension: existing[0] ? sanitizeExtension(existing[0]) : null } + }) + + server.put("/telephony/extensions/target", async (req, reply) => { + const tenantId = requireTenant(req.user.tenant_id) + const body = (req.body || {}) as any + const targetType = normalizeExtensionTargetType(bodyString(body, "targetType")) + const targetId = resolveExtensionTargetId(targetType, body) + const extension = normalizeExtension(bodyString(body, "extension")) + + if (!targetId) { + return reply.code(400).send({ error: "Ziel der Nebenstelle ist erforderlich." }) + } + + if (!extension) { + return reply.code(400).send({ error: "Nebenstelle ist erforderlich." }) + } + + const existing = await server.db + .select() + .from(telephonyExtensions) + .where(extensionTargetWhere(tenantId, targetType, targetId)) + .limit(1) + + const duplicateError = await validateExtensionDuplicate(tenantId, extension, existing[0]?.id || null) + if (duplicateError) { + return reply.code(409).send({ error: duplicateError }) + } + + const displayName = bodyString(body, "displayName") + const now = new Date() + + if (existing[0]) { + const [updated] = await server.db + .update(telephonyExtensions) + .set({ + extension, + displayName, + sipUsername: extension, + enabled: body?.enabled !== false, + updatedAt: now, + updatedBy: req.user.user_id, + }) + .where(and( + eq(telephonyExtensions.tenantId, tenantId), + eq(telephonyExtensions.id, existing[0].id) + )) + .returning() + + return sanitizeExtension(updated) + } + + const [created] = await server.db + .insert(telephonyExtensions) + .values({ + tenantId, + targetType, + targetUserId: targetType === "user" ? String(targetId) : null, + targetTeamId: targetType === "team" ? Number(targetId) : null, + targetBranchId: targetType === "branch" ? Number(targetId) : null, + extension, + displayName, + sipUsername: extension, + sipPassword: generateSipPassword(), + enabled: body?.enabled !== false, + createdBy: req.user.user_id, + updatedAt: now, + updatedBy: req.user.user_id, + }) + .returning() + + return reply.code(201).send(sanitizeExtension(created)) + }) + + server.delete("/telephony/extensions/target", async (req, reply) => { + const tenantId = requireTenant(req.user.tenant_id) + const query = req.query as { targetType?: string; targetId?: string } + const targetType = normalizeExtensionTargetType(query.targetType) + const targetId = targetType === "user" ? query.targetId : Number(query.targetId) + + if (!targetId || (targetType !== "user" && !Number.isFinite(Number(targetId)))) { + return reply.code(400).send({ error: "Ziel der Nebenstelle ist erforderlich." }) + } + + await server.db + .delete(telephonyExtensions) + .where(extensionTargetWhere(tenantId, targetType, targetId)) + + return { deleted: true } + }) + server.post("/telephony/extensions", async (req, reply) => { const tenantId = requireTenant(req.user.tenant_id) const body = (req.body || {}) as any @@ -1023,6 +1231,11 @@ export default async function telephonyRoutes(server: FastifyInstance) { return reply.code(400).send({ error: "Nebenstelle ist erforderlich." }) } + const duplicateError = await validateExtensionDuplicate(tenantId, extension) + if (duplicateError) { + return reply.code(409).send({ error: duplicateError }) + } + const targetUserId = targetType === "user" ? bodyString(body, "targetUserId") : null const targetTeamId = targetType === "team" ? bodyNumber(body, "targetTeamId") : null const targetBranchId = targetType === "branch" ? bodyNumber(body, "targetBranchId") : null @@ -1083,6 +1296,11 @@ export default async function telephonyRoutes(server: FastifyInstance) { return reply.code(404).send({ error: "Nebenstelle nicht gefunden." }) } + const duplicateError = await validateExtensionDuplicate(tenantId, extension, params.id) + if (duplicateError) { + return reply.code(409).send({ error: duplicateError }) + } + const sipPassword = bodyString(body, "sipPassword") const [updated] = await server.db .update(telephonyExtensions) diff --git a/docker-compose.yml b/docker-compose.yml index a5d3703..8eddd42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,19 +58,15 @@ services: - WEB_PUSH_SUBJECT=${WEB_PUSH_SUBJECT:-mailto:admin@example.com} - NODE_EXPORTER_URL=${NODE_EXPORTER_URL:-http://node-exporter:9100} - TELEPHONY_ENABLED=${TELEPHONY_ENABLED:-false} - - TELEPHONY_ASTERISK_HTTP_URL=${TELEPHONY_ASTERISK_HTTP_URL:-http://asterisk-dev:8088/ws} - - TELEPHONY_ASTERISK_WS_URL=${TELEPHONY_ASTERISK_WS_URL:-ws://localhost:8088/ws} + - TELEPHONY_ASTERISK_HTTP_URL=${TELEPHONY_ASTERISK_HTTP_URL:-} + - TELEPHONY_ASTERISK_WS_URL=${TELEPHONY_ASTERISK_WS_URL:-} - TELEPHONY_SIP_DOMAIN=${TELEPHONY_SIP_DOMAIN:-localhost} - - TELEPHONY_TEST_EXTENSION=${TELEPHONY_TEST_EXTENSION:-1001} - - TELEPHONY_TEST_PASSWORD=${TELEPHONY_TEST_PASSWORD:-fedeo-test-1001} - - TELEPHONY_TEST_EXTENSION_2=${TELEPHONY_TEST_EXTENSION_2:-1002} - - TELEPHONY_TEST_PASSWORD_2=${TELEPHONY_TEST_PASSWORD_2:-fedeo-test-1002} - TELEPHONY_ECHO_EXTENSION=${TELEPHONY_ECHO_EXTENSION:-600} - TELEPHONY_EXTERNAL_PROVIDER=${TELEPHONY_EXTERNAL_PROVIDER:-} - TELEPHONY_EXTERNAL_ENABLED=${TELEPHONY_EXTERNAL_ENABLED:-false} - TELEPHONY_EXTERNAL_INBOUND_EXTENSION=${TELEPHONY_EXTERNAL_INBOUND_EXTENSION:-1001} - TELEPHONY_ASTERISK_GENERATED_DIR=${TELEPHONY_ASTERISK_GENERATED_DIR:-/var/lib/fedeo/asterisk/generated} - - TELEPHONY_ASTERISK_AMI_HOST=${TELEPHONY_ASTERISK_AMI_HOST:-asterisk-dev} + - TELEPHONY_ASTERISK_AMI_HOST=${TELEPHONY_ASTERISK_AMI_HOST:-} - TELEPHONY_ASTERISK_AMI_PORT=${TELEPHONY_ASTERISK_AMI_PORT:-5038} - TELEPHONY_ASTERISK_AMI_USER=${TELEPHONY_ASTERISK_AMI_USER:-fedeo} - TELEPHONY_ASTERISK_AMI_PASSWORD=${TELEPHONY_ASTERISK_AMI_PASSWORD:-fedeo-ami-dev} @@ -111,39 +107,6 @@ services: networks: - traefik - asterisk-dev: - image: ${ASTERISK_IMAGE:-andrius/asterisk:20} - restart: unless-stopped - profiles: - - telephony-dev - environment: - - TELEPHONY_TELEKOM_ENABLED=${TELEPHONY_TELEKOM_ENABLED:-false} - - TELEPHONY_TELEKOM_REGISTRAR=${TELEPHONY_TELEKOM_REGISTRAR:-tel.t-online.de} - - TELEPHONY_TELEKOM_SIP_USER=${TELEPHONY_TELEKOM_SIP_USER:-} - - TELEPHONY_TELEKOM_AUTH_USER=${TELEPHONY_TELEKOM_AUTH_USER:-} - - TELEPHONY_TELEKOM_PASSWORD=${TELEPHONY_TELEKOM_PASSWORD:-} - - TELEPHONY_TELEKOM_CALLER_ID=${TELEPHONY_TELEKOM_CALLER_ID:-} - - TELEPHONY_TELEKOM_INBOUND_EXTENSION=${TELEPHONY_TELEKOM_INBOUND_EXTENSION:-1001} - - TELEPHONY_TELEKOM_OUTBOUND_PREFIX=${TELEPHONY_TELEKOM_OUTBOUND_PREFIX:-0} - - TELEPHONY_ASTERISK_EXTERNAL_SIGNALING_ADDRESS=${TELEPHONY_ASTERISK_EXTERNAL_SIGNALING_ADDRESS:-} - - TELEPHONY_ASTERISK_EXTERNAL_MEDIA_ADDRESS=${TELEPHONY_ASTERISK_EXTERNAL_MEDIA_ADDRESS:-} - - ASTERISK_GENERATED_DIR=/etc/asterisk/generated - command: - - /bin/sh - - -c - - /usr/local/bin/render-asterisk-config.sh && asterisk -f - volumes: - - ./telephony/asterisk:/etc/asterisk - - ./telephony/generated:/etc/asterisk/generated - - ./telephony/render-asterisk-config.sh:/usr/local/bin/render-asterisk-config.sh:ro - ports: - - "${TELEPHONY_DEV_WS_PORT:-8088}:8088" - - "${TELEPHONY_DEV_AMI_PORT:-5038}:5038" - - "${TELEPHONY_DEV_SIP_PORT:-5060}:5060/udp" - - "${TELEPHONY_DEV_RTP_MIN_PORT:-10000}-${TELEPHONY_DEV_RTP_MAX_PORT:-10100}:10000-10100/udp" - networks: - - traefik - matrix-db: image: postgres:16-alpine restart: unless-stopped diff --git a/frontend/components/EntityEdit.vue b/frontend/components/EntityEdit.vue index ac771bf..98b39c3 100644 --- a/frontend/components/EntityEdit.vue +++ b/frontend/components/EntityEdit.vue @@ -400,6 +400,32 @@ const canArchiveItem = computed(() => { return true }) +const telephonyExtensionTarget = computed(() => { + if (!item.value?.id) return null + + if (type === "teams") { + return { + targetType: "team", + targetId: item.value.id, + displayName: item.value.name || "Team", + title: "Telefonie", + description: "Lege fest, unter welcher Nebenstelle dieses Team erreichbar ist." + } + } + + if (type === "branches") { + return { + targetType: "branch", + targetId: item.value.id, + displayName: item.value.name || "Niederlassung", + title: "Telefonie", + description: "Lege fest, unter welcher Nebenstelle diese Niederlassung erreichbar ist." + } + } + + return null +}) + const createItem = async () => { let ret = null @@ -1036,6 +1062,16 @@ const updateItem = async () => { + + diff --git a/frontend/components/MainNav.vue b/frontend/components/MainNav.vue index e5d9314..792c3ac 100644 --- a/frontend/components/MainNav.vue +++ b/frontend/components/MainNav.vue @@ -85,11 +85,6 @@ const links = computed(() => { to: "/communication/phone", icon: "i-heroicons-phone" }, - { - label: "Telefonie Setup", - to: "/communication/phone-setup", - icon: "i-heroicons-cog-6-tooth" - }, featureEnabled("helpdesk") ? { label: "Helpdesk", to: "/helpdesk", @@ -342,6 +337,11 @@ const links = computed(() => { to: "/settings/tenant", icon: "i-heroicons-building-office", } : null, + { + label: "Telefonie", + to: "/settings/telephony", + icon: "i-heroicons-phone", + }, { label: "Matrix-Setup", to: "/communication", diff --git a/frontend/components/TelephonyExtensionField.vue b/frontend/components/TelephonyExtensionField.vue new file mode 100644 index 0000000..f42729b --- /dev/null +++ b/frontend/components/TelephonyExtensionField.vue @@ -0,0 +1,197 @@ + + + diff --git a/frontend/composables/useTelephonySoftphone.js b/frontend/composables/useTelephonySoftphone.js index 4498991..f9964f8 100644 --- a/frontend/composables/useTelephonySoftphone.js +++ b/frontend/composables/useTelephonySoftphone.js @@ -40,8 +40,8 @@ export const useTelephonySoftphone = () => { }) const selectedAccount = computed(() => - (config.value?.testAccounts || []).find((account) => account.extension === selectedExtension.value) - || config.value?.testAccounts?.[0] + (config.value?.accounts || []).find((account) => account.extension === selectedExtension.value) + || (config.value?.accounts || [])[0] ) const canRegisterSip = computed(() => @@ -324,8 +324,9 @@ export const useTelephonySoftphone = () => { status.value = statusRes lastUpdated.value = new Date() - if (!selectedAccount.value && configRes?.testAccounts?.length) { - selectedExtension.value = configRes.testAccounts[0].extension + const accounts = configRes?.accounts || [] + if (!selectedAccount.value && accounts.length) { + selectedExtension.value = accounts[0].extension } await loadCallHistory() diff --git a/frontend/pages/communication/phone-setup.vue b/frontend/pages/communication/phone-setup.vue index f7c2257..bc03a56 100644 --- a/frontend/pages/communication/phone-setup.vue +++ b/frontend/pages/communication/phone-setup.vue @@ -1,451 +1,7 @@ diff --git a/frontend/pages/communication/phone.vue b/frontend/pages/communication/phone.vue index 6a1a3c8..54d2cde 100644 --- a/frontend/pages/communication/phone.vue +++ b/frontend/pages/communication/phone.vue @@ -137,7 +137,7 @@ onMounted(loadTelephony)
+

+ Für deinen Benutzer ist noch keine Nebenstelle hinterlegt. +

diff --git a/frontend/pages/settings/telephony.vue b/frontend/pages/settings/telephony.vue new file mode 100644 index 0000000..bd49c0e --- /dev/null +++ b/frontend/pages/settings/telephony.vue @@ -0,0 +1,406 @@ + + + diff --git a/frontend/pages/settings/tenant.vue b/frontend/pages/settings/tenant.vue index 340b492..1db196d 100644 --- a/frontend/pages/settings/tenant.vue +++ b/frontend/pages/settings/tenant.vue @@ -562,17 +562,6 @@ const savePlanningBoardConfig = async () => { setupPage() onMounted(() => { loadMcpTokens() - loadTelephonyExtensionOptions() - loadTelephonyExtensions() - loadTelephonyTrunk() -}) - -watch(() => telephonyTrunkForm.provider, async (provider) => { - const defaults = telephonyProviderDefaults[provider] || telephonyProviderDefaults.easybell - if (!telephonyTrunkForm.registrar || ["tel.t-online.de", "voip.easybell.de"].includes(telephonyTrunkForm.registrar)) { - telephonyTrunkForm.registrar = defaults.registrar - } - await loadTelephonyTrunk() }) @@ -727,248 +716,14 @@ watch(() => telephonyTrunkForm.provider, async (provider) => {
-
-
-
-

Telefonie-Trunk

-

Konfiguriere den SIP-Trunk für externe Anrufe über Asterisk.

-
- - {{ telephonyTrunkForm.enabled ? "Aktiv" : "Inaktiv" }} - -
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - Laden - - - Telefonie-Trunk speichern - - - In Asterisk anwenden - -
- - - -
-
-
-

Nebenstellen

-

Ordne Benutzer, Teams und Niederlassungen routbaren Nebenstellen zu.

-
- - Aktualisieren - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - {{ telephonyExtensionForm.id ? "Nebenstelle speichern" : "Nebenstelle anlegen" }} - - - Abbrechen - -
- -
-
-
-
- {{ extension.extension }} - {{ extension.displayName || extension.targetType }} -
-
- {{ extension.targetType }} · Ziele: {{ extension.dialTargets?.length ? extension.dialTargets.join(", ") : "noch keine Benutzer-Nebenstelle" }} -
-
-
- - Bearbeiten - - - Löschen - -
-
-
- Noch keine Nebenstellen angelegt. -
-
-
-
- - +
diff --git a/frontend/pages/staff/profiles/[id].vue b/frontend/pages/staff/profiles/[id].vue index 17e45a2..b2c02bd 100644 --- a/frontend/pages/staff/profiles/[id].vue +++ b/frontend/pages/staff/profiles/[id].vue @@ -445,6 +445,17 @@ onMounted(async () => { + + + diff --git a/telephony/vps-asterisk.env b/telephony/vps-asterisk.env index 4af05b2..80cba2b 100644 --- a/telephony/vps-asterisk.env +++ b/telephony/vps-asterisk.env @@ -11,10 +11,6 @@ TELEPHONY_ASTERISK_AMI_HOST=127.0.0.1 TELEPHONY_ASTERISK_AMI_PORT=5038 TELEPHONY_ASTERISK_AMI_USER=fedeo TELEPHONY_ASTERISK_AMI_PASSWORD=fedeo-ami-dev -TELEPHONY_TEST_EXTENSION=1001 -TELEPHONY_TEST_PASSWORD=fedeo-test-1001 -TELEPHONY_TEST_EXTENSION_2=1002 -TELEPHONY_TEST_PASSWORD_2=fedeo-test-1002 TELEPHONY_EXTERNAL_PROVIDER=easybell TELEPHONY_EXTERNAL_ENABLED=true TELEPHONY_EXTERNAL_INBOUND_EXTENSION=1001