Terminplaner: Portal-Ansicht – Nachbesserungen (2/2)

- Annehmen mit Rueckfrage in der Seite (schliesst den Vertrag, schickt G02)
- Offene Anfragen: alle bis Ende des Buchungszeitraums (Fenster zu je 62 Tagen,
  hoechstens 6 Aufrufe), passend zur Zahl auf Kachel und Banner
- Meldung ins Bild geholt, Fehler mit Fokus; einzige Live-Region
- Netzwerkfehler auf Deutsch; Antwort einer frueheren Website erscheint nie
  unter der neuen (Zaehler je Aufruf); Datumssprung nur mit echtem Tag
- Ende um Mitternacht als 24:00, Statusfilter nach Eintragen auf Alle,
  abgeschaltet ohne Einstellung mit Export-Link, dunkles Datumsfeld
- Playwright: Zeitzone New York/Berlin, ferne Anfragen, Projektwechsel,
  Meldung im Bild, Datumssprung, abgeschaltet ohne Einstellung

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-03 08:17:44 +00:00
1 parent 9c07f6a8c5
commit 650919b022
10 files changed
+410 -73

No files matched your search

+15 -9
View File
@@ -4,7 +4,7 @@
Rueckfrage stehen in termine-aktionen.js, die Formulare in termine-formulare.js. */
import {
FILTER, anfragenText, bereichFuer, datumKurz, endeZeit, filterStatus, gruppiereNachTag, heuteBerlin, istSperre,
FILTER, anfragenText, bereichFuer, datumKurz, endeZeit, fensterAufteilen, filterStatus, gruppiereNachTag, heuteBerlin, istSperre, istTag,
moeglicheAktionen, statusLabel, statusTon, tagPlus, tagUeberschrift, verschiebeTag, zeitraumTitel,
} from './termine-view.js'
import { fehlerKarte, h } from './termine-dom.js'
@@ -27,8 +27,9 @@ export function render(panel, ctx) {
h('button', { type: 'button', class: 'kb-btn-outline tm-heute', onclick: () => setze(t, { tag: heuteBerlin() }) }, 'Heute'),
h('button', { type: 'button', class: 'tm-pfeil', 'aria-label': 'Weiter', onclick: () => setze(t, { tag: verschiebeTag(t.auswahl.ansicht, s.tag, 1) }) }, h('span', { 'aria-hidden': 'true' }, '›')))
t.datumFeld = h('input', { type: 'date', id: 'tm-sprung', 'aria-label': 'Zu einem Datum springen' })
t.datumFeld.addEventListener('change', () => { if (t.datumFeld.value) setze(t, { tag: t.datumFeld.value }) })
t.titel = h('h3', { class: 'tm-zeitraum', 'aria-live': 'polite' })
// nur ein echter Tag springt an (ein Datumsfeld mit fuenfstelligem Jahr liefert anderes und wuerde die Berechnung der Ansicht festfahren)
t.datumFeld.addEventListener('change', () => { if (istTag(t.datumFeld.value)) setze(t, { tag: t.datumFeld.value }) })
t.titel = h('h3', { class: 'tm-zeitraum' })
const filterKnoepfe = FILTER.map((f) => h('button', { type: 'button', class: 'tm-chip', 'data-filter': f.key, onclick: () => setze(t, { filter: f.key }) }, f.label))
t.filterBox = h('div', { class: 'tm-filter', role: 'group', 'aria-label': 'Nach Status filtern' }, filterKnoepfe)
t.ansichtBox = h('div', { class: 'tm-segment', role: 'group', 'aria-label': 'Ansicht' }, ansichtKnoepfe)
@@ -36,9 +37,9 @@ export function render(panel, ctx) {
h('button', { type: 'button', class: 'kb-btn-primary', 'data-werkzeug': 'manuell', onclick: () => oeffneFormular(t, 'manuell') }, '+ Termin eintragen'),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-werkzeug': 'sperre', onclick: () => oeffneFormular(t, 'sperre') }, 'Zeit sperren'))
t.neuBtn = h('button', { type: 'button', class: 'kb-btn-outline', onclick: () => ladeListe(t, { auchZahl: true }) }, 'Aktualisieren')
t.anfragenBox = h('div', { class: 'tm-box tm-box-info', role: 'status', hidden: true })
t.anfragenBox = h('div', { class: 'tm-box tm-box-info', hidden: true })
t.formularBox = h('div', { class: 'tm-formular-box' })
t.liste = h('div', { class: 'tm-liste', 'aria-live': 'polite' })
t.liste = h('div', { class: 'tm-liste' })
panel.replaceChildren(
h('div', { class: 'tm-leiste' }, t.ansichtBox, t.nav, h('label', { class: 'tm-sprung', for: 'tm-sprung' }, h('span', null, 'Datum'), t.datumFeld), t.zurueck),
@@ -91,16 +92,19 @@ async function ladeListe(t, { auchZahl = false } = {}) {
const { s } = ctx
const nr = ++t.nr
const anfragen = t.auswahl.ansicht === 'anfragen'
const { von, bis } = bereichFuer(t.auswahl.ansicht, s.tag, heuteBerlin())
// Offene Anfragen: alle bis zum Ende des Buchungszeitraums; die Schnittstelle liefert hoechstens 62 Tage je Aufruf, darum mehrere Fenster
const { von, bis } = bereichFuer(t.auswahl.ansicht, s.tag, heuteBerlin(), ctx.daten.einstellung?.regeln?.horizontTage)
const fenster = anfragen ? fensterAufteilen(von, bis) : [{ von, bis }]
const status = anfragen ? 'angefragt' : filterStatus(t.auswahl.filter)
t.liste.setAttribute('aria-busy', 'true')
try {
const [antwort] = await Promise.all([
ctx.api.get('/buchungen', { von, bis, ...(status ? { status } : {}) }),
const [antworten] = await Promise.all([
Promise.all(fenster.map((f) => ctx.api.get('/buchungen', { von: f.von, bis: f.bis, ...(status ? { status } : {}) }))),
auchZahl ? ctx.neuLaden().then(() => aktualisiereKopf(t), () => {}) : null, // die Zahl der Anfragen ist Beiwerk: ein Fehler dabei kostet nicht die Liste
])
if (nr !== t.nr || !ctx.aktuell() || !t.liste.isConnected) return
t.buchungen = antwort.buchungen || []
// ein Termin ueber die Fenstergrenze (Mitternacht) steht in beiden Antworten: nach ID zusammenfuehren
t.buchungen = [...new Map(antworten.flatMap((a) => a.buchungen || []).map((b) => [b.id, b])).values()]
zeichneListe(t)
} catch (err) {
if (nr !== t.nr || !ctx.aktuell()) return
@@ -220,8 +224,10 @@ function schliesseFormular(t) {
async function nachErfolg(t, tag, text) {
schliesseFormular(t)
if (!t.ctx.aktuell()) return
t.ctx.s.tag = tag
if (t.auswahl.ansicht === 'anfragen') t.auswahl.ansicht = 'tag'
t.auswahl.filter = 'alle' // sonst bliebe der neue Eintrag unter einem Statusfilter (z. B. „Angefragt“) unsichtbar
aktualisiereKopf(t)
await ladeListe(t, { auchZahl: true })
t.ctx.meldung(text)