diff --git a/scripts/setup-legal.mjs b/scripts/setup-legal.mjs index fdfbdcb..6b9de37 100644 --- a/scripts/setup-legal.mjs +++ b/scripts/setup-legal.mjs @@ -99,6 +99,8 @@ export const DOCUMENTS = [ { key: "leistungsbeschreibung-terminplaner", title: "Leistungsbeschreibung Terminplaner", liveUrl: "https://project.webklar.com/rechtliches/leistungsbeschreibung-terminplaner.html" }, { key: "avv-terminplaner", title: "AVV Terminplaner (Auftragsverarbeitung)", liveUrl: "https://project.webklar.com/rechtliches/avv-terminplaner.html" }, { key: "widerrufsbelehrung-funktionen", title: "Widerrufsbelehrung und Muster-Widerrufsformular (Funktionen)", liveUrl: "https://project.webklar.com/rechtliches/widerrufsbelehrung-funktionen.html" }, + // Oeffnungszeiten live: Schluessel hoechstens 36 Zeichen (WOMS-Grenze fuer Dokument-IDs); die Fassungen (PDF/HTML) laedt der Inhaber hoch + { key: "leistungsbeschreibung-zeiten", title: "Leistungsbeschreibung Öffnungszeiten live", liveUrl: "https://project.webklar.com/vertragsunterlagen/zeiten-leistungsbeschreibung.html" }, ].map((d, i) => ({ ...d, sort: i + 1 })); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); diff --git a/src/components/customer/CustomerFunktionen.jsx b/src/components/customer/CustomerFunktionen.jsx index 7ea749f..2e39052 100644 --- a/src/components/customer/CustomerFunktionen.jsx +++ b/src/components/customer/CustomerFunktionen.jsx @@ -10,6 +10,8 @@ import { } from '../../lib/funktionenView' import { CHANNELS, channelLabel } from '../../lib/analyseView' import { berlinYmd } from '../../lib/format' +import { zeitenStatusText, mergeTeam, abschaltMeldung } from '../../lib/zeitenView' +import ZeitenPanel from './ZeitenPanel' import '../../styles/admin-areas.css' // Reiter "Funktionen": News-Pinnwand und Terminplaner je Projekt an/aus wie im frueheren Admin-Panel (Kundenliste, Zeile "Funktionen") @@ -186,7 +188,7 @@ export default function CustomerFunktionen({ customerId }) { setStatus(null) try { const mitTextform = textformNoetig(key, action, termine) - const { projects: changed } = await setFeature(customerId, key, { + const { projects: changed, ersatzTicket } = await setFeature(customerId, key, { projectId, action, note: note.trim(), ...(mitTextform ? { textformDate: textform.date, textformChannel: textform.channel } : {}), }) @@ -196,7 +198,8 @@ export default function CustomerFunktionen({ customerId }) { setConfirm(null) setNote('') setTextform(LEERE_TEXTFORM) - setStatus({ ok: true, text: 'Gespeichert.' }) + // Abschalten der Zeiten: das Portal meldet, ob das Ticket "Ersatz-Tabelle aktualisieren" angelegt wurde (bei 'fehler' warnen) + setStatus(key === 'zeiten' && action === 'disable' ? abschaltMeldung(ersatzTicket) : { ok: true, text: 'Gespeichert.' }) } catch (err) { setConfirm(null) setStatus({ ok: false, text: err.message }) @@ -207,6 +210,7 @@ export default function CustomerFunktionen({ customerId }) { } } + const teamGespeichert = (projectId, antwort) => setProjects((list) => mergeTeam(list, projectId, antwort)) const ask = (projectId, key, action) => { setNote(''); setTextform(LEERE_TEXTFORM); setConfirm({ projectId, key, action }) } const heute = berlinYmd(new Date()) const panelOffen = (projectId) => (Object.prototype.hasOwnProperty.call(panel, projectId) ? panel[projectId] : anleitungOffen(window.location.search, projectId)) @@ -228,6 +232,8 @@ export default function CustomerFunktionen({ customerId }) { const anleitungOffenHier = hatAnleitung && panelOffen(p.projectId) const braucheTextform = isConfirming && textformNoetig(f.key, confirm.action, termine) const textformFehlt = braucheTextform && !textformBereit({ ...textform, today: heute }, termine) + const hatZeitenPanel = f.key === 'zeiten' && feature.state !== 'off' + const zeitenText = f.key === 'zeiten' ? zeitenStatusText(feature) : '' return (
@@ -243,6 +249,7 @@ export default function CustomerFunktionen({ customerId }) { {label} {feature.state === 'setup' && hindernisText(feature.hindernis) && <>
{hindernisText(feature.hindernis)}} + {zeitenText && <>
{zeitenText}} {abgeschaltetText(feature) && <>
{abgeschaltetText(feature)}} {feature.adminNote && <>
{short(feature.adminNote)}} {siteKeyOf(feature, f.key) && <>
} @@ -264,7 +271,7 @@ export default function CustomerFunktionen({ customerId }) { ) : ( - {aktionen(feature.state, f.key, { ticketMissing: feature.ticketMissing }).map((action) => ( + {aktionen(feature.state, f.key, { ticketMissing: feature.ticketMissing, hindernis: feature.hindernis }).map((action) => ( @@ -281,6 +288,9 @@ export default function CustomerFunktionen({ customerId }) { {braucheTextform && ( )} + {hatZeitenPanel && ( + + )} {anleitungOffenHier && ( )} diff --git a/src/components/customer/ZeitenPanel.jsx b/src/components/customer/ZeitenPanel.jsx new file mode 100644 index 0000000..2b78988 --- /dev/null +++ b/src/components/customer/ZeitenPanel.jsx @@ -0,0 +1,138 @@ +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 ( +
+
+ Öffnungszeiten live + Einstellungen des Teams (der Kunde sieht und ändert sie nicht) +
+ +
+ +
+ Nur nötig, wenn die Hinweis-Leiste den festen Header der Website überlappt: CSS-Selektor des Headers. +
+ {selektorFehler &&
{selektorFehler}
} +
+ + +
+
+ + {meldung && {meldung.text}} + {meldung?.neuLaden && ( + + )} +
+
+ +
+ Ersatz-HTML + Wochentabelle des Plans für den Platzhalter der Website (Einbau) +
+ {knoepfe.length === 0 ? ( +
{KEINE_ZEITEN_TEXT}
+ ) : ( +
+ {knoepfe.map((k) => ( + + ))} +
+ )} + {ersatz.meldung &&
{ersatz.meldung.text}
} + {ersatz.html && ( +