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:
@@ -7,6 +7,7 @@ import 'services/websocket_service.dart';
|
||||
import 'services/dart_mq.dart';
|
||||
import 'services/database_service.dart';
|
||||
import 'app_state.dart';
|
||||
import 'app_theme.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
|
||||
class LoginView extends StatefulWidget {
|
||||
@@ -57,7 +58,7 @@ class _LoginViewState extends State<LoginView> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).loginSuccess),
|
||||
backgroundColor: Colors.green,
|
||||
backgroundColor: AppColors.success,
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
@@ -228,7 +229,7 @@ class _LoginViewState extends State<LoginView> {
|
||||
content: Text(
|
||||
'${AppLocalizations.of(context).loginFailed}: $errorMessage',
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: AppColors.danger,
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
@@ -292,7 +293,7 @@ class _LoginViewState extends State<LoginView> {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(localizations.connecting),
|
||||
backgroundColor: Colors.blue,
|
||||
backgroundColor: AppColors.primary,
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
@@ -345,7 +346,7 @@ class _LoginViewState extends State<LoginView> {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(localizations.connectionTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: AppColors.danger,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@@ -364,7 +365,7 @@ class _LoginViewState extends State<LoginView> {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${localizations.connectionError}: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: AppColors.danger,
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
@@ -420,7 +421,7 @@ class _LoginViewState extends State<LoginView> {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${localizations.loginError}: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: AppColors.danger,
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
@@ -440,203 +441,207 @@ class _LoginViewState extends State<LoginView> {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Logo oder App-Name
|
||||
Icon(
|
||||
Icons.account_circle,
|
||||
size: 100,
|
||||
color: Colors.deepPurple,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Text(
|
||||
l10n.welcomeBack,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[800],
|
||||
body: DecoratedBox(
|
||||
decoration: const BoxDecoration(gradient: AppGradients.shellBackground),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.account_circle,
|
||||
size: 100,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Text(
|
||||
l10n.loginSubtitle,
|
||||
style: Theme.of(context).textTheme.bodyLarge
|
||||
?.copyWith(color: Colors.grey[600]),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
// E-Mail-Feld
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.emailAddress,
|
||||
hintText: l10n.emailAddressHint,
|
||||
prefixIcon: const Icon(Icons.email_outlined),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
Text(
|
||||
l10n.welcomeBack,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textStrong,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return l10n.emailAddressRequired;
|
||||
}
|
||||
if (!RegExp(
|
||||
r'^[A-Za-z0-9_.+-]+@[A-Za-z0-9-]+\.[A-Za-z0-9.-]+$',
|
||||
).hasMatch(value)) {
|
||||
return l10n.emailAddressInvalid;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Passwort-Feld
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: !_isPasswordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.password,
|
||||
hintText: l10n.passwordHint,
|
||||
prefixIcon: const Icon(Icons.lock_outlined),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_isPasswordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
Text(
|
||||
l10n.loginSubtitle,
|
||||
style: Theme.of(context).textTheme.bodyLarge
|
||||
?.copyWith(color: AppColors.textMuted),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
// E-Mail-Feld
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.emailAddress,
|
||||
hintText: l10n.emailAddressHint,
|
||||
prefixIcon: const Icon(Icons.email_outlined),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isPasswordVisible = !_isPasswordVisible;
|
||||
});
|
||||
},
|
||||
filled: true,
|
||||
fillColor: AppColors.surface,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return l10n.passwordRequired;
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return l10n.passwordMinLength;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Passwort vergessen Link
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
// Hier würde die "Passwort vergessen" Funktionalität implementiert werden
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.forgotPasswordMessage),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return l10n.emailAddressRequired;
|
||||
}
|
||||
if (!RegExp(
|
||||
r'^[A-Za-z0-9_.+-]+@[A-Za-z0-9-]+\.[A-Za-z0-9.-]+$',
|
||||
).hasMatch(value)) {
|
||||
return l10n.emailAddressInvalid;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
child: Text(
|
||||
l10n.forgotPassword,
|
||||
style: const TextStyle(
|
||||
color: Colors.deepPurple,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Passwort-Feld
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: !_isPasswordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.password,
|
||||
hintText: l10n.passwordHint,
|
||||
prefixIcon: const Icon(Icons.lock_outlined),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_isPasswordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isPasswordVisible = !_isPasswordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColors.surface,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return l10n.passwordRequired;
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return l10n.passwordMinLength;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Passwort vergessen Link
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
// Hier würde die "Passwort vergessen" Funktionalität implementiert werden
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.forgotPasswordMessage),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
l10n.forgotPassword,
|
||||
style: const TextStyle(
|
||||
color: AppColors.primaryStrong,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Verbindungsstatus
|
||||
// Anmelden Button
|
||||
ElevatedButton(
|
||||
onPressed: _isLoggingIn ? null : _handleLogin,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.deepPurple,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
// Verbindungsstatus
|
||||
// Anmelden Button
|
||||
ElevatedButton(
|
||||
onPressed: _isLoggingIn ? null : _handleLogin,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child:
|
||||
_isLoggingIn
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
child:
|
||||
_isLoggingIn
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
l10n.loggingIn,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
l10n.loggingIn,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
l10n.login,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
l10n.login,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Version number at the bottom
|
||||
if (_appVersion.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
'Version $_appVersion',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||
textAlign: TextAlign.center,
|
||||
if (_appVersion.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
'Version $_appVersion',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user