feat: Drag-and-Drop-Reihenfolge, Station-Abschluss-Flow und UI-Verbesserungen
Lieferstationen-Dialog (Backend/Vaadin): - Aufgaben per Drag & Drop neu anordnen, inkl. Drag-Handle, komprimierter Kachelansicht während des Drags und horizontaler Einfügelinie als Drop-Target - Drop-Indikator wird unterdrückt, wenn der Drop keine Positionsänderung bewirken würde, und nach dem Abschluss clientseitig zuverlässig aufgeräumt - Drag-Handle, Aufgabentyp-Label und Close-Button auf einheitlicher Position ausgerichtet; Abstände in der Kachel komprimiert Station-Abschluss-Flow (Flutter-App + Backend): - Neuer Button "Station abschließen" unter den Aufgaben; deaktiviert, solange Pflichtaufgaben offen sind, ansonsten aktiv (auch wenn nur optionale Aufgaben existieren) - Hinweisdialog nach Erledigung der letzten Pflichtaufgabe sowie Warnung bei offenen optionalen Aufgaben vor dem Senden - Neue station_completed-Nachricht (jobId, jobNumber, stationOrder, completedAt, hasIncompleteOptionalTasks) wird an den Server gesendet - Backend: Auftrag wird nicht mehr automatisch beim Erledigen der letzten Pflichtaufgabe abgeschlossen, sondern erst beim Empfang der station_completed-Nachricht (neuer Handler in MessageController und MessagingConfig) Aufgabenliste in der App: - Farbcodierung optionaler Aufgaben entfernt; stattdessen vertikal zentrierter "Optional"-Chip am rechten Kartenrand Weitere UI-Überarbeitungen über Login, Jobs, Chats, Settings, Aufgaben-Capture- Screens, Offline-Banner und zugehörige Widgets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ 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';
|
||||
@@ -89,7 +90,6 @@ class _TaskViewState extends State<TaskView> {
|
||||
? '${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: () {
|
||||
@@ -116,14 +116,14 @@ class _TaskViewState extends State<TaskView> {
|
||||
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),
|
||||
color: AppColors.surfaceMuted,
|
||||
border: Border.all(color: AppColors.border, width: 1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
_getRemark(),
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.text),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -132,7 +132,13 @@ class _TaskViewState extends State<TaskView> {
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [Expanded(child: _buildTasksStepper())],
|
||||
children: [
|
||||
Expanded(child: _buildTasksStepper()),
|
||||
if (_visibleTasks.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildCompleteStationButton(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -141,22 +147,111 @@ class _TaskViewState extends State<TaskView> {
|
||||
);
|
||||
}
|
||||
|
||||
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: [
|
||||
Icon(Icons.task_outlined, size: 64, color: Colors.grey[400]),
|
||||
const Icon(
|
||||
Icons.task_outlined,
|
||||
size: 64,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
AppLocalizations.of(context).noTasks,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
style: const TextStyle(fontSize: 16, color: AppColors.textMuted),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
AppLocalizations.of(context).noTasksMessage,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.textMuted),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
@@ -174,24 +269,25 @@ class _TaskViewState extends State<TaskView> {
|
||||
|
||||
// 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
|
||||
? const Color(0xFFE8F5E9) // hellgrün
|
||||
? AppColors.successSoft
|
||||
: canBeCompletedNow
|
||||
? Colors.white
|
||||
: const Color(0xFFF5F5F5); // hellgrau
|
||||
? AppColors.surface
|
||||
: AppColors.surfaceMuted;
|
||||
final Color borderColor =
|
||||
isCompleted
|
||||
? Colors.green[300]!
|
||||
? AppColors.success.withValues(alpha: 0.35)
|
||||
: canBeCompletedNow
|
||||
? Colors.grey[300]!
|
||||
: Colors.grey[200]!;
|
||||
? AppColors.border
|
||||
: AppColors.border.withValues(alpha: 0.7);
|
||||
final Color circleColor =
|
||||
isCompleted
|
||||
? Colors.green[600]!
|
||||
? AppColors.success
|
||||
: canBeCompletedNow
|
||||
? Colors.deepPurple[400]!
|
||||
: Colors.grey[400]!;
|
||||
? AppColors.primary
|
||||
: AppColors.textMuted;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
@@ -212,79 +308,93 @@ class _TaskViewState extends State<TaskView> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: cardColor,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
child: Stack(
|
||||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Task content
|
||||
Expanded(
|
||||
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 (task.optional) ...[
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber[50],
|
||||
border: Border.all(color: Colors.amber[300]!),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).optional,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.amber[800],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isCompleted) ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.check_circle, color: Colors.green[600]),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -622,6 +732,7 @@ class _TaskViewState extends State<TaskView> {
|
||||
String? taskType,
|
||||
Map<String, dynamic>? extraData,
|
||||
}) {
|
||||
final bool hadOpenMandatoryBefore = _hasOpenMandatoryTasks;
|
||||
setState(() {
|
||||
_completedTasks.add(taskId);
|
||||
});
|
||||
@@ -638,6 +749,53 @@ class _TaskViewState extends State<TaskView> {
|
||||
} 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) {
|
||||
|
||||
Reference in New Issue
Block a user