Terminplaner: Abschluss – Site-Key des Terminplaners im Funktionen-Reiter mit Kopierknopf

Die Zeile Terminplaner zeigt den siteKey aus der Antwort des Portals in kleiner Schrift mit fester
Breite und einem Kopierknopf (copyText aus CopyableCredential, mit Ersatzweg ohne Zwischenablage).
Damit braucht der Einbau (Schritt L4 des Pilots) keinen Datenbankzugriff mehr.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-03 01:49:23 +00:00
1 parent 426a5d6654
commit 65e9931f2c
5 files changed
+55 -5

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { FaCheck, FaCopy, FaEye, FaEyeSlash } from 'react-icons/fa6' import { FaCheck, FaCopy, FaEye, FaEyeSlash } from 'react-icons/fa6'
async function copyText(text) { export async function copyText(text) {
try { try {
await navigator.clipboard.writeText(text) await navigator.clipboard.writeText(text)
} catch { } catch {
+27 -3
View File
@@ -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 { 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 { Card, Badge, Button, EmptyState } from '../ui'
import { copyText } from '../CopyableCredential'
import { getFunktionen, setFeature } from '../../lib/teamApi' 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' import '../../styles/admin-areas.css'
// Reiter "Funktionen": News-Pinnwand und Terminplaner je Projekt an/aus wie im frueheren Admin-Panel (Kundenliste, Zeile "Funktionen") // 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) 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 (
<span className="funktionen-sitekey">
<span className="area-muted">Site-Key</span>
<code>{value}</code>
<button type="button" className="funktionen-sitekey-copy" onClick={copy} title={copied ? 'Kopiert' : 'Site-Key kopieren'} aria-label={copied ? 'Site-Key kopiert' : 'Site-Key kopieren'}>
{copied ? <FaCheck className="funktionen-sitekey-ok" /> : <FaCopy />}
</button>
</span>
)
}
export default function CustomerFunktionen({ customerId }) { export default function CustomerFunktionen({ customerId }) {
const [projects, setProjects] = useState(null) const [projects, setProjects] = useState(null)
const [error, setError] = useState(null) const [error, setError] = useState(null)
@@ -72,6 +95,7 @@ export default function CustomerFunktionen({ customerId }) {
<span> <span>
<Badge tone={tone}>{label}</Badge> <Badge tone={tone}>{label}</Badge>
{feature.adminNote && <><br /><span className="area-muted" title={feature.adminNote}>{short(feature.adminNote)}</span></>} {feature.adminNote && <><br /><span className="area-muted" title={feature.adminNote}>{short(feature.adminNote)}</span></>}
{siteKeyOf(feature, f.key) && <><br /><SiteKey value={siteKeyOf(feature, f.key)} /></>}
</span> </span>
<span> <span>
{feature.ticketMissing {feature.ticketMissing
+9
View File
@@ -33,6 +33,15 @@ export const ZUSTAND = {
/** Ansicht einer Funktion im Projekt (Portal: project.features[key]); ohne Eintrag gilt "off". */ /** Ansicht einer Funktion im Projekt (Portal: project.features[key]); ohne Eintrag gilt "off". */
export const funktionOf = (project, key) => project?.features?.[key] || { state: '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. */ /** Moegliche Aktionen je Zustand: Aktivieren (ohne Zahlung) ausser bei "active", Abschalten solange bestellt, in Einrichtung oder aktiv. */
export function aktionen(state) { export function aktionen(state) {
const list = [] const list = []
+8
View File
@@ -22,6 +22,14 @@
.area-inline { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; } .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; } .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-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) { @media (max-width: 720px) {
.area-row { grid-template-columns: minmax(0, 1fr) !important; } .area-row { grid-template-columns: minmax(0, 1fr) !important; }
.area-row.head { display: none; } .area-row.head { display: none; }
+10 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
import { loadModule } from './load.mjs' import { loadModule } from './load.mjs'
// Kunden-Reiter "Funktionen": Register der Funktionen (News, Terminplaner), Aktionen je Zustand, Antwort einarbeiten // 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', () => { test('FUNKTIONEN: News und Terminplaner mit den Bestätigungstexten', () => {
assert.deepEqual(FUNKTIONEN, [ 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' } } }) const merged = mergeFunktion([{ projectId: 'p9' }], 'p9', 'termine', { features: { termine: { state: 'setup' } } })
assert.deepEqual(merged, [{ projectId: 'p9', 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))
}
})