Panel unter der Zeile Öffnungszeiten live (Header-Selektor, Kontakt-/Hinweis-Leiste aus, Speichern mit teamVersion, Ersatz-HTML kopieren je Plan), Meldung zum Ersatz-Ticket nach dem Abschalten, Hindernis an aktionen, Statustext der Zeiten, Team-API (GET ersatz, PATCH team), Karte Leistungsbeschreibung Öffnungszeiten live in setup-legal (Schlüssel höchstens 36 Zeichen). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
139 lines
6.5 KiB
React
139 lines
6.5 KiB
React
import { useEffect, useState } from 'react'
|
|
import { FaCopy, FaSpinner } from 'react-icons/fa6'
|
|
import { Button } from '../ui'
|
|
import { getZeitenErsatz, patchZeitenTeam } from '../../lib/teamApi'
|
|
import {
|
|
KEINE_ZEITEN_TEXT, ersatzKnoepfe, ersatzKopieren, ersatzMeldung, navSelektorFehler, teamBody, teamFormular, teamGeaendert,
|
|
teamSpeichernFehler,
|
|
} from '../../lib/zeitenView'
|
|
|
|
// Panel "Öffnungszeiten live" unter der Zeile im Reiter "Funktionen": Team-Einstellungen der Website-Leisten (nur das Team ändert sie)
|
|
// und "Ersatz-HTML kopieren" je Plan. Entscheidungen, Texte und Prüfungen stehen in zeitenView (rein, getestet); hier wird nur verdrahtet.
|
|
// Texte stehen ausschließlich als React-Textknoten da (nie als HTML). Die Hülle (area-row funktionen-panel) liegt als eigene Zeile außerhalb
|
|
// des Fünf-Spalten-Rasters, wie die Einbauanleitung des Terminplaners.
|
|
export default function ZeitenPanel({ customerId, projectId, feature, onSaved, onReload }) {
|
|
const start = teamFormular(feature)
|
|
const [form, setForm] = useState(start)
|
|
const [busy, setBusy] = useState(false)
|
|
const [meldung, setMeldung] = useState(null) // { ok, text, neuLaden? } zum Speichern
|
|
const [ersatz, setErsatz] = useState({ kurz: null, busy: false, meldung: null, html: '' })
|
|
|
|
// Neuer Stand vom Server (nach dem Speichern oder Neuladen): Formular auf die gespeicherten Werte setzen
|
|
useEffect(() => {
|
|
setForm({ navSelektor: start.navSelektor, kontaktleisteAus: start.kontaktleisteAus, hinweisleisteAus: start.hinweisleisteAus })
|
|
}, [feature.teamVersion, start.navSelektor, start.kontaktleisteAus, start.hinweisleisteAus])
|
|
|
|
// Eine Eingabe macht "Gespeichert." und alte Fehler unpassend; die Aufforderung zum Neuladen bleibt, bis sie befolgt wird
|
|
const aendern = (teil) => {
|
|
setMeldung((m) => (m?.neuLaden ? m : null))
|
|
setForm((f) => ({ ...f, ...teil }))
|
|
}
|
|
const selektorFehler = navSelektorFehler(form.navSelektor)
|
|
const geaendert = teamGeaendert(form, feature)
|
|
|
|
const speichern = async (event) => {
|
|
event.preventDefault()
|
|
if (selektorFehler || !geaendert) return
|
|
setBusy(true)
|
|
setMeldung(null)
|
|
try {
|
|
const antwort = await patchZeitenTeam(customerId, teamBody({ projectId, teamVersion: feature.teamVersion, ...form }))
|
|
onSaved(projectId, antwort)
|
|
setMeldung({ ok: true, text: 'Gespeichert.' })
|
|
} catch (err) {
|
|
const { text, neuLaden } = teamSpeichernFehler(err)
|
|
setMeldung({ ok: false, text, neuLaden })
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
const kopieren = async ({ kurz, bezeichnung }) => {
|
|
setErsatz({ kurz, busy: true, meldung: null, html: '' })
|
|
try {
|
|
const { html } = await getZeitenErsatz(customerId, projectId, kurz)
|
|
const ergebnis = await ersatzKopieren(html, typeof navigator === 'undefined' ? undefined : navigator.clipboard)
|
|
setErsatz({ kurz, busy: false, meldung: ersatzMeldung(ergebnis, feature.plaene?.length > 1 ? bezeichnung : ''), html: ergebnis === 'manuell' ? html : '' })
|
|
} catch (err) {
|
|
setErsatz({ kurz, busy: false, meldung: { ok: false, text: err?.message || 'Ersatz-HTML konnte nicht geladen werden.' }, html: '' })
|
|
}
|
|
}
|
|
|
|
const knoepfe = ersatzKnoepfe(feature)
|
|
|
|
return (
|
|
<div className="area-row funktionen-panel funktionen-panel-zeiten">
|
|
<div className="funktionen-panel-head">
|
|
<strong>Öffnungszeiten live</strong>
|
|
<span className="area-muted">Einstellungen des Teams (der Kunde sieht und ändert sie nicht)</span>
|
|
</div>
|
|
|
|
<form onSubmit={speichern} className="funktionen-panel-form">
|
|
<label className="funktionen-panel-feld">
|
|
<span>Header-Selektor</span>
|
|
<input
|
|
className="form-control"
|
|
value={form.navSelektor}
|
|
onChange={(e) => aendern({ navSelektor: e.target.value })}
|
|
disabled={busy}
|
|
placeholder="z. B. header.site-header (leer = Standard)"
|
|
spellCheck={false}
|
|
autoComplete="off"
|
|
aria-invalid={selektorFehler ? 'true' : 'false'}
|
|
/>
|
|
</label>
|
|
<div className="area-muted">
|
|
Nur nötig, wenn die Hinweis-Leiste den festen Header der Website überlappt: CSS-Selektor des Headers.
|
|
</div>
|
|
{selektorFehler && <div className="area-error" role="alert">{selektorFehler}</div>}
|
|
<div className="area-inline">
|
|
<label className="area-inline">
|
|
<input type="checkbox" checked={form.kontaktleisteAus} disabled={busy} onChange={(e) => aendern({ kontaktleisteAus: e.target.checked })} />
|
|
Kontakt-Leiste aus
|
|
</label>
|
|
<label className="area-inline">
|
|
<input type="checkbox" checked={form.hinweisleisteAus} disabled={busy} onChange={(e) => aendern({ hinweisleisteAus: e.target.checked })} />
|
|
Hinweis-Leiste aus
|
|
</label>
|
|
</div>
|
|
<div className="area-inline">
|
|
<Button type="submit" size="sm" variant="primary" disabled={busy || Boolean(selektorFehler) || !geaendert}>
|
|
{busy ? <FaSpinner className="spinner" /> : 'Speichern'}
|
|
</Button>
|
|
{meldung && <span role="status" className={meldung.ok ? 'area-message funktionen-panel-meldung' : 'area-error funktionen-panel-meldung'}>{meldung.text}</span>}
|
|
{meldung?.neuLaden && (
|
|
<Button type="button" size="sm" variant="ghost" onClick={() => { setMeldung(null); onReload() }}>Neu laden</Button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
|
|
<div className="funktionen-panel-head">
|
|
<strong>Ersatz-HTML</strong>
|
|
<span className="area-muted">Wochentabelle des Plans für den Platzhalter der Website (Einbau)</span>
|
|
</div>
|
|
{knoepfe.length === 0 ? (
|
|
<div className="area-muted">{KEINE_ZEITEN_TEXT}</div>
|
|
) : (
|
|
<div className="area-inline">
|
|
{knoepfe.map((k) => (
|
|
<Button key={k.kurz} type="button" size="sm" variant="ghost" disabled={ersatz.busy} onClick={() => kopieren(k)} title={`Plan „${k.kurz}“`}>
|
|
{ersatz.busy && ersatz.kurz === k.kurz ? <FaSpinner className="spinner" /> : <FaCopy />} {k.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
{ersatz.meldung && <div role="status" className={ersatz.meldung.ok ? 'area-message' : 'area-error'}>{ersatz.meldung.text}</div>}
|
|
{ersatz.html && (
|
|
<textarea
|
|
readOnly
|
|
className="form-control funktionen-panel-html"
|
|
rows={8}
|
|
value={ersatz.html}
|
|
aria-label="Ersatz-HTML zum Kopieren"
|
|
onFocus={(e) => e.target.select()}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|