1114 lines
41 KiB
Vue
1114 lines
41 KiB
Vue
<script setup>
|
|
import {
|
|
TAX_EVALUATION_PERIOD_OPTIONS,
|
|
normalizeTaxEvaluationPeriod
|
|
} from "~/composables/useTaxEvaluation"
|
|
|
|
const auth = useAuthStore()
|
|
const toast = useToast()
|
|
const defaultFeatures = {
|
|
objects: true,
|
|
calendar: true,
|
|
contacts: true,
|
|
projects: true,
|
|
vehicles: true,
|
|
contracts: true,
|
|
inventory: true,
|
|
accounting: true,
|
|
timeTracking: true,
|
|
planningBoard: true,
|
|
workingTimeTracking: true,
|
|
dashboard: true,
|
|
historyitems: true,
|
|
tasks: true,
|
|
wiki: true,
|
|
files: true,
|
|
createdletters: true,
|
|
documentboxes: true,
|
|
helpdesk: true,
|
|
email: true,
|
|
members: true,
|
|
customers: true,
|
|
vendors: true,
|
|
contactsList: true,
|
|
staffTime: true,
|
|
createDocument: true,
|
|
serialInvoice: true,
|
|
incomingInvoices: true,
|
|
outgoingsepamandates: true,
|
|
costcentres: true,
|
|
branches: true,
|
|
teams: true,
|
|
accounts: true,
|
|
ownaccounts: true,
|
|
banking: true,
|
|
spaces: true,
|
|
customerspaces: true,
|
|
customerinventoryitems: true,
|
|
inventoryitems: true,
|
|
inventoryitemgroups: true,
|
|
products: true,
|
|
productcategories: true,
|
|
services: true,
|
|
servicecategories: true,
|
|
memberrelations: true,
|
|
staffProfiles: true,
|
|
hourrates: true,
|
|
projecttypes: true,
|
|
contracttypes: true,
|
|
plants: true,
|
|
settingsNumberRanges: true,
|
|
settingsEmailAccounts: true,
|
|
settingsBanking: true,
|
|
settingsTexttemplates: true,
|
|
settingsTenant: true,
|
|
export: true,
|
|
}
|
|
const featureOptions = [
|
|
{ key: "dashboard", label: "Dashboard" },
|
|
{ key: "historyitems", label: "Logbuch" },
|
|
{ key: "tasks", label: "Aufgaben" },
|
|
{ key: "planningBoard", label: "Plantafel" },
|
|
{ key: "wiki", label: "Wiki" },
|
|
{ key: "files", label: "Dateien" },
|
|
{ key: "createdletters", label: "Anschreiben" },
|
|
{ key: "documentboxes", label: "Boxen" },
|
|
{ key: "helpdesk", label: "Helpdesk" },
|
|
{ key: "email", label: "E-Mail" },
|
|
{ key: "members", label: "Mitglieder" },
|
|
{ key: "customers", label: "Kunden" },
|
|
{ key: "vendors", label: "Lieferanten" },
|
|
{ key: "contactsList", label: "Ansprechpartner" },
|
|
{ key: "staffTime", label: "Mitarbeiter: Zeiten" },
|
|
{ key: "createDocument", label: "Buchhaltung: Ausgangsbelege" },
|
|
{ key: "serialInvoice", label: "Buchhaltung: Serienvorlagen" },
|
|
{ key: "incomingInvoices", label: "Buchhaltung: Eingangsbelege" },
|
|
{ key: "outgoingsepamandates", label: "Buchhaltung: Ausgehende SEPA-Mandate" },
|
|
{ key: "costcentres", label: "Buchhaltung: Kostenstellen" },
|
|
{ key: "branches", label: "Stammdaten: Niederlassungen" },
|
|
{ key: "teams", label: "Mitarbeiter: Teams" },
|
|
{ key: "accounts", label: "Buchhaltung: Buchungskonten" },
|
|
{ key: "ownaccounts", label: "Buchhaltung: Zusätzliche Buchungskonten" },
|
|
{ key: "banking", label: "Buchhaltung: Bank" },
|
|
{ key: "spaces", label: "Lagerplätze" },
|
|
{ key: "customerspaces", label: "Kundenlagerplätze" },
|
|
{ key: "customerinventoryitems", label: "Kundeninventar" },
|
|
{ key: "inventoryitems", label: "Inventar" },
|
|
{ key: "inventoryitemgroups", label: "Inventargruppen" },
|
|
{ key: "products", label: "Stammdaten: Artikel" },
|
|
{ key: "productcategories", label: "Stammdaten: Artikelkategorien" },
|
|
{ key: "services", label: "Stammdaten: Leistungen" },
|
|
{ key: "servicecategories", label: "Stammdaten: Leistungskategorien" },
|
|
{ key: "memberrelations", label: "Stammdaten: Mitgliedsverhältnisse" },
|
|
{ key: "staffProfiles", label: "Mitarbeiter: Mitarbeiterprofile" },
|
|
{ key: "hourrates", label: "Stammdaten: Stundensätze" },
|
|
{ key: "projecttypes", label: "Stammdaten: Projekttypen" },
|
|
{ key: "contracttypes", label: "Stammdaten: Vertragstypen" },
|
|
{ key: "vehicles", label: "Stammdaten: Fahrzeuge" },
|
|
{ key: "projects", label: "Projekte" },
|
|
{ key: "contracts", label: "Verträge" },
|
|
{ key: "plants", label: "Objekte" },
|
|
{ key: "settingsNumberRanges", label: "Einstellungen: Nummernkreise" },
|
|
{ key: "settingsEmailAccounts", label: "Einstellungen: E-Mail Konten" },
|
|
{ key: "settingsBanking", label: "Einstellungen: Bankkonten" },
|
|
{ key: "settingsTexttemplates", label: "Einstellungen: Textvorlagen" },
|
|
{ key: "settingsTenant", label: "Einstellungen: Firmeneinstellungen" },
|
|
{ key: "export", label: "Einstellungen: Export" },
|
|
]
|
|
|
|
const itemInfo = ref({
|
|
features: {},
|
|
businessInfo: {},
|
|
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)
|
|
const mcpTokenCreating = ref(false)
|
|
const mcpTokenDeletingId = ref(null)
|
|
const createdMcpToken = ref("")
|
|
const mcpTokenForm = reactive({
|
|
name: "Codex MCP Token",
|
|
expiresAt: ""
|
|
})
|
|
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",
|
|
title: "Easybell SIP-Trunk",
|
|
description: "Nutze SIP-Benutzername und SIP-Passwort aus dem Easybell-Kundenportal. Falls dort eine Kennung wie K... angezeigt wird, trage sie als Auth-User ein. Die Absendernummer ist die Stammnummer im internationalen Format ohne führende 00."
|
|
},
|
|
telekom: {
|
|
registrar: "tel.t-online.de",
|
|
title: "Telekom Zugangsdaten",
|
|
description: "Die SIP-ID ist meistens deine Rufnummer mit Vorwahl ohne Leerzeichen und Sonderzeichen. Falls dein Anschluss die klassischen Zugangsdaten nutzt, kannst du den Auth-User aus Anschlusskennung, Zugangsnummer, #, Mitbenutzernummer und @t-online.de bilden."
|
|
}
|
|
}
|
|
const telephonyTrunkForm = reactive({
|
|
provider: "easybell",
|
|
enabled: false,
|
|
registrar: "voip.easybell.de",
|
|
sipUser: "",
|
|
authUser: "",
|
|
password: "",
|
|
passwordConfigured: false,
|
|
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 || {}) })
|
|
const businessInfo = ref(auth.activeTenantData.businessInfo)
|
|
const accountChart = ref(auth.activeTenantData.accountChart || "skr03")
|
|
const taxEvaluationPeriod = ref(normalizeTaxEvaluationPeriod(auth.activeTenantData.taxEvaluationPeriod))
|
|
const accountChartOptions = [
|
|
{ label: "SKR 03", value: "skr03" },
|
|
{ label: "Verein", value: "verein" }
|
|
]
|
|
|
|
const updateTenant = async (newData) => {
|
|
|
|
const res = await useNuxtApp().$api(`/api/tenant/other/${auth.activeTenant}`, {
|
|
method: "PUT",
|
|
body: {
|
|
data: newData,
|
|
}
|
|
})
|
|
|
|
if (res) {
|
|
itemInfo.value = res
|
|
auth.activeTenantData = res
|
|
features.value = { ...defaultFeatures, ...(res?.features || {}) }
|
|
taxEvaluationPeriod.value = normalizeTaxEvaluationPeriod(res?.taxEvaluationPeriod)
|
|
}
|
|
}
|
|
const saveFeatures = async () => {
|
|
await updateTenant({features: features.value})
|
|
}
|
|
|
|
const formatMcpTokenDate = (value) => {
|
|
if (!value) return "Nie"
|
|
|
|
return new Date(value).toLocaleString("de-DE", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit"
|
|
})
|
|
}
|
|
|
|
const loadMcpTokens = async () => {
|
|
if (!canManageMcpTokens.value) return
|
|
|
|
mcpTokensLoading.value = true
|
|
try {
|
|
const res = await useNuxtApp().$api("/api/mcp/tokens")
|
|
mcpTokens.value = res?.rows || []
|
|
} catch (error) {
|
|
toast.add({ title: "MCP Tokens konnten nicht geladen werden", color: "error" })
|
|
} finally {
|
|
mcpTokensLoading.value = false
|
|
}
|
|
}
|
|
|
|
const loadTelephonyTrunk = async () => {
|
|
telephonyTrunkLoading.value = true
|
|
|
|
try {
|
|
const res = await useNuxtApp().$api(`/api/telephony/trunk-config?provider=${telephonyTrunkForm.provider}`)
|
|
telephonyTrunkForm.provider = res?.provider || telephonyTrunkForm.provider || "easybell"
|
|
telephonyTrunkForm.enabled = Boolean(res?.enabled)
|
|
telephonyTrunkForm.registrar = res?.registrar || activeTelephonyProvider.value.registrar
|
|
telephonyTrunkForm.sipUser = res?.sipUser || ""
|
|
telephonyTrunkForm.authUser = res?.authUser || ""
|
|
telephonyTrunkForm.password = ""
|
|
telephonyTrunkForm.passwordConfigured = Boolean(res?.passwordConfigured)
|
|
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 || ""
|
|
telephonyTrunkForm.localNetworks = res?.localNetworks || "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8"
|
|
} catch (error) {
|
|
toast.add({ title: "Telefonie-Trunk konnte nicht geladen werden", color: "error" })
|
|
} finally {
|
|
telephonyTrunkLoading.value = false
|
|
}
|
|
}
|
|
|
|
const saveTelephonyTrunk = async () => {
|
|
if (telephonyTrunkForm.enabled && (!telephonyTrunkForm.sipUser?.trim() || (!telephonyTrunkForm.password?.trim() && !telephonyTrunkForm.passwordConfigured))) {
|
|
toast.add({
|
|
title: "Trunk-Zugang unvollständig",
|
|
description: "Bitte gib mindestens SIP-ID und Kennwort an.",
|
|
color: "orange"
|
|
})
|
|
return
|
|
}
|
|
|
|
telephonyTrunkSaving.value = true
|
|
|
|
try {
|
|
const res = await useNuxtApp().$api("/api/telephony/trunk-config", {
|
|
method: "PUT",
|
|
body: {
|
|
provider: telephonyTrunkForm.provider,
|
|
enabled: telephonyTrunkForm.enabled,
|
|
registrar: telephonyTrunkForm.registrar,
|
|
sipUser: telephonyTrunkForm.sipUser,
|
|
authUser: telephonyTrunkForm.authUser,
|
|
password: telephonyTrunkForm.password,
|
|
clearPassword: telephonyTrunkForm.clearPassword,
|
|
callerId: telephonyTrunkForm.callerId,
|
|
inboundExtension: telephonyTrunkForm.inboundExtension,
|
|
defaultRouteExtensionId: telephonyTrunkForm.defaultRouteExtensionId,
|
|
outboundPrefix: telephonyTrunkForm.outboundPrefix,
|
|
externalSignalingAddress: telephonyTrunkForm.externalSignalingAddress,
|
|
externalMediaAddress: telephonyTrunkForm.externalMediaAddress,
|
|
localNetworks: telephonyTrunkForm.localNetworks
|
|
}
|
|
})
|
|
|
|
telephonyTrunkForm.password = ""
|
|
telephonyTrunkForm.passwordConfigured = Boolean(res?.passwordConfigured)
|
|
telephonyTrunkForm.clearPassword = false
|
|
toast.add({ title: "Telefonie-Trunk gespeichert", color: "success" })
|
|
} catch (error) {
|
|
toast.add({
|
|
title: "Telefonie-Trunk konnte nicht gespeichert werden",
|
|
description: error?.data?.error || error?.message,
|
|
color: "error"
|
|
})
|
|
} finally {
|
|
telephonyTrunkSaving.value = false
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
try {
|
|
const res = await useNuxtApp().$api("/api/telephony/trunk-config/apply", {
|
|
method: "POST"
|
|
})
|
|
|
|
toast.add({
|
|
title: res?.warning ? "Trunk-Konfiguration geschrieben" : "Telefonie-Trunk angewendet",
|
|
description: res?.warning || (res?.status?.registered ? "Trunk-Registration ist aktiv." : "Asterisk wurde neu geladen."),
|
|
color: res?.warning ? "orange" : "success"
|
|
})
|
|
} catch (error) {
|
|
toast.add({
|
|
title: "Telefonie-Trunk konnte nicht angewendet werden",
|
|
description: error?.data?.error || error?.message,
|
|
color: "error"
|
|
})
|
|
} finally {
|
|
telephonyTrunkApplying.value = false
|
|
}
|
|
}
|
|
|
|
const createMcpToken = async () => {
|
|
if (!mcpTokenForm.name?.trim()) {
|
|
toast.add({ title: "Name fehlt", description: "Bitte gib einen Namen für den Token an.", color: "orange" })
|
|
return
|
|
}
|
|
|
|
mcpTokenCreating.value = true
|
|
createdMcpToken.value = ""
|
|
|
|
try {
|
|
const res = await useNuxtApp().$api("/api/mcp/tokens", {
|
|
method: "POST",
|
|
body: {
|
|
name: mcpTokenForm.name.trim(),
|
|
expiresAt: mcpTokenForm.expiresAt || null
|
|
}
|
|
})
|
|
|
|
createdMcpToken.value = res?.token || ""
|
|
toast.add({ title: "MCP Token erstellt", color: "success" })
|
|
await loadMcpTokens()
|
|
} catch (error) {
|
|
toast.add({ title: "MCP Token konnte nicht erstellt werden", color: "error" })
|
|
} finally {
|
|
mcpTokenCreating.value = false
|
|
}
|
|
}
|
|
|
|
const copyCreatedMcpToken = async () => {
|
|
if (!createdMcpToken.value) return
|
|
|
|
await navigator.clipboard.writeText(createdMcpToken.value)
|
|
toast.add({ title: "Token kopiert", color: "success" })
|
|
}
|
|
|
|
const deactivateMcpToken = async (token) => {
|
|
if (!token?.id || !confirm(`MCP Token "${token.name}" deaktivieren?`)) return
|
|
|
|
mcpTokenDeletingId.value = token.id
|
|
try {
|
|
await useNuxtApp().$api(`/api/mcp/tokens/${token.id}`, {
|
|
method: "DELETE"
|
|
})
|
|
toast.add({ title: "MCP Token deaktiviert", color: "success" })
|
|
await loadMcpTokens()
|
|
} catch (error) {
|
|
toast.add({ title: "MCP Token konnte nicht deaktiviert werden", color: "error" })
|
|
} finally {
|
|
mcpTokenDeletingId.value = null
|
|
}
|
|
}
|
|
|
|
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()
|
|
})
|
|
|
|
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()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<UDashboardNavbar title="Firmeneinstellungen">
|
|
|
|
</UDashboardNavbar>
|
|
<UTabs
|
|
class="p-5"
|
|
:items="[
|
|
{
|
|
label: 'Dokubox'
|
|
},{
|
|
label: 'Rechnung & Kontakt'
|
|
},{
|
|
label: 'Plantafel'
|
|
},{
|
|
label: 'Integrationen'
|
|
},{
|
|
label: 'Funktionen'
|
|
}
|
|
]"
|
|
>
|
|
<template #content="{item}">
|
|
<div v-if="item.label === 'Dokubox'">
|
|
<UAlert
|
|
class="mt-5"
|
|
title="DOKUBOX"
|
|
color="neutral"
|
|
variant="outline"
|
|
>
|
|
<template #description>
|
|
<p>Die Dokubox ist eine E-Mail Inbox um deine Anhänge direkt als Dokumente zu importieren. Leite Deine E-Mails einfach an die folgende E-Mail Adresse weiter, diese ist für dein Unternehmen einzigartig. Die E-Mails werden dann alle 5 min abgerufen.</p>
|
|
<br><br>
|
|
<a
|
|
v-if="itemInfo.id"
|
|
:href="`mailto:${itemInfo.dokuboxkey}@fedeo-dokubox.de`"
|
|
>
|
|
{{itemInfo.dokuboxkey}}@fedeo-dokubox.de
|
|
</a>
|
|
</template>
|
|
</UAlert>
|
|
|
|
</div>
|
|
<div v-if="item.label === 'Rechnung & Kontakt'">
|
|
<UCard class="mt-5">
|
|
<UForm class="w-1/2">
|
|
<UFormField
|
|
label="Firmenname:"
|
|
>
|
|
<UInput v-model="businessInfo.name"/>
|
|
</UFormField>
|
|
<UFormField
|
|
label="Straße + Hausnummer:"
|
|
>
|
|
<UInput v-model="businessInfo.street"/>
|
|
</UFormField>
|
|
<UFormField
|
|
label="PLZ + Ort"
|
|
class="w-full"
|
|
>
|
|
<InputGroup class="w-full">
|
|
<UInput v-model="businessInfo.zip"/>
|
|
<UInput v-model="businessInfo.city" class="flex-auto"/>
|
|
</InputGroup>
|
|
</UFormField>
|
|
<UButton
|
|
class="mt-3"
|
|
@click="updateTenant({businessInfo: businessInfo})"
|
|
>
|
|
Speichern
|
|
</UButton>
|
|
<UFormField
|
|
label="Kontenrahmen:"
|
|
class="mt-6"
|
|
>
|
|
<USelectMenu
|
|
v-model="accountChart"
|
|
:items="accountChartOptions"
|
|
label-key="label"
|
|
value-key="value"
|
|
/>
|
|
</UFormField>
|
|
<UButton
|
|
class="mt-3"
|
|
@click="updateTenant({accountChart: accountChart})"
|
|
>
|
|
Kontenrahmen speichern
|
|
</UButton>
|
|
<UFormField
|
|
label="USt-Auswertung:"
|
|
class="mt-6"
|
|
>
|
|
<USelectMenu
|
|
v-model="taxEvaluationPeriod"
|
|
:items="TAX_EVALUATION_PERIOD_OPTIONS"
|
|
label-key="label"
|
|
value-key="value"
|
|
/>
|
|
</UFormField>
|
|
<UButton
|
|
class="mt-3"
|
|
@click="updateTenant({taxEvaluationPeriod: taxEvaluationPeriod})"
|
|
>
|
|
Zeitraum speichern
|
|
</UButton>
|
|
</UForm>
|
|
</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">
|
|
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
|
<div>
|
|
<h3 class="text-base font-semibold text-highlighted">Telefonie-Trunk</h3>
|
|
<p class="text-sm text-muted">Konfiguriere den SIP-Trunk für externe Anrufe über Asterisk.</p>
|
|
</div>
|
|
<UBadge :color="telephonyTrunkForm.enabled ? 'success' : 'neutral'" variant="soft">
|
|
{{ telephonyTrunkForm.enabled ? "Aktiv" : "Inaktiv" }}
|
|
</UBadge>
|
|
</div>
|
|
|
|
<UAlert
|
|
:title="activeTelephonyProvider.title"
|
|
color="neutral"
|
|
variant="outline"
|
|
>
|
|
<template #description>
|
|
<p class="text-sm">
|
|
{{ activeTelephonyProvider.description }}
|
|
</p>
|
|
</template>
|
|
</UAlert>
|
|
|
|
<div class="grid gap-4 md:grid-cols-2">
|
|
<UFormField label="Provider">
|
|
<USelectMenu
|
|
v-model="telephonyTrunkForm.provider"
|
|
:items="telephonyProviderOptions"
|
|
label-key="label"
|
|
value-key="value"
|
|
/>
|
|
</UFormField>
|
|
<UFormField label="Trunk aktivieren">
|
|
<USwitch v-model="telephonyTrunkForm.enabled" />
|
|
</UFormField>
|
|
<UFormField label="Registrar">
|
|
<UInput v-model="telephonyTrunkForm.registrar" :placeholder="activeTelephonyProvider.registrar" />
|
|
</UFormField>
|
|
<UFormField label="SIP-ID / Rufnummer">
|
|
<UInput v-model="telephonyTrunkForm.sipUser" placeholder="SIP-Benutzername" />
|
|
</UFormField>
|
|
<UFormField label="Auth-User">
|
|
<UInput v-model="telephonyTrunkForm.authUser" placeholder="Optional" />
|
|
</UFormField>
|
|
<UFormField :label="telephonyTrunkForm.passwordConfigured ? 'Kennwort ersetzen' : 'Kennwort'">
|
|
<UInput
|
|
v-model="telephonyTrunkForm.password"
|
|
type="password"
|
|
:placeholder="telephonyTrunkForm.passwordConfigured ? 'Bereits gespeichert' : 'SIP-Kennwort'"
|
|
/>
|
|
</UFormField>
|
|
<UFormField label="Gespeichertes Kennwort löschen">
|
|
<USwitch
|
|
v-model="telephonyTrunkForm.clearPassword"
|
|
:disabled="!telephonyTrunkForm.passwordConfigured"
|
|
/>
|
|
</UFormField>
|
|
<UFormField label="Absendernummer">
|
|
<UInput v-model="telephonyTrunkForm.callerId" placeholder="Optional, z. B. 49301234567" />
|
|
</UFormField>
|
|
<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>
|
|
<UFormField label="Öffentliche Signaling-Adresse">
|
|
<UInput v-model="telephonyTrunkForm.externalSignalingAddress" placeholder="Öffentliche IP oder DNS-Name" />
|
|
</UFormField>
|
|
<UFormField label="Öffentliche Medien-Adresse">
|
|
<UInput v-model="telephonyTrunkForm.externalMediaAddress" placeholder="Leer = Signaling-Adresse" />
|
|
</UFormField>
|
|
<UFormField label="Lokale Netze">
|
|
<UInput v-model="telephonyTrunkForm.localNetworks" placeholder="172.16.0.0/12,192.168.0.0/16,10.0.0.0/8" />
|
|
</UFormField>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
<UButton
|
|
icon="i-heroicons-arrow-path"
|
|
variant="outline"
|
|
:loading="telephonyTrunkLoading"
|
|
@click="loadTelephonyTrunk"
|
|
>
|
|
Laden
|
|
</UButton>
|
|
<UButton
|
|
icon="i-heroicons-check"
|
|
:loading="telephonyTrunkSaving"
|
|
@click="saveTelephonyTrunk"
|
|
>
|
|
Telefonie-Trunk speichern
|
|
</UButton>
|
|
<UButton
|
|
icon="i-heroicons-arrow-path-rounded-square"
|
|
color="primary"
|
|
variant="soft"
|
|
:loading="telephonyTrunkApplying"
|
|
@click="applyTelephonyTrunk"
|
|
>
|
|
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" />
|
|
|
|
<div class="space-y-6">
|
|
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
|
<div>
|
|
<h3 class="text-base font-semibold text-highlighted">MCP Tokens</h3>
|
|
<p class="text-sm text-muted">Verwalte dauerhafte Tokens für Codex und andere MCP Clients.</p>
|
|
</div>
|
|
<UButton
|
|
icon="i-heroicons-arrow-path"
|
|
variant="outline"
|
|
:loading="mcpTokensLoading"
|
|
:disabled="!canManageMcpTokens"
|
|
@click="loadMcpTokens"
|
|
>
|
|
Aktualisieren
|
|
</UButton>
|
|
</div>
|
|
|
|
<UAlert
|
|
v-if="!canManageMcpTokens"
|
|
title="Keine Berechtigung"
|
|
description="Du benötigst Adminrechte oder die Berechtigung mcp.tokens.write."
|
|
color="orange"
|
|
variant="outline"
|
|
/>
|
|
|
|
<div v-else class="space-y-6">
|
|
<div class="grid gap-4 md:grid-cols-3">
|
|
<UFormField label="Name" class="md:col-span-2">
|
|
<UInput v-model="mcpTokenForm.name" placeholder="z.B. Codex MCP Token" />
|
|
</UFormField>
|
|
<UFormField label="Ablaufdatum">
|
|
<UInput v-model="mcpTokenForm.expiresAt" type="date" />
|
|
</UFormField>
|
|
</div>
|
|
|
|
<UButton
|
|
icon="i-heroicons-plus"
|
|
:loading="mcpTokenCreating"
|
|
@click="createMcpToken"
|
|
>
|
|
Token erstellen
|
|
</UButton>
|
|
|
|
<UAlert
|
|
v-if="createdMcpToken"
|
|
title="Token wurde erstellt"
|
|
description="Der Token wird nur jetzt vollständig angezeigt."
|
|
color="success"
|
|
variant="outline"
|
|
>
|
|
<template #description>
|
|
<div class="mt-3 space-y-3">
|
|
<UTextarea
|
|
:model-value="createdMcpToken"
|
|
readonly
|
|
autoresize
|
|
/>
|
|
<UButton
|
|
icon="i-heroicons-clipboard-document"
|
|
variant="outline"
|
|
@click="copyCreatedMcpToken"
|
|
>
|
|
Token kopieren
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
</UAlert>
|
|
|
|
<div class="overflow-x-auto rounded-md border border-default">
|
|
<table class="min-w-full divide-y divide-default text-sm">
|
|
<thead class="bg-muted">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left font-medium">Name</th>
|
|
<th class="px-4 py-3 text-left font-medium">Prefix</th>
|
|
<th class="px-4 py-3 text-left font-medium">Erstellt</th>
|
|
<th class="px-4 py-3 text-left font-medium">Zuletzt genutzt</th>
|
|
<th class="px-4 py-3 text-left font-medium">Läuft ab</th>
|
|
<th class="px-4 py-3 text-left font-medium">Status</th>
|
|
<th class="px-4 py-3 text-right font-medium">Aktion</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-default">
|
|
<tr v-if="!mcpTokensLoading && !mcpTokens.length">
|
|
<td colspan="7" class="px-4 py-6 text-center text-muted">Noch keine MCP Tokens vorhanden.</td>
|
|
</tr>
|
|
<tr v-for="token in mcpTokens" :key="token.id">
|
|
<td class="px-4 py-3">{{ token.name }}</td>
|
|
<td class="px-4 py-3 font-mono text-xs">{{ token.keyPrefix }}</td>
|
|
<td class="px-4 py-3">{{ formatMcpTokenDate(token.createdAt) }}</td>
|
|
<td class="px-4 py-3">{{ formatMcpTokenDate(token.lastUsedAt) }}</td>
|
|
<td class="px-4 py-3">{{ formatMcpTokenDate(token.expiresAt) }}</td>
|
|
<td class="px-4 py-3">
|
|
<UBadge :color="token.active ? 'success' : 'neutral'" variant="soft">
|
|
{{ token.active ? "Aktiv" : "Inaktiv" }}
|
|
</UBadge>
|
|
</td>
|
|
<td class="px-4 py-3 text-right">
|
|
<UButton
|
|
v-if="token.active"
|
|
icon="i-heroicons-archive-box"
|
|
color="error"
|
|
variant="ghost"
|
|
:loading="mcpTokenDeletingId === token.id"
|
|
@click="deactivateMcpToken(token)"
|
|
>
|
|
Deaktivieren
|
|
</UButton>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
<div v-else-if="item.label === 'Funktionen'">
|
|
<UCard class="mt-5">
|
|
<UAlert
|
|
title="Funktionen ausblenden"
|
|
description="Nur Funktionen mit gesetztem Haken sind im Unternehmen verfügbar. Diese Einstellungen gelten für alle Mitarbeiter und sind unabhängig von Berechtigungen."
|
|
color="error"
|
|
variant="outline"
|
|
class="mb-5"
|
|
/>
|
|
<UCheckbox
|
|
v-for="option in featureOptions"
|
|
:key="option.key"
|
|
:label="option.label"
|
|
v-model="features[option.key]"
|
|
@change="saveFeatures"
|
|
/>
|
|
</UCard>
|
|
</div>
|
|
</template>
|
|
</UTabs>
|
|
</template>
|
|
|
|
<style scoped>
|
|
|
|
</style>
|