- Chat: Nachrichten-Status (read/unread), WebSocket-Verbesserungen - App: Login-Optimierung, Job-Übersicht verbessert, neue Übersetzungen - Backend: Dialog-Styling, Invoice-Generator, Job-Verwaltung erweitert - Mehrsprachigkeit: Neue Übersetzungen für DE, EN, ES, ET, FR, LT, LV, PL, RU, TR
802 lines
26 KiB
Dart
802 lines
26 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 '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 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 != null
|
|
? localizeKnownText(context, task.displayName!)
|
|
: null;
|
|
final description =
|
|
task.description != null
|
|
? localizeKnownText(context, task.description!)
|
|
: 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 (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 localizedStationLabel(context, stationOrder + 1, suffix: suffix);
|
|
}
|
|
}
|
|
|
|
return AppLocalizations.of(context).stationNumber(stationOrder + 1);
|
|
}
|
|
}
|