Bläddra i källkod

Taak 128: plan voor nieuwsbrief-abonnementen in de app

Simplenews-kant volledig doorgemeten op productie: 3 nieuwsbrieven
(taxonomy-termen, vocabulary newsletter), opslag en statuscodes, en het
verschil tussen de confirm=TRUE- en confirm=FALSE-tak van
simplenews_subscribe_user.

Belangrijkste bevindingen:
- De user-settings-pagina abonneert een ingelogde gebruiker DIRECT en
  stuurt geen enkele mail; een bevestigingsmail hoort bij confirm=TRUE en
  laat het abonnement op onbevestigd staan. Er is geen welkomstmail-Rule.
  Daarom geeft de resource de status terug i.p.v. een bool: dan werkt de
  app bij beide keuzes.
- De wekelijkse agenda hangt al aan de flag favorite_town, die de app via
  "Gemeente volgen" al zet. Alleen het abonnement zelf ontbreekt.
- 450 onbevestigde inschrijvingen, en 17 van de 18 actieve abonnees op de
  wekelijkse agenda volgen nul gemeenten.

Bob's keuzes: ondernemersnieuwsbrief alleen voor horeca-eigenaren
(server-side gefilterd), waarschuwing bij 0 gevolgde gemeenten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bob 21 timmar sedan
förälder
incheckning
a7fe5993dc
1 ändrade filer med 182 tillägg och 0 borttagningar
  1. 182 0
      TASKS.md

+ 182 - 0
TASKS.md

@@ -33,6 +33,7 @@ Google Play en één doorloop die alleen Bob kan doen.
 
 
 | **114** | Opruimen na de filterbalk-ombouw | **Eigenaar: Bob.** Restjes van taak 113 (afgerond 2026-09-20): (a) component-parameter **`parameter1`** op `FilterBalkComponent` ontstond automatisch bij *Convert to Component* en wordt nergens gelezen — mag weg, en daarmee ook de meegegeven waarde op Home en `PUitgaanPage`; (b) **page state `zoekOpen` op Home** is overbodig (de App State-versie stuurt alles); (c) App State **`zoekActief`** (Boolean) bestond al vóór deze taak en heeft **0 gebruikers**; (d) wegwerp: componenten **`FilterBalkKanweg`** en **`FilterBalkComponentCopy`** + pagina's **`homeCopy`** en **`PUitgaanPageCopy`**. Geen van alle blokkeert iets — `dart analyze` geeft 0 errors |
+| **128-B/C/D** | Nieuwsbrieven-blok op `mijnProfiel` | **Wacht op 128-A (Drupal).** Plan is af en doorgemeten — blok, waarschuwing bij 0 gevolgde gemeenten, vertalingen. Eigen blok hieronder |
 
 ## 👤 BOB — OPEN, in volgorde van wat de livegang blokkeert
 
@@ -54,6 +55,7 @@ Google Play en één doorloop die alleen Bob kan doen.
 | **123** | `mijn_stadsrechten` uitbreiden met de keten | **Code staat klaar en is op productie getest.** Vervang regels 316-372 van `custom.evenementen_aanmaken.inc` + `cc all`. Blokkeert 124 |
 | **124-126** | Stadsrechten-cascade op `stadsactiviteitAanmaken` | Voorvullen van provincie/gemeente/plaats (124), een **crash** bij een lege plaatsenlijst (125) en een stille verkeerde plaats bij het wisselen van gemeente (126). Eigen blok hieronder |
 | **127** | **Kopie-knop op `MijnAanmeldingen`** (clonen) | Wens Bob 2026-09-20, **vóór de livegang**. Backend bestaat al; Drupal-patch voor 3 gaten staat klaar en is getest (127-A). App-deel in stappen B t/m F; het activiteitendeel (127-E) deelt de cascade met 124. Eigen blok hieronder |
+| **128-A** | **Nieuwsbrieven: Drupal-resource** | Wens Bob 2026-09-21. Plan + code-opzet staan klaar en zijn doorgemeten; alleen bouwen, deployen en de resource **plus de 3 operaties** aanvinken in het endpoint. Blokkeert 128-B/C/D. Eigen blok hieronder |
 
 ---
 #
