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.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; }