Stationen-Dialoge als eigene Komponenten, AddJob-Seite auf Einzelansicht ohne Tabs umgestellt

- StationTile, PickupStationDialog und DeliveryStationDialog als eigenständige UI-Komponenten extrahiert
- Preis- und Leistungselemente (Streckeneingabe, Leistungen-Grid, Zusammenfassung, Bemerkung) unter das Stationen-Grid verschoben
- TabSheet entfernt, alle Inhalte auf einer einzigen Seite dargestellt
- LlmRestClient-Formatierung angepasst, BaseTask und TaskRepository erweitert
- Übersetzungen für neue Dialog-Labels ergänzt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 15:53:19 +01:00
parent 22fdc13cba
commit cd8b82cd71
10 changed files with 2460 additions and 2089 deletions

View File

@@ -30,8 +30,7 @@ public class LlmRestClient {
public LlmRestClient(@Value("${app.ai.lmstudio.base-url}") String lmstudioBaseUrl, public LlmRestClient(@Value("${app.ai.lmstudio.base-url}") String lmstudioBaseUrl,
@Value("${app.ai.lmstudio.model}") String lmstudioModel, @Value("${app.ai.lmstudio.model}") String lmstudioModel,
@Value("${app.ai.lmstudio.htaccess-username}") String lmstudioHtaccessUsername, @Value("${app.ai.lmstudio.htaccess-username}") String lmstudioHtaccessUsername,
@Value("${app.ai.lmstudio.htaccess-password}") String lmstudioHtaccessPassword, @Value("${app.ai.lmstudio.htaccess-password}") String lmstudioHtaccessPassword, ObjectMapper objectMapper) {
ObjectMapper objectMapper) {
this.model = lmstudioModel; this.model = lmstudioModel;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
@@ -39,17 +38,16 @@ public class LlmRestClient {
WebClient.Builder builder = WebClient.builder(); WebClient.Builder builder = WebClient.builder();
builder.baseUrl(lmstudioBaseUrl + "/v1/chat/completions"); builder.baseUrl(lmstudioBaseUrl + "/v1/chat/completions");
if (lmstudioHtaccessUsername != null && !lmstudioHtaccessUsername.isBlank() if (lmstudioHtaccessUsername != null && !lmstudioHtaccessUsername.isBlank() && lmstudioHtaccessPassword != null
&& lmstudioHtaccessPassword != null && !lmstudioHtaccessPassword.isBlank()) { && !lmstudioHtaccessPassword.isBlank()) {
String credentials = lmstudioHtaccessUsername + ":" + lmstudioHtaccessPassword; String credentials = lmstudioHtaccessUsername + ":" + lmstudioHtaccessPassword;
String encoded = Base64.getEncoder() String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
.encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Basic " + encoded); builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Basic " + encoded);
log.info("LlmRestClient initialized (with HTACCESS auth) - URL: {}/v1/chat/completions, Model: {}", log.info("LlmRestClient initialized (with HTACCESS auth) - URL: {}/v1/chat/completions, Model: {}",
lmstudioBaseUrl, lmstudioModel); lmstudioBaseUrl, lmstudioModel);
} else { } else {
log.info("LlmRestClient initialized - URL: {}/v1/chat/completions, Model: {}", log.info("LlmRestClient initialized - URL: {}/v1/chat/completions, Model: {}", lmstudioBaseUrl,
lmstudioBaseUrl, lmstudioModel); lmstudioModel);
} }
this.webClient = builder.build(); this.webClient = builder.build();
@@ -88,8 +86,7 @@ public class LlmRestClient {
Map.of("role", "user", "content", userMessage)), Map.of("role", "user", "content", userMessage)),
"temperature", temperature, "max_tokens", maxTokens, "stream", false); "temperature", temperature, "max_tokens", maxTokens, "stream", false);
log.info("Sending request to LLM (model: {}, prompt length: {} chars)...", model, log.info("Sending request to LLM (model: {}, prompt length: {} chars)...", model, userMessage.length());
userMessage.length());
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
String response = webClient.post().contentType(MediaType.APPLICATION_JSON).bodyValue(request).retrieve() String response = webClient.post().contentType(MediaType.APPLICATION_JSON).bodyValue(request).retrieve()

View File

