feat: erweiterte Chat-Funktionalität, UI-Verbesserungen und Lokalisierungsupdates

- Chat: Nachrichten-Status (read/unread), WebSocket-Verbesserungen
- App: Login-Optimierung, Job-Übersicht verbessert, neue Übersetzungen
- Backend: Dialog-Styling, Invoice-Generator, Job-Verwaltung erweitert
- Mehrsprachigkeit: Neue Übersetzungen für DE, EN, ES, ET, FR, LT, LV, PL, RU, TR
This commit is contained in:
2026-04-04 10:30:36 +02:00
parent d6132fabe1
commit bba5733783
55 changed files with 2708 additions and 697 deletions

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart';
import 'l10n/localization_helpers.dart';
import 'models/delivery_station.dart';
import 'models/job.dart';
import 'services/database_service.dart';
@@ -51,6 +52,8 @@ class _CargoItemsViewState extends State<CargoItemsView> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(widget.job.jobNumber),
@@ -93,7 +96,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
Text(
widget.job.jobNumber.isNotEmpty
? widget.job.jobNumber
: widget.job.title,
: localizeKnownText(context, widget.job.title),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@@ -164,7 +167,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
),
const SizedBox(width: 8),
Text(
'Lieferstationen (${_deliveryStations.length})',
l10n.deliveryStationsCount(_deliveryStations.length),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@@ -221,12 +224,12 @@ class _CargoItemsViewState extends State<CargoItemsView> {
),
const SizedBox(height: 16),
Text(
'Keine Lieferstationen',
AppLocalizations.of(context).noDeliveryStations,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
'Dieser Job enthält aktuell keine Lieferstationen.',
AppLocalizations.of(context).noDeliveryStationsMessage,
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
textAlign: TextAlign.center,
),
@@ -255,6 +258,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
station.company.isNotEmpty && station.company != title
? station.company
: null;
final l10n = AppLocalizations.of(context);
final addressLines =
<String>[
[
@@ -289,7 +293,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
stationTitle:
station.displayName.isNotEmpty
? station.displayName
: 'Station ${station.stationOrder + 1}',
: l10n.stationNumber(station.stationOrder + 1),
),
),
);
@@ -313,7 +317,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Station ${station.stationOrder + 1}',
l10n.stationNumber(station.stationOrder + 1),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
@@ -327,7 +331,9 @@ class _CargoItemsViewState extends State<CargoItemsView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title.isNotEmpty ? title : 'Unbenannte Station',
title.isNotEmpty
? localizeKnownText(context, title)
: l10n.unnamedStation,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -359,7 +365,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
const SizedBox(height: 12),
_buildDetailItem(
Icons.phone_outlined,
'Telefon',
l10n.phone,
station.phone,
Colors.green,
),

View File

