import { Fragment, useCallback, useEffect, useRef, useState } from 'react' import { Link } from 'react-router-dom' import { FaCheck, FaCopy, FaRotateRight, FaSpinner } from 'react-icons/fa6' import { Card, Badge, Button, EmptyState } from '../ui' import { copyText } from '../CopyableCredential' import { getFunktionen, setFeature, getTermineAnleitung } from '../../lib/teamApi' import { FUNKTIONEN, ZUSTAND, AKTION_LABEL, funktionOf, aktionen, mergeFunktion, siteKeyOf, abgeschaltetText, hindernisText, panelModell, anleitungOffen, } 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} ) } // Kopierknopf mit "Kopiert"-Rueckmeldung (wie SiteKey) function KopierKnopf({ text, label }) { const [copied, setCopied] = useState(false) const timer = useRef(null) useEffect(() => () => clearTimeout(timer.current), []) const copy = async () => { await copyText(text) setCopied(true) clearTimeout(timer.current) timer.current = setTimeout(() => setCopied(false), 1800) } return ( ) } // Einbauanleitung des Terminplaners unter der Zeile. Inhalte und Entscheidungen kommen aus funktionenView (panelModell); Texte // stehen ausschliesslich als React-Textknoten da (nie als HTML oder Link). Laedt beim Oeffnen und bei jedem neuen reloadKey // (nach einer Aktion aendern sich Version und Hindernis) neu. function AnleitungPanel({ customerId, projectId, feature, reloadKey }) { const [antwort, setAntwort] = useState(null) const [fehler, setFehler] = useState(null) const [retry, setRetry] = useState(0) useEffect(() => { let aktuell = true setAntwort(null) setFehler(null) getTermineAnleitung(customerId, projectId) .then((res) => { if (aktuell) setAntwort(res) }) .catch((err) => { if (aktuell) setFehler(err?.message || 'Anleitung konnte nicht geladen werden.') }) return () => { aktuell = false } }, [customerId, projectId, reloadKey, retry]) const hindernis = hindernisText(feature.hindernis) if (fehler || !antwort) { return (
{fehler ?
{fehler}
: } {hindernis &&
{hindernis}
} {fehler &&
}
) } const modell = panelModell(antwort, feature) return (
Einbauanleitung {modell.kopf && {modell.kopf}} {modell.ticketPfad && Zum Ticket}
{modell.warnungen.length > 0 && ( )} {modell.hinweis &&
{modell.hinweis}
} {modell.kopien.length > 0 && (
{modell.kopien.map((k) => )}
)} {modell.abschnitte.map((a) => (
{a.titel}
{a.text}
))}
) } export default function CustomerFunktionen({ customerId }) { const [projects, setProjects] = useState(null) const [error, setError] = useState(null) const [confirm, setConfirm] = useState(null) // { projectId, key, action } const [note, setNote] = useState('') const [busy, setBusy] = useState(false) const [status, setStatus] = useState(null) // { ok, text } const [panel, setPanel] = useState({}) // projectId -> true/false (Knopf "Anleitung"); ohne Eintrag entscheidet die Adresse (?anleitung=) const [panelReload, setPanelReload] = useState({}) // projectId -> Zaehler, nach jeder Aktion hochgezaehlt const load = useCallback(() => { setError(null) getFunktionen(customerId) .then(({ projects: list }) => setProjects(list || [])) .catch((err) => setError(err.message || 'Funktionen konnten nicht geladen werden.')) }, [customerId]) useEffect(() => { load() }, [load]) const save = async () => { const { projectId, key, action } = confirm setBusy(true) setStatus(null) try { const { projects: changed } = await setFeature(customerId, key, { projectId, action, note: note.trim() }) // Die Antwort enthaelt das ganze Projekt; uebernommen wird nur die geaenderte Funktion const updated = changed?.[0] if (updated) setProjects((list) => mergeFunktion(list, updated.projectId, key, updated)) setConfirm(null) setNote('') setStatus({ ok: true, text: 'Gespeichert.' }) } catch (err) { setConfirm(null) setStatus({ ok: false, text: err.message }) } finally { setBusy(false) // Version und Hindernis der Anleitung haengen vom Zustand ab: Panel dieser Zeile neu laden if (key === 'termine') setPanelReload((r) => ({ ...r, [projectId]: (r[projectId] || 0) + 1 })) } } const ask = (projectId, key, action) => { setNote(''); setConfirm({ projectId, key, action }) } const panelOffen = (projectId) => (Object.prototype.hasOwnProperty.call(panel, projectId) ? panel[projectId] : anleitungOffen(window.location.search, projectId)) const togglePanel = (projectId) => setPanel((p) => ({ ...p, [projectId]: !panelOffen(projectId) })) if (error) return
{error}
if (!projects) return if (!projects.length) return return (
ProjektFunktionStatusTicketAktionen
{projects.flatMap((p) => FUNKTIONEN.map((f, index) => { const feature = funktionOf(p, f.key) const [tone, label] = ZUSTAND[feature.state] || ['muted', feature.state] const isConfirming = confirm?.projectId === p.projectId && confirm?.key === f.key const hatAnleitung = f.key === 'termine' && feature.state !== 'off' const anleitungOffenHier = hatAnleitung && panelOffen(p.projectId) return (
{index === 0 ? ( {p.projectName} {p.liveUrl && <>
{p.liveUrl.replace(/^https?:\/\//, '')}}
) : ( )} {f.label} {label} {feature.state === 'setup' && hindernisText(feature.hindernis) && <>
{hindernisText(feature.hindernis)}} {abgeschaltetText(feature) && <>
{abgeschaltetText(feature)}} {feature.adminNote && <>
{short(feature.adminNote)}} {siteKeyOf(feature, f.key) && <>
}
{feature.ticketMissing ? Ticket fehlt : feature.ticketWoid ? #{feature.ticketWoid} : '–'} {isConfirming ? ( {f.bestaetigen[confirm.action]} setNote(e.target.value)} style={{ maxWidth: 220 }} /> ) : ( {aktionen(feature.state, f.key, { ticketMissing: feature.ticketMissing }).map((action) => ( ))} {hatAnleitung && ( )} )}
{anleitungOffenHier && ( )}
) }))}
{status &&
{status.text}
}
) }