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:
2026-04-13 11:26:30 +02:00
parent 1ac755bcbd
commit 6e8bedd9b4
19 changed files with 1458 additions and 548 deletions

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'app_state.dart';
import 'app_theme.dart';
import 'l10n/app_localizations.dart';
import 'l10n/localization_helpers.dart';
import 'services/websocket_service.dart';
@@ -98,7 +99,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
if (isConnected && !_wasConnected) {
_showSnack(
AppLocalizations.of(context).connectionRestored,
backgroundColor: Colors.green,
backgroundColor: AppColors.success,
);
if (_appState.isLoggedIn) {
_loadJobs();
@@ -115,7 +116,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
if (_appState.isLoggedIn && !_isLoggingOut) {
_showSnack(
AppLocalizations.of(context).connectionLost,
backgroundColor: Colors.red,
backgroundColor: AppColors.danger,
);
}
}
@@ -146,7 +147,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
jobNumber != null
? 'Job $jobNumber ${AppLocalizations.of(context).jobRemoved}'
: AppLocalizations.of(context).jobRemoved;
_showSnack(message, backgroundColor: Colors.orange);
_showSnack(message, backgroundColor: AppColors.warning);
}
});
@@ -177,7 +178,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
jobNumber.isNotEmpty
? '${AppLocalizations.of(context).newJobReceived}: $jobNumber'
: AppLocalizations.of(context).newJobReceived;
_showSnack(message, backgroundColor: Colors.green);
_showSnack(message, backgroundColor: AppColors.success);
}
} catch (e) {
developer.log('Error handling job_created event: $e', name: 'JobsView');
@@ -204,7 +205,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
});
_showSnack(
AppLocalizations.of(context).jobsUpdated,
backgroundColor: Colors.green,
backgroundColor: AppColors.success,
);
}
} finally {
@@ -560,7 +561,6 @@ class _JobsViewState extends State<JobsView> with RouteAware {
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(AppLocalizations.of(context).availableJobs),
backgroundColor: Colors.deepPurple[100],
leading: IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
@@ -694,7 +694,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
backgroundColor: AppColors.danger,
foregroundColor: Colors.white,
),
child: Text(AppLocalizations.of(context).logout),
@@ -766,8 +766,8 @@ class _JobsViewState extends State<JobsView> with RouteAware {
icon: const Icon(Icons.refresh),
label: Text(AppLocalizations.of(context).refresh),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple[100],
foregroundColor: Colors.deepPurple[700],
backgroundColor: AppColors.primarySoft,
foregroundColor: AppColors.primaryStrong,
),
),
],
@@ -792,7 +792,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
} else {
_showSnack(
AppLocalizations.of(context).offline,
backgroundColor: Colors.red,
backgroundColor: AppColors.danger,
);
}
}
@@ -908,7 +908,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
if (mounted) {
_showSnack(
AppLocalizations.of(context).jobDeleted,
backgroundColor: Colors.red,
backgroundColor: AppColors.danger,
);
}
} catch (e, st) {
@@ -917,7 +917,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
if (mounted) {
_showSnack(
AppLocalizations.of(context).jobDeleteError,
backgroundColor: Colors.red,
backgroundColor: AppColors.danger,
);
}
} finally {
@@ -935,19 +935,19 @@ class _JobsViewState extends State<JobsView> with RouteAware {
Color statusColor;
switch (job.statusColor) {
case 'green':
statusColor = Colors.green;
statusColor = AppColors.success;
break;
case 'blue':
statusColor = Colors.blue;
statusColor = AppColors.primary;
break;
case 'orange':
statusColor = Colors.orange;
statusColor = AppColors.warning;
break;
case 'red':
statusColor = Colors.red;
statusColor = AppColors.danger;
break;
default:
statusColor = Colors.grey;
statusColor = AppColors.textMuted;
}
// Determine card background color based on task completion
@@ -965,9 +965,9 @@ class _JobsViewState extends State<JobsView> with RouteAware {
if (totalTasks == 0 || completedTasks == 0) {
cardBg = null; // unchanged (default)
} else if (completedTasks > 0 && completedTasks < totalTasks) {
cardBg = Colors.yellow[50];
cardBg = AppColors.warningSoft;
} else if (completedTasks == totalTasks) {
cardBg = Colors.green[50];
cardBg = AppColors.successSoft;
}
// Build robust display strings with fallbacks
final pickupName = _joinNonEmpty([job.pickupFirstName, job.pickupLastName]);
@@ -1033,7 +1033,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
iconSize: 28,
padding: const EdgeInsets.all(10),
splashRadius: 24,
icon: const Icon(Icons.delete, color: Colors.red),
icon: const Icon(Icons.delete, color: AppColors.danger),
tooltip: AppLocalizations.of(context).deleteJob,
onPressed: () {
if (isDeleting) {
@@ -1233,13 +1233,13 @@ class _JobsViewState extends State<JobsView> with RouteAware {
? 0
: completedTasks / totalTasks,
minHeight: 8,
backgroundColor: Colors.grey[200],
backgroundColor: AppColors.border,
valueColor: AlwaysStoppedAnimation<Color>(
completedTasks >= totalTasks
? Colors.green
? AppColors.success
: (completedTasks > 0
? Colors.amber
: Colors.deepPurpleAccent),
? AppColors.warning
: AppColors.primary),
),
),
),
@@ -1336,7 +1336,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
Icon(
Icons.arrow_downward,
size: 16,
color: Colors.blue[600],
color: AppColors.primary,
),
const SizedBox(width: 4),
Text(
@@ -1375,7 +1375,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
tooltip: 'Route planen',
icon: const Icon(
Icons.route,
color: Colors.blueAccent,
color: AppColors.primary,
),
onPressed: () {
if (_routeActionInProgress) return;