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

@@ -4,6 +4,7 @@ import 'package:file_selector/file_selector.dart' as file_selector;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:votianlt_app/services/developer.dart' as developer;
import '../app_theme.dart';
import '../l10n/app_localizations.dart';
class ChatPhotoDialog extends StatefulWidget {
@@ -278,7 +279,7 @@ class _ChatPhotoDialogState extends State<ChatPhotoDialog> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.warning, color: Colors.orange[700], size: 40),
const Icon(Icons.warning, color: AppColors.warning, size: 40),
const SizedBox(height: 12),
Text(_errorMessage!, textAlign: TextAlign.center),
],
@@ -330,11 +331,7 @@ class _ChatPhotoDialogState extends State<ChatPhotoDialog> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.photo_camera_back,
color: Colors.deepPurple[400],
size: 48,
),
Icon(Icons.photo_camera_back, color: AppColors.primary, size: 48),
const SizedBox(height: 12),
const Text(
'Wähle ein Foto von deinem Gerät aus.',

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:votianlt_app/services/developer.dart' as developer;
import 'package:votianlt_app/services/websocket_service.dart';
import 'package:votianlt_app/services/dart_mq.dart';
import '../app_theme.dart';
class OfflineBanner extends StatefulWidget {
const OfflineBanner({super.key});
@@ -24,8 +25,13 @@ class _OfflineBannerState extends State<OfflineBanner> {
// Check if we're already connected (e.g., coming back to this screen)
_hadConnection = _stompService.isConnected && _stompService.isAuthenticated;
// Initialize countdown based on current connection state
_onConnectionChange(_stompService.isConnected && _stompService.isAuthenticated);
_connSub = DartMQ().subscribe<bool>(MQTopics.connectionStatus, _onConnectionChange);
_onConnectionChange(
_stompService.isConnected && _stompService.isAuthenticated,
);
_connSub = DartMQ().subscribe<bool>(
MQTopics.connectionStatus,
_onConnectionChange,
);
}
void _onConnectionChange(bool isConnected) {
@@ -68,7 +74,10 @@ class _OfflineBannerState extends State<OfflineBanner> {
// Only auto-reconnect if we already know the target; discovery remains user-initiated
await _stompService.connect();
} catch (e, stackTrace) {
developer.log('Auto-reconnect attempt failed: $e', name: 'OfflineBanner');
developer.log(
'Auto-reconnect attempt failed: $e',
name: 'OfflineBanner',
);
developer.log('Stack trace: $stackTrace', name: 'OfflineBanner');
}
@@ -114,19 +123,19 @@ class _OfflineBannerState extends State<OfflineBanner> {
title = 'Offline Verbindung verloren';
subtitle = 'Verbindung wird wiederhergestellt.';
icon = Icons.wifi_off;
bgColor = Colors.red[50];
iconColor = Colors.red[700];
titleColor = Colors.red[900];
subtitleColor = Colors.red[800];
bgColor = AppColors.dangerSoft;
iconColor = AppColors.danger;
titleColor = AppColors.danger;
subtitleColor = AppColors.danger.withValues(alpha: 0.85);
} else {
// Initial connection attempt
title = 'Verbinde mit Server...';
subtitle = 'Bitte warten.';
icon = Icons.sync;
bgColor = Colors.orange[50];
iconColor = Colors.orange[700];
titleColor = Colors.orange[900];
subtitleColor = Colors.orange[800];
bgColor = AppColors.warningSoft;
iconColor = AppColors.warning;
titleColor = AppColors.warning;
subtitleColor = AppColors.warning.withValues(alpha: 0.85);
}
return Container(
@@ -152,10 +161,7 @@ class _OfflineBannerState extends State<OfflineBanner> {
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
color: subtitleColor,
fontSize: 12,
),
style: TextStyle(color: subtitleColor, fontSize: 12),
),
],
),
@@ -165,4 +171,3 @@ class _OfflineBannerState extends State<OfflineBanner> {
);
}
}