@@ -5,6 +5,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'l10n/app_localizations.dart';
import 'l10n/localization_helpers.dart';
import 'app_state.dart';
import 'models/chat.dart';
import 'models/chat_message.dart';
@@ -195,9 +196,7 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
if (sender == null || sender.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).noSenderMessage),
),
SnackBar(content: Text(AppLocalizations.of(context).noSenderMessage)),
);
}
return;
@@ -233,7 +232,6 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
return;
}
await _chatService.saveOutgoingMessage(result);
_syncActiveChatFromService();
_messageController.clear();
@@ -250,7 +248,10 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_activeChat.title, style: const TextStyle(fontSize: 16)),
Text(
localizedChatTitle(context, _activeChat),
style: const TextStyle(fontSize: 16),
),
if (isJobChat && _activeChat.jobNumber != null)
Text(
'Job-Nr: ${_activeChat.jobNumber}',
@@ -540,9 +541,7 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
if (sender == null || sender.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).noSenderMessage),
),
SnackBar(content: Text(AppLocalizations.of(context).noSenderMessage)),
);
}
return;
@@ -589,7 +588,6 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
return;
}
await _chatService.saveOutgoingMessage(result);
_syncActiveChatFromService();
if (prepared.bytes.isNotEmpty) {
@@ -645,7 +643,7 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
return '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} else if (messageDate == today.subtract(const Duration(days: 1))) {
// Yesterday
return 'Gestern ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
return '${AppLocalizations.of(context).yesterday} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} else {
// Older - show date and time
return '${dateTime.day.toString().padLeft(2, '0')}.${dateTime.month.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
@@ -659,21 +657,27 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(_activeChat.title),
title: Text(localizedChatTitle(context, _activeChat)),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${AppLocalizations.of(context).status}: ${isJobChat ? AppLocalizations.of(context).chatTypeJob : AppLocalizations.of(context).chatTypeGeneral}'),
Text(
'${AppLocalizations.of(context).status}: ${isJobChat ? AppLocalizations.of(context).chatTypeJob : AppLocalizations.of(context).chatTypeGeneral}',
),
const SizedBox(height: 8),
if (isJobChat && _activeChat.jobNumber != null) ...[
Text('${AppLocalizations.of(context).jobNumber}: ${_activeChat.jobNumber}'),
Text(
'${AppLocalizations.of(context).jobNumber}: ${_activeChat.jobNumber}',
),
const SizedBox(height: 8),
],
Text('${AppLocalizations.of(context).messages}: ${_messages.length}'),
Text(
'${AppLocalizations.of(context).messages}: ${_messages.length}',
),
const SizedBox(height: 8),
Text(
'Erstellt: ${_formatMessageTime(_messages.isNotEmpty ? _messages.first.createdAt : DateTime.now())}',
'${AppLocalizations.of(context).created}: ${_formatMessageTime(_messages.isNotEmpty ? _messages.first.createdAt : DateTime.now())}',
),
],
),

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart';
import 'l10n/localization_helpers.dart';
import 'models/chat.dart';
import 'services/chat_service.dart';
import 'widgets/offline_banner.dart';
@@ -56,10 +57,7 @@ class _ChatsViewState extends State<ChatsView> {
backgroundColor: Colors.deepPurple[100],
),
body: Column(
children: [
const OfflineBanner(),
Expanded(child: _buildBody()),
],
children: [const OfflineBanner(), Expanded(child: _buildBody())],
),
);
}
@@ -70,15 +68,15 @@ class _ChatsViewState extends State<ChatsView> {
}
if (_chats.isEmpty) {
return const Center(
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.chat_outlined, size: 64, color: Colors.grey),
SizedBox(height: 16),
const Icon(Icons.chat_outlined, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(
'Keine Chats verfügbar',
style: TextStyle(fontSize: 16, color: Colors.grey),
AppLocalizations.of(context).noChatsAvailable,
style: const TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
@@ -98,7 +96,9 @@ class _ChatsViewState extends State<ChatsView> {
final isJobChat = chat.type == ChatType.jobSpecific;
final hasMessages = chat.messages.isNotEmpty;
final previewText =
hasMessages ? chat.lastMessagePreview : 'Noch keine Nachrichten';
hasMessages
? chat.lastMessagePreview
: AppLocalizations.of(context).noMessagesYet;
final timeLabel = hasMessages ? _formatTime(chat.lastMessageTime) : '--';
final jobId = chat.jobId?.trim();
final jobNumber = chat.jobNumber?.trim();
@@ -123,9 +123,7 @@ class _ChatsViewState extends State<ChatsView> {
return 'Job $jobId';
}
}
return chat.type == ChatType.general
? 'Allgemeine Nachrichten'
: chat.title;
return localizedChatTitle(context, chat);
}(), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
subtitle: Text(
previewText,

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'app_state.dart';
import 'l10n/app_localizations.dart';
import 'l10n/localization_helpers.dart';
import 'services/websocket_service.dart';
import 'services/dart_mq.dart';
import 'services/chat_service.dart';
@@ -996,7 +997,9 @@ class _JobsViewState extends State<JobsView> with RouteAware {
: job.deliveryCompany));
final deliveryAddress =
hasMultipleDeliveryStations
? '${job.deliveryStations.length} Stationen'
? AppLocalizations.of(
context,
).deliveryStationsCount(job.deliveryStations.length)
: (firstDeliveryStation?.formattedAddress.isNotEmpty == true
? firstDeliveryStation!.formattedAddress
: _joinNonEmpty([
@@ -1116,7 +1119,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
Text(
job.jobNumber.isNotEmpty
? job.jobNumber
: job.title,
: localizeKnownText(context, job.title),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -1571,19 +1574,19 @@ class _JobsViewState extends State<JobsView> with RouteAware {
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(job.title),
title: Text(localizeKnownText(context, job.title)),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${AppLocalizations.of(context).status}: ${job.statusDisplayText}',
'${AppLocalizations.of(context).status}: ${_localizedStatusText(job.status)}',
style: const TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Text(
'${AppLocalizations.of(context).priority}: ${job.priorityDisplayText}',
'${AppLocalizations.of(context).priority}: ${_localizedPriorityText(job.priority)}',
style: const TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
@@ -1612,7 +1615,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Text(job.description),
Text(localizeKnownText(context, job.description)),
],
// CargoItems section
if (job.cargoItems.isNotEmpty) ...[
@@ -1657,7 +1660,9 @@ class _JobsViewState extends State<JobsView> with RouteAware {
if (job.deliveryStations.isNotEmpty) ...[
const SizedBox(height: 16),
Text(
'${AppLocalizations.of(context).delivery} (${job.deliveryStations.length})',
AppLocalizations.of(
context,
).deliveryStationsCount(job.deliveryStations.length),
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
@@ -1674,7 +1679,11 @@ class _JobsViewState extends State<JobsView> with RouteAware {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Station ${station.stationOrder + 1}: ${station.displayName}',
localizedStationLabel(
context,
station.stationOrder + 1,
suffix: station.displayName,
),
style: const TextStyle(fontWeight: FontWeight.w500),
),
if (station.formattedAddress.isNotEmpty) ...[
@@ -1855,12 +1864,49 @@ class _JobsViewState extends State<JobsView> with RouteAware {
if (station.stationOrder == stationOrder) {
final suffix =
station.displayName.isNotEmpty ? station.displayName : station.city;
return suffix.isNotEmpty
? 'Station ${stationOrder + 1}: $suffix'
: 'Station ${stationOrder + 1}';
return localizedStationLabel(context, stationOrder + 1, suffix: suffix);
}
}
return 'Station ${stationOrder + 1}';
return AppLocalizations.of(context).stationNumber(stationOrder + 1);
}
String _localizedStatusText(String status) {
final l10n = AppLocalizations.of(context);
switch (status.toLowerCase()) {
case 'created':
return l10n.statusCreated;
case 'pending':
return l10n.statusPending;
case 'assigned':
return l10n.statusAssigned;
case 'in_progress':
case 'started':
return l10n.statusInProgress;
case 'completed':
case 'done':
return l10n.statusCompleted;
case 'cancelled':
return l10n.statusCancelled;
case 'failed':
return l10n.statusFailed;
default:
return localizeKnownText(context, status);
}
}
String _localizedPriorityText(String priority) {
final l10n = AppLocalizations.of(context);
switch (priority.toLowerCase()) {
case 'low':
return l10n.priorityLow;
case 'high':
return l10n.priorityHigh;
case 'urgent':
return l10n.priorityUrgent;
case 'normal':
default:
return l10n.priorityMedium;
}
}
}

View File

@@ -11,15 +11,28 @@ import 'app_localizations_lv.dart';
import 'app_localizations_lt.dart';
/// Supported language codes
const List<String> supportedLanguageCodes = ['de', 'en', 'es', 'fr', 'pl', 'ru', 'tr', 'et', 'lv', 'lt'];
const List<String> supportedLanguageCodes = [
'de',
'en',
'es',
'fr',
'pl',
'ru',
'tr',
'et',
'lv',
'lt',
];
/// AppLocalizations provides localized strings for the app
abstract class AppLocalizations {
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations) ?? AppLocalizationsDe();
return Localizations.of<AppLocalizations>(context, AppLocalizations) ??
AppLocalizationsDe();
}
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// Language name
String get languageName;
@@ -41,6 +54,7 @@ abstract class AppLocalizations {
String get refresh;
String get version;
String get unknown;
String get yesterday;
// ==================== NAVIGATION ====================
String get jobs;
@@ -58,7 +72,14 @@ abstract class AppLocalizations {
String get welcomeBack;
String get loginSubtitle;
String get email;
String get emailAddress;
String get emailAddressHint;
String get emailAddressRequired;
String get emailAddressInvalid;
String get password;
String get passwordHint;
String get passwordRequired;
String get passwordMinLength;
String get login;
String get loggingIn;
String get forgotPassword;
@@ -101,6 +122,15 @@ abstract class AppLocalizations {
String get deleteJob;
String get jobRemoved;
String get newJobReceived;
String get jobDetails;
String get jobTasks;
String get deliveryStations;
String deliveryStationsCount(int count);
String get noDeliveryStations;
String get noDeliveryStationsMessage;
String get phone;
String get unnamedStation;
String stationNumber(int number);
// ==================== TASKS ====================
String get tasks;
@@ -182,6 +212,9 @@ abstract class AppLocalizations {
String get chatTypeGeneral;
String get jobNumber;
String get messages;
String get generalMessages;
String get noMessagesYet;
String get noChatsAvailable;
String get selectPhoto;
String get unreadMessages;
@@ -217,16 +250,20 @@ abstract class AppLocalizations {
// ==================== STATUS ====================
String get statusCreated;
String get statusPending;
String get statusAssigned;
String get statusInProgress;
String get statusCompleted;
String get statusCancelled;
String get statusFailed;
String get priorityLow;
String get priorityMedium;
String get priorityHigh;
String get priorityUrgent;
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override

View File

@@ -47,6 +47,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get unknown => 'Unbekannt';
@override
String get yesterday => 'Gestern';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Jobs';
@@ -88,9 +91,32 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get email => 'E-Mail';
@override
String get emailAddress => 'E-Mail-Adresse';
@override
String get emailAddressHint => 'Geben Sie Ihre E-Mail-Adresse ein';
@override
String get emailAddressRequired => 'Bitte geben Sie Ihre E-Mail-Adresse ein';
@override
String get emailAddressInvalid =>
'Bitte geben Sie eine gültige E-Mail-Adresse ein';
@override
String get password => 'Passwort';
@override
String get passwordHint => 'Geben Sie Ihr Passwort ein';
@override
String get passwordRequired => 'Bitte geben Sie Ihr Passwort ein';
@override
String get passwordMinLength =>
'Das Passwort muss mindestens 6 Zeichen lang sein';
@override
String get login => 'Anmelden';
@@ -101,7 +127,8 @@ class AppLocalizationsDe extends AppLocalizations {
String get forgotPassword => 'Passwort vergessen?';
@override
String get forgotPasswordMessage => 'Passwort vergessen Funktion noch nicht implementiert';
String get forgotPasswordMessage =>
'Passwort vergessen Funktion noch nicht implementiert';
@override
String get loginSuccess => 'Erfolgreich abgemeldet';
@@ -110,10 +137,12 @@ class AppLocalizationsDe extends AppLocalizations {
String get loginFailed => 'Anmeldung fehlgeschlagen';
@override
String get connectionFailed => 'Verbindung zum Server fehlgeschlagen (Timeout).';
String get connectionFailed =>
'Verbindung zum Server fehlgeschlagen (Timeout).';
@override
String get connectionTimeout => 'Verbindung zum Server fehlgeschlagen (Timeout).';
String get connectionTimeout =>
'Verbindung zum Server fehlgeschlagen (Timeout).';
@override
String get connecting => 'Verbindung zum Server wird hergestellt...';
@@ -212,6 +241,34 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get newJobReceived => 'Neuer Job erhalten';
@override
String get jobDetails => 'Auftragsdetails';
@override
String get jobTasks => 'Aufgaben eines Auftrags';
@override
String get deliveryStations => 'Lieferstationen';
@override
String deliveryStationsCount(int count) => 'Lieferstationen ($count)';
@override
String get noDeliveryStations => 'Keine Lieferstationen';
@override
String get noDeliveryStationsMessage =>
'Dieser Job enthält aktuell keine Lieferstationen.';
@override
String get phone => 'Telefon';
@override
String get unnamedStation => 'Unbenannte Station';
@override
String stationNumber(int number) => 'Station $number';
// ==================== TASKS ====================
@override
String get tasks => 'Aufgaben';
@@ -229,7 +286,8 @@ class AppLocalizationsDe extends AppLocalizations {
String get confirmationRequired => 'Bestätigung erforderlich';
@override
String get confirmationDescription => 'Klicken Sie auf den Button um die Aufgabe zu erledigen.';
String get confirmationDescription =>
'Klicken Sie auf den Button um die Aufgabe zu erledigen.';
@override
String get checklist => 'Checkliste';
@@ -241,7 +299,8 @@ class AppLocalizationsDe extends AppLocalizations {
String get completeTask => 'Aufgabe abschließen';
@override
String get completeTaskConfirm => 'Möchten Sie diese Aufgabe als erledigt markieren?';
String get completeTaskConfirm =>
'Möchten Sie diese Aufgabe als erledigt markieren?';
@override
String get completeTaskNote => 'Notiz (optional)';
@@ -280,7 +339,8 @@ class AppLocalizationsDe extends AppLocalizations {
String get signatureError => 'Fehler beim Speichern der Unterschrift';
@override
String get signatureInstruction => 'Bitte unterschreiben Sie im Feld unten (Maus oder Finger).';
String get signatureInstruction =>
'Bitte unterschreiben Sie im Feld unten (Maus oder Finger).';
@override
String get photoCapture => 'Fotos aufnehmen';
@@ -371,10 +431,12 @@ class AppLocalizationsDe extends AppLocalizations {
String get cameraNotAvailable => 'Kamera nicht verfügbar';
@override
String get cameraNotSupportedMessage => 'Auf dieser Plattform wird die Kamera nicht unterstützt.';
String get cameraNotSupportedMessage =>
'Auf dieser Plattform wird die Kamera nicht unterstützt.';
@override
String get cameraNotSupportedOnPlatform => 'Nicht unterstützt auf dieser Plattform';
String get cameraNotSupportedOnPlatform =>
'Nicht unterstützt auf dieser Plattform';
@override
String get maxPhotosReached => 'Maximum erreicht';
@@ -389,13 +451,15 @@ class AppLocalizationsDe extends AppLocalizations {
String get cameraInitializing => 'Kamera wird initialisiert...';
@override
String get cameraLoadingMessage => 'Bitte warten Sie, während die Kamera geladen wird';
String get cameraLoadingMessage =>
'Bitte warten Sie, während die Kamera geladen wird';
@override
String get addPhotos => 'Fotos hinzufügen';
@override
String get addPhotosInstruction => 'Verwenden Sie den Button „Foto auswählen", um Bilder von Ihrer Kamera oder Festplatte hinzuzufügen.';
String get addPhotosInstruction =>
'Verwenden Sie den Button „Foto auswählen", um Bilder von Ihrer Kamera oder Festplatte hinzuzufügen.';
@override
String get photoOf => 'von';
@@ -411,13 +475,15 @@ class AppLocalizationsDe extends AppLocalizations {
String get noSender => 'Kein Absender verfügbar';
@override
String get noSenderMessage => 'Kein Absender verfügbar. Bitte erneut anmelden.';
String get noSenderMessage =>
'Kein Absender verfügbar. Bitte erneut anmelden.';
@override
String get noRecipient => 'Kein Empfänger konfiguriert';
@override
String get noRecipientMessage => 'Kein Empfänger für diesen Chat konfiguriert.';
String get noRecipientMessage =>
'Kein Empfänger für diesen Chat konfiguriert.';
@override
String get messageSendError => 'Nachricht konnte nicht gesendet werden.';
@@ -443,6 +509,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get messages => 'Nachrichten';
@override
String get generalMessages => 'Allgemeine Nachrichten';
@override
String get noMessagesYet => 'Noch keine Nachrichten';
@override
String get noChatsAvailable => 'Keine Chats verfügbar';
@override
String get selectPhoto => 'Foto auswählen';
@@ -482,7 +557,8 @@ class AppLocalizationsDe extends AppLocalizations {
String get noCargoItems => 'Keine Frachtgüter';
@override
String get noCargoItemsMessage => 'Für diesen Job sind keine Frachtgüter definiert.';
String get noCargoItemsMessage =>
'Für diesen Job sind keine Frachtgüter definiert.';
@override
String get article => 'Artikel';
@@ -528,6 +604,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get statusCreated => 'Erstellt';
@override
String get statusPending => 'Wartend';
@override
String get statusAssigned => 'Zugewiesen';
@@ -537,6 +616,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get statusCompleted => 'Abgeschlossen';
@override
String get statusCancelled => 'Abgebrochen';
@override
String get statusFailed => 'Fehlgeschlagen';
@override
String get priorityLow => 'Niedrig';

View File

@@ -47,6 +47,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get unknown => 'Unknown';
@override
String get yesterday => 'Yesterday';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Jobs';
@@ -88,9 +91,30 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get email => 'Email';
@override
String get emailAddress => 'Email Address';
@override
String get emailAddressHint => 'Enter your email address';
@override
String get emailAddressRequired => 'Please enter your email address';
@override
String get emailAddressInvalid => 'Please enter a valid email address';
@override
String get password => 'Password';
@override
String get passwordHint => 'Enter your password';
@override
String get passwordRequired => 'Please enter your password';
@override
String get passwordMinLength => 'Password must be at least 6 characters long';
@override
String get login => 'Login';
@@ -101,7 +125,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get forgotPassword => 'Forgot Password?';
@override
String get forgotPasswordMessage => 'Forgot password feature not yet implemented';
String get forgotPasswordMessage =>
'Forgot password feature not yet implemented';
@override
String get loginSuccess => 'Successfully logged out';
@@ -212,6 +237,34 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get newJobReceived => 'New job received';
@override
String get jobDetails => 'Job Details';
@override
String get jobTasks => 'Job Tasks';
@override
String get deliveryStations => 'Delivery Stations';
@override
String deliveryStationsCount(int count) => 'Delivery Stations ($count)';
@override
String get noDeliveryStations => 'No Delivery Stations';
@override
String get noDeliveryStationsMessage =>
'This job currently contains no delivery stations.';
@override
String get phone => 'Phone';
@override
String get unnamedStation => 'Unnamed Station';
@override
String stationNumber(int number) => 'Station $number';
// ==================== TASKS ====================
@override
String get tasks => 'Tasks';
@@ -229,7 +282,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get confirmationRequired => 'Confirmation Required';
@override
String get confirmationDescription => 'Click the button to complete the task.';
String get confirmationDescription =>
'Click the button to complete the task.';
@override
String get checklist => 'Checklist';
@@ -241,7 +295,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get completeTask => 'Complete Task';
@override
String get completeTaskConfirm => 'Do you want to mark this task as completed?';
String get completeTaskConfirm =>
'Do you want to mark this task as completed?';
@override
String get completeTaskNote => 'Note (optional)';
@@ -280,7 +335,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get signatureError => 'Error saving signature';
@override
String get signatureInstruction => 'Please sign in the field below (mouse or finger).';
String get signatureInstruction =>
'Please sign in the field below (mouse or finger).';
@override
String get photoCapture => 'Take Photos';
@@ -371,7 +427,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get cameraNotAvailable => 'Camera not available';
@override
String get cameraNotSupportedMessage => 'The camera is not supported on this platform.';
String get cameraNotSupportedMessage =>
'The camera is not supported on this platform.';
@override
String get cameraNotSupportedOnPlatform => 'Not supported on this platform';
@@ -395,7 +452,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get addPhotos => 'Add photos';
@override
String get addPhotosInstruction => 'Use the "Select photo" button to add images from your camera or hard drive.';
String get addPhotosInstruction =>
'Use the "Select photo" button to add images from your camera or hard drive.';
@override
String get photoOf => 'of';
@@ -443,6 +501,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get messages => 'Messages';
@override
String get generalMessages => 'General Messages';
@override
String get noMessagesYet => 'No messages yet';
@override
String get noChatsAvailable => 'No chats available';
@override
String get selectPhoto => 'Select Photo';
@@ -528,6 +595,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get statusCreated => 'Created';
@override
String get statusPending => 'Pending';
@override
String get statusAssigned => 'Assigned';
@@ -537,6 +607,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get statusCompleted => 'Completed';
@override
String get statusCancelled => 'Cancelled';
@override
String get statusFailed => 'Failed';
@override
String get priorityLow => 'Low';

View File

@@ -35,6 +35,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get unknown => 'Desconocido';
@override
String get yesterday => 'Ayer';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Trabajos';
@@ -64,8 +67,33 @@ class AppLocalizationsEs extends AppLocalizations {
String get loginSubtitle => 'Inicie sesión en su cuenta';
@override
String get email => 'Correo electrónico';
@override
String get emailAddress => 'Dirección de correo electrónico';
@override
String get emailAddressHint =>
'Introduzca su dirección de correo electrónico';
@override
String get emailAddressRequired =>
'Por favor, introduzca su dirección de correo electrónico';
@override
String get emailAddressInvalid =>
'Por favor, introduzca una dirección de correo electrónico válida';
@override
String get password => 'Contraseña';
@override
String get passwordHint => 'Introduzca su contraseña';
@override
String get passwordRequired => 'Por favor, introduzca su contraseña';
@override
String get passwordMinLength =>
'La contraseña debe tener al menos 6 caracteres';
@override
String get login => 'Iniciar sesión';
@override
@@ -73,15 +101,18 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get forgotPassword => '¿Olvidó su contraseña?';
@override
String get forgotPasswordMessage => 'Función de contraseña olvidada aún no implementada';
String get forgotPasswordMessage =>
'Función de contraseña olvidada aún no implementada';
@override
String get loginSuccess => 'Sesión cerrada correctamente';
@override
String get loginFailed => 'Error al iniciar sesión';
@override
String get connectionFailed => 'Error de conexión al servidor (Tiempo agotado).';
String get connectionFailed =>
'Error de conexión al servidor (Tiempo agotado).';
@override
String get connectionTimeout => 'Error de conexión al servidor (Tiempo agotado).';
String get connectionTimeout =>
'Error de conexión al servidor (Tiempo agotado).';
@override
String get connecting => 'Conectando al servidor...';
@override
@@ -149,6 +180,34 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get newJobReceived => 'Nuevo trabajo recibido';
@override
String get jobDetails => 'Detalles del pedido';
@override
String get jobTasks => 'Tareas del pedido';
@override
String get deliveryStations => 'Estaciones de entrega';
@override
String deliveryStationsCount(int count) => 'Estaciones de entrega ($count)';
@override
String get noDeliveryStations => 'No hay estaciones de entrega';
@override
String get noDeliveryStationsMessage =>
'Este trabajo no contiene estaciones de entrega actualmente.';
@override
String get phone => 'Teléfono';
@override
String get unnamedStation => 'Estación sin nombre';
@override
String stationNumber(int number) => 'Estación $number';
// ==================== TASKS ====================
@override
String get tasks => 'Tareas';
@@ -161,7 +220,8 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get confirmationRequired => 'Confirmación requerida';
@override
String get confirmationDescription => 'Haga clic en el botón para completar la tarea.';
String get confirmationDescription =>
'Haga clic en el botón para completar la tarea.';
@override
String get checklist => 'Lista de verificación';
@override
@@ -195,7 +255,8 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get signatureError => 'Error al guardar la firma';
@override
String get signatureInstruction => 'Por favor, firme en el campo de abajo (ratón o dedo).';
String get signatureInstruction =>
'Por favor, firme en el campo de abajo (ratón o dedo).';
@override
String get photoCapture => 'Tomar fotos';
@override
@@ -243,11 +304,14 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get enterBarcode => 'Ingresar código de barras';
@override
String get barcodeEnterDescription => 'Por favor ingrese los códigos de barras:';
String get barcodeEnterDescription =>
'Por favor ingrese los códigos de barras:';
@override
String barcodeNumberRequired(int number) => 'Código de barras $number (requerido)';
String barcodeNumberRequired(int number) =>
'Código de barras $number (requerido)';
@override
String barcodeNumberOptional(int number) => 'Código de barras $number (opcional)';
String barcodeNumberOptional(int number) =>
'Código de barras $number (opcional)';
@override
String get barcodeError => 'Error al escanear el código de barras';
@override
@@ -257,7 +321,8 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get cameraNotAvailable => 'Cámara no disponible';
@override
String get cameraNotSupportedMessage => 'La cámara no es compatible con esta plataforma.';
String get cameraNotSupportedMessage =>
'La cámara no es compatible con esta plataforma.';
@override
String get cameraNotSupportedOnPlatform => 'No soportado en esta plataforma';
@override
@@ -269,11 +334,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get cameraInitializing => 'Inicializando cámara...';
@override
String get cameraLoadingMessage => 'Por favor espere mientras se carga la cámara';
String get cameraLoadingMessage =>
'Por favor espere mientras se carga la cámara';
@override
String get addPhotos => 'Añadir fotos';
@override
String get addPhotosInstruction => 'Use el botón "Seleccionar foto" para añadir imágenes de su cámara o disco duro.';
String get addPhotosInstruction =>
'Use el botón "Seleccionar foto" para añadir imágenes de su cámara o disco duro.';
@override
String get photoOf => 'de';
@@ -285,11 +352,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get noSender => 'No hay remitente disponible';
@override
String get noSenderMessage => 'No hay remitente disponible. Por favor inicie sesión de nuevo.';
String get noSenderMessage =>
'No hay remitente disponible. Por favor inicie sesión de nuevo.';
@override
String get noRecipient => 'No hay destinatario configurado';
@override
String get noRecipientMessage => 'No hay destinatario configurado para este chat.';
String get noRecipientMessage =>
'No hay destinatario configurado para este chat.';
@override
String get messageSendError => 'El mensaje no pudo ser enviado.';
@override
@@ -306,6 +375,15 @@ class AppLocalizationsEs extends AppLocalizations {
String get jobNumber => 'Número de trabajo';
@override
String get messages => 'Mensajes';
@override
String get generalMessages => 'Mensajes generales';
@override
String get noMessagesYet => 'Todavía no hay mensajes';
@override
String get noChatsAvailable => 'No hay chats disponibles';
@override
String get selectPhoto => 'Seleccionar foto';
@override
@@ -327,7 +405,8 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get noCargoItems => 'Sin artículos de carga';
@override
String get noCargoItemsMessage => 'No hay artículos de carga definidos para este trabajo.';
String get noCargoItemsMessage =>
'No hay artículos de carga definidos para este trabajo.';
@override
String get article => 'Artículo';
@@ -369,12 +448,18 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get statusCreated => 'Creado';
@override
String get statusPending => 'Pendiente';
@override
String get statusAssigned => 'Asignado';
@override
String get statusInProgress => 'En progreso';
@override
String get statusCompleted => 'Completado';
@override
String get statusCancelled => 'Cancelado';
@override
String get statusFailed => 'Fallido';
@override
String get priorityLow => 'Baja';
@override
String get priorityMedium => 'Media';

View File

@@ -35,6 +35,9 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get unknown => 'Tundmatu';
@override
String get yesterday => 'Eile';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Tööd';
@@ -64,8 +67,29 @@ class AppLocalizationsEt extends AppLocalizations {
String get loginSubtitle => 'Logige oma kontosse sisse';
@override
String get email => 'E-post';
@override
String get emailAddress => 'E-posti aadress';
@override
String get emailAddressHint => 'Sisestage oma e-posti aadress';
@override
String get emailAddressRequired => 'Palun sisestage oma e-posti aadress';
@override
String get emailAddressInvalid => 'Palun sisestage kehtiv e-posti aadress';
@override
String get password => 'Parool';
@override
String get passwordHint => 'Sisestage oma parool';
@override
String get passwordRequired => 'Palun sisestage oma parool';
@override
String get passwordMinLength => 'Parool peab olema vähemalt 6 tähemärki pikk';
@override
String get login => 'Logi sisse';
@override
@@ -73,15 +97,18 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get forgotPassword => 'Unustasid parooli?';
@override
String get forgotPasswordMessage => 'Unustatud parooli funktsioon pole veel rakendatud';
String get forgotPasswordMessage =>
'Unustatud parooli funktsioon pole veel rakendatud';
@override
String get loginSuccess => 'Edukalt välja logitud';
@override
String get loginFailed => 'Sisselogimine ebaõnnestus';
@override
String get connectionFailed => 'Serveriga ühenduse loomine ebaõnnestus (Aegunud).';
String get connectionFailed =>
'Serveriga ühenduse loomine ebaõnnestus (Aegunud).';
@override
String get connectionTimeout => 'Serveriga ühenduse loomine ebaõnnestus (Aegunud).';
String get connectionTimeout =>
'Serveriga ühenduse loomine ebaõnnestus (Aegunud).';
@override
String get connecting => 'Serveriga ühendamine...';
@override
@@ -149,6 +176,34 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get newJobReceived => 'Uus töö saadud';
@override
String get jobDetails => 'Töö üksikasjad';
@override
String get jobTasks => 'Töö ülesanded';
@override
String get deliveryStations => 'Tarnejaamad';
@override
String deliveryStationsCount(int count) => 'Tarnejaamad ($count)';
@override
String get noDeliveryStations => 'Tarnejaamu pole';
@override
String get noDeliveryStationsMessage =>
'Sellel tööl ei ole praegu tarnejaamu.';
@override
String get phone => 'Telefon';
@override
String get unnamedStation => 'Nimetu jaam';
@override
String stationNumber(int number) => 'Jaam $number';
// ==================== TASKS ====================
@override
String get tasks => 'Ülesanded';
@@ -161,7 +216,8 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get confirmationRequired => 'Vajalik kinnitus';
@override
String get confirmationDescription => 'Ülesande lõpuleviimiseks klõpsake nuppu.';
String get confirmationDescription =>
'Ülesande lõpuleviimiseks klõpsake nuppu.';
@override
String get checklist => 'Kontrollnimekiri';
@override
@@ -169,7 +225,8 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get completeTask => 'Lõpeta ülesanne';
@override
String get completeTaskConfirm => 'Kas soovite selle ülesande lõpetatuks märgistada?';
String get completeTaskConfirm =>
'Kas soovite selle ülesande lõpetatuks märgistada?';
@override
String get completeTaskNote => 'Märkus (valikuline)';
@override
@@ -195,7 +252,8 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get signatureError => 'Viga allkirja salvestamisel';
@override
String get signatureInstruction => 'Palun allkirjastage allolevas väljas (hiir või sõrm).';
String get signatureInstruction =>
'Palun allkirjastage allolevas väljas (hiir või sõrm).';
@override
String get photoCapture => 'Tee pilte';
@override
@@ -257,7 +315,8 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get cameraNotAvailable => 'Kaamera pole saadaval';
@override
String get cameraNotSupportedMessage => 'Kaamerat ei toetata sellel platvormil.';
String get cameraNotSupportedMessage =>
'Kaamerat ei toetata sellel platvormil.';
@override
String get cameraNotSupportedOnPlatform => 'Sellel platvormil ei toetata';
@override
@@ -273,7 +332,8 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get addPhotos => 'Lisa fotod';
@override
String get addPhotosInstruction => 'Kasutage nuppu "Vali foto", et lisada pilte kaamerast või kõvakettalt.';
String get addPhotosInstruction =>
'Kasutage nuppu "Vali foto", et lisada pilte kaamerast või kõvakettalt.';
@override
String get photoOf => '/';
@@ -285,11 +345,13 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get noSender => 'Saatja pole saadaval';
@override
String get noSenderMessage => 'Saatja pole saadaval. Palun logige uuesti sisse.';
String get noSenderMessage =>
'Saatja pole saadaval. Palun logige uuesti sisse.';
@override
String get noRecipient => 'Vastuvõtjat pole konfigureeritud';
@override
String get noRecipientMessage => 'Selle vestluse jaoks pole vastuvõtjat konfigureeritud.';
String get noRecipientMessage =>
'Selle vestluse jaoks pole vastuvõtjat konfigureeritud.';
@override
String get messageSendError => 'Sõnumi saatmine ebaõnnestus.';
@override
@@ -306,6 +368,15 @@ class AppLocalizationsEt extends AppLocalizations {
String get jobNumber => 'Töö number';
@override
String get messages => 'Sõnumid';
@override
String get generalMessages => 'Üldised sõnumid';
@override
String get noMessagesYet => 'Sõnumeid veel pole';
@override
String get noChatsAvailable => 'Vestlusi pole saadaval';
@override
String get selectPhoto => 'Vali foto';
@override
@@ -369,12 +440,18 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get statusCreated => 'Loodud';
@override
String get statusPending => 'Ootel';
@override
String get statusAssigned => 'Määratud';
@override
String get statusInProgress => 'Töös';
@override
String get statusCompleted => 'Lõpetatud';
@override
String get statusCancelled => 'Tühistatud';
@override
String get statusFailed => 'Ebaõnnestunud';
@override
String get priorityLow => 'Madal';
@override
String get priorityMedium => 'Keskmine';

View File

@@ -35,6 +35,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get unknown => 'Inconnu';
@override
String get yesterday => 'Hier';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Emplois';
@@ -64,8 +67,30 @@ class AppLocalizationsFr extends AppLocalizations {
String get loginSubtitle => 'Connectez-vous à votre compte';
@override
String get email => 'E-mail';
@override
String get emailAddress => 'Adresse e-mail';
@override
String get emailAddressHint => 'Saisissez votre adresse e-mail';
@override
String get emailAddressRequired => 'Veuillez saisir votre adresse e-mail';
@override
String get emailAddressInvalid => 'Veuillez saisir une adresse e-mail valide';
@override
String get password => 'Mot de passe';
@override
String get passwordHint => 'Saisissez votre mot de passe';
@override
String get passwordRequired => 'Veuillez saisir votre mot de passe';
@override
String get passwordMinLength =>
'Le mot de passe doit contenir au moins 6 caractères';
@override
String get login => 'Connexion';
@override
@@ -73,15 +98,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get forgotPassword => 'Mot de passe oublié?';
@override
String get forgotPasswordMessage => 'Fonction mot de passe oublié pas encore implémentée';
String get forgotPasswordMessage =>
'Fonction mot de passe oublié pas encore implémentée';
@override
String get loginSuccess => 'Déconnexion réussie';
@override
String get loginFailed => 'Échec de la connexion';
@override
String get connectionFailed => 'Échec de la connexion au serveur (Délai dépassé).';
String get connectionFailed =>
'Échec de la connexion au serveur (Délai dépassé).';
@override
String get connectionTimeout => 'Échec de la connexion au serveur (Délai dépassé).';
String get connectionTimeout =>
'Échec de la connexion au serveur (Délai dépassé).';
@override
String get connecting => 'Connexion au serveur...';
@override
@@ -137,7 +165,8 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get jobsUpdated => 'Emplois actualisés';
@override
String get connectionRestored => 'Connexion restaurée. Chargement des emplois...';
String get connectionRestored =>
'Connexion restaurée. Chargement des emplois...';
@override
String get connectionLost => 'Connexion perdue. Hors ligne.';
@override
@@ -149,6 +178,34 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get newJobReceived => 'Nouvel emploi reçu';
@override
String get jobDetails => 'Détails de la commande';
@override
String get jobTasks => 'Tâches de la commande';
@override
String get deliveryStations => 'Stations de livraison';
@override
String deliveryStationsCount(int count) => 'Stations de livraison ($count)';
@override
String get noDeliveryStations => 'Aucune station de livraison';
@override
String get noDeliveryStationsMessage =>
'Cette mission ne contient actuellement aucune station de livraison.';
@override
String get phone => 'Téléphone';
@override
String get unnamedStation => 'Station sans nom';
@override
String stationNumber(int number) => 'Station $number';
// ==================== TASKS ====================
@override
String get tasks => 'Tâches';
@@ -161,7 +218,8 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get confirmationRequired => 'Confirmation requise';
@override
String get confirmationDescription => 'Cliquez sur le bouton pour terminer la tâche.';
String get confirmationDescription =>
'Cliquez sur le bouton pour terminer la tâche.';
@override
String get checklist => 'Liste de contrôle';
@override
@@ -169,7 +227,8 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get completeTask => 'Terminer la tâche';
@override
String get completeTaskConfirm => 'Voulez-vous marquer cette tâche comme terminée?';
String get completeTaskConfirm =>
'Voulez-vous marquer cette tâche comme terminée?';
@override
String get completeTaskNote => 'Note (optionnelle)';
@override
@@ -193,9 +252,11 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get clear => 'Effacer';
@override
String get signatureError => 'Erreur lors de l\'enregistrement de la signature';
String get signatureError =>
'Erreur lors de l\'enregistrement de la signature';
@override
String get signatureInstruction => 'Veuillez signer dans le champ ci-dessous (souris ou doigt).';
String get signatureInstruction =>
'Veuillez signer dans le champ ci-dessous (souris ou doigt).';
@override
String get photoCapture => 'Prendre des photos';
@override
@@ -221,7 +282,8 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get deletePhoto => 'Supprimer la photo';
@override
String get deletePhotoConfirm => 'Voulez-vous vraiment supprimer cette photo?';
String get deletePhotoConfirm =>
'Voulez-vous vraiment supprimer cette photo?';
@override
String get barcode => 'Code-barres';
@override
@@ -257,9 +319,11 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get cameraNotAvailable => 'Caméra non disponible';
@override
String get cameraNotSupportedMessage => 'La caméra n\'est pas prise en charge sur cette plateforme.';
String get cameraNotSupportedMessage =>
'La caméra n\'est pas prise en charge sur cette plateforme.';
@override
String get cameraNotSupportedOnPlatform => 'Non supporté sur cette plateforme';
String get cameraNotSupportedOnPlatform =>
'Non supporté sur cette plateforme';
@override
String get maxPhotosReached => 'Maximum atteint';
@override
@@ -269,11 +333,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get cameraInitializing => 'Initialisation de la caméra...';
@override
String get cameraLoadingMessage => 'Veuillez patienter pendant le chargement de la caméra';
String get cameraLoadingMessage =>
'Veuillez patienter pendant le chargement de la caméra';
@override
String get addPhotos => 'Ajouter des photos';
@override
String get addPhotosInstruction => 'Utilisez le bouton "Sélectionner une photo" pour ajouter des images depuis votre appareil photo ou disque dur.';
String get addPhotosInstruction =>
'Utilisez le bouton "Sélectionner une photo" pour ajouter des images depuis votre appareil photo ou disque dur.';
@override
String get photoOf => 'sur';
@@ -285,11 +351,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get noSender => 'Aucun expéditeur disponible';
@override
String get noSenderMessage => 'Aucun expéditeur disponible. Veuillez vous reconnecter.';
String get noSenderMessage =>
'Aucun expéditeur disponible. Veuillez vous reconnecter.';
@override
String get noRecipient => 'Aucun destinataire configuré';
@override
String get noRecipientMessage => 'Aucun destinataire configuré pour cette discussion.';
String get noRecipientMessage =>
'Aucun destinataire configuré pour cette discussion.';
@override
String get messageSendError => 'Le message n\'a pas pu être envoyé.';
@override
@@ -306,6 +374,15 @@ class AppLocalizationsFr extends AppLocalizations {
String get jobNumber => 'Numéro d\'emploi';
@override
String get messages => 'Messages';
@override
String get generalMessages => 'Messages généraux';
@override
String get noMessagesYet => 'Pas encore de messages';
@override
String get noChatsAvailable => 'Aucune discussion disponible';
@override
String get selectPhoto => 'Sélectionner une photo';
@override
@@ -327,7 +404,8 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get noCargoItems => 'Aucun article de cargaison';
@override
String get noCargoItemsMessage => 'Aucun article de cargaison défini pour cet emploi.';
String get noCargoItemsMessage =>
'Aucun article de cargaison défini pour cet emploi.';
@override
String get article => 'Article';
@@ -369,12 +447,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get statusCreated => 'Créé';
@override
String get statusPending => 'En attente';
@override
String get statusAssigned => 'Assigné';
@override
String get statusInProgress => 'En cours';
@override
String get statusCompleted => 'Terminé';
@override
String get statusCancelled => 'Annulé';
@override
String get statusFailed => 'Échoué';
@override
String get priorityLow => 'Basse';
@override
String get priorityMedium => 'Moyenne';

View File

@@ -35,6 +35,9 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get unknown => 'Nežinoma';
@override
String get yesterday => 'Vakar';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Darbai';
@@ -64,8 +67,30 @@ class AppLocalizationsLt extends AppLocalizations {
String get loginSubtitle => 'Prisijunkite prie savo paskyros';
@override
String get email => 'El. paštas';
@override
String get emailAddress => 'El. pašto adresas';
@override
String get emailAddressHint => 'Įveskite savo el. pašto adresą';
@override
String get emailAddressRequired => 'Prašome įvesti savo el. pašto adresą';
@override
String get emailAddressInvalid =>
'Prašome įvesti galiojantį el. pašto adresą';
@override
String get password => 'Slaptažodis';
@override
String get passwordHint => 'Įveskite savo slaptažodį';
@override
String get passwordRequired => 'Prašome įvesti savo slaptažodį';
@override
String get passwordMinLength => 'Slaptažodis turi būti bent 6 simbolių ilgio';
@override
String get login => 'Prisijungti';
@override
@@ -73,15 +98,18 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get forgotPassword => 'Pamiršote slaptažodį?';
@override
String get forgotPasswordMessage => 'Pamiršto slaptažodžio funkcija dar neįdiegta';
String get forgotPasswordMessage =>
'Pamiršto slaptažodžio funkcija dar neįdiegta';
@override
String get loginSuccess => 'Sėkmingai atsijungta';
@override
String get loginFailed => 'Prisijungimas nepavyko';
@override
String get connectionFailed => 'Nepavyko prisijungti prie serverio (Laikas baigėsi).';
String get connectionFailed =>
'Nepavyko prisijungti prie serverio (Laikas baigėsi).';
@override
String get connectionTimeout => 'Nepavyko prisijungti prie serverio (Laikas baigėsi).';
String get connectionTimeout =>
'Nepavyko prisijungti prie serverio (Laikas baigėsi).';
@override
String get connecting => 'Jungiamasi prie serverio...';
@override
@@ -149,6 +177,34 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get newJobReceived => 'Gautas naujas darbas';
@override
String get jobDetails => 'Užsakymo detalės';
@override
String get jobTasks => 'Užsakymo užduotys';
@override
String get deliveryStations => 'Pristatymo stotelės';
@override
String deliveryStationsCount(int count) => 'Pristatymo stotelės ($count)';
@override
String get noDeliveryStations => 'Nėra pristatymo stotelių';
@override
String get noDeliveryStationsMessage =>
'Ši užduotis šiuo metu neturi pristatymo stotelių.';
@override
String get phone => 'Telefonas';
@override
String get unnamedStation => 'Neįvardyta stotelė';
@override
String stationNumber(int number) => 'Stotelė $number';
// ==================== TASKS ====================
@override
String get tasks => 'Užduotys';
@@ -161,7 +217,8 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get confirmationRequired => 'Reikalingas patvirtinimas';
@override
String get confirmationDescription => 'Spustelėkite mygtuką, kad atliktumėte užduotį.';
String get confirmationDescription =>
'Spustelėkite mygtuką, kad atliktumėte užduotį.';
@override
String get checklist => 'Patikros sąrašas';
@override
@@ -169,7 +226,8 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get completeTask => 'Baigti užduotį';
@override
String get completeTaskConfirm => 'Ar norite pažymėti šią užduotį kaip baigtą?';
String get completeTaskConfirm =>
'Ar norite pažymėti šią užduotį kaip baigtą?';
@override
String get completeTaskNote => 'Pastaba (neprivaloma)';
@override
@@ -195,7 +253,8 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get signatureError => 'Klaida išsaugant parašą';
@override
String get signatureInstruction => 'Prašome pasirašyti laukelyje žemiau (pele arba pirštu).';
String get signatureInstruction =>
'Prašome pasirašyti laukelyje žemiau (pele arba pirštu).';
@override
String get photoCapture => 'Daryti nuotraukas';
@override
@@ -245,9 +304,11 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get barcodeEnterDescription => 'Prašome įvesti brūkšninius kodus:';
@override
String barcodeNumberRequired(int number) => 'Brūkšninis kodas $number (būtinas)';
String barcodeNumberRequired(int number) =>
'Brūkšninis kodas $number (būtinas)';
@override
String barcodeNumberOptional(int number) => 'Brūkšninis kodas $number (neprivalomas)';
String barcodeNumberOptional(int number) =>
'Brūkšninis kodas $number (neprivalomas)';
@override
String get barcodeError => 'Klaida skaitant brūkšninį kodą';
@override
@@ -257,7 +318,8 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get cameraNotAvailable => 'Kamera nepasiekiama';
@override
String get cameraNotSupportedMessage => 'Šioje platformoje kamera nepalaikoma.';
String get cameraNotSupportedMessage =>
'Šioje platformoje kamera nepalaikoma.';
@override
String get cameraNotSupportedOnPlatform => 'Nepalaikoma šioje platformoje';
@override
@@ -273,7 +335,8 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get addPhotos => 'Pridėti nuotraukas';
@override
String get addPhotosInstruction => 'Naudokite mygtuką "Pasirinkti nuotrauką", norėdami pridėti vaizdų iš fotoaparato ar standžiojo disko.';
String get addPhotosInstruction =>
'Naudokite mygtuką "Pasirinkti nuotrauką", norėdami pridėti vaizdų iš fotoaparato ar standžiojo disko.';
@override
String get photoOf => '';
@@ -285,7 +348,8 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get noSender => 'Siuntėjas nepasiekiamas';
@override
String get noSenderMessage => 'Siuntėjas nepasiekiamas. Prašome prisijungti dar kartą.';
String get noSenderMessage =>
'Siuntėjas nepasiekiamas. Prašome prisijungti dar kartą.';
@override
String get noRecipient => 'Gavėjas nesukonfigūruotas';
@override
@@ -306,6 +370,15 @@ class AppLocalizationsLt extends AppLocalizations {
String get jobNumber => 'Darbo numeris';
@override
String get messages => 'Žinutės';
@override
String get generalMessages => 'Bendri pranešimai';
@override
String get noMessagesYet => 'Pranešimų dar nėra';
@override
String get noChatsAvailable => 'Nėra galimų pokalbių';
@override
String get selectPhoto => 'Pasirinkti nuotrauką';
@override
@@ -327,7 +400,8 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get noCargoItems => 'Nėra krovinių pozicijų';
@override
String get noCargoItemsMessage => 'Šiam darbui nėra apibrėžtų krovinių pozicijų.';
String get noCargoItemsMessage =>
'Šiam darbui nėra apibrėžtų krovinių pozicijų.';
@override
String get article => 'Pozicija';
@@ -369,12 +443,18 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get statusCreated => 'Sukurta';
@override
String get statusPending => 'Laukiama';
@override
String get statusAssigned => 'Priskirta';
@override
String get statusInProgress => 'Vykdoma';
@override
String get statusCompleted => 'Baigta';
@override
String get statusCancelled => 'Atšaukta';
@override
String get statusFailed => 'Nepavyko';
@override
String get priorityLow => 'Žemas';
@override
String get priorityMedium => 'Vidutinis';

View File

@@ -35,6 +35,9 @@ class AppLocalizationsLv extends AppLocalizations {
@override
String get unknown => 'Nezināms';
@override
String get yesterday => 'Vakar';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Darbi';
@@ -64,8 +67,29 @@ class AppLocalizationsLv extends AppLocalizations {
String get loginSubtitle => 'Pierakstieties savā kontā';
@override
String get email => 'E-pasts';
@override
String get emailAddress => 'E-pasta adrese';
@override
String get emailAddressHint => 'Ievadiet savu e-pasta adresi';
@override
String get emailAddressRequired => 'Lūdzu, ievadiet savu e-pasta adresi';
@override
String get emailAddressInvalid => 'Lūdzu, ievadiet derīgu e-pasta adresi';
@override
String get password => 'Parole';
@override
String get passwordHint => 'Ievadiet savu paroli';
@override
String get passwordRequired => 'Lūdzu, ievadiet savu paroli';
@override
String get passwordMinLength => 'Parolei jābūt vismaz 6 rakstzīmes garai';
@override
String get login => 'Pierakstīties';
@override
@@ -73,7 +97,8 @@ class AppLocalizationsLv extends AppLocalizations {
@override
String get forgotPassword => 'Aizmirsāt paroli?';
@override
String get forgotPasswordMessage => 'Aizmirstās paroles funkcija vēl nav ieviesta';
String get forgotPasswordMessage =>
'Aizmirstās paroles funkcija vēl nav ieviesta';
@override
String get loginSuccess => 'Veiksmīgi izrakstījās';
@override
@@ -149,6 +174,34 @@ class AppLocalizationsLv extends AppLocalizations {
@override
String get newJobReceived => 'Saņemts jauns darbs';
@override
String get jobDetails => 'Darba detaļas';
@override
String get jobTasks => 'Darba uzdevumi';
@override
String get deliveryStations => 'Piegādes stacijas';
@override
String deliveryStationsCount(int count) => 'Piegādes stacijas ($count)';
@override
String get noDeliveryStations => 'Nav piegādes staciju';
@override
String get noDeliveryStationsMessage =>
'Šajā darbā pašlaik nav piegādes staciju.';
@override
String get phone => 'Tālrunis';
@override
String get unnamedStation => 'Nenosaukta stacija';
@override
String stationNumber(int number) => 'Stacija $number';
// ==================== TASKS ====================
@override
String get tasks => 'Uzdevumi';
@@ -161,7 +214,8 @@ class AppLocalizationsLv extends AppLocalizations {
@override
String get confirmationRequired => 'Nepieciešams apstiprinājums';
@override
String get confirmationDescription => 'Noklikšķiniet uz pogas, lai pabeigtu uzdevumu.';
String get confirmationDescription =>
'Noklikšķiniet uz pogas, lai pabeigtu uzdevumu.';
@override
String get checklist => 'Pārbaudes saraksts';
@override
@@ -169,7 +223,8 @@ class AppLocalizationsLv extends AppLocalizations {
@override
String get completeTask => 'Pabeigt uzdevumu';
@override
String get completeTaskConfirm => 'Vai vēlaties atzīmēt šo uzdevumu kā pabeigtu?';
String get completeTaskConfirm =>
'Vai vēlaties atzīmēt šo uzdevumu kā pabeigtu?';
@override
String get completeTaskNote => 'Piezīme (neobligāta)';
@override
@@ -195,7 +250,8 @@ class AppLocalizationsLv extends AppLocalizations {
@override
String get signatureError => 'Kļūda saglabājot parakstu';
@override
String get signatureInstruction => 'Lūdzu parakstieties zemāk esošajā laukā (pele vai pirksts).';
String get signatureInstruction =>
'Lūdzu parakstieties zemāk esošajā laukā (pele vai pirksts).';
@override
String get photoCapture => 'Uzņemt fotogrāfijas';
@override
@@ -257,7 +313,8 @@ class AppLocalizationsLv extends AppLocalizations {
@override
String get cameraNotAvailable => 'Kamera nav pieejama';
@override
String get cameraNotSupportedMessage => 'Šajā platformā kamera netiek atbalstīta.';
String get cameraNotSupportedMessage =>
'Šajā platformā kamera netiek atbalstīta.';
@override
String get cameraNotSupportedOnPlatform => 'Šajā platformā netiek atbalstīts';
@override
@@ -269,11 +326,13 @@ class AppLocalizationsLv extends AppLocalizations {
@override
String get cameraInitializing => 'Kamera tiek inicializēta...';
@override
String get cameraLoadingMessage => 'Lūdzu, uzgaidiet, kamēr kamera tiek ielādēta';
String get cameraLoadingMessage =>
'Lūdzu, uzgaidiet, kamēr kamera tiek ielādēta';
@override
String get addPhotos => 'Pievienot fotogrāfijas';
@override
String get addPhotosInstruction => 'Izmantojiet pogu "Izvēlēties fotogrāfiju", lai pievienotu attēlus no kameras vai cietā diska.';
String get addPhotosInstruction =>
'Izmantojiet pogu "Izvēlēties fotogrāfiju", lai pievienotu attēlus no kameras vai cietā diska.';
@override
String get photoOf => 'no';
@@ -285,7 +344,8 @@ class AppLocalizationsLv extends AppLocalizations {
@override
String get noSender => 'Sūtītājs nav pieejams';
@override
String get noSenderMessage => 'Sūtītājs nav pieejams. Lūdzu, piesakieties vēlreiz.';
String get noSenderMessage =>
'Sūtītājs nav pieejams. Lūdzu, piesakieties vēlreiz.';
@override
String get noRecipient => 'Saņēmējs nav konfigurēts';
@override
@@ -306,6 +366,15 @@ class AppLocalizationsLv extends AppLocalizations {
String get jobNumber => 'Darba numurs';
@override
String get messages => 'Ziņojumi';
@override
String get generalMessages => 'Vispārīgi ziņojumi';
@override
String get noMessagesYet => 'Ziņojumu vēl nav';
@override
String get noChatsAvailable => 'Nav pieejamu tērzēšanu';
@override
String get selectPhoto => 'Izvēlēties fotogrāfiju';
@override
@@ -369,12 +438,18 @@ class AppLocalizationsLv extends AppLocalizations {
@override
String get statusCreated => 'Izveidots';
@override
String get statusPending => 'Gaida';
@override
String get statusAssigned => 'Piešķirts';
@override
String get statusInProgress => 'Procesā';
@override
String get statusCompleted => 'Pabeigts';
@override
String get statusCancelled => 'Atcelts';
@override
String get statusFailed => 'Neizdevās';
@override
String get priorityLow => 'Zema';
@override
String get priorityMedium => 'Vidēja';

View File

@@ -35,6 +35,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get unknown => 'Nieznany';
@override
String get yesterday => 'Wczoraj';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Zadania';
@@ -64,8 +67,29 @@ class AppLocalizationsPl extends AppLocalizations {
String get loginSubtitle => 'Zaloguj się do swojego konta';
@override
String get email => 'E-mail';
@override
String get emailAddress => 'Adres e-mail';
@override
String get emailAddressHint => 'Wpisz adres e-mail';
@override
String get emailAddressRequired => 'Proszę wpisać adres e-mail';
@override
String get emailAddressInvalid => 'Proszę wpisać prawidłowy adres e-mail';
@override
String get password => 'Hasło';
@override
String get passwordHint => 'Wpisz hasło';
@override
String get passwordRequired => 'Proszę wpisać hasło';
@override
String get passwordMinLength => 'Hasło musi mieć co najmniej 6 znaków';
@override
String get login => 'Zaloguj';
@override
@@ -73,7 +97,8 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get forgotPassword => 'Zapomniałeś hasła?';
@override
String get forgotPasswordMessage => 'Funkcja zapomnianego hasła jeszcze nie zaimplementowana';
String get forgotPasswordMessage =>
'Funkcja zapomnianego hasła jeszcze nie zaimplementowana';
@override
String get loginSuccess => 'Pomyślnie wylogowano';
@override
@@ -93,7 +118,8 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get noJobsAssigned => 'Brak przypisanych zadań';
@override
String get noJobsMessage => 'Twoje przypisane zadania będą wyświetlane tutaj.';
String get noJobsMessage =>
'Twoje przypisane zadania będą wyświetlane tutaj.';
@override
String get pullToRefresh => 'Przeciągnij w dół, aby odświeżyć';
@override
@@ -149,6 +175,34 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get newJobReceived => 'Otrzymano nowe zadanie';
@override
String get jobDetails => 'Szczegóły zlecenia';
@override
String get jobTasks => 'Zadania zlecenia';
@override
String get deliveryStations => 'Stacje dostawy';
@override
String deliveryStationsCount(int count) => 'Stacje dostawy ($count)';
@override
String get noDeliveryStations => 'Brak stacji dostawy';
@override
String get noDeliveryStationsMessage =>
'To zlecenie nie zawiera obecnie żadnych stacji dostawy.';
@override
String get phone => 'Telefon';
@override
String get unnamedStation => 'Nienazwana stacja';
@override
String stationNumber(int number) => 'Stacja $number';
// ==================== TASKS ====================
@override
String get tasks => 'Zadania';
@@ -161,7 +215,8 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get confirmationRequired => 'Wymagane potwierdzenie';
@override
String get confirmationDescription => 'Kliknij przycisk, aby ukończyć zadanie.';
String get confirmationDescription =>
'Kliknij przycisk, aby ukończyć zadanie.';
@override
String get checklist => 'Lista kontrolna';
@override
@@ -169,7 +224,8 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get completeTask => 'Ukończ zadanie';
@override
String get completeTaskConfirm => 'Czy chcesz oznaczyć to zadanie jako ukończone?';
String get completeTaskConfirm =>
'Czy chcesz oznaczyć to zadanie jako ukończone?';
@override
String get completeTaskNote => 'Notatka (opcjonalnie)';
@override
@@ -195,7 +251,8 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get signatureError => 'Błąd podczas zapisywania podpisu';
@override
String get signatureInstruction => 'Proszę podpisać się w polu poniżej (mysz lub palec).';
String get signatureInstruction =>
'Proszę podpisać się w polu poniżej (mysz lub palec).';
@override
String get photoCapture => 'Zrób zdjęcia';
@override
@@ -247,7 +304,8 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String barcodeNumberRequired(int number) => 'Kod kreskowy $number (wymagany)';
@override
String barcodeNumberOptional(int number) => 'Kod kreskowy $number (opcjonalny)';
String barcodeNumberOptional(int number) =>
'Kod kreskowy $number (opcjonalny)';
@override
String get barcodeError => 'Błąd podczas skanowania kodu kreskowego';
@override
@@ -257,7 +315,8 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get cameraNotAvailable => 'Kamera niedostępna';
@override
String get cameraNotSupportedMessage => 'Kamera nie jest obsługiwana na tej platformie.';
String get cameraNotSupportedMessage =>
'Kamera nie jest obsługiwana na tej platformie.';
@override
String get cameraNotSupportedOnPlatform => 'Nieobsługiwane na tej platformie';
@override
@@ -273,7 +332,8 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get addPhotos => 'Dodaj zdjęcia';
@override
String get addPhotosInstruction => 'Użyj przycisku "Wybierz zdjęcie", aby dodać obrazy z kamery lub dysku twardego.';
String get addPhotosInstruction =>
'Użyj przycisku "Wybierz zdjęcie", aby dodać obrazy z kamery lub dysku twardego.';
@override
String get photoOf => 'z';
@@ -285,11 +345,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get noSender => 'Brak dostępnego nadawcy';
@override
String get noSenderMessage => 'Brak dostępnego nadawcy. Proszę zalogować się ponownie.';
String get noSenderMessage =>
'Brak dostępnego nadawcy. Proszę zalogować się ponownie.';
@override
String get noRecipient => 'Brak skonfigurowanego odbiorcy';
@override
String get noRecipientMessage => 'Brak skonfigurowanego odbiorcy dla tego czatu.';
String get noRecipientMessage =>
'Brak skonfigurowanego odbiorcy dla tego czatu.';
@override
String get messageSendError => 'Wiadomość nie mogła zostać wysłana.';
@override
@@ -306,6 +368,15 @@ class AppLocalizationsPl extends AppLocalizations {
String get jobNumber => 'Numer zadania';
@override
String get messages => 'Wiadomości';
@override
String get generalMessages => 'Wiadomości ogólne';
@override
String get noMessagesYet => 'Brak wiadomości';
@override
String get noChatsAvailable => 'Brak dostępnych czatów';
@override
String get selectPhoto => 'Wybierz zdjęcie';
@override
@@ -327,7 +398,8 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get noCargoItems => 'Brak pozycji ładunku';
@override
String get noCargoItemsMessage => 'Brak pozycji ładunku zdefiniowanych dla tego zadania.';
String get noCargoItemsMessage =>
'Brak pozycji ładunku zdefiniowanych dla tego zadania.';
@override
String get article => 'Pozycja';
@@ -369,12 +441,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get statusCreated => 'Utworzono';
@override
String get statusPending => 'Oczekujące';
@override
String get statusAssigned => 'Przypisano';
@override
String get statusInProgress => 'W trakcie';
@override
String get statusCompleted => 'Ukończono';
@override
String get statusCancelled => 'Anulowano';
@override
String get statusFailed => 'Nieudane';
@override
String get priorityLow => 'Niski';
@override
String get priorityMedium => 'Średni';

View File

@@ -35,6 +35,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get unknown => 'Неизвестно';
@override
String get yesterday => 'Вчера';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Задания';
@@ -64,8 +67,30 @@ class AppLocalizationsRu extends AppLocalizations {
String get loginSubtitle => 'Войдите в свою учетную запись';
@override
String get email => 'Эл. почта';
@override
String get emailAddress => 'Адрес эл. почты';
@override
String get emailAddressHint => 'Введите адрес эл. почты';
@override
String get emailAddressRequired => 'Пожалуйста, введите адрес эл. почты';
@override
String get emailAddressInvalid =>
'Пожалуйста, введите корректный адрес эл. почты';
@override
String get password => 'Пароль';
@override
String get passwordHint => 'Введите пароль';
@override
String get passwordRequired => 'Пожалуйста, введите пароль';
@override
String get passwordMinLength => 'Пароль должен содержать не менее 6 символов';
@override
String get login => 'Войти';
@override
@@ -73,7 +98,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get forgotPassword => 'Забыли пароль?';
@override
String get forgotPasswordMessage => 'Функция восстановления пароля еще не реализована';
String get forgotPasswordMessage =>
'Функция восстановления пароля еще не реализована';
@override
String get loginSuccess => 'Успешный выход из системы';
@override
@@ -93,7 +119,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get noJobsAssigned => 'Нет назначенных заданий';
@override
String get noJobsMessage => 'Ваши назначенные задания будут отображаться здесь.';
String get noJobsMessage =>
'Ваши назначенные задания будут отображаться здесь.';
@override
String get pullToRefresh => 'Потяните вниз, чтобы обновить';
@override
@@ -137,7 +164,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get jobsUpdated => 'Задания обновлены';
@override
String get connectionRestored => 'Соединение восстановлено. Загрузка заданий...';
String get connectionRestored =>
'Соединение восстановлено. Загрузка заданий...';
@override
String get connectionLost => 'Соединение потеряно. Офлайн.';
@override
@@ -149,6 +177,34 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get newJobReceived => 'Получено новое задание';
@override
String get jobDetails => 'Детали заказа';
@override
String get jobTasks => 'Задачи заказа';
@override
String get deliveryStations => 'Точки доставки';
@override
String deliveryStationsCount(int count) => 'Точки доставки ($count)';
@override
String get noDeliveryStations => 'Нет точек доставки';
@override
String get noDeliveryStationsMessage =>
'Для этого заказа сейчас нет точек доставки.';
@override
String get phone => 'Телефон';
@override
String get unnamedStation => 'Станция без названия';
@override
String stationNumber(int number) => 'Станция $number';
// ==================== TASKS ====================
@override
String get tasks => 'Задачи';
@@ -161,7 +217,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get confirmationRequired => 'Требуется подтверждение';
@override
String get confirmationDescription => 'Нажмите кнопку, чтобы выполнить задачу.';
String get confirmationDescription =>
'Нажмите кнопку, чтобы выполнить задачу.';
@override
String get checklist => 'Контрольный список';
@override
@@ -169,7 +226,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get completeTask => 'Завершить задачу';
@override
String get completeTaskConfirm => 'Хотите отметить эту задачу как выполненную?';
String get completeTaskConfirm =>
'Хотите отметить эту задачу как выполненную?';
@override
String get completeTaskNote => 'Примечание (необязательно)';
@override
@@ -195,7 +253,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get signatureError => 'Ошибка при сохранении подписи';
@override
String get signatureInstruction => 'Пожалуйста, подпишитесь в поле ниже (мышь или палец).';
String get signatureInstruction =>
'Пожалуйста, подпишитесь в поле ниже (мышь или палец).';
@override
String get photoCapture => 'Сделать фото';
@override
@@ -247,7 +306,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String barcodeNumberRequired(int number) => 'Штрих-код $number (обязательно)';
@override
String barcodeNumberOptional(int number) => 'Штрих-код $number (необязательно)';
String barcodeNumberOptional(int number) =>
'Штрих-код $number (необязательно)';
@override
String get barcodeError => 'Ошибка при сканировании штрих-кода';
@override
@@ -257,9 +317,11 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get cameraNotAvailable => 'Камера недоступна';
@override
String get cameraNotSupportedMessage => 'Камера не поддерживается на этой платформе.';
String get cameraNotSupportedMessage =>
'Камера не поддерживается на этой платформе.';
@override
String get cameraNotSupportedOnPlatform => 'Не поддерживается на этой платформе';
String get cameraNotSupportedOnPlatform =>
'Не поддерживается на этой платформе';
@override
String get maxPhotosReached => 'Максимум достигнут';
@override
@@ -269,11 +331,13 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get cameraInitializing => 'Инициализация камеры...';
@override
String get cameraLoadingMessage => 'Пожалуйста, подождите, пока загружается камера';
String get cameraLoadingMessage =>
'Пожалуйста, подождите, пока загружается камера';
@override
String get addPhotos => 'Добавить фото';
@override
String get addPhotosInstruction => 'Используйте кнопку "Выбрать фото", чтобы добавить изображения с камеры или жёсткого диска.';
String get addPhotosInstruction =>
'Используйте кнопку "Выбрать фото", чтобы добавить изображения с камеры или жёсткого диска.';
@override
String get photoOf => 'из';
@@ -285,7 +349,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get noSender => 'Отправитель недоступен';
@override
String get noSenderMessage => 'Отправитель недоступен. Пожалуйста, войдите снова.';
String get noSenderMessage =>
'Отправитель недоступен. Пожалуйста, войдите снова.';
@override
String get noRecipient => 'Получатель не настроен';
@override
@@ -306,6 +371,15 @@ class AppLocalizationsRu extends AppLocalizations {
String get jobNumber => 'Номер задания';
@override
String get messages => 'Сообщения';
@override
String get generalMessages => 'Общие сообщения';
@override
String get noMessagesYet => 'Сообщений пока нет';
@override
String get noChatsAvailable => 'Нет доступных чатов';
@override
String get selectPhoto => 'Выбрать фото';
@override
@@ -327,7 +401,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get noCargoItems => 'Нет позиций груза';
@override
String get noCargoItemsMessage => 'Для этого задания не определены позиции груза.';
String get noCargoItemsMessage =>
'Для этого задания не определены позиции груза.';
@override
String get article => 'Позиция';
@@ -369,12 +444,18 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get statusCreated => 'Создано';
@override
String get statusPending => 'В ожидании';
@override
String get statusAssigned => 'Назначено';
@override
String get statusInProgress => 'В процессе';
@override
String get statusCompleted => 'Завершено';
@override
String get statusCancelled => 'Отменено';
@override
String get statusFailed => 'Не удалось';
@override
String get priorityLow => 'Низкий';
@override
String get priorityMedium => 'Средний';

View File

@@ -35,6 +35,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get unknown => 'Bilinmiyor';
@override
String get yesterday => 'Dün';
// ==================== NAVIGATION ====================
@override
String get jobs => 'İşler';
@@ -64,8 +67,29 @@ class AppLocalizationsTr extends AppLocalizations {
String get loginSubtitle => 'Hesabınıza giriş yapın';
@override
String get email => 'E-posta';
@override
String get emailAddress => 'E-posta adresi';
@override
String get emailAddressHint => 'E-posta adresinizi girin';
@override
String get emailAddressRequired => 'Lütfen e-posta adresinizi girin';
@override
String get emailAddressInvalid => 'Lütfen geçerli bir e-posta adresi girin';
@override
String get password => 'Şifre';
@override
String get passwordHint => 'Şifrenizi girin';
@override
String get passwordRequired => 'Lütfen şifrenizi girin';
@override
String get passwordMinLength => 'Şifre en az 6 karakter olmalıdır';
@override
String get login => 'Giriş';
@override
@@ -73,7 +97,8 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get forgotPassword => 'Şifrenizi mi unuttunuz?';
@override
String get forgotPasswordMessage => 'Şifremi unuttum özelliği henüz uygulanmadı';
String get forgotPasswordMessage =>
'Şifremi unuttum özelliği henüz uygulanmadı';
@override
String get loginSuccess => 'Başarıyla çıkış yapıldı';
@override
@@ -137,7 +162,8 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get jobsUpdated => 'İşler güncellendi';
@override
String get connectionRestored => 'Bağlantı geri yüklendi. İşler yükleniyor...';
String get connectionRestored =>
'Bağlantı geri yüklendi. İşler yükleniyor...';
@override
String get connectionLost => 'Bağlantı kesildi. Çevrimdışı.';
@override
@@ -149,6 +175,34 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get newJobReceived => 'Yeni iş alındı';
@override
String get jobDetails => 'İş detayları';
@override
String get jobTasks => 'İş görevleri';
@override
String get deliveryStations => 'Teslimat durakları';
@override
String deliveryStationsCount(int count) => 'Teslimat durakları ($count)';
@override
String get noDeliveryStations => 'Teslimat durağı yok';
@override
String get noDeliveryStationsMessage =>
'Bu iş şu anda hiçbir teslimat durağı içermiyor.';
@override
String get phone => 'Telefon';
@override
String get unnamedStation => 'Adsız durak';
@override
String stationNumber(int number) => 'Durak $number';
// ==================== TASKS ====================
@override
String get tasks => 'Görevler';
@@ -161,7 +215,8 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get confirmationRequired => 'Onay gerekli';
@override
String get confirmationDescription => 'Görevi tamamlamak için butona tıklayın.';
String get confirmationDescription =>
'Görevi tamamlamak için butona tıklayın.';
@override
String get checklist => 'Kontrol listesi';
@override
@@ -169,7 +224,8 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get completeTask => 'Görevi tamamla';
@override
String get completeTaskConfirm => 'Bu görevi tamamlandı olarak işaretlemek istiyor musunuz?';
String get completeTaskConfirm =>
'Bu görevi tamamlandı olarak işaretlemek istiyor musunuz?';
@override
String get completeTaskNote => 'Not (isteğe bağlı)';
@override
@@ -195,7 +251,8 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get signatureError => 'İmza kaydedilirken hata oluştu';
@override
String get signatureInstruction => 'Lütfen aşağıdaki alana imzanızı atın (fare veya parmak).';
String get signatureInstruction =>
'Lütfen aşağıdaki alana imzanızı atın (fare veya parmak).';
@override
String get photoCapture => 'Fotoğraf çek';
@override
@@ -221,7 +278,8 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get deletePhoto => 'Fotoğrafı sil';
@override
String get deletePhotoConfirm => 'Bu fotoğrafı gerçekten silmek istiyor musunuz?';
String get deletePhotoConfirm =>
'Bu fotoğrafı gerçekten silmek istiyor musunuz?';
@override
String get barcode => 'Barkod';
@override
@@ -257,7 +315,8 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get cameraNotAvailable => 'Kamera kullanılamıyor';
@override
String get cameraNotSupportedMessage => 'Bu platformda kamera desteklenmiyor.';
String get cameraNotSupportedMessage =>
'Bu platformda kamera desteklenmiyor.';
@override
String get cameraNotSupportedOnPlatform => 'Bu platformda desteklenmiyor';
@override
@@ -273,7 +332,8 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get addPhotos => 'Fotoğraf ekle';
@override
String get addPhotosInstruction => 'Kamera veya sabit diskten görüntü eklemek için "Fotoğraf seç" düğmesini kullanın.';
String get addPhotosInstruction =>
'Kamera veya sabit diskten görüntü eklemek için "Fotoğraf seç" düğmesini kullanın.';
@override
String get photoOf => '/';
@@ -285,7 +345,8 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get noSender => 'Gönderen mevcut değil';
@override
String get noSenderMessage => 'Gönderen mevcut değil. Lütfen tekrar giriş yapın.';
String get noSenderMessage =>
'Gönderen mevcut değil. Lütfen tekrar giriş yapın.';
@override
String get noRecipient => 'Alıcı yapılandırılmamış';
@override
@@ -306,6 +367,15 @@ class AppLocalizationsTr extends AppLocalizations {
String get jobNumber => 'İş numarası';
@override
String get messages => 'Mesajlar';
@override
String get generalMessages => 'Genel mesajlar';
@override
String get noMessagesYet => 'Henüz mesaj yok';
@override
String get noChatsAvailable => 'Kullanılabilir sohbet yok';
@override
String get selectPhoto => 'Fotoğraf seç';
@override
@@ -369,12 +439,18 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get statusCreated => 'Oluşturuldu';
@override
String get statusPending => 'Beklemede';
@override
String get statusAssigned => 'Atandı';
@override
String get statusInProgress => 'Devam ediyor';
@override
String get statusCompleted => 'Tamamlandı';
@override
String get statusCancelled => 'İptal edildi';
@override
String get statusFailed => 'Başarısız';
@override
String get priorityLow => 'Düşük';
@override
String get priorityMedium => 'Orta';

View File

@@ -0,0 +1,50 @@
import 'package:flutter/widgets.dart';
import '../models/chat.dart';
import 'app_localizations.dart';
String localizeKnownText(BuildContext context, String text) {
final l10n = AppLocalizations.of(context);
switch (text.trim()) {
case 'Auftragsdetails':
return l10n.jobDetails;
case 'Aufgaben eines Auftrags':
return l10n.jobTasks;
case 'Unterschrift':
return l10n.signature;
case 'Allgemeine Nachrichten':
return l10n.generalMessages;
case 'Telefon':
return l10n.phone;
case 'Erstellt':
return l10n.created;
case 'E-Mail-Adresse':
return l10n.emailAddress;
case 'Passwort':
return l10n.password;
case 'Anmelden':
return l10n.login;
default:
return text;
}
}
String localizedStationLabel(
BuildContext context,
int number, {
String? suffix,
}) {
final base = AppLocalizations.of(context).stationNumber(number);
final trimmedSuffix = suffix?.trim() ?? '';
if (trimmedSuffix.isEmpty) {
return base;
}
return '$base: ${localizeKnownText(context, trimmedSuffix)}';
}
String localizedChatTitle(BuildContext context, Chat chat) {
if (chat.type == ChatType.general) {
return AppLocalizations.of(context).generalMessages;
}
return localizeKnownText(context, chat.title);
}

View File

@@ -34,6 +34,8 @@ class _LoginViewState extends State<LoginView> {
bool _logoutNoticeShown = false;
bool _hasNavigatedToJobs = false;
String _appVersion = '';
String? _pendingLoginEmail;
String? _pendingLoginPassword;
@override
void initState() {
@@ -52,7 +54,13 @@ class _LoginViewState extends State<LoginView> {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_logoutNoticeShown = true;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).loginSuccess), backgroundColor: Colors.green, duration: const Duration(seconds: 1)));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).loginSuccess),
backgroundColor: Colors.green,
duration: const Duration(seconds: 1),
),
);
});
}
}
@@ -86,70 +94,144 @@ class _LoginViewState extends State<LoginView> {
// Listen to connection status changes via dart_mq
// Note: Don't reset _isLoggingIn here - the login flow in _handleLogin
// manages button state through its own error/success handling.
_connectionStatusSubscription = DartMQ().subscribe<bool>(MQTopics.connectionStatus, (isConnected) {
if (mounted) {
setState(() {});
}
});
_connectionStatusSubscription = DartMQ().subscribe<bool>(
MQTopics.connectionStatus,
(isConnected) {
if (mounted) {
setState(() {});
}
},
);
// Listen to authentication responses via dart_mq
_authResponseSubscription = DartMQ().subscribe<Map<String, dynamic>>(MQTopics.authResponse, (response) {
final responseTime = DateTime.now();
developer.log('=== AUTHENTICATION RESPONSE RECEIVED ===', name: 'LoginView');
developer.log('Timestamp: ${responseTime.toIso8601String()}', name: 'LoginView');
developer.log('Response data: $response', name: 'LoginView');
_authResponseSubscription = DartMQ().subscribe<Map<String, dynamic>>(
MQTopics.authResponse,
(response) {
final responseTime = DateTime.now();
developer.log(
'=== AUTHENTICATION RESPONSE RECEIVED ===',
name: 'LoginView',
);
developer.log(
'Timestamp: ${responseTime.toIso8601String()}',
name: 'LoginView',
);
developer.log('Response data: $response', name: 'LoginView');
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() {
_isLoggingIn = false;
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleAuthResponse(response);
});
} else {
developer.log(
'Widget not mounted - skipping UI updates for auth response',
name: 'LoginView',
);
}
if (response['success'] == true) {
// Prevent duplicate navigation from multiple auth responses
if (_hasNavigatedToJobs) {
developer.log('Already navigated to jobs view - ignoring duplicate auth response', name: 'LoginView');
return;
}
_hasNavigatedToJobs = true;
developer.log(
'Authentication response processing completed',
name: 'LoginView',
);
},
);
}
final message = response['message'] ?? 'Anmeldung erfolgreich';
final email = _emailController.text.trim();
final password = _passwordController.text;
void _clearPendingLoginCredentials() {
_pendingLoginEmail = null;
_pendingLoginPassword = null;
}
developer.log('=== LOGIN SUCCESS ===', name: 'LoginView');
developer.log('Email: $email', name: 'LoginView');
developer.log('Message: $message', name: 'LoginView');
Future<void> _handleAuthResponse(Map<String, dynamic> response) async {
if (!mounted) return;
// Store email as login identifier
_appState.setLoggedInEmail(email);
final pendingEmail = _pendingLoginEmail?.trim();
final pendingPassword = _pendingLoginPassword;
final hadPendingLogin =
pendingEmail != null &&
pendingEmail.isNotEmpty &&
pendingPassword != null &&
pendingPassword.isNotEmpty;
_clearPendingLoginCredentials();
// Save credentials for auto-login on app restart
DatabaseService().saveCredentials(email, password);
setState(() {
_isLoggingIn = false;
});
// Navigate directly to jobs view - jobs will be loaded there
developer.log('Navigating to jobs view - jobs will be loaded there...', name: 'LoginView');
Navigator.of(context).pushReplacementNamed('/jobs');
} else {
final errorMessage = response['message'] ?? 'Unbekannter Fehler';
final errorCode = response['code'] ?? 'No code';
developer.log('=== LOGIN FAILURE ===', name: 'LoginView');
developer.log('Error message: $errorMessage', name: 'LoginView');
developer.log('Error code: $errorCode', name: 'LoginView');
developer.log('Full error response: $response', name: 'LoginView');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${AppLocalizations.of(context).loginFailed}: $errorMessage'), backgroundColor: Colors.red, duration: const Duration(seconds: 1)));
}
});
} else {
developer.log('Widget not mounted - skipping UI updates for auth response', name: 'LoginView');
if (response['success'] == true) {
// Prevent duplicate navigation from multiple auth responses
if (_hasNavigatedToJobs) {
developer.log(
'Already navigated to jobs view - ignoring duplicate auth response',
name: 'LoginView',
);
return;
}
developer.log('Authentication response processing completed', name: 'LoginView');
});
final message = response['message'] ?? 'Anmeldung erfolgreich';
final savedCredentials = await DatabaseService().loadCredentials();
final effectiveEmail =
(pendingEmail != null && pendingEmail.isNotEmpty)
? pendingEmail
: (savedCredentials?.email ??
_appState.loggedInEmail ??
_emailController.text.trim());
final effectivePassword =
(pendingPassword != null && pendingPassword.isNotEmpty)
? pendingPassword
: (savedCredentials?.password ?? _passwordController.text);
developer.log('=== LOGIN SUCCESS ===', name: 'LoginView');
developer.log('Email: $effectiveEmail', name: 'LoginView');
developer.log('Message: $message', name: 'LoginView');
if (effectiveEmail.isNotEmpty) {
_appState.setLoggedInEmail(effectiveEmail);
}
if (effectiveEmail.isNotEmpty && effectivePassword.isNotEmpty) {
await DatabaseService().saveCredentials(
effectiveEmail,
effectivePassword,
);
}
if (!mounted) return;
_hasNavigatedToJobs = true;
// Navigate directly to jobs view - jobs will be loaded there
developer.log(
'Navigating to jobs view - jobs will be loaded there...',
name: 'LoginView',
);
Navigator.of(context).pushReplacementNamed('/jobs');
return;
}
final errorMessage = response['message'] ?? 'Unbekannter Fehler';
final errorCode = response['code'] ?? 'No code';
developer.log('=== LOGIN FAILURE ===', name: 'LoginView');
developer.log('Error message: $errorMessage', name: 'LoginView');
developer.log('Error code: $errorCode', name: 'LoginView');
developer.log('Full error response: $response', name: 'LoginView');
if (!hadPendingLogin || !mounted) {
developer.log(
'Ignoring auth failure in LoginView because no manual login attempt is pending',
name: 'LoginView',
);
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${AppLocalizations.of(context).loginFailed}: $errorMessage',
),
backgroundColor: Colors.red,
duration: const Duration(seconds: 1),
),
);
}
Future<void> _handleLogin() async {
@@ -158,34 +240,62 @@ class _LoginViewState extends State<LoginView> {
developer.log('=== LOGIN ATTEMPT STARTED ===', name: 'LoginView');
developer.log('Session ID: $sessionId', name: 'LoginView');
developer.log('Timestamp: ${loginStartTime.toIso8601String()}', name: 'LoginView');
developer.log(
'Timestamp: ${loginStartTime.toIso8601String()}',
name: 'LoginView',
);
if (!_formKey.currentState!.validate()) {
developer.log('Login validation failed - form is invalid', name: 'LoginView');
developer.log(
'Login validation failed - form is invalid',
name: 'LoginView',
);
return;
}
if (_isLoggingIn) {
developer.log('Login already in progress - ignoring duplicate request', name: 'LoginView');
developer.log(
'Login already in progress - ignoring duplicate request',
name: 'LoginView',
);
return;
}
String email = _emailController.text.trim();
String password = _passwordController.text;
_pendingLoginEmail = email;
_pendingLoginPassword = password;
developer.log('Login attempt for email: $email', name: 'LoginView');
developer.log('Password length: ${_passwordController.text.length} characters', name: 'LoginView');
developer.log(
'Password length: ${_passwordController.text.length} characters',
name: 'LoginView',
);
// Capture ScaffoldMessenger and localizations before any async operations
final scaffoldMessenger = ScaffoldMessenger.of(context);
final localizations = AppLocalizations.of(context);
if (!_stompService.isConnected) {
developer.log('Not connected to STOMP server - establishing connection first', name: 'LoginView');
developer.log('STOMP service connection state: ${_stompService.isConnected}', name: 'LoginView');
developer.log(
'Not connected to STOMP server - establishing connection first',
name: 'LoginView',
);
developer.log(
'STOMP service connection state: ${_stompService.isConnected}',
name: 'LoginView',
);
// Always attempt connection to fixed STOMP endpoint (no discovery gating)
// Show connecting message
if (!widget.suppressConnectionSnack) {
scaffoldMessenger.showSnackBar(SnackBar(content: Text(localizations.connecting), backgroundColor: Colors.blue, duration: const Duration(seconds: 1)));
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text(localizations.connecting),
backgroundColor: Colors.blue,
duration: const Duration(seconds: 1),
),
);
}
// Set loading state
@@ -202,20 +312,29 @@ class _LoginViewState extends State<LoginView> {
// Wait for connection to be established with a timeout
try {
final completer = Completer<bool>();
final subscription = DartMQ().subscribe<bool>(MQTopics.connectionStatus, (isConnected) {
if (isConnected && !completer.isCompleted) {
completer.complete(true);
}
});
final subscription = DartMQ().subscribe<bool>(
MQTopics.connectionStatus,
(isConnected) {
if (isConnected && !completer.isCompleted) {
completer.complete(true);
}
},
);
await completer.future.timeout(const Duration(seconds: 12));
subscription.cancel();
developer.log('STOMP connection established - proceeding with login', name: 'LoginView');
developer.log(
'STOMP connection established - proceeding with login',
name: 'LoginView',
);
} on TimeoutException {
developer.log('STOMP connection timed out', name: 'LoginView');
}
} else {
developer.log('STOMP already connected after connect - proceeding with login', name: 'LoginView');
developer.log(
'STOMP already connected after connect - proceeding with login',
name: 'LoginView',
);
}
// Check if connection was successful
@@ -223,43 +342,74 @@ class _LoginViewState extends State<LoginView> {
setState(() {
_isLoggingIn = false;
});
scaffoldMessenger.showSnackBar(SnackBar(content: Text(localizations.connectionTimeout), backgroundColor: Colors.red, duration: const Duration(seconds: 2)));
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text(localizations.connectionTimeout),
backgroundColor: Colors.red,
duration: const Duration(seconds: 2),
),
);
_clearPendingLoginCredentials();
return;
}
} catch (e, stackTrace) {
setState(() {
_isLoggingIn = false;
});
developer.log('Error connecting to STOMP server: $e', name: 'LoginView');
developer.log(
'Error connecting to STOMP server: $e',
name: 'LoginView',
);
developer.log('Stack trace: $stackTrace', name: 'LoginView');
scaffoldMessenger.showSnackBar(SnackBar(content: Text('${localizations.connectionError}: $e'), backgroundColor: Colors.red, duration: const Duration(seconds: 1)));
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text('${localizations.connectionError}: $e'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 1),
),
);
_clearPendingLoginCredentials();
return;
}
}
developer.log('Pre-login checks passed - initiating login request', name: 'LoginView');
developer.log('Connection status: connected=${_stompService.isConnected}', name: 'LoginView');
developer.log(
'Pre-login checks passed - initiating login request',
name: 'LoginView',
);
developer.log(
'Connection status: connected=${_stompService.isConnected}',
name: 'LoginView',
);
setState(() {
_isLoggingIn = true;
});
String password = _passwordController.text;
developer.log('Sending login request via STOMP service...', name: 'LoginView');
developer.log(
'Sending login request via STOMP service...',
name: 'LoginView',
);
try {
// Send login request via STOMP
await _stompService.login(email, password);
final requestSentTime = DateTime.now();
final requestDuration = requestSentTime.difference(loginStartTime).inMilliseconds;
developer.log('Login request sent successfully after ${requestDuration}ms', name: 'LoginView');
final requestDuration =
requestSentTime.difference(loginStartTime).inMilliseconds;
developer.log(
'Login request sent successfully after ${requestDuration}ms',
name: 'LoginView',
);
} catch (e, stackTrace) {
final errorTime = DateTime.now();
final errorDuration = errorTime.difference(loginStartTime).inMilliseconds;
developer.log('LOGIN ERROR: Exception during login request after ${errorDuration}ms', name: 'LoginView');
developer.log(
'LOGIN ERROR: Exception during login request after ${errorDuration}ms',
name: 'LoginView',
);
developer.log('Error: $e', name: 'LoginView');
developer.log('Stack trace: $stackTrace', name: 'LoginView');
@@ -267,16 +417,28 @@ class _LoginViewState extends State<LoginView> {
_isLoggingIn = false;
});
scaffoldMessenger.showSnackBar(SnackBar(content: Text('${localizations.loginError}: $e'), backgroundColor: Colors.red, duration: const Duration(seconds: 1)));
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text('${localizations.loginError}: $e'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 1),
),
);
_clearPendingLoginCredentials();
}
// The auth response will be handled by the stream listener
// _isLoggingIn will be set to false in the listener
developer.log('Login request phase completed - waiting for auth response', name: 'LoginView');
developer.log(
'Login request phase completed - waiting for auth response',
name: 'LoginView',
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
backgroundColor: Colors.grey[50],
body: Column(
@@ -293,25 +455,54 @@ class _LoginViewState extends State<LoginView> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo oder App-Name
Icon(Icons.account_circle, size: 100, color: Colors.deepPurple),
Icon(
Icons.account_circle,
size: 100,
color: Colors.deepPurple,
),
const SizedBox(height: 32),
Text(AppLocalizations.of(context).welcomeBack, style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.grey[800]), textAlign: TextAlign.center),
Text(
l10n.welcomeBack,
style: Theme.of(
context,
).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(AppLocalizations.of(context).loginSubtitle, style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.grey[600]), textAlign: TextAlign.center),
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: 'E-Mail-Adresse', hintText: 'Geben Sie Ihre E-Mail-Adresse ein', prefixIcon: const Icon(Icons.email_outlined), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), filled: true, fillColor: Colors.white),
decoration: InputDecoration(
labelText: l10n.emailAddress,
hintText: l10n.emailAddressHint,
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: Colors.white,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte geben Sie Ihre E-Mail-Adresse ein';
return l10n.emailAddressRequired;
}
if (!RegExp(r'^[A-Za-z0-9_.+-]+@[A-Za-z0-9-]+\.[A-Za-z0-9.-]+$').hasMatch(value)) {
return 'Bitte geben Sie eine gültige E-Mail-Adresse ein';
if (!RegExp(
r'^[A-Za-z0-9_.+-]+@[A-Za-z0-9-]+\.[A-Za-z0-9.-]+$',
).hasMatch(value)) {
return l10n.emailAddressInvalid;
}
return null;
},
@@ -323,27 +514,33 @@ class _LoginViewState extends State<LoginView> {
controller: _passwordController,
obscureText: !_isPasswordVisible,
decoration: InputDecoration(
labelText: 'Passwort',
hintText: 'Geben Sie Ihr Passwort ein',
labelText: l10n.password,
hintText: l10n.passwordHint,
prefixIcon: const Icon(Icons.lock_outlined),
suffixIcon: IconButton(
icon: Icon(_isPasswordVisible ? Icons.visibility : Icons.visibility_off),
icon: Icon(
_isPasswordVisible
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: Colors.white,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte geben Sie Ihr Passwort ein';
return l10n.passwordRequired;
}
if (value.length < 6) {
return 'Das Passwort muss mindestens 6 Zeichen lang sein';
return l10n.passwordMinLength;
}
return null;
},
@@ -356,16 +553,71 @@ class _LoginViewState extends State<LoginView> {
child: TextButton(
onPressed: () {
// Hier würde die "Passwort vergessen" Funktionalität implementiert werden
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).forgotPasswordMessage), duration: const Duration(seconds: 1)));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.forgotPasswordMessage),
duration: const Duration(seconds: 1),
),
);
},
child: Text(AppLocalizations.of(context).forgotPassword, style: const TextStyle(color: Colors.deepPurple, fontWeight: FontWeight.w500)),
child: Text(
l10n.forgotPassword,
style: const TextStyle(
color: Colors.deepPurple,
fontWeight: FontWeight.w500,
),
),
),
),
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)), 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))), SizedBox(width: 12), Text('Verbinden…', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600))]) : const Text('Anmelden', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600))),
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),
),
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,
),
),
),
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,
),
),
),
const SizedBox(height: 24),
],
),
@@ -375,7 +627,15 @@ class _LoginViewState extends State<LoginView> {
),
),
// 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: TextStyle(fontSize: 12, color: Colors.grey[500]),
textAlign: TextAlign.center,
),
),
],
),
);

