Jelajahi Sumber

P2-29: verbeterronde aanmaakpagina's uitgeschreven als overdracht (blok A/B/C), einddatum-check als snippets, P2-22-notitie en P2-7-lijst bijgewerkt

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
bob 2 jam lalu
induk
melakukan
b7a2549b1a
4 mengubah file dengan 470 tambahan dan 5 penghapusan
  1. 92 5
      TASKS.md
  2. 16 0
      snippets/LEESMIJ.md
  3. 176 0
      snippets/evenement_create.dart.txt
  4. 186 0
      snippets/stadsactiviteit_create.dart.txt

+ 92 - 5
TASKS.md

@@ -17,6 +17,93 @@ 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`.*
 
+### 🆕 P2-29 · Aanmaakpagina's `uitgaansevenementAanmaken` + `stadsactiviteitAanmaken` — verbeterronde · Eigenaar: de sessie die op deze twee pagina's zit (Bob 2026-09-15 avond: "een andere sessie is bezig met de aanmaakpagina's") — niet nog een sessie erbij
+
+Bob's akkoord 2026-09-15 op blok A, daarna blok B. Alles hieronder is
+uitgeschreven door de audit-sessie van 15 sep (code van beide pagina's, de
+custom actions en de vertaalsleutels gelezen); die sessie heeft **niets in de
+builder aangeraakt**. Afgeronde punten verwijderen; per punt export-verifiëren.
+
+**Blok A · gedrag**
+- **A1** De verzendknop `ButtonIndienen` is een direct kind van de hoofd-Column
+  en staat op Visibility `createHorecagelegenheidNid` / `createPlaatsID` *Is
+  Set and Not Empty*. Zet daarnaast een `Text` (Insert Widget op die Column,
+  landt onderaan, dat is prima): "Kies eerst een horecagelegenheid om het
+  evenement in te dienen" resp. "Kies eerst een plaats om de activiteit in te
+  dienen", bodyMedium, Secondary Text, centered, padding 12, Visibility
+  Conditional → Single Condition → die page state → **Is Not Set**. (Lege
+  string komt niet voor: de state wordt alleen door de dropdown gezet, altijd
+  met een id.) Export: `if (_model.createHorecagelegenheidNid == null)`.
+- **A2** Sterretjes. Evenement: `TextFieldTitel` Label Text `Titel *`,
+  `TextFieldDatumStart` Label Text `Datum *`, kop-Text `Uw Horecagelegenheid`
+  → `Uw horecagelegenheid *`. Stadsactiviteit: `TextFieldTitel` `Titel *`,
+  `TextFieldDatumStart` `Datum *`, kop `Waar` → `Waar *`. Let op: Label Text,
+  niet Hint Text (`xceonmpt`/`dfa8vusy` zijn de labels).
+- **A3a** Einddatum-picker (actie op de Container rond `TextFieldDatumEind`):
+  **Initial Date** én **Minimum Date** binden aan Widget State `Date Picked 1`
+  (zoekveld leeg laten, Widget State openklappen, 10 s wachten, hoveren — zie
+  `CLAUDE.md`). Verifieer in de export dat er `?? getCurrentTimestamp` of een
+  null-veilige vorm staat en géén `datePicked1!`; zo wel, Minimum Date weer op
+  Current Time en alleen Initial Date houden.
+- **A3b** Controle "einde vóór begin" in beide custom actions: kant-en-klaar in
+  `snippets/evenement_create.dart.txt` en `snippets/stadsactiviteit_create.dart.txt`
+  (zelfde signature, één extra blok, Ctrl+S niet vergeten). Export-check:
+  `grep -c "einddatum ligt vóór" lib/custom_code/actions/*.dart` → 2.
+- **A4** Evenement: `TextFieldTitel` + `TextFieldOmschrijving` → Text
+  Capitalization **Sentences**; `TextFieldWebsiteURL` + `TextFieldTicketsURL`
+  → Keyboard Type **URL**. Stadsactiviteit: idem Titel/Omschrijving, en
+  `TextFieldWebsiteURL` → URL. **Entreeprijs bewust op Text laten** (numeriek
+  toetsenbord heeft geen komma, en "gratis" moet kunnen). Export:
+  `textCapitalization: TextCapitalization.sentences`,
+  `keyboardType: TextInputType.url`.
+
+**Blok B · teksten en consistentie (Nederlandse kant, los van P1-17)**
+- **B7** Engelse FlutterFlow-defaults die een NL-gebruiker nu ziet. Evenement:
+  categorie-dropdown hint `2hitlrxa` "Select..." → "Kies categorie(ën)",
+  zoekhint `qtnlg7nd` "Search..." → "Zoek...", entree-hint `ht4toe4o` →
+  "Kies entree", `xcfwz8cp` → "Zoek...". Stadsactiviteit: provincie `dqeuq7t8`
+  → "Kies provincie", gemeente `xiba4gka` → "Kies gemeente", plaats `l9f37oxb`
+  → "Kies plaats", entree `ajotnz1f` → "Kies entree", zoekhints `d3gwaxun`,
+  `yb8u6i28`, `b5raidm6`, `bizq1hlw`, `5rec163i` → "Zoek...". De "Option 1/2/3"-
+  sleutels zijn ongebruikte defaults (opties komen uit de API), laten staan.
+- **B8** Stadsactiviteit: kop `moj9sft5` "Categorie evenement" → "Categorie
+  activiteit". Evenement: kop `vkyrmq6a` "Organisatie" staat boven Website +
+  Tickets → "Links"; labels `e9n6cj1o` "WebsiteURL" → "Website", `zfofdq9a`
+  "TicketsURL" → "Tickets".
+- **B9** Drie **foute** (niet lege) Engelse vertalingen op de evenement-pagina,
+  nu al zichtbaar op een Engels toestel — via het globe-icoontje: `vu6qv8cj`
+  "Title cityactvity" → "Event title"; `9qx16tm9` EN "Activiteit indienen" →
+  "Submit event"; `nzaqrq0w` EN "Website van de activiteit" → "Link to buy
+  tickets". De overige 46 blijven P1-17 (livegang).
+- **B10** Radius gelijktrekken: stadsactiviteit-pagina staat op `circular(0.0)`
+  voor dropdowns (6), tekstvelden, beide thumbnail-`ClipRRect`s en
+  `ButtonIndienen`; evenement-pagina op 8. Alles op **8** zetten (de
+  mediaknoppen staan al op 8 sinds 14 sep). Check: `grep -c "circular(0.0)"`
+  op `stadsactiviteit_aanmaken_widget.dart` hoort daarna 0 te zijn.
+
+**Blok C · voor Bob**
+- **C11** P2-22 (twee Property Override-fouten): recept met Predefined Paths
+  staat in de chat van 15 sep en hieronder bij P2-22. Onveranderd Bob.
+- **C12** `datumVoorApi()` (`custom_functions.dart:202`) heeft nul aanroepers
+  → staat nu op de P2-7-lijst.
+- **C13** Bronkeuze-sheet ("Choose Source / Gallery / Camera") + 46 velden →
+  P1-17 bij livegang.
+- **C14** Losse upload-fids zonder indienen. Eén vraag beslist het: slaat
+  `bestand_upload/upload` in `custom.module` het bestand op als **tijdelijk
+  (status 0)** of **permanent**? Tijdelijk = niets doen (`file_field_presave`
+  maakt aangehangen bestanden bij `node_save()` vanzelf permanent, `system_cron`
+  ruimt de rest na 6 uur op). Permanent = `$file->status = 0;` vóór de
+  `file_save()` in de upload-resource. Meten:
+  `drush @prod sqlq "SELECT fid, status, uri, FROM_UNIXTIME(timestamp) FROM file_managed ORDER BY fid DESC LIMIT 20;"`
+  — een `status = 1` op een fid zonder node is de tweede situatie. (De drie
+  fids van 13 sep zijn niet bruikbaar als test: die hangen aan nid 220906.)
+
+**Later, eigen sessie (A5/A6, niet geagendeerd):** kruisje op een thumbnail om
+een logo/foto weer te verwijderen (fid + url uit de lijst), en een lege-staat-
+tekst op de evenement-pagina als `MijnHorecagelegenheden` leeg is (vereist de
+wrap-Column-truc, want een widget kan zijn eigen query niet in zijn Visibility
+gebruiken).
+
 ### ✅ Sessie 2026-09-15 (Claude) — wat er af is
 
 *P2-28 (`Selectprovinciegemeente`) en P2-27a (grijze band Home-slider) staan
@@ -4494,6 +4581,8 @@ regels** staan (`XWidget\n    .routeName`), dus normaliseer whitespace vóór
 je grept — anders lijkt `HorecagelegenhedenOverzichtProvinciePage` ten
 onrechte dood. Uitkomst nu: **166 dart-bestanden, 135 bereikbaar**.
 
+**Ook dood (2026-09-15): custom function `datumVoorApi`** (`custom_functions.dart:202`, nul aanroepers sinds de datumvelden op 14 sep rechtstreeks op `datePicked1/2` zijn gezet) — verwijderen via Custom Code → Functions.
+
 **A′. Dode componenten die FlutterFlow nog exporteert (9):**
 `header_buttons_component_copymethartje`, `kaart_slider_uitgaan_s_comp`,
 `kaart_tabel_uitgaan_comp`, `kaart_tabel_uitgaan_s_comp`,
@@ -4979,11 +5068,9 @@ dezelfde waarde terug en hielp al niet) maar met het reset-icoontje
 Conditional uitzetten WIST de conditie.** Weer aanzetten levert een lege
 conditie op (`Unset` in rood) en dus géén guard. Zet je 'm uit om iets te
 testen, noteer dan eerst de conditie — je moet hem daarna volledig
-opnieuw opbouwen. **Stand bij het afsluiten van sessie 2026-09-03: de
-conditie op `DropDownGemeenten-mijngemeenten` staat nog op `Unset`** (Bob
-heeft 'm geprobeerd terug te zetten, maar drie verse exports op rij tonen
-de `if` niet terug). Terugzetten: First Value → MijnStadsrechten Response
-→ JSON Body → JSON Path `$[0].titel` → operator `Is Set`.
+opnieuw opbouwen. ~~Stand 2026-09-03: de conditie op `DropDownGemeenten-mijngemeenten` staat nog op `Unset`.~~ **Vervallen 2026-09-15:** de export toont de guard `if (getJsonField(columnMijnStadsrechtenResponse.jsonBody, r'$[0].titel') != null)` gewoon weer (`stadsactiviteit_aanmaken_widget.dart:1440`). Niets meer aan te doen.
+
+**Recept voor de twee resterende fouten (nog niet geprobeerd, 2026-09-15):** klik de fout in het Issues-paneel aan; bij `DropDownProvincie` ("return type mismatch") is de kandidaat dat `$[:].provincieid` een getal is waar de dropdown Strings wil — voeg op de call `provincies` JSON Paths `provincieid` = `$[:].provincieid` en `provincienaam` = `$[:].provinciename` toe (String, Is List), gooi Options Values/Labels leeg met het reset-icoontje en bind opnieuw via **Predefined Path**. Voor `DropDownGemeenten-mijngemeenten` hetzelfde op `MijnStadsrechten` (`tid`, `titel`), gebonden aan de Column-response. Zakt de teller niet: laten staan, blokkeert niets.
 
 *Eerder al uitgesloten (2026-09-01, Claude):* de API-calls zelf zijn in orde
 (`MijnStadsrechten` en `provincies`: absolute URL, GET, variabelen en

+ 16 - 0
snippets/LEESMIJ.md

@@ -29,3 +29,19 @@ Container met een vaste hoogte. Zie de valkuil in `TASKS.md`.
 
 Beide horen bij de verbindingsmelding in de header; de bouwstappen staan in
 `TASKS.md`.
+
+## Aanmaakpagina's (P2-29 / A3) — einddatum-controle
+
+- `evenement_create.dart.txt` — Custom **Action** `evenementCreate`,
+  volledige code zoals hij nu in de builder staat **plus** één extra blok
+  direct na de `Datum is verplicht`-controle: is de einddatum gevuld én ligt
+  hij vóór de startdatum, dan komt `{'success': false, 'error': 'De
+  einddatum ligt vóór de startdatum'}` terug. De app toont die tekst al in
+  de bestaande snackbar; er hoeft aan de pagina niets te veranderen.
+- `stadsactiviteit_create.dart.txt` — idem voor `stadsactiviteitCreate`.
+
+Argumenten, volgorde en return type blijven exact gelijk, dus de bestaande
+bindingen op `ButtonIndienen` blijven werken. Invoeren: Custom Code →
+action openen → Cancel/Discard is niet nodig, gewoon de hele body vervangen
+(alles onder `// DO NOT REMOVE`), **Ctrl+S**, en verifiëren met een verse
+export op de string `einddatum ligt vóór`.

+ 176 - 0
snippets/evenement_create.dart.txt

@@ -0,0 +1,176 @@
+// Automatic FlutterFlow imports
+import '/backend/schema/structs/index.dart';
+import '/flutter_flow/flutter_flow_theme.dart';
+import '/flutter_flow/flutter_flow_util.dart';
+import 'index.dart'; // Imports other custom actions
+import '/flutter_flow/custom_functions.dart'; // Imports custom functions
+import 'package:flutter/material.dart';
+// Begin custom action code
+// DO NOT REMOVE OR MODIFY THE CODE ABOVE!
+
+// ⬇⬇⬇ PLAK VANAF HIER — de drie importregels horen erbij! ⬇⬇⬇
+// Zonder 'package:http' faalt de build met: Undefined name 'http'.
+
+import 'dart:async';
+import 'dart:convert';
+import 'package:http/http.dart' as http;
+
+Future<dynamic> evenementCreate(
+  String sessionName,
+  String sessionId,
+  String token,
+  String horecagelegenheidNid,
+  String titel,
+  String datumStart,
+  String? datumEind,
+  String? omschrijving,
+  String? entreeTid,
+  String? entreePrijs,
+  String? toelichtingEntree,
+  String? ticketsUrl,
+  String? websiteUrl,
+  List<String>? categorieTids,
+  String? logoFid,
+  List<String>? fotosFids,
+) async {
+  final horecaNidInt = int.tryParse(horecagelegenheidNid.trim());
+  if (horecaNidInt == null) {
+    return {'success': false, 'error': 'Kies eerst een horecagelegenheid'};
+  }
+  if (titel.trim().isEmpty) {
+    return {'success': false, 'error': 'Titel is verplicht'};
+  }
+  if (datumStart.trim().isEmpty) {
+    return {'success': false, 'error': 'Datum is verplicht'};
+  }
+
+  // Einddatum mag niet vóór de startdatum liggen (P2-29 / A3).
+  // Beide komen binnen als 'yyyy-MM-dd HH:mm:ss'; DateTime.tryParse
+  // accepteert die vorm. Een lege einddatum betekent: geen einddatum.
+  if ((datumEind ?? '').trim().isNotEmpty) {
+    final start = DateTime.tryParse(datumStart.trim());
+    final eind = DateTime.tryParse(datumEind!.trim());
+    if (start != null && eind != null && eind.isBefore(start)) {
+      return {
+        'success': false,
+        'error': 'De einddatum ligt vóór de startdatum',
+      };
+    }
+  }
+
+  final body = <String, dynamic>{
+    'horecagelegenheid_nid': horecaNidInt,
+    'titel': titel.trim(),
+    'datum_start': datumStart.trim(),
+  };
+
+  if ((datumEind ?? '').trim().isNotEmpty) {
+    body['datum_eind'] = datumEind!.trim();
+  }
+  if ((omschrijving ?? '').trim().isNotEmpty) {
+    body['omschrijving'] = omschrijving!.trim();
+  }
+  if ((entreeTid ?? '').trim().isNotEmpty) {
+    final tid = int.tryParse(entreeTid!.trim());
+    if (tid != null) body['entree_tid'] = tid;
+  }
+  if ((entreePrijs ?? '').trim().isNotEmpty) {
+    body['entree_prijs'] = entreePrijs!.trim();
+  }
+  if ((toelichtingEntree ?? '').trim().isNotEmpty) {
+    body['toelichting_entree'] = toelichtingEntree!.trim();
+  }
+  if ((ticketsUrl ?? '').trim().isNotEmpty) {
+    body['tickets_url'] = ticketsUrl!.trim();
+  }
+  if ((websiteUrl ?? '').trim().isNotEmpty) {
+    body['website_url'] = websiteUrl!.trim();
+  }
+  if (categorieTids != null && categorieTids.isNotEmpty) {
+    final tids = categorieTids
+        .map((e) => int.tryParse(e.trim()))
+        .whereType<int>()
+        .toList();
+    if (tids.isNotEmpty) body['categorie_tids'] = tids;
+  }
+  if ((logoFid ?? '').trim().isNotEmpty) {
+    final fid = int.tryParse(logoFid!.trim());
+    if (fid != null) body['logo_fid'] = fid;
+  }
+  if (fotosFids != null && fotosFids.isNotEmpty) {
+    final fids =
+        fotosFids.map((e) => int.tryParse(e.trim())).whereType<int>().toList();
+    if (fids.isNotEmpty) body['fotos_fids'] = fids;
+  }
+
+  final headers = <String, String>{
+    'Accept': 'application/json',
+    'Content-Type': 'application/json',
+  };
+  if (sessionName.trim().isNotEmpty && sessionId.trim().isNotEmpty) {
+    headers['Cookie'] = '$sessionName=$sessionId';
+  }
+  if (token.trim().isNotEmpty) {
+    headers['X-CSRF-Token'] = token;
+  }
+
+  final base = FFAppState().apiBaseUrl.trim().isEmpty
+      ? 'https://uitgaanskrant.com'
+      : FFAppState().apiBaseUrl.trim();
+  final url = '$base/en/flutterdrup/evenementen/create.json';
+  print('========================================');
+  print('EVENEMENT CREATE');
+  print('URL  : $url');
+  print('BODY : ${jsonEncode(body)}');
+  print('========================================');
+
+  try {
+    final response = await http
+        .post(Uri.parse(url), headers: headers, body: jsonEncode(body))
+        .timeout(const Duration(seconds: 30));
+
+    print('========================================');
+    print('STATUS : ${response.statusCode}');
+    print('BODY');
+    print(response.body);
+    print('========================================');
+
+    dynamic data;
+    try {
+      data = jsonDecode(response.body);
+    } catch (_) {
+      data = response.body;
+    }
+
+    if (response.statusCode >= 200 && response.statusCode < 300) {
+      return {
+        'success': true,
+        'nid': data is Map ? (data['nid']?.toString() ?? '') : '',
+        'status': data is Map ? (data['status']?.toString() ?? '') : '',
+      };
+    }
+
+    if (response.statusCode == 403) {
+      return {
+        'success': false,
+        'statusCode': 403,
+        'error': 'Deze horecagelegenheid is niet van jou',
+      };
+    }
+
+    return {
+      'success': false,
+      'statusCode': response.statusCode,
+      'error': data is Map
+          ? ((data['message'] ?? data['error'] ?? data).toString())
+          : data.toString(),
+    };
+  } on TimeoutException {
+    return {'success': false, 'error': 'Timeout'};
+  } catch (e, stack) {
+    print(e);
+    print(stack);
+    return {'success': false, 'error': e.toString()};
+  }
+}
+

+ 186 - 0
snippets/stadsactiviteit_create.dart.txt

@@ -0,0 +1,186 @@
+// Automatic FlutterFlow imports
+import '/backend/schema/structs/index.dart';
+import '/flutter_flow/flutter_flow_theme.dart';
+import '/flutter_flow/flutter_flow_util.dart';
+import 'index.dart'; // Imports other custom actions
+import '/flutter_flow/custom_functions.dart'; // Imports custom functions
+import 'package:flutter/material.dart';
+// Begin custom action code
+// DO NOT REMOVE OR MODIFY THE CODE ABOVE!
+
+// ⬇⬇⬇ PLAK VANAF HIER — de drie importregels horen erbij! ⬇⬇⬇
+// Zonder 'package:http' faalt de build met: Undefined name 'http'.
+
+import 'dart:async';
+import 'dart:convert';
+import 'package:http/http.dart' as http;
+
+Future<dynamic> stadsactiviteitCreate(
+  String sessionName,
+  String sessionId,
+  String token,
+  String titel,
+  String plaatsTid,
+  String datumStart,
+  String? datumEind,
+  String? omschrijving,
+  String? organisator,
+  String? contact,
+  String? adresStraat,
+  String? adresPlaats,
+  String? adresPostcode,
+  String? entreePrijs,
+  String? toelichtingEntree,
+  List<String>? categorieTids,
+  String? websiteUrl,
+  String? logoFid,
+  List<String>? fotosFids,
+  String? entreeTid,
+) async {
+  if (titel.trim().isEmpty) {
+    return {'success': false, 'error': 'Titel is verplicht'};
+  }
+  final plaatsTidInt = int.tryParse(plaatsTid.trim());
+  if (plaatsTidInt == null) {
+    return {'success': false, 'error': 'Ongeldige plaats'};
+  }
+  if (datumStart.trim().isEmpty) {
+    return {'success': false, 'error': 'Datum is verplicht'};
+  }
+
+  // Einddatum mag niet vóór de startdatum liggen (P2-29 / A3).
+  // Beide komen binnen als 'yyyy-MM-dd HH:mm:ss'; DateTime.tryParse
+  // accepteert die vorm. Een lege einddatum betekent: geen einddatum.
+  if ((datumEind ?? '').trim().isNotEmpty) {
+    final start = DateTime.tryParse(datumStart.trim());
+    final eind = DateTime.tryParse(datumEind!.trim());
+    if (start != null && eind != null && eind.isBefore(start)) {
+      return {
+        'success': false,
+        'error': 'De einddatum ligt vóór de startdatum',
+      };
+    }
+  }
+
+  final body = <String, dynamic>{
+    'titel': titel.trim(),
+    'plaats_tid': plaatsTidInt,
+    'datum_start': datumStart.trim(),
+  };
+
+  if ((datumEind ?? '').trim().isNotEmpty) {
+    body['datum_eind'] = datumEind!.trim();
+  }
+  if ((omschrijving ?? '').trim().isNotEmpty) {
+    body['omschrijving'] = omschrijving!.trim();
+  }
+  if ((organisator ?? '').trim().isNotEmpty) {
+    body['organisator'] = organisator!.trim();
+  }
+  if ((contact ?? '').trim().isNotEmpty) {
+    body['contact'] = contact!.trim();
+  }
+  if ((adresStraat ?? '').trim().isNotEmpty ||
+      (adresPlaats ?? '').trim().isNotEmpty ||
+      (adresPostcode ?? '').trim().isNotEmpty) {
+    body['adres'] = {
+      if ((adresStraat ?? '').trim().isNotEmpty)
+        'thoroughfare': adresStraat!.trim(),
+      if ((adresPlaats ?? '').trim().isNotEmpty)
+        'locality': adresPlaats!.trim(),
+      if ((adresPostcode ?? '').trim().isNotEmpty)
+        'postal_code': adresPostcode!.trim(),
+      'country': 'NL',
+    };
+  }
+  if ((entreePrijs ?? '').trim().isNotEmpty) {
+    body['entree_prijs'] = entreePrijs!.trim();
+  }
+  if ((toelichtingEntree ?? '').trim().isNotEmpty) {
+    body['toelichting_entree'] = toelichtingEntree!.trim();
+  }
+  if (categorieTids != null && categorieTids.isNotEmpty) {
+    final tids = categorieTids
+        .map((e) => int.tryParse(e.trim()))
+        .whereType<int>()
+        .toList();
+    if (tids.isNotEmpty) body['categorie_tids'] = tids;
+  }
+  if ((websiteUrl ?? '').trim().isNotEmpty) {
+    body['website_url'] = websiteUrl!.trim();
+  }
+  if ((logoFid ?? '').trim().isNotEmpty) {
+    final fid = int.tryParse(logoFid!.trim());
+    if (fid != null) body['logo_fid'] = fid;
+  }
+  if (fotosFids != null && fotosFids.isNotEmpty) {
+    final fids =
+        fotosFids.map((e) => int.tryParse(e.trim())).whereType<int>().toList();
+    if (fids.isNotEmpty) body['fotos_fids'] = fids;
+  }
+  if ((entreeTid ?? '').trim().isNotEmpty) {
+    final tid = int.tryParse(entreeTid!.trim());
+    if (tid != null) body['entree_tid'] = tid;
+  }
+
+  final headers = <String, String>{
+    'Accept': 'application/json',
+    'Content-Type': 'application/json',
+  };
+  if (sessionName.trim().isNotEmpty && sessionId.trim().isNotEmpty) {
+    headers['Cookie'] = '$sessionName=$sessionId';
+  }
+  if (token.trim().isNotEmpty) {
+    headers['X-CSRF-Token'] = token;
+  }
+
+  final base = FFAppState().apiBaseUrl.trim().isEmpty
+      ? 'https://uitgaanskrant.com'
+      : FFAppState().apiBaseUrl.trim();
+  final url = '$base/en/flutterdrup/stadsactiviteiten/create.json';
+  print('========================================');
+  print('STADSACTIVITEIT CREATE');
+  print('URL  : $url');
+  print('BODY : ${jsonEncode(body)}');
+  print('========================================');
+
+  try {
+    final response = await http
+        .post(Uri.parse(url), headers: headers, body: jsonEncode(body))
+        .timeout(const Duration(seconds: 30));
+
+    print('========================================');
+    print('STATUS : ${response.statusCode}');
+    print('BODY');
+    print(response.body);
+    print('========================================');
+
+    dynamic data;
+    try {
+      data = jsonDecode(response.body);
+    } catch (_) {
+      data = response.body;
+    }
+
+    if (response.statusCode >= 200 && response.statusCode < 300) {
+      return {
+        'success': true,
+        'nid': data is Map ? (data['nid']?.toString() ?? '') : '',
+        'status': data is Map ? (data['status']?.toString() ?? '') : '',
+      };
+    }
+
+    return {
+      'success': false,
+      'statusCode': response.statusCode,
+      'error':
+          data is Map ? (data['message'] ?? data.toString()) : data.toString(),
+    };
+  } on TimeoutException {
+    return {'success': false, 'error': 'Timeout'};
+  } catch (e, stack) {
+    print(e);
+    print(stack);
+    return {'success': false, 'error': e.toString()};
+  }
+}