From f58f3f0c596ce9c03ca52854e93623b7160dcb39 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Mon, 6 Jul 2026 08:03:51 +0200 Subject: [PATCH] Pflichtfeld-Kennzeichen (required) aus dem Workflow-Editor auswerten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TourField/TourEntry um optionales required-Flag erweitert (Altdaten ohne Angabe verhalten sich unverändert) - Weiter-Button frei, wenn alle Pflichtfelder erfüllt sind oder der Task optional ist; Listen ohne Pflichtfelder (inkl. leerer Listen) sind frei - Pflichtfelder und Pflicht-Checkboxen mit rotem * markiert; Titel-* nur noch, wenn der Task tatsächlich zwingende Eingaben verlangt Co-Authored-By: Claude Fable 5 --- lib/content_views/task_list_page.dart | 86 +++++++++++++++++++++++++-- lib/model/tour_task.dart | 12 ++++ 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/lib/content_views/task_list_page.dart b/lib/content_views/task_list_page.dart index 8f42a4b..98082f4 100644 --- a/lib/content_views/task_list_page.dart +++ b/lib/content_views/task_list_page.dart @@ -215,7 +215,7 @@ class _TaskListPageState extends State with ProducerMixin { children: [ Expanded( child: Text( - (taskInfo.task?.topic ?? '') + ((taskInfo.task?.opt ?? false) ? "" : " *"), + (taskInfo.task?.topic ?? '') + (_isTaskOptional(taskInfo) ? "" : " *"), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), ), @@ -455,7 +455,16 @@ class _TaskListPageState extends State with ProducerMixin { if ((fields[i].title ?? '').isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 4, top: 4), - child: Text(fields[i].title!, style: const TextStyle(fontWeight: FontWeight.w600)), + child: Text.rich( + TextSpan( + text: fields[i].title!, + children: [ + if (fields[i].required == true) + const TextSpan(text: ' *', style: TextStyle(color: Colors.red)), + ], + ), + style: const TextStyle(fontWeight: FontWeight.w600), + ), ), Util.getDefaultTextField( fields[i].placeholder ?? '', @@ -485,7 +494,15 @@ class _TaskListPageState extends State with ProducerMixin { contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, activeColor: Constants.colorPrimary, - title: Text(e.text), + title: Text.rich( + TextSpan( + text: e.text, + children: [ + if (e.required == true) + const TextSpan(text: ' *', style: TextStyle(color: Colors.red)), + ], + ), + ), onChanged: (value) => setState(() { if (value ?? false) { draft.checked.add(e.code); @@ -616,6 +633,31 @@ class _TaskListPageState extends State with ProducerMixin { return _isValid(taskInfo); } + /// Ob die Aufgabe insgesamt optional ist (steuert das "*" im Titel): + /// entweder per {@code opt}-Flag oder – bei Listen-Tasks – weil kein + /// einziger Eintrag als Pflichtfeld gekennzeichnet ist. Altdaten ohne + /// Pflicht-Kennzeichen gelten wie bisher als verpflichtend. + bool _isTaskOptional(TaskInfo taskInfo) { + final task = taskInfo.task; + if (task == null) return true; + if (task.opt ?? false) return true; + switch (task.type) { + case 12: + case 13: + final fields = task.fields ?? []; + if (fields.isEmpty) return true; + if (fields.every((f) => f.required == null)) return false; + return fields.every((f) => f.required != true); + case 14: + final entries = task.entries ?? []; + if (entries.isEmpty) return true; + if (entries.every((e) => e.required == null)) return false; + return entries.every((e) => e.required != true); + default: + return false; + } + } + bool _isValid(TaskInfo taskInfo) { final draft = _draftFor(taskInfo); final type = taskInfo.task?.type ?? 0; @@ -647,17 +689,49 @@ class _TaskListPageState extends State with ProducerMixin { return true; case 12: _ensureFieldControllers(draft, taskInfo.task?.fields?.length ?? 0); - return draft.fields.isNotEmpty && draft.fields.every((c) => c.text.trim().isNotEmpty); + return _fieldsValid(taskInfo, draft, numeric: false); case 13: _ensureFieldControllers(draft, taskInfo.task?.fields?.length ?? 0); - return draft.fields.isNotEmpty && draft.fields.every((c) => _isNumeric(c.text)); + return _fieldsValid(taskInfo, draft, numeric: true); case 14: - return draft.checked.isNotEmpty; + final entries = taskInfo.task?.entries ?? []; + // Ohne definierte Einträge gibt es keine Pflichtfelder -> freigeben. + if (entries.isEmpty) return true; + // Altdaten ohne Pflicht-Kennzeichen: wie bisher mindestens ein Häkchen. + if (entries.every((e) => e.required == null)) { + return draft.checked.isNotEmpty; + } + return entries + .where((e) => e.required == true) + .every((e) => draft.checked.contains(e.code)); default: return true; } } + /// Prüft die Eingabefelder einer Text-/Zahlen-Liste (Typ 12/13) gegen ihre + /// Pflicht-Kennzeichen. Altdaten ohne Kennzeichen behalten das bisherige + /// Verhalten (alle Felder müssen gefüllt bzw. numerisch sein). Sonst müssen + /// nur Pflichtfelder gefüllt (bei Zahlenlisten numerisch) sein; optionale + /// Felder dürfen leer bleiben, müssen aber – falls ausgefüllt – bei + /// Zahlenlisten ebenfalls numerisch sein. + bool _fieldsValid(TaskInfo taskInfo, _TaskDraft draft, {required bool numeric}) { + final fields = taskInfo.task?.fields ?? []; + // Ohne definierte Felder gibt es keine Pflichtfelder -> freigeben. + if (fields.isEmpty) return true; + final legacy = fields.every((f) => f.required == null); + for (int i = 0; i < fields.length && i < draft.fields.length; i++) { + final text = draft.fields[i].text.trim(); + final mandatory = legacy || fields[i].required == true; + if (mandatory) { + if (numeric ? !_isNumeric(text) : text.isEmpty) return false; + } else if (numeric && text.isNotEmpty && !_isNumeric(text)) { + return false; + } + } + return true; + } + /// Ob der Bearbeiter überhaupt Daten erfasst hat (für optionale Aufgaben: /// entscheidet, ob eine Antwort geschrieben oder die Aufgabe übersprungen wird). bool _hasData(TaskInfo taskInfo) { diff --git a/lib/model/tour_task.dart b/lib/model/tour_task.dart index f1aee99..2001f7e 100644 --- a/lib/model/tour_task.dart +++ b/lib/model/tour_task.dart @@ -82,6 +82,10 @@ class TourEntry { late String text; late bool queryRemark; + /// Pflicht-Kennzeichen aus dem Workflow-Editor (Checkbox-Liste, Typ 14). + /// `null`, wenn das JSON keine Angabe enthält (Altdaten). + bool? required; + TourEntry(); factory TourEntry.fromJson(Map json) { @@ -89,6 +93,7 @@ class TourEntry { entry.code = json['code'] as int; entry.text = json['text'] as String; entry.queryRemark = json['query_remark'] as bool? ?? false; + entry.required = json['required'] as bool?; return entry; } @@ -97,6 +102,7 @@ class TourEntry { 'code': code, 'text': text, 'query_remark': queryRemark, + if (required != null) 'required': required, }; } } @@ -144,12 +150,17 @@ class TourField { String? title; String? placeholder; + /// Pflicht-Kennzeichen aus dem Workflow-Editor. `null`, wenn das JSON + /// keine Angabe enthält (Altdaten). + bool? required; + TourField(); factory TourField.fromJson(Map json) { final field = TourField(); field.title = json['title'] as String?; field.placeholder = json['placeholder'] as String?; + field.required = json['required'] as bool?; return field; } @@ -157,6 +168,7 @@ class TourField { return { 'title': title, 'placeholder': placeholder, + if (required != null) 'required': required, }; } }