Files
votianlt/app/lib/task_view.dart
Sven Carstensen 704d1e7378 feat: Adressbuch mit Kundennummer, Update-Flow und interne Einträge
- Menüpunkt "Kunden" in "Adressbuch" umbenannt und App-Label
  "Verfügbare Jobs" zu "Auftragsliste" geändert (alle 10 Sprachen)
- Fortlaufende Kundennummer (usrId) ab 10000 über neuen
  SequenceGeneratorService und Counter-Dokument in misc-Collection
- Abholung/Lieferstation-Dialog: Änderungen an verknüpften
  Stammdaten aktualisieren den bestehenden Adressbuch-Eintrag
  statt einen neuen zu erzeugen; Checkbox-Label wechselt zu
  "Adresse im Adressbuch aktualisieren"
- Geänderte Adressen ohne Checkbox werden als interner Customer
  (internal=true) gesichert und im Adressbuch ausgeblendet
- E-Mail in AddCustomer und in Stations-Dialogen kein Pflichtfeld
  mehr; "(Login)" aus profile.email entfernt
- Manuelles Beenden eines Auftrags öffnet neue Seite
  JobManualCompleteView statt eines Dialogs
2026-04-20 12:42:56 +02:00

999 lines
32 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:votianlt_app/services/developer.dart' as developer;
import 'package:image/image.dart' as img;
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'app_theme.dart';
import 'l10n/app_localizations.dart';
import 'l10n/localization_helpers.dart';
import 'models/job.dart';
import 'models/task.dart';
import 'models/tasks/confirmation_task.dart';
import 'models/tasks/photo_task.dart';
import 'models/tasks/todolist_task.dart';
import 'models/tasks/signature_task.dart';
import 'models/tasks/barcode_task.dart';
import 'models/tasks/comment_task.dart';
import 'services/database_service.dart';
import 'widgets/offline_banner.dart';
import 'services/websocket_service.dart';
import 'Tasks/photo_capture_screen.dart';
import 'Tasks/barcode_capture_screen.dart';
import 'Tasks/signature_capture_screen.dart';
class TaskView extends StatefulWidget {
final Job job;
final int? stationOrder;
final String? stationTitle;
const TaskView({
super.key,
required this.job,
this.stationOrder,
this.stationTitle,
});
@override
State<TaskView> createState() => _TaskViewState();
}
class _TaskViewState extends State<TaskView> {
final Set<String> _completedTasks = {};
final DatabaseService _databaseService = DatabaseService();
// Store SVG representations of signatures per task for later use
final Map<String, String> _signatureSvgByTask = {};
@override
void initState() {
super.initState();
_loadTaskStatuses();
}
List<Task> get _visibleTasks {
final stationOrder = widget.stationOrder;
if (stationOrder == null) {
return widget.job.tasks;
}
return widget.job.tasks
.where((task) => task.stationOrder == stationOrder)
.toList();
}
/// Load task completion and skipped statuses from database and merge with JSON task states
Future<void> _loadTaskStatuses() async {
final statuses = await _databaseService.loadAllTaskStatuses();
setState(() {
_completedTasks.clear();
// 1) Add all completed from DB
for (final entry in statuses.entries) {
if (entry.value) {
_completedTasks.add(entry.key);
}
}
// 2) Merge: also mark tasks completed if the job JSON already had them completed
for (final t in widget.job.tasks) {
if (t.completed) {
_completedTasks.add(t.id);
}
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
widget.stationTitle?.isNotEmpty == true
? '${AppLocalizations.of(context).tasks} - ${widget.stationTitle}'
: '${AppLocalizations.of(context).tasks} - ${widget.job.jobNumber}',
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
},
),
actions: [
IconButton(
icon: const Icon(Icons.chat),
onPressed: () {
Navigator.of(context).pushNamed('/chats');
},
tooltip: AppLocalizations.of(context).openChat,
),
],
),
body: Column(
children: [
OfflineBanner(),
if (_getRemark().isNotEmpty)
Container(
width: double.infinity,
margin: const EdgeInsets.all(5),
constraints: const BoxConstraints(maxHeight: 150),
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: AppColors.surfaceMuted,
border: Border.all(color: AppColors.border, width: 1),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
child: Text(
_getRemark(),
style: const TextStyle(fontSize: 14, color: AppColors.text),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildTasksStepper()),
if (_visibleTasks.isNotEmpty) ...[
const SizedBox(height: 12),
_buildCompleteStationButton(),
],
],
),
),
),
],
),
);
}
bool get _canCompleteStation {
// Station kann abgeschlossen werden, wenn alle nicht-optionalen Aufgaben
// erledigt sind. Sind nur optionale Aufgaben vorhanden, ist der Button
// immer aktiv.
for (final t in _visibleTasks) {
if (!t.optional && !_completedTasks.contains(t.id)) {
return false;
}
}
return true;
}
bool get _hasIncompleteOptionalTasks {
for (final t in _visibleTasks) {
if (t.optional && !_completedTasks.contains(t.id)) {
return true;
}
}
return false;
}
Widget _buildCompleteStationButton() {
final bool enabled = _canCompleteStation;
return SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: enabled ? _onCompleteStationPressed : null,
icon: const Icon(Icons.flag),
label: const Text('Station abschließen'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
);
}
void _onCompleteStationPressed() {
if (_hasIncompleteOptionalTasks) {
showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
title: const Text('Offene optionale Aufgaben'),
content: const Text(
'Es gibt nicht erledigte optionale Aufgaben. Möchten Sie die Station trotzdem abschließen?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(AppLocalizations.of(context).cancel),
),
ElevatedButton(
onPressed: () {
Navigator.of(ctx).pop();
_sendStationCompleted();
},
child: const Text('Trotzdem abschließen'),
),
],
);
},
);
} else {
_sendStationCompleted();
}
}
void _sendStationCompleted() {
final stationOrder = widget.stationOrder ?? 0;
try {
StompService().sendStationCompleted(
jobId: widget.job.id,
jobNumber: widget.job.jobNumber,
stationOrder: stationOrder,
hasIncompleteOptionalTasks: _hasIncompleteOptionalTasks,
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Station abgeschlossen')),
);
Navigator.of(context).pop();
} catch (e) {
developer.log('Error sending station completion: $e', name: 'TaskView');
}
}
Widget _buildTasksStepper() {
if (_visibleTasks.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.task_outlined,
size: 64,
color: AppColors.textMuted,
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context).noTasks,
style: const TextStyle(fontSize: 16, color: AppColors.textMuted),
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context).noTasksMessage,
style: const TextStyle(fontSize: 14, color: AppColors.textMuted),
textAlign: TextAlign.center,
),
],
),
);
}
return ListView.builder(
itemCount: _visibleTasks.length,
itemBuilder: (context, index) {
final task = _visibleTasks[index];
final isCompleted = _completedTasks.contains(task.id);
final canBeCompletedNow =
!isCompleted && _arePreviousTasksCompleted(index);
// Hintergrundfarbe je nach Status:
// abgeschlossen → hellgrün, bearbeitbar → weiß, gesperrt → hellgrau
// (Optionale Aufgaben werden durch einen Chip markiert, nicht per Farbe.)
final Color cardColor =
isCompleted
? AppColors.successSoft
: canBeCompletedNow
? AppColors.surface
: AppColors.surfaceMuted;
final Color borderColor =
isCompleted
? AppColors.success.withValues(alpha: 0.35)
: canBeCompletedNow
? AppColors.border
: AppColors.border.withValues(alpha: 0.7);
final Color circleColor =
isCompleted
? AppColors.success
: canBeCompletedNow
? AppColors.primary
: AppColors.textMuted;
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: isCompleted || canBeCompletedNow ? 2 : 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: borderColor, width: 1),
),
child: InkWell(
onTap:
canBeCompletedNow
? () => _showTaskCompletionDialog(task, index)
: null,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: cardColor,
),
child: Stack(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Task number circle
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: circleColor,
),
child: Center(
child: Text(
'${index + 1}',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
const SizedBox(width: 16),
// Task content
Expanded(
child: Padding(
padding: EdgeInsets.only(
right: task.optional ? 72 : 0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTaskDisplayText(task, isCompleted, index),
if (_getTaskStationLabel(task) != null) ...[
const SizedBox(height: 4),
Text(
_getTaskStationLabel(task)!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
],
],
),
),
),
if (isCompleted) ...[
const SizedBox(width: 8),
const Icon(
Icons.check_circle,
color: AppColors.success,
),
],
],
),
if (task.optional)
Positioned.fill(
child: Align(
alignment: Alignment.centerRight,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColors.warningSoft,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.warning.withValues(alpha: 0.5),
width: 1,
),
),
child: const Text(
'Optional',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: AppColors.warning,
),
),
),
),
),
],
),
),
),
);
},
);
}
void _showTaskCompletionDialog(Task task, int taskIndex) {
switch (task) {
case ConfirmationTask():
_showConfirmationDialog(task, taskIndex);
break;
case PhotoTask():
_showPhotoDialog(task);
break;
case TodoListTask():
_showTodoListDialog(task);
break;
case SignatureTask():
_showSignatureDialog(task);
break;
case BarcodeTask():
_showBarcodeDialog(task);
break;
case CommentTask():
_showCommentDialog(task);
break;
default:
_showGenericDialog(task);
break;
}
}
void _showConfirmationDialog(ConfirmationTask task, int taskIndex) {
final description =
task.description?.isNotEmpty == true
? task.description!
: AppLocalizations.of(context).confirmationDescription;
final buttonText =
task.buttonText.isNotEmpty
? task.buttonText
: AppLocalizations.of(context).confirm;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).confirmationRequired),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text(description)],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context).cancel),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_completeTask(task.id, taskType: 'CONFIRMATION');
},
child: Text(buttonText),
),
],
);
},
);
}
// Compress photos and Base64-encode while keeping total payload under a cap
Future<List<String>> _compressAndEncodePhotos(
List<Uint8List> photos, {
int maxDim = 1280,
int jpegQuality = 70,
int maxTotalBase64Bytes = 450 * 1024,
}) async {
final List<String> encoded = [];
int total = 0;
for (final bytes in photos) {
try {
final img.Image? decoded = img.decodeImage(bytes);
if (decoded == null) {
continue;
}
// Resize if needed keeping aspect ratio
final int w = decoded.width;
final int h = decoded.height;
img.Image resized = decoded;
final int longest = w > h ? w : h;
if (longest > maxDim) {
if (w >= h) {
resized = img.copyResize(decoded, width: maxDim);
} else {
resized = img.copyResize(decoded, height: maxDim);
}
}
final List<int> jpg = img.encodeJpg(resized, quality: jpegQuality);
final String b64 = base64Encode(jpg);
// Respect total payload cap
if (total + b64.length > maxTotalBase64Bytes) {
break;
}
encoded.add(b64);
total += b64.length;
} catch (e, st) {
developer.log('Photo compress/encode error: $e', name: 'TaskView');
developer.log('Stack: $st', name: 'TaskView');
}
}
return encoded;
}
void _showPhotoDialog(PhotoTask task) {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => PhotoCaptureScreen(
task: task,
onPhotosCompleted: (List<Uint8List> photoData) async {
// Compress + encode photos for network send (limit payload)
final List<String> base64List = await _compressAndEncodePhotos(
photoData,
);
final bool truncated = base64List.length < photoData.length;
// Try to persist full-quality (encoded) photos to DB for offline/backup
try {
// Persist the compressed versions to keep DB size reasonable as well
await _databaseService.saveTaskPhotos(task.id, base64List);
} catch (e, stackTrace) {
developer.log(
'Error saving task photos: $e',
name: 'TaskView',
);
developer.log('Stack trace: $stackTrace', name: 'TaskView');
}
// Always complete the task regardless of persistence success/failure
_completeTask(
task.id,
taskType: 'PHOTO',
extraData: {
'photos': base64List,
'count': photoData.length,
if (truncated) 'truncated': true,
},
);
},
),
),
);
}
void _showTodoListDialog(TodoListTask task) {
final items = task.todoItems;
final List<bool> checkedItems = List.filled(items.length, false);
showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Text(AppLocalizations.of(context).checklist),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(context).checklistDescription),
const SizedBox(height: 16),
...items.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return CheckboxListTile(
title: Text(item),
value: checkedItems[index],
onChanged: (bool? value) {
setState(() {
checkedItems[index] = value ?? false;
});
},
dense: true,
contentPadding: EdgeInsets.zero,
);
}),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context).abort),
),
ElevatedButton(
onPressed:
checkedItems.every((checked) => checked)
? () {
Navigator.of(context).pop();
_completeTask(
task.id,
taskType: 'TODOLIST',
extraData: {
'items': task.todoItems,
'checkedStates': checkedItems,
},
);
}
: null,
child: Text(AppLocalizations.of(context).finish),
),
],
);
},
);
},
);
}
void _showSignatureDialog(SignatureTask task) {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => SignatureCaptureScreen(
task: task,
onSignatureCompleted: (String svg, String note) async {
try {
// Persist SVG only (no PNG)
await _databaseService.saveTaskSignature(task.id, svg);
await _databaseService.saveTaskSignatureNote(task.id, note);
} catch (e, stackTrace) {
developer.log(
'Error saving task signature: $e',
name: 'TaskView',
);
developer.log('Stack trace: $stackTrace', name: 'TaskView');
}
// Store SVG for later use in this TaskView session
setState(() {
_signatureSvgByTask[task.id] = svg;
});
// Read back once (for analyzer to see it used) and optional debug
debugPrint(
'Signature SVG stored for task ${task.id}: length=${_signatureSvgByTask[task.id]?.length ?? 0}',
);
_completeTask(
task.id,
taskType: 'SIGNATURE',
extraData: {
'signatureSvg': svg,
'svgLength': svg.length,
'hasSignature': true,
'signatureNote': note,
},
);
},
),
),
);
}
void _showBarcodeDialog(BarcodeTask task) {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => BarcodeCaptureScreen(
task: task,
onBarcodesCompleted: (List<String> barcodes) async {
try {
// Save barcodes to database for later use
await _databaseService.saveTaskBarcodes(task.id, barcodes);
} catch (e, stackTrace) {
developer.log(
'Error saving task barcodes: $e',
name: 'TaskView',
);
developer.log('Stack trace: $stackTrace', name: 'TaskView');
}
_completeTask(
task.id,
taskType: 'BARCODE',
extraData: {'barcodes': barcodes, 'count': barcodes.length},
);
},
),
),
);
}
void _showGenericDialog(Task task) {
final TextEditingController noteController = TextEditingController();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).completeTask),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(context).completeTaskConfirm),
const SizedBox(height: 16),
TextField(
controller: noteController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).completeTaskNote,
border: const OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context).abort),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_completeTask(task.id, taskType: 'GENERIC');
},
child: Text(AppLocalizations.of(context).complete),
),
],
);
},
);
}
void _completeTask(
String taskId, {
String? taskType,
Map<String, dynamic>? extraData,
}) {
final bool hadOpenMandatoryBefore = _hasOpenMandatoryTasks;
setState(() {
_completedTasks.add(taskId);
});
// Save to database
_databaseService.saveTaskStatus(taskId, true);
// Notify server via STOMP about task completion (best-effort)
try {
StompService().sendTaskCompleted(
taskId: taskId,
taskType: taskType,
extraData: extraData,
);
} catch (e) {
developer.log('Error sending task completion: $e', name: 'TaskView');
}
// Wenn die letzte nicht-optionale Aufgabe gerade erledigt wurde,
// den Benutzer fragen, ob er die Station jetzt abschließen möchte.
if (hadOpenMandatoryBefore && !_hasOpenMandatoryTasks) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_showLastMandatoryCompletedDialog();
}
});
}
}
bool get _hasOpenMandatoryTasks {
for (final t in _visibleTasks) {
if (!t.optional && !_completedTasks.contains(t.id)) {
return true;
}
}
return false;
}
void _showLastMandatoryCompletedDialog() {
showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
title: const Text('Alle Pflichtaufgaben erledigt'),
content: const Text(
'Alle nicht optionalen Aufgaben dieser Station sind erledigt. '
'Möchten Sie die Station jetzt abschließen?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Später'),
),
ElevatedButton(
onPressed: () {
Navigator.of(ctx).pop();
_onCompleteStationPressed();
},
child: const Text('Station abschließen'),
),
],
);
},
);
}
bool _arePreviousTasksCompleted(int index) {
if (index <= 0) return true;
for (int i = 0; i < index; i++) {
final t = _visibleTasks[i];
if (!t.optional && !_completedTasks.contains(t.id)) {
return false;
}
}
return true;
}
void _showCommentDialog(CommentTask task) {
final TextEditingController commentController = TextEditingController();
commentController.text = task.commentText;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).enterComment),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(context).commentDescription),
const SizedBox(height: 16),
TextField(
controller: commentController,
decoration: InputDecoration(
labelText:
task.required
? AppLocalizations.of(context).commentRequired
: AppLocalizations.of(context).comment,
border: const OutlineInputBorder(),
hintText: '...',
),
maxLines: 4,
minLines: 2,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context).abort),
),
ElevatedButton(
onPressed: () {
final comment = commentController.text.trim();
if (task.required && comment.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).commentRequired,
),
),
);
return;
}
Navigator.of(context).pop();
_completeTask(
task.id,
taskType: 'COMMENT',
extraData: {
'commentText': comment,
'required': task.required,
},
);
},
child: Text(AppLocalizations.of(context).save),
),
],
);
},
);
}
Widget _buildTaskDisplayText(Task task, bool isCompleted, int taskIndex) {
final titleStyle = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
decoration: isCompleted ? TextDecoration.lineThrough : null,
);
final subtitleStyle = TextStyle(
fontSize: 13,
color: Colors.grey[600],
decoration: isCompleted ? TextDecoration.lineThrough : null,
);
final displayName =
task.displayName != null
? localizeKnownText(context, task.displayName!)
: null;
final description =
task.description != null
? localizeKnownText(context, task.description!)
: null;
final String? signatureNote =
(task is SignatureTask && task.note != null && task.note!.trim().isNotEmpty)
? task.note!.trim()
: null;
if (displayName?.isNotEmpty == true) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(displayName!, style: titleStyle),
if (description?.isNotEmpty == true) ...[
const SizedBox(height: 2),
Text(description!, style: subtitleStyle),
],
if (signatureNote != null) ...[
const SizedBox(height: 2),
Text(signatureNote, style: subtitleStyle),
],
],
);
}
if (description?.isNotEmpty == true) {
if (signatureNote != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(description!, style: titleStyle),
const SizedBox(height: 2),
Text(signatureNote, style: subtitleStyle),
],
);
}
return Text(description!, style: titleStyle);
}
if (signatureNote != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_getStandardTaskDisplayText(task), style: titleStyle),
const SizedBox(height: 2),
Text(signatureNote, style: subtitleStyle),
],
);
}
// Fall back to standard text based on task type
return Text(_getStandardTaskDisplayText(task), style: titleStyle);
}
String _getStandardTaskDisplayText(Task task) {
// Generate display text based on task type
switch (task) {
case PhotoTask():
return '${AppLocalizations.of(context).takePhotos} (${task.minPhotoCount}-${task.maxPhotoCount} ${AppLocalizations.of(context).photosCount})';
case TodoListTask():
return '${AppLocalizations.of(context).checklist} (${task.todoItems.length} ${AppLocalizations.of(context).checklistPoints})';
case SignatureTask():
return AppLocalizations.of(context).signatureRequiredText;
case BarcodeTask():
return '${AppLocalizations.of(context).scanBarcodes} (${task.minBarcodeCount}-${task.maxBarcodeCount} ${AppLocalizations.of(context).barcodeCount})';
case CommentTask():
return task.required
? AppLocalizations.of(context).commentRequired
: AppLocalizations.of(context).commentOptional;
default:
return AppLocalizations.of(context).genericTask;
}
}
String _getRemark() => widget.job.remark;
String? _getTaskStationLabel(Task task) {
if (widget.stationOrder != null) {
return null;
}
final stationOrder = task.stationOrder;
if (stationOrder == null) {
return null;
}
for (final station in widget.job.deliveryStations) {
if (station.stationOrder == stationOrder) {
final suffix =
station.displayName.isNotEmpty ? station.displayName : station.city;
return localizedStationLabel(context, stationOrder + 1, suffix: suffix);
}
}
return AppLocalizations.of(context).stationNumber(stationOrder + 1);
}
}