View File

@@ -13,6 +13,7 @@ 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 {
@@ -43,14 +44,59 @@ void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
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 appState = AppState();
final initialRoute = appState.isLoggedIn ? '/jobs' : '/login';
final initialRoute = _appState.isLoggedIn ? '/jobs' : '/login';
return ValueListenableBuilder<Locale>(
valueListenable: localeNotifier,
@@ -58,11 +104,20 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: 'VotianLT App',
debugShowCheckedModeBanner: false,
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true),
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
// Localization configuration
locale: locale,
localizationsDelegates: const [AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate],
supportedLocales: supportedLanguageCodes.map((code) => Locale(code)).toList(),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales:
supportedLanguageCodes.map((code) => Locale(code)).toList(),
navigatorObservers: [routeObserver],
initialRoute: initialRoute,
onGenerateRoute: (settings) {
@@ -70,21 +125,30 @@ class MyApp extends StatelessWidget {
case '/login':
final arg = settings.arguments;
final suppress = (arg is bool) ? arg : false;
return MaterialPageRoute(builder: (_) => LoginView(suppressConnectionSnack: suppress));
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));
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));
return MaterialPageRoute(
builder: (_) => ChatDetailsView(chat: chat),
);
case '/settings':
return MaterialPageRoute(builder: (_) => const SettingsView());
default:
return MaterialPageRoute(builder: (_) => const LoginView(suppressConnectionSnack: false));
return MaterialPageRoute(
builder:
(_) => const LoginView(suppressConnectionSnack: false),
);
}
},
);
@@ -114,9 +178,27 @@ class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: Theme.of(context).colorScheme.inversePrimary, 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.
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
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.
);
}
}

