Pflichtfeld-Kennzeichen (required) aus dem Workflow-Editor auswerten

- 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 <noreply@anthropic.com>
This commit is contained in:
SvenandClaude Fable 5 committed 2026-07-06 08:03:51 +02:00
1 parent d864b9c72a
commit f58f3f0c59
2 files changed
+91 -5

No files matched your search

+79 -5
View File
@@ -215,7 +215,7 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
(taskInfo.task?.topic ?? '') + ((taskInfo.task?.opt ?? false) ? "" : " *"), (taskInfo.task?.topic ?? '') + (_isTaskOptional(taskInfo) ? "" : " *"),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
), ),
), ),
@@ -455,7 +455,16 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
if ((fields[i].title ?? '').isNotEmpty) if ((fields[i].title ?? '').isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.only(bottom: 4, top: 4), 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( Util.getDefaultTextField(
fields[i].placeholder ?? '', fields[i].placeholder ?? '',
@@ -485,7 +494,15 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading, controlAffinity: ListTileControlAffinity.leading,
activeColor: Constants.colorPrimary, 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(() { onChanged: (value) => setState(() {
if (value ?? false) { if (value ?? false) {
draft.checked.add(e.code); draft.checked.add(e.code);
@@ -616,6 +633,31 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
return _isValid(taskInfo); 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) { bool _isValid(TaskInfo taskInfo) {
final draft = _draftFor(taskInfo); final draft = _draftFor(taskInfo);
final type = taskInfo.task?.type ?? 0; final type = taskInfo.task?.type ?? 0;
@@ -647,17 +689,49 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
return true; return true;
case 12: case 12:
_ensureFieldControllers(draft, taskInfo.task?.fields?.length ?? 0); _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: case 13:
_ensureFieldControllers(draft, taskInfo.task?.fields?.length ?? 0); _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: case 14:
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 draft.checked.isNotEmpty;
}
return entries
.where((e) => e.required == true)
.every((e) => draft.checked.contains(e.code));
default: default:
return true; 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: /// Ob der Bearbeiter überhaupt Daten erfasst hat (für optionale Aufgaben:
/// entscheidet, ob eine Antwort geschrieben oder die Aufgabe übersprungen wird). /// entscheidet, ob eine Antwort geschrieben oder die Aufgabe übersprungen wird).
bool _hasData(TaskInfo taskInfo) { bool _hasData(TaskInfo taskInfo) {
+12
View File
@@ -82,6 +82,10 @@ class TourEntry {
late String text; late String text;
late bool queryRemark; 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(); TourEntry();
factory TourEntry.fromJson(Map<String, dynamic> json) { factory TourEntry.fromJson(Map<String, dynamic> json) {
@@ -89,6 +93,7 @@ class TourEntry {
entry.code = json['code'] as int; entry.code = json['code'] as int;
entry.text = json['text'] as String; entry.text = json['text'] as String;
entry.queryRemark = json['query_remark'] as bool? ?? false; entry.queryRemark = json['query_remark'] as bool? ?? false;
entry.required = json['required'] as bool?;
return entry; return entry;
} }
@@ -97,6 +102,7 @@ class TourEntry {
'code': code, 'code': code,
'text': text, 'text': text,
'query_remark': queryRemark, 'query_remark': queryRemark,
if (required != null) 'required': required,
}; };
} }
} }
@@ -144,12 +150,17 @@ class TourField {
String? title; String? title;
String? placeholder; String? placeholder;
/// Pflicht-Kennzeichen aus dem Workflow-Editor. `null`, wenn das JSON
/// keine Angabe enthält (Altdaten).
bool? required;
TourField(); TourField();
factory TourField.fromJson(Map<String, dynamic> json) { factory TourField.fromJson(Map<String, dynamic> json) {
final field = TourField(); final field = TourField();
field.title = json['title'] as String?; field.title = json['title'] as String?;
field.placeholder = json['placeholder'] as String?; field.placeholder = json['placeholder'] as String?;
field.required = json['required'] as bool?;
return field; return field;
} }
@@ -157,6 +168,7 @@ class TourField {
return { return {
'title': title, 'title': title,
'placeholder': placeholder, 'placeholder': placeholder,
if (required != null) 'required': required,
}; };
} }
} }