Explorar o código

Zoekveld op Home (P2-26) + drie look&feel-fixes; taken 57-59

Zoekveld (P2-26) volledig afgerond, export-geverifieerd en live getest op
de telefoon-emulator:
- API Call homeTabel: variabele + query-parameter `zoek`
- HomeUitgaantabelKaartComponent: parameter `zoekterm` (nullable) tot in
  de Backend Query
- App State `zoekterm`, TextField op Home met On Change -> Update App
  State + herlaadLijsten
- alle 5 tabs (services_3 t/m _7) gebonden

Look & feel: actieve datumfilter-chip van logo-rood naar de oranje
actiekleur, ontbrekend menu-icoon "Zoek stad" (stond op size 1), en de
twee headerknoppen dezelfde vorm (beide radius 8).

TASKS.md: taak 57 (resterende look&feel, verwijst naar de review van de
andere sessie), taak 58 (performance/caching op volgorde van winst, met
het antwoord dat Views-caching hier niets oplevert), taak 59 (helppagina
via een Drupal-node + Launch URL).

CLAUDE.md: recept voor een query-parameter door de hele keten, de
Set-Variable-dialoog die wel werkt in de Action Flow Editor, scherpere
hover-truc, en waarom Views-caching hier niet helpt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bob hai 14 horas
pai
achega
0c8319db13
Modificáronse 2 ficheiros con 257 adicións e 14 borrados
  1. 82 0
      CLAUDE.md
  2. 175 14
      TASKS.md

+ 82 - 0
CLAUDE.md

@@ -3650,6 +3650,88 @@ parameternaam zijn). Bevestigd 2026-09-05: `onzin_param=123` gaf exact
 dezelfde 25 items als vijf serieuze kandidaat-filternamen, wat pas hard
 maakte dat er geen exposed datumfilter bestond.
 
