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>
200 lines
5.9 KiB
Dart
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.
|
|
);
|
|
}
|
|
}
|