Files
votianlt/app/lib/widgets/offline_banner.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

174 lines
4.8 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
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});
@override
State<OfflineBanner> createState() => _OfflineBannerState();
}
class _OfflineBannerState extends State<OfflineBanner> {
final StompService _stompService = StompService();
DartMQSubscription? _connSub;
Timer? _countdownTimer;
int _secondsToRetry = 15;
bool _hadConnection = false; // Track if we ever had a successful connection
@override
void initState() {
super.initState();
// 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,
);
}
void _onConnectionChange(bool isConnected) {
if (!mounted) return;
if (isConnected) {
_hadConnection = true; // Mark that we had a successful connection
_stopCountdown();
setState(() {});
} else {
_startCountdown();
}
}
void _startCountdown() {
_stopCountdown();
setState(() {
_secondsToRetry = 15;
});
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
if (!mounted) return;
if (_stompService.isConnected) {
_stopCountdown();
return;
}
// Decrement until 0, then attempt reconnect
if (_secondsToRetry > 1) {
setState(() {
_secondsToRetry = _secondsToRetry - 1;
});
return;
}
// Show 0 for one tick and try to reconnect now
setState(() {
_secondsToRetry = 0;
});
try {
// 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('Stack trace: $stackTrace', name: 'OfflineBanner');
}
if (!mounted) return;
if (!_stompService.isConnected) {
// Still offline -> reset countdown for next attempt
setState(() {
_secondsToRetry = 15;
});
}
});
}
void _stopCountdown() {
_countdownTimer?.cancel();
_countdownTimer = null;
}
@override
void dispose() {
_stopCountdown();
_connSub?.cancel();
_connSub = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
final isOnline = _stompService.isConnected && _stompService.isAuthenticated;
if (isOnline) return const SizedBox.shrink();
// Different messages for initial connection vs connection lost
final String title;
final String subtitle;
final IconData icon;
final Color? bgColor;
final Color? iconColor;
final Color? titleColor;
final Color? subtitleColor;
if (_hadConnection) {
// Connection was lost
title = 'Offline Verbindung verloren';
subtitle = 'Verbindung wird wiederhergestellt.';
icon = Icons.wifi_off;
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 = AppColors.warningSoft;
iconColor = AppColors.warning;
titleColor = AppColors.warning;
subtitleColor = AppColors.warning.withValues(alpha: 0.85);
}
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
color: bgColor,
child: Row(
children: [
Icon(icon, color: iconColor),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: TextStyle(
color: titleColor,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(color: subtitleColor, fontSize: 12),
),
],
),
),
],
),
);
}
}