+**Een query-parameter door de hele keten heen leggen (API Call → component →
+pagina) is één vast recept — 2026-09-16 in één ronde gelukt voor `zoek`.** In
+deze volgorde, want elke stap heeft de vorige nodig:
+1. **API Call** → tab *Variables* → "+ Add Variable" (naam, String, default
+   leeg) → **Save** → tab *Query Parameters* → "+ Add Query Parameter" → naam →
+   Value Source **From Variable** → Select Variable → de nieuwe variabele →
+   **Save**. Twee keer opslaan; de variabele moet bestaan vóór de parameter 'm
+   kan kiezen. Verschijnt de variabele in die tweede dropdown, dan wéét je dat
+   de eerste Save is doorgekomen.
+2. **Component** → root-node in de tree selecteren → potlood naast *Component
+   Parameters* → "+ Add Parameter" → naam, Type **String**, en **"Required"
+   uitvinken** (anders genereert FlutterFlow een `!` en crasht de pagina zodra
+   de waarde nog leeg is — hetzelfde patroon als bij het bekende P1-15-geval).
+3. **Backend Query** van de lijstwidget → *Edit* → onderrand omlaag slepen →
+   "+ Set Additional Variable" → Parameter Name = de nieuwe variabele → het
+   icoontje naast **Value** → Set Variable → **Component Parameters** → de
+   parameter → Confirm. De `page`-binding van Infinite Scroll overleeft dit.
+4. **Pagina** → per component-instantie de parameter aan App State binden.
+Verifieer na elke stap met een verse export; stap 1 zie je terug als
+`'zoek': zoek` in `params`, stap 3 als `zoek: widget!.zoekterm`.
+
+**De "Set Variable"-dialoog in de ACTION FLOW EDITOR werkt wél — in het compacte
+Actions-paneel ernaast niet.** Belangrijke nuance op de waarschuwing elders in
+dit bestand. Bij een `Update App State`-actie deed een klik op het `Unset`-veld
+én op het icoontje naast "Value to set" in het rechterpaneel **niets** (3
+pogingen). Zodra je de volledige editor opent (knop **Edit** naast "Action Flow
+Editor", reken op twee klikken) en daar het icoontje naast "Value to set" klikt,
+opent de dialoog gewoon en is hij volledig bruikbaar — inclusief het uitklappen
+van bronnen en de hover-truc. Klap eerst de trigger-kolom in met het
+**"|<"-icoontje linksboven**; dan is het rechterpaneel breed genoeg.
+Werkt in die editor ook gewoon: het actie-zoekveld (typ `herlaad` en de custom
+action staat er als Top Result).
+
+**De hover-truc voor een leeg gerenderde optielijst: hover eerst ergens ANDERS,
+dán op de rij — en tel de kliks.** Scherper dan de beschrijving elders, 5×
+herhaald in één sessie:
+- Eén klik op de sectiekop klapt uit. Een tweede klik klapt 'm weer **dicht**,
+  dus klik nooit "omdat er niets gebeurt" — kijk eerst met een `zoom` naar de
+  chevron: wijst hij omlaag, dan staat hij open en is de rij alleen onzichtbaar.
+- De hover werkt alleen als de muis er **naartoe beweegt**. Stond hij er al, dan
+  gebeurt er niets. Vaste reeks die elke keer werkte: `hover` op een punt 100+ px
+  weg → wacht 3 s → `hover` op de rij → wacht 10 s → `zoom` → klikken.
+- Zit de rij er na twee volledige rondes nog niet, dan klopt de aanname over de
+  y-positie meestal niet: de rij zit ±20 px onder de kop "Available Options",
+  de optie zelf ±38 px onder de sectiekop.
+
+**Het commandopalet (de zoekbalk bovenin) negeert structureel de EERSTE
+typ-actie.** Het palet opent wel, maar toont dan de resultaten van je vórige
+zoekopdracht terwijl het zoekveld leeg lijkt — klik je dan op "het eerste
+resultaat", dan open je de verkeerde pagina. Vaste werkwijze: klik de zoekbalk,
+typ, en typ **nog een keer** (zonder opnieuw te klikken); pas als de screenshot
+jouw term in het veld toont, klik je een resultaat aan. Dit kostte deze sessie
+vijf keer een extra ronde.
+
+**Tekstvelden in het rechterpaneel: `left_click` + `ctrl+a` + typen is de
+betrouwbaarste combinatie — ook voor gewone tekstvelden, niet alleen numerieke.**
+Dit corrigeert de oudere notitie die `triple_click` als vaste route voor
+tekstvelden noemt. Bij het *Hint Text*-veld faalde `triple_click` + typen twee
+keer stil (veld bleef de oude waarde tonen, geen foutmelding); dezelfde plek met
+`left_click` + `ctrl+a` + typen lukte meteen. Commit daarna met een klik op een
+**sectiekop**, niet met Tab.
+
+**"Zoek stad" in het menu miste zijn icoon — het stond op `size: 1.0`, niet op
+ontbrekend.** Algemener punt: een ListTile's leading-icoon is géén tree-node
+(geen chevron op de rij), maar een property. Je vindt 'm door het widget te
+selecteren en het eigenschappen-zoekveld op **`icon`** te filteren; dan
+verschijnen *Leading Icon Properties* en *Trailing Icon Properties* met elk hun
+eigen Icon / Icon Size / Icon Color. Een afwijkende waarde valt in de builder
+niet op maar in de export meteen: `grep -A3 "leading: Icon(" <widget>.dart` en
+kijk welke rij een expliciete `size:` heeft die de andere niet hebben.
+
+**Views-caching aanzetten voor de app-endpoints heeft geen zin — en op
+`flutterflowmobiel_establishments` is het riskant.** Views-cache zit áchter
+Drupal's page cache, en sinds taak 54 zit Cloudflare daar nog eens vóór met 5
+minuten edge-cache; vrijwel elk app-verzoek is anoniem en wordt dus al
+beantwoord vóórdat Views draait. Alleen ingelogde calls passeren de page cache
+(`entreeopties`, `categorieen`, `mijn_horecagelegenheden`, `mijn_stadsrechten`)
+en die zijn klein. Op de establishments-view zou een time-based Views-cache
+bovendien het per-gebruiker favorietenfilter uit `custom_views_query_alter()`
+kunnen serveren aan de verkeerde bezoeker. De winst zit in edge-caching en in
+minder calls per scherm, niet in Views.
+
 ## Domein/architectuurcontext
 
 - **Geen user-specifieke elementen op overzichtspagina's — besluit Bob,

+ 175 - 14
TASKS.md

@@ -28,20 +28,179 @@ FlutterFlow-commit op `main`**, de commit-knop in het Version Control-paneel
 reageerde niet op Claude's klik. `dart analyze` op de verse export: 0 errors.
 Emulator-5556 heeft nu de profile-APK van die export + per-app-locale `nl-NL`.*
 
-### 🔴 IN UITVOERING 2026-09-16 (Claude, sessie "zoekveld + look&feel") — Eigenaar: Claude — bezig
+### ✅ Sessie 2026-09-16 (Claude, builder + verse export per stap) — zoekveld + look&feel
 
-*Geclaimd om 00:0x, vóór het builder-werk begon. Bob's opdracht: "maak van het
-zoekveld een taak, doe dat direct; pas ook de look & feel suggesties aan; je kunt
-bij de browser". Raakt deze onderdelen — een andere sessie moet hier NIET
-tegelijk in bouwen:*
-
-- **`home` (pagina)** — zoekveld boven de tabs, chip-kleur.
-- **API Call `homeTabel`** — nieuwe query-variabele `zoek`.
-- **`HomeUitgaantabelKaartComponent`** — nieuwe parameter `zoekterm` + Backend Query.
-- **`HeaderButtonsComponent`** — knopvorm terugknop.
-- **`drawerComponent`** — ontbrekend icoon "Zoek stad" + Help-menu-item.
+*Opdracht Bob: "maak van het zoekveld een taak, doe dat direct; pas ook de look &
+feel suggesties aan; je kunt bij de browser." Alles hieronder is geverifieerd met
+een verse export; `dart analyze` op die export: **0 errors**.
+FlutterFlow-commit: Bob.*
 
-*Niet geraakt (bewust): de twee aanmaakpagina's (P2-29, andere sessie).*
+**P2-26 · Zoekveld op de Home-evenementenlijsten — ✅ AF.** De Drupal-kant stond
+al klaar (taak 55, `zoek=` op alle zeven `flutterflowmobiel1`-displays);
+nagemeten op productie met cache-buster vlak vóór het bouwen:
+`zoek=jazz` → 3, `zoek=qqqzzz` → 0, `zoek=` (leeg) → 50, controleparameter
+`onzin=jazz` → 50. Een lege waarde schakelt het filter dus netjes uit.
+Wat er nu staat:
+- API Call **`homeTabel`**: variabele `zoek` (String, default leeg) +
+  query-parameter `zoek` → From Variable. Export: `'zoek': zoek` in `params`.
+- Component **`HomeUitgaantabelKaartComponent`**: parameter `zoekterm`
+  (String, **niet** required → nullable, dus geen `!`-crash zoals bij het
+  bekende P1-15-patroon), doorgegeven in de Backend Query als
+  `zoek: widget!.zoekterm`.
+- App State **`zoekterm`** (String, niet persisted, default leeg).
+- **Home**: een `TextField` met hint *"Zoek op titel"*, On Change →
+  `Update App State zoekterm = Widget State TextField` → custom action
+  **`herlaadLijsten`** (dezelfde die voor P2-1 gebouwd is; die reset de
+  `pagingController`, een Value Key doet dat niet — zie `CLAUDE.md`).
+- Alle **5** component-instanties (services_3 t/m _7) krijgen
+  `zoekterm: FFAppState().zoekterm`. Geverifieerd: 5 treffers in de export.
+- Reken op ~2 s vertraging na de laatste toetsaanslag: FlutterFlow zet
+  automatisch een `EasyDebounce` van 2000 ms op On Change en die is niet
+  instelbaar. Bij server-side zoeken is dat juist prettig.
+
+**Restpunt voor Bob (1 sleepactie, 10 seconden in je eigen browser):** het
+zoekveld staat nu **bovenaan** de Column, dus bóven de slider. Bedoeld was:
+slider bovenaan, dan zoekveld, dan de chips. Twee sleeppogingen in de widget
+tree landden niet (bekend probleem, zie `CLAUDE.md`). Fix: sleep in de tree op
+`home` → `Column` de node **`Container`** (die met de slider erin) op de
+`Column`-rij; een drop op een container-rij voegt altijd als eerste kind in, dus
+daarna staat de volgorde goed. Functioneel maakt het niets uit — het zoekveld
+werkt waar het nu staat, en op het horeca-overzicht staat het zoekveld ook
+bovenaan.
+
+**Live getest op de telefoon-emulator (411 dp, profile-APK van deze export,
+per-app-locale `nl-NL`):** zoeken op `jazz` filtert de Uitgaan-tab meteen naar
+"Newport Jazz Session" en "Swingende Jazz Middag"; de tab Activiteiten filtert
+mee en toont terecht zijn Empty List Widget (geen jazz-activiteiten). De
+gemeentekeuze blijft staan, de slider blijft staan, er is geen paginaherlaad.
+*Let op, bestaand gedrag en geen regressie:* de Home-tabs zijn **landelijk** —
+`homeTabel` kent geen `townid`-parameter — dus je krijgt ook treffers uit
+Rotterdam en Groningen terwijl de header Amsterdam toont.
+
+**Look & feel — drie punten aangepast, alle drie export-geverifieerd en op het
+toestel nagekeken:**
+1. **De actieve datumfilter-chip was logo-rood** (`primary` #9A141D) terwijl het
+   merk-DNA zegt: rood = identiteit/chroom, oranje = actie, en de site rood
+   **nooit** als knopkleur gebruikt. Nu `secondary` (#FF680D). Export:
+   `selectedChipStyle: ChipStyle(backgroundColor: ...secondary`.
+2. **"Zoek stad" in het hoofdmenu miste zijn icoon** — het bleek niet te
+   ontbreken maar op `size: 1.0` te staan (alle andere menu-items hebben geen
+   expliciete size en krijgen dus de default 24). Nu `size: 24.0`, zelfde beeld
+   als de rest van het menu.
+3. **De twee headerknoppen hadden verschillende vormen**: hamburger
+   `borderRadius: 8`, terugknop `borderRadius: 30` (rond). Beide nu 8. Dat
+   raakt élke pagina, want `HeaderButtonsComponent` staat op 11 pagina's.
+
+**Niet gedaan, bewust (staat hieronder als taak 57):** de overige look&feel-
+punten uit de review van 15/16 sep — `EventCurrent` (ligt bij de andere sessie,
+P2-30), de twee kaartstijlen, het horeca-overzicht en de horeca-detailpagina.
+
+### 🆕 Taak 57 · Look & feel — twee punten die de review hieronder NIET dekt
+
+*De uitgebreide look&feel-review van dezelfde dag (blok "🎨 Look&feel-review
+2026-09-16" verderop) dekt de rest al en met meer detail: P1-51 headerkleuren,
+P1-52 `EventCurrent`, P2-30 horeca-overzicht, P2-31 kaartstijlen, P2-32 titels,
+P2-33 horeca-detail. **Werk uit die lijst, niet uit deze** — hier staan alleen
+de twee dingen die daar niet in voorkomen, gezien op de telefoon-emulator.*
+
+1. **Het Cultuur-icoon in het hoofdmenu springt uit de lijn** — het staat iets
+   lager en groter dan de zes andere icoontjes in het Gemeente-blok. Zelfde soort
+   oorzaak als het "Zoek stad"-icoon dat deze sessie gefixt is (dat stond op
+   `size: 1.0`): kijk met `grep -A3 "leading: Icon(" drawer_component_widget.dart`
+   welke rij een afwijkende `size:` heeft. Kleine ingreep.
+2. **Het menu is lang doordat het Gemeente-blok en het Provincie-blok dezelfde
+   zeven rijen herhalen** (Uitgaan, Activiteiten, Cultuur, Films, Jeugd, Horeca,
+   Thuis bezorgen). Overweeg het Provincie-blok standaard ingeklapt te tonen.
+   **Besluit voor Bob**, want het verandert hoe mensen navigeren.
+
+*Hangt samen met P1-50:* op `PUitgaanPage` staat een zwart **"Ad Loading…"**-blok.
+In een testbuild is dat de placeholder, maar zonder consent-initialisatie is dit
+in productie waarschijnlijk een leeg of zwart vlak. Los P1-50 op (banner weg óf
+consentflow aan), dan is dit punt vanzelf weg.
+
+### 🆕 Taak 58 · Performance — caching, op volgorde van winst
+
+*Uitgezocht 2026-09-16 (Claude, code + productiemetingen). De volgorde is
+opbrengst gedeeld door moeite; 1 en 2 zijn de moeite waard, 3 is klein, 4 niet
+vóór livegang.*
+
+**Eerst het antwoord op Bob's vraag "moet ik caching invullen in de views?" —
+nee, laat Views-caching op "None".** Views-cache zit **áchter** Drupal's page
+cache. Vrijwel elk verzoek van de app is anoniem, dus de page cache antwoordt al
+vóórdat Views aan de beurt komt; sinds taak 54 zit Cloudflare er nog eens vóór
+met 5 minuten edge-cache. Alleen ingelogde verzoeken passeren de page cache, en
+dat zijn hier vier kleine calls (`entreeopties`, `categorieen`,
+`mijn_horecagelegenheden`, `mijn_stadsrechten`). Op `flutterflowmobiel_establishments`
+is een time-based Views-cache zelfs **riskant**, want die view wordt in
+`custom_views_query_alter()` per gebruiker op favorieten gefilterd. Meetbewijs
+uit de sessie van 15 sep: dezelfde URL drie keer achter elkaar gaf 1,23 s (MISS)
+→ 0,10 s (HIT) → 0,11 s (HIT). Drupal's page cache doet dus 12× zoveel als alle
+micro-optimalisaties samen.
+
+1. **`flutterflowmobiel_establishments.json` alsnog in de Cloudflare-rule
+   "FlutterFlow publieke endpoints".** Grootste winst die er nog ligt. Dit is
+   het zwaarste endpoint (100 items per pagina, 1,3–1,8 s gemeten) en het
+   horeca-overzicht laadt er **zes** van, één per tab. Met de bestaande
+   rule-voorwaarde *"GET zonder `SESS`-cookie"* is het veilig: de app stuurt op
+   `HorecagelegenheidoverzichtCall` **geen** sessiecookie mee (`headers: {}` in
+   de export), en de favorietenfilter in `custom_views_query_alter()` grijpt
+   alleen bij een ingelogd verzoek. Verwachte winst: ~1,5 s → ~0,1 s per tab.
+   ⚠️ Zet dit endpoint **nooit** in de rule zonder die cookie-voorwaarde.
+2. **`cache: true` op meer API Calls.** Nu staat de vlag op 5 van de 18:
+   `homeTabel`, `EstablishmentInfo`, `Gemeenten`, `Provincies`,
+   `Horecagelegenheidoverzicht`. Ontbreekt op user-onafhankelijke reads die
+   binnen één sessie vaak herhaald worden: **`homeSlider`**, **`Uitgaanstabel`**,
+   **`UitgaanSlider`**, **`Evenement`** (evenement-detail),
+   **`HorecagelegenheidEvents`**, **`PlaatsenBijGemeente`**, **`Categorieen`**,
+   **`Entreeopties`**. Levert winst bij terugnavigeren binnen een sessie.
+   *De cache is veilig* — `ApiCallOptions extends Equatable` met `params` en
+   `headers` in `props`, dus hij vergelijkt op echte parameterwaarden.
+   *Kanttekening:* FlutterFlow's cache verloopt nooit binnen een sessie, dus
+   voor de **sliders** betekent dit dat een gebruiker die de app uren open laat
+   staan oude slider-inhoud houdt. Voor `Evenement` en `Entreeopties` is dat
+   geen bezwaar.
+3. **Langere edge-TTL voor taxonomie.** `plaatsen.json` en
+   `plaatsen_bij_gemeente.json` veranderen zelden — 30 tot 60 minuten in plaats
+   van 5. Puur een getal in de bestaande Cloudflare-rule.
+4. **Lokaal persistent cachen** (Home meteen tonen uit een opgeslagen snapshot,
+   ook offline). Een custom action van gemiddelde omvang die slecht past op
+   FlutterFlow's FutureBuilder-patroon, en met Cloudflare op 0,1 s is de winst
+   klein. **Niet vóór livegang.**
+
+### 🆕 Taak 59 · Helppagina — antwoord op Bob's vraag, plus hoe te bouwen
+
+**Bob's vraag: "kan ik een helppagina maken waarin URL's van de website zelf
+getoond worden, zodat ik teksten niet 2× hoef bij te houden?" — Ja, en dat is
+ook het advies.** Gemeten 2026-09-16: de site heeft er nu nog niets voor staan.
+`uitgaanskrant.com/nl/help`, `/faq`, `/veelgestelde-vragen`, `/over-ons`,
+`/contact`, `/uitleg`, `/hoe-werkt-het` en `/privacy` geven alle **404**;
+`/account-verwijderen` bestaat wél maar geeft anoniem een **403**.
+
+**Aanpak, twee delen:**
+1. **Bob, in Drupal:** maak één node, bijvoorbeeld `/nl/app-help`, met de
+   inhoud. Wat er in elk geval in hoort: wat Uitgaanskrant is · gemeente kiezen
+   en volgen · inloggen en favorieten · een evenement of stadsactiviteit
+   aanmaken (voor horeca en city-editors) · contact · privacy · account
+   verwijderen. Eén node bijwerken = de app is meteen bij, géén nieuwe release.
+2. **Claude, in de builder (kleine taak):** menu-item **"Help"** onderaan
+   `drawerComponent` (dupliceer een bestaand `ListTile`, zie `CLAUDE.md` — dat
+   is betrouwbaarder dan Insert Widget) met On Tap → **Launch URL** naar die
+   node. Launch URL staat in de Actions-zoeklijst onder categorie *Share*, niet
+   onder Navigation.
+
+**Keuze die Bob daarbij maakt: extern openen of in de app?**
+- **Launch URL** (externe browser) is één actie, nul onderhoud, en de gebruiker
+  ziet de vertrouwde site. Nadeel: hij verlaat de app.
+- **Een WebView-pagina in de app** houdt hem binnen de app en kan de standaard
+  header + terugknop krijgen, maar toont de volledige sitehuisstijl inclusief
+  menu en cookiebanner, tenzij je in Drupal een kale weergave maakt.
+- **Advies: begin met Launch URL.** Dat kost tien minuten en is altijd nog om
+  te bouwen naar een WebView.
+
+⚠️ **Los hiervan, en het blokkeert de store-review:** `/nl/account-verwijderen`
+geeft anoniem een 403. Google's reviewers openen die URL **zonder in te loggen**.
+Daar moet minimaal een publiek zichtbare uitleg staan van hoe je je account laat
+verwijderen. Dit hoort bij Bob's taak 2 (Play Console).
 
 ### 🟢 AFGEROND 2026-09-16 (Claude, sessie "taal-cluster") — browserloos, geen builder aangeraakt
 
@@ -695,8 +854,10 @@ tonen; alternatief is default "Amsterdam", te wisselen in dezelfde binding).
 Verifiëren op telefoonformaat (393 dp): hamburger + terugknop + logo + naam
 moeten op één regel passen, ook bij "'s-Hertogenbosch".
 
-**P2-26 · Zoekveld op de evenementenlijsten · ⚠️ HERZIEN 2026-09-15 (Claude,
-gemeten) — het horeca-patroon kan hier NIET, dit begint bij jou in Drupal.**
+**P2-26 · Zoekveld op de evenementenlijsten · ✅ AFGEROND 2026-09-16 (Drupal:
+Bob, taak 55; app-kant: Claude) — zie de sessienotitie bovenaan dit bestand.**
+
+*Historie, ter referentie (de analyse die tot de gekozen aanpak leidde):*
 De aanname "zelfde patroon als horeca" gaat niet op. Horeca laadt zijn hele
 lijst in `_model.alleX` en filtert client-side; de Home-tabs draaien op een
 `PagedMasonryGridView` met een `pagingController` (server-side paginering, 25