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)) + } +})