From 9ad857b8a72085f2f50a042e7aaa42bb9712ab5b Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Thu, 14 Aug 2025 10:53:17 +0200 Subject: [PATCH] Erweiterungen --- .../assecutor/votianlt/model/CargoItem.java | 40 ++++ .../java/de/assecutor/votianlt/model/Job.java | 20 +- .../pages/add_job/service/AddJobService.java | 62 ++++++ .../pages/add_job/ui/view/AddJobView.java | 185 +++++++++++++++--- .../repository/CargoItemRepository.java | 12 ++ .../votianlt/security/SecurityConfig.java | 4 +- 6 files changed, 294 insertions(+), 29 deletions(-) create mode 100644 src/main/java/de/assecutor/votianlt/model/CargoItem.java create mode 100644 src/main/java/de/assecutor/votianlt/repository/CargoItemRepository.java diff --git a/src/main/java/de/assecutor/votianlt/model/CargoItem.java b/src/main/java/de/assecutor/votianlt/model/CargoItem.java new file mode 100644 index 0000000..c00860d --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/CargoItem.java @@ -0,0 +1,40 @@ +package de.assecutor.votianlt.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.bson.types.ObjectId; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Document(collection = "cargo_items") +public class CargoItem { + @Id + private ObjectId id; + + @Field("job_id") + private ObjectId jobId; + + @Field("description") + private String description; + + @Field("quantity") + private Integer quantity; + + @Field("weight_kg") + private Double weightKg; + + @Field("length_mm") + private Double lengthMm; + + @Field("width_mm") + private Double widthMm; + + @Field("height_mm") + private Double heightMm; +} + diff --git a/src/main/java/de/assecutor/votianlt/model/Job.java b/src/main/java/de/assecutor/votianlt/model/Job.java index 9ff48e9..a1d1e91 100644 --- a/src/main/java/de/assecutor/votianlt/model/Job.java +++ b/src/main/java/de/assecutor/votianlt/model/Job.java @@ -6,7 +6,10 @@ import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; +import java.math.BigDecimal; @Data @Document(collection = "jobs") @@ -112,7 +115,22 @@ public class Job { @Field("app_user") private String appUser; + // Termine + @Field("pickup_date") + private LocalDate pickupDate; + + @Field("delivery_date") + private LocalDate deliveryDate; + + // Bemerkung + @Field("remark") + private String remark; + + // Aufgaben + @Field("tasks") + private List tasks; + // Preis (netto) @Field("price") - private String price; + private BigDecimal price; } diff --git a/src/main/java/de/assecutor/votianlt/pages/add_job/service/AddJobService.java b/src/main/java/de/assecutor/votianlt/pages/add_job/service/AddJobService.java index 898e185..684b1d7 100644 --- a/src/main/java/de/assecutor/votianlt/pages/add_job/service/AddJobService.java +++ b/src/main/java/de/assecutor/votianlt/pages/add_job/service/AddJobService.java @@ -1,17 +1,21 @@ package de.assecutor.votianlt.pages.add_job.service; +import de.assecutor.votianlt.model.CargoItem; import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.JobStatus; +import de.assecutor.votianlt.repository.CargoItemRepository; import de.assecutor.votianlt.repository.JobRepository; import de.assecutor.votianlt.security.SecurityService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.bson.types.ObjectId; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -19,7 +23,57 @@ import java.util.Optional; public class AddJobService { private final JobRepository jobRepository; + private final CargoItemRepository cargoItemRepository; private final SecurityService securityService; + /** + * Speichert einen neuen Auftrag samt CargoItems (separat in cargo_items) + * @param job der Auftrag + * @param transientCargo zugehörige, noch nicht gespeicherte CargoItems aus der View + */ + public Job addJobWithCargo(Job job, List transientCargo) { + try { + // Metadaten setzen + LocalDateTime now = LocalDateTime.now(); + job.setCreatedAt(now); + job.setUpdatedAt(now); + job.setStatus(JobStatus.CREATED); + job.setCreatedBy(securityService.getCurrentUsername()); + + // Auftragsnummer generieren, falls nicht vorhanden + if (job.getJobNumber() == null || job.getJobNumber().isEmpty()) { + job.setJobNumber(generateJobNumber()); + } + + // Auftrag speichern + Job savedJob = jobRepository.save(job); + + // CargoItems separat mit Referenz auf Job speichern, IDs im Job verknüpfen + if (transientCargo != null && !transientCargo.isEmpty()) { + final ObjectId jobId = savedJob.getId(); + List itemsWithJob = transientCargo.stream().map(ci -> { + CargoItem copy = new CargoItem(); + copy.setJobId(jobId); + copy.setDescription(ci.getDescription()); + copy.setQuantity(ci.getQuantity()); + copy.setWeightKg(ci.getWeightKg()); + copy.setLengthMm(ci.getLengthMm()); + copy.setWidthMm(ci.getWidthMm()); + copy.setHeightMm(ci.getHeightMm()); + return copy; + }).collect(java.util.stream.Collectors.toList()); + List savedItems = cargoItemRepository.saveAll(itemsWithJob); + List cargoIds = savedItems.stream().map(CargoItem::getId).collect(java.util.stream.Collectors.toList()); + savedJob = jobRepository.save(savedJob); + } + + log.info("Auftrag erfolgreich gespeichert: {}", savedJob.getJobNumber()); + return savedJob; + + } catch (Exception e) { + log.error("Fehler beim Speichern des Auftrags: {}", e.getMessage(), e); + throw new RuntimeException("Auftrag konnte nicht gespeichert werden: " + e.getMessage()); + } + } /** * Speichert einen neuen Auftrag in der MongoDB @@ -40,6 +94,7 @@ public class AddJobService { // Auftrag speichern Job savedJob = jobRepository.save(job); + log.info("Auftrag erfolgreich gespeichert: {}", savedJob.getJobNumber()); return savedJob; @@ -224,6 +279,13 @@ public class AddJobService { // Digital processing existingJob.setDigitalProcessing(formJob.isDigitalProcessing()); existingJob.setAppUser(formJob.getAppUser()); + + // Termine, Bemerkung, Aufgaben, Preis (Cargo separat persistiert) + existingJob.setPickupDate(formJob.getPickupDate()); + existingJob.setDeliveryDate(formJob.getDeliveryDate()); + existingJob.setRemark(formJob.getRemark()); + existingJob.setTasks(formJob.getTasks()); + existingJob.setPrice(formJob.getPrice()); } /** diff --git a/src/main/java/de/assecutor/votianlt/pages/add_job/ui/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/add_job/ui/view/AddJobView.java index 2399c53..5d2acb2 100644 --- a/src/main/java/de/assecutor/votianlt/pages/add_job/ui/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/add_job/ui/view/AddJobView.java @@ -93,8 +93,6 @@ public class AddJobView extends Main { private DatePicker pickupDate; private DatePicker deliveryDate; - // TabSheet and Tab references for dynamic label updates - private TabSheet tabSheet; private com.vaadin.flow.component.tabs.Tab addressesTab; private com.vaadin.flow.component.tabs.Tab appointmentsTab; private com.vaadin.flow.component.tabs.Tab cargoTab; @@ -102,8 +100,14 @@ public class AddJobView extends Main { // Submit button private Button submitButton; + // Transient transfer list for cargo items (not part of Job entity anymore) + private java.util.List jobTransientCargo; // Stage sections for drag and drop + // Dynamic lists and additional controls + private VerticalLayout cargoList; + private VerticalLayout tasksList; + private TextArea remarkArea; private VerticalLayout pickupSection; private VerticalLayout deliverySection; @@ -112,6 +116,7 @@ public class AddJobView extends Main { public AddJobView(AddJobService addJobService) { this.addJobService = addJobService; initializeComponents(); + populateTestData(); // Pre-populate all required fields with test data setupLayout(); setupValidation(); loadDraftIfExists(); @@ -131,6 +136,7 @@ public class AddJobView extends Main { // Pickup address pickupCompany = new TextField("Firma"); pickupCompany.setPlaceholder("z.B. IKEA, McDonald's, DHL..."); + pickupCompany.setValue("Test Abholfirma GmbH"); addGooglePlacesAutocomplete(pickupCompany, 0); // Stage 0 für Pickup pickupSalutation = new ComboBox<>("Anrede"); pickupSalutation.setItems("Herr", "Frau", "Divers"); @@ -162,6 +168,7 @@ public class AddJobView extends Main { // Delivery address deliveryCompany = new TextField("Firma"); deliveryCompany.setPlaceholder("z.B. EDEKA, Bauhaus, Amazon..."); + deliveryCompany.setValue("Test Lieferfirma AG"); addGooglePlacesAutocomplete(deliveryCompany, 1); // Stage 1 für Delivery deliverySalutation = new ComboBox<>("Anrede"); deliverySalutation.setItems("Herr", "Frau", "Divers"); @@ -201,6 +208,14 @@ public class AddJobView extends Main { price.setPlaceholder("z.B. 150.00"); price.setRequiredIndicatorVisible(true); + // Erzwinge Komma als Dezimaltrennzeichen: ersetze Punkt beim Tippen + price.addValueChangeListener(e -> { + String v = e.getValue(); + if (v != null && v.contains(".")) { + String replaced = v.replace('.', ','); + if (!replaced.equals(v)) price.setValue(replaced); + } + }); // Date picker fields for appointments pickupDate = new DatePicker("Datum"); pickupDate.setRequiredIndicatorVisible(true); @@ -212,6 +227,33 @@ public class AddJobView extends Main { submitButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); } + private void populateTestData() { + // Populate pickup address required fields + pickupCompany.setValue("Firma 1"); + pickupFirstName.setValue("Max"); + pickupLastName.setValue("Mustermann"); + pickupStreet.setValue("Musterstraße"); + pickupHouseNumber.setValue("123"); + pickupZip.setValue("20095"); + pickupCity.setValue("Hamburg"); + + // Populate delivery address required fields + deliveryFirstName.setValue("Anna"); + deliveryLastName.setValue("Beispiel"); + deliveryStreet.setValue("Beispielweg"); + deliveryHouseNumber.setValue("456"); + deliveryZip.setValue("10115"); + deliveryCity.setValue("Berlin"); + + // Populate price field + price.setValue("150.00"); + + // Populate date fields with current date + 1 day for pickup, +2 days for delivery + java.time.LocalDate today = java.time.LocalDate.now(); + pickupDate.setValue(today.plusDays(1)); + deliveryDate.setValue(today.plusDays(2)); + } + private void setupLayout() { setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, @@ -221,18 +263,19 @@ public class AddJobView extends Main { add(new ViewToolbar("Neuen Auftrag anlegen")); // Create TabSheet for organizing the form - tabSheet = new TabSheet(); + // TabSheet and Tab references for dynamic label updates + TabSheet tabSheet = new TabSheet(); tabSheet.setSizeFull(); // Tab 1: Customer & Addresses addressesTab = tabSheet.add("Auftraggeber & Adressen", createCustomerAndAddressesTab()); - + // Tab 2: Appointments & Processing appointmentsTab = tabSheet.add("Termine & Verarbeitung", createAppointmentsAndProcessingTab()); - + // Tab 3: Cargo & Tasks cargoTab = tabSheet.add("Ladung & Aufgaben", createCargoAndTasksTab()); - + // Tab 4: Price & Submit priceTab = tabSheet.add("Preis & Abschluss", createPriceAndSubmitTab()); @@ -244,7 +287,7 @@ public class AddJobView extends Main { buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); buttonLayout.setPadding(true); buttonLayout.add(submitButton); - + add(buttonLayout); } @@ -350,7 +393,7 @@ public class AddJobView extends Main { // Add cargo section tabContent.add(createCargoSection()); - + // Add tasks and notes section tabContent.add(createTasksAndNotesSection()); @@ -521,6 +564,31 @@ public class AddJobView extends Main { } private void setupValidation() { + // Bind pickup address fields with validation + binder.forField(pickupFirstName) + .asRequired("") + .bind(Job::getPickupFirstName, Job::setPickupFirstName); + + binder.forField(pickupLastName) + .asRequired("") + .bind(Job::getPickupLastName, Job::setPickupLastName); + + binder.forField(pickupStreet) + .asRequired("") + .bind(Job::getPickupStreet, Job::setPickupStreet); + + binder.forField(pickupHouseNumber) + .asRequired("") + .bind(Job::getPickupHouseNumber, Job::setPickupHouseNumber); + + binder.forField(pickupZip) + .asRequired("") + .bind(Job::getPickupZip, Job::setPickupZip); + + binder.forField(pickupCity) + .asRequired("") + .bind(Job::getPickupCity, Job::setPickupCity); + // Bind delivery address fields with validation binder.forField(deliveryFirstName) .asRequired("") @@ -546,15 +614,32 @@ public class AddJobView extends Main { .asRequired("") .bind(Job::getDeliveryCity, Job::setDeliveryCity); - // Bind price field with validation + // Bind price field: Komma-Zahlen in Punkt-Zahlen umsetzen, dann nach BigDecimal konvertieren binder.forField(price) - .asRequired("") + .withNullRepresentation("") + .asRequired("Preis erforderlich") + .withConverter( + (String s) -> { + if (s == null || s.trim().isEmpty()) return null; + String normalized = s.replace(" ", "").replace(".", "").replace(',', '.'); + try { return new java.math.BigDecimal(normalized); } + catch (NumberFormatException ex) { throw new NumberFormatException("Ungültiger Betrag"); } + }, + (java.math.BigDecimal bd) -> bd == null ? "" : bd.toString(), + "Ungültiger Betrag" + ) + .withValidator(value -> value != null && value.compareTo(java.math.BigDecimal.ZERO) > 0, + "Der Preis muss größer als 0 sein") .bind(Job::getPrice, Job::setPrice); - // Bind date picker fields with validation (we'll need to add these to Job model later) - // For now, we'll just set up the validation without binding to the model - binder.forField(pickupDate).asRequired(""); - binder.forField(deliveryDate).asRequired(""); + // Bind date picker fields with validation + binder.forField(pickupDate) + .asRequired("") + .bind(Job::getPickupDate, Job::setPickupDate); + + binder.forField(deliveryDate) + .asRequired("") + .bind(Job::getDeliveryDate, Job::setDeliveryDate); // Bind optional fields without validation binder.bind(customerSelection, Job::getCustomerSelection, Job::setCustomerSelection); @@ -578,7 +663,7 @@ public class AddJobView extends Main { // Trigger initial validation when view is displayed triggerValidation(); - + // Update tab labels with initial validation state updateTabLabels(); } @@ -685,12 +770,12 @@ public class AddJobView extends Main { boolean pickupErrors = isFieldEmpty(pickupFirstName) || isFieldEmpty(pickupLastName) || isFieldEmpty(pickupStreet) || isFieldEmpty(pickupHouseNumber) || isFieldEmpty(pickupZip) || isFieldEmpty(pickupCity); - + // Check delivery address fields boolean deliveryErrors = isFieldEmpty(deliveryFirstName) || isFieldEmpty(deliveryLastName) || isFieldEmpty(deliveryStreet) || isFieldEmpty(deliveryHouseNumber) || isFieldEmpty(deliveryZip) || isFieldEmpty(deliveryCity); - + return pickupErrors || deliveryErrors; } @@ -711,10 +796,58 @@ public class AddJobView extends Main { try { Job job = new Job(); + // Zusätzliche Felder, die nicht über den Binder gebunden sind, manuell setzen + job.setPickupDate(pickupDate.getValue()); + job.setDeliveryDate(deliveryDate.getValue()); + if (remarkArea != null) job.setRemark(remarkArea.getValue()); + if (tasksList != null) { + java.util.List tasks = new java.util.ArrayList<>(); + tasksList.getChildren().forEach(comp -> { + if (comp instanceof com.vaadin.flow.component.orderedlayout.HorizontalLayout row) { + row.getChildren().filter(c -> c instanceof TextField).forEach(tf -> { + String v = ((TextField) tf).getValue(); + if (v != null && !v.trim().isEmpty()) tasks.add(v.trim()); + }); + } + }); + job.setTasks(tasks); + } + if (cargoList != null) { + java.util.List items = new java.util.ArrayList<>(); + cargoList.getChildren().forEach(comp -> { + if (comp instanceof com.vaadin.flow.component.orderedlayout.HorizontalLayout row) { + String desc = null; Integer qty = null; Double weight = null, len = null, wid = null, hei = null; + for (com.vaadin.flow.component.Component c : row.getChildren().toList()) { + if (c instanceof TextField tf && tf.getLabel() != null && tf.getLabel().contains("Beschreibung")) desc = tf.getValue(); + if (c instanceof IntegerField ifld) qty = ifld.getValue(); + if (c instanceof NumberField nf) { + String label = nf.getLabel(); + if (label != null) { + switch (label) { + case "Gewicht" -> weight = nf.getValue(); + case "Länge" -> len = nf.getValue(); + case "Breite" -> wid = nf.getValue(); + case "Höhe" -> hei = nf.getValue(); + } + } + } + } + if (desc != null || qty != null || weight != null || len != null || wid != null || hei != null) { + de.assecutor.votianlt.model.CargoItem ci = new de.assecutor.votianlt.model.CargoItem(); + ci.setDescription(desc); ci.setQuantity(qty); ci.setWeightKg(weight); + ci.setLengthMm(len); ci.setWidthMm(wid); ci.setHeightMm(hei); + items.add(ci); + } + } + }); + // temporär im Job-Objekt als Transfertyp beilegen (über transient Helper) + jobTransientCargo = items; + } + // Validate all required fields using the binder if (binder.writeBeanIfValid(job)) { - // All validations passed, save the job - Job savedJob = addJobService.addJob(job); + // All validations passed, save the job with cargo items + Job savedJob = addJobService.addJobWithCargo(job, jobTransientCargo); // Erfolgsmeldung anzeigen Notification successNotification = Notification.show( @@ -892,7 +1025,7 @@ public class AddJobView extends Main { wrapper.add(new H3("Ladung")); - VerticalLayout cargoList = new VerticalLayout(); + cargoList = new VerticalLayout(); cargoList.setPadding(false); cargoList.setSpacing(true); cargoArea.add(cargoList); @@ -996,7 +1129,7 @@ public class AddJobView extends Main { content.add(tasksTitle); // Dynamische Aufgabenliste - VerticalLayout tasksList = new VerticalLayout(); + tasksList = new VerticalLayout(); tasksList.setPadding(false); tasksList.setSpacing(true); @@ -1030,11 +1163,11 @@ public class AddJobView extends Main { // Bemerkung H3 remarksTitle = new H3("Bemerkung"); remarksTitle.getStyle().set("margin", "0"); - TextArea remark = new TextArea(); - remark.setPlaceholder("z.B. rückwärtigen Liefereingang benutzen o. ä."); - remark.setWidthFull(); - remark.setMinHeight("180px"); - content.add(remarksTitle, remark); + remarkArea = new TextArea(); + remarkArea.setPlaceholder("z.B. rückwärtigen Liefereingang benutzen o. ä."); + remarkArea.setWidthFull(); + remarkArea.setMinHeight("180px"); + content.add(remarksTitle, remarkArea); wrapper.add(content); return wrapper; diff --git a/src/main/java/de/assecutor/votianlt/repository/CargoItemRepository.java b/src/main/java/de/assecutor/votianlt/repository/CargoItemRepository.java new file mode 100644 index 0000000..7e715c8 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/repository/CargoItemRepository.java @@ -0,0 +1,12 @@ +package de.assecutor.votianlt.repository; + +import de.assecutor.votianlt.model.CargoItem; +import org.bson.types.ObjectId; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +public interface CargoItemRepository extends MongoRepository { + List findByJobId(ObjectId jobId); +} + diff --git a/src/main/java/de/assecutor/votianlt/security/SecurityConfig.java b/src/main/java/de/assecutor/votianlt/security/SecurityConfig.java index 3e9e8b9..e5c0c09 100644 --- a/src/main/java/de/assecutor/votianlt/security/SecurityConfig.java +++ b/src/main/java/de/assecutor/votianlt/security/SecurityConfig.java @@ -47,8 +47,8 @@ public class SecurityConfig extends VaadinWebSecurity { // Dies fügt automatisch .anyRequest().authenticated() hinzu super.configure(http); - // Setze die Login-View - setLoginView(http, LoginView.class); + // Setze die Login-View (per Pfad, um Router-Resolution-Probleme zu vermeiden) + setLoginView(http, "/login"); // Logout-Konfiguration http.logout(logout -> logout