Terminplaner: Staff-Aktivierung nur mit Zustimmung in Textform (Modus on), WOMS

Im Dialog "Terminplaner ohne Zahlung aktivieren?" erscheinen, wenn das Portal
termine.textformNoetig meldet, der Haken (Leistungsbeschreibung und AVV in
der Fassung, Ausschluss Gesundheitsbereich), das Datum der Zustimmung und der
Weg (Chat | E-Mail) sowie die Textvorlage fuer den Chat mit Kopierknopf
(versionierte Adressen aus dem Portal). "Ja" bleibt gesperrt, bis alles
stimmt (canSubmitTextform mit minDate = Fassung); der Rumpf traegt
textformDate und textformChannel. News und Abschalten bleiben unveraendert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 05:07:37 +00:00
1 parent 857e001911
commit 45e5dc30dc
4 files changed
+256 -9

No files matched your search

+67 -5
View File
@@ -6,8 +6,10 @@ import { copyText } from '../CopyableCredential'
import { getFunktionen, setFeature, getTermineAnleitung } from '../../lib/teamApi'
import {
FUNKTIONEN, ZUSTAND, AKTION_LABEL, funktionOf, aktionen, mergeFunktion, siteKeyOf, abgeschaltetText,
hindernisText, panelModell, anleitungOffen,
hindernisText, panelModell, anleitungOffen, textformNoetig, textformAnsicht, textformBereit,
} from '../../lib/funktionenView'
import { CHANNELS, channelLabel } from '../../lib/analyseView'
import { berlinYmd } from '../../lib/format'
import '../../styles/admin-areas.css'
// Reiter "Funktionen": News-Pinnwand und Terminplaner je Projekt an/aus wie im frueheren Admin-Panel (Kundenliste, Zeile "Funktionen")
@@ -111,8 +113,57 @@ function AnleitungPanel({ customerId, projectId, feature, reloadKey }) {
)
}
// Zustimmung des Kunden in Textform (Terminplaner im Modus on): Haken, Datum, Weg und die Textvorlage fuer den Chat. Die Pflichtangaben
// prueft textformBereit (funktionenView); das Portal prueft sie noch einmal. Links nur als geprueftes https (textformAnsicht).
function TextformPanel({ termine, value, onChange, projectId, today, disabled }) {
const ansicht = textformAnsicht(termine)
const { label, links, vorlage } = ansicht
return (
<div className="area-row funktionen-panel">
<label className="area-inline" style={{ alignItems: 'flex-start', flexWrap: 'nowrap' }}>
<input type="checkbox" style={{ marginTop: 3 }} checked={value.confirmed} disabled={disabled} onChange={(e) => onChange({ ...value, confirmed: e.target.checked })} />
<span>
{label.before}
{links.lb ? <a href={links.lb} target="_blank" rel="noopener noreferrer">{label.lb}</a> : label.lb}
{label.between}
{links.avv ? <a href={links.avv} target="_blank" rel="noopener noreferrer">{label.avv}</a> : label.avv}
{label.after}
</span>
</label>
<div className="area-inline">
<label>
Datum der Zustimmung:{' '}
<input type="date" required className="form-control" value={value.date} min={ansicht.version} max={today} disabled={disabled} onChange={(e) => onChange({ ...value, date: e.target.value })} />
</label>
<span role="radiogroup" aria-label="Weg der Zustimmung" className="area-inline">
<span>Weg:</span>
{CHANNELS.map((c) => (
<label key={c} className="area-inline">
<input type="radio" name={`termine-weg-${projectId}`} value={c} checked={value.channel === c} disabled={disabled} onChange={() => onChange({ ...value, channel: c })} />
{channelLabel(c)}
</label>
))}
</span>
</div>
{vorlage ? (
<details>
<summary style={{ cursor: 'pointer' }}>Textvorlage für den Chat</summary>
<p style={{ margin: '6px 0', whiteSpace: 'pre-wrap' }}>{vorlage}</p>
<KopierKnopf text={vorlage} label="Vorlage kopieren" />
</details>
) : (
<div className="area-summary warn">Die Adressen der Unterlagen fehlen; die Chat-Vorlage ist nicht verfügbar.</div>
)}
</div>
)
}
const LEERE_TEXTFORM = { confirmed: false, date: '', channel: '' }
export default function CustomerFunktionen({ customerId }) {
const [projects, setProjects] = useState(null)
const [termine, setTermine] = useState(null) // Portal-Feld termine: { textformNoetig, avvVersion, links }
const [textform, setTextform] = useState(LEERE_TEXTFORM)
const [error, setError] = useState(null)
const [confirm, setConfirm] = useState(null) // { projectId, key, action }
const [note, setNote] = useState('')
@@ -124,7 +175,7 @@ export default function CustomerFunktionen({ customerId }) {
const load = useCallback(() => {
setError(null)
getFunktionen(customerId)
.then(({ projects: list }) => setProjects(list || []))
.then(({ projects: list, termine: feld }) => { setProjects(list || []); setTermine(feld || null) })
.catch((err) => setError(err.message || 'Funktionen konnten nicht geladen werden.'))
}, [customerId])
useEffect(() => { load() }, [load])
@@ -134,12 +185,17 @@ export default function CustomerFunktionen({ customerId }) {
setBusy(true)
setStatus(null)
try {
const { projects: changed } = await setFeature(customerId, key, { projectId, action, note: note.trim() })
const mitTextform = textformNoetig(key, action, termine)
const { projects: changed } = await setFeature(customerId, key, {
projectId, action, note: note.trim(),
...(mitTextform ? { textformDate: textform.date, textformChannel: textform.channel } : {}),
})
// 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('')
setTextform(LEERE_TEXTFORM)
setStatus({ ok: true, text: 'Gespeichert.' })
} catch (err) {
setConfirm(null)
@@ -151,7 +207,8 @@ export default function CustomerFunktionen({ customerId }) {
}
}
const ask = (projectId, key, action) => { setNote(''); setConfirm({ projectId, key, action }) }
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))
const togglePanel = (projectId) => setPanel((p) => ({ ...p, [projectId]: !panelOffen(projectId) }))
@@ -169,6 +226,8 @@ export default function CustomerFunktionen({ customerId }) {
const isConfirming = confirm?.projectId === p.projectId && confirm?.key === f.key
const hatAnleitung = f.key === 'termine' && feature.state !== 'off'
const anleitungOffenHier = hatAnleitung && panelOffen(p.projectId)
const braucheTextform = isConfirming && textformNoetig(f.key, confirm.action, termine)
const textformFehlt = braucheTextform && !textformBereit({ ...textform, today: heute }, termine)
return (
<Fragment key={`${p.projectId}:${f.key}`}>
<div className="area-row funktionen-row">
@@ -200,7 +259,7 @@ export default function CustomerFunktionen({ customerId }) {
<Fragment>
<span>{f.bestaetigen[confirm.action]}</span>
<input className="form-control" placeholder="Notiz (optional)" maxLength={120} value={note} onChange={(e) => setNote(e.target.value)} style={{ maxWidth: 220 }} />
<Button size="sm" variant="primary" onClick={save} disabled={busy}>{busy ? <FaSpinner className="spinner" /> : 'Ja'}</Button>
<Button size="sm" variant="primary" onClick={save} disabled={busy || textformFehlt}>{busy ? <FaSpinner className="spinner" /> : 'Ja'}</Button>
<Button size="sm" variant="ghost" onClick={() => setConfirm(null)} disabled={busy}>Abbrechen</Button>
</Fragment>
) : (
@@ -219,6 +278,9 @@ export default function CustomerFunktionen({ customerId }) {
)}
</span>
</div>
{braucheTextform && (
<TextformPanel termine={termine} value={textform} onChange={setTextform} projectId={p.projectId} today={heute} disabled={busy} />
)}
{anleitungOffenHier && (
<AnleitungPanel customerId={customerId} projectId={p.projectId} feature={feature} reloadKey={panelReload[p.projectId] || 0} />
)}