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:
1 parent
426a5d6654
commit
65e9931f2c
5 files changed
+55
-5
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<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 }) {
|
||||
const [projects, setProjects] = useState(null)
|
||||
const [error, setError] = useState(null)
|
||||
@@ -72,6 +95,7 @@ export default function CustomerFunktionen({ customerId }) {
|
||||
<span>
|
||||
<Badge tone={tone}>{label}</Badge>
|
||||
{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>
|
||||
{feature.ticketMissing
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user