diff --git a/src/components/customer/CustomerFunktionen.jsx b/src/components/customer/CustomerFunktionen.jsx
index f14a8ae..7283a3a 100644
--- a/src/components/customer/CustomerFunktionen.jsx
+++ b/src/components/customer/CustomerFunktionen.jsx
@@ -1,10 +1,13 @@
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { Link } from 'react-router-dom'
-import { FaCheck, FaCopy, FaSpinner } from 'react-icons/fa6'
+import { FaCheck, FaCopy, FaRotateRight, 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, siteKeyOf, abgeschaltetText } from '../../lib/funktionenView'
+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")
@@ -32,6 +35,82 @@ function SiteKey({ 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.warnungen.map((text, i) => - {text}
)}
+
+ )}
+ {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)
@@ -39,6 +118,8 @@ export default function CustomerFunktionen({ customerId }) {
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)
@@ -49,13 +130,14 @@ export default function CustomerFunktionen({ customerId }) {
useEffect(() => { load() }, [load])
const save = async () => {
+ const { projectId, key, action } = confirm
setBusy(true)
setStatus(null)
try {
- const { projects: changed } = await setFeature(customerId, confirm.key, { projectId: confirm.projectId, action: confirm.action, note: note.trim() })
+ 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, confirm.key, updated))
+ if (updated) setProjects((list) => mergeFunktion(list, updated.projectId, key, updated))
setConfirm(null)
setNote('')
setStatus({ ok: true, text: 'Gespeichert.' })
@@ -64,10 +146,14 @@ export default function CustomerFunktionen({ customerId }) {
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
@@ -81,47 +167,62 @@ export default function CustomerFunktionen({ customerId }) {
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}
- {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 }} />
-
-
-
+
+
+ {index === 0 ? (
+
+ {p.projectName}
+ {p.liveUrl && <>
{p.liveUrl.replace(/^https?:\/\//, '')}>}
+
) : (
- aktionen(feature.state).map((action) => (
-
- ))
+
)}
-
-
+ {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 && (
+
+ )}
+
)
}))}
diff --git a/src/styles/admin-areas.css b/src/styles/admin-areas.css
index d35f5cb..916a147 100644
--- a/src/styles/admin-areas.css
+++ b/src/styles/admin-areas.css
@@ -30,6 +30,17 @@
}
.funktionen-sitekey-copy:hover { background: var(--surface-2); color: var(--text); }
.funktionen-sitekey-ok { color: var(--ok); }
+.funktionen-panel { grid-template-columns: minmax(0, 1fr); gap: var(--sp-2); background: var(--surface-2); }
+.funktionen-panel-head { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3); align-items: center; }
+.funktionen-panel-warnungen { margin: 0; padding-left: 32px; }
+.funktionen-panel .area-summary { margin-bottom: 0; }
+.funktionen-panel-abschnitt { border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface-1); min-width: 0; }
+.funktionen-panel-abschnitt > summary { padding: 8px 12px; cursor: pointer; font-weight: 600; }
+.funktionen-panel-abschnitt pre {
+ margin: 0; padding: 10px 12px; border-top: 1px solid var(--border); max-width: 100%;
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--text);
+ white-space: pre-wrap; overflow-wrap: anywhere; user-select: text;
+}
@media (max-width: 720px) {
.area-row { grid-template-columns: minmax(0, 1fr) !important; }
.area-row.head { display: none; }