From 704d1e737814577d9c71a485b1e437124be097a9 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Mon, 20 Apr 2026 12:42:56 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Adressbuch=20mit=20Kundennummer,=20Upda?= =?UTF-8?q?te-Flow=20und=20interne=20Eintr=C3=A4ge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Menüpunkt "Kunden" in "Adressbuch" umbenannt und App-Label "Verfügbare Jobs" zu "Auftragsliste" geändert (alle 10 Sprachen) - Fortlaufende Kundennummer (usrId) ab 10000 über neuen SequenceGeneratorService und Counter-Dokument in misc-Collection - Abholung/Lieferstation-Dialog: Änderungen an verknüpften Stammdaten aktualisieren den bestehenden Adressbuch-Eintrag statt einen neuen zu erzeugen; Checkbox-Label wechselt zu "Adresse im Adressbuch aktualisieren" - Geänderte Adressen ohne Checkbox werden als interner Customer (internal=true) gesichert und im Adressbuch ausgeblendet - E-Mail in AddCustomer und in Stations-Dialogen kein Pflichtfeld mehr; "(Login)" aus profile.email entfernt - Manuelles Beenden eines Auftrags öffnet neue Seite JobManualCompleteView statt eines Dialogs --- app/lib/l10n/app_localizations_de.dart | 2 +- app/lib/l10n/app_localizations_en.dart | 2 +- app/lib/l10n/app_localizations_es.dart | 2 +- app/lib/l10n/app_localizations_et.dart | 2 +- app/lib/l10n/app_localizations_fr.dart | 2 +- app/lib/l10n/app_localizations_lt.dart | 2 +- app/lib/l10n/app_localizations_lv.dart | 2 +- app/lib/l10n/app_localizations_pl.dart | 2 +- app/lib/l10n/app_localizations_ru.dart | 2 +- app/lib/l10n/app_localizations_tr.dart | 2 +- app/lib/models/tasks/signature_task.dart | 19 ++- app/lib/services/database_service.dart | 36 +++++ app/lib/task_view.dart | 33 +++- app/lib/tasks/signature_capture_screen.dart | 19 ++- .../controller/MessageController.java | 16 +- .../de/assecutor/votianlt/model/Counter.java | 13 ++ .../de/assecutor/votianlt/model/Customer.java | 6 + .../assecutor/votianlt/model/Signature.java | 6 + .../votianlt/model/task/SignatureTask.java | 16 +- .../ui/component/DeliveryStationDialog.java | 92 +++++++++-- .../ui/component/PickupStationDialog.java | 83 +++++++++- .../pages/domain/CustomerRepository.java | 2 + .../pages/service/AddCustomerService.java | 41 ++++- .../pages/service/CustomerService.java | 2 +- .../service/SequenceGeneratorService.java | 54 +++++++ .../votianlt/pages/view/AddCustomerView.java | 13 +- .../votianlt/pages/view/AddJobView.java | 105 ++++++++---- .../votianlt/pages/view/CustomersView.java | 3 +- .../pages/view/JobManualCompleteView.java | 152 ++++++++++++++++++ .../votianlt/pages/view/JobSummaryView.java | 80 +-------- .../pages/view/ShowCustomersView.java | 3 +- .../repository/SignatureRepository.java | 2 + .../src/main/resources/messages_de.properties | 10 +- .../src/main/resources/messages_ee.properties | 10 +- .../src/main/resources/messages_en.properties | 10 +- .../src/main/resources/messages_es.properties | 10 +- .../src/main/resources/messages_fr.properties | 10 +- .../src/main/resources/messages_lt.properties | 10 +- .../src/main/resources/messages_lv.properties | 10 +- .../src/main/resources/messages_pl.properties | 10 +- .../src/main/resources/messages_ru.properties | 10 +- .../src/main/resources/messages_tr.properties | 10 +- 42 files changed, 720 insertions(+), 196 deletions(-) create mode 100644 backend/src/main/java/de/assecutor/votianlt/model/Counter.java create mode 100644 backend/src/main/java/de/assecutor/votianlt/pages/service/SequenceGeneratorService.java create mode 100644 backend/src/main/java/de/assecutor/votianlt/pages/view/JobManualCompleteView.java diff --git a/app/lib/l10n/app_localizations_de.dart b/app/lib/l10n/app_localizations_de.dart index cd3834e..d05ae92 100644 --- a/app/lib/l10n/app_localizations_de.dart +++ b/app/lib/l10n/app_localizations_de.dart @@ -55,7 +55,7 @@ class AppLocalizationsDe extends AppLocalizations { String get jobs => 'Jobs'; @override - String get availableJobs => 'Verfügbare Jobs'; + String get availableJobs => 'Auftragsliste'; @override String get chats => 'Chats'; diff --git a/app/lib/l10n/app_localizations_en.dart b/app/lib/l10n/app_localizations_en.dart index a26d2b0..063f7e7 100644 --- a/app/lib/l10n/app_localizations_en.dart +++ b/app/lib/l10n/app_localizations_en.dart @@ -55,7 +55,7 @@ class AppLocalizationsEn extends AppLocalizations { String get jobs => 'Jobs'; @override - String get availableJobs => 'Available Jobs'; + String get availableJobs => 'Order List'; @override String get chats => 'Chats'; diff --git a/app/lib/l10n/app_localizations_es.dart b/app/lib/l10n/app_localizations_es.dart index 718ae64..b7d9a90 100644 --- a/app/lib/l10n/app_localizations_es.dart +++ b/app/lib/l10n/app_localizations_es.dart @@ -42,7 +42,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get jobs => 'Trabajos'; @override - String get availableJobs => 'Trabajos Disponibles'; + String get availableJobs => 'Lista de pedidos'; @override String get chats => 'Chats'; @override diff --git a/app/lib/l10n/app_localizations_et.dart b/app/lib/l10n/app_localizations_et.dart index b5fec55..36ff17f 100644 --- a/app/lib/l10n/app_localizations_et.dart +++ b/app/lib/l10n/app_localizations_et.dart @@ -42,7 +42,7 @@ class AppLocalizationsEt extends AppLocalizations { @override String get jobs => 'Tööd'; @override - String get availableJobs => 'Saadaolevad tööd'; + String get availableJobs => 'Tellimuste loend'; @override String get chats => 'Vestlused'; @override diff --git a/app/lib/l10n/app_localizations_fr.dart b/app/lib/l10n/app_localizations_fr.dart index f10fe02..f14442c 100644 --- a/app/lib/l10n/app_localizations_fr.dart +++ b/app/lib/l10n/app_localizations_fr.dart @@ -42,7 +42,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get jobs => 'Emplois'; @override - String get availableJobs => 'Emplois Disponibles'; + String get availableJobs => 'Liste des commandes'; @override String get chats => 'Discussions'; @override diff --git a/app/lib/l10n/app_localizations_lt.dart b/app/lib/l10n/app_localizations_lt.dart index 3b59d8d..1e9e9f0 100644 --- a/app/lib/l10n/app_localizations_lt.dart +++ b/app/lib/l10n/app_localizations_lt.dart @@ -42,7 +42,7 @@ class AppLocalizationsLt extends AppLocalizations { @override String get jobs => 'Darbai'; @override - String get availableJobs => 'Galimi darbai'; + String get availableJobs => 'Užsakymų sąrašas'; @override String get chats => 'Pokalbiai'; @override diff --git a/app/lib/l10n/app_localizations_lv.dart b/app/lib/l10n/app_localizations_lv.dart index e9116d3..29f4114 100644 --- a/app/lib/l10n/app_localizations_lv.dart +++ b/app/lib/l10n/app_localizations_lv.dart @@ -42,7 +42,7 @@ class AppLocalizationsLv extends AppLocalizations { @override String get jobs => 'Darbi'; @override - String get availableJobs => 'Pieejamie darbi'; + String get availableJobs => 'Pasūtījumu saraksts'; @override String get chats => 'Tērzēšanas'; @override diff --git a/app/lib/l10n/app_localizations_pl.dart b/app/lib/l10n/app_localizations_pl.dart index eef2fdb..42ec2d5 100644 --- a/app/lib/l10n/app_localizations_pl.dart +++ b/app/lib/l10n/app_localizations_pl.dart @@ -42,7 +42,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String get jobs => 'Zadania'; @override - String get availableJobs => 'Dostępne Zadania'; + String get availableJobs => 'Lista zleceń'; @override String get chats => 'Czaty'; @override diff --git a/app/lib/l10n/app_localizations_ru.dart b/app/lib/l10n/app_localizations_ru.dart index 17ec8d6..5a8052b 100644 --- a/app/lib/l10n/app_localizations_ru.dart +++ b/app/lib/l10n/app_localizations_ru.dart @@ -42,7 +42,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get jobs => 'Задания'; @override - String get availableJobs => 'Доступные задания'; + String get availableJobs => 'Список заказов'; @override String get chats => 'Чаты'; @override diff --git a/app/lib/l10n/app_localizations_tr.dart b/app/lib/l10n/app_localizations_tr.dart index 8ffe54f..d94df70 100644 --- a/app/lib/l10n/app_localizations_tr.dart +++ b/app/lib/l10n/app_localizations_tr.dart @@ -42,7 +42,7 @@ class AppLocalizationsTr extends AppLocalizations { @override String get jobs => 'İşler'; @override - String get availableJobs => 'Mevcut İşler'; + String get availableJobs => 'Sipariş Listesi'; @override String get chats => 'Sohbetler'; @override diff --git a/app/lib/models/tasks/signature_task.dart b/app/lib/models/tasks/signature_task.dart index 41f4a75..697d505 100644 --- a/app/lib/models/tasks/signature_task.dart +++ b/app/lib/models/tasks/signature_task.dart @@ -2,6 +2,8 @@ import '../task.dart'; // Signature Task class SignatureTask extends Task { + final String? note; + SignatureTask({ required super.id, required super.jobId, @@ -14,11 +16,19 @@ class SignatureTask extends Task { super.title, super.description, super.displayName, + this.note, }); factory SignatureTask.fromJson(Map json) { final commonProps = Task.parseCommonProperties(json); + String? note; + final taskSpecificData = json['taskSpecificData']; + if (taskSpecificData is Map) { + final n = taskSpecificData['note']; + if (n is String) note = n; + } + return SignatureTask( id: commonProps['id'], jobId: commonProps['jobId'], @@ -31,6 +41,7 @@ class SignatureTask extends Task { title: commonProps['title'], description: commonProps['description'], displayName: commonProps['displayName'], + note: note, ); } @@ -47,7 +58,11 @@ class SignatureTask extends Task { 'taskOrder': taskOrder, 'description': description, 'displayName': displayName, - 'taskSpecificData': {'taskType': 'SIGNATURE', 'title': title}, + 'taskSpecificData': { + 'taskType': 'SIGNATURE', + 'title': title, + 'note': note, + }, }; } @@ -64,6 +79,7 @@ class SignatureTask extends Task { String? title, String? description, String? displayName, + String? note, }) { return SignatureTask( id: id ?? this.id, @@ -77,6 +93,7 @@ class SignatureTask extends Task { title: title ?? this.title, description: description ?? this.description, displayName: displayName ?? this.displayName, + note: note ?? this.note, ); } } diff --git a/app/lib/services/database_service.dart b/app/lib/services/database_service.dart index 9136716..579d96e 100644 --- a/app/lib/services/database_service.dart +++ b/app/lib/services/database_service.dart @@ -831,6 +831,42 @@ class DatabaseService { } } + /// Save signature note (Bemerkung) for a task into user_data table + Future saveTaskSignatureNote(String taskId, String note) async { + try { + if (_store == null) { + developer.log('Database not initialized', name: 'DatabaseService'); + return; + } + final key = 'task_signature_note:$taskId'; + await saveKeyValue(key, note); + } catch (e, st) { + developer.log( + 'Error saving task signature note: $e', + name: 'DatabaseService', + ); + developer.log('Stack trace: $st', name: 'DatabaseService'); + } + } + + /// Load signature note (Bemerkung) for a task from user_data table + Future loadTaskSignatureNote(String taskId) async { + try { + if (_store == null) { + developer.log('Database not initialized', name: 'DatabaseService'); + return null; + } + return await loadKeyValue('task_signature_note:$taskId'); + } catch (e, st) { + developer.log( + 'Error loading task signature note: $e', + name: 'DatabaseService', + ); + developer.log('Stack trace: $st', name: 'DatabaseService'); + return null; + } + } + /// Load signature SVG for a task from user_data table Future loadTaskSignature(String taskId) async { try { diff --git a/app/lib/task_view.dart b/app/lib/task_view.dart index 03adbd3..f3d47b4 100644 --- a/app/lib/task_view.dart +++ b/app/lib/task_view.dart @@ -622,10 +622,11 @@ class _TaskViewState extends State { builder: (context) => SignatureCaptureScreen( task: task, - onSignatureCompleted: (String svg) async { + onSignatureCompleted: (String svg, String note) async { try { // Persist SVG only (no PNG) await _databaseService.saveTaskSignature(task.id, svg); + await _databaseService.saveTaskSignatureNote(task.id, note); } catch (e, stackTrace) { developer.log( 'Error saving task signature: $e', @@ -649,6 +650,7 @@ class _TaskViewState extends State { 'signatureSvg': svg, 'svgLength': svg.length, 'hasSignature': true, + 'signatureNote': note, }, ); }, @@ -896,6 +898,10 @@ class _TaskViewState extends State { task.description != null ? localizeKnownText(context, task.description!) : null; + final String? signatureNote = + (task is SignatureTask && task.note != null && task.note!.trim().isNotEmpty) + ? task.note!.trim() + : null; if (displayName?.isNotEmpty == true) { return Column( @@ -906,14 +912,39 @@ class _TaskViewState extends State { const SizedBox(height: 2), Text(description!, style: subtitleStyle), ], + if (signatureNote != null) ...[ + const SizedBox(height: 2), + Text(signatureNote, style: subtitleStyle), + ], ], ); } if (description?.isNotEmpty == true) { + if (signatureNote != null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(description!, style: titleStyle), + const SizedBox(height: 2), + Text(signatureNote, style: subtitleStyle), + ], + ); + } return Text(description!, style: titleStyle); } + if (signatureNote != null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(_getStandardTaskDisplayText(task), style: titleStyle), + const SizedBox(height: 2), + Text(signatureNote, style: subtitleStyle), + ], + ); + } + // Fall back to standard text based on task type return Text(_getStandardTaskDisplayText(task), style: titleStyle); } diff --git a/app/lib/tasks/signature_capture_screen.dart b/app/lib/tasks/signature_capture_screen.dart index fea9bba..46edeb9 100644 --- a/app/lib/tasks/signature_capture_screen.dart +++ b/app/lib/tasks/signature_capture_screen.dart @@ -9,7 +9,7 @@ import '../widgets/offline_banner.dart'; class SignatureCaptureScreen extends StatefulWidget { final SignatureTask task; - final void Function(String svg) onSignatureCompleted; + final void Function(String svg, String note) onSignatureCompleted; const SignatureCaptureScreen({ super.key, @@ -23,6 +23,7 @@ class SignatureCaptureScreen extends StatefulWidget { class _SignatureCaptureScreenState extends State { late final SignatureController _controller; + final TextEditingController _noteController = TextEditingController(); bool _hasSignature = false; bool _isMobilePlatform = false; @@ -84,6 +85,7 @@ class _SignatureCaptureScreenState extends State { void dispose() { _controller.removeListener(_onSignatureChanged); _controller.dispose(); + _noteController.dispose(); _restoreOrientation(); super.dispose(); } @@ -155,14 +157,15 @@ class _SignatureCaptureScreenState extends State { // Build SVG from the captured signature points final String svg = _buildSvgFromPoints(_controller.points); + final String note = _noteController.text.trim(); // Close this screen first to show the updated TaskView quickly if (!mounted) return; _restoreOrientation(); Navigator.of(context).pop(); - // Then notify the caller (SVG only) - widget.onSignatureCompleted(svg); + // Then notify the caller (SVG + Bemerkung) + widget.onSignatureCompleted(svg, note); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( @@ -233,6 +236,16 @@ class _SignatureCaptureScreenState extends State { ), ), ), + const SizedBox(height: 12), + TextField( + controller: _noteController, + decoration: InputDecoration( + labelText: AppLocalizations.of(context).completeTaskNote, + border: const OutlineInputBorder(), + ), + maxLines: 2, + minLines: 1, + ), const SizedBox(height: 16), Row( children: [ diff --git a/backend/src/main/java/de/assecutor/votianlt/controller/MessageController.java b/backend/src/main/java/de/assecutor/votianlt/controller/MessageController.java index dc673fa..e503e56 100644 --- a/backend/src/main/java/de/assecutor/votianlt/controller/MessageController.java +++ b/backend/src/main/java/de/assecutor/votianlt/controller/MessageController.java @@ -8,6 +8,7 @@ import de.assecutor.votianlt.model.AppUser; import de.assecutor.votianlt.model.CargoItem; import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.task.BaseTask; +import de.assecutor.votianlt.model.task.SignatureTask; import de.assecutor.votianlt.pages.service.AppUserService; import de.assecutor.votianlt.repository.AppUserRepository; import de.assecutor.votianlt.repository.CargoItemRepository; @@ -133,6 +134,14 @@ public class MessageController { List jobsWithRelatedData = assignedJobs.stream().map(job -> { List cargoItems = cargoItemRepository.findByJobId(job.getId()); List tasks = taskAssignmentService.findTasksForJob(job); + for (BaseTask task : tasks) { + if (task instanceof SignatureTask signatureTask && task.getId() != null) { + List signatures = signatureRepository.findByTaskIdOrderByCreatedAtDesc(task.getId()); + if (!signatures.isEmpty()) { + signatureTask.setNote(signatures.get(0).getNote()); + } + } + } return new JobWithRelatedDataDTO(job, cargoItems, tasks); }).toList(); @@ -246,13 +255,18 @@ public class MessageController { Object extra = payload.get("extraData"); if (extra instanceof Map extraData) { Object signatureSvgObj = extraData.get("signatureSvg"); + Object signatureNoteObj = extraData.get("signatureNote"); + String signatureNote = signatureNoteObj instanceof String s ? s : null; if (signatureSvgObj instanceof String signatureSvg) { if (!signatureSvg.isBlank()) { String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown"; Signature signatureEntry = new Signature(new ObjectId(taskId.toString()), signatureSvg, - completedBy); + signatureNote, completedBy); signatureRepository.save(signatureEntry); extraDataSummary = "Unterschrift erfasst (SVG, " + signatureSvg.length() + " Zeichen)"; + if (signatureNote != null && !signatureNote.isBlank()) { + extraDataSummary += ", Bemerkung: " + signatureNote; + } } else { extraDataSummary = "Leere Unterschrift"; } diff --git a/backend/src/main/java/de/assecutor/votianlt/model/Counter.java b/backend/src/main/java/de/assecutor/votianlt/model/Counter.java new file mode 100644 index 0000000..0c3a6b6 --- /dev/null +++ b/backend/src/main/java/de/assecutor/votianlt/model/Counter.java @@ -0,0 +1,13 @@ +package de.assecutor.votianlt.model; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Data +@Document(collection = "misc") +public class Counter { + @Id + private String id; + private long sequence; +} diff --git a/backend/src/main/java/de/assecutor/votianlt/model/Customer.java b/backend/src/main/java/de/assecutor/votianlt/model/Customer.java index 45a87f5..693af07 100644 --- a/backend/src/main/java/de/assecutor/votianlt/model/Customer.java +++ b/backend/src/main/java/de/assecutor/votianlt/model/Customer.java @@ -53,4 +53,10 @@ public class Customer { @Field("owner") private ObjectId owner; + + @Field("internal") + private boolean internal; + + @Field("usrId") + private Integer usrId; } \ No newline at end of file diff --git a/backend/src/main/java/de/assecutor/votianlt/model/Signature.java b/backend/src/main/java/de/assecutor/votianlt/model/Signature.java index 317c163..cd2cdda 100644 --- a/backend/src/main/java/de/assecutor/votianlt/model/Signature.java +++ b/backend/src/main/java/de/assecutor/votianlt/model/Signature.java @@ -20,6 +20,7 @@ public class Signature { private ObjectId taskId; private String signatureSvg; + private String note; private LocalDateTime createdAt; private String completedBy; @@ -35,4 +36,9 @@ public class Signature { this.signatureSvg = signatureSvg; this.completedBy = completedBy; } + + public Signature(ObjectId taskId, String signatureSvg, String note, String completedBy) { + this(taskId, signatureSvg, completedBy); + this.note = note; + } } \ No newline at end of file diff --git a/backend/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java b/backend/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java index 22b17b2..22e94fb 100644 --- a/backend/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java +++ b/backend/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java @@ -1,14 +1,20 @@ package de.assecutor.votianlt.model.task; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Transient; @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class SignatureTask extends BaseTask { + @Transient + @JsonIgnore + private String note; + @Override public String getTaskType() { return "SIGNATURE"; @@ -21,11 +27,17 @@ public class SignatureTask extends BaseTask { @Override public Object getTaskSpecificData() { - return new TaskSpecificData(); + return new TaskSpecificData(note); } + @Data + @NoArgsConstructor public class TaskSpecificData { public String taskType = getTaskType(); - // No specific data for signature task + public String note; + + public TaskSpecificData(String note) { + this.note = note; + } } } 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 61c4c1c..bf7b703 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 @@ -56,10 +56,28 @@ public class DeliveryStationDialog extends Dialog { private String zip; private String city; private boolean saveAddress; + private boolean addressDiffersFromCustomer; + private org.bson.types.ObjectId customerId; + + public boolean isAddressDiffersFromCustomer() { + return addressDiffersFromCustomer; + } + + public void setAddressDiffersFromCustomer(boolean addressDiffersFromCustomer) { + this.addressDiffersFromCustomer = addressDiffersFromCustomer; + } private List tasks = new ArrayList<>(); private boolean addressValidatedByGoogle; private AddressValidationResult addressValidationResult; + public org.bson.types.ObjectId getCustomerId() { + return customerId; + } + + public void setCustomerId(org.bson.types.ObjectId customerId) { + this.customerId = customerId; + } + public boolean isAddressValidatedByGoogle() { return addressValidatedByGoogle; } @@ -214,6 +232,7 @@ public class DeliveryStationDialog extends Dialog { private final DeliveryStationTile.TranslationHelper translationHelper; private final java.util.Map companyAddressOptions = new java.util.LinkedHashMap<>(); + private org.bson.types.ObjectId selectedCustomerId; public DeliveryStationDialog(String dialogTitle, List customers, DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener, @@ -512,6 +531,13 @@ public class DeliveryStationDialog extends Dialog { zip.setValue(data.getZip()); if (data.getCity() != null) city.setValue(data.getCity()); + selectedCustomerId = data.getCustomerId(); + if (selectedCustomerId == null && customerSelectedFromOptions) { + Customer matched = companyAddressOptions.get(companyOption); + if (matched != null) { + selectedCustomerId = matched.getId(); + } + } saveAddress.setValue(customerSelectedFromOptions ? false : data.isSaveAddress()); updateSaveAddressState(); @@ -548,10 +574,43 @@ public class DeliveryStationDialog extends Dialog { data.setZip(zip.getValue()); data.setCity(city.getValue()); data.setSaveAddress(saveAddress.getValue()); + data.setCustomerId(selectedCustomerId); + data.setAddressDiffersFromCustomer(computeAddressDiffers()); data.setTasks(new ArrayList<>(tasksState)); return data; } + private boolean computeAddressDiffers() { + boolean hasAnyValue = !isBlank(resolveCompanyValue(company.getValue())) || !isBlank(firstName.getValue()) + || !isBlank(lastName.getValue()) || !isBlank(phone.getValue()) || !isBlank(mail.getValue()) + || !isBlank(street.getValue()) || !isBlank(houseNumber.getValue()) + || !isBlank(addressAddition.getValue()) || !isBlank(zip.getValue()) || !isBlank(city.getValue()); + if (!hasAnyValue) { + return false; + } + if (selectedCustomerId == null) { + return true; + } + Customer linked = findCustomerById(selectedCustomerId); + return linked == null || !matchesCurrentCustomer(linked); + } + + private Customer findCustomerById(org.bson.types.ObjectId id) { + if (id == null) { + return null; + } + for (Customer c : companyAddressOptions.values()) { + if (c != null && id.equals(c.getId())) { + return c; + } + } + return null; + } + + private boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } + private boolean validateRequiredFields() { // Address tab validation boolean addressValid = true; @@ -601,11 +660,9 @@ public class DeliveryStationDialog extends Dialog { String value = mail.getValue(); String normalizedValue = value == null ? "" : value.trim(); boolean empty = normalizedValue.isEmpty(); - boolean required = Boolean.TRUE.equals(saveAddress.getValue()); boolean invalid = !empty && !normalizedValue.contains("@"); - boolean hasError = invalid || (required && empty); - applyErrorStyling(mail, hasError); - return !hasError; + applyErrorStyling(mail, invalid); + return !invalid; } private void applyErrorStyling(com.vaadin.flow.component.Component field, boolean error) { @@ -648,10 +705,12 @@ public class DeliveryStationDialog extends Dialog { companyField.addValueChangeListener(event -> { Customer customer = companyAddressOptions.get(event.getValue()); if (customer == null) { + selectedCustomerId = null; updateSaveAddressState(); return; } + selectedCustomerId = customer.getId(); if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle()) || "Divers".equalsIgnoreCase(customer.getTitle()))) { @@ -680,27 +739,34 @@ public class DeliveryStationDialog extends Dialog { companyField.addCustomValueSetListener(event -> { companyField.setValue(event.getDetail()); + selectedCustomerId = null; updateSaveAddressState(); }); } private void updateSaveAddressState() { Customer selectedCustomer = companyAddressOptions.get(company.getValue()); - boolean customerSelectedFromOptions = selectedCustomer != null && matchesCurrentCustomer(selectedCustomer); + boolean customerDataMatches = selectedCustomer != null && matchesCurrentCustomer(selectedCustomer); - if (customerSelectedFromOptions) { + if (customerDataMatches) { saveAddress.setValue(false); saveAddress.setEnabled(false); + saveAddress.setLabel(translationHelper.getTranslation("addjob.address.save")); updateMailRequirement(); return; } saveAddress.setEnabled(true); + if (selectedCustomerId != null) { + saveAddress.setLabel(translationHelper.getTranslation("addjob.address.update")); + } else { + saveAddress.setLabel(translationHelper.getTranslation("addjob.address.save")); + } updateMailRequirement(); } private void updateMailRequirement() { - mail.setRequiredIndicatorVisible(Boolean.TRUE.equals(saveAddress.getValue())); + mail.setRequiredIndicatorVisible(false); } private String buildCompanyAddressLabel(Customer customer) { @@ -1343,10 +1409,14 @@ public class DeliveryStationDialog extends Dialog { break; case SIGNATURE: - Span info = new Span(translationHelper.getTranslation("addjob.tasks.signature.noconfig")); - info.getStyle().set("color", "var(--lumo-secondary-text-color)"); - info.getStyle().set("font-style", "italic"); - configContainer.add(info); + TextField signatureNoteField = new TextField( + translationHelper.getTranslation("addjob.tasks.signature.notelabel")); + signatureNoteField.setPlaceholder( + translationHelper.getTranslation("addjob.tasks.signature.notelabel.placeholder")); + signatureNoteField.setWidthFull(); + signatureNoteField.setValue(task.getDescription() != null ? task.getDescription() : ""); + signatureNoteField.addValueChangeListener(ev -> task.setDescription(ev.getValue())); + configContainer.add(signatureNoteField); break; case TODOLIST: 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 b716886..ef41bd3 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 @@ -228,6 +228,25 @@ public class PickupStationDialog extends Dialog { public void setCargoItems(List cargoItems) { this.cargoItems = cargoItems != null ? cargoItems : new ArrayList<>(); } + + private org.bson.types.ObjectId customerId; + private boolean addressDiffersFromCustomer; + + public org.bson.types.ObjectId getCustomerId() { + return customerId; + } + + public void setCustomerId(org.bson.types.ObjectId customerId) { + this.customerId = customerId; + } + + public boolean isAddressDiffersFromCustomer() { + return addressDiffersFromCustomer; + } + + public void setAddressDiffersFromCustomer(boolean addressDiffersFromCustomer) { + this.addressDiffersFromCustomer = addressDiffersFromCustomer; + } } public interface SaveListener { @@ -250,6 +269,7 @@ public class PickupStationDialog extends Dialog { private final ComboBox customerComboBox; private final Map customerLabelMap = new LinkedHashMap<>(); private final Map companyCustomerMap = new LinkedHashMap<>(); + private org.bson.types.ObjectId selectedCustomerId; private DatePicker appointmentDatePicker; private TimePicker appointmentTimePicker; private Checkbox digitalProcessingCheckbox; @@ -431,14 +451,17 @@ public class PickupStationDialog extends Dialog { customerComboBox.addValueChangeListener(ev -> { String selected = ev.getValue(); if (selected == null) { + selectedCustomerId = null; updateSaveAddressState(); return; } Customer c = customerLabelMap.get(selected); if (c == null) { + selectedCustomerId = null; updateSaveAddressState(); return; } + selectedCustomerId = c.getId(); if (c.getCompanyName() != null) company.setValue(c.getCompanyName()); else @@ -663,6 +686,15 @@ public class PickupStationDialog extends Dialog { } } + if (data.getCustomerId() != null) { + selectedCustomerId = data.getCustomerId(); + } else { + Customer matched = customerLabelMap.get(data.getCustomerSelection()); + if (matched == null) { + matched = companyCustomerMap.get(normalizeValue(data.getCompany())); + } + selectedCustomerId = matched != null ? matched.getId() : null; + } saveAddress.setValue(data.isSaveAddress()); updateSaveAddressState(); } @@ -681,6 +713,8 @@ public class PickupStationDialog extends Dialog { data.setZip(zip.getValue()); data.setCity(city.getValue()); data.setSaveAddress(saveAddress.getValue()); + data.setCustomerId(selectedCustomerId); + data.setAddressDiffersFromCustomer(computeAddressDiffers()); data.setCustomerSelection(customerComboBox.getValue()); if (appointmentDatePicker != null) { data.setAppointmentDate(appointmentDatePicker.getValue()); @@ -820,6 +854,7 @@ public class PickupStationDialog extends Dialog { companyField.addValueChangeListener(event -> { String selectedCompany = event.getValue(); if (selectedCompany == null || selectedCompany.trim().isEmpty()) { + selectedCustomerId = null; updateSaveAddressState(); return; } @@ -829,6 +864,7 @@ public class PickupStationDialog extends Dialog { if (matchingCustomer.isPresent()) { Customer customer = matchingCustomer.get(); + selectedCustomerId = customer.getId(); if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle()) || "Divers".equalsIgnoreCase(customer.getTitle()))) { @@ -859,6 +895,7 @@ public class PickupStationDialog extends Dialog { companyField.addCustomValueSetListener(event -> { companyField.setValue(event.getDetail()); + selectedCustomerId = null; updateSaveAddressState(); }); } @@ -866,17 +903,23 @@ public class PickupStationDialog extends Dialog { private void updateSaveAddressState() { Customer selectedCustomer = customerLabelMap.get(customerComboBox.getValue()); Customer selectedCompanyCustomer = companyCustomerMap.get(normalizeValue(company.getValue())); - boolean existingCustomerSelected = selectedCustomer != null && matchesCustomer(selectedCustomer); - boolean existingCompanySelected = selectedCompanyCustomer != null && matchesCustomer(selectedCompanyCustomer); + boolean customerDataMatches = selectedCustomer != null && matchesCustomer(selectedCustomer); + boolean companyDataMatches = selectedCompanyCustomer != null && matchesCustomer(selectedCompanyCustomer); - if (existingCustomerSelected || existingCompanySelected) { + if (customerDataMatches || companyDataMatches) { saveAddress.setValue(false); saveAddress.setEnabled(false); + saveAddress.setLabel(translationHelper.getTranslation("addjob.address.save")); updateMailRequirement(); return; } saveAddress.setEnabled(true); + if (selectedCustomerId != null) { + saveAddress.setLabel(translationHelper.getTranslation("addjob.address.update")); + } else { + saveAddress.setLabel(translationHelper.getTranslation("addjob.address.save")); + } updateMailRequirement(); } @@ -906,6 +949,40 @@ public class PickupStationDialog extends Dialog { return value == null ? "" : value.trim(); } + private boolean computeAddressDiffers() { + boolean hasAnyValue = !normalizeValue(company.getValue()).isEmpty() + || !normalizeValue(firstName.getValue()).isEmpty() || !normalizeValue(lastName.getValue()).isEmpty() + || !normalizeValue(phone.getValue()).isEmpty() || !normalizeValue(mail.getValue()).isEmpty() + || !normalizeValue(street.getValue()).isEmpty() || !normalizeValue(houseNumber.getValue()).isEmpty() + || !normalizeValue(addressAddition.getValue()).isEmpty() || !normalizeValue(zip.getValue()).isEmpty() + || !normalizeValue(city.getValue()).isEmpty(); + if (!hasAnyValue) { + return false; + } + if (selectedCustomerId == null) { + return true; + } + Customer linked = findCustomerById(selectedCustomerId); + return linked == null || !matchesCustomer(linked); + } + + private Customer findCustomerById(org.bson.types.ObjectId id) { + if (id == null) { + return null; + } + for (Customer c : customerLabelMap.values()) { + if (c != null && id.equals(c.getId())) { + return c; + } + } + for (Customer c : companyCustomerMap.values()) { + if (c != null && id.equals(c.getId())) { + return c; + } + } + return null; + } + // ============================================ // Appointments & Processing Tab // ============================================ diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/domain/CustomerRepository.java b/backend/src/main/java/de/assecutor/votianlt/pages/domain/CustomerRepository.java index 9da3373..6615015 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/domain/CustomerRepository.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/domain/CustomerRepository.java @@ -13,4 +13,6 @@ public interface CustomerRepository extends MongoRepository Slice findAllBy(Pageable pageable); List findByOwner(ObjectId owner); + + List findByOwnerAndInternalFalse(ObjectId owner); } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/service/AddCustomerService.java b/backend/src/main/java/de/assecutor/votianlt/pages/service/AddCustomerService.java index 3af1a7b..8e308d6 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/service/AddCustomerService.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/service/AddCustomerService.java @@ -12,10 +12,13 @@ import org.springframework.transaction.annotation.Transactional; public class AddCustomerService { private final AddCustomerRepository addCustomerRepository; private final SecurityService securityService; + private final SequenceGeneratorService sequenceGeneratorService; - AddCustomerService(AddCustomerRepository addCustomerRepository, SecurityService securityService) { + AddCustomerService(AddCustomerRepository addCustomerRepository, SecurityService securityService, + SequenceGeneratorService sequenceGeneratorService) { this.addCustomerRepository = addCustomerRepository; this.securityService = securityService; + this.sequenceGeneratorService = sequenceGeneratorService; } public void addCustomer(Customer customer) { @@ -25,6 +28,35 @@ public class AddCustomerService { de.assecutor.votianlt.model.User currentUser = securityService.getCurrentDatabaseUser(); customer.setCreatedBy(currentUser.getId()); customer.setOwner(currentUser.getId()); + if (customer.getUsrId() == null) { + customer.setUsrId(sequenceGeneratorService.nextCustomerNumber()); + } + + addCustomerRepository.save(customer); + } + + public void addInternalCustomer(Customer customer) { + if (customer == null) { + return; + } + customer.setId(null); + customer.setInternal(true); + addCustomer(customer); + } + + public void updateCustomer(Customer customer) { + if (customer == null || customer.getId() == null) { + throw new IllegalArgumentException("Kunden-ID fehlt"); + } + validateCustomer(customer); + + Customer existing = addCustomerRepository.findById(customer.getId()) + .orElseThrow(() -> new IllegalArgumentException("Kunde nicht gefunden")); + customer.setCreatedBy(existing.getCreatedBy()); + customer.setOwner(existing.getOwner()); + if (customer.getUsrId() == null) { + customer.setUsrId(existing.getUsrId()); + } addCustomerRepository.save(customer); } @@ -35,13 +67,10 @@ public class AddCustomerService { } String mail = customer.getMail() != null ? customer.getMail().trim() : ""; - if (mail.isEmpty()) { - throw new IllegalArgumentException("E-Mail-Adresse ist ein Pflichtfeld"); - } - if (!mail.contains("@")) { + if (!mail.isEmpty() && !mail.contains("@")) { throw new IllegalArgumentException("Bitte geben Sie eine gültige E-Mail-Adresse ein"); } - customer.setMail(mail); + customer.setMail(mail.isEmpty() ? null : mail); } } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/service/CustomerService.java b/backend/src/main/java/de/assecutor/votianlt/pages/service/CustomerService.java index 5962432..bf1ac5b 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/service/CustomerService.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/service/CustomerService.java @@ -32,7 +32,7 @@ public class CustomerService { public List findAllForCurrentOwner() { ObjectId ownerId = securityService.getCurrentUserId(); - return todoRepository.findByOwner(ownerId); + return todoRepository.findByOwnerAndInternalFalse(ownerId); } public Customer save(Customer customer) { diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/service/SequenceGeneratorService.java b/backend/src/main/java/de/assecutor/votianlt/pages/service/SequenceGeneratorService.java new file mode 100644 index 0000000..3193661 --- /dev/null +++ b/backend/src/main/java/de/assecutor/votianlt/pages/service/SequenceGeneratorService.java @@ -0,0 +1,54 @@ +package de.assecutor.votianlt.pages.service; + +import de.assecutor.votianlt.model.Counter; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.mongodb.core.FindAndModifyOptions; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.stereotype.Service; + +@Service +public class SequenceGeneratorService { + + public static final String CUSTOMER_NUMBER_SEQ = "customerNumber"; + public static final int CUSTOMER_NUMBER_START = 10000; + + private final MongoTemplate mongoTemplate; + + public SequenceGeneratorService(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + public int nextCustomerNumber() { + return (int) nextSequence(CUSTOMER_NUMBER_SEQ, CUSTOMER_NUMBER_START); + } + + private long nextSequence(String sequenceId, long startValue) { + ensureInitialized(sequenceId, startValue - 1); + Counter updated = mongoTemplate.findAndModify( + Query.query(Criteria.where("_id").is(sequenceId)), + new Update().inc("sequence", 1), + FindAndModifyOptions.options().returnNew(true), + Counter.class); + return updated != null ? updated.getSequence() : startValue; + } + + private void ensureInitialized(String sequenceId, long initialValue) { + boolean exists = mongoTemplate.exists( + Query.query(Criteria.where("_id").is(sequenceId)), + Counter.class); + if (exists) { + return; + } + Counter counter = new Counter(); + counter.setId(sequenceId); + counter.setSequence(initialValue); + try { + mongoTemplate.insert(counter); + } catch (DuplicateKeyException ignored) { + // Ein anderer Thread hat den Counter gleichzeitig angelegt — passt. + } + } +} 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 4c235d2..8ebd2be 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 @@ -81,9 +81,8 @@ public class AddCustomerView extends Main implements HasDynamicTitle { fax = new TextField(getTranslation("profile.fax")); fax.setWidthFull(); - // E-Mail (Pflichtfeld) + // E-Mail (optional) mail = new TextField(getTranslation("profile.email")); - mail.setRequiredIndicatorVisible(true); mail.setWidthFull(); mail.addBlurListener(e -> validateEmail()); @@ -179,8 +178,9 @@ public class AddCustomerView extends Main implements HasDynamicTitle { binder.forField(fax).bind(Customer::getFax, Customer::setFax); - binder.forField(mail).asRequired(getTranslation("profile.validation.email.required")) - .withValidator(email -> email.contains("@"), getTranslation("profile.validation.email.invalid")) + binder.forField(mail) + .withValidator(email -> email == null || email.isBlank() || email.contains("@"), + getTranslation("profile.validation.email.invalid")) .bind(Customer::getMail, Customer::setMail); binder.forField(street).asRequired(getTranslation("profile.validation.street.required")) @@ -247,10 +247,7 @@ public class AddCustomerView extends Main implements HasDynamicTitle { private void validateEmail() { String value = mail.getValue(); - if (value == null || value.trim().isEmpty()) { - mail.setInvalid(true); - mail.setErrorMessage(getTranslation("profile.validation.email.required")); - } else if (!value.contains("@")) { + if (value != null && !value.trim().isEmpty() && !value.contains("@")) { mail.setInvalid(true); mail.setErrorMessage(getTranslation("profile.validation.email.invalid")); } else { 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 12a097b..64cec4d 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 @@ -137,12 +137,16 @@ public class AddJobView extends Main implements HasDynamicTitle { private TextField pickupZip; private TextField pickupCity; private Checkbox savePickupAddress; + private org.bson.types.ObjectId pickupCustomerId; + private boolean pickupAddressDiffers; // Delivery stations as tiles in a 3x3 grid (max 7 delivery + 1 pickup + 1 plus // = 9) private final List deliveryStationTilesList = new ArrayList<>(); private final List deliveryStationsState = new ArrayList<>(); private final List deliveryStationsSaveAddress = new ArrayList<>(); + private final List deliveryStationsCustomerId = new ArrayList<>(); + private final List deliveryStationsAddressDiffers = new ArrayList<>(); private final List deliveryStationsMailState = new ArrayList<>(); private final List
deliveryStationSlotList = new ArrayList<>(); private final List deliveryStationDistanceChips = new ArrayList<>(); @@ -721,6 +725,8 @@ public class AddJobView extends Main implements HasDynamicTitle { // Add empty state for this station deliveryStationsState.add(new DeliveryStation()); deliveryStationsSaveAddress.add(true); + deliveryStationsCustomerId.add(null); + deliveryStationsAddressDiffers.add(false); deliveryStationsMailState.add(null); deliveryStationsValidatedByGoogle.add(false); @@ -769,6 +775,8 @@ public class AddJobView extends Main implements HasDynamicTitle { deliveryStationTilesList.remove(removeIdx); deliveryStationsState.remove(removeIdx); deliveryStationsSaveAddress.remove(removeIdx); + deliveryStationsCustomerId.remove(removeIdx); + deliveryStationsAddressDiffers.remove(removeIdx); deliveryStationsMailState.remove(removeIdx); deliveryStationsValidatedByGoogle.remove(removeIdx); deliveryStationTasksState.remove(removeIdx); @@ -867,6 +875,8 @@ public class AddJobView extends Main implements HasDynamicTitle { pickupZip.setValue(data.getZip() != null ? data.getZip() : ""); pickupCity.setValue(data.getCity() != null ? data.getCity() : ""); savePickupAddress.setValue(data.isSaveAddress()); + pickupCustomerId = data.getCustomerId(); + pickupAddressDiffers = data.isAddressDiffersFromCustomer(); // Sync appointment fields for binder/submit pickupDate.setValue(data.getAppointmentDate()); @@ -913,6 +923,7 @@ public class AddJobView extends Main implements HasDynamicTitle { currentData.setZip(pickupZip.getValue()); currentData.setCity(pickupCity.getValue()); currentData.setSaveAddress(savePickupAddress.getValue()); + currentData.setCustomerId(pickupCustomerId); currentData.setCustomerSelection(customerSelection.getValue()); // Pre-fill pickup-specific fields currentData.setAppointmentDate(pickupDate.getValue()); @@ -1137,6 +1148,14 @@ public class AddJobView extends Main implements HasDynamicTitle { station.setCity(data.getCity()); station.setTasks(data.getTasks() != null ? new ArrayList<>(data.getTasks()) : new ArrayList<>()); deliveryStationsSaveAddress.set(idx, data.isSaveAddress()); + while (deliveryStationsCustomerId.size() <= idx) { + deliveryStationsCustomerId.add(null); + } + deliveryStationsCustomerId.set(idx, data.getCustomerId()); + while (deliveryStationsAddressDiffers.size() <= idx) { + deliveryStationsAddressDiffers.add(false); + } + deliveryStationsAddressDiffers.set(idx, data.isAddressDiffersFromCustomer()); deliveryStationsMailState.set(idx, trimToNull(data.getMail())); // Store tasks for this delivery station @@ -1182,6 +1201,9 @@ public class AddJobView extends Main implements HasDynamicTitle { currentData.setZip(station.getZip()); currentData.setCity(station.getCity()); currentData.setSaveAddress(deliveryStationsSaveAddress.get(actualIndex)); + if (actualIndex < deliveryStationsCustomerId.size()) { + currentData.setCustomerId(deliveryStationsCustomerId.get(actualIndex)); + } if (actualIndex < deliveryStationsValidatedByGoogle.size()) { currentData.setAddressValidatedByGoogle(deliveryStationsValidatedByGoogle.get(actualIndex)); } @@ -1817,39 +1839,60 @@ public class AddJobView extends Main implements HasDynamicTitle { return; } - // NEU: Kunden anlegen, wenn Checkboxen aktiviert + // Kunden anlegen/aktualisieren bzw. intern sichern + Customer pickupCustomer = new Customer(); + pickupCustomer.setCompanyName(pickupCompany.getValue()); + pickupCustomer.setTitle(pickupSalutation.getValue()); + pickupCustomer.setFirstname(pickupFirstName.getValue()); + pickupCustomer.setLastName(pickupLastName.getValue()); + pickupCustomer.setTelephone(pickupPhone.getValue()); + pickupCustomer.setMail(pickupMail); + pickupCustomer.setStreet(pickupStreet.getValue()); + pickupCustomer.setHouseNumber(pickupHouseNumber.getValue()); + pickupCustomer.setAddressAddition(pickupAddressAddition.getValue()); + pickupCustomer.setZip(pickupZip.getValue()); + pickupCustomer.setCity(pickupCity.getValue()); if (savePickupAddress.getValue()) { - Customer pickupCustomer = new Customer(); - pickupCustomer.setCompanyName(pickupCompany.getValue()); - pickupCustomer.setTitle(pickupSalutation.getValue()); - pickupCustomer.setFirstname(pickupFirstName.getValue()); - pickupCustomer.setLastName(pickupLastName.getValue()); - pickupCustomer.setTelephone(pickupPhone.getValue()); - pickupCustomer.setMail(pickupMail); - pickupCustomer.setStreet(pickupStreet.getValue()); - pickupCustomer.setHouseNumber(pickupHouseNumber.getValue()); - pickupCustomer.setAddressAddition(pickupAddressAddition.getValue()); - pickupCustomer.setZip(pickupZip.getValue()); - pickupCustomer.setCity(pickupCity.getValue()); - addCustomerService.addCustomer(pickupCustomer); + if (pickupCustomerId != null) { + pickupCustomer.setId(pickupCustomerId); + addCustomerService.updateCustomer(pickupCustomer); + } else { + addCustomerService.addCustomer(pickupCustomer); + } + } else if (pickupAddressDiffers) { + addCustomerService.addInternalCustomer(pickupCustomer); } - // Save delivery station addresses as customers if checkbox is checked + // Delivery-Stationen: anlegen, aktualisieren oder als intern sichern for (int i = 0; i < deliveryStationsState.size(); i++) { - if (i < deliveryStationsSaveAddress.size() && deliveryStationsSaveAddress.get(i)) { - DeliveryStation ds = deliveryStationsState.get(i); - Customer deliveryCustomer = new Customer(); - deliveryCustomer.setCompanyName(ds.getCompany()); - deliveryCustomer.setTitle(ds.getSalutation()); - deliveryCustomer.setFirstname(ds.getFirstName()); - deliveryCustomer.setLastName(ds.getLastName()); - deliveryCustomer.setTelephone(ds.getPhone()); - deliveryCustomer.setMail(i < deliveryStationsMailState.size() ? deliveryStationsMailState.get(i) : null); - deliveryCustomer.setStreet(ds.getStreet()); - deliveryCustomer.setHouseNumber(ds.getHouseNumber()); - deliveryCustomer.setAddressAddition(ds.getAddressAddition()); - deliveryCustomer.setZip(ds.getZip()); - deliveryCustomer.setCity(ds.getCity()); - addCustomerService.addCustomer(deliveryCustomer); + DeliveryStation ds = deliveryStationsState.get(i); + Customer deliveryCustomer = new Customer(); + deliveryCustomer.setCompanyName(ds.getCompany()); + deliveryCustomer.setTitle(ds.getSalutation()); + deliveryCustomer.setFirstname(ds.getFirstName()); + deliveryCustomer.setLastName(ds.getLastName()); + deliveryCustomer.setTelephone(ds.getPhone()); + deliveryCustomer.setMail(i < deliveryStationsMailState.size() ? deliveryStationsMailState.get(i) : null); + deliveryCustomer.setStreet(ds.getStreet()); + deliveryCustomer.setHouseNumber(ds.getHouseNumber()); + deliveryCustomer.setAddressAddition(ds.getAddressAddition()); + deliveryCustomer.setZip(ds.getZip()); + deliveryCustomer.setCity(ds.getCity()); + boolean saveRequested = i < deliveryStationsSaveAddress.size() + && deliveryStationsSaveAddress.get(i); + org.bson.types.ObjectId existingId = i < deliveryStationsCustomerId.size() + ? deliveryStationsCustomerId.get(i) + : null; + boolean addressDiffers = i < deliveryStationsAddressDiffers.size() + && deliveryStationsAddressDiffers.get(i); + if (saveRequested) { + if (existingId != null) { + deliveryCustomer.setId(existingId); + addCustomerService.updateCustomer(deliveryCustomer); + } else { + addCustomerService.addCustomer(deliveryCustomer); + } + } else if (addressDiffers) { + addCustomerService.addInternalCustomer(deliveryCustomer); } } @@ -2119,6 +2162,8 @@ public class AddJobView extends Main implements HasDynamicTitle { deliveryStationTilesList.remove(idx); deliveryStationsState.remove(idx); deliveryStationsSaveAddress.remove(idx); + deliveryStationsCustomerId.remove(idx); + deliveryStationsAddressDiffers.remove(idx); deliveryStationsMailState.remove(idx); deliveryStationsValidatedByGoogle.remove(idx); deliveryStationTasksState.remove(idx); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/CustomersView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/CustomersView.java index 80e0022..c4245ba 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/CustomersView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/CustomersView.java @@ -36,7 +36,8 @@ public class CustomersView extends Main implements HasDynamicTitle { createBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); todoGrid = new Grid<>(); - todoGrid.setItems(query -> todoService.list(toSpringPageRequest(query)).stream()); + todoGrid.setItems(query -> todoService.list(toSpringPageRequest(query)).stream() + .filter(c -> !c.isInternal())); todoGrid.addColumn(Customer::getCompanyName).setHeader(getTranslation("customers.column.company")); todoGrid.setSizeFull(); todoGrid.addClassName("data-grid"); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/JobManualCompleteView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/JobManualCompleteView.java new file mode 100644 index 0000000..7049e4b --- /dev/null +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/JobManualCompleteView.java @@ -0,0 +1,152 @@ +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.html.Main; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.router.BeforeEvent; +import com.vaadin.flow.router.HasDynamicTitle; +import com.vaadin.flow.router.HasUrlParameter; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.theme.lumo.LumoUtility; +import de.assecutor.votianlt.model.Job; +import de.assecutor.votianlt.model.JobHistoryType; +import de.assecutor.votianlt.model.JobStatus; +import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; +import de.assecutor.votianlt.repository.JobRepository; +import de.assecutor.votianlt.security.SecurityService; +import de.assecutor.votianlt.service.JobHistoryService; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; +import org.bson.types.ObjectId; + +import java.time.LocalDateTime; + +@Route(value = "job_manual_complete", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) +@RolesAllowed("USER") +@Slf4j +public class JobManualCompleteView extends Main implements HasUrlParameter, HasDynamicTitle { + + private final JobRepository jobRepository; + private final JobHistoryService jobHistoryService; + private final SecurityService securityService; + private final VerticalLayout content; + + public JobManualCompleteView(JobRepository jobRepository, JobHistoryService jobHistoryService, + SecurityService securityService) { + this.jobRepository = jobRepository; + this.jobHistoryService = jobHistoryService; + this.securityService = securityService; + + setSizeFull(); + addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, + LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); + addClassName("data-view"); + + add(new ViewToolbar(getTranslation("jobsummary.dialog.manualcomplete.title"))); + + content = new VerticalLayout(); + content.setSpacing(true); + content.setPadding(true); + content.setWidthFull(); + content.addClassNames("form-shell", "form-card"); + add(content); + } + + @Override + public String getPageTitle() { + return getTranslation("jobsummary.dialog.manualcomplete.title"); + } + + @Override + public void setParameter(BeforeEvent event, String parameter) { + content.removeAll(); + + if (parameter == null || parameter.isBlank()) { + content.add(new Span(getTranslation("jobhistory.error.no.id"))); + return; + } + + ObjectId jobId; + try { + jobId = new ObjectId(parameter); + } catch (Exception e) { + content.add(new Span(getTranslation("jobhistory.error.invalid.id", parameter))); + return; + } + + Job job = jobRepository.findById(jobId).orElse(null); + if (job == null) { + content.add(new Span(getTranslation("jobhistory.error.not.found", parameter))); + return; + } + + render(job); + } + + private void render(Job job) { + Span warningText = new Span(getTranslation("jobsummary.dialog.manualcomplete.text", job.getJobNumber())); + warningText.getStyle().set("color", "var(--lumo-error-text-color)"); + + TextArea reasonField = new TextArea(getTranslation("jobsummary.dialog.manualcomplete.reason")); + reasonField.setWidthFull(); + reasonField.setMinHeight("100px"); + reasonField.setRequired(true); + + content.add(warningText, reasonField); + + HorizontalLayout buttonBar = new HorizontalLayout(); + buttonBar.setWidthFull(); + buttonBar.setJustifyContentMode(HorizontalLayout.JustifyContentMode.END); + buttonBar.setSpacing(true); + + Button cancelButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.cancel"), + e -> getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString()))); + + Button confirmButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.confirm")); + confirmButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); + confirmButton.addClickListener(e -> { + String reason = reasonField.getValue(); + if (reason == null || reason.trim().isEmpty()) { + reasonField.setInvalid(true); + reasonField.setErrorMessage(getTranslation("jobsummary.dialog.manualcomplete.reason.required")); + return; + } + + try { + JobStatus oldStatus = job.getStatus(); + job.setStatus(JobStatus.COMPLETED); + job.setUpdatedAt(LocalDateTime.now()); + jobRepository.save(job); + + String currentUser = securityService.getCurrentUsername(); + jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, currentUser); + + String description = String.format("Auftrag manuell beendet von %s. Begründung: %s", + currentUser, reason.trim()); + jobHistoryService.logCustomEvent(job.getId(), + getTranslation("jobsummary.history.manualcomplete.reason"), + description, currentUser, JobHistoryType.STATUS_CHANGE); + + Notification + .show(getTranslation("jobsummary.notification.completed", job.getJobNumber()), 3000, + Notification.Position.BOTTOM_END) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + 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); + } + }); + + buttonBar.add(cancelButton, confirmButton); + content.add(buttonBar); + } +} 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 15136a7..592b1cb 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 @@ -14,7 +14,6 @@ import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; -import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.DetachEvent; import com.vaadin.flow.component.UI; @@ -60,8 +59,6 @@ import de.assecutor.votianlt.service.JobUpdateBroadcaster; import de.assecutor.votianlt.service.LocationService; import de.assecutor.votianlt.service.MessageService; import de.assecutor.votianlt.service.TaskAssignmentService; -import de.assecutor.votianlt.model.JobHistoryType; -import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.util.DateTimeFormatUtil; import jakarta.annotation.security.RolesAllowed; import org.bson.types.ObjectId; @@ -91,7 +88,6 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has private final LocationService locationService; private final ServiceRepository serviceRepository; private final TaskAssignmentService taskAssignmentService; - private final SecurityService securityService; @Value("${app.google.maps.api-key}") private String googleMapsApiKey; @@ -107,8 +103,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService, MessageService messageService, JobHistoryService jobHistoryService, JobUpdateBroadcaster jobUpdateBroadcaster, LocationService locationService, - ServiceRepository serviceRepository, TaskAssignmentService taskAssignmentService, - SecurityService securityService) { + ServiceRepository serviceRepository, TaskAssignmentService taskAssignmentService) { this.jobRepository = jobRepository; this.cargoItemRepository = cargoItemRepository; this.signatureRepository = signatureRepository; @@ -121,7 +116,6 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has this.locationService = locationService; this.serviceRepository = serviceRepository; this.taskAssignmentService = taskAssignmentService; - this.securityService = securityService; setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, @@ -224,7 +218,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has && job.getStatus() != JobStatus.CANCELLED) { manualCompleteButton = new Button(getTranslation("jobsummary.button.manualcomplete")); manualCompleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); - manualCompleteButton.addClickListener(e -> openManualCompleteDialog(job)); + manualCompleteButton.addClickListener(e -> getUI() + .ifPresent(ui -> ui.navigate("job_manual_complete/" + job.getId().toHexString()))); } // Create Job History Button for toolbar @@ -379,75 +374,6 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has } } - private void openManualCompleteDialog(Job job) { - Dialog dialog = DialogStylingHelper.createStyledDialog( - getTranslation("jobsummary.dialog.manualcomplete.title"), "560px"); - - VerticalLayout dialogContent = DialogStylingHelper.createContentLayout("520px"); - - Span warningText = new Span(getTranslation("jobsummary.dialog.manualcomplete.text", job.getJobNumber())); - warningText.getStyle().set("color", "var(--lumo-error-text-color)"); - - TextArea reasonField = new TextArea(getTranslation("jobsummary.dialog.manualcomplete.reason")); - reasonField.setWidthFull(); - reasonField.setMinHeight("100px"); - reasonField.setRequired(true); - - dialogContent.add(warningText, reasonField); - dialog.add(DialogStylingHelper.wrapContent(dialogContent)); - - HorizontalLayout buttonBar = new HorizontalLayout(); - buttonBar.setWidthFull(); - buttonBar.setJustifyContentMode(HorizontalLayout.JustifyContentMode.END); - buttonBar.setSpacing(true); - - Button cancelButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.cancel"), - e -> dialog.close()); - - Button confirmButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.confirm")); - confirmButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); - confirmButton.addClickListener(e -> { - String reason = reasonField.getValue(); - if (reason == null || reason.trim().isEmpty()) { - reasonField.setInvalid(true); - reasonField.setErrorMessage(getTranslation("jobsummary.dialog.manualcomplete.reason.required")); - return; - } - - try { - JobStatus oldStatus = job.getStatus(); - job.setStatus(JobStatus.COMPLETED); - job.setUpdatedAt(LocalDateTime.now()); - jobRepository.save(job); - - String currentUser = securityService.getCurrentUsername(); - jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, currentUser); - - String description = String.format("Auftrag manuell beendet von %s. Begründung: %s", - currentUser, reason.trim()); - jobHistoryService.logCustomEvent(job.getId(), - getTranslation("jobsummary.history.manualcomplete.reason"), - description, currentUser, JobHistoryType.STATUS_CHANGE); - - dialog.close(); - Notification - .show(getTranslation("jobsummary.notification.completed", job.getJobNumber()), 3000, - Notification.Position.BOTTOM_END) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - 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); - } - }); - - buttonBar.add(cancelButton, confirmButton); - dialog.getFooter().add(buttonBar); - dialog.open(); - } - private VerticalLayout borderedBox() { VerticalLayout box = new VerticalLayout(); box.addClassName("summary-card"); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java index 6eeeb60..a2591f3 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java @@ -86,7 +86,8 @@ public class ShowCustomersView extends VerticalLayout implements HasDynamicTitle var customers = customerService.findAll(); var currentUserId = securityService.getCurrentUserId(); var ownCustomers = customers.stream() - .filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId)).toList(); + .filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId)) + .filter(c -> !c.isInternal()).toList(); grid.setItems(ownCustomers); } diff --git a/backend/src/main/java/de/assecutor/votianlt/repository/SignatureRepository.java b/backend/src/main/java/de/assecutor/votianlt/repository/SignatureRepository.java index f0f3635..fcb9a8e 100644 --- a/backend/src/main/java/de/assecutor/votianlt/repository/SignatureRepository.java +++ b/backend/src/main/java/de/assecutor/votianlt/repository/SignatureRepository.java @@ -10,4 +10,6 @@ import java.util.List; @Repository public interface SignatureRepository extends MongoRepository { List findByTaskId(ObjectId taskId); + + List findByTaskIdOrderByCreatedAtDesc(ObjectId taskId); } \ No newline at end of file diff --git a/backend/src/main/resources/messages_de.properties b/backend/src/main/resources/messages_de.properties index db9e202..49d2a1c 100644 --- a/backend/src/main/resources/messages_de.properties +++ b/backend/src/main/resources/messages_de.properties @@ -5,7 +5,7 @@ dialog.confirm=Bestätigen # Navigation and Main Layout nav.jobs=Aufträge nav.job.create=Auftragserstellung -nav.customers=Kunden +nav.customers=Adressbuch nav.appusers=App-Nutzer nav.statistics=Statistiken nav.invoices=Rechnungen @@ -31,7 +31,7 @@ profile.lastname=Nachname profile.phone=Telefonnummer profile.fax=Telefon (Fax) profile.mobile=Telefon (Mobil) -profile.email=E-Mail-Adresse (Login)* +profile.email=E-Mail-Adresse profile.street=Straße profile.housenr=Hausnr profile.addressadd=Adresszusatz @@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Ort (Abholung) addjob.address.city.placeholder.delivery=Ort (Lieferung) addjob.address.delivery.street.placeholder=Straße (Lieferung) addjob.address.delivery.addition.placeholder=Adresszusatz (Lieferung) -addjob.address.save=Adresse speichern +addjob.address.save=Adresse in Adressbuch übernehmen +addjob.address.update=Adresse im Adressbuch aktualisieren addjob.section.pickup=Abholung addjob.section.delivery=Lieferung addjob.stations.apply=Stationen \u00fcbernehmen @@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. Fotos addjob.tasks.photo.max=Max. Fotos addjob.tasks.barcode.min=Min. Barcodes addjob.tasks.barcode.max=Max. Barcodes -addjob.tasks.signature.noconfig=Keine Konfiguration erforderlich +addjob.tasks.signature.notelabel=Bemerkung (optional) +addjob.tasks.signature.notelabel.placeholder=Hinweistext für die Bemerkung eingeben addjob.tasks.todolist.title=To-Do Liste addjob.tasks.todolist.item.placeholder=To-Do eingeben addjob.tasks.todolist.add=To-Do hinzufügen diff --git a/backend/src/main/resources/messages_ee.properties b/backend/src/main/resources/messages_ee.properties index 76f864a..8331d56 100644 --- a/backend/src/main/resources/messages_ee.properties +++ b/backend/src/main/resources/messages_ee.properties @@ -3,7 +3,7 @@ dialog.cancel=T\u00fchista dialog.confirm=Kinnita nav.jobs=Tellimused nav.job.create=Tellimuse loomine -nav.customers=Kliendid +nav.customers=Aadressiraamat nav.appusers=\u00c4pikasutajad nav.statistics=Statistika nav.invoices=Arved @@ -27,7 +27,7 @@ profile.lastname=Perekonnanimi profile.phone=Telefoninumber profile.fax=Telefon (faks) profile.mobile=Telefon (mobiil) -profile.email=E-posti aadress (sisselogimine)* +profile.email=E-posti aadress profile.street=T\u00e4nav profile.housenr=Majanumber profile.addressadd=Aadressi t\u00e4iend @@ -396,7 +396,8 @@ addjob.address.city.placeholder.pickup=Asukoht (pealekorje) addjob.address.city.placeholder.delivery=Asukoht (kohaletoimetamine) addjob.address.delivery.street.placeholder=T\u00e4nav (kohaletoimetamine) addjob.address.delivery.addition.placeholder=Aadressi t\u00e4iend (kohaletoimetamine) -addjob.address.save=Salvesta aadress +addjob.address.save=Lisa aadress aadressiraamatusse +addjob.address.update=Uuenda aadressi aadressiraamatus addjob.section.pickup=Pealekorje addjob.section.delivery=Kohaletoimetamine addjob.stations.apply=Rakenda jaamad @@ -462,7 +463,8 @@ addjob.tasks.photo.min=Min. fotosid addjob.tasks.photo.max=Max. fotosid addjob.tasks.barcode.min=Min. v\u00f6\u00f6tkoode addjob.tasks.barcode.max=Max. v\u00f6\u00f6tkoode -addjob.tasks.signature.noconfig=Seadistamine pole vajalik +addjob.tasks.signature.notelabel=Märkus (valikuline) +addjob.tasks.signature.notelabel.placeholder=Sisestage vihje tekst märkusele addjob.tasks.todolist.title=\u00dclesannete nimekiri addjob.tasks.todolist.item.placeholder=Sisestage \u00fclesanne addjob.tasks.todolist.add=Lisa \u00fclesanne diff --git a/backend/src/main/resources/messages_en.properties b/backend/src/main/resources/messages_en.properties index 12b5c35..8b3b096 100644 --- a/backend/src/main/resources/messages_en.properties +++ b/backend/src/main/resources/messages_en.properties @@ -5,7 +5,7 @@ dialog.confirm=Confirm # Navigation and Main Layout nav.jobs=Jobs nav.job.create=Create Job -nav.customers=Customers +nav.customers=Address Book nav.appusers=App Users nav.statistics=Statistics nav.invoices=Invoices @@ -31,7 +31,7 @@ profile.lastname=Last Name profile.phone=Phone Number profile.fax=Phone (Fax) profile.mobile=Phone (Mobile) -profile.email=Email Address (Login)* +profile.email=Email Address profile.street=Street profile.housenr=House No. profile.addressadd=Address Suffix @@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=City (Pickup) addjob.address.city.placeholder.delivery=City (Delivery) addjob.address.delivery.street.placeholder=Street (Delivery) addjob.address.delivery.addition.placeholder=Address suffix (Delivery) -addjob.address.save=Save Address +addjob.address.save=Add address to address book +addjob.address.update=Update address in address book addjob.section.pickup=Pickup addjob.section.delivery=Delivery addjob.stations.apply=Apply Stations @@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. Photos addjob.tasks.photo.max=Max. Photos addjob.tasks.barcode.min=Min. Barcodes addjob.tasks.barcode.max=Max. Barcodes -addjob.tasks.signature.noconfig=No configuration required +addjob.tasks.signature.notelabel=Note (optional) +addjob.tasks.signature.notelabel.placeholder=Enter hint text for the note addjob.tasks.todolist.title=To-Do List addjob.tasks.todolist.item.placeholder=Enter to-do addjob.tasks.todolist.add=Add To-Do diff --git a/backend/src/main/resources/messages_es.properties b/backend/src/main/resources/messages_es.properties index 47e4583..b51e8e0 100644 --- a/backend/src/main/resources/messages_es.properties +++ b/backend/src/main/resources/messages_es.properties @@ -5,7 +5,7 @@ dialog.confirm=Confirmar # Navigation and Main Layout nav.jobs=Pedidos nav.job.create=Crear pedido -nav.customers=Clientes +nav.customers=Libreta de direcciones nav.appusers=Usuarios de la app nav.statistics=Estad\u00edsticas nav.invoices=Facturas @@ -31,7 +31,7 @@ profile.lastname=Apellido profile.phone=N\u00famero de tel\u00e9fono profile.fax=Tel\u00e9fono (Fax) profile.mobile=Tel\u00e9fono (M\u00f3vil) -profile.email=Direcci\u00f3n de correo electr\u00f3nico (Login)* +profile.email=Direcci\u00f3n de correo electr\u00f3nico profile.street=Calle profile.housenr=N\u00famero profile.addressadd=Complemento de direcci\u00f3n @@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Localidad (Recogida) addjob.address.city.placeholder.delivery=Localidad (Entrega) addjob.address.delivery.street.placeholder=Calle (Entrega) addjob.address.delivery.addition.placeholder=Complemento de direcci\u00f3n (Entrega) -addjob.address.save=Guardar direcci\u00f3n +addjob.address.save=A\u00f1adir direcci\u00f3n a la libreta de direcciones +addjob.address.update=Actualizar direcci\u00f3n en la libreta de direcciones addjob.section.pickup=Recogida addjob.section.delivery=Entrega addjob.stations.apply=Aplicar estaciones @@ -513,7 +514,8 @@ addjob.tasks.photo.min=M\u00edn. fotos addjob.tasks.photo.max=M\u00e1x. fotos addjob.tasks.barcode.min=M\u00edn. c\u00f3digos de barras addjob.tasks.barcode.max=M\u00e1x. c\u00f3digos de barras -addjob.tasks.signature.noconfig=No se requiere configuraci\u00f3n +addjob.tasks.signature.notelabel=Nota (opcional) +addjob.tasks.signature.notelabel.placeholder=Introducir texto de sugerencia para la nota addjob.tasks.todolist.title=Lista de tareas pendientes addjob.tasks.todolist.item.placeholder=Introducir tarea pendiente addjob.tasks.todolist.add=A\u00f1adir tarea pendiente diff --git a/backend/src/main/resources/messages_fr.properties b/backend/src/main/resources/messages_fr.properties index 4d467e6..d958081 100644 --- a/backend/src/main/resources/messages_fr.properties +++ b/backend/src/main/resources/messages_fr.properties @@ -5,7 +5,7 @@ dialog.confirm=Confirmer # Navigation and Main Layout nav.jobs=Missions nav.job.create=Cr\u00e9ation de mission -nav.customers=Clients +nav.customers=Carnet d'adresses nav.appusers=Utilisateurs d'app nav.statistics=Statistiques nav.invoices=Factures @@ -31,7 +31,7 @@ profile.lastname=Nom profile.phone=Num\u00e9ro de t\u00e9l\u00e9phone profile.fax=T\u00e9l\u00e9phone (fax) profile.mobile=T\u00e9l\u00e9phone (mobile) -profile.email=Adresse e-mail (connexion)* +profile.email=Adresse e-mail profile.street=Rue profile.housenr=N\u00b0 profile.addressadd=Compl\u00e9ment d'adresse @@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Ville (enl\u00e8vement) addjob.address.city.placeholder.delivery=Ville (livraison) addjob.address.delivery.street.placeholder=Rue (livraison) addjob.address.delivery.addition.placeholder=Compl\u00e9ment d'adresse (livraison) -addjob.address.save=Enregistrer l'adresse +addjob.address.save=Ajouter l'adresse au carnet d'adresses +addjob.address.update=Mettre \u00e0 jour l'adresse dans le carnet d'adresses addjob.section.pickup=Enl\u00e8vement addjob.section.delivery=Livraison addjob.stations.apply=Appliquer les stations @@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. photos addjob.tasks.photo.max=Max. photos addjob.tasks.barcode.min=Min. codes-barres addjob.tasks.barcode.max=Max. codes-barres -addjob.tasks.signature.noconfig=Aucune configuration n\u00e9cessaire +addjob.tasks.signature.notelabel=Note (optionnelle) +addjob.tasks.signature.notelabel.placeholder=Saisir le texte d'indication pour la note addjob.tasks.todolist.title=Liste de t\u00e2ches addjob.tasks.todolist.item.placeholder=Saisir la t\u00e2che addjob.tasks.todolist.add=Ajouter une t\u00e2che diff --git a/backend/src/main/resources/messages_lt.properties b/backend/src/main/resources/messages_lt.properties index e8a3f6a..3be4689 100644 --- a/backend/src/main/resources/messages_lt.properties +++ b/backend/src/main/resources/messages_lt.properties @@ -5,7 +5,7 @@ dialog.confirm=Patvirtinti # Navigation and Main Layout nav.jobs=Užsakymai nav.job.create=Užsakymo kūrimas -nav.customers=Klientai +nav.customers=Adres\u0173 knyga nav.appusers=Programėlės naudotojai nav.statistics=Statistika nav.invoices=Sąskaitos faktūros @@ -31,7 +31,7 @@ profile.lastname=Pavardė profile.phone=Telefono numeris profile.fax=Telefonas (faksas) profile.mobile=Telefonas (mob.) -profile.email=El. pašto adresas (prisijungimas)* +profile.email=El. pašto adresas* profile.street=Gatvė profile.housenr=Namo nr. profile.addressadd=Adreso priedas @@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Vietovė (atsiėmimas) addjob.address.city.placeholder.delivery=Vietovė (pristatymas) addjob.address.delivery.street.placeholder=Gatvė (pristatymas) addjob.address.delivery.addition.placeholder=Adreso priedas (pristatymas) -addjob.address.save=Išsaugoti adresą +addjob.address.save=Pridėti adresą į adresų knygą +addjob.address.update=Atnaujinti adresą adresų knygoje addjob.section.pickup=Atsiėmimas addjob.section.delivery=Pristatymas addjob.stations.apply=Pritaikyti stotis @@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. nuotraukų addjob.tasks.photo.max=Maks. nuotraukų addjob.tasks.barcode.min=Min. brūkšninių kodų addjob.tasks.barcode.max=Maks. brūkšninių kodų -addjob.tasks.signature.noconfig=Konfigūracija nereikalinga +addjob.tasks.signature.notelabel=Pastaba (neprivaloma) +addjob.tasks.signature.notelabel.placeholder=Įveskite patarimo tekstą pastabai addjob.tasks.todolist.title=Užduočių sąrašas addjob.tasks.todolist.item.placeholder=Įveskite užduotį addjob.tasks.todolist.add=Pridėti užduotį diff --git a/backend/src/main/resources/messages_lv.properties b/backend/src/main/resources/messages_lv.properties index 044a722..ee4aab7 100644 --- a/backend/src/main/resources/messages_lv.properties +++ b/backend/src/main/resources/messages_lv.properties @@ -5,7 +5,7 @@ dialog.confirm=Apstiprināt # Navigation and Main Layout nav.jobs=Uzdevumi nav.job.create=Izveidot uzdevumu -nav.customers=Klienti +nav.customers=Adrešu gr\u0101mata nav.appusers=Lietotnes lietotāji nav.statistics=Statistika nav.invoices=Rēķini @@ -31,7 +31,7 @@ profile.lastname=Uzvārds profile.phone=Tālruņa numurs profile.fax=Tālrunis (fakss) profile.mobile=Tālrunis (mobilais) -profile.email=E-pasta adrese (pieteikšanās)* +profile.email=E-pasta adrese profile.street=Iela profile.housenr=Mājas nr. profile.addressadd=Adreses papildinājums @@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Vieta (saņemšana) addjob.address.city.placeholder.delivery=Vieta (piegāde) addjob.address.delivery.street.placeholder=Iela (piegāde) addjob.address.delivery.addition.placeholder=Adreses papildinājums (piegāde) -addjob.address.save=Saglabāt adresi +addjob.address.save=Pievienot adresi adrešu grāmatai +addjob.address.update=Atjaunin\u0101t adresi adrešu gr\u0101mat\u0101 addjob.section.pickup=Saņemšana addjob.section.delivery=Piegāde addjob.stations.apply=Pārņemt stacijas @@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. fotogrāfijas addjob.tasks.photo.max=Maks. fotogrāfijas addjob.tasks.barcode.min=Min. svītrkodi addjob.tasks.barcode.max=Maks. svītrkodi -addjob.tasks.signature.noconfig=Konfigurācija nav nepieciešama +addjob.tasks.signature.notelabel=Piezīme (neobligāta) +addjob.tasks.signature.notelabel.placeholder=Ievadiet padoma tekstu piezīmei addjob.tasks.todolist.title=Uzdevumu saraksts addjob.tasks.todolist.item.placeholder=Ievadiet uzdevumu addjob.tasks.todolist.add=Pievienot uzdevumu diff --git a/backend/src/main/resources/messages_pl.properties b/backend/src/main/resources/messages_pl.properties index f70b98f..1d4066b 100644 --- a/backend/src/main/resources/messages_pl.properties +++ b/backend/src/main/resources/messages_pl.properties @@ -5,7 +5,7 @@ dialog.confirm=Potwierd\u017a # Navigation and Main Layout nav.jobs=Zlecenia nav.job.create=Tworzenie zlecenia -nav.customers=Klienci +nav.customers=Ksi\u0105\u017cka adresowa nav.appusers=U\u017cytkownicy aplikacji nav.statistics=Statystyki nav.invoices=Faktury @@ -31,7 +31,7 @@ profile.lastname=Nazwisko profile.phone=Numer telefonu profile.fax=Telefon (faks) profile.mobile=Telefon (kom\u00f3rkowy) -profile.email=Adres e-mail (login)* +profile.email=Adres e-mail profile.street=Ulica profile.housenr=Nr domu profile.addressadd=Dodatek do adresu @@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Miejscowo\u015b\u0107 (odbi\u00f3r) addjob.address.city.placeholder.delivery=Miejscowo\u015b\u0107 (dostawa) addjob.address.delivery.street.placeholder=Ulica (dostawa) addjob.address.delivery.addition.placeholder=Dodatek do adresu (dostawa) -addjob.address.save=Zapisz adres +addjob.address.save=Dodaj adres do ksi\u0105\u017cki adresowej +addjob.address.update=Zaktualizuj adres w ksi\u0105\u017cce adresowej addjob.section.pickup=Odbi\u00f3r addjob.section.delivery=Dostawa addjob.stations.apply=Zastosuj stacje @@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. zdj\u0119\u0107 addjob.tasks.photo.max=Maks. zdj\u0119\u0107 addjob.tasks.barcode.min=Min. kod\u00f3w kreskowych addjob.tasks.barcode.max=Maks. kod\u00f3w kreskowych -addjob.tasks.signature.noconfig=Konfiguracja nie jest wymagana +addjob.tasks.signature.notelabel=Notatka (opcjonalnie) +addjob.tasks.signature.notelabel.placeholder=Wprowadź tekst podpowiedzi dla notatki addjob.tasks.todolist.title=Lista zada\u0144 addjob.tasks.todolist.item.placeholder=Wprowad\u017a zadanie addjob.tasks.todolist.add=Dodaj zadanie diff --git a/backend/src/main/resources/messages_ru.properties b/backend/src/main/resources/messages_ru.properties index bca4a0d..333953c 100644 --- a/backend/src/main/resources/messages_ru.properties +++ b/backend/src/main/resources/messages_ru.properties @@ -5,7 +5,7 @@ dialog.confirm=Подтвердить # Navigation and Main Layout nav.jobs=Заказы nav.job.create=Создание заказа -nav.customers=Клиенты +nav.customers=Адресная книга nav.appusers=Пользователи приложения nav.statistics=Статистика nav.invoices=Счета @@ -31,7 +31,7 @@ profile.lastname=Фамилия profile.phone=Номер телефона profile.fax=Телефон (факс) profile.mobile=Телефон (мобильный) -profile.email=Адрес электронной почты (логин)* +profile.email=Адрес электронной почты profile.street=Улица profile.housenr=Дом profile.addressadd=Дополнение к адресу @@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Город (забор) addjob.address.city.placeholder.delivery=Город (доставка) addjob.address.delivery.street.placeholder=Улица (доставка) addjob.address.delivery.addition.placeholder=Дополнение к адресу (доставка) -addjob.address.save=Сохранить адрес +addjob.address.save=Добавить адрес в адресную книгу +addjob.address.update=Обновить адрес в адресной книге addjob.section.pickup=Забор addjob.section.delivery=Доставка addjob.stations.apply=Применить станции @@ -513,7 +514,8 @@ addjob.tasks.photo.min=Мин. фото addjob.tasks.photo.max=Макс. фото addjob.tasks.barcode.min=Мин. штрих-кодов addjob.tasks.barcode.max=Макс. штрих-кодов -addjob.tasks.signature.noconfig=Настройка не требуется +addjob.tasks.signature.notelabel=Примечание (необязательно) +addjob.tasks.signature.notelabel.placeholder=Введите текст подсказки для примечания addjob.tasks.todolist.title=Список дел addjob.tasks.todolist.item.placeholder=Введите задачу addjob.tasks.todolist.add=Добавить задачу diff --git a/backend/src/main/resources/messages_tr.properties b/backend/src/main/resources/messages_tr.properties index a88fd12..6ffd930 100644 --- a/backend/src/main/resources/messages_tr.properties +++ b/backend/src/main/resources/messages_tr.properties @@ -5,7 +5,7 @@ dialog.confirm=Onayla # Navigation and Main Layout nav.jobs=\u0130\u015fler nav.job.create=\u0130\u015f Olu\u015ftur -nav.customers=M\u00fc\u015fteriler +nav.customers=Adres Defteri nav.appusers=Uygulama Kullan\u0131c\u0131lar\u0131 nav.statistics=\u0130statistikler nav.invoices=Faturalar @@ -31,7 +31,7 @@ profile.lastname=Soyad profile.phone=Telefon Numaras\u0131 profile.fax=Telefon (Faks) profile.mobile=Telefon (Mobil) -profile.email=E-Posta Adresi (Giri\u015f)* +profile.email=E-Posta Adresi* profile.street=Sokak profile.housenr=Kap\u0131 No profile.addressadd=Adres Eki @@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=\u015eehir (Al\u0131m) addjob.address.city.placeholder.delivery=\u015eehir (Teslimat) addjob.address.delivery.street.placeholder=Sokak (Teslimat) addjob.address.delivery.addition.placeholder=Adres eki (Teslimat) -addjob.address.save=Adresi Kaydet +addjob.address.save=Adresi adres defterine ekle +addjob.address.update=Adres defterindeki adresi g\u00fcncelle addjob.section.pickup=Al\u0131m addjob.section.delivery=Teslimat addjob.stations.apply=\u0130stasyonlar\u0131 \u00fcbernehmennehmen @@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. Foto\u011fraf addjob.tasks.photo.max=Maks. Foto\u011fraf addjob.tasks.barcode.min=Min. Barkod addjob.tasks.barcode.max=Maks. Barkod -addjob.tasks.signature.noconfig=Yap\u0131land\u0131rma gerekli de\u011fil +addjob.tasks.signature.notelabel=Not (iste\u011fe ba\u011fl\u0131) +addjob.tasks.signature.notelabel.placeholder=Not i\u00e7in ipucu metnini girin addjob.tasks.todolist.title=Yap\u0131lacaklar Listesi addjob.tasks.todolist.item.placeholder=Yap\u0131lacak \u00f6\u011feyi girin addjob.tasks.todolist.add=Yap\u0131lacak \u00d6\u011fe Ekle