diff --git a/src/components/CopyableCredential.jsx b/src/components/CopyableCredential.jsx
index 3369156..023081d 100644
--- a/src/components/CopyableCredential.jsx
+++ b/src/components/CopyableCredential.jsx
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react'
import { FaCheck, FaCopy, FaEye, FaEyeSlash } from 'react-icons/fa6'
-async function copyText(text) {
+export async function copyText(text) {
try {
await navigator.clipboard.writeText(text)
} catch {
diff --git a/src/components/customer/CustomerFunktionen.jsx b/src/components/customer/CustomerFunktionen.jsx
index dac901d..2b4219c 100644
--- a/src/components/customer/CustomerFunktionen.jsx
+++ b/src/components/customer/CustomerFunktionen.jsx
@@ -1,14 +1,37 @@
-import { Fragment, useCallback, useEffect, useState } from 'react'
+import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { Link } from 'react-router-dom'
-import { FaSpinner } from 'react-icons/fa6'
+import { FaCheck, FaCopy, FaSpinner } from 'react-icons/fa6'
import { Card, Badge, Button, EmptyState } from '../ui'
+import { copyText } from '../CopyableCredential'
import { getFunktionen, setFeature } from '../../lib/teamApi'
-import { FUNKTIONEN, ZUSTAND, funktionOf, aktionen, mergeFunktion } from '../../lib/funktionenView'
+import { FUNKTIONEN, ZUSTAND, funktionOf, aktionen, mergeFunktion, siteKeyOf } from '../../lib/funktionenView'
import '../../styles/admin-areas.css'
// Reiter "Funktionen": News-Pinnwand und Terminplaner je Projekt an/aus wie im frueheren Admin-Panel (Kundenliste, Zeile "Funktionen")
const short = (text) => (text.length > 60 ? `${text.slice(0, 60)}…` : text)
+// Site-Key des Terminplaners (steht im Einbau-Code der Kundenwebsite): kleine Schrift mit fester Breite und Kopierknopf
+function SiteKey({ value }) {
+ const [copied, setCopied] = useState(false)
+ const timer = useRef(null)
+ useEffect(() => () => clearTimeout(timer.current), [])
+ const copy = async () => {
+ await copyText(value)
+ setCopied(true)
+ clearTimeout(timer.current)
+ timer.current = setTimeout(() => setCopied(false), 1800)
+ }
+ return (
+
+ Site-Key
+ {value}
+
+
+ )
+}
+
export default function CustomerFunktionen({ customerId }) {
const [projects, setProjects] = useState(null)
const [error, setError] = useState(null)
@@ -72,6 +95,7 @@ export default function CustomerFunktionen({ customerId }) {
{label}
{feature.adminNote && <>
{short(feature.adminNote)}>}
+ {siteKeyOf(feature, f.key) && <>
>}
{feature.ticketMissing
diff --git a/src/lib/funktionenView.js b/src/lib/funktionenView.js
index 11c6aed..5fb88de 100644
--- a/src/lib/funktionenView.js
+++ b/src/lib/funktionenView.js
@@ -33,6 +33,15 @@ export const ZUSTAND = {
/** Ansicht einer Funktion im Projekt (Portal: project.features[key]); ohne Eintrag gilt "off". */
export const funktionOf = (project, key) => project?.features?.[key] || { state: 'off' }
+/**
+ * Site-Key (Schluessel fuer den Einbau auf der Kundenwebsite) einer Funktion: nur beim Terminplaner und nur, wenn das Portal
+ * einen nennt (feature.siteKey); sonst ''.
+ */
+export function siteKeyOf(feature, key) {
+ const wert = feature?.siteKey
+ return key === 'termine' && typeof wert === 'string' ? wert.trim() : ''
+}
+
/** Moegliche Aktionen je Zustand: Aktivieren (ohne Zahlung) ausser bei "active", Abschalten solange bestellt, in Einrichtung oder aktiv. */
export function aktionen(state) {
const list = []
diff --git a/src/styles/admin-areas.css b/src/styles/admin-areas.css
index 42d85d9..d35f5cb 100644
--- a/src/styles/admin-areas.css
+++ b/src/styles/admin-areas.css
@@ -22,6 +22,14 @@
.area-inline { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.storage-row { grid-template-columns: 150px 220px minmax(0, 1fr) 150px 110px; }
.funktionen-row { grid-template-columns: minmax(0, 1.2fr) 110px 200px 110px minmax(0, 1.4fr); }
+.funktionen-sitekey { display: inline-flex; align-items: center; gap: 6px; margin-top: 4px; }
+.funktionen-sitekey code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--text); user-select: all; }
+.funktionen-sitekey-copy {
+ display: inline-flex; align-items: center; justify-content: center; width: 25px; height: 25px; flex: 0 0 25px;
+ border: 0; border-radius: 4px; background: transparent; color: var(--text-faint); cursor: pointer;
+}
+.funktionen-sitekey-copy:hover { background: var(--surface-2); color: var(--text); }
+.funktionen-sitekey-ok { color: var(--ok); }
@media (max-width: 720px) {
.area-row { grid-template-columns: minmax(0, 1fr) !important; }
.area-row.head { display: none; }
diff --git a/tests/unit/funktionenView.test.mjs b/tests/unit/funktionenView.test.mjs
index c344787..2437272 100644
--- a/tests/unit/funktionenView.test.mjs
+++ b/tests/unit/funktionenView.test.mjs
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
// Kunden-Reiter "Funktionen": Register der Funktionen (News, Terminplaner), Aktionen je Zustand, Antwort einarbeiten
-const { FUNKTIONEN, ZUSTAND, funktionOf, aktionen, mergeFunktion } = await loadModule('src/lib/funktionenView.js')
+const { FUNKTIONEN, ZUSTAND, funktionOf, aktionen, mergeFunktion, siteKeyOf } = await loadModule('src/lib/funktionenView.js')
test('FUNKTIONEN: News und Terminplaner mit den Bestätigungstexten', () => {
assert.deepEqual(FUNKTIONEN, [
@@ -92,3 +92,12 @@ test('mergeFunktion: Projekt ohne bisherige features bekommt die Funktion', () =
const merged = mergeFunktion([{ projectId: 'p9' }], 'p9', 'termine', { features: { termine: { state: 'setup' } } })
assert.deepEqual(merged, [{ projectId: 'p9', features: { termine: { state: 'setup' } } }])
})
+
+test('siteKeyOf: nur der Terminplaner zeigt seinen Site-Key, nur wenn das Portal einen nennt', () => {
+ assert.equal(siteKeyOf({ state: 'active', siteKey: 'abcdefghijklmnop' }, 'termine'), 'abcdefghijklmnop')
+ assert.equal(siteKeyOf({ state: 'active', siteKey: ' abcdefghijklmnop ' }, 'termine'), 'abcdefghijklmnop', 'getrimmt')
+ assert.equal(siteKeyOf({ state: 'active', siteKey: 'abcdefghijklmnop' }, 'news'), '', 'News: kein Site-Key in der Tabelle')
+ for (const feature of [{ state: 'off' }, { state: 'active', siteKey: null }, { state: 'active', siteKey: '' }, { state: 'active', siteKey: 5 }, null, undefined]) {
+ assert.equal(siteKeyOf(feature, 'termine'), '', JSON.stringify(feature))
+ }
+})