Erweiterungen

This commit is contained in:
2025-08-14 10:53:17 +02:00
parent 248b71aab9
commit 9ad857b8a7
6 changed files with 294 additions and 29 deletions

View File

@@ -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;
}

View File

@@ -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<String> tasks;
// Preis (netto)
@Field("price")
private String price;
private BigDecimal price;
}

View File

@@ -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<CargoItem> 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<CargoItem> 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<CargoItem> savedItems = cargoItemRepository.saveAll(itemsWithJob);
List<ObjectId> 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());
}
/**

View File

@@ -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<de.assecutor.votianlt.model.CargoItem> 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<String> 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<de.assecutor.votianlt.model.CargoItem> 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;

View File

@@ -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<CargoItem, ObjectId> {
List<CargoItem> findByJobId(ObjectId jobId);
}

View File

@@ -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