View File

@@ -15,6 +15,8 @@ class ChatService {
static const _jobIdPrefix = 'job:';
static const _jobNumberPrefix = 'job_number:';
static const _generalPrefix = 'general:';
static const _defaultGeneralConversationKey =
'general:allgemeine-nachrichten';
final DatabaseService _databaseService = DatabaseService();
final AppState _appState = AppState();
@@ -103,9 +105,11 @@ class ChatService {
_chats.removeWhere((chat) {
final matchesKey = conversationKeys.contains(chat.id);
final matchesId = trimmedJobId.isNotEmpty &&
final matchesId =
trimmedJobId.isNotEmpty &&
(chat.jobId?.trim().toLowerCase() == lowerJobId);
final matchesNumber = trimmedJobNumber.isNotEmpty &&
final matchesNumber =
trimmedJobNumber.isNotEmpty &&
(chat.jobNumber?.trim().toLowerCase() == lowerJobNumber);
return matchesKey || matchesId || matchesNumber;
});
@@ -129,18 +133,11 @@ class ChatService {
// Messages with GENERAL messageType should always go to the default general chat
if (message.messageType == ChatMessageType.general) {
final localId = _primaryLocalIdentifier();
if (localId != null && localId.isNotEmpty) {
final key = _conversationKeyForParticipants(
localId,
_appState.loggedInEmail!,
);
developer.log(
'[DEBUG_LOG] GENERAL message detected, routing to conversation key: $key (localId=$localId, receiver=${_appState.loggedInEmail})',
name: 'ChatService',
);
return key;
}
developer.log(
'[DEBUG_LOG] GENERAL message detected, routing to conversation key: $_defaultGeneralConversationKey',
name: 'ChatService',
);
return _defaultGeneralConversationKey;
}
// Job-related messages go to job-specific chats
@@ -165,30 +162,11 @@ class ChatService {
return key;
}
// Fallback: create conversation based on userId
final localId = _primaryLocalIdentifier();
if (localId != null && localId.isNotEmpty) {
final key = _conversationKeyForParticipants(
localId,
_appState.loggedInEmail!,
);
developer.log(
'[DEBUG_LOG] Using fallback routing, conversation key: $key',
name: 'ChatService',
);
return key;
}
developer.log(
'[DEBUG_LOG] No local identifier available for fallback routing',
'[DEBUG_LOG] No job context available, routing to default general chat',
name: 'ChatService',
);
return '$_generalPrefix${_appState.loggedInEmail!}';
}
String _conversationKeyForParticipants(String a, String b) {
final participants = <String>[a.toLowerCase(), b.toLowerCase()]..sort();
return '$_generalPrefix${participants.join('|')}';
return _defaultGeneralConversationKey;
}
Future<void> saveIncomingMessage(ChatMessage message) async {
@@ -205,6 +183,20 @@ class ChatService {
await _persistMessage(message);
}
Future<void> markOutgoingMessageSynced(String messageId) async {
if (!_initialized) {
await initialize();
}
final conversationKey = await _databaseService.updateChatMessagePendingSync(
messageId,
false,
);
if (conversationKey != null && conversationKey.isNotEmpty) {
await _refreshConversation(conversationKey);
}
}
Future<void> _persistMessage(ChatMessage message) async {
final conversationKey = conversationKeyForMessage(message);
@@ -239,7 +231,7 @@ class ChatService {
Future<void> _loadChatsFromDatabase() async {
await _databaseService.ensureInitialized();
final grouped = await _databaseService.loadAllChatMessagesGrouped();
final grouped = await _loadNormalizedChatGroups();
_chats.clear();
grouped.forEach((conversationKey, messages) {
final chat = _buildChat(conversationKey, messages);
@@ -254,6 +246,14 @@ class ChatService {
}
Future<void> _refreshConversation(String conversationKey) async {
if (_isLegacyGeneralConversationKey(conversationKey)) {
await _databaseService.migrateConversationKey(
conversationKey,
_defaultGeneralConversationKey,
);
conversationKey = _defaultGeneralConversationKey;
}
final messages = await _databaseService.loadChatMessages(
conversationKey: conversationKey,
);
@@ -317,15 +317,13 @@ class ChatService {
final counterpartNormalized =
counterpart != null &&
counterpart.toLowerCase() == _appState.loggedInEmail!.toLowerCase()
counterpart.toLowerCase() ==
_appState.loggedInEmail!.toLowerCase()
? _appState.loggedInEmail!
: counterpart;
final bool isDefaultGeneral =
!isJobChat &&
conversationKey.startsWith(_generalPrefix) &&
(counterpartNormalized?.toLowerCase() ==
_appState.loggedInEmail!.toLowerCase());
!isJobChat && conversationKey == _defaultGeneralConversationKey;
final title =
isJobChat
@@ -406,23 +404,46 @@ class ChatService {
}
}
Future<Map<String, List<ChatMessage>>> _loadNormalizedChatGroups() async {
var grouped = await _databaseService.loadAllChatMessagesGrouped();
final legacyGeneralKeys =
grouped.keys.where(_isLegacyGeneralConversationKey).toList();
if (legacyGeneralKeys.isEmpty) {
return grouped;
}
for (final key in legacyGeneralKeys) {
await _databaseService.migrateConversationKey(
key,
_defaultGeneralConversationKey,
);
}
grouped = await _databaseService.loadAllChatMessagesGrouped();
return grouped;
}
bool _isLegacyGeneralConversationKey(String conversationKey) {
return conversationKey != _defaultGeneralConversationKey &&
conversationKey.startsWith(_generalPrefix) &&
!conversationKey.startsWith(_jobIdPrefix) &&
!conversationKey.startsWith(_jobNumberPrefix);
}
void _ensureDefaultGeneralChat() {
final localId = _primaryLocalIdentifier();
if (localId == null || localId.isEmpty) {
final receiver = _appState.loggedInEmail;
if (receiver == null || receiver.isEmpty) {
developer.log(
'[DEBUG_LOG] _ensureDefaultGeneralChat: No local identifier available, skipping',
'[DEBUG_LOG] _ensureDefaultGeneralChat: No receiver available, skipping',
name: 'ChatService',
);
return;
}
final conversationKey = _conversationKeyForParticipants(
localId,
_appState.loggedInEmail!,
);
const conversationKey = _defaultGeneralConversationKey;
developer.log(
'[DEBUG_LOG] _ensureDefaultGeneralChat: Creating/ensuring default general chat with key: $conversationKey (localId=$localId, receiver=${_appState.loggedInEmail})',
'[DEBUG_LOG] _ensureDefaultGeneralChat: Creating/ensuring default general chat with key: $conversationKey (receiver=$receiver)',
name: 'ChatService',
);
@@ -431,8 +452,7 @@ class ChatService {
chat.id != conversationKey &&
chat.type == ChatType.general &&
chat.receiver != null &&
chat.receiver!.toLowerCase() ==
_appState.loggedInEmail!.toLowerCase() &&
chat.receiver!.toLowerCase() == receiver.toLowerCase() &&
chat.messages.isEmpty,
);
final index = _chats.indexWhere((chat) => chat.id == conversationKey);
@@ -446,7 +466,7 @@ class ChatService {
Chat(
id: conversationKey,
title: 'Allgemeine Nachrichten',
receiver: _appState.loggedInEmail!,
receiver: receiver,
type: ChatType.general,
jobId: null,
jobNumber: null,
@@ -463,8 +483,7 @@ class ChatService {
final existing = _chats[index];
if (existing.type != ChatType.general ||
existing.receiver == null ||
existing.receiver!.toLowerCase() !=
_appState.loggedInEmail!.toLowerCase() ||
existing.receiver!.toLowerCase() != receiver.toLowerCase() ||
(existing.messages.isEmpty &&
existing.title != 'Allgemeine Nachrichten')) {
developer.log(
@@ -477,7 +496,7 @@ class ChatService {
existing.messages.isEmpty
? 'Allgemeine Nachrichten'
: existing.title,
receiver: _appState.loggedInEmail!,
receiver: receiver,
type: ChatType.general,
jobId: existing.jobId,
jobNumber: existing.jobNumber,
@@ -493,8 +512,4 @@ class ChatService {
}
}
}
String? _primaryLocalIdentifier() {
return _appState.loggedInEmail;
}
}

View File

@@ -38,7 +38,10 @@ class DatabaseService {
final completer = Completer<void>();
_initializingCompleter = completer;
try {
developer.log('Initializing ObjectBox database...', name: 'DatabaseService');
developer.log(
'Initializing ObjectBox database...',
name: 'DatabaseService',
);
// Get database path
final docsDir = await getApplicationDocumentsDirectory();
@@ -75,8 +78,6 @@ class DatabaseService {
await initialize();
}
/// Log database statistics
Future<void> _logDatabaseStats() async {
try {
@@ -164,7 +165,10 @@ class DatabaseService {
return;
}
developer.log('Deleting job $jobId from database...', name: 'DatabaseService');
developer.log(
'Deleting job $jobId from database...',
name: 'DatabaseService',
);
final jobBox = _store!.box<JobEntity>();
final query = jobBox.query(JobEntity_.jobId.equals(jobId)).build();
@@ -173,9 +177,15 @@ class DatabaseService {
if (entities.isNotEmpty) {
jobBox.remove(entities.first.id);
developer.log('Job $jobId deleted successfully', name: 'DatabaseService');
developer.log(
'Job $jobId deleted successfully',
name: 'DatabaseService',
);
} else {
developer.log('Job $jobId not found in database', name: 'DatabaseService');
developer.log(
'Job $jobId not found in database',
name: 'DatabaseService',
);
}
} catch (e, stackTrace) {
developer.log('Error deleting job: $e', name: 'DatabaseService');
@@ -220,9 +230,13 @@ class DatabaseService {
if (jobs.isNotEmpty) {
try {
final chatBox = _store!.box<ChatMessageEntity>();
final query = chatBox.query(
(ChatMessageEntity_.jobId.notNull() | ChatMessageEntity_.jobNumber.notNull())
).build();
final query =
chatBox
.query(
(ChatMessageEntity_.jobId.notNull() |
ChatMessageEntity_.jobNumber.notNull()),
)
.build();
final messagesWithJobs = query.find();
query.close();
@@ -282,7 +296,8 @@ class DatabaseService {
final taskStatusBox = _store!.box<TaskStatusEntity>();
// Find existing entity by taskId
final query = taskStatusBox.query(TaskStatusEntity_.taskId.equals(taskId)).build();
final query =
taskStatusBox.query(TaskStatusEntity_.taskId.equals(taskId)).build();
final existing = query.findFirst();
query.close();
@@ -321,7 +336,8 @@ class DatabaseService {
}
final taskStatusBox = _store!.box<TaskStatusEntity>();
final query = taskStatusBox.query(TaskStatusEntity_.taskId.equals(taskId)).build();
final query =
taskStatusBox.query(TaskStatusEntity_.taskId.equals(taskId)).build();
final entity = query.findFirst();
query.close();
@@ -449,7 +465,8 @@ class DatabaseService {
final keys = jobIds.map((id) => 'job_seen:$id').toList();
for (final key in keys) {
final query = userDataBox.query(UserDataEntity_.key.equals(key)).build();
final query =
userDataBox.query(UserDataEntity_.key.equals(key)).build();
final entity = query.findFirst();
query.close();
@@ -545,7 +562,8 @@ class DatabaseService {
final taskStatusBox = _store!.box<TaskStatusEntity>();
// Find existing job entity by jobId
final jobQuery = jobBox.query(JobEntity_.jobId.equals(normalized.id)).build();
final jobQuery =
jobBox.query(JobEntity_.jobId.equals(normalized.id)).build();
final existingJob = jobQuery.findFirst();
jobQuery.close();
@@ -568,7 +586,10 @@ class DatabaseService {
final taskIds = normalized.tasks.map((t) => t.id).toList();
if (taskIds.isNotEmpty) {
for (final taskId in taskIds) {
final query = taskStatusBox.query(TaskStatusEntity_.taskId.equals(taskId)).build();
final query =
taskStatusBox
.query(TaskStatusEntity_.taskId.equals(taskId))
.build();
final entities = query.find();
query.close();
for (final entity in entities) {
@@ -617,7 +638,8 @@ class DatabaseService {
if (trimmedJobId.isNotEmpty) {
// Delete job
final jobQuery = jobBox.query(JobEntity_.jobId.equals(trimmedJobId)).build();
final jobQuery =
jobBox.query(JobEntity_.jobId.equals(trimmedJobId)).build();
final jobEntities = jobQuery.find();
jobQuery.close();
for (final entity in jobEntities) {
@@ -625,7 +647,10 @@ class DatabaseService {
}
// Delete job_seen flag
final seenQuery = userDataBox.query(UserDataEntity_.key.equals('job_seen:$trimmedJobId')).build();
final seenQuery =
userDataBox
.query(UserDataEntity_.key.equals('job_seen:$trimmedJobId'))
.build();
final seenEntities = seenQuery.find();
seenQuery.close();
for (final entity in seenEntities) {
@@ -633,15 +658,19 @@ class DatabaseService {
}
}
final taskIds = job.tasks
.map((task) => task.id.trim())
.where((id) => id.isNotEmpty)
.toList();
final taskIds =
job.tasks
.map((task) => task.id.trim())
.where((id) => id.isNotEmpty)
.toList();
if (taskIds.isNotEmpty) {
for (final taskId in taskIds) {
// Delete task status
final taskQuery = taskStatusBox.query(TaskStatusEntity_.taskId.equals(taskId)).build();
final taskQuery =
taskStatusBox
.query(TaskStatusEntity_.taskId.equals(taskId))
.build();
final taskEntities = taskQuery.find();
taskQuery.close();
for (final entity in taskEntities) {
@@ -649,7 +678,8 @@ class DatabaseService {
}
// Delete photos
final photoQuery = photoBox.query(PhotoEntity_.taskId.equals(taskId)).build();
final photoQuery =
photoBox.query(PhotoEntity_.taskId.equals(taskId)).build();
final photoEntities = photoQuery.find();
photoQuery.close();
for (final entity in photoEntities) {
@@ -960,9 +990,21 @@ class DatabaseService {
/// Save login credentials for auto-login on app restart
Future<void> saveCredentials(String email, String password) async {
await saveKeyValue('auth_email', email);
final normalizedEmail = email.trim();
if (normalizedEmail.isEmpty || password.isEmpty) {
developer.log(
'Skipping credential save because email or password is empty',
name: 'DatabaseService',
);
return;
}
await saveKeyValue('auth_email', normalizedEmail);
await saveKeyValue('auth_password', password);
developer.log('Credentials saved for $email', name: 'DatabaseService');
developer.log(
'Credentials saved for $normalizedEmail',
name: 'DatabaseService',
);
}
/// Load saved login credentials
@@ -970,11 +1012,29 @@ class DatabaseService {
Future<({String email, String password})?> loadCredentials() async {
final email = await loadKeyValue('auth_email');
final password = await loadKeyValue('auth_password');
if (email != null && password != null) {
developer.log('Credentials loaded for $email', name: 'DatabaseService');
return (email: email, password: password);
final normalizedEmail = email?.trim();
if (normalizedEmail != null &&
normalizedEmail.isNotEmpty &&
password != null &&
password.isNotEmpty) {
developer.log(
'Credentials loaded for $normalizedEmail',
name: 'DatabaseService',
);
return (email: normalizedEmail, password: password);
}
developer.log('No credentials found', name: 'DatabaseService');
if ((email != null && email.isNotEmpty) ||
(password != null && password.isNotEmpty)) {
developer.log(
'Stored credentials are incomplete or empty - removing them',
name: 'DatabaseService',
);
await deleteCredentials();
}
developer.log('No valid credentials found', name: 'DatabaseService');
return null;
}
@@ -1008,7 +1068,10 @@ class DatabaseService {
final chatBox = _store!.box<ChatMessageEntity>();
// Find existing entity by messageId
final query = chatBox.query(ChatMessageEntity_.messageId.equals(message.id)).build();
final query =
chatBox
.query(ChatMessageEntity_.messageId.equals(message.id))
.build();
final existing = query.findFirst();
query.close();
@@ -1060,7 +1123,10 @@ class DatabaseService {
return;
}
final chatBox = _store!.box<ChatMessageEntity>();
final query = chatBox.query(ChatMessageEntity_.conversationKey.equals(fromKey)).build();
final query =
chatBox
.query(ChatMessageEntity_.conversationKey.equals(fromKey))
.build();
final entities = query.find();
query.close();
@@ -1089,13 +1155,18 @@ class DatabaseService {
}
final chatBox = _store!.box<ChatMessageEntity>();
final query = chatBox.query(
ChatMessageEntity_.conversationKey.equals(conversationKey) &
ChatMessageEntity_.pendingSync.equals(true) &
ChatMessageEntity_.content.equals(message.content) &
ChatMessageEntity_.contentType.equals(chatContentTypeToString(message.contentType)) &
ChatMessageEntity_.messageId.notEquals(message.id)
).build();
final query =
chatBox
.query(
ChatMessageEntity_.conversationKey.equals(conversationKey) &
ChatMessageEntity_.pendingSync.equals(true) &
ChatMessageEntity_.content.equals(message.content) &
ChatMessageEntity_.contentType.equals(
chatContentTypeToString(message.contentType),
) &
ChatMessageEntity_.messageId.notEquals(message.id),
)
.build();
final entities = query.find();
query.close();
@@ -1123,9 +1194,13 @@ class DatabaseService {
List<ChatMessageEntity> entities;
if (conversationKey != null) {
final query = chatBox.query(ChatMessageEntity_.conversationKey.equals(conversationKey))
.order(ChatMessageEntity_.createdAt)
.build();
final query =
chatBox
.query(
ChatMessageEntity_.conversationKey.equals(conversationKey),
)
.order(ChatMessageEntity_.createdAt)
.build();
entities = query.find();
query.close();
} else {
@@ -1186,7 +1261,10 @@ class DatabaseService {
}
final chatBox = _store!.box<ChatMessageEntity>();
final query = chatBox.query(ChatMessageEntity_.conversationKey.equals(conversationKey)).build();
final query =
chatBox
.query(ChatMessageEntity_.conversationKey.equals(conversationKey))
.build();
final entities = query.find();
query.close();
@@ -1211,7 +1289,8 @@ class DatabaseService {
}
final chatBox = _store!.box<ChatMessageEntity>();
final query = chatBox.query(ChatMessageEntity_.messageId.equals(messageId)).build();
final query =
chatBox.query(ChatMessageEntity_.messageId.equals(messageId)).build();
final entities = query.find();
query.close();
@@ -1241,15 +1320,18 @@ class DatabaseService {
final trimmedJobId = jobId?.trim() ?? '';
final trimmedJobNumber = jobNumber?.trim() ?? '';
final keysList = conversationKeys == null
? <String>[]
: conversationKeys
.map((key) => key.trim())
.where((key) => key.isNotEmpty)
.toSet()
.toList();
final keysList =
conversationKeys == null
? <String>[]
: conversationKeys
.map((key) => key.trim())
.where((key) => key.isNotEmpty)
.toSet()
.toList();
if (trimmedJobId.isEmpty && trimmedJobNumber.isEmpty && keysList.isEmpty) {
if (trimmedJobId.isEmpty &&
trimmedJobNumber.isEmpty &&
keysList.isEmpty) {
developer.log(
'No chat messages matched deletion criteria for jobId=$jobId jobNumber=$jobNumber',
name: 'DatabaseService',
@@ -1261,20 +1343,29 @@ class DatabaseService {
final entitiesToDelete = <ChatMessageEntity>[];
if (trimmedJobId.isNotEmpty) {
final query = chatBox.query(ChatMessageEntity_.jobId.equals(trimmedJobId)).build();
final query =
chatBox
.query(ChatMessageEntity_.jobId.equals(trimmedJobId))
.build();
entitiesToDelete.addAll(query.find());
query.close();
}
if (trimmedJobNumber.isNotEmpty) {
final query = chatBox.query(ChatMessageEntity_.jobNumber.equals(trimmedJobNumber)).build();
final query =
chatBox
.query(ChatMessageEntity_.jobNumber.equals(trimmedJobNumber))
.build();
entitiesToDelete.addAll(query.find());
query.close();
}
if (keysList.isNotEmpty) {
for (final key in keysList) {
final query = chatBox.query(ChatMessageEntity_.conversationKey.equals(key)).build();
final query =
chatBox
.query(ChatMessageEntity_.conversationKey.equals(key))
.build();
entitiesToDelete.addAll(query.find());
query.close();
}
@@ -1309,7 +1400,8 @@ class DatabaseService {
}
final chatBox = _store!.box<ChatMessageEntity>();
final query = chatBox.query(ChatMessageEntity_.read.equals(false)).build();
final query =
chatBox.query(ChatMessageEntity_.read.equals(false)).build();
final count = query.count();
query.close();
@@ -1349,6 +1441,7 @@ class DatabaseService {
/// Save a failed message to the queue
Future<void> queueMessage(QueuedMessage message) async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
@@ -1357,7 +1450,8 @@ class DatabaseService {
final box = _store!.box<QueuedMessageEntity>();
// Find existing entity by messageId
final query = box.query(QueuedMessageEntity_.messageId.equals(message.id)).build();
final query =
box.query(QueuedMessageEntity_.messageId.equals(message.id)).build();
final existing = query.findFirst();
query.close();
@@ -1391,6 +1485,7 @@ class DatabaseService {
/// Get all queued messages
Future<List<QueuedMessage>> getQueuedMessages() async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return [];
@@ -1424,13 +1519,15 @@ class DatabaseService {
/// Remove a successfully sent message from the queue
Future<void> removeQueuedMessage(String messageId) async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
final box = _store!.box<QueuedMessageEntity>();
final query = box.query(QueuedMessageEntity_.messageId.equals(messageId)).build();
final query =
box.query(QueuedMessageEntity_.messageId.equals(messageId)).build();
final entities = query.find();
query.close();
@@ -1452,18 +1549,17 @@ class DatabaseService {
}
/// Update retry count for a message
Future<void> updateMessageRetryCount(
String messageId,
int retryCount,
) async {
Future<void> updateMessageRetryCount(String messageId, int retryCount) async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
final box = _store!.box<QueuedMessageEntity>();
final query = box.query(QueuedMessageEntity_.messageId.equals(messageId)).build();
final query =
box.query(QueuedMessageEntity_.messageId.equals(messageId)).build();
final entity = query.findFirst();
query.close();
@@ -1488,16 +1584,14 @@ class DatabaseService {
/// Clear all queued messages (for cleanup)
Future<void> clearQueuedMessages() async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
_store!.box<QueuedMessageEntity>().removeAll();
developer.log(
'Cleared all queued messages',
name: 'DatabaseService',
);
developer.log('Cleared all queued messages', name: 'DatabaseService');
} catch (e, st) {
developer.log(
'Error clearing queued messages: $e',
@@ -1507,12 +1601,49 @@ class DatabaseService {
}
}
Future<String?> updateChatMessagePendingSync(
String messageId,
bool pendingSync,
) async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return null;
}
final chatBox = _store!.box<ChatMessageEntity>();
final query =
chatBox.query(ChatMessageEntity_.messageId.equals(messageId)).build();
final entity = query.findFirst();
query.close();
if (entity == null) {
return null;
}
entity.pendingSync = pendingSync;
chatBox.put(entity);
return entity.conversationKey;
} catch (e, st) {
developer.log(
'Error updating pendingSync for message $messageId: $e',
name: 'DatabaseService',
);
developer.log('Stack trace: $st', name: 'DatabaseService');
return null;
}
}
// Language preference persistence ----------------------------------------------------
/// Save language preference
Future<void> saveLanguagePreference(String languageCode) async {
await saveKeyValue('language_preference', languageCode);
developer.log('Language preference saved: $languageCode', name: 'DatabaseService');
developer.log(
'Language preference saved: $languageCode',
name: 'DatabaseService',
);
}
/// Load saved language preference
@@ -1520,7 +1651,10 @@ class DatabaseService {
Future<String?> loadLanguagePreference() async {
final languageCode = await loadKeyValue('language_preference');
if (languageCode != null) {
developer.log('Language preference loaded: $languageCode', name: 'DatabaseService');
developer.log(
'Language preference loaded: $languageCode',
name: 'DatabaseService',
);
return languageCode;
}
developer.log('No language preference found', name: 'DatabaseService');

View File

@@ -13,6 +13,7 @@ import 'location_service.dart';
import '../app_state.dart';
import '../models/chat_message.dart';
import '../models/job.dart';
import '../models/queued_message.dart';
import 'dart_mq.dart';
class WebSocketService {
@@ -193,6 +194,73 @@ class WebSocketService {
_reconnectTimer = null;
}
/// Force a clean reconnect after the app resumes from standby.
/// Keeps buffered outbound messages intact and relies on saved credentials
/// for the subsequent auto-login inside [connect].
Future<void> reconnectForAppResume() async {
final credentials = await _databaseService.loadCredentials();
if (credentials == null) {
developer.log(
'Skipping reconnect after resume - no saved credentials',
name: 'WebSocketService',
);
return;
}
if (_isConnecting) {
developer.log(
'Skipping reconnect after resume - connection attempt already running',
name: 'WebSocketService',
);
return;
}
developer.log(
'Restarting WebSocket connection after app resume',
name: 'WebSocketService',
);
_stopReconnectTimer();
final existingSubscription = _wsSubscription;
final existingChannel = _wsChannel;
_wsSubscription = null;
_wsChannel = null;
_disconnectCompleter = null;
_isConnected = false;
_isConnecting = false;
_isAuthenticated = false;
_authToken = null;
_lastAuthResponse = null;
Future.microtask(() {
DartMQ().publish<bool>(MQTopics.connectionStatus, false);
});
try {
await existingSubscription?.cancel();
} catch (e, st) {
developer.log(
'Error cancelling old WebSocket subscription on resume: $e',
name: 'WebSocketService',
);
developer.log('Stack: $st', name: 'WebSocketService');
}
try {
await existingChannel?.sink.close(ws_status.goingAway);
} catch (e, st) {
developer.log(
'Error closing old WebSocket channel on resume: $e',
name: 'WebSocketService',
);
developer.log('Stack: $st', name: 'WebSocketService');
}
await connect();
}
// ---------------------------------------------------------------------------
// WebSocket Send / Receive
// ---------------------------------------------------------------------------
@@ -290,6 +358,8 @@ class WebSocketService {
_handleJobDeletedMessage(data);
} else if (topic.endsWith('/job_created')) {
_handleJobCreatedMessage(data);
} else if (topic.endsWith('/message_ack')) {
await _handleChatMessageAck(data);
} else if (topic.endsWith('/message')) {
await _handleChatMessage(topic, data);
} else {
@@ -598,6 +668,20 @@ class WebSocketService {
}
}
Future<void> _handleChatMessageAck(Map<String, dynamic> data) async {
final clientMessageId = data['clientMessageId']?.toString().trim() ?? '';
if (clientMessageId.isEmpty) {
developer.log(
'Received message ACK without clientMessageId',
name: 'WebSocketService',
);
return;
}
await _databaseService.removeQueuedMessage(clientMessageId);
await ChatService().markOutgoingMessageSynced(clientMessageId);
}
void _handleOtherClientMessage(String topic, Map<String, dynamic> data) {
final type = data['type'];
if (topic.contains('/tasks/') || type == 'task') {
@@ -731,6 +815,7 @@ class WebSocketService {
/// Clears all local jobs and related data, then notifies the server.
Future<void> _flushMessageBuffer() async {
final initialBufferSize = _messageBuffer.length;
final sentQueuedChatCount = await _flushQueuedChatMessages();
if (initialBufferSize > 0) {
developer.log(
@@ -766,7 +851,8 @@ class WebSocketService {
await _databaseService.clearAllJobsAndRelatedData();
// Notify server that buffer flush is complete
final sentCount = initialBufferSize - _messageBuffer.length;
final sentCount =
(initialBufferSize - _messageBuffer.length) + sentQueuedChatCount;
final bufferFlushedPayload = jsonEncode({
'timestamp': DateTime.now().toIso8601String(),
'messageCount': sentCount,
@@ -774,9 +860,51 @@ class WebSocketService {
_sendWebSocket('/server/buffer_flushed', bufferFlushedPayload);
}
Future<int> _flushQueuedChatMessages() async {
final queuedMessages = await _databaseService.getQueuedMessages();
if (queuedMessages.isEmpty) {
return 0;
}
developer.log(
'Flushing ${queuedMessages.length} queued chat messages',
name: 'WebSocketService',
);
var sentCount = 0;
for (final message in queuedMessages) {
final success = await _trySendQueuedChatMessage(
message,
incrementRetryOnFailure: true,
);
if (success) {
sentCount++;
}
}
return sentCount;
}
Future<bool> _trySendQueuedChatMessage(
QueuedMessage message, {
bool incrementRetryOnFailure = false,
}) async {
if (!_isConnected || !_isAuthenticated || _wsChannel == null) {
return false;
}
final success = _sendWebSocket(message.topic, jsonEncode(message.payload));
if (!success && incrementRetryOnFailure) {
await _databaseService.updateMessageRetryCount(
message.id,
message.retryCount + 1,
);
}
return success;
}
/// Publish a chat message according to the backend contract.
/// Returns the locally constructed message so callers can persist it locally.
/// Messages are buffered if offline and sent automatically when reconnected.
/// The message is stored locally and remains queued until the server confirms it.
Future<ChatMessage?> sendChatMessage({
required String sender,
required String receiver,
@@ -790,6 +918,9 @@ class WebSocketService {
final trimmedContent = content.trim();
final normalizedJobId = jobId?.trim();
final normalizedJobNumber = jobNumber?.trim();
final hasJobContext =
(normalizedJobId?.isNotEmpty ?? false) ||
(normalizedJobNumber?.isNotEmpty ?? false);
if (trimmedSender.isEmpty ||
trimmedReceiver.isEmpty ||
@@ -816,6 +947,9 @@ class WebSocketService {
'receiver': trimmedReceiver,
'content': trimmedContent,
};
final now = DateTime.now();
final clientMessageId = 'local-${now.microsecondsSinceEpoch}';
payload['messageId'] = clientMessageId;
if (normalizedJobId != null && normalizedJobId.isNotEmpty) {
payload['jobId'] = normalizedJobId;
@@ -828,18 +962,13 @@ class WebSocketService {
const topic = '/server/message';
try {
final jsonPayload = jsonEncode(payload);
// sendMessage buffers automatically if not connected/authenticated
sendMessage(topic, jsonPayload);
final now = DateTime.now();
final message = ChatMessage(
id: 'local-${now.microsecondsSinceEpoch}',
id: clientMessageId,
content: trimmedContent,
createdAt: now,
direction: ChatDirection.outgoing,
messageType:
normalizedJobId != null && normalizedJobId.isNotEmpty
hasJobContext
? ChatMessageType.jobRelated
: ChatMessageType.general,
contentType: contentType,
@@ -849,13 +978,26 @@ class WebSocketService {
read: false,
pendingSync: true,
);
final queuedMessage = QueuedMessage(
id: clientMessageId,
topic: topic,
payload: payload,
createdAt: now,
);
await _databaseService.queueMessage(queuedMessage);
await ChatService().saveOutgoingMessage(message);
final sentImmediately = await _trySendQueuedChatMessage(queuedMessage);
if (!sentImmediately) {
developer.log(
'Chat message $clientMessageId queued for retry after reconnect',
name: 'WebSocketService',
);
}
return message;
} catch (e, st) {
developer.log(
'Error encoding chat message payload: $e',
name: 'WebSocketService',
);
developer.log('Error sending chat message: $e', name: 'WebSocketService');
developer.log('Stack: $st', name: 'WebSocketService');
return null;
}

View File

@@ -6,6 +6,7 @@ import 'package:image/image.dart' as img;
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'l10n/app_localizations.dart';
import 'l10n/localization_helpers.dart';
import 'models/job.dart';
import 'models/task.dart';
import 'models/tasks/confirmation_task.dart';
@@ -721,8 +722,14 @@ class _TaskViewState extends State<TaskView> {
decoration: isCompleted ? TextDecoration.lineThrough : null,
);
final displayName = task.displayName;
final description = task.description;
final displayName =
task.displayName != null
? localizeKnownText(context, task.displayName!)
: null;
final description =
task.description != null
? localizeKnownText(context, task.description!)
: null;
if (displayName?.isNotEmpty == true) {
return Column(
@@ -785,12 +792,10 @@ class _TaskViewState extends State<TaskView> {
if (station.stationOrder == stationOrder) {
final suffix =
station.displayName.isNotEmpty ? station.displayName : station.city;
return suffix.isNotEmpty
? 'Station ${stationOrder + 1}: $suffix'
: 'Station ${stationOrder + 1}';
return localizedStationLabel(context, stationOrder + 1, suffix: suffix);
}
}
return 'Station ${stationOrder + 1}';
return AppLocalizations.of(context).stationNumber(stationOrder + 1);
}
}

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.9.12+1
version: 0.9.15+1
environment:
sdk: ^3.7.0