Files
FEDEO/frontend/pages/settings/tenant.vue

751 lines
27 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 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 telephonyProviderOptions = [
{ label: "Easybell", value: "easybell" },
{ label: "Telekom", value: "telekom" }
]
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",
outboundPrefix: "0",
externalSignalingAddress: "",
externalMediaAddress: "",
localNetworks: "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8"
})
const activeTelephonyProvider = computed(() => telephonyProviderDefaults[telephonyTrunkForm.provider] || telephonyProviderDefaults.easybell)
const setupPage = async () => {
itemInfo.value = auth.activeTenantData
console.log(itemInfo.value)
}
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.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,
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 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
}
}
setupPage()
onMounted(() => {
loadMcpTokens()
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: '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 === '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="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>
</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>