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

175
app/lib/app_theme.dart Normal file
View File

@@ -0,0 +1,175 @@
import 'package:flutter/material.dart';
class AppColors {
static const Color primary = Color(0xFF2563EB);
static const Color primaryStrong = Color(0xFF1D4ED8);
static const Color primarySoft = Color(0xFFE8F0FF);
static const Color secondary = Color(0xFF0F4C5C);
static const Color secondarySoft = Color(0xFFDDEEF2);
static const Color success = Color(0xFF059669);
static const Color successSoft = Color(0xFFE7F6F1);
static const Color warning = Color(0xFFD97706);
static const Color warningSoft = Color(0xFFFFF4E5);
static const Color danger = Color(0xFFDC2626);
static const Color dangerSoft = Color(0xFFFDECEC);
static const Color surface = Color(0xFFFFFFFF);
static const Color surfaceMuted = Color(0xFFF7FAFF);
static const Color scaffold = Color(0xFFF5F7FB);
static const Color scaffoldAccent = Color(0xFFEEF4FF);
static const Color border = Color(0xFFD6DDE7);
static const Color borderStrong = Color(0xFFC6D0DD);
static const Color text = Color(0xFF1E293B);
static const Color textStrong = Color(0xFF0F172A);
static const Color textMuted = Color(0xFF64748B);
}
class AppGradients {
static const LinearGradient shellBackground = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColors.scaffoldAccent,
AppColors.surfaceMuted,
AppColors.scaffold,
],
stops: [0, 0.45, 1],
);
}
ThemeData buildAppTheme() {
final colorScheme = ColorScheme.fromSeed(
seedColor: AppColors.primary,
brightness: Brightness.light,
).copyWith(
primary: AppColors.primary,
onPrimary: Colors.white,
primaryContainer: AppColors.primarySoft,
onPrimaryContainer: AppColors.primaryStrong,
secondary: AppColors.secondary,
onSecondary: Colors.white,
secondaryContainer: AppColors.secondarySoft,
onSecondaryContainer: AppColors.secondary,
tertiary: AppColors.success,
onTertiary: Colors.white,
tertiaryContainer: AppColors.successSoft,
onTertiaryContainer: AppColors.success,
surface: AppColors.surface,
onSurface: AppColors.textStrong,
onSurfaceVariant: AppColors.textMuted,
outline: AppColors.border,
surfaceTint: Colors.transparent,
error: AppColors.danger,
onError: Colors.white,
);
final baseTheme = ThemeData(useMaterial3: true, colorScheme: colorScheme);
const radius = Radius.circular(14);
final border = OutlineInputBorder(
borderRadius: const BorderRadius.all(radius),
borderSide: const BorderSide(color: AppColors.border),
);
return baseTheme.copyWith(
scaffoldBackgroundColor: AppColors.scaffold,
canvasColor: AppColors.scaffold,
textTheme: baseTheme.textTheme
.apply(bodyColor: AppColors.text, displayColor: AppColors.textStrong)
.copyWith(
headlineMedium: baseTheme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w700,
color: AppColors.textStrong,
),
titleLarge: baseTheme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
color: AppColors.textStrong,
),
bodyLarge: baseTheme.textTheme.bodyLarge?.copyWith(
color: AppColors.text,
),
bodyMedium: baseTheme.textTheme.bodyMedium?.copyWith(
color: AppColors.text,
),
),
appBarTheme: const AppBarTheme(
backgroundColor: AppColors.primarySoft,
foregroundColor: AppColors.primaryStrong,
surfaceTintColor: Colors.transparent,
elevation: 0,
),
cardTheme: const CardThemeData(
color: AppColors.surface,
surfaceTintColor: Colors.transparent,
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(18)),
side: BorderSide(color: AppColors.border),
),
),
dividerTheme: const DividerThemeData(
color: AppColors.border,
space: 1,
thickness: 1,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.surface,
labelStyle: const TextStyle(color: AppColors.textMuted),
hintStyle: const TextStyle(color: AppColors.textMuted),
prefixIconColor: AppColors.textMuted,
suffixIconColor: AppColors.textMuted,
border: border,
enabledBorder: border,
focusedBorder: border.copyWith(
borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
),
errorBorder: border.copyWith(
borderSide: const BorderSide(color: AppColors.danger),
),
focusedErrorBorder: border.copyWith(
borderSide: const BorderSide(color: AppColors.danger, width: 1.5),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
disabledBackgroundColor: AppColors.border,
disabledForegroundColor: AppColors.textMuted,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
textStyle: const TextStyle(fontWeight: FontWeight.w600),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryStrong,
side: const BorderSide(color: AppColors.border),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.primaryStrong,
textStyle: const TextStyle(fontWeight: FontWeight.w600),
),
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
backgroundColor: AppColors.textStrong,
contentTextStyle: baseTheme.textTheme.bodyMedium?.copyWith(
color: Colors.white,
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
badgeTheme: const BadgeThemeData(
backgroundColor: AppColors.primary,
textColor: Colors.white,
),
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: AppColors.primary,
),
listTileTheme: const ListTileThemeData(iconColor: AppColors.textMuted),
);
}