Files
votianlt/app/lib/main.dart
Sven Carstensen 6e8bedd9b4 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>
2026-04-13 11:26:30 +02:00

200 lines
5.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'app_theme.dart';
import 'login_view.dart';
import 'jobs_view.dart';
import 'cargo_items_view.dart';
import 'chats_view.dart';
import 'chat_details_view.dart';
import 'settings_view.dart';
import 'models/job.dart';
import 'models/chat.dart';
import 'services/database_service.dart';
import 'services/chat_service.dart';
import 'app_state.dart';
import 'navigation_observer.dart';
import 'services/notification_service.dart';
import 'services/websocket_service.dart';
import 'l10n/app_localizations.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize SQLite database
await DatabaseService().initialize();
// Load data from database
await AppState().loadLoginFromDatabase();
// Load language preference
await AppState().loadLanguagePreference();
// Load jobs from database to trigger message type logging at startup
await AppState().refreshJobsFromDatabase();
// Prepare chat service before WebSocket events start flowing
await ChatService().initialize();
// Initialize notification service for local notifications with sound
await NotificationService().initialize();
// Note: WebSocket connection is initiated from the view that needs it:
// - If userId exists: JobsView initiates connection on startup
// - If no userId: LoginView initiates connection when login button is clicked
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
static const Duration _resumeReconnectThreshold = Duration(seconds: 30);
final AppState _appState = AppState();
final WebSocketService _webSocketService = WebSocketService();
DateTime? _lastPausedAt;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
_lastPausedAt = DateTime.now();
return;
}
if (state == AppLifecycleState.resumed) {
final pausedAt = _lastPausedAt;
_lastPausedAt = null;
if (pausedAt == null) {
return;
}
final standbyDuration = DateTime.now().difference(pausedAt);
if (standbyDuration < _resumeReconnectThreshold ||
!_appState.isLoggedIn) {
return;
}
_webSocketService.reconnectForAppResume();
}
}
@override
Widget build(BuildContext context) {
// Check if user is already logged in
final initialRoute = _appState.isLoggedIn ? '/jobs' : '/login';
return ValueListenableBuilder<Locale>(
valueListenable: localeNotifier,
builder: (context, locale, child) {
return MaterialApp(
title: 'VotianLT App',
debugShowCheckedModeBanner: false,
theme: buildAppTheme(),
// Localization configuration
locale: locale,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales:
supportedLanguageCodes.map((code) => Locale(code)).toList(),
navigatorObservers: [routeObserver],
initialRoute: initialRoute,
onGenerateRoute: (settings) {
switch (settings.name) {
case '/login':
final arg = settings.arguments;
final suppress = (arg is bool) ? arg : false;
return MaterialPageRoute(
builder: (_) => LoginView(suppressConnectionSnack: suppress),
);
case '/jobs':
return MaterialPageRoute(builder: (_) => const JobsView());
case '/cargo_items':
final job = settings.arguments as Job;
return MaterialPageRoute(
builder: (_) => CargoItemsView(job: job),
);
case '/chats':
return MaterialPageRoute(builder: (_) => const ChatsView());
case '/chat_details':
final chat = settings.arguments as Chat;
return MaterialPageRoute(
builder: (_) => ChatDetailsView(chat: chat),
);
case '/settings':
return MaterialPageRoute(builder: (_) => const SettingsView());
default:
return MaterialPageRoute(
builder:
(_) => const LoginView(suppressConnectionSnack: false),
);
}
},
);
},
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}