@@ -81,6 +83,182 @@ verse export — zie de commit van die datum.)*
 ---
 
 
+## 📰 128 · Nieuwsbrieven aan/uit in de app · Eigenaar: Bob (A) + Claude (B-D)
+
+**Wens Bob 2026-09-21.** Op de website kan een ingelogde gebruiker op
+`/user/<uid>/simplenews` drie nieuwsbrieven aan- en uitvinken. Dat moet ook in
+de app kunnen. **Plan is af en volledig doorgemeten (2026-09-21); alleen A moet
+gebouwd/gedeployd worden vóór B t/m D kunnen.**
+
+### Wat er al staat — gemeten op productie
+
+Simplenews **7.x-1.1**, nieuwsbrieven zijn taxonomy-termen in vocabulary
+`newsletter` (vid 11). ⚠️ `variable_get('simplenews_vid')` geeft **0** — leid de
+vocabulary dus nooit daaruit af, gebruik `simplenews_category_get_visible()`.
+
+| tid | Nieuwsbrief | actief | onbevestigd | uitgeschreven |
+|---|---|---|---|---|
+| 18017 | Ondernemers nieuwsbrief | 11 | **360** | 0 |
+| 36667 | Uitgaanskrant voor bezoekers van de horeca | 16 | 47 | 1 |
+| 36676 | Uitgaanskrant.com wekelijkse uitgaansagenda | 18 | 43 | 2 |
+
+Opslag: `simplenews_subscriber` (snid/mail/uid/activated) +
+`simplenews_subscription` (snid/tid/**status**/source). Status: **1 = actief,
+0 = uitgeschreven, 2 = onbevestigd**.
+
+**De wekelijkse agenda (36676) hangt al aan de app.** De edities heten
+"Favoriete Gemeenten" en selecteren content via de flag **`favorite_town`
+(fid 2)** — precies de flag die "Gemeente volgen" in
+`SelectStateDropDownComponent` al zet via `favorieten/flag`. Er hoeft dus niets
+gekoppeld te worden; alleen het abonnement zelf ontbreekt.
+
+### ⚠️ De bevestigingsmail — gemeten, want hier zit een valkuil
+
+`simplenews_subscribe_user($mail, $tid, $confirm, $source)` heeft twee takken
+(`simplenews.module:1281`):
+
+- **`$confirm = FALSE`** → status meteen **1 (actief)**, **géén mail**, alleen
+  `module_invoke_all('simplenews_subscribe_user', ...)`. Dit is wat de
+  user-settings-pagina uit het screenshot doet
+  (`simplenews.subscription.inc:97`, source `'website'`).
+- **`$confirm = TRUE`** → status **2 (onbevestigd)** + bevestigingsmail met
+  link; pas ná het klikken wordt het actief. **Hier komen die 360 onbevestigde
+  inschrijvingen vandaan.**
+
+Een **welkomstmail bestaat niet**: `simplenews_rules` levert alleen het *event*,
+en geen van de 6 Rules op de site luistert ernaar (alle zes hangen aan
+`node_insert`/`user_insert`/`node_presave`). "Drupal doet dat standaard al"
+klopt dus niet voor de ingelogde route.
+
+**Daarom geeft de resource de STATUS terug, niet een kale bool** — dan werkt de
+app bij beide keuzes en is wisselen later één regel in Drupal, zonder
+FlutterFlow-wijziging. Zonder status krijg je bij `confirm = TRUE` een echte
+bug: de switch springt bij de volgende paginaload terug naar uit, want het
+abonnement is nog onbevestigd. Advies blijft `FALSE` (gebruiker is ingelogd,
+mailadres is bewezen, en het is exact wat de site zelf doet).
+
+### 128-A · Drupal: resource `nieuwsbrieven` · Eigenaar: Bob
+
+Zelfde vorm als het bestaande `favorieten` (flag/unflag/is_flagged):
+
+```
+GET  /nl/flutterdrup/nieuwsbrieven.json               -> lijst + status
+POST /nl/flutterdrup/nieuwsbrieven/subscribe.json     -> {"tid": 36676}
+POST /nl/flutterdrup/nieuwsbrieven/unsubscribe.json   -> {"tid": 36676}
+```
+
+Index-respons (alle waarden als string/int, `status` als int):
+
+```json
+[{"tid":"36676","naam":"...","omschrijving":"...","status":1}]
+```
+
+Kern van de index-callback:
+
+```php
+function _custom_nieuwsbrieven_index() {
+  global $user;
+  if (empty($user->uid)) return services_error('Niet ingelogd', 401);
+  $out = array();
+  foreach (_custom_nieuwsbrieven_zichtbaar() as $tid => $c) {
+    $sub = simplenews_subscriber_load_by_mail($user->mail);
+    $st  = ($sub && isset($sub->newsletter_subscription[$tid]))
+         ? (int) $sub->newsletter_subscription[$tid]->status : 0;
+    $out[] = array(
+      'tid' => (string) $tid,
+      'naam' => $c->name,
+      'omschrijving' => trim(strip_tags($c->description)),
+      'status' => $st,
+    );
+  }
+  return $out;
+}
+```
+
+**`_custom_nieuwsbrieven_zichtbaar()` is de spil** en wordt door alle drie de
+callbacks gebruikt: hij geeft `simplenews_category_get_visible()` terug (dat
+laat `hidden`-nieuwsbrieven automatisch weg), **minus tid 18017 wanneer de
+gebruiker geen horecagelegenheid heeft** (Bob's keuze 2026-09-21: de
+ondernemersnieuwsbrief alleen voor horeca-eigenaren). Eigenaarschap = dezelfde
+query als `_custom_mijn_horecagelegenheden_service()`
+(`custom.evenementen_aanmaken.inc:261`): `node` waar
+`type = CUSTOM_ESTABLISHMENT_NODE_TYPE` en `uid = $user->uid`.
+
+Door die filtering **server-side** te doen heeft de app géén conditie nodig, en
+kan een gemanipuleerd verzoek zich er ook niet op abonneren — mits
+subscribe/unsubscribe de binnenkomende tid tegen diezelfde lijst controleren
+(`if (!isset($lijst[$tid])) return services_error('Onbekende nieuwsbrief', 400);`).
+Zonder die guard kan een client zich op een verborgen nieuwsbrief abonneren.
+
+Subscribe/unsubscribe zijn daarna drie regels elk, met
+`simplenews_subscribe_user($user->mail, $tid, FALSE, 'app')`. **Gebruik source
+`'app'`** (niet `'website'`), dan is later meetbaar hoeveel abonnees uit de app
+komen. Geef als respons dezelfde rij terug als in de index, zodat de app de
+nieuwe status meteen kan tonen.
+
+⚠️ **Resource aanzetten is een APARTE stap na het deployen:** in
+`/admin/structure/services/list/flutterdrup/resources` de regel `nieuwsbrieven`
+aanvinken **én de drie losse operaties eronder** — precies waar
+`mijn_aanmeldingen` maandenlang op 404 stond. Daarna `cc all`.
+Verificatie: anoniem `curl` moet **403** geven (route bestaat, sessie
+ontbreekt), niet 404.
+
+⚠️ **Nooit edge-cachen** — user-specifiek. Het nieuwe pad valt buiten de
+bestaande Cloudflare Cache Rules (die matchen op `/flutterdrup/views/…`,
+`plaatsen`), dus dat gaat vanzelf goed; het mag er alleen nooit bij.
+
+### 128-B · App: blok "Nieuwsbrieven" op `mijnProfiel` · Eigenaar: Claude
+
+Derde blok onder "Mijn horecagelegenheden" en "Mijn redactierechten", in
+dezelfde vorm. Backend Query op de nieuwe call + **Generate Dynamic Children**,
+zodat een vierde nieuwsbrief vanzelf verschijnt. Per rij: naam, omschrijving
+eronder (`bodySmall`/`secondaryText`, het hulpregel-patroon van taak 91), rechts
+een **Switch**. On Toggle -> `drupalRequest` naar subscribe/unsubscribe met
+`{"tid": …}` — exact het patroon van het hartje op
+`HorecagelegenheidCurrent`.
+
+⚠️ **Predefined Path `items` (`$[:]`, Is List) gebruiken, geen JSON Path `$`** —
+anders crasht het blok bij een mislukte call (`jsonBody == null`).
+⚠️ **Testrespons in FlutterFlow moet gevuld zijn** vóór je `$.status` bindt,
+anders krijgt het pad type "Anything" en plakt de builder er `.toString()`
+achter.
+
+Bij status 2 een regeltje "Check je mail om je inschrijving te bevestigen"
+tonen (Visibility-conditie op `$.status == 2`). Bij `confirm = FALSE` komt die
+stand nooit voor, maar dan is de app al klaar als Bob later wisselt.
+
+### 128-C · Waarschuwing bij 0 gevolgde gemeenten · Eigenaar: Claude
+
+Bob's keuze 2026-09-21. **Gemeten: 17 van de 18 actieve abonnees op de
+wekelijkse agenda hebben nul favoriete gemeenten**, en de laatste editie
+(nid 222817, 2026-09-17) ging naar `sent_subscriber_count: 3`. Wie die switch
+aanzet zonder gemeenten te volgen, krijgt dus niets — zonder enige uitleg.
+
+Onder de rij van tid 36676 een waarschuwing tonen wanneer
+`FFAppState().favorieteGemeenteIds` leeg is, met een knop naar het
+gemeente-keuzescherm. Visibility via de bekende route: App State-lijst ->
+Available Options **Is Set and Not Empty** + **Apply Opposite Statement**.
+De conditie op de tid zelf hoort een **bool custom function** te zijn (geen
+JSON-Path-binding) — anders komt er `if (getJsonField(...))` uit, een `dynamic`
+als `bool` zonder null-check.
+
+*(Niet nagelezen: de scs-contentselectie zelf. De tabel heet niet `scs_node`;
+ik heb dus niet hard bewezen dát die 17 een lege mail krijgen — het beeld past
+wel. Wie dit ooit exact wil weten, begint bij node 189237, de scheduler-parent.)*
+
+### 128-D · Vertalingen · Eigenaar: Claude
+
+Kop + waarschuwingstekst + "check je mail"-regel via het globe-icoontje.
+⚠️ De **omschrijvingen komen uit Drupal** en krijgen dus géén globe en geen
+EN-vertaling. Die van 36676 bevat bovendien een tikfout die Bob in Drupal kan
+meenemen: *"Deze wordt donderdagavond **verstuurt**"* -> *verstuurd*.
+
+### Bevinding terzijde — geen taak
+
+**450 onbevestigde inschrijvingen** (360 + 47 + 43). Die mensen hebben zich ooit
+aangemeld maar nooit op de bevestigingslink geklikt en krijgen dus niets. Geen
+app-probleem, wel het sterkste argument voor `confirm = FALSE` in de app.
+
 ## 🔭 VERSIE 2.0 — na de livegang · niet agenderen vóór release
 
 *Ideeën die af zijn onderzocht maar bewust wachten. Bob 2026-09-20: eerst live,
@@ -486,6 +664,10 @@ met taak 124 — samen oppakken.
 
 ### 127 · "Kopie"-knop op `MijnAanmeldingen` — evenement én activiteit klonen
 
+**Eigenaar: Claude — bezig** (sessie 2026-09-21). 127-A is AF: Bob heeft de
+Drupal-patch op productie gezet en runtime geverifieerd (`entree_tid 36197`,
+`logo_url` gevuld, `keten 28666|28737|28739`).
+
 **Wens Bob (2026-09-20):** op elke kaart in *Mijn aanmeldingen* een knop **Kopie**.
 Eén tik opent het aanmaakformulier volledig ingevuld; de gebruiker vult de datum
 aan, past eventueel iets aan en dient in. Vóór de livegang. Beide types.