From bba57337835eddc4490377ab071b2cc5128483f9 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Sat, 4 Apr 2026 10:30:36 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20erweiterte=20Chat-Funktionalit=C3=A4t,?= =?UTF-8?q?=20UI-Verbesserungen=20und=20Lokalisierungsupdates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/lib/cargo_items_view.dart | 22 +- app/lib/chat_details_view.dart | 34 +- app/lib/chats_view.dart | 24 +- app/lib/jobs_view.dart | 70 ++- app/lib/l10n/app_localizations.dart | 45 +- app/lib/l10n/app_localizations_de.dart | 111 ++++- app/lib/l10n/app_localizations_en.dart | 88 +++- app/lib/l10n/app_localizations_es.dart | 113 ++++- app/lib/l10n/app_localizations_et.dart | 97 +++- app/lib/l10n/app_localizations_fr.dart | 116 ++++- app/lib/l10n/app_localizations_lt.dart | 104 +++- app/lib/l10n/app_localizations_lv.dart | 91 +++- app/lib/l10n/app_localizations_pl.dart | 100 +++- app/lib/l10n/app_localizations_ru.dart | 107 ++++- app/lib/l10n/app_localizations_tr.dart | 94 +++- app/lib/l10n/localization_helpers.dart | 50 ++ app/lib/login_view.dart | 454 ++++++++++++++---- app/lib/main.dart | 108 ++++- app/lib/services/chat_service.dart | 133 ++--- app/lib/services/database_service.dart | 268 ++++++++--- app/lib/services/websocket_service.dart | 170 ++++++- app/lib/task_view.dart | 17 +- app/pubspec.yaml | 2 +- backend/pom.xml | 2 +- .../dto/ChatMessageInboundPayload.java | 7 +- .../de/assecutor/votianlt/model/Message.java | 7 + .../ui/component/DeliveryStationDialog.java | 35 +- .../ui/component/DialogStylingHelper.java | 56 ++- .../ui/component/PickupStationDialog.java | 35 +- .../pages/base/ui/view/AdminLayout.java | 16 +- .../pages/base/ui/view/MainLayout.java | 17 +- .../votianlt/pages/view/AddCustomerView.java | 2 +- .../votianlt/pages/view/AddJobView.java | 152 +++--- .../pages/view/CreateInvoiceView.java | 30 +- .../votianlt/pages/view/EditProfileView.java | 8 +- .../votianlt/pages/view/ImprintView.java | 90 +++- .../pages/view/InvoiceGeneratorView.java | 14 +- .../votianlt/pages/view/JobHistoryView.java | 60 ++- .../votianlt/pages/view/JobSummaryView.java | 89 ++-- .../pages/view/MessageDetailsView.java | 2 +- .../votianlt/pages/view/ShowJobsView.java | 88 ++-- .../votianlt/pages/view/StartView.java | 20 +- .../repository/MessageRepository.java | 4 + .../votianlt/service/MessageService.java | 36 ++ .../src/main/resources/messages_de.properties | 20 + .../src/main/resources/messages_ee.properties | 21 + .../src/main/resources/messages_en.properties | 20 + .../src/main/resources/messages_es.properties | 20 + .../src/main/resources/messages_fr.properties | 20 + .../src/main/resources/messages_lt.properties | 20 + .../src/main/resources/messages_lv.properties | 20 + .../src/main/resources/messages_pl.properties | 20 + .../src/main/resources/messages_ru.properties | 20 + .../src/main/resources/messages_tr.properties | 20 + upload.sh | 16 + 55 files changed, 2708 insertions(+), 697 deletions(-) create mode 100644 app/lib/l10n/localization_helpers.dart create mode 100755 upload.sh diff --git a/app/lib/cargo_items_view.dart b/app/lib/cargo_items_view.dart index 2c659c8..25af807 100644 --- a/app/lib/cargo_items_view.dart +++ b/app/lib/cargo_items_view.dart @@ -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 { @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 { 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 { ), 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 { ), 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 { station.company.isNotEmpty && station.company != title ? station.company : null; + final l10n = AppLocalizations.of(context); final addressLines = [ [ @@ -289,7 +293,7 @@ class _CargoItemsViewState extends State { stationTitle: station.displayName.isNotEmpty ? station.displayName - : 'Station ${station.stationOrder + 1}', + : l10n.stationNumber(station.stationOrder + 1), ), ), ); @@ -313,7 +317,7 @@ class _CargoItemsViewState extends State { 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 { 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 { const SizedBox(height: 12), _buildDetailItem( Icons.phone_outlined, - 'Telefon', + l10n.phone, station.phone, Colors.green, ), diff --git a/app/lib/chat_details_view.dart b/app/lib/chat_details_view.dart index 7db0089..40f34d8 100644 --- a/app/lib/chat_details_view.dart +++ b/app/lib/chat_details_view.dart @@ -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 { 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 { return; } - await _chatService.saveOutgoingMessage(result); _syncActiveChatFromService(); _messageController.clear(); @@ -250,7 +248,10 @@ class _ChatDetailsViewState extends State { 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 { 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 { return; } - await _chatService.saveOutgoingMessage(result); _syncActiveChatFromService(); if (prepared.bytes.isNotEmpty) { @@ -645,7 +643,7 @@ class _ChatDetailsViewState extends State { 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 { 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())}', ), ], ), diff --git a/app/lib/chats_view.dart b/app/lib/chats_view.dart index f009002..a821e3f 100644 --- a/app/lib/chats_view.dart +++ b/app/lib/chats_view.dart @@ -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 { 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 { } 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 { 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 { 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, diff --git a/app/lib/jobs_view.dart b/app/lib/jobs_view.dart index 86c1810..7663277 100644 --- a/app/lib/jobs_view.dart +++ b/app/lib/jobs_view.dart @@ -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 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 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 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 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 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 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 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; + } } } diff --git a/app/lib/l10n/app_localizations.dart b/app/lib/l10n/app_localizations.dart index 09fb421..4f506db 100644 --- a/app/lib/l10n/app_localizations.dart +++ b/app/lib/l10n/app_localizations.dart @@ -11,15 +11,28 @@ import 'app_localizations_lv.dart'; import 'app_localizations_lt.dart'; /// Supported language codes -const List supportedLanguageCodes = ['de', 'en', 'es', 'fr', 'pl', 'ru', 'tr', 'et', 'lv', 'lt']; +const List 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(context, AppLocalizations) ?? AppLocalizationsDe(); + return Localizations.of(context, AppLocalizations) ?? + AppLocalizationsDe(); } - static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); + static const LocalizationsDelegate 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 { +class _AppLocalizationsDelegate + extends LocalizationsDelegate { const _AppLocalizationsDelegate(); @override diff --git a/app/lib/l10n/app_localizations_de.dart b/app/lib/l10n/app_localizations_de.dart index 0139050..cd3834e 100644 --- a/app/lib/l10n/app_localizations_de.dart +++ b/app/lib/l10n/app_localizations_de.dart @@ -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'; diff --git a/app/lib/l10n/app_localizations_en.dart b/app/lib/l10n/app_localizations_en.dart index 7a6eb9c..a26d2b0 100644 --- a/app/lib/l10n/app_localizations_en.dart +++ b/app/lib/l10n/app_localizations_en.dart @@ -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'; diff --git a/app/lib/l10n/app_localizations_es.dart b/app/lib/l10n/app_localizations_es.dart index d2c676b..718ae64 100644 --- a/app/lib/l10n/app_localizations_es.dart +++ b/app/lib/l10n/app_localizations_es.dart @@ -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'; diff --git a/app/lib/l10n/app_localizations_et.dart b/app/lib/l10n/app_localizations_et.dart index 380c585..b5fec55 100644 --- a/app/lib/l10n/app_localizations_et.dart +++ b/app/lib/l10n/app_localizations_et.dart @@ -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'; diff --git a/app/lib/l10n/app_localizations_fr.dart b/app/lib/l10n/app_localizations_fr.dart index 8d035fe..f10fe02 100644 --- a/app/lib/l10n/app_localizations_fr.dart +++ b/app/lib/l10n/app_localizations_fr.dart @@ -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'; diff --git a/app/lib/l10n/app_localizations_lt.dart b/app/lib/l10n/app_localizations_lt.dart index 61397b2..3b59d8d 100644 --- a/app/lib/l10n/app_localizations_lt.dart +++ b/app/lib/l10n/app_localizations_lt.dart @@ -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 => 'iš'; @@ -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'; diff --git a/app/lib/l10n/app_localizations_lv.dart b/app/lib/l10n/app_localizations_lv.dart index 06426e9..e9116d3 100644 --- a/app/lib/l10n/app_localizations_lv.dart +++ b/app/lib/l10n/app_localizations_lv.dart @@ -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'; diff --git a/app/lib/l10n/app_localizations_pl.dart b/app/lib/l10n/app_localizations_pl.dart index 43b7bfc..eef2fdb 100644 --- a/app/lib/l10n/app_localizations_pl.dart +++ b/app/lib/l10n/app_localizations_pl.dart @@ -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'; diff --git a/app/lib/l10n/app_localizations_ru.dart b/app/lib/l10n/app_localizations_ru.dart index 357f3da..17ec8d6 100644 --- a/app/lib/l10n/app_localizations_ru.dart +++ b/app/lib/l10n/app_localizations_ru.dart @@ -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 => 'Средний'; diff --git a/app/lib/l10n/app_localizations_tr.dart b/app/lib/l10n/app_localizations_tr.dart index 5ed83be..8ffe54f 100644 --- a/app/lib/l10n/app_localizations_tr.dart +++ b/app/lib/l10n/app_localizations_tr.dart @@ -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'; diff --git a/app/lib/l10n/localization_helpers.dart b/app/lib/l10n/localization_helpers.dart new file mode 100644 index 0000000..caf9507 --- /dev/null +++ b/app/lib/l10n/localization_helpers.dart @@ -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); +} diff --git a/app/lib/login_view.dart b/app/lib/login_view.dart index 731f73d..5984e4f 100644 --- a/app/lib/login_view.dart +++ b/app/lib/login_view.dart @@ -34,6 +34,8 @@ class _LoginViewState extends State { bool _logoutNoticeShown = false; bool _hasNavigatedToJobs = false; String _appVersion = ''; + String? _pendingLoginEmail; + String? _pendingLoginPassword; @override void initState() { @@ -52,7 +54,13 @@ class _LoginViewState extends State { 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 { // 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(MQTopics.connectionStatus, (isConnected) { - if (mounted) { - setState(() {}); - } - }); + _connectionStatusSubscription = DartMQ().subscribe( + MQTopics.connectionStatus, + (isConnected) { + if (mounted) { + setState(() {}); + } + }, + ); // Listen to authentication responses via dart_mq - _authResponseSubscription = DartMQ().subscribe>(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>( + 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 _handleAuthResponse(Map 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 _handleLogin() async { @@ -158,34 +240,62 @@ class _LoginViewState extends State { 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 { // Wait for connection to be established with a timeout try { final completer = Completer(); - final subscription = DartMQ().subscribe(MQTopics.connectionStatus, (isConnected) { - if (isConnected && !completer.isCompleted) { - completer.complete(true); - } - }); + final subscription = DartMQ().subscribe( + 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 { 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 { _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 { 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 { 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 { 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(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( + 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 { ), ), // 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, + ), + ), ], ), ); diff --git a/app/lib/main.dart b/app/lib/main.dart index d2fbe87..581876f 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -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 createState() => _MyAppState(); +} + +class _MyAppState extends State 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( 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 { @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: [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: [ + 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. ); } } diff --git a/app/lib/services/chat_service.dart b/app/lib/services/chat_service.dart index 4ef3de8..25ca319 100644 --- a/app/lib/services/chat_service.dart +++ b/app/lib/services/chat_service.dart @@ -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 = [a.toLowerCase(), b.toLowerCase()]..sort(); - return '$_generalPrefix${participants.join('|')}'; + return _defaultGeneralConversationKey; } Future saveIncomingMessage(ChatMessage message) async { @@ -205,6 +183,20 @@ class ChatService { await _persistMessage(message); } + Future markOutgoingMessageSynced(String messageId) async { + if (!_initialized) { + await initialize(); + } + + final conversationKey = await _databaseService.updateChatMessagePendingSync( + messageId, + false, + ); + if (conversationKey != null && conversationKey.isNotEmpty) { + await _refreshConversation(conversationKey); + } + } + Future _persistMessage(ChatMessage message) async { final conversationKey = conversationKeyForMessage(message); @@ -239,7 +231,7 @@ class ChatService { Future _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 _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>> _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; - } } diff --git a/app/lib/services/database_service.dart b/app/lib/services/database_service.dart index 2199dcb..9136716 100644 --- a/app/lib/services/database_service.dart +++ b/app/lib/services/database_service.dart @@ -38,7 +38,10 @@ class DatabaseService { final completer = Completer(); _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 _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(); 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(); - 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(); // 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(); - 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(); // 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 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(); // 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(); - 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(); - 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 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(); - 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(); - 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 - ? [] - : conversationKeys - .map((key) => key.trim()) - .where((key) => key.isNotEmpty) - .toSet() - .toList(); + final keysList = + conversationKeys == null + ? [] + : 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 = []; 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(); - 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 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(); // 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> 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 removeQueuedMessage(String messageId) async { try { + await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final box = _store!.box(); - 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 updateMessageRetryCount( - String messageId, - int retryCount, - ) async { + Future updateMessageRetryCount(String messageId, int retryCount) async { try { + await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final box = _store!.box(); - 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 clearQueuedMessages() async { try { + await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } _store!.box().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 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(); + 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 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 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'); diff --git a/app/lib/services/websocket_service.dart b/app/lib/services/websocket_service.dart index 3cf6165..5ba380c 100644 --- a/app/lib/services/websocket_service.dart +++ b/app/lib/services/websocket_service.dart @@ -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 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(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 _handleChatMessageAck(Map 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 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 _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 _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 _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 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; } diff --git a/app/lib/task_view.dart b/app/lib/task_view.dart index 414f225..ab4b5a2 100644 --- a/app/lib/task_view.dart +++ b/app/lib/task_view.dart @@ -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 { 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 { 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); } } diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 1b9de10..85f4d98 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -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 diff --git a/backend/pom.xml b/backend/pom.xml index e6f1cef..16001d6 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -11,7 +11,7 @@ jar - 0.9.14 + 0.9.15 21 21 21 diff --git a/backend/src/main/java/de/assecutor/votianlt/dto/ChatMessageInboundPayload.java b/backend/src/main/java/de/assecutor/votianlt/dto/ChatMessageInboundPayload.java index 50563f8..50a9be5 100644 --- a/backend/src/main/java/de/assecutor/votianlt/dto/ChatMessageInboundPayload.java +++ b/backend/src/main/java/de/assecutor/votianlt/dto/ChatMessageInboundPayload.java @@ -8,8 +8,8 @@ import org.bson.types.ObjectId; * Normalized payload for chat messages sent by mobile clients via WebSocket. * receiver = AppUser ID (clientId) extracted from topic */ -public record ChatMessageInboundPayload(String receiver, String content, MessageContentType contentType, ObjectId jobId, - String jobNumber) { +public record ChatMessageInboundPayload(String receiver, String content, MessageContentType contentType, + String messageId, ObjectId jobId, String jobNumber) { public ChatMessageInboundPayload { contentType = contentType != null ? contentType : MessageContentType.TEXT; @@ -23,10 +23,11 @@ public record ChatMessageInboundPayload(String receiver, String content, Message String receiver = extractRequiredString(payload, "receiver"); String content = extractRequiredString(payload, "content"); MessageContentType contentType = extractContentType(payload.get("contentType")); + String messageId = extractOptionalString(payload.get("messageId")); ObjectId jobId = extractObjectId(payload.get("jobId"), "jobId"); String jobNumber = extractOptionalString(payload.get("jobNumber")); - return new ChatMessageInboundPayload(receiver, content, contentType, jobId, jobNumber); + return new ChatMessageInboundPayload(receiver, content, contentType, messageId, jobId, jobNumber); } public boolean hasJobContext() { diff --git a/backend/src/main/java/de/assecutor/votianlt/model/Message.java b/backend/src/main/java/de/assecutor/votianlt/model/Message.java index 1f5da37..55af35c 100644 --- a/backend/src/main/java/de/assecutor/votianlt/model/Message.java +++ b/backend/src/main/java/de/assecutor/votianlt/model/Message.java @@ -42,6 +42,13 @@ public class Message { @Field("receiver") private String receiver; + /** + * Optional stable client-side ID used for idempotent retries from the mobile + * app. + */ + @Field("client_message_id") + private String clientMessageId; + /** * Timestamp when the message was created */ diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java index 8e5d8a6..270b241 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java @@ -4,7 +4,6 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.combobox.ComboBox; -import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.H3; @@ -441,25 +440,21 @@ public class DeliveryStationDialog extends Dialog { close(); } else { // Adresse nicht gefunden: Benutzer fragen - ConfirmDialog confirmDialog = new ConfirmDialog(); - confirmDialog.setHeader( - translationHelper.getTranslation("addjob.validation.address.not.found.title")); - confirmDialog.setText( - translationHelper.getTranslation("addjob.validation.address.not.found.message")); - confirmDialog.setConfirmText( - translationHelper.getTranslation("addjob.validation.address.save.anyway")); - confirmDialog.setConfirmButtonTheme("primary"); - confirmDialog.setCancelable(true); - confirmDialog.setCancelText( - translationHelper.getTranslation("addjob.validation.address.correct")); - confirmDialog.addConfirmListener(ev -> { - data.setAddressValidatedByGoogle(false); - data.setAddressValidationResult(validationResult); - if (saveListener != null) { - saveListener.onSave(data); - } - close(); - }); + Dialog confirmDialog = DialogStylingHelper.createConfirmationDialog( + translationHelper.getTranslation("addjob.validation.address.not.found.title"), + translationHelper.getTranslation("addjob.validation.address.not.found.message"), + "560px", + translationHelper.getTranslation("addjob.validation.address.correct"), + translationHelper.getTranslation("addjob.validation.address.save.anyway"), + () -> { + data.setAddressValidatedByGoogle(false); + data.setAddressValidationResult(validationResult); + if (saveListener != null) { + saveListener.onSave(data); + } + close(); + }, + ButtonVariant.LUMO_PRIMARY); confirmDialog.open(); } })); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DialogStylingHelper.java b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DialogStylingHelper.java index acd5093..2f52791 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DialogStylingHelper.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DialogStylingHelper.java @@ -1,8 +1,11 @@ package de.assecutor.votianlt.pages.base.ui.component; import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.VerticalLayout; @@ -20,6 +23,33 @@ public final class DialogStylingHelper { return dialog; } + public static Dialog createConfirmationDialog(String title, String message, String width, String cancelText, + String confirmText, Runnable onConfirm, ButtonVariant... confirmVariants) { + Dialog dialog = createStyledDialog(title, width); + dialog.setCloseOnEsc(true); + dialog.setCloseOnOutsideClick(true); + + VerticalLayout dialogContent = createContentLayout("320px"); + if (message != null && !message.isBlank()) { + dialogContent.add(new Span(message)); + } + + Button cancelButton = new Button(cancelText, event -> dialog.close()); + cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + Button confirmButton = new Button(confirmText, event -> { + dialog.close(); + onConfirm.run(); + }); + if (confirmVariants != null && confirmVariants.length > 0) { + confirmButton.addThemeVariants(confirmVariants); + } + + dialog.add(wrapContent(dialogContent)); + dialog.getFooter().add(cancelButton, confirmButton); + return dialog; + } + public static void apply(Dialog dialog, String title, String width) { if (title != null && !title.isBlank()) { dialog.setHeaderTitle(title); @@ -32,16 +62,40 @@ public final class DialogStylingHelper { } public static Component wrapContent(Component content) { + return wrapContent(content, false); + } + + public static Component wrapContent(Component content, boolean fillHeight) { Div frame = new Div(); frame.getStyle().set("border", "10px solid transparent"); frame.getStyle().set("border-radius", "0"); frame.getStyle().set("box-sizing", "border-box"); + if (fillHeight) { + frame.getStyle().set("display", "flex"); + frame.getStyle().set("flex-direction", "column"); + frame.getStyle().set("height", "100%"); + frame.getStyle().set("min-height", "0"); + frame.getStyle().set("flex", "1"); + } frame.setWidthFull(); Div whiteCard = new Div(); whiteCard.getStyle().set("background", "white"); whiteCard.getStyle().set("border-radius", "24px"); - whiteCard.getStyle().set("overflow", "auto"); + if (fillHeight) { + whiteCard.getStyle().set("display", "flex"); + whiteCard.getStyle().set("flex-direction", "column"); + whiteCard.getStyle().set("height", "100%"); + whiteCard.getStyle().set("min-height", "0"); + whiteCard.getStyle().set("flex", "1"); + whiteCard.getStyle().set("overflow", "hidden"); + content.getElement().getStyle().set("width", "100%"); + content.getElement().getStyle().set("height", "100%"); + content.getElement().getStyle().set("min-height", "0"); + content.getElement().getStyle().set("flex", "1"); + } else { + whiteCard.getStyle().set("overflow", "auto"); + } whiteCard.setWidthFull(); whiteCard.add(content); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java index 00897e3..b716886 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java @@ -21,7 +21,6 @@ import com.vaadin.flow.component.textfield.NumberField; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.timepicker.TimePicker; import com.vaadin.flow.component.UI; -import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.progressbar.ProgressBar; import de.assecutor.votianlt.model.AddressValidationResult; @@ -577,25 +576,21 @@ public class PickupStationDialog extends Dialog { close(); } else { // Adresse nicht gefunden: Benutzer fragen - ConfirmDialog confirmDialog = new ConfirmDialog(); - confirmDialog.setHeader( - translationHelper.getTranslation("addjob.validation.address.not.found.title")); - confirmDialog.setText( - translationHelper.getTranslation("addjob.validation.address.not.found.message")); - confirmDialog.setConfirmText( - translationHelper.getTranslation("addjob.validation.address.save.anyway")); - confirmDialog.setConfirmButtonTheme("primary"); - confirmDialog.setCancelable(true); - confirmDialog.setCancelText( - translationHelper.getTranslation("addjob.validation.address.correct")); - confirmDialog.addConfirmListener(ev -> { - data.setAddressValidatedByGoogle(false); - data.setAddressValidationResult(validationResult); - if (saveListener != null) { - saveListener.onSave(data); - } - close(); - }); + Dialog confirmDialog = DialogStylingHelper.createConfirmationDialog( + translationHelper.getTranslation("addjob.validation.address.not.found.title"), + translationHelper.getTranslation("addjob.validation.address.not.found.message"), + "560px", + translationHelper.getTranslation("addjob.validation.address.correct"), + translationHelper.getTranslation("addjob.validation.address.save.anyway"), + () -> { + data.setAddressValidatedByGoogle(false); + data.setAddressValidationResult(validationResult); + if (saveListener != null) { + saveListener.onSave(data); + } + close(); + }, + ButtonVariant.LUMO_PRIMARY); confirmDialog.open(); } })); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/view/AdminLayout.java b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/view/AdminLayout.java index 43c4022..08806b1 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/view/AdminLayout.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/view/AdminLayout.java @@ -17,6 +17,7 @@ import com.vaadin.flow.component.sidenav.SideNav; import com.vaadin.flow.component.sidenav.SideNavItem; import com.vaadin.flow.router.Layout; import com.vaadin.flow.server.auth.AnonymousAllowed; +import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper; import de.assecutor.votianlt.pages.view.EditProfileView; import de.assecutor.votianlt.security.SecurityService; @@ -136,7 +137,7 @@ public final class AdminLayout extends AppLayout { // Profile display with navigation userMenuItem.getSubMenu().addItem("Profil anzeigen", e -> UI.getCurrent().navigate(EditProfileView.class)); userMenuItem.getSubMenu().addItem("Admin-Einstellungen"); - userMenuItem.getSubMenu().addItem("Abmelden", e -> securityService.logout()); + userMenuItem.getSubMenu().addItem("Abmelden", e -> openLogoutConfirmDialog()); // Update function for username and avatar Runnable updateUserInfo = () -> { @@ -151,4 +152,17 @@ public final class AdminLayout extends AppLayout { return userMenu; } + + private void openLogoutConfirmDialog() { + var dialog = DialogStylingHelper.createConfirmationDialog( + getTranslation("logout.confirm.title"), + getTranslation("logout.confirm.message"), + "460px", + getTranslation("button.cancel"), + getTranslation("nav.logout"), + securityService::logout, + com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY, + com.vaadin.flow.component.button.ButtonVariant.LUMO_ERROR); + dialog.open(); + } } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java index e374f39..3959811 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java @@ -27,6 +27,7 @@ import com.vaadin.flow.server.auth.AnonymousAllowed; import com.vaadin.flow.shared.Registration; import de.assecutor.votianlt.model.User; import de.assecutor.votianlt.model.Language; +import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper; import de.assecutor.votianlt.pages.service.AppUserService; import de.assecutor.votianlt.pages.view.EditProfileView; import de.assecutor.votianlt.security.SecurityService; @@ -327,8 +328,7 @@ public final class MainLayout extends AppLayout { // Profil anzeigen mit Navigation userMenuItem.getSubMenu().addItem(getTranslation("nav.showprofile"), e -> UI.getCurrent().navigate(EditProfileView.class)); - userMenuItem.getSubMenu().addItem(getTranslation("nav.settings")); - userMenuItem.getSubMenu().addItem(getTranslation("nav.logout"), e -> securityService.logout()); + userMenuItem.getSubMenu().addItem(getTranslation("nav.logout"), e -> openLogoutConfirmDialog()); // Update-Funktion für Benutzername und Avatar Runnable updateUserInfo = () -> { @@ -344,6 +344,19 @@ public final class MainLayout extends AppLayout { return userMenu; } + private void openLogoutConfirmDialog() { + var dialog = DialogStylingHelper.createConfirmationDialog( + getTranslation("logout.confirm.title"), + getTranslation("logout.confirm.message"), + "460px", + getTranslation("button.cancel"), + getTranslation("nav.logout"), + securityService::logout, + com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY, + com.vaadin.flow.component.button.ButtonVariant.LUMO_ERROR); + dialog.open(); + } + @Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java index 35febf6..4c235d2 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java @@ -249,7 +249,7 @@ public class AddCustomerView extends Main implements HasDynamicTitle { String value = mail.getValue(); if (value == null || value.trim().isEmpty()) { mail.setInvalid(true); - mail.setErrorMessage(getTranslation("profile.email.required")); + mail.setErrorMessage(getTranslation("profile.validation.email.required")); } else if (!value.contains("@")) { mail.setInvalid(true); mail.setErrorMessage(getTranslation("profile.validation.email.invalid")); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index 7373166..5be8489 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -4,7 +4,6 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.combobox.ComboBox; -import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.UI; @@ -756,86 +755,89 @@ public class AddJobView extends Main implements HasDynamicTitle { if (idx < 0) return; - ConfirmDialog dialog = new ConfirmDialog(); - dialog.setHeader(getTranslation("addjob.station.remove.confirm", idx + 1)); - dialog.setCancelable(true); - dialog.setCancelText(getTranslation("dialog.cancel")); - dialog.setConfirmText(getTranslation("dialog.confirm")); - dialog.addConfirmListener(e -> { - int removeIdx = deliveryStationTilesList.indexOf(tile); - if (removeIdx < 0) - return; + Dialog dialog = DialogStylingHelper.createConfirmationDialog( + getTranslation("addjob.station.remove.confirm", idx + 1), + null, + "460px", + getTranslation("dialog.cancel"), + getTranslation("dialog.confirm"), + () -> { + int removeIdx = deliveryStationTilesList.indexOf(tile); + if (removeIdx < 0) + return; - deliveryStationTilesList.remove(removeIdx); - deliveryStationsState.remove(removeIdx); - deliveryStationsSaveAddress.remove(removeIdx); - deliveryStationsMailState.remove(removeIdx); - deliveryStationsValidatedByGoogle.remove(removeIdx); - deliveryStationTasksState.remove(removeIdx); - Div removedSlot = deliveryStationSlotList.remove(removeIdx); - deliveryStationDistanceChips.remove(removeIdx); - pickupToDeliveryRouteResults.remove(removeIdx); - // Re-index tasks state for remaining stations - Map> reindexed = new HashMap<>(); - for (Map.Entry> entry : deliveryStationTasksState.entrySet()) { - int oldIdx = entry.getKey(); - int newIdx = oldIdx > removeIdx ? oldIdx - 1 : oldIdx; - reindexed.put(newIdx, entry.getValue()); - } - deliveryStationTasksState.clear(); - deliveryStationTasksState.putAll(reindexed); + deliveryStationTilesList.remove(removeIdx); + deliveryStationsState.remove(removeIdx); + deliveryStationsSaveAddress.remove(removeIdx); + deliveryStationsMailState.remove(removeIdx); + deliveryStationsValidatedByGoogle.remove(removeIdx); + deliveryStationTasksState.remove(removeIdx); + Div removedSlot = deliveryStationSlotList.remove(removeIdx); + deliveryStationDistanceChips.remove(removeIdx); + pickupToDeliveryRouteResults.remove(removeIdx); + // Re-index tasks state for remaining stations + Map> reindexed = new HashMap<>(); + for (Map.Entry> entry : deliveryStationTasksState.entrySet()) { + int oldIdx = entry.getKey(); + int newIdx = oldIdx > removeIdx ? oldIdx - 1 : oldIdx; + reindexed.put(newIdx, entry.getValue()); + } + deliveryStationTasksState.clear(); + deliveryStationTasksState.putAll(reindexed); - Map reindexedRoutes = new HashMap<>(); - for (Map.Entry entry : pickupToDeliveryRouteResults.entrySet()) { - int oldIdx = entry.getKey(); - int newIdx = oldIdx > removeIdx ? oldIdx - 1 : oldIdx; - reindexedRoutes.put(newIdx, entry.getValue()); - } - pickupToDeliveryRouteResults.clear(); - pickupToDeliveryRouteResults.putAll(reindexedRoutes); + Map reindexedRoutes = new HashMap<>(); + for (Map.Entry entry : pickupToDeliveryRouteResults.entrySet()) { + int oldIdx = entry.getKey(); + int newIdx = oldIdx > removeIdx ? oldIdx - 1 : oldIdx; + reindexedRoutes.put(newIdx, entry.getValue()); + } + pickupToDeliveryRouteResults.clear(); + pickupToDeliveryRouteResults.putAll(reindexedRoutes); - for (SelectedServiceEntry selectedService : selectedServices) { - Integer stationOrder = selectedService.getDeliveryStationOrder(); - if (stationOrder == null) { - continue; - } - if (stationOrder == removeIdx) { - selectedService.setDeliveryStationOrder(deliveryStationsState.isEmpty() ? null : 0); - } else if (stationOrder > removeIdx) { - selectedService.setDeliveryStationOrder(stationOrder - 1); - } - } - stationsGridContainer.remove(removedSlot); + for (SelectedServiceEntry selectedService : selectedServices) { + Integer stationOrder = selectedService.getDeliveryStationOrder(); + if (stationOrder == null) { + continue; + } + if (stationOrder == removeIdx) { + selectedService.setDeliveryStationOrder(deliveryStationsState.isEmpty() ? null : 0); + } else if (stationOrder > removeIdx) { + selectedService.setDeliveryStationOrder(stationOrder - 1); + } + } + stationsGridContainer.remove(removedSlot); - // Renumber remaining tiles and update click listeners - for (int i = 0; i < deliveryStationTilesList.size(); i++) { - StationTile t = deliveryStationTilesList.get(i); - int newNumber = i + 1; - t.updateStationNumber(newNumber); - t.updateTitle(getTranslation("addjob.station.delivery", newNumber)); - // Update click listener to use correct index - final int newIdx = i; - t.setClickListener(tt -> openDeliveryDialog(tt, newIdx)); - // First station should not be removable - if (i == 0) { - t.setDeleteListener(null); - } - } + // Renumber remaining tiles and update click listeners + for (int i = 0; i < deliveryStationTilesList.size(); i++) { + StationTile t = deliveryStationTilesList.get(i); + int newNumber = i + 1; + t.updateStationNumber(newNumber); + t.updateTitle(getTranslation("addjob.station.delivery", newNumber)); + // Update click listener to use correct index + final int newIdx = i; + t.setClickListener(tt -> openDeliveryDialog(tt, newIdx)); + // First station should not be removable + if (i == 0) { + t.setDeleteListener(null); + } + } - // Ensure "+" button is visible if under max - if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS && addStationButtonSlot.getParent().isEmpty()) { - stationsGridContainer.add(addStationButtonSlot); - } + // Ensure "+" button is visible if under max + if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS + && addStationButtonSlot.getParent().isEmpty()) { + stationsGridContainer.add(addStationButtonSlot); + } - resetRouteInformation(); - resetStationsAppliedState(); - if (servicesGrid != null) { - servicesGrid.getDataProvider().refreshAll(); - } - updatePriceSummary(); - triggerValidation(); - updateTabLabels(); - }); + resetRouteInformation(); + resetStationsAppliedState(); + if (servicesGrid != null) { + servicesGrid.getDataProvider().refreshAll(); + } + updatePriceSummary(); + triggerValidation(); + updateTabLabels(); + }, + ButtonVariant.LUMO_PRIMARY); dialog.open(); } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java index 5c564c2..ba29a0d 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java @@ -23,6 +23,7 @@ import de.assecutor.votianlt.model.Service; import de.assecutor.votianlt.model.User; import de.assecutor.votianlt.model.InvoiceTemplate; import de.assecutor.votianlt.model.invoices.CustomerInvoice; +import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper; import de.assecutor.votianlt.pages.service.CustomerService; import de.assecutor.votianlt.pages.service.UserInvoiceDataService; import de.assecutor.votianlt.repository.CustomerInvoiceRepository; @@ -45,7 +46,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.html.IFrame; @@ -659,9 +659,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter private void showPdfPreviewDialog(byte[] pdfBytes, String templateData, User user) { String title = getTranslation("createinvoice.preview.title"); - Dialog pdfDialog = new Dialog(); - pdfDialog.setHeaderTitle(title); - pdfDialog.setWidth("90vw"); + Dialog pdfDialog = DialogStylingHelper.createStyledDialog(title, "90vw"); pdfDialog.setHeight("90vh"); IFrame pdfFrame = new IFrame(); @@ -676,19 +674,19 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); Button saveButton = new Button(getTranslation("createinvoice.button.save"), e -> { - ConfirmDialog confirm = new ConfirmDialog(); - confirm.setHeader(getTranslation("createinvoice.confirm.save.title")); - confirm.setText(getTranslation("createinvoice.confirm.save.message")); - confirm.setConfirmText(getTranslation("createinvoice.confirm.save.confirm")); - confirm.setConfirmButtonTheme("primary"); - confirm.setCancelText(getTranslation("button.cancel")); - confirm.setCancelable(true); - confirm.addConfirmListener(ev -> saveInvoice(templateData, user, pdfDialog)); + Dialog confirm = DialogStylingHelper.createConfirmationDialog( + getTranslation("createinvoice.confirm.save.title"), + getTranslation("createinvoice.confirm.save.message"), + "560px", + getTranslation("button.cancel"), + getTranslation("createinvoice.confirm.save.confirm"), + () -> saveInvoice(templateData, user, pdfDialog), + ButtonVariant.LUMO_PRIMARY); confirm.open(); }); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS); - pdfDialog.add(pdfFrame); + pdfDialog.add(DialogStylingHelper.wrapContent(pdfFrame, true)); pdfDialog.getFooter().add(closeButton, saveButton); pdfDialog.open(); } @@ -696,9 +694,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter public static void showSavedInvoiceDialog(byte[] pdfBytes, String invoiceNumber, com.vaadin.flow.component.Component parent) { String title = "Rechnung " + invoiceNumber; - Dialog pdfDialog = new Dialog(); - pdfDialog.setHeaderTitle(title); - pdfDialog.setWidth("90vw"); + Dialog pdfDialog = DialogStylingHelper.createStyledDialog(title, "90vw"); pdfDialog.setHeight("90vh"); IFrame pdfFrame = new IFrame(); @@ -720,7 +716,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter Button closeButton = new Button("Schließen", e -> pdfDialog.close()); closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - pdfDialog.add(pdfFrame); + pdfDialog.add(DialogStylingHelper.wrapContent(pdfFrame, true)); pdfDialog.getFooter().add(downloadButton, closeButton); pdfDialog.open(); } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java index c8d4dd3..ac43ea4 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java @@ -145,7 +145,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle TextField faxField = new TextField(getTranslation("profile.fax")); TextField mobileField = new TextField(getTranslation("profile.mobile")); - EmailField emailField = new EmailField(getTranslation("profile.email.required")); + EmailField emailField = new EmailField(getTranslation("profile.email")); emailField.addBlurListener(e -> validateEmailField(emailField)); TextField streetField = new TextField(getTranslation("profile.street")); @@ -882,9 +882,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle String dataUrl = "data:application/pdf;base64," + base64Pdf; // Create dialog - Dialog pdfDialog = new Dialog(); - pdfDialog.setHeaderTitle(getTranslation("profile.invoice.pdf.preview")); - pdfDialog.setWidth("90vw"); + Dialog pdfDialog = DialogStylingHelper.createStyledDialog(getTranslation("profile.invoice.pdf.preview"), "90vw"); pdfDialog.setHeight("90vh"); // Create a Div to hold the PDF viewer @@ -914,7 +912,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle }); downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - pdfDialog.add(pdfContainer); + pdfDialog.add(DialogStylingHelper.wrapContent(pdfContainer, true)); pdfDialog.getFooter().add(downloadButton, closeButton); pdfDialog.open(); } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/ImprintView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/ImprintView.java index 8ff5e1b..3fe974c 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/ImprintView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/ImprintView.java @@ -1,9 +1,12 @@ package de.assecutor.votianlt.pages.view; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.html.Anchor; import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H5; +import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; -import org.springframework.core.io.ClassPathResource; import com.vaadin.flow.router.HasDynamicTitle; import com.vaadin.flow.router.Route; import jakarta.annotation.security.PermitAll; @@ -25,29 +28,82 @@ public class ImprintView extends VerticalLayout implements HasDynamicTitle { ViewToolbar toolbar = new ViewToolbar(getTranslation("page.title.imprint")); content.add(toolbar); - try { - // Load HTML content from resources - ClassPathResource resource = new ClassPathResource("html/imprint.html"); - String htmlContent = new String(resource.getInputStream().readAllBytes()); + Div imprintCard = new Div(); + imprintCard.addClassNames("form-card", "form-shell"); + imprintCard.getStyle().set("max-width", "800px").set("margin", "0 auto"); - // Create a Div to hold the HTML content - Div imprintDiv = new Div(); - imprintDiv.addClassNames("form-card", "form-shell"); - imprintDiv.getElement().setProperty("innerHTML", htmlContent); + VerticalLayout imprintContent = new VerticalLayout(); + imprintContent.setPadding(false); + imprintContent.setSpacing(false); + imprintContent.getStyle().set("gap", "var(--lumo-space-l)"); - content.add(imprintDiv); + imprintContent.add( + createSection("Assecutor Data Service GmbH", + createLinesBlock(createLine("Gerhart-Hauptmann-Weg 14"), createLine("21502 Geesthacht"), + createLine(getTranslation("imprint.country")), + createLine(getTranslation("imprint.phone") + ": +49 40 18 123 771 0"), + createEmailLine())), + createSection(getTranslation("imprint.management"), + createLinesBlock(createLine("Carsten Annacker"), createLine("Gunnar Timm"))), + createSection(getTranslation("imprint.registeredoffice"), + createLinesBlock(createLine("Gerhart-Hauptmann-Weg 14, 21502 Geesthacht"))), + createSection(getTranslation("imprint.commercialregister"), + createLinesBlock(createLine("HRB 8595 HL"))), + createSection(getTranslation("imprint.vatid"), createLinesBlock(createLine("DE261094748"))), + createSection(getTranslation("imprint.imagecredits"), + createLinesBlock(createSectionHeading(getTranslation("imprint.backgroundimage")), + createLine("MAN Financial Services (EURO-Leasing), flickr"), + createLine( + "(Creative Commons, Attribution-ShareAlike 2.0 Generic (CC BY-SA 2.0))"), + createExternalLink( + "https://www.flickr.com/photos/mbwa_pr/15969764443/in/album-72157632488355514/")))); - } catch (Exception e) { - // Fallback content in case of error - Div errorDiv = new Div(); - errorDiv.addClassNames("form-card", "form-shell"); - errorDiv.setText(getTranslation("imprint.error", e.getMessage())); - content.add(errorDiv); - } + imprintCard.add(imprintContent); + content.add(imprintCard); add(content); } + private Component createSection(String title, Component body) { + Div section = new Div(); + section.getStyle().set("text-align", "left"); + section.add(createSectionHeading(title), body); + return section; + } + + private H5 createSectionHeading(String title) { + H5 heading = new H5(title); + heading.getStyle().set("margin", "0 0 var(--lumo-space-s) 0"); + return heading; + } + + private Div createLinesBlock(Component... lines) { + Div block = new Div(); + block.getStyle().set("display", "flex").set("flex-direction", "column").set("gap", "4px"); + block.add(lines); + return block; + } + + private Span createLine(String text) { + Span line = new Span(text); + line.getStyle().set("display", "block"); + return line; + } + + private Div createEmailLine() { + Div line = new Div(); + line.getStyle().set("display", "block"); + line.add(new Span(getTranslation("imprint.email") + ": ")); + line.add(new Anchor("mailto:ahoi@assecutor.de", "ahoi@assecutor.de")); + return line; + } + + private Anchor createExternalLink(String href) { + Anchor link = new Anchor(href, href); + link.setTarget("_blank"); + return link; + } + @Override public String getPageTitle() { return getTranslation("page.title.imprint"); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java index ca51367..ab349fa 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java @@ -16,7 +16,6 @@ import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.html.Input; import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.html.IFrame; -import com.vaadin.flow.server.StreamResource; import elemental.json.JsonValue; import elemental.json.JsonType; import com.vaadin.flow.component.upload.Upload; @@ -306,15 +305,8 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi } private void showPdfInDialog(byte[] pdfBytes) { - // Create a stream resource for the PDF - StreamResource resource = new StreamResource("preview.pdf", () -> new java.io.ByteArrayInputStream(pdfBytes)); - resource.setContentType("application/pdf"); - resource.setCacheTime(0); - - // Create dialog - Dialog pdfDialog = new Dialog(); - pdfDialog.setHeaderTitle(getTranslation("invoicegenerator.pdf.preview.title")); - pdfDialog.setWidth("90vw"); + Dialog pdfDialog = DialogStylingHelper.createStyledDialog(getTranslation("invoicegenerator.pdf.preview.title"), + "90vw"); pdfDialog.setHeight("90vh"); // Create a Div to hold the PDF viewer @@ -347,7 +339,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi }); downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - pdfDialog.add(pdfContainer); + pdfDialog.add(DialogStylingHelper.wrapContent(pdfContainer, true)); pdfDialog.getFooter().add(downloadButton, closeButton); pdfDialog.open(); } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java index bd7b2fd..c3de2cc 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java @@ -20,6 +20,7 @@ import de.assecutor.votianlt.model.JobHistoryType; import de.assecutor.votianlt.model.Barcode; import de.assecutor.votianlt.model.Photo; import de.assecutor.votianlt.model.Signature; +import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper; import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import de.assecutor.votianlt.repository.BarcodeRepository; import de.assecutor.votianlt.repository.JobRepository; @@ -182,8 +183,7 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has Icon typeIcon = getTypeIcon(entry.getChangeType()); typeIcon.getStyle().set("color", getTypeColor(entry.getChangeType())); - Span reason = new Span( - entry.getReason() != null ? entry.getReason() : getTranslation("jobhistory.entry.unknown")); + Span reason = new Span(getLocalizedReason(entry)); reason.addClassName("timeline-reason"); Span timestamp = new Span(formatDateTime(entry.getTimestamp())); @@ -202,8 +202,9 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has cardContent.add(headerRow); // Description - if (entry.getDescription() != null && !entry.getDescription().isBlank()) { - Span description = new Span(entry.getDescription()); + String localizedDescription = getLocalizedDescription(entry); + if (localizedDescription != null && !localizedDescription.isBlank()) { + Span description = new Span(localizedDescription); description.addClassName("timeline-description"); cardContent.add(description); } @@ -252,6 +253,37 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has return card; } + private String getLocalizedReason(JobHistory entry) { + if (entry == null) { + return ""; + } + if (entry.getChangeType() == JobHistoryType.CREATE) { + return getTranslation("jobhistory.entry.create.reason"); + } + return entry.getReason() != null ? entry.getReason() : ""; + } + + private String getLocalizedDescription(JobHistory entry) { + if (entry == null || entry.getDescription() == null || entry.getDescription().isBlank()) { + return entry != null ? entry.getDescription() : ""; + } + if (entry.getChangeType() == JobHistoryType.CREATE) { + return getTranslation("jobhistory.entry.create.description", extractDescriptionValue(entry.getDescription())); + } + return entry.getDescription(); + } + + private String extractDescriptionValue(String description) { + if (description == null || description.isBlank()) { + return ""; + } + int separatorIndex = description.indexOf(':'); + if (separatorIndex < 0 || separatorIndex == description.length() - 1) { + return description.trim(); + } + return description.substring(separatorIndex + 1).trim(); + } + private Icon getTypeIcon(JobHistoryType type) { if (type == null) return new Icon(VaadinIcon.INFO_CIRCLE); @@ -300,15 +332,15 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has private String formatStatus(de.assecutor.votianlt.model.JobStatus status) { if (status == null) - return getTranslation("jobhistory.entry.unknown"); + return ""; return switch (status) { case CREATED -> getTranslation("jobstatus.CREATED"); case IN_PROGRESS -> getTranslation("jobstatus.IN_PROGRESS"); - case PICKUP_SCHEDULED -> "Abholung geplant"; - case PICKED_UP -> "Abgeholt"; - case IN_TRANSIT -> "Unterwegs"; - case DELIVERED -> "Zugestellt"; + case PICKUP_SCHEDULED -> getTranslation("jobhistory.status.pickupscheduled"); + case PICKED_UP -> getTranslation("jobhistory.status.pickedup"); + case IN_TRANSIT -> getTranslation("jobhistory.status.intransit"); + case DELIVERED -> getTranslation("jobhistory.status.delivered"); case COMPLETED -> getTranslation("jobstatus.COMPLETED"); case CANCELLED -> getTranslation("jobstatus.CANCELLED"); }; @@ -393,8 +425,7 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has } private void showEnlargedPhoto(String base64Photo) { - Dialog photoDialog = new Dialog(); - photoDialog.setWidth("80vw"); + Dialog photoDialog = DialogStylingHelper.createStyledDialog(getTranslation("jobhistory.image.alt"), "80vw"); photoDialog.setHeight("80vh"); photoDialog.setModal(true); photoDialog.setCloseOnOutsideClick(true); @@ -412,7 +443,7 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has dialogContent.setJustifyContentMode(VerticalLayout.JustifyContentMode.CENTER); dialogContent.setSizeFull(); - photoDialog.add(dialogContent); + photoDialog.add(DialogStylingHelper.wrapContent(dialogContent, true)); photoDialog.open(); } catch (Exception e) { @@ -549,8 +580,7 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has } private void showEnlargedSignature(String svgContent) { - Dialog signatureDialog = new Dialog(); - signatureDialog.setWidth("60vw"); + Dialog signatureDialog = DialogStylingHelper.createStyledDialog(getTranslation("tasktype.SIGNATURE"), "60vw"); signatureDialog.setHeight("40vh"); signatureDialog.setModal(true); signatureDialog.setCloseOnOutsideClick(true); @@ -567,7 +597,7 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has dialogContent.setPadding(true); dialogContent.add(enlargedSignature); - signatureDialog.add(dialogContent); + signatureDialog.add(DialogStylingHelper.wrapContent(dialogContent, true)); signatureDialog.open(); } catch (Exception e) { diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java index c88e2ad..970452e 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java @@ -60,7 +60,6 @@ import de.assecutor.votianlt.service.LocationService; import de.assecutor.votianlt.service.MessageService; import de.assecutor.votianlt.service.TaskAssignmentService; import de.assecutor.votianlt.util.DateTimeFormatUtil; -import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import jakarta.annotation.security.RolesAllowed; import org.bson.types.ObjectId; import org.springframework.beans.factory.annotation.Value; @@ -135,8 +134,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has if (parameter == null || parameter.isBlank()) { content.removeAll(); removeAll(); - add(new ViewToolbar("Zusammenfassung")); - content.add(new Span("Fehler: Keine Job-ID angegeben")); + add(new ViewToolbar(getTranslation("jobsummary.title"))); + content.add(new Span(getTranslation("jobsummary.error.noid"))); add(content); return; } @@ -146,8 +145,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has } catch (Exception e) { content.removeAll(); removeAll(); - add(new ViewToolbar("Zusammenfassung")); - content.add(new Span("Fehler: Ungültige Job-ID Format: " + parameter)); + add(new ViewToolbar(getTranslation("jobsummary.title"))); + content.add(new Span(getTranslation("jobsummary.error.invalidid", parameter))); add(content); return; } @@ -186,8 +185,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has Job job = jobRepository.findById(currentJobId).orElse(null); if (job == null) { - add(new ViewToolbar("Zusammenfassung")); - content.add(new Span("Fehler: Job mit ID " + currentJobId.toHexString() + " nicht gefunden")); + add(new ViewToolbar(getTranslation("jobsummary.title"))); + content.add(new Span(getTranslation("jobsummary.error.notfound", currentJobId.toHexString()))); add(content); return; } @@ -221,7 +220,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has }); // Add toolbar with both buttons in top right (Send Message button on the left) - add(new ViewToolbar("Zusammenfassung", sendMessageButton, jobHistoryButton)); + add(new ViewToolbar(getTranslation("jobsummary.title"), sendMessageButton, jobHistoryButton)); List cargo = cargoItemRepository.findByJobId(currentJobId); List tasks = taskAssignmentService.findTasksForJob(job); @@ -325,33 +324,33 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has new Icon(VaadinIcon.CHECK_CIRCLE)); completeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS); completeButton.addClickListener(e -> { - ConfirmDialog dialog = new ConfirmDialog(); - dialog.setHeader(getTranslation("jobsummary.dialog.complete.title")); - dialog.setText(getTranslation("jobsummary.dialog.complete.text", job.getJobNumber())); - dialog.setCancelable(true); - dialog.setCancelText(getTranslation("jobsummary.dialog.complete.cancel")); - dialog.setConfirmText(getTranslation("jobsummary.dialog.complete.confirm")); - dialog.setConfirmButtonTheme("primary"); - dialog.addConfirmListener(ev -> { - try { - JobStatus oldStatus = job.getStatus(); - job.setStatus(JobStatus.COMPLETED); - job.setUpdatedAt(LocalDateTime.now()); - jobRepository.save(job); - jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, "Manuell"); - Notification - .show(getTranslation("jobsummary.notification.completed", job.getJobNumber()), 3000, - Notification.Position.BOTTOM_END) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - // Re-render the page - getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString())); - } catch (Exception ex) { - Notification - .show(getTranslation("jobsummary.notification.complete.error", ex.getMessage()), 5000, - Notification.Position.BOTTOM_END) - .addThemeVariants(NotificationVariant.LUMO_ERROR); - } - }); + Dialog dialog = DialogStylingHelper.createConfirmationDialog( + getTranslation("jobsummary.dialog.complete.title"), + getTranslation("jobsummary.dialog.complete.text", job.getJobNumber()), + "560px", + getTranslation("jobsummary.dialog.complete.cancel"), + getTranslation("jobsummary.dialog.complete.confirm"), + () -> { + try { + JobStatus oldStatus = job.getStatus(); + job.setStatus(JobStatus.COMPLETED); + job.setUpdatedAt(LocalDateTime.now()); + jobRepository.save(job); + jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, "Manuell"); + Notification + .show(getTranslation("jobsummary.notification.completed", job.getJobNumber()), + 3000, Notification.Position.BOTTOM_END) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + // Re-render the page + getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString())); + } catch (Exception ex) { + Notification + .show(getTranslation("jobsummary.notification.complete.error", + ex.getMessage()), 5000, Notification.Position.BOTTOM_END) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + } + }, + ButtonVariant.LUMO_PRIMARY); dialog.open(); }); @@ -896,8 +895,9 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has // Gespeicherte Dauer formatieren int hours = savedDuration / 3600; int minutes = (savedDuration % 3600) / 60; - String savedDurationText = hours > 0 ? String.format("%d Std. %d Min.", hours, minutes) - : String.format("%d Min.", minutes); + String savedDurationText = formatDurationShort(hours, minutes); + String plannedRouteLabel = escapeJs(getTranslation("jobsummary.route.planned")); + String durationLabel = escapeJs(getTranslation("createinvoice.route.duration")); // Build waypoints JS array StringBuilder waypointsJs = new StringBuilder("["); @@ -925,6 +925,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has var hasSavedRouteData = %s; var savedDistance = %s; var savedDurationText = '%s'; + var plannedRouteLabel = '%s'; + var durationLabel = '%s'; var waypoints = %s; var appUserMarker = null; @@ -968,7 +970,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has savedRouteDiv.style.backgroundColor = '#e3f2fd'; savedRouteDiv.style.borderRadius = '4px'; savedRouteDiv.style.fontWeight = 'bold'; - savedRouteDiv.textContent = '📍 Geplante Route: ' + savedDistance.toFixed(1) + ' km • Fahrtzeit: ' + savedDurationText; + savedRouteDiv.textContent = '📍 ' + plannedRouteLabel + ': ' + savedDistance.toFixed(1) + ' km • ' + durationLabel + ': ' + savedDurationText; infoEl.appendChild(savedRouteDiv); } @@ -1075,7 +1077,16 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has .formatted(escapeJs(origin), escapeJs(destination), escapeJs(apiKey), lat, lng, Boolean.toString(hasPosition), escapeJs(appUserId), Boolean.toString(shouldUpdate), Boolean.toString(hasSavedRouteData), savedDistanceStr, escapeJs(savedDurationText), - waypointsJs.toString()); + plannedRouteLabel, durationLabel, waypointsJs.toString()); + } + + private String formatDurationShort(int hours, int minutes) { + String hourUnit = getTranslation("duration.hours.short"); + String minuteUnit = getTranslation("duration.minutes.short"); + if (hours > 0) { + return String.format("%d %s %d %s", hours, hourUnit, minutes, minuteUnit); + } + return String.format("%d %s", minutes, minuteUnit); } // Hilfsfunktion zum einfachen Escapen von JS-Zeichen in Strings diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java index e630f3b..f43a565 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java @@ -693,7 +693,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has private HorizontalLayout createMessageInputArea() { messageInput = new TextArea(); - messageInput.setPlaceholder("Nachricht eingeben..."); + messageInput.setPlaceholder(getTranslation("messagedetails.placeholder")); messageInput.setWidthFull(); messageInput.getStyle().set("min-height", "60px"); messageInput.getStyle().set("max-height", "120px"); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java index 109bb7a..479c877 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java @@ -3,7 +3,7 @@ package de.assecutor.votianlt.pages.view; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.combobox.ComboBox; -import com.vaadin.flow.component.confirmdialog.ConfirmDialog; +import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.datepicker.DatePicker; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.html.Anchor; @@ -21,6 +21,7 @@ import com.vaadin.flow.router.Route; import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.JobStatus; import de.assecutor.votianlt.messaging.MessagingPublisher; +import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper; import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import de.assecutor.votianlt.util.DateTimeFormatUtil; import de.assecutor.votianlt.repository.CustomerInvoiceRepository; @@ -214,53 +215,54 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle { } private void showCompleteJobDialog(Job job) { - ConfirmDialog dialog = new ConfirmDialog(); - dialog.setHeader(getTranslation("jobs.dialog.complete.title")); - dialog.setText(getTranslation("jobs.dialog.complete.text", job.getJobNumber())); - dialog.setCancelable(true); - dialog.setCancelText(getTranslation("button.cancel")); - dialog.setConfirmText(getTranslation("jobs.dialog.complete.confirm")); - dialog.setConfirmButtonTheme("primary"); - dialog.addConfirmListener(e -> { - try { - JobStatus oldStatus = job.getStatus(); - job.setStatus(JobStatus.COMPLETED); - job.setUpdatedAt(LocalDateTime.now()); - jobRepository.save(job); - jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, "Manuell"); - Notification.show(getTranslation("jobs.notification.completed", job.getJobNumber()), 3000, - Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_SUCCESS); - loadData(); - } catch (Exception ex) { - Notification.show(getTranslation("jobs.notification.complete.error", ex.getMessage()), 5000, - Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_ERROR); - } - }); + Dialog dialog = DialogStylingHelper.createConfirmationDialog( + getTranslation("jobs.dialog.complete.title"), + getTranslation("jobs.dialog.complete.text", job.getJobNumber()), + "560px", + getTranslation("button.cancel"), + getTranslation("jobs.dialog.complete.confirm"), + () -> { + try { + JobStatus oldStatus = job.getStatus(); + job.setStatus(JobStatus.COMPLETED); + job.setUpdatedAt(LocalDateTime.now()); + jobRepository.save(job); + jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, "Manuell"); + Notification.show(getTranslation("jobs.notification.completed", job.getJobNumber()), 3000, + Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_SUCCESS); + loadData(); + } catch (Exception ex) { + Notification.show(getTranslation("jobs.notification.complete.error", ex.getMessage()), 5000, + Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_ERROR); + } + }, + ButtonVariant.LUMO_PRIMARY); dialog.open(); } private void showDeleteJobDialog(Job job) { - ConfirmDialog dialog = new ConfirmDialog(); - dialog.setHeader(getTranslation("jobs.dialog.delete.title")); - dialog.setText(getTranslation("jobs.dialog.delete.text", job.getJobNumber())); - dialog.setCancelable(true); - dialog.setCancelText(getTranslation("button.cancel")); - dialog.setConfirmText(getTranslation("button.delete")); - dialog.setConfirmButtonTheme("error primary"); - dialog.addConfirmListener(e -> { - try { - // Notify client before deleting if online - notifyClientJobDeleted(job); + Dialog dialog = DialogStylingHelper.createConfirmationDialog( + getTranslation("jobs.dialog.delete.title"), + getTranslation("jobs.dialog.delete.text", job.getJobNumber()), + "560px", + getTranslation("button.cancel"), + getTranslation("button.delete"), + () -> { + try { + // Notify client before deleting if online + notifyClientJobDeleted(job); - jobRepository.delete(job); - Notification.show(getTranslation("jobs.notification.deleted", job.getJobNumber()), 3000, - Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_SUCCESS); - loadData(); - } catch (Exception ex) { - Notification.show(getTranslation("jobs.notification.delete.error", ex.getMessage()), 5000, - Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_ERROR); - } - }); + jobRepository.delete(job); + Notification.show(getTranslation("jobs.notification.deleted", job.getJobNumber()), 3000, + Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_SUCCESS); + loadData(); + } catch (Exception ex) { + Notification.show(getTranslation("jobs.notification.delete.error", ex.getMessage()), 5000, + Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_ERROR); + } + }, + ButtonVariant.LUMO_PRIMARY, + ButtonVariant.LUMO_ERROR); dialog.open(); } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/StartView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/StartView.java index a5745c9..0142670 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/StartView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/StartView.java @@ -20,6 +20,7 @@ import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.BeforeEnterObserver; import com.vaadin.flow.server.auth.AnonymousAllowed; import de.assecutor.votianlt.model.Language; +import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper; import de.assecutor.votianlt.security.SessionAuthenticationService; import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.service.DemoModeService; @@ -241,17 +242,15 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha String currentUser = securityService.getCurrentUsername(); ComboBox userCombo = new ComboBox<>(); userCombo.setPlaceholder(currentUser); - userCombo.setItems("Profil anzeigen", "Einstellungen", "Abmelden"); + userCombo.setItems("Profil anzeigen", "Abmelden"); userCombo.addValueChangeListener(event -> { String value = event.getValue(); if (value != null) { switch (value) { case "Profil anzeigen": break; - case "Einstellungen": - break; case "Abmelden": - securityService.logout(); + openLogoutConfirmDialog(); break; } userCombo.clear(); // Reset selection @@ -450,6 +449,19 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha return footer; } + private void openLogoutConfirmDialog() { + var dialog = DialogStylingHelper.createConfirmationDialog( + getTranslation("logout.confirm.title"), + getTranslation("logout.confirm.message"), + "460px", + getTranslation("button.cancel"), + getTranslation("nav.logout"), + securityService::logout, + ButtonVariant.LUMO_PRIMARY, + ButtonVariant.LUMO_ERROR); + dialog.open(); + } + private void register() { UI.getCurrent().navigate("register"); } diff --git a/backend/src/main/java/de/assecutor/votianlt/repository/MessageRepository.java b/backend/src/main/java/de/assecutor/votianlt/repository/MessageRepository.java index 5b6d2f3..f21d8de 100644 --- a/backend/src/main/java/de/assecutor/votianlt/repository/MessageRepository.java +++ b/backend/src/main/java/de/assecutor/votianlt/repository/MessageRepository.java @@ -9,6 +9,7 @@ import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface MessageRepository extends MongoRepository { @@ -66,6 +67,9 @@ public interface MessageRepository extends MongoRepository { List findByReceiverAndDeliveryStatusOrderByCreatedAtAsc(String receiver, MessageDeliveryStatus deliveryStatus); + Optional findFirstByReceiverAndOriginAndClientMessageId(String receiver, MessageOrigin origin, + String clientMessageId); + /** * Find all undelivered messages (NOTSEND status) for a receiver */ diff --git a/backend/src/main/java/de/assecutor/votianlt/service/MessageService.java b/backend/src/main/java/de/assecutor/votianlt/service/MessageService.java index 3d0110d..f8d413d 100644 --- a/backend/src/main/java/de/assecutor/votianlt/service/MessageService.java +++ b/backend/src/main/java/de/assecutor/votianlt/service/MessageService.java @@ -19,6 +19,7 @@ import org.springframework.stereotype.Service; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Optional; @@ -100,6 +101,16 @@ public class MessageService { * Inbound message payload where receiver = AppUser ID (clientId) */ public Message receiveMessageFromClient(ChatMessageInboundPayload payload) { + String clientMessageId = payload.messageId(); + if (clientMessageId != null && !clientMessageId.isBlank()) { + Optional existingMessage = messageRepository.findFirstByReceiverAndOriginAndClientMessageId( + payload.receiver(), MessageOrigin.CLIENT, clientMessageId); + if (existingMessage.isPresent()) { + sendClientMessageAck(payload.receiver(), clientMessageId, existingMessage.get()); + return existingMessage.get(); + } + } + Message message; MessageContentType contentType = payload.contentType(); if (payload.hasJobContext()) { @@ -109,11 +120,36 @@ public class MessageService { } else { message = new Message(payload.content(), payload.receiver(), MessageOrigin.CLIENT, contentType); } + message.setClientMessageId(clientMessageId); message = saveMessage(message); + sendClientMessageAck(payload.receiver(), clientMessageId, message); eventPublisher.publishEvent(new MessageReceivedEvent(this, message)); return message; } + private void sendClientMessageAck(String receiver, String clientMessageId, Message message) { + if (receiver == null || receiver.isBlank() || clientMessageId == null || clientMessageId.isBlank() + || message == null) { + return; + } + + try { + LinkedHashMap payload = new LinkedHashMap<>(); + payload.put("clientMessageId", clientMessageId); + payload.put("messageId", message.getIdAsString()); + payload.put("createdAt", message.getCreatedAt()); + + byte[] data = objectMapper.writeValueAsString(payload).getBytes(StandardCharsets.UTF_8); + webSocketService.sendToClient(receiver, "message_ack", data).exceptionally(ex -> { + log.debug("[Messaging] Failed to send ACK for client message {} to {}: {}", clientMessageId, receiver, + ex.getMessage()); + return null; + }); + } catch (Exception e) { + log.error("[Messaging] Error sending ACK for client message {}: {}", clientMessageId, e.getMessage()); + } + } + /** * Publish message to topic for the receiver. Only sends if client is connected, * otherwise keeps NOTSEND status. diff --git a/backend/src/main/resources/messages_de.properties b/backend/src/main/resources/messages_de.properties index 784e794..6ae737c 100644 --- a/backend/src/main/resources/messages_de.properties +++ b/backend/src/main/resources/messages_de.properties @@ -18,6 +18,8 @@ nav.users=Benutzer nav.showprofile=Profil anzeigen nav.settings=Einstellungen nav.logout=Abmelden +logout.confirm.title=Abmelden bestätigen +logout.confirm.message=Möchten Sie sich wirklich abmelden? # Profile View profile.title=Profil bearbeiten @@ -200,6 +202,10 @@ common.error=Fehler common.success=Erfolg common.required=Pflichtfeld +# Duration +duration.hours.short=Std. +duration.minutes.short=Min. + # Validation validation.required=Feld ist erforderlich validation.email=Ungültige E-Mail-Adresse @@ -603,6 +609,7 @@ jobsummary.task.photo.maxonly=Maximal {0} Fotos erlaubt jobsummary.task.photo.taken=Aufgenommene Fotos ({0}) jobsummary.task.button.text=Button-Text jobsummary.button.schliessen=Schließen +jobsummary.route.planned=Geplante Route # Jobs jobs.title=Aufträge @@ -730,6 +737,8 @@ statistics.loading=Berechne... # Job Status jobstatus.IN_PROGRESS=In Bearbeitung jobstatus.COMPLETED=Abgeschlossen +jobstatus.CREATED=Erstellt +jobstatus.CANCELLED=Storniert # Task Types tasktype.CONFIRMATION=Bestätigung @@ -832,6 +841,15 @@ start.imprint.company=Assecutor Data Service GmbH start.imprint.address=Ottensener Str. 8, 22525 Hamburg start.imprint.phone=Telefon: +49 40 18 123 771 0 start.imprint.email=E-Mail: ahoi@assecutor.de +imprint.phone=Telefon +imprint.email=E-Mail +imprint.country=Deutschland +imprint.management=Geschäftsführung +imprint.registeredoffice=Firmensitz +imprint.commercialregister=Handelsregister +imprint.vatid=Umsatzsteuer-ID +imprint.imagecredits=Quellenangaben für die verwendeten Bilder und Grafiken +imprint.backgroundimage=Hintergrundbild Startseite start.cta.text=Registrieren Sie sich noch heute und nutzen den kostenfreien Probemonat, um das System auf Herz und Nieren zu testen. start.slogan=Betreiben Sie Ihr Geschäft smart … mit votianLT! start.version=Version @@ -914,6 +932,8 @@ jobhistory.info.createdat=Erstellt am: {0} jobhistory.info.status=Status: {0} jobhistory.count={0} Einträge in der Historie jobhistory.changedby=Geändert von: {0} +jobhistory.entry.create.reason=Job erstellt +jobhistory.entry.create.description=Neuer Job wurde erstellt: {0} # Version version.label=Version diff --git a/backend/src/main/resources/messages_ee.properties b/backend/src/main/resources/messages_ee.properties index c600108..2079d42 100644 --- a/backend/src/main/resources/messages_ee.properties +++ b/backend/src/main/resources/messages_ee.properties @@ -16,6 +16,8 @@ nav.users=Kasutajad nav.showprofile=Kuva profiil nav.settings=Seaded nav.logout=Logi v\u00e4lja +logout.confirm.title=Kinnita v\u00e4ljalogimine +logout.confirm.message=Kas soovite t\u00f5esti v\u00e4lja logida? profile.title=Profiili muutmine profile.language=Keel profile.company=Ettev\u00f5te @@ -181,6 +183,11 @@ common.loading=Laadimine... common.error=Viga common.success=Edukas common.required=Kohustuslik v\u00e4li + +# Duration +duration.hours.short=t +duration.minutes.short=min + validation.required=V\u00e4li on kohustuslik validation.email=Vigane e-posti aadress validation.error=Valideerimise viga @@ -549,6 +556,7 @@ jobsummary.task.photo.maxonly=Maksimaalselt {0} fotot lubatud jobsummary.task.photo.taken=Tehtud fotod ({0}) jobsummary.task.button.text=Nupu tekst jobsummary.button.schliessen=Sulge +jobsummary.route.planned=Planeeritud marsruut jobs.title=Tellimused jobs.filter.search=Otsi jobs.filter.search.placeholder=Otsi tellimuse numbri j\u00e4rgi... @@ -662,6 +670,8 @@ statistics.data.fetched=Andmed on k\u00e4tte saadud statistics.loading=Arvutamine... jobstatus.IN_PROGRESS=T\u00f6\u00f6s jobstatus.COMPLETED=L\u00f5petatud +jobstatus.CREATED=Loodud +jobstatus.CANCELLED=T\u00fchistatud tasktype.CONFIRMATION=Kinnitus tasktype.SIGNATURE=Allkiri tasktype.TODOLIST=\u00dclesannete nimekiri @@ -754,6 +764,15 @@ start.imprint.company=Assecutor Data Service GmbH start.imprint.address=Ottensener Str. 8, 22525 Hamburg start.imprint.phone=Telefon: +49 40 18 123 771 0 start.imprint.email=E-post: ahoi@assecutor.de +imprint.phone=Telefon +imprint.email=E-post +imprint.country=Saksamaa +imprint.management=Juhtkond +imprint.registeredoffice=Registrijärgne asukoht +imprint.commercialregister=Äriregister +imprint.vatid=KMKR number +imprint.imagecredits=Kasutatud piltide ja graafika allikad +imprint.backgroundimage=Avalehe taustapilt start.cta.text=Registreeruge juba t\u00e4na ja kasutage tasuta proovikuud, et s\u00fcsteemi p\u00f5hjalikult testida. start.slogan=Ajage oma \u00e4ri targalt \u2026 votianLT-ga! start.version=Versioon @@ -824,6 +843,8 @@ jobhistory.info.createdat=Loodud: {0} jobhistory.info.status=Olek: {0} jobhistory.count={0} kirjet ajaloos jobhistory.changedby=Muutnud: {0} +jobhistory.entry.create.reason=T\u00f6\u00f6 loodud +jobhistory.entry.create.description=Uus t\u00f6\u00f6 loodi: {0} version.label=Versioon management.placeholder=Haldus management.customers=Kliendid diff --git a/backend/src/main/resources/messages_en.properties b/backend/src/main/resources/messages_en.properties index c36ff92..85f389a 100644 --- a/backend/src/main/resources/messages_en.properties +++ b/backend/src/main/resources/messages_en.properties @@ -18,6 +18,8 @@ nav.users=Users nav.showprofile=Show Profile nav.settings=Settings nav.logout=Log Out +logout.confirm.title=Confirm logout +logout.confirm.message=Do you really want to log out? # Profile View profile.title=Edit Profile @@ -200,6 +202,10 @@ common.error=Error common.success=Success common.required=Required +# Duration +duration.hours.short=hr +duration.minutes.short=min + # Validation validation.required=Field is required validation.email=Invalid email address @@ -603,6 +609,7 @@ jobsummary.task.photo.maxonly=Maximum {0} photos allowed jobsummary.task.photo.taken=Photos taken ({0}) jobsummary.task.button.text=Button Text jobsummary.button.schliessen=Close +jobsummary.route.planned=Planned Route # Jobs jobs.title=Jobs @@ -730,6 +737,8 @@ statistics.loading=Calculating... # Job Status jobstatus.IN_PROGRESS=In Progress jobstatus.COMPLETED=Completed +jobstatus.CREATED=Created +jobstatus.CANCELLED=Cancelled # Task Types tasktype.CONFIRMATION=Confirmation @@ -832,6 +841,15 @@ start.imprint.company=Assecutor Data Service GmbH start.imprint.address=Ottensener Str. 8, 22525 Hamburg start.imprint.phone=Phone: +49 40 18 123 771 0 start.imprint.email=Email: ahoi@assecutor.de +imprint.phone=Phone +imprint.email=Email +imprint.country=Germany +imprint.management=Management +imprint.registeredoffice=Registered Office +imprint.commercialregister=Commercial Register +imprint.vatid=VAT ID +imprint.imagecredits=Image Credits for Pictures and Graphics Used +imprint.backgroundimage=Homepage Background Image start.cta.text=Register today and use the free trial month to put the system through its paces. start.slogan=Run your business smart ... with votianLT! start.version=Version @@ -914,6 +932,8 @@ jobhistory.info.createdat=Created on: {0} jobhistory.info.status=Status: {0} jobhistory.count={0} entries in history jobhistory.changedby=Changed by: {0} +jobhistory.entry.create.reason=Job Created +jobhistory.entry.create.description=New job was created: {0} # Version version.label=Version diff --git a/backend/src/main/resources/messages_es.properties b/backend/src/main/resources/messages_es.properties index 38b7e15..369c152 100644 --- a/backend/src/main/resources/messages_es.properties +++ b/backend/src/main/resources/messages_es.properties @@ -18,6 +18,8 @@ nav.users=Usuarios nav.showprofile=Ver perfil nav.settings=Configuraci\u00f3n nav.logout=Cerrar sesi\u00f3n +logout.confirm.title=Confirmar cierre de sesi\u00f3n +logout.confirm.message=\u00bfRealmente desea cerrar sesi\u00f3n? # Profile View profile.title=Editar perfil @@ -200,6 +202,10 @@ common.error=Error common.success=\u00c9xito common.required=Campo obligatorio +# Duration +duration.hours.short=h +duration.minutes.short=min + # Validation validation.required=El campo es obligatorio validation.email=Direcci\u00f3n de correo electr\u00f3nico no v\u00e1lida @@ -603,6 +609,7 @@ jobsummary.task.photo.maxonly=Se permiten como m\u00e1ximo {0} fotos jobsummary.task.photo.taken=Fotos tomadas ({0}) jobsummary.task.button.text=Texto del bot\u00f3n jobsummary.button.schliessen=Cerrar +jobsummary.route.planned=Ruta planificada # Jobs jobs.title=Pedidos @@ -730,6 +737,8 @@ statistics.loading=Calculando... # Job Status jobstatus.IN_PROGRESS=En proceso jobstatus.COMPLETED=Completado +jobstatus.CREATED=Creado +jobstatus.CANCELLED=Cancelado # Task Types tasktype.CONFIRMATION=Confirmaci\u00f3n @@ -832,6 +841,15 @@ start.imprint.company=Assecutor Data Service GmbH start.imprint.address=Ottensener Str. 8, 22525 Hamburg start.imprint.phone=Tel\u00e9fono: +49 40 18 123 771 0 start.imprint.email=Correo electr\u00f3nico: ahoi@assecutor.de +imprint.phone=Tel\u00e9fono +imprint.email=Correo electr\u00f3nico +imprint.country=Alemania +imprint.management=Direcci\u00f3n +imprint.registeredoffice=Domicilio social +imprint.commercialregister=Registro mercantil +imprint.vatid=ID de IVA +imprint.imagecredits=Fuentes de las im\u00e1genes y gr\u00e1ficos utilizados +imprint.backgroundimage=Imagen de fondo de la p\u00e1gina de inicio start.cta.text=Reg\u00edstrese hoy mismo y utilice el mes de prueba gratuito para probar el sistema a fondo. start.slogan=\u00a1Gestione su negocio de forma inteligente... con votianLT! start.version=Versi\u00f3n @@ -914,6 +932,8 @@ jobhistory.info.createdat=Creado el: {0} jobhistory.info.status=Estado: {0} jobhistory.count={0} entradas en el historial jobhistory.changedby=Modificado por: {0} +jobhistory.entry.create.reason=Pedido creado +jobhistory.entry.create.description=Se ha creado un nuevo pedido: {0} # Version version.label=Versi\u00f3n diff --git a/backend/src/main/resources/messages_fr.properties b/backend/src/main/resources/messages_fr.properties index d99a5e2..d8ed262 100644 --- a/backend/src/main/resources/messages_fr.properties +++ b/backend/src/main/resources/messages_fr.properties @@ -18,6 +18,8 @@ nav.users=Utilisateurs nav.showprofile=Afficher le profil nav.settings=Param\u00e8tres nav.logout=D\u00e9connexion +logout.confirm.title=Confirmer la d\u00e9connexion +logout.confirm.message=Voulez-vous vraiment vous d\u00e9connecter ? # Profile View profile.title=Modifier le profil @@ -200,6 +202,10 @@ common.error=Erreur common.success=Succ\u00e8s common.required=Champ obligatoire +# Duration +duration.hours.short=h +duration.minutes.short=min + # Validation validation.required=Le champ est obligatoire validation.email=Adresse e-mail invalide @@ -603,6 +609,7 @@ jobsummary.task.photo.maxonly=Maximum {0} photos autoris\u00e9es jobsummary.task.photo.taken=Photos prises ({0}) jobsummary.task.button.text=Texte du bouton jobsummary.button.schliessen=Fermer +jobsummary.route.planned=Itin\u00e9raire pr\u00e9vu # Jobs jobs.title=Missions @@ -730,6 +737,8 @@ statistics.loading=Calcul en cours... # Job Status jobstatus.IN_PROGRESS=En cours jobstatus.COMPLETED=Termin\u00e9e +jobstatus.CREATED=Cr\u00e9\u00e9e +jobstatus.CANCELLED=Annul\u00e9e # Task Types tasktype.CONFIRMATION=Confirmation @@ -832,6 +841,15 @@ start.imprint.company=Assecutor Data Service GmbH start.imprint.address=Ottensener Str. 8, 22525 Hamburg start.imprint.phone=Telefon: +49 40 18 123 771 0 start.imprint.email=E-Mail: ahoi@assecutor.de +imprint.phone=T\u00e9l\u00e9phone +imprint.email=E-mail +imprint.country=Allemagne +imprint.management=Direction +imprint.registeredoffice=Si\u00e8ge social +imprint.commercialregister=Registre du commerce +imprint.vatid=ID TVA +imprint.imagecredits=Cr\u00e9dits des images et graphiques utilis\u00e9s +imprint.backgroundimage=Image d'arri\u00e8re-plan de la page d'accueil start.cta.text=Inscrivez-vous d\u00e8s aujourd'hui et profitez du mois d'essai gratuit pour tester le syst\u00e8me en profondeur. start.slogan=G\u00e9rez votre activit\u00e9 intelligemment ... avec votianLT ! start.version=Version @@ -914,6 +932,8 @@ jobhistory.info.createdat=Cr\u00e9\u00e9 le : {0} jobhistory.info.status=Statut : {0} jobhistory.count={0} entr\u00e9es dans l'historique jobhistory.changedby=Modifi\u00e9 par : {0} +jobhistory.entry.create.reason=Mission cr\u00e9\u00e9e +jobhistory.entry.create.description=Nouvelle mission cr\u00e9\u00e9e : {0} # Version version.label=Version diff --git a/backend/src/main/resources/messages_lt.properties b/backend/src/main/resources/messages_lt.properties index 4ad7874..add31f1 100644 --- a/backend/src/main/resources/messages_lt.properties +++ b/backend/src/main/resources/messages_lt.properties @@ -18,6 +18,8 @@ nav.users=Naudotojai nav.showprofile=Rodyti profilį nav.settings=Nustatymai nav.logout=Atsijungti +logout.confirm.title=Patvirtinti atsijungimą +logout.confirm.message=Ar tikrai norite atsijungti? # Profile View profile.title=Redaguoti profilį @@ -200,6 +202,10 @@ common.error=Klaida common.success=Sėkmė common.required=Privalomas laukas +# Duration +duration.hours.short=val. +duration.minutes.short=min. + # Validation validation.required=Laukas yra privalomas validation.email=Neteisingas el. pašto adresas @@ -603,6 +609,7 @@ jobsummary.task.photo.maxonly=Daugiausiai {0} nuotraukų jobsummary.task.photo.taken=Padarytos nuotraukos ({0}) jobsummary.task.button.text=Mygtuko tekstas jobsummary.button.schliessen=Uždaryti +jobsummary.route.planned=Planuotas maršrutas # Jobs jobs.title=Užsakymai @@ -730,6 +737,8 @@ statistics.loading=Skaičiuojama... # Job Status jobstatus.IN_PROGRESS=Vykdomas jobstatus.COMPLETED=Užbaigtas +jobstatus.CREATED=Sukurtas +jobstatus.CANCELLED=Atšauktas # Task Types tasktype.CONFIRMATION=Patvirtinimas @@ -832,6 +841,15 @@ start.imprint.company=Assecutor Data Service GmbH start.imprint.address=Ottensener Str. 8, 22525 Hamburg start.imprint.phone=Telefon: +49 40 18 123 771 0 start.imprint.email=E-Mail: ahoi@assecutor.de +imprint.phone=Telefonas +imprint.email=El. paštas +imprint.country=Vokietija +imprint.management=Vadovybė +imprint.registeredoffice=Buveinė +imprint.commercialregister=Komercinis registras +imprint.vatid=PVM kodas +imprint.imagecredits=Naudotų vaizdų ir grafikos šaltiniai +imprint.backgroundimage=Pradžios puslapio fono paveikslėlis start.cta.text=Užsiregistruokite šiandien ir pasinaudokite nemokamu bandomuoju mėnesiu, kad galėtumėte išbandyti sistemą. start.slogan=Valdykite savo verslą protingai … su votianLT! start.version=Versija @@ -914,6 +932,8 @@ jobhistory.info.createdat=Sukurta: {0} jobhistory.info.status=Būsena: {0} jobhistory.count={0} įrašų istorijoje jobhistory.changedby=Pakeitė: {0} +jobhistory.entry.create.reason=Užsakymas sukurtas +jobhistory.entry.create.description=Sukurtas naujas užsakymas: {0} # Version version.label=Versija diff --git a/backend/src/main/resources/messages_lv.properties b/backend/src/main/resources/messages_lv.properties index beb4c39..01e93a5 100644 --- a/backend/src/main/resources/messages_lv.properties +++ b/backend/src/main/resources/messages_lv.properties @@ -18,6 +18,8 @@ nav.users=Lietotāji nav.showprofile=Rādīt profilu nav.settings=Iestatījumi nav.logout=Izrakstīties +logout.confirm.title=Apstiprināt izrakstīšanos +logout.confirm.message=Vai tiešām vēlaties izrakstīties? # Profile View profile.title=Rediģēt profilu @@ -200,6 +202,10 @@ common.error=Kļūda common.success=Veiksmīgi common.required=Obligāts lauks +# Duration +duration.hours.short=st. +duration.minutes.short=min. + # Validation validation.required=Lauks ir obligāts validation.email=Nederīga e-pasta adrese @@ -603,6 +609,7 @@ jobsummary.task.photo.maxonly=Atļautas ne vairāk kā {0} fotogrāfijas jobsummary.task.photo.taken=Uzņemtās fotogrāfijas ({0}) jobsummary.task.button.text=Pogas teksts jobsummary.button.schliessen=Aizvērt +jobsummary.route.planned=Plānotais maršruts # Jobs jobs.title=Uzdevumi @@ -730,6 +737,8 @@ statistics.loading=Aprēķina... # Job Status jobstatus.IN_PROGRESS=Izpildē jobstatus.COMPLETED=Pabeigts +jobstatus.CREATED=Izveidots +jobstatus.CANCELLED=Atcelts # Task Types tasktype.CONFIRMATION=Apstiprinājums @@ -832,6 +841,15 @@ start.imprint.company=Assecutor Data Service GmbH start.imprint.address=Ottensener Str. 8, 22525 Hamburg start.imprint.phone=Telefon: +49 40 18 123 771 0 start.imprint.email=E-Mail: ahoi@assecutor.de +imprint.phone=Tālrunis +imprint.email=E-pasts +imprint.country=Vācija +imprint.management=Vadība +imprint.registeredoffice=Juridiskā adrese +imprint.commercialregister=Komercreģistrs +imprint.vatid=PVN ID +imprint.imagecredits=Izmantoto attēlu un grafiku avoti +imprint.backgroundimage=Sākumlapas fona attēls start.cta.text=Reģistrējieties jau šodien un izmantojiet bezmaksas izmēģinājuma mēnesi, lai rūpīgi pārbaudītu sistēmu. start.slogan=Vadiet savu biznesu gudri ... ar votianLT! start.version=Versija @@ -914,6 +932,8 @@ jobhistory.info.createdat=Izveidots: {0} jobhistory.info.status=Statuss: {0} jobhistory.count={0} ieraksti vēsturē jobhistory.changedby=Mainīja: {0} +jobhistory.entry.create.reason=Uzdevums izveidots +jobhistory.entry.create.description=Izveidots jauns uzdevums: {0} # Version version.label=Versija diff --git a/backend/src/main/resources/messages_pl.properties b/backend/src/main/resources/messages_pl.properties index 994e000..ba185ba 100644 --- a/backend/src/main/resources/messages_pl.properties +++ b/backend/src/main/resources/messages_pl.properties @@ -18,6 +18,8 @@ nav.users=U\u017cytkownicy nav.showprofile=Poka\u017c profil nav.settings=Ustawienia nav.logout=Wyloguj si\u0119 +logout.confirm.title=Potwierd\u017a wylogowanie +logout.confirm.message=Czy na pewno chcesz si\u0119 wylogowa\u0107? # Profile View profile.title=Edytuj profil @@ -200,6 +202,10 @@ common.error=B\u0142\u0105d common.success=Sukces common.required=Pole wymagane +# Duration +duration.hours.short=godz. +duration.minutes.short=min. + # Validation validation.required=Pole jest wymagane validation.email=Nieprawid\u0142owy adres e-mail @@ -603,6 +609,7 @@ jobsummary.task.photo.maxonly=Dozwolone maksymalnie {0} zdj\u0119\u0107 jobsummary.task.photo.taken=Wykonane zdj\u0119cia ({0}) jobsummary.task.button.text=Tekst przycisku jobsummary.button.schliessen=Zamknij +jobsummary.route.planned=Planowana trasa # Jobs jobs.title=Zlecenia @@ -730,6 +737,8 @@ statistics.loading=Obliczanie... # Job Status jobstatus.IN_PROGRESS=W trakcie realizacji jobstatus.COMPLETED=Zako\u0144czone +jobstatus.CREATED=Utworzone +jobstatus.CANCELLED=Anulowane # Task Types tasktype.CONFIRMATION=Potwierdzenie @@ -832,6 +841,15 @@ start.imprint.company=Assecutor Data Service GmbH start.imprint.address=Ottensener Str. 8, 22525 Hamburg start.imprint.phone=Telefon: +49 40 18 123 771 0 start.imprint.email=E-Mail: ahoi@assecutor.de +imprint.phone=Telefon +imprint.email=E-mail +imprint.country=Niemcy +imprint.management=Zarz\u0105d +imprint.registeredoffice=Siedziba firmy +imprint.commercialregister=Rejestr handlowy +imprint.vatid=Numer VAT +imprint.imagecredits=\u0179r\u00f3d\u0142a wykorzystanych zdj\u0119\u0107 i grafik +imprint.backgroundimage=Obraz t\u0142a strony startowej start.cta.text=Zarejestruj si\u0119 ju\u017c dzi\u015b i skorzystaj z bezp\u0142atnego miesi\u0105ca pr\u00f3bnego, aby dok\u0142adnie przetestowa\u0107 system. start.slogan=Prowad\u017a sw\u00f3j biznes m\u0105drze \u2026 z votianLT! start.version=Wersja @@ -914,6 +932,8 @@ jobhistory.info.createdat=Utworzono dnia: {0} jobhistory.info.status=Status: {0} jobhistory.count={0} wpis\u00f3w w historii jobhistory.changedby=Zmienione przez: {0} +jobhistory.entry.create.reason=Zlecenie utworzone +jobhistory.entry.create.description=Utworzono nowe zlecenie: {0} # Version version.label=Wersja diff --git a/backend/src/main/resources/messages_ru.properties b/backend/src/main/resources/messages_ru.properties index a57658a..78ec4a7 100644 --- a/backend/src/main/resources/messages_ru.properties +++ b/backend/src/main/resources/messages_ru.properties @@ -18,6 +18,8 @@ nav.users=Пользователи nav.showprofile=Показать профиль nav.settings=Настройки nav.logout=Выйти +logout.confirm.title=Подтвердите выход +logout.confirm.message=Вы действительно хотите выйти? # Profile View profile.title=Редактирование профиля @@ -200,6 +202,10 @@ common.error=Ошибка common.success=Успех common.required=Обязательное поле +# Duration +duration.hours.short=ч +duration.minutes.short=мин. + # Validation validation.required=Поле обязательно для заполнения validation.email=Недействительный адрес электронной почты @@ -603,6 +609,7 @@ jobsummary.task.photo.maxonly=Максимум {0} фото разрешено jobsummary.task.photo.taken=Сделанные фотографии ({0}) jobsummary.task.button.text=Текст кнопки jobsummary.button.schliessen=Закрыть +jobsummary.route.planned=Запланированный маршрут # Jobs jobs.title=Заказы @@ -730,6 +737,8 @@ statistics.loading=Вычисление... # Job Status jobstatus.IN_PROGRESS=В обработке jobstatus.COMPLETED=Завершено +jobstatus.CREATED=Создано +jobstatus.CANCELLED=Отменено # Task Types tasktype.CONFIRMATION=Подтверждение @@ -832,6 +841,15 @@ start.imprint.company=Assecutor Data Service GmbH start.imprint.address=Ottensener Str. 8, 22525 Hamburg start.imprint.phone=Telefon: +49 40 18 123 771 0 start.imprint.email=E-Mail: ahoi@assecutor.de +imprint.phone=Телефон +imprint.email=Эл. почта +imprint.country=Германия +imprint.management=Руководство +imprint.registeredoffice=Юридический адрес +imprint.commercialregister=Торговый реестр +imprint.vatid=Номер НДС +imprint.imagecredits=Источники использованных изображений и графики +imprint.backgroundimage=Фоновое изображение главной страницы start.cta.text=Зарегистрируйтесь сегодня и воспользуйтесь бесплатным пробным месяцем, чтобы тщательно протестировать систему. start.slogan=Ведите свой бизнес умно ... с votianLT! start.version=Версия @@ -914,6 +932,8 @@ jobhistory.info.createdat=Создано: {0} jobhistory.info.status=Статус: {0} jobhistory.count={0} записей в истории jobhistory.changedby=Изменено: {0} +jobhistory.entry.create.reason=Заказ создан +jobhistory.entry.create.description=Создан новый заказ: {0} # Version version.label=Версия diff --git a/backend/src/main/resources/messages_tr.properties b/backend/src/main/resources/messages_tr.properties index 589688f..2c9cd79 100644 --- a/backend/src/main/resources/messages_tr.properties +++ b/backend/src/main/resources/messages_tr.properties @@ -18,6 +18,8 @@ nav.users=Kullan\u0131c\u0131lar nav.showprofile=Profili G\u00f6ster nav.settings=Ayarlar nav.logout=\u00c7\u0131k\u0131\u015f +logout.confirm.title=\u00c7\u0131k\u0131\u015f\u0131 onayla +logout.confirm.message=Ger\u00e7ekten \u00e7\u0131k\u0131\u015f yapmak istiyor musunuz? # Profile View profile.title=Profili D\u00fczenle @@ -200,6 +202,10 @@ common.error=Hata common.success=Ba\u015far\u0131l\u0131 common.required=Zorunlu Alan +# Duration +duration.hours.short=sa. +duration.minutes.short=dk. + # Validation validation.required=Alan gereklidir validation.email=Ge\u00e7ersiz e-posta adresi @@ -603,6 +609,7 @@ jobsummary.task.photo.maxonly=En fazla {0} foto\u011fraf izin verilir jobsummary.task.photo.taken=\u00c7ekilen Foto\u011fraflar ({0}) jobsummary.task.button.text=Buton Metni jobsummary.button.schliessen=Kapat +jobsummary.route.planned=Planlanan Rota # Jobs jobs.title=\u0130\u015fler @@ -730,6 +737,8 @@ statistics.loading=Hesaplan\u0131yor... # Job Status jobstatus.IN_PROGRESS=Devam Ediyor jobstatus.COMPLETED=Tamamland\u0131 +jobstatus.CREATED=Olu\u015fturuldu +jobstatus.CANCELLED=\u0130ptal Edildi # Task Types tasktype.CONFIRMATION=Onay @@ -832,6 +841,15 @@ start.imprint.company=Assecutor Data Service GmbH start.imprint.address=Ottensener Str. 8, 22525 Hamburg start.imprint.phone=Telefon: +49 40 18 123 771 0 start.imprint.email=E-Mail: ahoi@assecutor.de +imprint.phone=Telefon +imprint.email=E-posta +imprint.country=Almanya +imprint.management=Y\u00f6netim +imprint.registeredoffice=\u015eirket merkezi +imprint.commercialregister=Ticaret sicili +imprint.vatid=KDV kimli\u011fi +imprint.imagecredits=Kullan\u0131lan g\u00f6rsel ve grafik kaynaklar\u0131 +imprint.backgroundimage=Ana sayfa arka plan g\u00f6rseli start.cta.text=Bug\u00fcn kay\u0131t olun ve sistemi ba\u015ftan sona test etmek i\u00e7in \u00fccretsiz deneme ay\u0131n\u0131 kullan\u0131n. start.slogan=\u0130\u015finizi ak\u0131ll\u0131ca y\u00f6netin ... votianLT ile! start.version=S\u00fcr\u00fcm @@ -914,6 +932,8 @@ jobhistory.info.createdat=Olu\u015fturulma Tarihi: {0} jobhistory.info.status=Durum: {0} jobhistory.count=Ge\u00e7mi\u015fte {0} kay\u0131t jobhistory.changedby=De\u011fi\u015ftiren: {0} +jobhistory.entry.create.reason=\u0130\u015f olu\u015fturuldu +jobhistory.entry.create.description=Yeni i\u015f olu\u015fturuldu: {0} # Version version.label=S\u00fcr\u00fcm diff --git a/upload.sh b/upload.sh new file mode 100755 index 0000000..ea23626 --- /dev/null +++ b/upload.sh @@ -0,0 +1,16 @@ +find . \ + -type f \ + \( -name "*.java" -o -name "*.dart" -o -name "*.md" \ + -o -name "*.yaml" -o -name "*.yml" -o -name "*.properties" \ + -o -name "*.xml" -o -name "*.gradle" \) \ + ! -path "*/build/*" \ + ! -path "*/.gradle/*" \ + ! -path "*/.dart_tool/*" \ + ! -path "*/node_modules/*" \ + ! -path "*/.git/*" \ + | while read file; do + echo "Upload: $file" + curl -s -X POST "http://192.168.180.16:3001/api/v1/document/upload/votianlt" \ + -H "Authorization: Bearer W25V4A8-5E24XSH-KKNJBX3-WN07B7P" \ + -F "file=@$file" + done