Kundenansicht neu (Meine): Kurzinfo nur auf Anfrage (GET /api/funktionen?kurzinfo=1)

Der Server berechnet kurzinfo (Terminplaner, Webshop, News, Buchungskalender, Öffnungszeiten) nur noch,
wenn die neue Ansicht ?kurzinfo=1 sendet. Das tut allein die Seite "Meine"; Etagen, Einschalt-Ablauf und
Module fragen ohne den Parameter, dann fehlt das Feld und es gibt keine Abfrage auf Termine, Bestellungen,
Beiträge und Anfragen. Mit dem Parameter bleibt die Antwort wie bisher (Text oder null je Funktion).
Nur der Wert "1" gilt; doppelte oder als Liste gesendete Parameter heißen: ohne. Alte Ansicht unverändert.

Tests: kundenansichtKurzinfo (neuer Fall ohne/mit Parameter, Abfragen und Feld), knFunktionenAnsicht
(die Seite lädt genau einmal ?kurzinfo=1, auch nach "Nochmal versuchen").

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-17 15:46:48 +00:00
1 parent d7724afc02
commit 6bfcaa892c
6 files changed
+57 -10

No files matched your search

@@ -281,7 +281,7 @@ Kürzel in den Fundstellen: `Task n` und `VFn` = Task bzw. Vertragsfrage n im ge
### Pilot-Nachträge
- **N52 Kurzinfo in „Meine“ (V5, Pilot-Feedback 2026-10-17, Strang kn/meine-pilot).** Mit neuer Ansicht trägt jede Funktion unter `projects[].features.<news|termine|shop|unterkunft|zeiten>` zusätzlich `kurzinfo: string | null`, den Text der zweiten Zeile in „Meine“. Nur aktive Funktionen bekommen einen Text, alle anderen `null`; die Analyse trägt kein Feld (ihre Zahlen kommen aus dem Auswertungsdienst, nicht aus Appwrite). Quelle `server/services/kundenansicht/kurzinfo.js`: Terminplaner „3 Termine heute · nächster 10:30“ bzw. „Heute keine Termine“ (bestätigte und erledigte Termine ohne Sperren, Berliner Tag), Webshop „2 neue Bestellungen“ bzw. „Keine offenen Bestellungen“ (Status `bezahlt`), News „4 Beiträge sichtbar“ bzw. „Keine Beiträge sichtbar“ (`isHanging`), Buchungskalender „1 offene Anfrage“ (Status `neu`), sonst „Nächste Anreise 24.10.“ („Anreise heute“, „Anreise morgen“), sonst „Keine offenen Anfragen“, Öffnungszeiten live der Status der Website (`zeitenStatus.text`, ohne eigene Abfrage; ohne gespeicherte Zeiten „Noch keine Öffnungszeiten eingetragen“). Je Funktion höchstens zwei schmale Abfragen (`limit`, `select`, Filter auf Index-Felder), alle gleichzeitig; jede Funktion einzeln auf `kurzinfoDeps.limitMs` (1500 ms) begrenzt, Fehler oder Zeitüberschreitung ergibt `null`, eine volle Seite (Zahl unbekannt) ebenfalls. Die Texte enthalten nie Namen, Adressen oder Beträge. Die Oberfläche zeigt bei `null` „Aktiv“ und setzt die Zeile nur als Text (höchstens 80 Zeichen); eine Pille trägt nur noch eine Zeile, deren Funktion nicht läuft. Ohne neue Ansicht bleibt die Antwort bytegleich und es wird nichts gelesen.
- **N52 Kurzinfo in „Meine“ (V5, Pilot-Feedback 2026-10-17, Strang kn/meine-pilot).** Mit neuer Ansicht und nur auf Anfrage (`GET /api/funktionen?kurzinfo=1`, sendet allein „Meine“; Etagen, Einschalt-Ablauf und Module fragen ohne den Parameter, dann fehlt das Feld und nichts wird gelesen; Runde 1, 17.10.) trägt jede Funktion unter `projects[].features.<news|termine|shop|unterkunft|zeiten>` zusätzlich `kurzinfo: string | null`, den Text der zweiten Zeile in „Meine“. Nur aktive Funktionen bekommen einen Text, alle anderen `null`; die Analyse trägt kein Feld (ihre Zahlen kommen aus dem Auswertungsdienst, nicht aus Appwrite). Quelle `server/services/kundenansicht/kurzinfo.js`: Terminplaner „3 Termine heute · nächster 10:30“ bzw. „Heute keine Termine“ (bestätigte und erledigte Termine ohne Sperren, Berliner Tag), Webshop „2 neue Bestellungen“ bzw. „Keine offenen Bestellungen“ (Status `bezahlt`), News „4 Beiträge sichtbar“ bzw. „Keine Beiträge sichtbar“ (`isHanging`), Buchungskalender „1 offene Anfrage“ (Status `neu`), sonst „Nächste Anreise 24.10.“ („Anreise heute“, „Anreise morgen“), sonst „Keine offenen Anfragen“, Öffnungszeiten live der Status der Website (`zeitenStatus.text`, ohne eigene Abfrage; ohne gespeicherte Zeiten „Noch keine Öffnungszeiten eingetragen“). Je Funktion höchstens zwei schmale Abfragen (`limit`, `select`, Filter auf Index-Felder), alle gleichzeitig; jede Funktion einzeln auf `kurzinfoDeps.limitMs` (1500 ms) begrenzt, Fehler oder Zeitüberschreitung ergibt `null`, eine volle Seite (Zahl unbekannt) ebenfalls. Die Texte enthalten nie Namen, Adressen oder Beträge. Die Oberfläche zeigt bei `null` „Aktiv“ und setzt die Zeile nur als Text (höchstens 80 Zeichen); eine Pille trägt nur noch eine Zeile, deren Funktion nicht läuft. Ohne neue Ansicht bleibt die Antwort bytegleich und es wird nichts gelesen.
(Pilot-Feedback 2026-10-17; `test/kundenansichtKurzinfo.test.js`, `test/knFunktionen*.test.js`)
### Zuständigkeit
@@ -496,7 +496,7 @@ katalog: [{
}]
```
- Je Projekt und Funktion zusätzlich `anfrageStatus: ''|'offen'|'angenommen'|'abgelehnt'|'zurueckgezogen'` (nur Shop und Unterkunft).
- Je Projekt und Funktion zusätzlich `kurzinfo: string | null` (Live-Detail der Zeile in „Meine“, nur aktive Funktionen, Fehler → `null`; N52).
- Auf Anfrage (`?kurzinfo=1`, nur „Meine“) je Projekt und Funktion zusätzlich `kurzinfo: string | null` (Live-Detail der Zeile in „Meine“, nur aktive Funktionen, Fehler → `null`; ohne den Parameter fehlt das Feld; N52).
- Funktionen mit `verfuegbar: false` stehen trotzdem im Katalog. Die Oberfläche blendet sie aus (C9).
- Die Analyse erscheint im Katalog nur, wenn `ANALYSE_ENABLED === 'on'`.
+4 -1
View File
@@ -149,6 +149,9 @@ export function vergissMerker() {
const BILD = /^\/neu\/bilder\/[a-z0-9-]+\.webp$/
/** Nur diese Seite (Meine, Detailzeile) bittet den Server um die Kurzinfo; Etagen, Einschalt-Ablauf und Module laden GET /api/funktionen ohne den Parameter. */
const FUNKTIONEN_PFAD = '/api/funktionen?kurzinfo=1'
/** Stück für die Pille einer Zeile. */
const pille = (p) => h('span', { class: `kn-fn-pille kn-fn-pille--${p.ton}` }, p.text)
@@ -179,7 +182,7 @@ export async function zeigeFunktionen(wurzel, { route, me, nav } = {}, abh = {})
} else {
leeren(wurzel).append(h('p', { class: 'kn-leer' }, 'Wird geladen …'))
try {
[daten, offen] = await Promise.all([A.api('/api/funktionen'), ladeOffeneRechnungen(A.api)])
[daten, offen] = await Promise.all([A.api(FUNKTIONEN_PFAD), ladeOffeneRechnungen(A.api)])
} catch {
if (!aktuell()) return
leeren(wurzel).append(
+4 -2
View File
@@ -255,15 +255,17 @@ router.get('/', requireSession, async (req, res) => {
zeiten: await zeitenAngebot(req.session.email, customerId, abos),
}
// Kundenansicht neu (V5): Abo-Modell mit Kundenart, Abo-Stand, Katalog und Anfragestatus von Webshop und Buchungskalender je Projekt,
// dazu je Projekt und Funktion die Kurzinfo (Live-Detail der Zeile in "Meine", Text oder null; jede Abfrage begrenzt und fehlertolerant).
// dazu auf Anfrage (?kurzinfo=1, nur "Meine") je Projekt und Funktion die Kurzinfo (Live-Detail der Zeile, Text oder null; jede Abfrage
// begrenzt und fehlertolerant). Etagen, Einschalt-Ablauf und Module fragen ohne den Parameter: dann fehlt das Feld und es gibt keine Abfrage dafuer.
// Ohne neue Ansicht bleibt die Antwort wie sie war. Ein Lesefehler beim Abo-Stand ist ein Fehler der Liste (nie ein erfundenes "kein Abo").
if (neu) {
const mitKurzinfo = req.query.kurzinfo === '1' // genau "1"; alles andere (auch doppelt oder als Liste) heisst: ohne
const [kunde, abo, katalog, anfrageStatus] = await Promise.all([
kundenDokument(customerId),
aboProdukt(customerId, { now: jetzt }),
katalogFuerKunde({ email: req.session.email }),
anfrageStatusJeProjekt({ docs, projects }),
ergaenzeKurzinfos({ projects, docs, jetzt }).catch((err) => console.error(`[funktionen] Kurzinfo nicht lesbar: ${err?.name ?? 'Fehler'}`)),
mitKurzinfo ? ergaenzeKurzinfos({ projects, docs, jetzt }).catch((err) => console.error(`[funktionen] Kurzinfo nicht lesbar: ${err?.name ?? 'Fehler'}`)) : null,
])
for (const p of projects) {
p.features.shop.anfrageStatus = anfrageStatus.get(p.projectId)?.shop ?? ''
+1 -1
View File
@@ -4,7 +4,7 @@ import { isHanging } from '../newsPosts.js'
import { heute, plusTage, uhrzeit, zuUtc } from '../termine/zeit.js'
/**
* Kurzinfo der aktiven Funktionen fuer GET /api/funktionen in der neuen Ansicht: je Projekt und Funktion `kurzinfo`, ein kurzer
* Kurzinfo der aktiven Funktionen fuer GET /api/funktionen?kurzinfo=1 in der neuen Ansicht (nur "Meine" fragt danach): je Projekt und Funktion `kurzinfo`, ein kurzer
* Text fuer die zweite Zeile in "Meine" ("3 Termine heute · nächster 10:30", "2 neue Bestellungen", ...) oder null. Berliner Zeit.
*
* Regeln:
+19 -2
View File
@@ -15,7 +15,7 @@ function umgebung(fx, { d = daten(), abo, api, post = { feature: { state: 'setup
const basis = async (pfad, o = {}) => {
u.aufrufe.push([o.method ?? 'GET', pfad])
if (o.method === 'POST') return post
if (pfad === '/api/funktionen') return u.d
if (pfad === '/api/funktionen?kurzinfo=1') return u.d // nur „Meine“ fragt die Kurzinfo ab; ein anderer Pfad wäre „unerwartet“
if (pfad === '/api/invoices') { if (u.rechnungenFehler) throw Object.assign(new Error('x'), { status: 500, body: {} }); return { invoices: u.rechnungen, incomplete: false } }
if (pfad === '/api/abo') { if (abo instanceof Error) throw abo; return abo ?? { subscription: null } }
throw new Error(`unerwartet ${pfad}`)
@@ -223,10 +223,27 @@ test('Meine: Pille und Pfeil liegen rechts nebeneinander (Pille zuerst); Namen u
}))
/* ---------------------------------------------------------------- Teil 2: Overlay, Erfolgsmeldung, Wechsel --- */
test('Kurzinfo nur auf Anfrage: die Seite lädt genau einmal GET /api/funktionen?kurzinfo=1, in beiden Reitern und nach „Nochmal versuchen“', mit(async (fx) => {
const u = umgebung(fx)
await u.zeige(MEINE)
assert.deepEqual(u.aufrufe.filter(([, p]) => p.startsWith('/api/funktionen')), [['GET', '/api/funktionen?kurzinfo=1']])
vergissMerker()
const e = umgebung(fx)
await e.zeige(ENTDECKEN)
assert.deepEqual(e.aufrufe.filter(([, p]) => p.startsWith('/api/funktionen')), [['GET', '/api/funktionen?kurzinfo=1']])
vergissMerker()
let versuche = 0
const f = umgebung(fx, { api: async (pfad) => { if (pfad.startsWith('/api/funktionen')) { versuche += 1; assert.equal(pfad, '/api/funktionen?kurzinfo=1'); if (versuche === 1) throw new Error('x'); return daten() } if (pfad === '/api/invoices') return { invoices: [], incomplete: false }; throw new Error(`unerwartet ${pfad}`) } })
await f.zeige(MEINE)
await fx.klick(fx.finde(f.wurzel, '[data-aktion="nochmal"]'))
assert.equal(versuche, 2)
assert.equal(fx.finde(f.wurzel, '.kn-fehler'), null) // die Fehlermeldung ist weg, die Seite steht
}))
test('Bestätigung als Overlay: Zurück-Geste bringt die Liste ohne neuen Abruf zurück, Abbrechen ebenso', mit(async (fx) => {
const u = umgebung(fx)
await u.zeige(ENTDECKEN)
const geladen = () => u.aufrufe.filter(([m, p]) => m === 'GET' && p === '/api/funktionen').length
const geladen = () => u.aufrufe.filter(([m, p]) => m === 'GET' && p === '/api/funktionen?kurzinfo=1').length
await fx.klick(fx.finde(u.wurzel, 'section[data-fn="news"] [data-aktion="einschalten"]'))
assert.match(fx.text(u.wurzel), /News-Pinnwand einschalten/)
assert.equal(fx.alle(u.wurzel, 'section[data-fn]').length, 0)
+27 -2
View File
@@ -100,7 +100,8 @@ function plan(woche) {
const zeitenDaten = (woche) => ({ land: 'RP', plaene: [plan(woche)], feiertage: {}, ausnahmen: [], hinweise: [], kontakt: { an: false, telefon: '', adresse: '', whatsapp: null } })
const zeitenDok = (p, daten) => aw.seed('oeffnungszeiten', { $id: fid(p, 'zeiten'), customerId: 'c1', projectId: p, featureId: fid(p, 'zeiten'), version: 1, teamVersion: 0, daten: JSON.stringify(daten) })
const holen = async (cookie = c1) => (await get('/api/funktionen', { cookie }))
const holen = async (cookie = c1) => (await get('/api/funktionen?kurzinfo=1', { cookie })) // so fragt „Meine“ (kurzinfo nur auf Anfrage)
const holenOhne = async (pfad = '/api/funktionen', cookie = c1) => (await get(pfad, { cookie })) // Etagen, Einschalt-Ablauf, Module
const info = (b, p, key) => b.projects.find((x) => x.projectId === p).features[key].kurzinfo
const abfragen = (sammlung) => aw.listCalls(sammlung)
@@ -330,9 +331,10 @@ test('ohne neue Ansicht: keine Kurzinfo in der Antwort, keine einzige Abfrage da
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' }); bestellung('p1', 'bezahlt'); zettel('p1'); anfrage('p1', 'neu', '2026-12-01', '2026-12-03')
config.kundenansichtNeu.mode = 'off'
aw.resetListCalls()
const alt = await holen()
const alt = await holen() // auch mit ?kurzinfo=1 ändert die alte Ansicht nichts
assert.doesNotMatch(JSON.stringify(alt), /kurzinfo/)
for (const sammlung of ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen']) assert.equal(abfragen(sammlung).length, 0, sammlung)
assert.deepEqual(await holenOhne(), alt)
config.kundenansichtNeu.mode = 'pilot'
config.kundenansichtNeu.pilotEmails = ['k2@example.invalid'] // Kunde nicht in der Liste
assert.deepEqual(await holen(), alt)
@@ -343,6 +345,29 @@ test('ohne neue Ansicht: keine Kurzinfo in der Antwort, keine einzige Abfrage da
assert.deepEqual(neu, alt)
})
/* ------------------------------------------------------------------------------------ nur auf Anfrage --- */
test('Kurzinfo nur auf Anfrage: ohne ?kurzinfo=1 fehlt das Feld überall und es gibt keine einzige Abfrage dafür; der Rest der Antwort ist gleich', async () => {
alleAktiv('p1'); projekt('p2'); funktion('p2', 'termine'); funktion('p2', 'news', 'setup')
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' }); bestellung('p1', 'bezahlt'); zettel('p1'); anfrage('p1', 'neu', '2026-12-01', '2026-12-03')
zeitenDok('p1', zeitenDaten({ 6: ['09:00', '18:00'] }))
const sammlungen = ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen']
for (const pfad of ['/api/funktionen', '/api/funktionen?kurzinfo=0', '/api/funktionen?kurzinfo=', '/api/funktionen?kurzinfo=true', '/api/funktionen?kurzinfo=1&kurzinfo=0', '/api/funktionen?kurzinfo[]=1', '/api/funktionen?x=1']) {
aw.resetListCalls()
const ohne = await holenOhne(pfad)
assert.doesNotMatch(JSON.stringify(ohne), /kurzinfo/, pfad)
assert.equal(ohne.modell, 'abo', pfad) // die neue Ansicht bekommt sonst alles wie bisher
for (const sammlung of sammlungen) assert.equal(abfragen(sammlung).length, 0, `${pfad}: ${sammlung}`)
}
aw.resetListCalls()
const mit = await holen()
for (const sammlung of sammlungen) assert.ok(abfragen(sammlung).length > 0, sammlung)
assert.equal(info(mit, 'p1', 'termine'), '1 Termin heute · nächster 10:30')
assert.equal(info(mit, 'p2', 'news'), null) // mit Anfrage haben auch Funktionen ohne Text das Feld (null)
const ohne = await holenOhne()
for (const p of mit.projects) for (const f of Object.values(p.features)) delete f.kurzinfo
assert.deepEqual(ohne, mit)
})
test('die Kurzinfo ist ein kurzer Text ohne Preis, Mail-Adresse oder Namen von Gästen', async () => {
alleAktiv('p1')
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z', gastName: 'Erika Beispiel', gastEmail: 'erika@example.invalid', preisCent: 4500 })