refactor: Projektstruktur in app/ und backend/ aufgeteilt
This commit is contained in:
796
app/lib/task_view.dart
Normal file
796
app/lib/task_view.dart
Normal file
@@ -0,0 +1,796 @@
|
||||
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 'l10n/app_localizations.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 Set<String> _skippedTasks = {};
|
||||
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 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}',
|
||||
),
|
||||
backgroundColor: Colors.deepPurple[100],
|
||||
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: const Color(0xFFF8F9FA),
|
||||
border: Border.all(color: Colors.grey[300]!, width: 1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
_getRemark(),
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [Expanded(child: _buildTasksStepper())],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTasksStepper() {
|
||||
if (_visibleTasks.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.task_outlined, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
AppLocalizations.of(context).noTasks,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
AppLocalizations.of(context).noTasksMessage,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: _visibleTasks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final task = _visibleTasks[index];
|
||||
final isCompleted = _completedTasks.contains(task.id);
|
||||
final isSkipped = _skippedTasks.contains(task.id);
|
||||
final canBeCompletedNow =
|
||||
!isCompleted && !isSkipped && _arePreviousTasksCompleted(index);
|
||||
|
||||
// Hintergrundfarbe je nach Status:
|
||||
// abgeschlossen → hellgrün, übersprungen → hellgelb, bearbeitbar → weiß, gesperrt → hellgrau
|
||||
final Color cardColor =
|
||||
isCompleted
|
||||
? const Color(0xFFE8F5E9) // hellgrün
|
||||
: isSkipped
|
||||
? const Color(0xFFFFF8E1) // hellgelb
|
||||
: canBeCompletedNow
|
||||
? Colors.white
|
||||
: const Color(0xFFF5F5F5); // hellgrau
|
||||
final Color borderColor =
|
||||
isCompleted
|
||||
? Colors.green[300]!
|
||||
: isSkipped
|
||||
? Colors.amber[300]!
|
||||
: canBeCompletedNow
|
||||
? Colors.grey[300]!
|
||||
: Colors.grey[200]!;
|
||||
final Color circleColor =
|
||||
isCompleted
|
||||
? Colors.green[600]!
|
||||
: isSkipped
|
||||
? Colors.amber[600]!
|
||||
: canBeCompletedNow
|
||||
? Colors.deepPurple[400]!
|
||||
: Colors.grey[400]!;
|
||||
|
||||
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: 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: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTaskDisplayText(
|
||||
task,
|
||||
isCompleted || isSkipped,
|
||||
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),
|
||||
Icon(Icons.check_circle, color: Colors.green[600]),
|
||||
],
|
||||
if (isSkipped) ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.skip_next, color: Colors.amber[600]),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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) async {
|
||||
try {
|
||||
// Persist SVG only (no PNG)
|
||||
await _databaseService.saveTaskSignature(task.id, svg);
|
||||
} 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,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
}) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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) &&
|
||||
!_skippedTasks.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;
|
||||
final description = task.description;
|
||||
|
||||
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 (description?.isNotEmpty == true) {
|
||||
return Text(description!, style: titleStyle);
|
||||
}
|
||||
|
||||
// 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 suffix.isNotEmpty
|
||||
? 'Station ${stationOrder + 1}: $suffix'
|
||||
: 'Station ${stationOrder + 1}';
|
||||
}
|
||||
}
|
||||
|
||||
return 'Station ${stationOrder + 1}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user