@@ -32,6 +32,9 @@ public abstract class BaseTask {
@JsonIgnore @JsonIgnore
private ObjectId jobId; private ObjectId jobId;
@Field("station_order")
private Integer stationOrder;
@Field("task_order") @Field("task_order")
private Integer taskOrder = 0; private Integer taskOrder = 0;

View File

@@ -188,7 +188,8 @@ public class DeliveryStationTile extends VerticalLayout {
// Register change listeners on all fields // Register change listeners on all fields
setupChangeListeners(); setupChangeListeners();
// Store references to expanded-mode components (excluding titleLayout which stays visible) // Store references to expanded-mode components (excluding titleLayout which
// stays visible)
expandedOnlyComponents = getChildren().filter(c -> c != titleLayout).toList(); expandedOnlyComponents = getChildren().filter(c -> c != titleLayout).toList();
getStyle().set("transition", "width 0.3s ease, min-width 0.3s ease"); getStyle().set("transition", "width 0.3s ease, min-width 0.3s ease");
@@ -478,8 +479,8 @@ public class DeliveryStationTile extends VerticalLayout {
private void addCollapsedLine(String text) { private void addCollapsedLine(String text) {
if (text != null && !text.trim().isEmpty()) { if (text != null && !text.trim().isEmpty()) {
Span span = new Span(text); Span span = new Span(text);
span.getStyle().set("font-size", "var(--lumo-font-size-xs)").set("word-break", "break-word") span.getStyle().set("font-size", "var(--lumo-font-size-xs)").set("word-break", "break-word").set("color",
.set("color", "var(--lumo-secondary-text-color)"); "var(--lumo-secondary-text-color)");
collapsedContent.add(span); collapsedContent.add(span);
} }
} }

View File

@@ -0,0 +1,778 @@
package de.assecutor.votianlt.pages.base.ui.component;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.tabs.TabSheet;
import com.vaadin.flow.component.textfield.IntegerField;
import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.timepicker.TimePicker;
import de.assecutor.votianlt.model.AppUser;
import de.assecutor.votianlt.model.CargoItem;
import de.assecutor.votianlt.model.Customer;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Dialog for editing pickup station data. Contains address form fields,
* customer selection, appointments & processing tab, and cargo tab.
*/
public class PickupStationDialog extends Dialog {
/**
* Data holder for pickup station fields.
*/
public static class PickupData {
private String company;
private String salutation;
private String firstName;
private String lastName;
private String phone;
private String street;
private String houseNumber;
private String addressAddition;
private String zip;
private String city;
private boolean saveAddress;
private String customerSelection;
private LocalDate appointmentDate;
private LocalTime appointmentTime;
private boolean digitalProcessing;
private AppUser appUser;
private List<CargoItem> cargoItems = new ArrayList<>();
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
public String getSalutation() {
return salutation;
}
public void setSalutation(String salutation) {
this.salutation = salutation;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getHouseNumber() {
return houseNumber;
}
public void setHouseNumber(String houseNumber) {
this.houseNumber = houseNumber;
}
public String getAddressAddition() {
return addressAddition;
}
public void setAddressAddition(String addressAddition) {
this.addressAddition = addressAddition;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public boolean isSaveAddress() {
return saveAddress;
}
public void setSaveAddress(boolean saveAddress) {
this.saveAddress = saveAddress;
}
public String getCustomerSelection() {
return customerSelection;
}
public void setCustomerSelection(String customerSelection) {
this.customerSelection = customerSelection;
}
public LocalDate getAppointmentDate() {
return appointmentDate;
}
public void setAppointmentDate(LocalDate appointmentDate) {
this.appointmentDate = appointmentDate;
}
public LocalTime getAppointmentTime() {
return appointmentTime;
}
public void setAppointmentTime(LocalTime appointmentTime) {
this.appointmentTime = appointmentTime;
}
public boolean isDigitalProcessing() {
return digitalProcessing;
}
public void setDigitalProcessing(boolean digitalProcessing) {
this.digitalProcessing = digitalProcessing;
}
public AppUser getAppUser() {
return appUser;
}
public void setAppUser(AppUser appUser) {
this.appUser = appUser;
}
public List<CargoItem> getCargoItems() {
return cargoItems;
}
public void setCargoItems(List<CargoItem> cargoItems) {
this.cargoItems = cargoItems != null ? cargoItems : new ArrayList<>();
}
}
public interface SaveListener {
void onSave(PickupData data);
}
private final ComboBox<String> company;
private final ComboBox<String> salutation;
private final TextField firstName;
private final TextField lastName;
private final TextField phone;
private final TextField street;
private final TextField houseNumber;
private final TextField addressAddition;
private final TextField zip;
private final TextField city;
private final Checkbox saveAddress;
private final ComboBox<String> customerComboBox;
private DatePicker appointmentDatePicker;
private TimePicker appointmentTimePicker;
private Checkbox digitalProcessingCheckbox;
private ComboBox<AppUser> appUserComboBox;
private final List<CargoItem> cargoItemsState = new ArrayList<>();
private VerticalLayout cargoList;
private final DeliveryStationTile.TranslationHelper translationHelper;
public PickupStationDialog(String dialogTitle, List<Customer> customers,
DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener,
List<AppUser> availableAppUsers) {
this.translationHelper = translationHelper;
setHeaderTitle(dialogTitle);
setCloseOnOutsideClick(false);
setWidth("800px");
setHeight("80vh");
// Address form
VerticalLayout formLayout = new VerticalLayout();
formLayout.setPadding(true);
formLayout.setSpacing(true);
formLayout.setWidthFull();
// Customer selection
customerComboBox = new ComboBox<>(translationHelper.getTranslation("addjob.customer.label"));
customerComboBox.setPlaceholder(translationHelper.getTranslation("addjob.customer.placeholder"));
customerComboBox.setRequiredIndicatorVisible(true);
customerComboBox.setWidthFull();
Map<String, Customer> customerLabelMap = new LinkedHashMap<>();
for (Customer c : customers) {
String label = (c.getCompanyName() != null && !c.getCompanyName().isBlank())
? c.getCompanyName() + " | "
+ ((c.getFirstname() != null ? c.getFirstname() : "") + " "
+ (c.getLastName() != null ? c.getLastName() : "")).trim()
: ((c.getFirstname() != null ? c.getFirstname() : "") + " "
+ (c.getLastName() != null ? c.getLastName() : "")).trim();
if (label.isBlank()) {
label = translationHelper.getTranslation("addjob.customer.unnamed");
}
String uniqueLabel = label;
int counter = 2;
while (customerLabelMap.containsKey(uniqueLabel)) {
uniqueLabel = label + " (" + counter++ + ")";
}
customerLabelMap.put(uniqueLabel, c);
}
customerComboBox.setItems(new ArrayList<>(customerLabelMap.keySet()));
// Company with autocomplete
company = new ComboBox<>(translationHelper.getTranslation("profile.company"));
company.setPlaceholder(translationHelper.getTranslation("addjob.address.company.placeholder"));
company.setAllowCustomValue(true);
company.setWidthFull();
setupCompanyAutocomplete(company, customers);
// Salutation
salutation = new ComboBox<>(translationHelper.getTranslation("addjob.address.salutation"));
salutation.setItems(translationHelper.getTranslation("addjob.salutation.mr"),
translationHelper.getTranslation("addjob.salutation.ms"),
translationHelper.getTranslation("addjob.salutation.other"));
salutation.setPlaceholder(translationHelper.getTranslation("addjob.address.salutation.placeholder"));
salutation.setWidthFull();
// First name
firstName = new TextField(translationHelper.getTranslation("profile.firstname"));
firstName.setPlaceholder(translationHelper.getTranslation("profile.firstname"));
firstName.setRequiredIndicatorVisible(true);
firstName.setWidthFull();
// Last name
lastName = new TextField(translationHelper.getTranslation("profile.lastname"));
lastName.setPlaceholder(translationHelper.getTranslation("profile.lastname"));
lastName.setRequiredIndicatorVisible(true);
lastName.setWidthFull();
// Phone
phone = new TextField(translationHelper.getTranslation("profile.phone"));
phone.setPlaceholder(translationHelper.getTranslation("profile.phone"));
phone.setWidthFull();
// Street + house number
street = new TextField(translationHelper.getTranslation("profile.street"));
street.setPlaceholder(translationHelper.getTranslation("profile.street"));
street.setRequiredIndicatorVisible(true);
houseNumber = new TextField(translationHelper.getTranslation("profile.housenr"));
houseNumber.setPlaceholder(translationHelper.getTranslation("addjob.address.housenumber"));
houseNumber.setRequiredIndicatorVisible(true);
HorizontalLayout streetLayout = new HorizontalLayout();
streetLayout.setWidthFull();
streetLayout.setSpacing(true);
street.setWidth("70%");
houseNumber.setWidth("30%");
streetLayout.add(street, houseNumber);
// Address addition
addressAddition = new TextField(translationHelper.getTranslation("profile.addressadd"));
addressAddition
.setPlaceholder(translationHelper.getTranslation("addjob.address.delivery.addition.placeholder"));
addressAddition.setWidthFull();
// Zip + city
zip = new TextField(translationHelper.getTranslation("profile.zip"));
zip.setPlaceholder(translationHelper.getTranslation("profile.zip"));
zip.setRequiredIndicatorVisible(true);
city = new TextField(translationHelper.getTranslation("addjob.address.city"));
city.setPlaceholder(translationHelper.getTranslation("addjob.address.city"));
city.setRequiredIndicatorVisible(true);
HorizontalLayout zipCityLayout = new HorizontalLayout();
zipCityLayout.setWidthFull();
zipCityLayout.setSpacing(true);
zip.setWidth("30%");
city.setWidth("70%");
zipCityLayout.add(zip, city);
// Save address checkbox
saveAddress = new Checkbox(translationHelper.getTranslation("addjob.address.save"));
saveAddress.setValue(true);
saveAddress.setWidthFull();
// Customer selection fills address fields
customerComboBox.addValueChangeListener(ev -> {
String selected = ev.getValue();
if (selected == null)
return;
Customer c = customerLabelMap.get(selected);
if (c == null)
return;
saveAddress.setValue(false);
if (c.getCompanyName() != null)
company.setValue(c.getCompanyName());
else
company.clear();
if (c.getTitle() != null && ("Herr".equalsIgnoreCase(c.getTitle()) || "Frau".equalsIgnoreCase(c.getTitle())
|| "Divers".equalsIgnoreCase(c.getTitle())))
salutation.setValue(c.getTitle());
else
salutation.clear();
if (c.getFirstname() != null)
firstName.setValue(c.getFirstname());
else
firstName.clear();
if (c.getLastName() != null)
lastName.setValue(c.getLastName());
else
lastName.clear();
if (c.getTelephone() != null)
phone.setValue(c.getTelephone());
else
phone.clear();
if (c.getStreet() != null)
street.setValue(c.getStreet());
else
street.clear();
if (c.getHouseNumber() != null)
houseNumber.setValue(c.getHouseNumber());
else
houseNumber.clear();
if (c.getAddressAddition() != null)
addressAddition.setValue(c.getAddressAddition());
else
addressAddition.clear();
if (c.getZip() != null)
zip.setValue(c.getZip());
else
zip.clear();
if (c.getCity() != null)
city.setValue(c.getCity());
else
city.clear();
});
formLayout.add(customerComboBox, company, salutation, firstName, lastName, phone, streetLayout, addressAddition,
zipCityLayout, saveAddress);
// TabSheet with address, appointments, and cargo tabs
TabSheet tabSheet = new TabSheet();
tabSheet.setWidthFull();
tabSheet.setSizeFull();
tabSheet.add(translationHelper.getTranslation("addjob.tab.addresses"), formLayout);
tabSheet.add(translationHelper.getTranslation("addjob.tab.appointments"),
createAppointmentsTab(availableAppUsers));
tabSheet.add(translationHelper.getTranslation("addjob.tab.cargo"), createCargoTab());
add(tabSheet);
// Footer buttons
Button saveButton = new Button(translationHelper.getTranslation("dialog.confirm"), e -> {
PickupData data = collectData();
if (saveListener != null) {
saveListener.onSave(data);
}
close();
});
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button cancelButton = new Button(translationHelper.getTranslation("dialog.cancel"), e -> close());
getFooter().add(cancelButton, saveButton);
}
/**
* Pre-fills the dialog fields with existing data.
*/
public void setData(PickupData data) {
if (data == null)
return;
if (data.getCompany() != null)
company.setValue(data.getCompany());
if (data.getSalutation() != null)
salutation.setValue(data.getSalutation());
if (data.getFirstName() != null)
firstName.setValue(data.getFirstName());
if (data.getLastName() != null)
lastName.setValue(data.getLastName());
if (data.getPhone() != null)
phone.setValue(data.getPhone());
if (data.getStreet() != null)
street.setValue(data.getStreet());
if (data.getHouseNumber() != null)
houseNumber.setValue(data.getHouseNumber());
if (data.getAddressAddition() != null)
addressAddition.setValue(data.getAddressAddition());
if (data.getZip() != null)
zip.setValue(data.getZip());
if (data.getCity() != null)
city.setValue(data.getCity());
saveAddress.setValue(data.isSaveAddress());
if (data.getCustomerSelection() != null) {
customerComboBox.setValue(data.getCustomerSelection());
}
if (data.getAppointmentDate() != null) {
appointmentDatePicker.setValue(data.getAppointmentDate());
}
if (data.getAppointmentTime() != null) {
appointmentTimePicker.setValue(data.getAppointmentTime());
}
digitalProcessingCheckbox.setValue(data.isDigitalProcessing());
if (data.getAppUser() != null) {
appUserComboBox.setValue(data.getAppUser());
}
if (data.getCargoItems() != null && !data.getCargoItems().isEmpty() && cargoList != null) {
cargoItemsState.clear();
cargoList.removeAll();
for (CargoItem item : data.getCargoItems()) {
addCargoRowWithData(item);
}
}
}
private PickupData collectData() {
PickupData data = new PickupData();
data.setCompany(company.getValue());
data.setSalutation(salutation.getValue());
data.setFirstName(firstName.getValue());
data.setLastName(lastName.getValue());
data.setPhone(phone.getValue());
data.setStreet(street.getValue());
data.setHouseNumber(houseNumber.getValue());
data.setAddressAddition(addressAddition.getValue());
data.setZip(zip.getValue());
data.setCity(city.getValue());
data.setSaveAddress(saveAddress.getValue());
data.setCustomerSelection(customerComboBox.getValue());
if (appointmentDatePicker != null) {
data.setAppointmentDate(appointmentDatePicker.getValue());
}
if (appointmentTimePicker != null) {
data.setAppointmentTime(appointmentTimePicker.getValue());
}
if (digitalProcessingCheckbox != null) {
data.setDigitalProcessing(digitalProcessingCheckbox.getValue());
}
if (appUserComboBox != null) {
data.setAppUser(appUserComboBox.getValue());
}
data.setCargoItems(new ArrayList<>(cargoItemsState));
return data;
}
private void setupCompanyAutocomplete(ComboBox<String> companyField, List<Customer> customers) {
List<String> companyNames = customers.stream().map(Customer::getCompanyName)
.filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList();
companyField.setItems(companyNames);
companyField.addValueChangeListener(event -> {
String selectedCompany = event.getValue();
if (selectedCompany == null || selectedCompany.trim().isEmpty()) {
return;
}
Optional<Customer> matchingCustomer = customers.stream()
.filter(c -> selectedCompany.equals(c.getCompanyName())).findFirst();
if (matchingCustomer.isPresent()) {
Customer customer = matchingCustomer.get();
if (customer.getTitle() != null
&& ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle())
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
salutation.setValue(customer.getTitle());
}
if (customer.getFirstname() != null)
firstName.setValue(customer.getFirstname());
if (customer.getLastName() != null)
lastName.setValue(customer.getLastName());
if (customer.getTelephone() != null)
phone.setValue(customer.getTelephone());
if (customer.getStreet() != null)
street.setValue(customer.getStreet());
if (customer.getHouseNumber() != null)
houseNumber.setValue(customer.getHouseNumber());
if (customer.getAddressAddition() != null)
addressAddition.setValue(customer.getAddressAddition());
if (customer.getZip() != null)
zip.setValue(customer.getZip());
if (customer.getCity() != null)
city.setValue(customer.getCity());
}
});
companyField.addCustomValueSetListener(event -> companyField.setValue(event.getDetail()));
}
// ============================================
// Appointments & Processing Tab
// ============================================
private VerticalLayout createAppointmentsTab(List<AppUser> availableAppUsers) {
VerticalLayout tabContent = new VerticalLayout();
tabContent.setSizeFull();
tabContent.setPadding(true);
tabContent.setSpacing(true);
tabContent.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
VerticalLayout content = new VerticalLayout();
content.setPadding(false);
content.setSpacing(true);
content.setWidth("720px");
content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
// Digital processing + App user
digitalProcessingCheckbox = new Checkbox(translationHelper.getTranslation("profile.settings.digitalprocess"));
digitalProcessingCheckbox.setValue(true);
HorizontalLayout digitalRow = new HorizontalLayout();
digitalRow.setWidthFull();
digitalRow.setAlignItems(FlexComponent.Alignment.BASELINE);
digitalRow.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
digitalProcessingCheckbox.getStyle().set("margin-right", "12px");
digitalRow.add(digitalProcessingCheckbox);
appUserComboBox = new ComboBox<>(translationHelper.getTranslation("addjob.appuser.label"));
appUserComboBox.setWidthFull();
if (availableAppUsers != null) {
appUserComboBox.setItems(availableAppUsers);
}
appUserComboBox.setItemLabelGenerator(
user -> user.getVorname() + " " + user.getNachname() + " (" + user.getEmail() + ")");
appUserComboBox.setPlaceholder(translationHelper.getTranslation("addjob.appuser.placeholder"));
content.add(digitalRow, appUserComboBox);
// Toggle app user visibility based on digital processing
digitalProcessingCheckbox.addValueChangeListener(e -> {
boolean required = Boolean.TRUE.equals(e.getValue());
appUserComboBox.setRequiredIndicatorVisible(required);
appUserComboBox.setVisible(required);
if (!required) {
appUserComboBox.clear();
}
});
boolean digitalInitial = Boolean.TRUE.equals(digitalProcessingCheckbox.getValue());
appUserComboBox.setRequiredIndicatorVisible(digitalInitial);
appUserComboBox.setVisible(digitalInitial);
// Appointment date & time
H3 pickupApptTitle = new H3(translationHelper.getTranslation("addjob.appointment.pickup"));
pickupApptTitle.getStyle().set("margin", "0");
appointmentDatePicker = new DatePicker(translationHelper.getTranslation("addjob.appointment.date"));
appointmentDatePicker.setRequiredIndicatorVisible(true);
appointmentDatePicker.setMin(LocalDate.now());
appointmentDatePicker.setLocale(java.util.Locale.GERMANY);
appointmentDatePicker.setI18n(new DatePicker.DatePickerI18n().setFirstDayOfWeek(1)
.setMonthNames(java.util.Arrays.asList("Januar", "Februar", "März", "April", "Mai", "Juni", "Juli",
"August", "September", "Oktober", "November", "Dezember"))
.setWeekdays(java.util.Arrays.asList("Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag",
"Freitag", "Samstag"))
.setWeekdaysShort(java.util.Arrays.asList("So", "Mo", "Di", "Mi", "Do", "Fr", "Sa")));
appointmentTimePicker = new TimePicker(translationHelper.getTranslation("addjob.appointment.time"));
appointmentTimePicker.setLocale(java.util.Locale.GERMANY);
HorizontalLayout pickupApptRow = new HorizontalLayout(appointmentDatePicker, appointmentTimePicker);
pickupApptRow.setWidthFull();
pickupApptRow.setSpacing(true);
appointmentDatePicker.setWidth("50%");
appointmentTimePicker.setWidth("50%");
content.add(pickupApptTitle, pickupApptRow);
// Info about delivery dates
Span deliveryInfoLabel = new Span(translationHelper.getTranslation("addjob.appointment.delivery.info"));
deliveryInfoLabel.getStyle().set("color", "var(--lumo-secondary-text-color)");
deliveryInfoLabel.getStyle().set("font-style", "italic");
deliveryInfoLabel.getStyle().set("margin-top", "var(--lumo-space-m)");
content.add(deliveryInfoLabel);
tabContent.add(content);
return tabContent;
}
// ============================================
// Cargo Tab
// ============================================
private VerticalLayout createCargoTab() {
VerticalLayout tabContent = new VerticalLayout();
tabContent.setSizeFull();
tabContent.setPadding(true);
tabContent.setSpacing(true);
VerticalLayout wrapper = new VerticalLayout();
wrapper.setWidthFull();
wrapper.setSpacing(true);
VerticalLayout cargoAreaContainer = new VerticalLayout();
cargoAreaContainer.setWidthFull();
cargoAreaContainer.setSpacing(true);
cargoAreaContainer.getStyle().set("background", "var(--lumo-base-color)");
cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
cargoAreaContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
cargoAreaContainer.getStyle().set("padding", "var(--lumo-space-m)");
H3 cargoTitle = new H3(translationHelper.getTranslation("addjob.tab.cargo"));
wrapper.add(cargoTitle);
cargoList = new VerticalLayout();
cargoList.setPadding(false);
cargoList.setSpacing(true);
cargoAreaContainer.add(cargoList);
// Add one empty row by default
addCargoRow();
// Add button
Button addCargoButton = new Button(translationHelper.getTranslation("addjob.cargo.add"),
new Icon(VaadinIcon.PLUS));
addCargoButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addCargoButton.setWidthFull();
addCargoButton.addClickListener(e -> addCargoRow());
cargoAreaContainer.add(addCargoButton);
wrapper.add(cargoAreaContainer);
tabContent.add(wrapper);
return tabContent;
}
private void addCargoRow() {
addCargoRowWithData(null);
}
private void addCargoRowWithData(CargoItem existingItem) {
HorizontalLayout row = new HorizontalLayout();
row.setWidthFull();
row.setAlignItems(FlexComponent.Alignment.END);
ComboBox<String> desc = new ComboBox<>(translationHelper.getTranslation("addjob.cargo.description"));
desc.setItems(translationHelper.getTranslation("addjob.cargo.europalette"),
translationHelper.getTranslation("addjob.cargo.disposablepalette"),
translationHelper.getTranslation("addjob.cargo.dusseldorfpalette"),
translationHelper.getTranslation("addjob.cargo.gridboxpalette"),
translationHelper.getTranslation("addjob.cargo.gridcart"),
translationHelper.getTranslation("addjob.cargo.parcel"));
desc.setAllowCustomValue(true);
desc.addCustomValueSetListener(event -> desc.setValue(event.getDetail()));
desc.setPlaceholder(translationHelper.getTranslation("addjob.cargo.description.placeholder"));
desc.setWidth("40%");
desc.setRequiredIndicatorVisible(true);
IntegerField qty = new IntegerField(translationHelper.getTranslation("addjob.cargo.quantity"));
qty.setMin(1);
qty.setMax(9999);
qty.setWidth("10%");
qty.setRequiredIndicatorVisible(true);
NumberField weight = new NumberField(translationHelper.getTranslation("addjob.cargo.weight"));
weight.setSuffixComponent(new Span("kg"));
weight.setWidth("15%");
weight.setRequiredIndicatorVisible(true);
NumberField len = new NumberField(translationHelper.getTranslation("addjob.cargo.length"));
len.setSuffixComponent(new Span("cm"));
len.setWidth("12%");
len.setRequiredIndicatorVisible(true);
NumberField wid = new NumberField(translationHelper.getTranslation("addjob.cargo.width"));
wid.setSuffixComponent(new Span("cm"));
wid.setWidth("12%");
wid.setRequiredIndicatorVisible(true);
NumberField hei = new NumberField(translationHelper.getTranslation("addjob.cargo.height"));
hei.setSuffixComponent(new Span("cm"));
hei.setWidth("12%");
hei.setRequiredIndicatorVisible(true);
Button remove = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
remove.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
remove.addClickListener(e -> {
int idx = cargoList.getChildren().toList().indexOf(row);
if (idx >= 0 && idx < cargoItemsState.size()) {
cargoItemsState.remove(idx);
}
cargoList.remove(row);
});
row.add(desc, qty, weight, len, wid, hei, remove);
cargoList.add(row);
// Create or use existing CargoItem
CargoItem item = new CargoItem();
if (existingItem != null) {
item.setDescription(existingItem.getDescription());
item.setQuantity(existingItem.getQuantity());
item.setWeightKg(existingItem.getWeightKg());
item.setLengthMm(existingItem.getLengthMm());
item.setWidthMm(existingItem.getWidthMm());
item.setHeightMm(existingItem.getHeightMm());
// Pre-fill fields
if (item.getDescription() != null)
desc.setValue(item.getDescription());
if (item.getQuantity() != null)
qty.setValue(item.getQuantity());
if (item.getWeightKg() != null)
weight.setValue(item.getWeightKg());
if (item.getLengthMm() != null)
len.setValue(item.getLengthMm());
if (item.getWidthMm() != null)
wid.setValue(item.getWidthMm());
if (item.getHeightMm() != null)
hei.setValue(item.getHeightMm());
}
cargoItemsState.add(item);
// Bind change listeners
desc.addValueChangeListener(ev -> item.setDescription(ev.getValue()));
qty.addValueChangeListener(ev -> item.setQuantity(ev.getValue()));
weight.addValueChangeListener(ev -> item.setWeightKg(ev.getValue()));
len.addValueChangeListener(ev -> item.setLengthMm(ev.getValue()));
wid.addValueChangeListener(ev -> item.setWidthMm(ev.getValue()));
hei.addValueChangeListener(ev -> item.setHeightMm(ev.getValue()));
}
}

View File

@@ -0,0 +1,164 @@
package de.assecutor.votianlt.pages.base.ui.component;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
/**
* A compact tile representing a station (pickup or delivery) in a grid layout.
* Shows only a title and text preview of entered data. Clicking the tile opens
* a dialog for data entry.
*/
public class StationTile extends VerticalLayout {
public enum StationType {
PICKUP, DELIVERY
}
public interface ClickListener {
void onClick(StationTile tile);
}
public interface DeleteListener {
void onDelete(StationTile tile);
}
private final StationType type;
private int stationNumber;
private final H3 title;
private final VerticalLayout previewContent;
private ClickListener clickListener;
private DeleteListener deleteListener;
public StationTile(StationType type, int stationNumber, String titleText, boolean removable) {
this.type = type;
this.stationNumber = stationNumber;
setPadding(true);
setSpacing(false);
getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
getStyle().set("border-radius", "var(--lumo-border-radius-m)");
getStyle().set("background-color", "var(--lumo-base-color)");
getStyle().set("cursor", "pointer");
getStyle().set("aspect-ratio", "1 / 1");
getStyle().set("overflow", "hidden");
// Header with title and optional delete button
title = new H3(titleText);
title.getStyle().set("margin", "0").set("flex-grow", "1").set("font-size", "var(--lumo-font-size-m)");
HorizontalLayout titleLayout = new HorizontalLayout();
titleLayout.setWidthFull();
titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
titleLayout.add(title);
if (removable) {
Button deleteButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
deleteButton.addClickListener(e -> {
e.getSource().getElement().executeJs("arguments[0].stopPropagation()", e.getSource().getElement());
if (deleteListener != null) {
deleteListener.onDelete(this);
}
});
titleLayout.add(deleteButton);
}
add(titleLayout);
// Preview content area
previewContent = new VerticalLayout();
previewContent.setPadding(false);
previewContent.setSpacing(false);
previewContent.getStyle().set("gap", "var(--lumo-space-xs)");
add(previewContent);
// Show placeholder when no data
updateEmptyPreview();
// Click on the tile opens the dialog
addClickListener(e -> {
if (clickListener != null) {
clickListener.onClick(this);
}
});
}
public void updatePreview(String company, String firstName, String lastName, String street, String houseNumber,
String zip, String city) {
previewContent.removeAll();
boolean hasData = false;
if (company != null && !company.trim().isEmpty()) {
addPreviewLine(company);
hasData = true;
}
String name = ((firstName != null ? firstName : "") + " " + (lastName != null ? lastName : "")).trim();
if (!name.isEmpty()) {
addPreviewLine(name);
hasData = true;
}
String streetLine = ((street != null ? street : "") + " " + (houseNumber != null ? houseNumber : "")).trim();
if (!streetLine.isEmpty()) {
addPreviewLine(streetLine);
hasData = true;
}
String zipCityLine = ((zip != null ? zip : "") + " " + (city != null ? city : "")).trim();
if (!zipCityLine.isEmpty()) {
addPreviewLine(zipCityLine);
hasData = true;
}
if (!hasData) {
updateEmptyPreview();
}
}
private void updateEmptyPreview() {
previewContent.removeAll();
Span placeholder = new Span("...");
placeholder.getStyle().set("color", "var(--lumo-contrast-40pct)").set("font-size", "var(--lumo-font-size-s)");
previewContent.add(placeholder);
}
private void addPreviewLine(String text) {
Span span = new Span(text);
span.getStyle().set("font-size", "var(--lumo-font-size-s)").set("word-break", "break-word").set("color",
"var(--lumo-secondary-text-color)");
previewContent.add(span);
}
public void updateTitle(String newTitle) {
title.setText(newTitle);
}
public void updateStationNumber(int newNumber) {
this.stationNumber = newNumber;
}
public StationType getType() {
return type;
}
public int getStationNumber() {
return stationNumber;
}
public void setClickListener(ClickListener listener) {
this.clickListener = listener;
}
public void setDeleteListener(DeleteListener listener) {
this.deleteListener = listener;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,8 @@ import java.util.List;
public interface TaskRepository extends MongoRepository<BaseTask, ObjectId> { public interface TaskRepository extends MongoRepository<BaseTask, ObjectId> {
List<BaseTask> findByJobIdOrderByTaskOrderAsc(ObjectId jobId); List<BaseTask> findByJobIdOrderByTaskOrderAsc(ObjectId jobId);
List<BaseTask> findByJobIdAndStationOrderOrderByTaskOrderAsc(ObjectId jobId, int stationOrder);
/** /**
* Count tasks by completion status * Count tasks by completion status
*/ */

View File

@@ -1,3 +1,7 @@
# Common Dialog
dialog.cancel=Abbrechen
dialog.confirm=Bestätigen
# Navigation and Main Layout # Navigation and Main Layout
nav.jobs=Aufträge nav.jobs=Aufträge
nav.job.create=Auftragserstellung nav.job.create=Auftragserstellung

View File

@@ -1,3 +1,7 @@
# Common Dialog
dialog.cancel=Cancel
dialog.confirm=Confirm
# Navigation and Main Layout # Navigation and Main Layout
nav.jobs=Jobs nav.jobs=Jobs
nav.job.create=Create New Job nav.job.create=Create New Job