Funktionen: Abrechnung der Einrichtung wählen, Abo-Fehler anzeigen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 13:55:05 +00:00
1 parent 8f75276c54
commit 06166221ec
2 files changed
+27 -5

No files matched your search

+24 -3
View File
@@ -7,6 +7,7 @@ import {
FUNKTIONEN, ZUSTAND, AKTION_LABEL, funktionOf, aktionen, mergeFunktion, siteKeyOf, abgeschaltetText,
hindernisText, panelModell, anleitungOffen, textformNoetig, textformAnsicht, textformBereit, speicherStatus,
anfrageZeilen, anfrageStatus, stripeText, stripeTon, shopHatEintrag, begruendungOk, BEGRUENDUNG_MAX, entscheidungStatus,
ABRECHNUNG_OPTIONEN, ABRECHNUNG_HINWEIS, fragtAbrechnung, abrechnungBereit, featureFehlerText,
} from '../../lib/funktionenView'
import { FUNKTION_CHANNELS, channelLabel } from '../../lib/analyseView'
import { berlinYmd, fmtDateTime } from '../../lib/format'
@@ -264,6 +265,22 @@ function UnterkunftPanel({ customerId, eintrag, onDecided }) {
)
}
// Wahl beim Aktivieren, wohin die Einrichtungszeit geht (V9): Segment-Schalter ohne Vorauswahl, dahinter die Erklaerung (ohne Preis)
function AbrechnungWahl({ value, onChange, disabled }) {
return (
<>
<div className="seg" role="group" aria-label="Abrechnung der Einrichtung">
{ABRECHNUNG_OPTIONEN.map((o) => (
<button key={o.value} type="button" className={value === o.value ? 'on' : ''} aria-pressed={value === o.value} disabled={disabled} onClick={() => onChange(o.value)}>
{o.label}
</button>
))}
</div>
<span className="muted small">{ABRECHNUNG_HINWEIS}</span>
</>
)
}
const LEERE_TEXTFORM = { confirmed: false, date: '', channel: '' }
export default function CustomerFunktionen({ customerId }) {
@@ -273,6 +290,7 @@ export default function CustomerFunktionen({ customerId }) {
const [error, setError] = useState(null)
const [confirm, setConfirm] = useState(null) // { projectId, key, action }
const [note, setNote] = useState('')
const [abrechnung, setAbrechnung] = useState('') // Wahl beim Aktivieren: '' (noch keine) | 'abo' | 'projekt'; nie vorbelegt
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=<projectId>)
@@ -309,9 +327,11 @@ export default function CustomerFunktionen({ customerId }) {
setStatus(null)
try {
const mitTextform = textformNoetig(key, action, felder[key])
const mitAbrechnung = fragtAbrechnung(key, action, funktionOf(projects?.find((p) => p.projectId === projectId), key))
const antwort = await setFeature(customerId, key, {
projectId, action, note: note.trim(),
...(mitTextform ? { textformDate: textform.date, textformChannel: textform.channel } : {}),
...(mitAbrechnung ? { abrechnung } : {}),
})
// Die Antwort enthaelt das ganze Projekt; uebernommen wird nur die geaenderte Funktion
const updated = antwort?.projects?.[0]
@@ -323,7 +343,7 @@ export default function CustomerFunktionen({ customerId }) {
setStatus(key === 'zeiten' && action === 'disable' ? abschaltMeldung(antwort?.ersatzTicket) : speicherStatus(antwort, key))
} catch (err) {
setConfirm(null)
setStatus({ ok: false, text: err.message })
setStatus({ ok: false, text: featureFehlerText(err, key) })
} finally {
setBusy(false)
// Version und Hindernis der Anleitung haengen vom Zustand ab: Panel dieser Zeile neu laden
@@ -334,7 +354,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 ask = (projectId, key, action) => { setNote(''); setTextform(LEERE_TEXTFORM); setAbrechnung(''); 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) }))
@@ -417,8 +437,9 @@ export default function CustomerFunktionen({ customerId }) {
<td colSpan={4}>
<div className="flex items-center gap-2 wrap">
<span>{f.bestaetigen[confirm.action]}</span>
{fragtAbrechnung(f.key, confirm.action, feature) && <AbrechnungWahl value={abrechnung} onChange={setAbrechnung} disabled={busy} />}
<input className="input funktionen-note" placeholder="Notiz (optional)" maxLength={120} aria-label="Notiz (optional)" value={note} onChange={(e) => setNote(e.target.value)} />
<Button type="button" variant="primary" onClick={save} disabled={busy || textformFehlt}>{busy ? 'Speichert …' : 'Ja'}</Button>
<Button type="button" variant="primary" onClick={save} disabled={busy || textformFehlt || !abrechnungBereit({ key: f.key, action: confirm.action, feature, abrechnung })}>{busy ? 'Speichert …' : 'Ja'}</Button>
<Button type="button" onClick={() => setConfirm(null)} disabled={busy}>Abbrechen</Button>
</div>
</td>
+3 -2
View File
@@ -98,9 +98,10 @@ export const getFunktionen = (customerId) => request(`/funktionen/${enc(customer
/**
* Aktivieren (ohne Zahlung) oder Abschalten einer Funktion (key: news | termine | shop | zeiten | unterkunft); Antwort { projects: [das eine Projekt] }.
* Terminplaner und Webshop im Modus on: beim Aktivieren zusaetzlich textformDate (JJJJ-MM-TT) und textformChannel (chat | email).
* abrechnung ('abo' | 'projekt', nur beim Aktivieren): wohin die Einrichtungszeit geht; 409 abo_fehlt bringt err.code und err.grund mit (featureFehlerText).
*/
export const setFeature = (customerId, key, { projectId, action, note, textformDate, textformChannel }) =>
request(`/funktionen/${enc(customerId)}/${enc(key)}`, { method: 'POST', body: featureBody({ projectId, action, note, textformDate, textformChannel }) })
export const setFeature = (customerId, key, { projectId, action, note, textformDate, textformChannel, abrechnung }) =>
request(`/funktionen/${enc(customerId)}/${enc(key)}`, { method: 'POST', body: featureBody({ projectId, action, note, textformDate, textformChannel, abrechnung }) })
export const setNewsFeature = (customerId, body) => setFeature(customerId, 'news', body)
/** Einbauanleitung des Terminplaners: { stand, warnungen, abschnitte, text, state, ticketWoid, ticketVersion, hindernis } (Aufbereitung: panelModell). */
export const getTermineAnleitung = (customerId, projectId) => request(anleitungPfad(customerId, projectId))