From 65e9931f2cbc3151b93f1638a3dfbbb95c61d9a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 01:49:23 +0000 Subject: [PATCH] =?UTF-8?q?Terminplaner:=20Abschluss=20=E2=80=93=20Site-Ke?= =?UTF-8?q?y=20des=20Terminplaners=20im=20Funktionen-Reiter=20mit=20Kopier?= =?UTF-8?q?knopf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/components/CopyableCredential.jsx | 2 +- .../customer/CustomerFunktionen.jsx | 30 +++++++++++++++++-- src/lib/funktionenView.js | 9 ++++++ src/styles/admin-areas.css | 8 +++++ tests/unit/funktionenView.test.mjs | 11 ++++++- 5 files changed, 55 insertions(+), 5 deletions(-) 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)) + } +})