diff --git a/docs/superpowers/plans/2026-10-16-kundenansicht-00-uebersicht.md b/docs/superpowers/plans/2026-10-16-kundenansicht-00-uebersicht.md index d2057a4..891a3b0 100644 --- a/docs/superpowers/plans/2026-10-16-kundenansicht-00-uebersicht.md +++ b/docs/superpowers/plans/2026-10-16-kundenansicht-00-uebersicht.md @@ -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.` 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.` 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'`. diff --git a/public/neu/ansicht-funktionen.js b/public/neu/ansicht-funktionen.js index d16738a..44fd5e4 100644 --- a/public/neu/ansicht-funktionen.js +++ b/public/neu/ansicht-funktionen.js @@ -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( diff --git a/server/routes/funktionen.js b/server/routes/funktionen.js index 9b203cd..8d6986a 100644 --- a/server/routes/funktionen.js +++ b/server/routes/funktionen.js @@ -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 ?? '' diff --git a/server/services/kundenansicht/kurzinfo.js b/server/services/kundenansicht/kurzinfo.js index d396e64..714c277 100644 --- a/server/services/kundenansicht/kurzinfo.js +++ b/server/services/kundenansicht/kurzinfo.js @@ -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: diff --git a/test/knFunktionenAnsicht.test.js b/test/knFunktionenAnsicht.test.js index d9cfa66..a629b2b 100644 --- a/test/knFunktionenAnsicht.test.js +++ b/test/knFunktionenAnsicht.test.js @@ -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) diff --git a/test/kundenansichtKurzinfo.test.js b/test/kundenansichtKurzinfo.test.js index fd441a6..0f131e8 100644 --- a/test/kundenansichtKurzinfo.test.js +++ b/test/kundenansichtKurzinfo.test.js @@ -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 })