From cd8b82cd7105b3f3595b9aae704da0e57ece78d5 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Wed, 4 Mar 2026 15:53:19 +0100 Subject: [PATCH] Stationen-Dialoge als eigene Komponenten, AddJob-Seite auf Einzelansicht ohne Tabs umgestellt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../votianlt/ai/service/LlmRestClient.java | 17 +- .../votianlt/model/task/BaseTask.java | 3 + .../ui/component/DeliveryStationDialog.java | 1093 ++++++++ .../ui/component/DeliveryStationTile.java | 7 +- .../ui/component/PickupStationDialog.java | 778 ++++++ .../pages/base/ui/component/StationTile.java | 164 ++ .../votianlt/pages/view/AddJobView.java | 2477 +++-------------- .../votianlt/repository/TaskRepository.java | 2 + src/main/resources/messages.properties | 4 + src/main/resources/messages_en.properties | 4 + 10 files changed, 2460 insertions(+), 2089 deletions(-) create mode 100644 src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java create mode 100644 src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java create mode 100644 src/main/java/de/assecutor/votianlt/pages/base/ui/component/StationTile.java diff --git a/src/main/java/de/assecutor/votianlt/ai/service/LlmRestClient.java b/src/main/java/de/assecutor/votianlt/ai/service/LlmRestClient.java index e705afb..2967b10 100644 --- a/src/main/java/de/assecutor/votianlt/ai/service/LlmRestClient.java +++ b/src/main/java/de/assecutor/votianlt/ai/service/LlmRestClient.java @@ -30,8 +30,7 @@ public class LlmRestClient { public LlmRestClient(@Value("${app.ai.lmstudio.base-url}") String lmstudioBaseUrl, @Value("${app.ai.lmstudio.model}") String lmstudioModel, @Value("${app.ai.lmstudio.htaccess-username}") String lmstudioHtaccessUsername, - @Value("${app.ai.lmstudio.htaccess-password}") String lmstudioHtaccessPassword, - ObjectMapper objectMapper) { + @Value("${app.ai.lmstudio.htaccess-password}") String lmstudioHtaccessPassword, ObjectMapper objectMapper) { this.model = lmstudioModel; this.objectMapper = objectMapper; @@ -39,17 +38,16 @@ public class LlmRestClient { WebClient.Builder builder = WebClient.builder(); builder.baseUrl(lmstudioBaseUrl + "/v1/chat/completions"); - if (lmstudioHtaccessUsername != null && !lmstudioHtaccessUsername.isBlank() - && lmstudioHtaccessPassword != null && !lmstudioHtaccessPassword.isBlank()) { + if (lmstudioHtaccessUsername != null && !lmstudioHtaccessUsername.isBlank() && lmstudioHtaccessPassword != null + && !lmstudioHtaccessPassword.isBlank()) { String credentials = lmstudioHtaccessUsername + ":" + lmstudioHtaccessPassword; - String encoded = Base64.getEncoder() - .encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Basic " + encoded); log.info("LlmRestClient initialized (with HTACCESS auth) - URL: {}/v1/chat/completions, Model: {}", lmstudioBaseUrl, lmstudioModel); } else { - log.info("LlmRestClient initialized - URL: {}/v1/chat/completions, Model: {}", - lmstudioBaseUrl, lmstudioModel); + log.info("LlmRestClient initialized - URL: {}/v1/chat/completions, Model: {}", lmstudioBaseUrl, + lmstudioModel); } this.webClient = builder.build(); @@ -88,8 +86,7 @@ public class LlmRestClient { Map.of("role", "user", "content", userMessage)), "temperature", temperature, "max_tokens", maxTokens, "stream", false); - log.info("Sending request to LLM (model: {}, prompt length: {} chars)...", model, - userMessage.length()); + log.info("Sending request to LLM (model: {}, prompt length: {} chars)...", model, userMessage.length()); long startTime = System.currentTimeMillis(); String response = webClient.post().contentType(MediaType.APPLICATION_JSON).bodyValue(request).retrieve() diff --git a/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java b/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java index eedc98a..4123fd9 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java @@ -32,6 +32,9 @@ public abstract class BaseTask { @JsonIgnore private ObjectId jobId; + @Field("station_order") + private Integer stationOrder; + @Field("task_order") private Integer taskOrder = 0; diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java new file mode 100644 index 0000000..580c53c --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java @@ -0,0 +1,1093 @@ +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.confirmdialog.ConfirmDialog; +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.notification.Notification; +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.TextField; +import de.assecutor.votianlt.model.Customer; +import de.assecutor.votianlt.model.TaskTemplate; +import de.assecutor.votianlt.model.task.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Dialog for editing delivery station data. Contains address form fields and a + * tasks tab for task management with templates. + */ +public class DeliveryStationDialog extends Dialog { + + /** + * Data holder for delivery station fields. + */ + public static class DeliveryData { + 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 List tasks = 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 List getTasks() { + return tasks; + } + + public void setTasks(List tasks) { + this.tasks = tasks != null ? tasks : new ArrayList<>(); + } + } + + public interface SaveListener { + void onSave(DeliveryData data); + } + + /** + * Callback interface for saving task templates. + */ + public interface TemplateSaveCallback { + void saveTemplate(String templateName, List tasks); + } + + private final ComboBox company; + private final ComboBox 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 List tasksState = new ArrayList<>(); + private VerticalLayout tasksList; + + private final DeliveryStationTile.TranslationHelper translationHelper; + + public DeliveryStationDialog(String dialogTitle, List customers, + DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener, + List templates, TemplateSaveCallback templateSaveCallback) { + + 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(); + + // 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); + formLayout.add(company); + + // 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(); + formLayout.add(salutation); + + // First name + firstName = new TextField(translationHelper.getTranslation("profile.firstname")); + firstName.setPlaceholder(translationHelper.getTranslation("profile.firstname")); + firstName.setRequiredIndicatorVisible(true); + firstName.setWidthFull(); + formLayout.add(firstName); + + // Last name + lastName = new TextField(translationHelper.getTranslation("profile.lastname")); + lastName.setPlaceholder(translationHelper.getTranslation("profile.lastname")); + lastName.setRequiredIndicatorVisible(true); + lastName.setWidthFull(); + formLayout.add(lastName); + + // Phone + phone = new TextField(translationHelper.getTranslation("profile.phone")); + phone.setPlaceholder(translationHelper.getTranslation("profile.phone")); + phone.setWidthFull(); + formLayout.add(phone); + + // 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); + formLayout.add(streetLayout); + + // Address addition + addressAddition = new TextField(translationHelper.getTranslation("profile.addressadd")); + addressAddition + .setPlaceholder(translationHelper.getTranslation("addjob.address.delivery.addition.placeholder")); + addressAddition.setWidthFull(); + formLayout.add(addressAddition); + + // 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); + formLayout.add(zipCityLayout); + + // Save address checkbox + saveAddress = new Checkbox(translationHelper.getTranslation("addjob.address.save")); + saveAddress.setValue(true); + saveAddress.setWidthFull(); + formLayout.add(saveAddress); + + // TabSheet with address and tasks tabs + TabSheet tabSheet = new TabSheet(); + tabSheet.setWidthFull(); + tabSheet.setSizeFull(); + + tabSheet.add(translationHelper.getTranslation("addjob.tab.addresses"), formLayout); + tabSheet.add(translationHelper.getTranslation("addjob.tab.tasks"), + createTasksTab(templates, templateSaveCallback)); + + add(tabSheet); + + // Footer buttons + Button saveButton = new Button(translationHelper.getTranslation("dialog.confirm"), e -> { + DeliveryData 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(DeliveryData 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()); + + // Load tasks into dialog state + if (data.getTasks() != null && !data.getTasks().isEmpty()) { + tasksState.clear(); + if (tasksList != null) { + tasksList.removeAll(); + } + for (BaseTask task : data.getTasks()) { + BaseTask copy = createTaskCopy(task); + if (copy != null) { + tasksState.add(copy); + if (tasksList != null) { + createTaskRowFromTask(copy); + } + } + } + } + } + + private DeliveryData collectData() { + DeliveryData data = new DeliveryData(); + 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.setTasks(new ArrayList<>(tasksState)); + return data; + } + + private void setupCompanyAutocomplete(ComboBox companyField, List customers) { + List 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 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())); + } + + // ============================================ + // Task Management + // ============================================ + + private VerticalLayout createTasksTab(List templates, TemplateSaveCallback templateSaveCallback) { + VerticalLayout tabContent = new VerticalLayout(); + tabContent.setSizeFull(); + tabContent.setPadding(true); + tabContent.setSpacing(true); + + VerticalLayout wrapper = new VerticalLayout(); + wrapper.setWidthFull(); + wrapper.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); + + VerticalLayout content = new VerticalLayout(); + content.setWidth("720px"); + content.setPadding(false); + content.setSpacing(true); + content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); + + // Task title with template selection + H3 tasksTitle = new H3(translationHelper.getTranslation("addjob.tasks.title")); + tasksTitle.getStyle().set("margin", "0"); + tasksTitle.getStyle().set("white-space", "nowrap"); + + ComboBox templateComboBox = new ComboBox<>(); + templateComboBox.setPlaceholder(translationHelper.getTranslation("addjob.tasks.template.placeholder")); + templateComboBox.setItemLabelGenerator(TaskTemplate::getTemplateName); + templateComboBox.setClearButtonVisible(true); + templateComboBox.setWidthFull(); + + if (templates != null) { + templateComboBox.setItems(templates); + } + + templateComboBox.addValueChangeListener(e -> { + if (e.getValue() != null) { + loadTasksFromTemplate(e.getValue(), templateComboBox); + } + }); + + // Icon button to save as template + Button saveAsTemplateBtn = new Button(new Icon(VaadinIcon.BOOKMARK)); + saveAsTemplateBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + saveAsTemplateBtn.setTooltipText(translationHelper.getTranslation("addjob.tasks.template.save.tooltip")); + saveAsTemplateBtn.addClickListener(e -> saveTasksAsTemplate(templateSaveCallback, templateComboBox)); + + HorizontalLayout titleWithTemplate = new HorizontalLayout(tasksTitle, templateComboBox, saveAsTemplateBtn); + titleWithTemplate.setAlignItems(FlexComponent.Alignment.CENTER); + titleWithTemplate.setSpacing(true); + content.add(titleWithTemplate); + + // Dynamic task list + tasksList = new VerticalLayout(); + tasksList.setPadding(false); + tasksList.setSpacing(true); + + // Add 1 example row + createTaskRow(); + + Button addTaskBtn = new Button(translationHelper.getTranslation("addjob.tasks.add"), new Icon(VaadinIcon.PLUS)); + addTaskBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + addTaskBtn.addClickListener(e -> createTaskRow()); + + content.add(tasksList, addTaskBtn); + wrapper.add(content); + tabContent.add(wrapper); + return tabContent; + } + + private void createTaskRow() { + VerticalLayout taskContainer = new VerticalLayout(); + taskContainer.setPadding(true); + taskContainer.setSpacing(true); + taskContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); + taskContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); + taskContainer.getStyle().set("background-color", "var(--lumo-base-color)"); + taskContainer.getStyle().set("position", "relative"); + + // Task type selection + ComboBox taskTypeCombo = new ComboBox<>(translationHelper.getTranslation("addjob.tasks.tasktype")); + taskTypeCombo.setItems(TaskType.values()); + taskTypeCombo.setItemLabelGenerator(TaskType::getDisplayName); + taskTypeCombo.setPlaceholder(translationHelper.getTranslation("addjob.tasks.tasktype.placeholder")); + taskTypeCombo.setWidthFull(); + + // Configuration container for dynamic fields + VerticalLayout configContainer = new VerticalLayout(); + configContainer.setPadding(false); + configContainer.setSpacing(true); + + // Red X button positioned in top-right corner + Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); + deleteXButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); + deleteXButton.getStyle().set("position", "absolute"); + deleteXButton.getStyle().set("top", "8px"); + deleteXButton.getStyle().set("right", "8px"); + deleteXButton.getStyle().set("z-index", "10"); + deleteXButton.getStyle().set("padding", "4px"); + deleteXButton.getStyle().set("min-width", "24px"); + deleteXButton.getStyle().set("min-height", "24px"); + deleteXButton.addClickListener(e -> { + int idx = tasksList.getChildren().toList().indexOf(taskContainer); + if (idx >= 0 && idx < tasksState.size()) { + tasksState.remove(idx); + reorderTasksAfterDeletion(); + } + tasksList.remove(taskContainer); + }); + + taskContainer.add(taskTypeCombo, configContainer); + taskContainer.add(deleteXButton); + + // Create Task and add to state with correct order + BaseTask task = new ConfirmationTask(""); + task.setTaskOrder(tasksState.size()); + tasksState.add(task); + + final BaseTask[] currentTask = { task }; + + taskTypeCombo.addValueChangeListener(ev -> { + TaskType selectedType = ev.getValue(); + if (selectedType != null) { + BaseTask newTask = createTaskByType(selectedType); + BaseTask oldTask = currentTask[0]; + + newTask.setDescription(oldTask.getDescription()); + newTask.setOptional(oldTask.isOptional()); + newTask.setCompleted(oldTask.isCompleted()); + newTask.setCompletedAt(oldTask.getCompletedAt()); + newTask.setCompletedBy(oldTask.getCompletedBy()); + + // Preserve task-specific properties + switch (oldTask) { + case ConfirmationTask oldConfirmationTask when newTask instanceof ConfirmationTask newConfirmationTask -> + newConfirmationTask.setButtonText(oldConfirmationTask.getButtonText()); + case TodoListTask oldTodoTask when newTask instanceof TodoListTask newTodoTask -> + newTodoTask.setTodoItems(oldTodoTask.getTodoItems()); + case PhotoTask oldPhotoTask when newTask instanceof PhotoTask newPhotoTask -> { + newPhotoTask.setMinPhotoCount(oldPhotoTask.getMinPhotoCount()); + newPhotoTask.setMaxPhotoCount(oldPhotoTask.getMaxPhotoCount()); + } + case BarcodeTask oldBarcodeTask when newTask instanceof BarcodeTask newBarcodeTask -> { + newBarcodeTask.setMinBarcodeCount(oldBarcodeTask.getMinBarcodeCount()); + newBarcodeTask.setMaxBarcodeCount(oldBarcodeTask.getMaxBarcodeCount()); + } + case CommentTask oldCommentTask when newTask instanceof CommentTask newCommentTask -> { + newCommentTask.setCommentText(oldCommentTask.getCommentText()); + newCommentTask.setRequired(oldCommentTask.isRequired()); + } + default -> { + } + } + + // Replace in state and preserve order + int index = tasksState.indexOf(oldTask); + if (index >= 0) { + newTask.setTaskOrder(index); + tasksState.set(index, newTask); + currentTask[0] = newTask; + } + + updateTaskConfiguration(configContainer, newTask); + } + }); + + // Set initial configuration + taskTypeCombo.setValue(TaskType.CONFIRMATION); + updateTaskConfiguration(configContainer, currentTask[0]); + + tasksList.add(taskContainer); + } + + private void createTaskRowFromTask(BaseTask task) { + VerticalLayout taskContainer = new VerticalLayout(); + taskContainer.setPadding(true); + taskContainer.setSpacing(true); + taskContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); + taskContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); + taskContainer.getStyle().set("background-color", "var(--lumo-base-color)"); + taskContainer.getStyle().set("position", "relative"); + + ComboBox taskTypeCombo = new ComboBox<>(translationHelper.getTranslation("addjob.tasks.tasktype")); + taskTypeCombo.setItems(TaskType.values()); + taskTypeCombo.setItemLabelGenerator(TaskType::getDisplayName); + taskTypeCombo.setPlaceholder(translationHelper.getTranslation("addjob.tasks.tasktype.placeholder")); + taskTypeCombo.setWidthFull(); + + VerticalLayout configContainer = new VerticalLayout(); + configContainer.setPadding(false); + configContainer.setSpacing(true); + + Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); + deleteXButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); + deleteXButton.getStyle().set("position", "absolute"); + deleteXButton.getStyle().set("top", "8px"); + deleteXButton.getStyle().set("right", "8px"); + deleteXButton.getStyle().set("z-index", "10"); + deleteXButton.getStyle().set("padding", "4px"); + deleteXButton.getStyle().set("min-width", "24px"); + deleteXButton.getStyle().set("min-height", "24px"); + deleteXButton.addClickListener(e -> { + int idx = tasksList.getChildren().toList().indexOf(taskContainer); + if (idx >= 0 && idx < tasksState.size()) { + tasksState.remove(idx); + reorderTasksAfterDeletion(); + } + tasksList.remove(taskContainer); + }); + + taskContainer.add(taskTypeCombo, configContainer); + taskContainer.add(deleteXButton); + + final BaseTask[] currentTask = { task }; + + // Set the combo value BEFORE registering the listener + TaskType taskType = getTaskTypeFromTask(task); + if (taskType != null) { + taskTypeCombo.setValue(taskType); + } + + // Register the listener for user-initiated type changes only + taskTypeCombo.addValueChangeListener(ev -> { + TaskType selectedType = ev.getValue(); + if (selectedType != null) { + BaseTask newTask = createTaskByType(selectedType); + BaseTask oldTask = currentTask[0]; + + newTask.setDescription(oldTask.getDescription()); + newTask.setOptional(oldTask.isOptional()); + newTask.setCompleted(oldTask.isCompleted()); + newTask.setCompletedAt(oldTask.getCompletedAt()); + newTask.setCompletedBy(oldTask.getCompletedBy()); + + switch (oldTask) { + case ConfirmationTask oldConfirmationTask when newTask instanceof ConfirmationTask newConfirmationTask -> + newConfirmationTask.setButtonText(oldConfirmationTask.getButtonText()); + case TodoListTask oldTodoTask when newTask instanceof TodoListTask newTodoTask -> + newTodoTask.setTodoItems(oldTodoTask.getTodoItems()); + case PhotoTask oldPhotoTask when newTask instanceof PhotoTask newPhotoTask -> { + newPhotoTask.setMinPhotoCount(oldPhotoTask.getMinPhotoCount()); + newPhotoTask.setMaxPhotoCount(oldPhotoTask.getMaxPhotoCount()); + } + case BarcodeTask oldBarcodeTask when newTask instanceof BarcodeTask newBarcodeTask -> { + newBarcodeTask.setMinBarcodeCount(oldBarcodeTask.getMinBarcodeCount()); + newBarcodeTask.setMaxBarcodeCount(oldBarcodeTask.getMaxBarcodeCount()); + } + case CommentTask oldCommentTask when newTask instanceof CommentTask newCommentTask -> { + newCommentTask.setCommentText(oldCommentTask.getCommentText()); + newCommentTask.setRequired(oldCommentTask.isRequired()); + } + default -> { + } + } + + int index = tasksState.indexOf(oldTask); + if (index >= 0) { + newTask.setTaskOrder(index); + tasksState.set(index, newTask); + currentTask[0] = newTask; + } + + updateTaskConfiguration(configContainer, newTask); + } + }); + + // Render the UI with the loaded task + updateTaskConfiguration(configContainer, task); + + tasksList.add(taskContainer); + } + + private BaseTask createTaskByType(TaskType taskType) { + return switch (taskType) { + case CONFIRMATION -> new ConfirmationTask(""); + case SIGNATURE -> new SignatureTask(); + case TODOLIST -> new TodoListTask(new ArrayList<>()); + case PHOTO -> new PhotoTask(1, 10); + case BARCODE -> new BarcodeTask(1, 10); + case COMMENT -> new CommentTask("", false); + }; + } + + private void reorderTasksAfterDeletion() { + for (int i = 0; i < tasksState.size(); i++) { + BaseTask task = tasksState.get(i); + if (task != null) { + task.setTaskOrder(i); + } + } + } + + private void updateTaskConfiguration(VerticalLayout configContainer, BaseTask task) { + configContainer.removeAll(); + + TaskType taskType = TaskType.valueOf(task.getTaskType()); + + switch (taskType) { + case CONFIRMATION: + TextField descriptionField = new TextField(translationHelper.getTranslation("addjob.tasks.description")); + descriptionField.setPlaceholder(translationHelper.getTranslation("addjob.tasks.description.placeholder")); + descriptionField.setWidthFull(); + descriptionField.setRequiredIndicatorVisible(true); + descriptionField.setValue(task.getDescription() != null ? task.getDescription() : ""); + descriptionField.addValueChangeListener(ev -> { + task.setDescription(ev.getValue()); + boolean isEmpty = ev.getValue() == null || ev.getValue().trim().isEmpty(); + if (isEmpty) { + descriptionField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); + descriptionField.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); + } else { + descriptionField.getStyle().remove("--vaadin-input-field-background"); + descriptionField.getStyle().remove("--vaadin-input-field-border-color"); + } + }); + if (task.getDescription() == null || task.getDescription().trim().isEmpty()) { + descriptionField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); + descriptionField.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); + } + + TextField buttonTextField = new TextField(translationHelper.getTranslation("addjob.tasks.buttontext")); + buttonTextField.setPlaceholder(translationHelper.getTranslation("addjob.tasks.buttontext.placeholder")); + buttonTextField.setWidthFull(); + buttonTextField.setRequiredIndicatorVisible(true); + ConfirmationTask confirmationTask = (ConfirmationTask) task; + buttonTextField.setValue(confirmationTask.getButtonText() != null ? confirmationTask.getButtonText() : ""); + buttonTextField.addValueChangeListener(ev -> { + confirmationTask.setButtonText(ev.getValue()); + boolean isEmpty = ev.getValue() == null || ev.getValue().trim().isEmpty(); + if (isEmpty) { + buttonTextField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); + buttonTextField.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); + } else { + buttonTextField.getStyle().remove("--vaadin-input-field-background"); + buttonTextField.getStyle().remove("--vaadin-input-field-border-color"); + } + }); + if (confirmationTask.getButtonText() == null || confirmationTask.getButtonText().trim().isEmpty()) { + buttonTextField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); + buttonTextField.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); + } + configContainer.add(descriptionField, buttonTextField); + 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); + break; + + case TODOLIST: + VerticalLayout todoContainer = new VerticalLayout(); + todoContainer.setPadding(false); + todoContainer.setSpacing(true); + + H3 todoTitle = new H3(translationHelper.getTranslation("addjob.tasks.todolist.title")); + todoTitle.getStyle().set("margin", "0"); + todoContainer.add(todoTitle); + + VerticalLayout todoList = new VerticalLayout(); + todoList.setPadding(false); + todoList.setSpacing(true); + + java.util.function.Consumer updateTodoFieldStyling = (field) -> { + boolean isEmpty = field.getValue() == null || field.getValue().trim().isEmpty(); + if (isEmpty) { + field.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); + field.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); + } else { + field.getStyle().remove("--vaadin-input-field-background"); + field.getStyle().remove("--vaadin-input-field-border-color"); + } + }; + + java.util.function.Consumer addTodoItem = (v) -> { + HorizontalLayout todoRow = new HorizontalLayout(); + todoRow.setWidthFull(); + todoRow.setAlignItems(FlexComponent.Alignment.END); + + TextField todoField = new TextField(); + todoField.setPlaceholder(translationHelper.getTranslation("addjob.tasks.todolist.item.placeholder")); + todoField.setWidth("100%"); + todoField.setRequiredIndicatorVisible(true); + updateTodoFieldStyling.accept(todoField); + + Button removeTodo = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); + removeTodo.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); + removeTodo.addClickListener(e -> { + todoList.remove(todoRow); + updateTodoItems(todoList, task); + }); + + todoRow.add(todoField, removeTodo); + todoRow.setFlexGrow(1, todoField); + todoList.add(todoRow); + + todoField.addValueChangeListener(ev -> { + updateTodoFieldStyling.accept(todoField); + updateTodoItems(todoList, task); + }); + }; + + Button addTodoBtn = new Button(translationHelper.getTranslation("addjob.tasks.todolist.add"), + new Icon(VaadinIcon.PLUS)); + addTodoBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + addTodoBtn.addClickListener(e -> addTodoItem.accept(null)); + + if (task instanceof TodoListTask todoTask) { + if (todoTask.getTodoItems() != null && !todoTask.getTodoItems().isEmpty()) { + for (String todoText : todoTask.getTodoItems()) { + HorizontalLayout todoRow = new HorizontalLayout(); + todoRow.setWidthFull(); + todoRow.setAlignItems(FlexComponent.Alignment.END); + + TextField todoField = new TextField(); + todoField.setPlaceholder( + translationHelper.getTranslation("addjob.tasks.todolist.item.placeholder")); + todoField.setWidth("100%"); + todoField.setRequiredIndicatorVisible(true); + todoField.setValue(todoText != null ? todoText : ""); + updateTodoFieldStyling.accept(todoField); + + Button removeTodo = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); + removeTodo.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); + removeTodo.addClickListener(e -> { + todoList.remove(todoRow); + updateTodoItems(todoList, task); + }); + + todoRow.add(todoField, removeTodo); + todoRow.setFlexGrow(1, todoField); + todoList.add(todoRow); + + todoField.addValueChangeListener(ev -> { + updateTodoFieldStyling.accept(todoField); + updateTodoItems(todoList, task); + }); + } + } else { + addTodoItem.accept(null); + } + } else { + addTodoItem.accept(null); + } + + todoContainer.add(todoList, addTodoBtn); + configContainer.add(todoContainer); + break; + + case PHOTO: + HorizontalLayout photoLayout = new HorizontalLayout(); + photoLayout.setWidthFull(); + photoLayout.setSpacing(true); + + PhotoTask photoTask = (PhotoTask) task; + IntegerField minPhotos = new IntegerField(translationHelper.getTranslation("addjob.tasks.photo.min")); + minPhotos.setPlaceholder("1"); + minPhotos.setMin(1); + minPhotos.setValue(photoTask.getMinPhotoCount() != null ? photoTask.getMinPhotoCount() : 1); + + IntegerField maxPhotos = new IntegerField(translationHelper.getTranslation("addjob.tasks.photo.max")); + maxPhotos.setPlaceholder("10"); + maxPhotos.setMin(1); + maxPhotos.setValue(photoTask.getMaxPhotoCount() != null ? photoTask.getMaxPhotoCount() : 10); + + photoLayout.add(minPhotos, maxPhotos); + + minPhotos.addValueChangeListener(ev -> photoTask.setMinPhotoCount(ev.getValue())); + maxPhotos.addValueChangeListener(ev -> photoTask.setMaxPhotoCount(ev.getValue())); + + configContainer.add(photoLayout); + break; + + case BARCODE: + HorizontalLayout barcodeLayout = new HorizontalLayout(); + barcodeLayout.setWidthFull(); + barcodeLayout.setSpacing(true); + + BarcodeTask barcodeTask = (BarcodeTask) task; + IntegerField minBarcodes = new IntegerField(translationHelper.getTranslation("addjob.tasks.barcode.min")); + minBarcodes.setPlaceholder("1"); + minBarcodes.setMin(1); + minBarcodes.setValue(barcodeTask.getMinBarcodeCount() != null ? barcodeTask.getMinBarcodeCount() : 1); + + IntegerField maxBarcodes = new IntegerField(translationHelper.getTranslation("addjob.tasks.barcode.max")); + maxBarcodes.setPlaceholder("10"); + maxBarcodes.setMin(1); + maxBarcodes.setValue(barcodeTask.getMaxBarcodeCount() != null ? barcodeTask.getMaxBarcodeCount() : 10); + + barcodeLayout.add(minBarcodes, maxBarcodes); + + minBarcodes.addValueChangeListener(ev -> barcodeTask.setMinBarcodeCount(ev.getValue())); + maxBarcodes.addValueChangeListener(ev -> barcodeTask.setMaxBarcodeCount(ev.getValue())); + + configContainer.add(barcodeLayout); + break; + + case COMMENT: + CommentTask commentTask = (CommentTask) task; + + TextField commentTextField = new TextField(translationHelper.getTranslation("addjob.tasks.comment.label")); + commentTextField.setPlaceholder(translationHelper.getTranslation("addjob.tasks.comment.placeholder")); + commentTextField.setWidthFull(); + commentTextField.setValue(commentTask.getCommentText() != null ? commentTask.getCommentText() : ""); + commentTextField.addValueChangeListener(ev -> commentTask.setCommentText(ev.getValue())); + + Checkbox requiredCheckbox = new Checkbox(translationHelper.getTranslation("addjob.tasks.comment.required")); + requiredCheckbox.setValue(commentTask.isRequired()); + requiredCheckbox.addValueChangeListener(ev -> commentTask.setRequired(ev.getValue())); + + configContainer.add(commentTextField, requiredCheckbox); + break; + + default: + throw new IllegalArgumentException("Unbekannter TaskType: " + taskType); + } + + // Optional checkbox – applies to all task types + Checkbox optionalCheckbox = new Checkbox(translationHelper.getTranslation("addjob.tasks.optional")); + optionalCheckbox.setValue(task.isOptional()); + optionalCheckbox.addValueChangeListener(ev -> task.setOptional(ev.getValue())); + configContainer.add(optionalCheckbox); + } + + private void updateTodoItems(VerticalLayout todoList, BaseTask task) { + List todoItems = todoList.getChildren().map(component -> { + if (component instanceof HorizontalLayout row) { + TextField field = (TextField) row.getChildren().findFirst().orElse(null); + return field != null ? field.getValue() : null; + } + return null; + }).filter(item -> item != null).toList(); + + if (task instanceof TodoListTask todoTask) { + todoTask.setTodoItems(new ArrayList<>(todoItems)); + } + } + + private TaskType getTaskTypeFromTask(BaseTask task) { + if (task instanceof ConfirmationTask) + return TaskType.CONFIRMATION; + if (task instanceof SignatureTask) + return TaskType.SIGNATURE; + if (task instanceof TodoListTask) + return TaskType.TODOLIST; + if (task instanceof PhotoTask) + return TaskType.PHOTO; + if (task instanceof BarcodeTask) + return TaskType.BARCODE; + if (task instanceof CommentTask) + return TaskType.COMMENT; + return TaskType.CONFIRMATION; + } + + private BaseTask createTaskCopy(BaseTask original) { + BaseTask copy = null; + + if (original instanceof ConfirmationTask origTask) { + copy = new ConfirmationTask(); + if (origTask.getButtonText() != null) { + ((ConfirmationTask) copy).setButtonText(origTask.getButtonText()); + } + } else if (original instanceof SignatureTask) { + copy = new SignatureTask(); + } else if (original instanceof TodoListTask origTask) { + copy = new TodoListTask(); + if (origTask.getTodoItems() != null) { + ((TodoListTask) copy).setTodoItems(new ArrayList<>(origTask.getTodoItems())); + } + } else if (original instanceof PhotoTask origTask) { + copy = new PhotoTask(origTask.getMinPhotoCount() != null ? origTask.getMinPhotoCount() : 1, + origTask.getMaxPhotoCount() != null ? origTask.getMaxPhotoCount() : 10); + } else if (original instanceof BarcodeTask origTask) { + copy = new BarcodeTask(origTask.getMinBarcodeCount() != null ? origTask.getMinBarcodeCount() : 1, + origTask.getMaxBarcodeCount() != null ? origTask.getMaxBarcodeCount() : 10); + } else if (original instanceof CommentTask origTask) { + copy = new CommentTask(origTask.getCommentText() != null ? origTask.getCommentText() : "", + origTask.isRequired()); + } + + if (copy != null) { + copy.setDescription(original.getDescription()); + copy.setTaskOrder(original.getTaskOrder() != null ? original.getTaskOrder() : 0); + copy.setCompleted(original.isCompleted()); + copy.setCompletedAt(original.getCompletedAt()); + copy.setCompletedBy(original.getCompletedBy()); + } + + return copy; + } + + private void saveTasksAsTemplate(TemplateSaveCallback templateSaveCallback, + ComboBox templateComboBox) { + if (templateSaveCallback == null) { + return; + } + + if (tasksState.isEmpty()) { + Notification.show(translationHelper.getTranslation("addjob.tasks.template.no.tasks"), 3000, + Notification.Position.BOTTOM_END); + return; + } + + Dialog dialog = new Dialog(); + dialog.setHeaderTitle(translationHelper.getTranslation("addjob.tasks.template.save.title")); + dialog.setWidth("400px"); + + VerticalLayout dialogLayout = new VerticalLayout(); + dialogLayout.setPadding(false); + dialogLayout.setSpacing(true); + + TextField templateNameField = new TextField(translationHelper.getTranslation("addjob.tasks.template.name")); + templateNameField.setPlaceholder(translationHelper.getTranslation("addjob.tasks.template.name.placeholder")); + templateNameField.setWidthFull(); + templateNameField.setRequiredIndicatorVisible(true); + + Button saveButton = new Button(translationHelper.getTranslation("button.savechanges")); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + saveButton.addClickListener(e -> { + String templateName = templateNameField.getValue(); + if (templateName == null || templateName.trim().isEmpty()) { + Notification.show(translationHelper.getTranslation("addjob.tasks.template.name.required"), 3000, + Notification.Position.BOTTOM_END); + return; + } + + List tasksCopy = new ArrayList<>(); + for (BaseTask task : tasksState) { + BaseTask taskCopy = createTaskCopy(task); + tasksCopy.add(taskCopy); + } + + templateSaveCallback.saveTemplate(templateName.trim(), tasksCopy); + dialog.close(); + }); + + Button cancelButton = new Button(translationHelper.getTranslation("button.cancel")); + cancelButton.addClickListener(e -> dialog.close()); + + HorizontalLayout buttonLayout = new HorizontalLayout(cancelButton, saveButton); + buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.END); + + dialogLayout.add(templateNameField, buttonLayout); + dialog.add(dialogLayout); + dialog.open(); + } + + private void loadTasksFromTemplate(TaskTemplate template, ComboBox templateComboBox) { + ConfirmDialog confirmDialog = new ConfirmDialog(); + confirmDialog.setHeader(translationHelper.getTranslation("addjob.tasks.template.load.title")); + confirmDialog.setText( + translationHelper.getTranslation("addjob.tasks.template.load.text", template.getTemplateName())); + confirmDialog.setCancelable(true); + confirmDialog.setCancelText(translationHelper.getTranslation("button.cancel")); + confirmDialog.setConfirmText(translationHelper.getTranslation("addjob.tasks.template.load.confirm")); + confirmDialog.setConfirmButtonTheme("primary"); + + confirmDialog.addConfirmListener(e -> { + tasksState.clear(); + tasksList.removeAll(); + + if (template.getTasks() != null) { + for (BaseTask templateTask : template.getTasks()) { + BaseTask taskCopy = createTaskCopy(templateTask); + if (taskCopy != null) { + tasksState.add(taskCopy); + createTaskRowFromTask(taskCopy); + } + } + } + + templateComboBox.clear(); + + Notification.show( + translationHelper.getTranslation("addjob.tasks.template.loaded", template.getTemplateName()), 3000, + Notification.Position.BOTTOM_END); + }); + + confirmDialog.addCancelListener(e -> templateComboBox.clear()); + + confirmDialog.open(); + } +} diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationTile.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationTile.java index 5dd1deb..9abd8b0 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationTile.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationTile.java @@ -188,7 +188,8 @@ public class DeliveryStationTile extends VerticalLayout { // Register change listeners on all fields 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(); 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) { if (text != null && !text.trim().isEmpty()) { Span span = new Span(text); - span.getStyle().set("font-size", "var(--lumo-font-size-xs)").set("word-break", "break-word") - .set("color", "var(--lumo-secondary-text-color)"); + span.getStyle().set("font-size", "var(--lumo-font-size-xs)").set("word-break", "break-word").set("color", + "var(--lumo-secondary-text-color)"); collapsedContent.add(span); } } diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java new file mode 100644 index 0000000..4ac128e --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java @@ -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 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 getCargoItems() { + return cargoItems; + } + + public void setCargoItems(List cargoItems) { + this.cargoItems = cargoItems != null ? cargoItems : new ArrayList<>(); + } + } + + public interface SaveListener { + void onSave(PickupData data); + } + + private final ComboBox company; + private final ComboBox 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 customerComboBox; + private DatePicker appointmentDatePicker; + private TimePicker appointmentTimePicker; + private Checkbox digitalProcessingCheckbox; + private ComboBox appUserComboBox; + private final List cargoItemsState = new ArrayList<>(); + private VerticalLayout cargoList; + + private final DeliveryStationTile.TranslationHelper translationHelper; + + public PickupStationDialog(String dialogTitle, List customers, + DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener, + List 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 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 companyField, List customers) { + List 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 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 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 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())); + } +} diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/StationTile.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/StationTile.java new file mode 100644 index 0000000..74bc38f --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/StationTile.java @@ -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; + } +} diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index a5f27a9..091960c 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -21,12 +21,9 @@ 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.textfield.TextField; -import com.vaadin.flow.component.textfield.IntegerField; -import com.vaadin.flow.component.textfield.NumberField; import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.textfield.TextArea; -import com.vaadin.flow.component.tabs.TabSheet; import com.vaadin.flow.router.HasDynamicTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.theme.lumo.LumoUtility; @@ -34,13 +31,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.task.BaseTask; -import de.assecutor.votianlt.model.task.TaskType; import de.assecutor.votianlt.model.task.ConfirmationTask; -import de.assecutor.votianlt.model.task.SignatureTask; import de.assecutor.votianlt.model.task.TodoListTask; -import de.assecutor.votianlt.model.task.PhotoTask; -import de.assecutor.votianlt.model.task.BarcodeTask; -import de.assecutor.votianlt.model.task.CommentTask; import de.assecutor.votianlt.pages.service.AddJobService; import de.assecutor.votianlt.pages.service.CustomerService; import de.assecutor.votianlt.pages.service.AddCustomerService; @@ -65,6 +57,9 @@ import de.assecutor.votianlt.model.DeliveryStation; import de.assecutor.votianlt.model.AddressValidationResult; import de.assecutor.votianlt.model.RouteCalculationResult; import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationTile; +import de.assecutor.votianlt.pages.base.ui.component.StationTile; +import de.assecutor.votianlt.pages.base.ui.component.PickupStationDialog; +import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationDialog; import java.time.LocalDate; import java.util.*; import java.util.Objects; @@ -91,7 +86,6 @@ public class AddJobView extends Main implements HasDynamicTitle { // Customer selection private ComboBox customerSelection; - private Button preloadAddressButton; // Pickup address fields private ComboBox pickupCompany; @@ -106,11 +100,15 @@ public class AddJobView extends Main implements HasDynamicTitle { private TextField pickupCity; private Checkbox savePickupAddress; - // Delivery station tiles (up to 25) - private final List deliveryStationTiles = new ArrayList<>(); - private Div stationsScrollContainer; + // 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 Div stationsGridContainer; private Div addStationButton; - private static final int MAX_DELIVERY_STATIONS = 25; + private StationTile pickupTile; + private static final int MAX_DELIVERY_STATIONS = 7; // Digital processing private Checkbox digitalProcessing; @@ -139,34 +137,14 @@ public class AddJobView extends Main implements HasDynamicTitle { // Time picker fields for appointments private TimePicker pickupTime; - private com.vaadin.flow.component.tabs.Tab addressesTab; - private com.vaadin.flow.component.tabs.Tab appointmentsTab; - private com.vaadin.flow.component.tabs.Tab cargoTab; - private com.vaadin.flow.component.tabs.Tab tasksTab; - private com.vaadin.flow.component.tabs.Tab priceTab; - // Submit button private Button submitButton; // Backing list for cargo items to mirror UI rows private final List cargoItemsState = new ArrayList<>(); - // Stage sections for drag and drop - // Backing list for tasks to mirror UI rows - private final List tasksState = new ArrayList<>(); - // Dynamic lists and additional controls - // Cargo section UI refs for error highlighting - private VerticalLayout cargoAreaContainer; - private Span cargoError; - private VerticalLayout cargoList; - private VerticalLayout tasksList; - private ComboBox templateComboBox; + // Backing list for tasks per delivery station (stationIndex -> tasks) + private final Map> deliveryStationTasksState = new HashMap<>(); private TextArea remarkArea; - private VerticalLayout pickupSection; - private boolean pickupCollapsed = false; - private Button pickupCollapseButton; - private VerticalLayout pickupCollapsedContent; - private List pickupExpandedOnlyComponents; - private final Binder binder = new Binder<>(Job.class); // Mapping für die Anzeige-Labels der Kunden zur Entität @@ -178,9 +156,6 @@ public class AddJobView extends Main implements HasDynamicTitle { // Adressvalidierung private final Map addressValidationResults = new HashMap<>(); private RouteCalculationResult routeCalculationResult; - private boolean addressesDirty = true; // true = Adressen müssen validiert werden - private boolean validationDialogOpen = false; // true = Dialog ist gerade geöffnet - private TabSheet tabSheet; public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, CustomerService customerService, AppUserService appUserService, TaskTemplateService taskTemplateService, @@ -230,87 +205,6 @@ public class AddJobView extends Main implements HasDynamicTitle { } customerSelection.setItems(new ArrayList<>(customerLabelToEntity.keySet())); - // Bei Auswahl eines Kunden Abholfelder befüllen - customerSelection.addValueChangeListener(ev -> { - String selected = ev.getValue(); - if (selected == null) { - // Wenn kein Kunde ausgewählt ist, Checkbox wieder aktivieren - savePickupAddress.setValue(true); - return; - } - - // Streckeninformationen zurücksetzen, da sich die Abholadresse ändert - resetRouteInformation(); - - Customer c = customerLabelToEntity.get(selected); - if (c == null) - return; - - // Pickup-Checkbox deaktivieren, da Kunde bereits existiert - savePickupAddress.setValue(false); - - // Firma - if (c.getCompanyName() != null) { - pickupCompany.setValue(c.getCompanyName()); - } else { - pickupCompany.clear(); - } - // Anrede (nur setzen, wenn vorhanden und zulässig) - if (c.getTitle() != null && ("Herr".equalsIgnoreCase(c.getTitle()) || "Frau".equalsIgnoreCase(c.getTitle()) - || "Divers".equalsIgnoreCase(c.getTitle()))) { - pickupSalutation.setValue(c.getTitle()); - } else { - pickupSalutation.clear(); - } - // Namen - if (c.getFirstname() != null) { - pickupFirstName.setValue(c.getFirstname()); - } else { - pickupFirstName.clear(); - } - if (c.getLastName() != null) { - pickupLastName.setValue(c.getLastName()); - } else { - pickupLastName.clear(); - } - // Telefon - if (c.getTelephone() != null) { - pickupPhone.setValue(c.getTelephone()); - } else { - pickupPhone.clear(); - } - // Adresse - if (c.getStreet() != null) { - pickupStreet.setValue(c.getStreet()); - } else { - pickupStreet.clear(); - } - if (c.getHouseNumber() != null) { - pickupHouseNumber.setValue(c.getHouseNumber()); - } else { - pickupHouseNumber.clear(); - } - if (c.getAddressAddition() != null) { - pickupAddressAddition.setValue(c.getAddressAddition()); - } else { - pickupAddressAddition.clear(); - } - if (c.getZip() != null) { - pickupZip.setValue(c.getZip()); - } else { - pickupZip.clear(); - } - if (c.getCity() != null) { - pickupCity.setValue(c.getCity()); - } else { - pickupCity.clear(); - } - }); - - preloadAddressButton = new Button(getTranslation("addjob.button.clearfields")); - preloadAddressButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - preloadAddressButton.addClickListener(event -> clearAllFields()); - // Pickup address pickupCompany = new ComboBox<>(getTranslation("profile.company")); pickupCompany.setPlaceholder(getTranslation("addjob.address.company.placeholder")); @@ -365,8 +259,11 @@ public class AddJobView extends Main implements HasDynamicTitle { user -> user.getVorname() + " " + user.getNachname() + " (" + user.getEmail() + ")"); appUser.setPlaceholder(getTranslation("addjob.appuser.placeholder")); - // Services grid will be initialized in createPriceAndSubmitTab() - // Date picker fields for appointments + // Services grid will be initialized in createCustomerAndAddressesTab() + // Date/time picker fields for appointments (stored in AddJobView for binder, UI + // is in PickupStationDialog) + pickupTime = new TimePicker(getTranslation("addjob.appointment.time")); + pickupTime.setLocale(java.util.Locale.GERMANY); pickupDate = new DatePicker(getTranslation("addjob.appointment.date")); pickupDate.setRequiredIndicatorVisible(true); pickupDate.setMin(LocalDate.now()); @@ -397,41 +294,10 @@ public class AddJobView extends Main implements HasDynamicTitle { H2 title = new H2(getTranslation("addjob.title")); add(title); - // Create TabSheet for organizing the form - // TabSheet and Tab references for dynamic label updates - tabSheet = new TabSheet(); - tabSheet.setWidthFull(); + // Add content directly (no tabs) + add(createCustomerAndAddressesTab()); - // Tab 1: Customer & Addresses - addressesTab = tabSheet.add(getTranslation("addjob.tab.addresses"), createCustomerAndAddressesTab()); - - // Tab 2: Appointments & Processing - appointmentsTab = tabSheet.add(getTranslation("addjob.tab.appointments"), createAppointmentsAndProcessingTab()); - - // Tab 3: Cargo - cargoTab = tabSheet.add(getTranslation("addjob.tab.cargo"), createCargoTab()); - - // Tab 4: Tasks - tasksTab = tabSheet.add(getTranslation("addjob.tab.tasks"), createTasksTab()); - - // Disable tasks tab initially if digital processing is off - if (!Boolean.TRUE.equals(digitalProcessing.getValue())) { - tasksTab.setEnabled(false); - tasksState.clear(); - if (tasksList != null) { - tasksList.removeAll(); - } - } - - // Tab 5: Price & Submit - priceTab = tabSheet.add(getTranslation("addjob.tab.price"), createPriceAndSubmitTab()); - - // Tab-Wechsel-Listener für Adressvalidierung - tabSheet.addSelectedChangeListener(this::onTabChange); - - add(tabSheet); - - // Add submit button horizontally centered below the tabs + // Add submit button horizontally centered below the content HorizontalLayout buttonLayout = new HorizontalLayout(); buttonLayout.setWidthFull(); buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); @@ -447,228 +313,31 @@ public class AddJobView extends Main implements HasDynamicTitle { tabContent.setPadding(true); tabContent.setSpacing(true); - // Customer selection section - HorizontalLayout customerLayout = new HorizontalLayout(); - customerLayout.setWidthFull(); - customerLayout.setAlignItems(FlexComponent.Alignment.END); - customerLayout.add(customerSelection, preloadAddressButton); - customerSelection.setWidth("70%"); - preloadAddressButton.setWidth("30%"); + // 3x3 Grid container for station tiles + stationsGridContainer = new Div(); + stationsGridContainer.getStyle().set("display", "grid"); + stationsGridContainer.getStyle().set("grid-template-columns", "repeat(4, 1fr)"); + stationsGridContainer.getStyle().set("gap", "var(--lumo-space-m)"); + stationsGridContainer.getStyle().set("padding", "var(--lumo-space-s)"); + stationsGridContainer.setWidthFull(); - tabContent.add(customerLayout); + // Pickup tile (always present) + pickupTile = new StationTile(StationTile.StationType.PICKUP, 0, getTranslation("addjob.section.pickup"), false); + pickupTile.setClickListener(tile -> openPickupDialog()); + stationsGridContainer.add(pickupTile); - // Horizontal scrolling container for pickup + delivery station tiles - stationsScrollContainer = new Div(); - stationsScrollContainer.getStyle().set("display", "flex"); - stationsScrollContainer.getStyle().set("overflow-x", "auto"); - stationsScrollContainer.getStyle().set("gap", "var(--lumo-space-m)"); - stationsScrollContainer.getStyle().set("padding", "var(--lumo-space-s)"); - stationsScrollContainer.getStyle().set("padding-bottom", "20px"); - stationsScrollContainer.getStyle().set("align-items", "stretch"); - stationsScrollContainer.getStyle().set("flex-shrink", "0"); - stationsScrollContainer.setWidthFull(); - - // Pickup section tile (always present) - pickupSection = createPickupSection(); - pickupSection.setWidth("40%"); - pickupSection.getStyle().set("min-width", "300px"); - pickupSection.getStyle().set("flex-shrink", "0"); - stationsScrollContainer.add(pickupSection); - - // "+" add station button tile + // "+" add station button tile (must be created before addDeliveryStationTile) addStationButton = createAddStationButton(); - stationsScrollContainer.add(addStationButton); - // Add first delivery station tile + // Add first delivery station tile (this will also add the "+" button) addDeliveryStationTile(); - // Setup focus listeners for pickup input fields + // Setup focus listeners for customer selection field setupInputFieldFocusListeners(); - tabContent.add(stationsScrollContainer); + tabContent.add(stationsGridContainer); - return tabContent; - } - - private Div createAddStationButton() { - Div button = new Div(); - button.getStyle().set("min-width", "300px"); - button.getStyle().set("width", "40%"); - button.getStyle().set("flex-shrink", "0"); - button.getStyle().set("border", "2px dashed var(--lumo-contrast-30pct)"); - button.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - button.getStyle().set("display", "flex"); - button.getStyle().set("align-items", "center"); - button.getStyle().set("justify-content", "center"); - button.getStyle().set("cursor", "pointer"); - button.getStyle().set("background-color", "var(--lumo-contrast-5pct)"); - button.getStyle().set("transition", "background-color 0.2s"); - - Icon plusIcon = new Icon(VaadinIcon.PLUS); - plusIcon.setSize("64px"); - plusIcon.getStyle().set("color", "var(--lumo-contrast-40pct)"); - button.add(plusIcon); - - button.addClickListener(e -> { - if (deliveryStationTiles.size() >= MAX_DELIVERY_STATIONS) { - Notification.show(getTranslation("addjob.station.max.reached"), 3000, - Notification.Position.BOTTOM_CENTER); - return; - } - addDeliveryStationTile(); - }); - - return button; - } - - private void addDeliveryStationTile() { - int stationNumber = deliveryStationTiles.size() + 1; - boolean removable = deliveryStationTiles.size() > 0; // First station is not removable - - List customers = customerService.findAllForCurrentOwner(); - DeliveryStationTile.TranslationHelper translationHelper = this::getTranslation; - - DeliveryStationTile tile = new DeliveryStationTile(stationNumber, removable, customers, translationHelper); - tile.setChangeListener(() -> { - resetRouteInformation(); - triggerValidation(); - updateTabLabels(); - }); - tile.setDeleteListener(this::removeDeliveryStationTile); - tile.setCollapseListener(collapsed -> updateAddStationButtonSize()); - - deliveryStationTiles.add(tile); - - // Insert tile before the "+" button - stationsScrollContainer.remove(addStationButton); - stationsScrollContainer.add(tile); - - // Hide "+" button if max reached - if (deliveryStationTiles.size() < MAX_DELIVERY_STATIONS) { - stationsScrollContainer.add(addStationButton); - } - - triggerValidation(); - updateTabLabels(); - } - - private void removeDeliveryStationTile(DeliveryStationTile tile) { - ConfirmDialog dialog = new ConfirmDialog(); - int idx = deliveryStationTiles.indexOf(tile) + 1; - dialog.setHeader(getTranslation("addjob.station.remove.confirm", idx)); - dialog.setCancelable(true); - dialog.setCancelText(getTranslation("dialog.cancel")); - dialog.setConfirmText(getTranslation("dialog.confirm")); - dialog.addConfirmListener(e -> { - deliveryStationTiles.remove(tile); - stationsScrollContainer.remove(tile); - - // Renumber remaining tiles - for (int i = 0; i < deliveryStationTiles.size(); i++) { - deliveryStationTiles.get(i).updateStationNumber(i + 1); - } - - // Ensure "+" button is visible if under max - if (deliveryStationTiles.size() < MAX_DELIVERY_STATIONS && addStationButton.getParent().isEmpty()) { - stationsScrollContainer.add(addStationButton); - } - - resetRouteInformation(); - triggerValidation(); - updateTabLabels(); - updateAddStationButtonSize(); - }); - dialog.open(); - } - - private Component createAppointmentsAndProcessingTab() { - VerticalLayout tabContent = new VerticalLayout(); - tabContent.setSizeFull(); - tabContent.setPadding(true); - tabContent.setSpacing(true); - tabContent.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - - // Container with fixed width to center content - VerticalLayout content = new VerticalLayout(); - content.setPadding(false); - content.setSpacing(true); - content.setWidth("720px"); - content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); - - // Row: Digital processing + App user - HorizontalLayout digitalRow = new HorizontalLayout(); - digitalRow.setWidthFull(); - digitalRow.setAlignItems(FlexComponent.Alignment.BASELINE); - digitalRow.setJustifyContentMode(FlexComponent.JustifyContentMode.START); - digitalProcessing.getStyle().set("margin-right", "12px"); - digitalRow.add(digitalProcessing); - - // App user selector full width - appUser.setWidthFull(); - content.add(digitalRow, appUser); - - // Appointment (Pickup) - H3 pickupApptTitle = new H3(getTranslation("addjob.appointment.pickup")); - pickupApptTitle.getStyle().set("margin", "0"); - pickupTime = new TimePicker(getTranslation("addjob.appointment.time")); - pickupTime.setLocale(java.util.Locale.GERMANY); - HorizontalLayout pickupApptRow = new HorizontalLayout(pickupDate, pickupTime); - pickupApptRow.setWidthFull(); - pickupApptRow.setSpacing(true); - pickupDate.setWidth("50%"); - pickupTime.setWidth("50%"); - content.add(pickupApptTitle, pickupApptRow); - - // Info: Delivery dates are set per-station in the tiles - Span deliveryInfoLabel = new Span(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; - } - - private Component createCargoTab() { - VerticalLayout tabContent = new VerticalLayout(); - tabContent.setSizeFull(); - tabContent.setPadding(true); - tabContent.setSpacing(true); - - // Add cargo section - tabContent.add(createCargoSection()); - - return tabContent; - } - - private Component createTasksTab() { - VerticalLayout tabContent = new VerticalLayout(); - tabContent.setSizeFull(); - tabContent.setPadding(true); - tabContent.setSpacing(true); - - // Add tasks and notes section - tabContent.add(createTasksAndNotesSection()); - - return tabContent; - } - - private Component createPriceAndSubmitTab() { - VerticalLayout tabContent = new VerticalLayout(); - tabContent.setSizeFull(); - tabContent.setPadding(true); - tabContent.setSpacing(true); - tabContent.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); - - // Container with full width like other tabs - VerticalLayout content = new VerticalLayout(); - content.setPadding(false); - content.setSpacing(true); - content.setWidthFull(); - content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); - - // Route Info Box (ganz oben) + // Route Info Box routeInfoBox = new VerticalLayout(); routeInfoBox.setPadding(true); routeInfoBox.setSpacing(true); @@ -709,7 +378,7 @@ public class AddJobView extends Main implements HasDynamicTitle { durationRow.add(durationLabel, routeDurationLabel); routeInfoBox.add(routeTitle, routeRow, durationRow); - content.add(routeInfoBox); + tabContent.add(routeInfoBox); // Manuelle Streckeneingabe (wenn keine Route berechnet wurde) manualRouteInputBox = new VerticalLayout(); @@ -761,12 +430,12 @@ public class AddJobView extends Main implements HasDynamicTitle { manualRouteHint.getStyle().set("font-style", "italic"); manualRouteInputBox.add(manualRouteTitle, manualInputRow, manualRouteHint); - content.add(manualRouteInputBox); + tabContent.add(manualRouteInputBox); - // Title + // Leistungen H3 servicesTitle = new H3(getTranslation("addjob.services.title")); servicesTitle.getStyle().set("margin", "0"); - content.add(servicesTitle); + tabContent.add(servicesTitle); // Services Grid servicesGrid = new Grid<>(); @@ -820,18 +489,17 @@ public class AddJobView extends Main implements HasDynamicTitle { servicesGrid.getDataProvider().refreshAll(); updatePriceSummary(); triggerValidation(); - updateTabLabels(); }); return removeButton; }).setHeader(getTranslation("common.actions")).setAutoWidth(true).setFlexGrow(0); - content.add(servicesGrid); + tabContent.add(servicesGrid); // Add Service Button Button addServiceButton = new Button(getTranslation("addjob.services.add"), new Icon(VaadinIcon.PLUS)); addServiceButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); addServiceButton.addClickListener(e -> openAddServiceDialog()); - content.add(addServiceButton); + tabContent.add(addServiceButton); // Price Summary VerticalLayout summaryLayout = new VerticalLayout(); @@ -884,12 +552,295 @@ public class AddJobView extends Main implements HasDynamicTitle { summaryLayout.add(priceTable); - content.add(summaryLayout); + tabContent.add(summaryLayout); + + // Bemerkung + H3 remarksTitle = new H3(getTranslation("addjob.tasks.remark")); + remarksTitle.getStyle().set("margin", "0"); + remarkArea = new TextArea(); + remarkArea.setPlaceholder(getTranslation("addjob.tasks.remark.placeholder")); + remarkArea.setWidthFull(); + remarkArea.setMinHeight("180px"); + tabContent.add(remarksTitle, remarkArea); - tabContent.add(content); return tabContent; } + private Div createAddStationButton() { + Div button = new Div(); + button.getStyle().set("border", "2px dashed var(--lumo-contrast-30pct)"); + button.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); + button.getStyle().set("display", "flex"); + button.getStyle().set("align-items", "center"); + button.getStyle().set("justify-content", "center"); + button.getStyle().set("cursor", "pointer"); + button.getStyle().set("background-color", "var(--lumo-contrast-5pct)"); + button.getStyle().set("aspect-ratio", "1 / 1"); + button.getStyle().set("overflow", "hidden"); + + Icon plusIcon = new Icon(VaadinIcon.PLUS); + plusIcon.setSize("64px"); + plusIcon.getStyle().set("color", "var(--lumo-contrast-40pct)"); + button.add(plusIcon); + + button.addClickListener(e -> { + if (deliveryStationTilesList.size() >= MAX_DELIVERY_STATIONS) { + Notification.show(getTranslation("addjob.station.max.reached"), 3000, + Notification.Position.BOTTOM_CENTER); + return; + } + addDeliveryStationTile(); + }); + + return button; + } + + private void addDeliveryStationTile() { + int stationNumber = deliveryStationTilesList.size() + 1; + boolean removable = deliveryStationTilesList.size() > 0; // First station is not removable + + String titleText = getTranslation("addjob.station.delivery", stationNumber); + StationTile tile = new StationTile(StationTile.StationType.DELIVERY, stationNumber, titleText, removable); + + // Add empty state for this station + deliveryStationsState.add(new DeliveryStation()); + deliveryStationsSaveAddress.add(true); + + int stationIndex = deliveryStationTilesList.size(); + tile.setClickListener(t -> openDeliveryDialog(t, stationIndex)); + tile.setDeleteListener(this::removeDeliveryStationTile); + + deliveryStationTilesList.add(tile); + + // Rebuild grid: remove plus button, add tile, re-add plus button + stationsGridContainer.remove(addStationButton); + stationsGridContainer.add(tile); + + // Hide "+" button if max reached + if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS) { + stationsGridContainer.add(addStationButton); + } + + triggerValidation(); + updateTabLabels(); + } + + private void removeDeliveryStationTile(StationTile tile) { + int idx = deliveryStationTilesList.indexOf(tile); + if (idx < 0) + return; + + ConfirmDialog dialog = new ConfirmDialog(); + dialog.setHeader(getTranslation("addjob.station.remove.confirm", idx + 1)); + dialog.setCancelable(true); + dialog.setCancelText(getTranslation("dialog.cancel")); + dialog.setConfirmText(getTranslation("dialog.confirm")); + dialog.addConfirmListener(e -> { + int removeIdx = deliveryStationTilesList.indexOf(tile); + if (removeIdx < 0) + return; + + deliveryStationTilesList.remove(removeIdx); + deliveryStationsState.remove(removeIdx); + deliveryStationsSaveAddress.remove(removeIdx); + deliveryStationTasksState.remove(removeIdx); + // Re-index tasks state for remaining stations + Map> reindexed = new HashMap<>(); + for (Map.Entry> entry : deliveryStationTasksState.entrySet()) { + int oldIdx = entry.getKey(); + int newIdx = oldIdx > removeIdx ? oldIdx - 1 : oldIdx; + reindexed.put(newIdx, entry.getValue()); + } + deliveryStationTasksState.clear(); + deliveryStationTasksState.putAll(reindexed); + stationsGridContainer.remove(tile); + + // Renumber remaining tiles and update click listeners + for (int i = 0; i < deliveryStationTilesList.size(); i++) { + StationTile t = deliveryStationTilesList.get(i); + int newNumber = i + 1; + t.updateStationNumber(newNumber); + t.updateTitle(getTranslation("addjob.station.delivery", newNumber)); + // Update click listener to use correct index + final int newIdx = i; + t.setClickListener(tt -> openDeliveryDialog(tt, newIdx)); + // First station should not be removable + if (i == 0) { + t.setDeleteListener(null); + } + } + + // Ensure "+" button is visible if under max + if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS && addStationButton.getParent().isEmpty()) { + stationsGridContainer.add(addStationButton); + } + + resetRouteInformation(); + triggerValidation(); + updateTabLabels(); + }); + dialog.open(); + } + + private void openPickupDialog() { + List customers = customerService.findAllForCurrentOwner(); + DeliveryStationTile.TranslationHelper translationHelper = this::getTranslation; + + PickupStationDialog dialog = new PickupStationDialog(getTranslation("addjob.section.pickup"), customers, + translationHelper, data -> { + // Update customer selection from dialog + if (data.getCustomerSelection() != null) { + customerSelection.setValue(data.getCustomerSelection()); + } else { + customerSelection.clear(); + } + + // Update pickup fields from dialog data + pickupCompany.setValue(data.getCompany() != null ? data.getCompany() : ""); + pickupSalutation.setValue(data.getSalutation() != null ? data.getSalutation() : ""); + pickupFirstName.setValue(data.getFirstName() != null ? data.getFirstName() : ""); + pickupLastName.setValue(data.getLastName() != null ? data.getLastName() : ""); + pickupPhone.setValue(data.getPhone() != null ? data.getPhone() : ""); + pickupStreet.setValue(data.getStreet() != null ? data.getStreet() : ""); + pickupHouseNumber.setValue(data.getHouseNumber() != null ? data.getHouseNumber() : ""); + pickupAddressAddition.setValue(data.getAddressAddition() != null ? data.getAddressAddition() : ""); + pickupZip.setValue(data.getZip() != null ? data.getZip() : ""); + pickupCity.setValue(data.getCity() != null ? data.getCity() : ""); + savePickupAddress.setValue(data.isSaveAddress()); + + // Sync appointment fields for binder/submit + if (data.getAppointmentDate() != null) { + pickupDate.setValue(data.getAppointmentDate()); + } + if (data.getAppointmentTime() != null) { + pickupTime.setValue(data.getAppointmentTime()); + } + digitalProcessing.setValue(data.isDigitalProcessing()); + if (data.getAppUser() != null) { + appUser.setValue(data.getAppUser()); + } else { + appUser.clear(); + } + + // Sync cargo items + cargoItemsState.clear(); + if (data.getCargoItems() != null) { + cargoItemsState.addAll(data.getCargoItems()); + } + + // Update tile preview + pickupTile.updatePreview(data.getCompany(), data.getFirstName(), data.getLastName(), + data.getStreet(), data.getHouseNumber(), data.getZip(), data.getCity()); + + resetRouteInformation(); + triggerValidation(); + updateTabLabels(); + }, availableAppUsers); + + // Pre-fill dialog with current pickup field values + PickupStationDialog.PickupData currentData = new PickupStationDialog.PickupData(); + currentData.setCompany(pickupCompany.getValue()); + currentData.setSalutation(pickupSalutation.getValue()); + currentData.setFirstName(pickupFirstName.getValue()); + currentData.setLastName(pickupLastName.getValue()); + currentData.setPhone(pickupPhone.getValue()); + currentData.setStreet(pickupStreet.getValue()); + currentData.setHouseNumber(pickupHouseNumber.getValue()); + currentData.setAddressAddition(pickupAddressAddition.getValue()); + currentData.setZip(pickupZip.getValue()); + currentData.setCity(pickupCity.getValue()); + currentData.setSaveAddress(savePickupAddress.getValue()); + currentData.setCustomerSelection(customerSelection.getValue()); + // Pre-fill pickup-specific fields + currentData.setAppointmentDate(pickupDate.getValue()); + currentData.setAppointmentTime(pickupTime != null ? pickupTime.getValue() : null); + currentData.setDigitalProcessing(digitalProcessing.getValue()); + currentData.setAppUser(appUser.getValue()); + currentData.setCargoItems(new ArrayList<>(cargoItemsState)); + dialog.setData(currentData); + + dialog.open(); + } + + private void openDeliveryDialog(StationTile tile, int stationIndex) { + // Ensure index is valid (could have changed due to deletions) + int actualIndex = deliveryStationTilesList.indexOf(tile); + if (actualIndex < 0 || actualIndex >= deliveryStationsState.size()) + return; + + List customers = customerService.findAllForCurrentOwner(); + DeliveryStationTile.TranslationHelper translationHelper = this::getTranslation; + + // Load task templates for the current user + List templates = loadTaskTemplates(); + + DeliveryStationDialog dialog = new DeliveryStationDialog( + getTranslation("addjob.station.delivery", actualIndex + 1), customers, translationHelper, data -> { + int idx = deliveryStationTilesList.indexOf(tile); + if (idx < 0 || idx >= deliveryStationsState.size()) + return; + + // Update state + DeliveryStation station = deliveryStationsState.get(idx); + station.setCompany(data.getCompany()); + station.setSalutation(data.getSalutation()); + station.setFirstName(data.getFirstName()); + station.setLastName(data.getLastName()); + station.setPhone(data.getPhone()); + station.setStreet(data.getStreet()); + station.setHouseNumber(data.getHouseNumber()); + station.setAddressAddition(data.getAddressAddition()); + station.setZip(data.getZip()); + station.setCity(data.getCity()); + deliveryStationsSaveAddress.set(idx, data.isSaveAddress()); + + // Store tasks for this delivery station + deliveryStationTasksState.put(idx, data.getTasks() != null ? data.getTasks() : new ArrayList<>()); + + // Update tile preview + tile.updatePreview(data.getCompany(), data.getFirstName(), data.getLastName(), data.getStreet(), + data.getHouseNumber(), data.getZip(), data.getCity()); + + resetRouteInformation(); + triggerValidation(); + updateTabLabels(); + }, templates, (templateName, tasks) -> { + try { + taskTemplateService.createTemplate(securityService.getCurrentDatabaseUser().getId(), + templateName, tasks); + Notification.show(getTranslation("addjob.tasks.template.saved", templateName), 3000, + Notification.Position.BOTTOM_END); + } catch (Exception ex) { + log.error("Error saving task template", ex); + Notification.show(getTranslation("addjob.tasks.template.save.error", ex.getMessage()), 4000, + Notification.Position.MIDDLE); + } + }); + + // Pre-fill dialog with current station data + DeliveryStation station = deliveryStationsState.get(actualIndex); + DeliveryStationDialog.DeliveryData currentData = new DeliveryStationDialog.DeliveryData(); + currentData.setCompany(station.getCompany()); + currentData.setSalutation(station.getSalutation()); + currentData.setFirstName(station.getFirstName()); + currentData.setLastName(station.getLastName()); + currentData.setPhone(station.getPhone()); + currentData.setStreet(station.getStreet()); + currentData.setHouseNumber(station.getHouseNumber()); + currentData.setAddressAddition(station.getAddressAddition()); + currentData.setZip(station.getZip()); + currentData.setCity(station.getCity()); + currentData.setSaveAddress(deliveryStationsSaveAddress.get(actualIndex)); + // Load existing tasks for this station + List stationTasks = deliveryStationTasksState.get(actualIndex); + if (stationTasks != null) { + currentData.setTasks(stationTasks); + } + dialog.setData(currentData); + + dialog.open(); + } + private void openAddServiceDialog() { Dialog dialog = new Dialog(); dialog.setHeaderTitle(getTranslation("addjob.services.dialog.title")); @@ -1007,146 +958,8 @@ public class AddJobView extends Main implements HasDynamicTitle { } } - private VerticalLayout createPickupSection() { - VerticalLayout section = new VerticalLayout(); - section.setSpacing(true); - section.setPadding(true); - section.setWidthFull(); - - // Hellgrauer Rahmen hinzufügen - section.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - section.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - section.getStyle().set("background-color", "var(--lumo-base-color)"); - - H3 title = new H3(getTranslation("addjob.section.pickup")); - title.getStyle().set("margin", "0").set("flex-grow", "1"); - - pickupCollapseButton = new Button(new Icon(VaadinIcon.ANGLE_DOUBLE_LEFT)); - pickupCollapseButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE); - pickupCollapseButton.getStyle().set("cursor", "pointer"); - pickupCollapseButton.addClickListener(e -> togglePickupCollapse()); - - // Invisible placeholder button to match DeliveryStationTile header height - Button placeholder = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); - placeholder.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - placeholder.getStyle().set("visibility", "hidden"); - - HorizontalLayout titleLayout = new HorizontalLayout(); - titleLayout.setWidthFull(); - titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); - titleLayout.add(title, pickupCollapseButton, placeholder); - - // Alle einzelnen Controls auf volle Breite setzen - pickupCompany.setWidthFull(); - pickupSalutation.setWidthFull(); - pickupFirstName.setWidthFull(); - pickupLastName.setWidthFull(); - pickupPhone.setWidthFull(); - pickupAddressAddition.setWidthFull(); - savePickupAddress.setWidthFull(); - - section.add(titleLayout); - section.add(pickupCompany); - section.add(pickupSalutation); - section.add(pickupFirstName); - section.add(pickupLastName); - section.add(pickupPhone); - - HorizontalLayout streetLayout = new HorizontalLayout(); - streetLayout.setWidthFull(); - streetLayout.setSpacing(true); - streetLayout.add(pickupStreet, pickupHouseNumber); - pickupStreet.setWidth("70%"); - pickupHouseNumber.setWidth("30%"); - section.add(streetLayout); - - section.add(pickupAddressAddition); - - // zip/city row - HorizontalLayout zipCityLayout = new HorizontalLayout(); - zipCityLayout.setWidthFull(); - zipCityLayout.setSpacing(true); - zipCityLayout.add(pickupZip, pickupCity); - pickupZip.setWidth("30%"); - pickupCity.setWidth("70%"); - section.add(zipCityLayout); - - section.add(savePickupAddress); - - // Store references to expanded-mode components (excluding titleLayout which stays visible) - pickupExpandedOnlyComponents = section.getChildren().filter(c -> c != titleLayout).toList(); - - section.getStyle().set("transition", "width 0.3s ease, min-width 0.3s ease"); - - // Collapsed content (initially hidden) - pickupCollapsedContent = new VerticalLayout(); - pickupCollapsedContent.setPadding(false); - pickupCollapsedContent.setSpacing(false); - pickupCollapsedContent.getStyle().set("gap", "var(--lumo-space-xs)"); - pickupCollapsedContent.setVisible(false); - section.add(pickupCollapsedContent); - - return section; - } - - private void togglePickupCollapse() { - pickupCollapsed = !pickupCollapsed; - if (pickupCollapsed) { - updatePickupCollapsedContent(); - pickupExpandedOnlyComponents.forEach(c -> c.setVisible(false)); - pickupCollapsedContent.setVisible(true); - pickupSection.setWidth("25%"); - pickupSection.getStyle().set("min-width", "150px"); - pickupCollapseButton.setIcon(new Icon(VaadinIcon.ANGLE_DOUBLE_RIGHT)); - } else { - pickupExpandedOnlyComponents.forEach(c -> c.setVisible(true)); - pickupCollapsedContent.setVisible(false); - pickupSection.setWidth("40%"); - pickupSection.getStyle().set("min-width", "300px"); - pickupCollapseButton.setIcon(new Icon(VaadinIcon.ANGLE_DOUBLE_LEFT)); - } - updateAddStationButtonSize(); - } - - private void updateAddStationButtonSize() { - boolean allCollapsed = pickupCollapsed - && deliveryStationTiles.stream().allMatch(DeliveryStationTile::isCollapsed); - if (allCollapsed) { - addStationButton.getStyle().set("width", "25%"); - addStationButton.getStyle().set("min-width", "150px"); - } else { - addStationButton.getStyle().set("width", "40%"); - addStationButton.getStyle().set("min-width", "300px"); - } - } - - private void updatePickupCollapsedContent() { - pickupCollapsedContent.removeAll(); - - addPickupCollapsedLine(pickupCompany.getValue()); - - String name = (safeValue(pickupFirstName) + " " + safeValue(pickupLastName)).trim(); - addPickupCollapsedLine(name); - - String streetLine = (safeValue(pickupStreet) + " " + safeValue(pickupHouseNumber)).trim(); - addPickupCollapsedLine(streetLine); - - String zipCityLine = (safeValue(pickupZip) + " " + safeValue(pickupCity)).trim(); - addPickupCollapsedLine(zipCityLine); - } - - private void addPickupCollapsedLine(String text) { - if (text != null && !text.trim().isEmpty()) { - Span span = new Span(text); - span.getStyle().set("font-size", "var(--lumo-font-size-xs)").set("word-break", "break-word") - .set("color", "var(--lumo-secondary-text-color)"); - pickupCollapsedContent.add(span); - } - } - - private String safeValue(TextField field) { - return field.getValue() != null ? field.getValue().trim() : ""; - } + // createPickupSection(), togglePickupCollapse(), updateAddStationButtonSize() + // removed - replaced by StationTile + StationDialog // createDeliverySection() removed - delivery stations are now handled by // DeliveryStationTile @@ -1287,22 +1100,10 @@ public class AddJobView extends Main implements HasDynamicTitle { appUser.setRequiredIndicatorVisible(required); appUser.setVisible(required); - // Enable/disable tasks tab based on digital processing - if (tasksTab != null) { - tasksTab.setEnabled(required); - } if (!required) { - // Clear app user and all tasks when digital processing is disabled + // Clear app user and all station tasks when digital processing is disabled appUser.clear(); - tasksState.clear(); - if (tasksList != null) { - tasksList.removeAll(); - } - } else { - // Add an empty task row when digital processing is re-enabled - if (tasksState.isEmpty() && tasksList != null) { - createTaskRow(); - } + deliveryStationTasksState.clear(); } triggerValidation(); @@ -1418,23 +1219,8 @@ public class AddJobView extends Main implements HasDynamicTitle { } private void updateTabLabels() { - // Check validation state for each tab and update labels with exclamation marks - updateTabLabel(addressesTab, getTranslation("addjob.tab.addresses"), hasAddressValidationErrors()); - updateTabLabel(appointmentsTab, getTranslation("addjob.tab.appointments"), hasAppointmentValidationErrors()); - updateTabLabel(cargoTab, getTranslation("addjob.tab.cargo"), hasCargoValidationErrors()); - updateTabLabel(tasksTab, getTranslation("addjob.tab.tasks"), hasTasksValidationErrors()); - updateTabLabel(priceTab, getTranslation("addjob.tab.price"), hasPriceValidationErrors()); - } - - private void updateTabLabel(com.vaadin.flow.component.tabs.Tab tab, String baseLabel, boolean hasErrors) { - if (tab == null) { - return; - } - if (hasErrors) { - tab.setLabel(baseLabel + " ⚠️"); - } else { - tab.setLabel(baseLabel); - } + // No-op: tabs removed, validation is shown via field styling and submit button + // state } private boolean hasAddressValidationErrors() { @@ -1447,13 +1233,22 @@ public class AddJobView extends Main implements HasDynamicTitle { || isFieldEmpty(pickupStreet) || isFieldEmpty(pickupHouseNumber) || isFieldEmpty(pickupZip) || isFieldEmpty(pickupCity); - // Check all delivery station tiles for errors - boolean deliveryErrors = deliveryStationTiles.isEmpty() - || deliveryStationTiles.stream().anyMatch(DeliveryStationTile::hasValidationErrors); + // Check all delivery stations for errors (from state) + boolean deliveryErrors = deliveryStationsState.isEmpty() + || deliveryStationsState.stream().anyMatch(this::hasDeliveryStationValidationErrors); return customerSelectionEmpty || pickupErrors || deliveryErrors; } + private boolean hasDeliveryStationValidationErrors(DeliveryStation station) { + return isBlank(station.getFirstName()) || isBlank(station.getLastName()) || isBlank(station.getStreet()) + || isBlank(station.getHouseNumber()) || isBlank(station.getZip()) || isBlank(station.getCity()); + } + + private boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } + private boolean hasAppointmentValidationErrors() { LocalDate today = LocalDate.now(); // Check pickup date @@ -1485,29 +1280,29 @@ public class AddJobView extends Main implements HasDynamicTitle { } private boolean hasTasksValidationErrors() { - for (BaseTask task : tasksState) { - // Check if any ConfirmationTask has an empty description or buttonText - // (required fields) - if (task instanceof ConfirmationTask confirmationTask) { - String description = task.getDescription(); - if (description == null || description.trim().isEmpty()) { - return true; + // Validate tasks across all delivery stations + for (List stationTasks : deliveryStationTasksState.values()) { + for (BaseTask task : stationTasks) { + if (task instanceof ConfirmationTask confirmationTask) { + String description = task.getDescription(); + if (description == null || description.trim().isEmpty()) { + return true; + } + String buttonText = confirmationTask.getButtonText(); + if (buttonText == null || buttonText.trim().isEmpty()) { + return true; + } } - String buttonText = confirmationTask.getButtonText(); - if (buttonText == null || buttonText.trim().isEmpty()) { - return true; - } - } - // Check if any TodoListTask has at least one non-empty todo item - if (task instanceof TodoListTask todoListTask) { - List todoItems = todoListTask.getTodoItems(); - if (todoItems == null || todoItems.isEmpty()) { - return true; - } - // Check if at least one todo item is non-empty - boolean hasValidTodoItem = todoItems.stream().anyMatch(item -> item != null && !item.trim().isEmpty()); - if (!hasValidTodoItem) { - return true; + if (task instanceof TodoListTask todoListTask) { + List todoItems = todoListTask.getTodoItems(); + if (todoItems == null || todoItems.isEmpty()) { + return true; + } + boolean hasValidTodoItem = todoItems.stream() + .anyMatch(item -> item != null && !item.trim().isEmpty()); + if (!hasValidTodoItem) { + return true; + } } } } @@ -1534,11 +1329,10 @@ public class AddJobView extends Main implements HasDynamicTitle { if (remarkArea != null) job.setRemark(remarkArea.getValue()); - // Collect delivery stations from tiles + // Collect delivery stations from state List stations = new ArrayList<>(); - for (int i = 0; i < deliveryStationTiles.size(); i++) { - DeliveryStationTile tile = deliveryStationTiles.get(i); - DeliveryStation station = tile.getDeliveryStation(); + for (int i = 0; i < deliveryStationsState.size(); i++) { + DeliveryStation station = deliveryStationsState.get(i); station.setStationOrder(i); stations.add(station); } @@ -1594,14 +1388,6 @@ public class AddJobView extends Main implements HasDynamicTitle { errorNotification.setDuration(5000); return; } - // toggle cargo error highlight - boolean hasCargo = true; - cargoError.setVisible(!hasCargo); - if (!hasCargo) { - cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-error-color-50pct)"); - } else { - cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - } // NEU: Kunden anlegen, wenn Checkboxen aktiviert if (savePickupAddress.getValue()) { @@ -1619,9 +1405,9 @@ public class AddJobView extends Main implements HasDynamicTitle { addCustomerService.addCustomer(pickupCustomer); } // Save delivery station addresses as customers if checkbox is checked - for (DeliveryStationTile tile : deliveryStationTiles) { - if (tile.isSaveAddressChecked()) { - DeliveryStation ds = tile.getDeliveryStation(); + 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()); @@ -1637,9 +1423,17 @@ public class AddJobView extends Main implements HasDynamicTitle { } } - // All validations passed, save the job with cargo items and tasks - // If digital processing is disabled, don't save any tasks - List tasksToSave = job.isDigitalProcessing() ? tasksState : List.of(); + // Collect tasks from all delivery stations and set stationOrder + List tasksToSave = new ArrayList<>(); + if (job.isDigitalProcessing()) { + for (Map.Entry> entry : deliveryStationTasksState.entrySet()) { + int stationIdx = entry.getKey(); + for (BaseTask task : entry.getValue()) { + task.setStationOrder(stationIdx); + tasksToSave.add(task); + } + } + } Job savedJob = addJobService.addJobWithCargo(job, cargoFilled, tasksToSave); // Erfolgsmeldung und Navigation zur Zusammenfassung @@ -1656,10 +1450,6 @@ public class AddJobView extends Main implements HasDynamicTitle { } catch (Exception e) { // Other errors // Reset cargo error - if (cargoError != null) - cargoError.setVisible(false); - if (cargoAreaContainer != null) - cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); Notification errorNotification = Notification .show(getTranslation("addjob.notification.error", e.getMessage())); errorNotification.setDuration(5000); @@ -1727,261 +1517,16 @@ public class AddJobView extends Main implements HasDynamicTitle { } } - private Component createCargoSection() { - VerticalLayout wrapper = new VerticalLayout(); - wrapper.setWidthFull(); - wrapper.setSpacing(true); - - 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(getTranslation("addjob.tab.cargo")); - cargoError = new Span(getTranslation("addjob.validation.cargo.required")); - cargoError.getStyle().set("color", "var(--lumo-error-text-color)"); - cargoError.getStyle().set("font-size", "var(--lumo-font-size-s)"); - cargoError.setVisible(false); - - wrapper.add(cargoTitle); - - cargoList = new VerticalLayout(); - cargoList.setPadding(false); - cargoList.setSpacing(true); - cargoAreaContainer.add(cargoError, cargoList); - - java.util.function.BiConsumer> addCargoRow = (iconName, - afterCreate) -> { - HorizontalLayout row = new HorizontalLayout(); - row.setWidthFull(); - row.setAlignItems(FlexComponent.Alignment.END); - - ComboBox desc = new ComboBox<>(getTranslation("addjob.cargo.description")); - desc.setItems(getTranslation("addjob.cargo.europalette"), getTranslation("addjob.cargo.disposablepalette"), - getTranslation("addjob.cargo.dusseldorfpalette"), getTranslation("addjob.cargo.gridboxpalette"), - getTranslation("addjob.cargo.gridcart"), getTranslation("addjob.cargo.parcel")); - desc.setAllowCustomValue(true); - desc.setPlaceholder(getTranslation("addjob.cargo.description.placeholder")); - desc.setWidth("40%"); - desc.setRequiredIndicatorVisible(true); - - IntegerField qty = new IntegerField(getTranslation("addjob.cargo.quantity")); - qty.setMin(1); - qty.setMax(9999); // Set reasonable maximum - qty.setWidth("10%"); - qty.setRequiredIndicatorVisible(true); - - NumberField weight = new NumberField(getTranslation("addjob.cargo.weight")); - weight.setSuffixComponent(new Span("kg")); - weight.setWidth("15%"); - weight.setRequiredIndicatorVisible(true); - - NumberField len = new NumberField(getTranslation("addjob.cargo.length")); - len.setSuffixComponent(new Span("cm")); - len.setWidth("12%"); - len.setRequiredIndicatorVisible(true); - NumberField wid = new NumberField(getTranslation("addjob.cargo.width")); - wid.setSuffixComponent(new Span("cm")); - wid.setWidth("12%"); - wid.setRequiredIndicatorVisible(true); - NumberField hei = new NumberField(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); - triggerValidation(); - updateTabLabels(); - }); - - row.add(desc, qty, weight, len, wid, hei, remove); - cargoList.add(row); - - // Ensure backing list is in sync: add new item and bind change listeners - CargoItem item = new CargoItem(); - cargoItemsState.add(item); - - // Initialize from current inputs - item.setDescription(desc.getValue()); - item.setQuantity(qty.getValue()); - item.setWeightKg(weight.getValue()); - item.setLengthMm(len.getValue()); - item.setWidthMm(wid.getValue()); - item.setHeightMm(hei.getValue()); - - // Validation helper to update field styling - java.util.function.Consumer> validateField = field -> { - if (field instanceof ComboBox) { - ComboBox combo = (ComboBox) field; - String value = combo.getValue() != null ? combo.getValue().toString() : ""; - combo.setInvalid(value.trim().isEmpty()); - } else if (field instanceof NumberField) { - NumberField numField = (NumberField) field; - numField.setInvalid(numField.getValue() == null || numField.getValue() <= 0); - } else if (field instanceof IntegerField) { - IntegerField intField = (IntegerField) field; - boolean isInvalid = intField.getValue() == null || intField.getValue() <= 0; - intField.setInvalid(isInvalid); - if (isInvalid) { - intField.setErrorMessage(""); - intField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); - intField.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); - } else { - intField.setErrorMessage(""); - intField.getStyle().remove("--vaadin-input-field-background"); - intField.getStyle().remove("--vaadin-input-field-border-color"); - } - } - }; - - desc.addValueChangeListener(ev -> { - item.setDescription(ev.getValue()); - validateField.accept(desc); - triggerValidation(); - updateTabLabels(); - }); - qty.addValueChangeListener(ev -> { - item.setQuantity(ev.getValue()); - validateField.accept(qty); - triggerValidation(); - updateTabLabels(); - }); - weight.addValueChangeListener(ev -> { - item.setWeightKg(ev.getValue()); - validateField.accept(weight); - triggerValidation(); - updateTabLabels(); - }); - len.addValueChangeListener(ev -> { - item.setLengthMm(ev.getValue()); - validateField.accept(len); - triggerValidation(); - updateTabLabels(); - }); - wid.addValueChangeListener(ev -> { - item.setWidthMm(ev.getValue()); - validateField.accept(wid); - triggerValidation(); - updateTabLabels(); - }); - hei.addValueChangeListener(ev -> { - item.setHeightMm(ev.getValue()); - validateField.accept(hei); - triggerValidation(); - updateTabLabels(); - }); - - // Initial validation - validateField.accept(desc); - validateField.accept(qty); - validateField.accept(weight); - validateField.accept(len); - validateField.accept(wid); - validateField.accept(hei); - - if (afterCreate != null) - afterCreate.accept(row); - }; - - addCargoRow.accept("", r -> { - }); // Show only one empty row by default - - // Add button to add more cargo rows - Button addCargoButton = new Button(getTranslation("addjob.cargo.add"), new Icon(VaadinIcon.PLUS)); - addCargoButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - addCargoButton.setWidthFull(); // Make button full width of container - addCargoButton.addClickListener(e -> addCargoRow.accept("", r -> { - })); - - cargoAreaContainer.add(addCargoButton); - wrapper.add(cargoAreaContainer); - return wrapper; - } - - private Component createTasksAndNotesSection() { - VerticalLayout wrapper = new VerticalLayout(); - wrapper.setWidthFull(); - wrapper.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - - // Fester Content-Container für zentrierte Darstellung - VerticalLayout content = new VerticalLayout(); - content.setWidth("720px"); - content.setPadding(false); - content.setSpacing(true); - content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); - - // Aufgabentitel mit Template-Auswahl - H3 tasksTitle = new H3(getTranslation("addjob.tasks.title")); - tasksTitle.getStyle().set("margin", "0"); - tasksTitle.getStyle().set("white-space", "nowrap"); - - templateComboBox = new ComboBox<>(); - templateComboBox.setPlaceholder(getTranslation("addjob.tasks.template.placeholder")); - templateComboBox.setItemLabelGenerator(TaskTemplate::getTemplateName); - templateComboBox.setClearButtonVisible(true); - // Breite auf verbleibenden Platz einstellen - templateComboBox.setWidthFull(); - - // Load templates for current user - loadTemplatesIntoComboBox(templateComboBox); - - // Handle template selection - templateComboBox.addValueChangeListener(e -> { - if (e.getValue() != null) { - loadTasksFromTemplate(e.getValue(), templateComboBox); - } - }); - - // Icon-Button zum Speichern als Template - Button saveAsTemplateBtn = new Button(new Icon(VaadinIcon.BOOKMARK)); - saveAsTemplateBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - saveAsTemplateBtn.setTooltipText(getTranslation("addjob.tasks.template.save.tooltip")); - saveAsTemplateBtn.addClickListener(e -> saveTasksAsTemplate()); - - HorizontalLayout titleWithTemplate = new HorizontalLayout(tasksTitle, templateComboBox, saveAsTemplateBtn); - titleWithTemplate.setAlignItems(FlexComponent.Alignment.CENTER); - titleWithTemplate.setSpacing(true); - content.add(titleWithTemplate); - - // Dynamische Aufgabenliste - tasksList = new VerticalLayout(); - tasksList.setPadding(false); - tasksList.setSpacing(true); - - java.util.function.Consumer addTask = (v) -> { - createTaskRow(); - }; - - // 1 Beispielzeile - addTask.accept(null); - - Button addTaskBtn = new Button(getTranslation("addjob.tasks.add"), new Icon(VaadinIcon.PLUS)); - addTaskBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - addTaskBtn.addClickListener(e -> addTask.accept(null)); - - content.add(tasksList, addTaskBtn); - - // Bemerkung - H3 remarksTitle = new H3(getTranslation("addjob.tasks.remark")); - remarksTitle.getStyle().set("margin", "0"); - remarkArea = new TextArea(); - remarkArea.setPlaceholder(getTranslation("addjob.tasks.remark.placeholder")); - remarkArea.setWidthFull(); - remarkArea.setMinHeight("180px"); - content.add(remarksTitle, remarkArea); - - wrapper.add(content); - return wrapper; + /** + * Loads task templates for the current user. + */ + private List loadTaskTemplates() { + try { + return taskTemplateService.findByUserId(securityService.getCurrentDatabaseUser().getId()); + } catch (Exception e) { + log.error("Error loading templates", e); + return List.of(); + } } /** @@ -2001,89 +1546,12 @@ public class AddJobView extends Main implements HasDynamicTitle { } } - /** - * Leert alle Felder im Formular - */ - private void clearAllFields() { - // Streckeninformationen zurücksetzen - resetRouteInformation(); - - // Customer selection - customerSelection.clear(); - - // Pickup address - pickupCompany.clear(); - pickupSalutation.clear(); - pickupFirstName.clear(); - pickupLastName.clear(); - pickupPhone.clear(); - pickupStreet.clear(); - pickupHouseNumber.clear(); - pickupAddressAddition.clear(); - pickupZip.clear(); - pickupCity.clear(); - savePickupAddress.setValue(false); - - // Delivery stations - remove all but the first, clear the first - while (deliveryStationTiles.size() > 1) { - DeliveryStationTile tile = deliveryStationTiles.remove(deliveryStationTiles.size() - 1); - stationsScrollContainer.remove(tile); - } - if (!deliveryStationTiles.isEmpty()) { - deliveryStationTiles.get(0).clearFields(); - } - // Ensure "+" button is visible - if (addStationButton.getParent().isEmpty()) { - stationsScrollContainer.add(addStationButton); - } - - // Digital processing - digitalProcessing.setValue(true); - appUser.clear(); - - // Clear services - selectedServices.clear(); - if (servicesGrid != null) { - servicesGrid.getDataProvider().refreshAll(); - } - updatePriceSummary(); - - // Benutzer-Feedback - Notification.show(getTranslation("addjob.notification.cleared"), 2000, Notification.Position.BOTTOM_CENTER); - } - /** * Konfiguriert Focus-Listener für alle Eingabefelder um Drag-and-Drop zu * steuern */ private void setupInputFieldFocusListeners() { - // Customer selection - customerSelection.addFocusListener(e -> disableDragSources()); - customerSelection.addBlurListener(e -> enableDragSources()); - - // Pickup fields - pickupCompany.addFocusListener(e -> disableDragSources()); - pickupCompany.addBlurListener(e -> enableDragSources()); - pickupSalutation.addFocusListener(e -> disableDragSources()); - pickupSalutation.addBlurListener(e -> enableDragSources()); - pickupFirstName.addFocusListener(e -> disableDragSources()); - pickupFirstName.addBlurListener(e -> enableDragSources()); - pickupLastName.addFocusListener(e -> disableDragSources()); - pickupLastName.addBlurListener(e -> enableDragSources()); - pickupPhone.addFocusListener(e -> disableDragSources()); - pickupPhone.addBlurListener(e -> enableDragSources()); - pickupStreet.addFocusListener(e -> disableDragSources()); - pickupStreet.addBlurListener(e -> enableDragSources()); - pickupHouseNumber.addFocusListener(e -> disableDragSources()); - pickupHouseNumber.addBlurListener(e -> enableDragSources()); - pickupAddressAddition.addFocusListener(e -> disableDragSources()); - pickupAddressAddition.addBlurListener(e -> enableDragSources()); - pickupZip.addFocusListener(e -> disableDragSources()); - pickupZip.addBlurListener(e -> enableDragSources()); - pickupCity.addFocusListener(e -> disableDragSources()); - pickupCity.addBlurListener(e -> enableDragSources()); - - // Delivery fields are handled inside DeliveryStationTile + // Pickup and delivery fields are now in dialogs - no focus listeners needed // Digital processing appUser.addFocusListener(e -> disableDragSources()); @@ -2094,9 +1562,8 @@ public class AddJobView extends Main implements HasDynamicTitle { * Deaktiviert alle Drag-Sources durch CSS */ private void disableDragSources() { - if (pickupSection != null) { - pickupSection.getStyle().set("pointer-events", "none"); - pickupSection.getElement().setAttribute("draggable", "false"); + if (pickupTile != null) { + pickupTile.getStyle().set("pointer-events", "none"); } } @@ -2104,1154 +1571,15 @@ public class AddJobView extends Main implements HasDynamicTitle { * Aktiviert alle Drag-Sources durch CSS */ private void enableDragSources() { - if (pickupSection != null) { - pickupSection.getStyle().remove("pointer-events"); - pickupSection.getElement().setAttribute("draggable", "true"); + if (pickupTile != null) { + pickupTile.getStyle().remove("pointer-events"); } } - private void createTaskRow() { - VerticalLayout taskContainer = new VerticalLayout(); - taskContainer.setPadding(true); - taskContainer.setSpacing(true); - taskContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - taskContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - taskContainer.getStyle().set("background-color", "var(--lumo-base-color)"); - taskContainer.getStyle().set("position", "relative"); - - // Task type selection - ComboBox taskTypeCombo = new ComboBox<>(getTranslation("addjob.tasks.tasktype")); - taskTypeCombo.setItems(TaskType.values()); - taskTypeCombo.setItemLabelGenerator(TaskType::getDisplayName); - taskTypeCombo.setPlaceholder(getTranslation("addjob.tasks.tasktype.placeholder")); - taskTypeCombo.setWidthFull(); - - // Configuration container for dynamic fields - VerticalLayout configContainer = new VerticalLayout(); - configContainer.setPadding(false); - configContainer.setSpacing(true); - - // Red X button positioned in top-right corner - Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); - deleteXButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); - deleteXButton.getStyle().set("position", "absolute"); - deleteXButton.getStyle().set("top", "8px"); - deleteXButton.getStyle().set("right", "8px"); - deleteXButton.getStyle().set("z-index", "10"); - deleteXButton.getStyle().set("padding", "4px"); - deleteXButton.getStyle().set("min-width", "24px"); - deleteXButton.getStyle().set("min-height", "24px"); - deleteXButton.addClickListener(e -> { - int idx = tasksList.getChildren().toList().indexOf(taskContainer); - if (idx >= 0 && idx < tasksState.size()) { - tasksState.remove(idx); - // Reorder remaining tasks to maintain correct sequence - reorderTasksAfterDeletion(); - } - tasksList.remove(taskContainer); - }); - - taskContainer.add(taskTypeCombo, configContainer); - taskContainer.add(deleteXButton); - - // Create Task and add to state with correct order - BaseTask task = new ConfirmationTask(""); - task.setTaskOrder(tasksState.size()); // Set order based on current position - tasksState.add(task); - - // Use an array to hold the current task reference (allows modification in - // lambda) - final BaseTask[] currentTask = { task }; - - taskTypeCombo.addValueChangeListener(ev -> { - TaskType selectedType = ev.getValue(); - if (selectedType != null) { - // Create new task instance based on type - BaseTask newTask = createTaskByType(selectedType); - BaseTask oldTask = currentTask[0]; - - newTask.setDescription(oldTask.getDescription()); - newTask.setOptional(oldTask.isOptional()); - newTask.setCompleted(oldTask.isCompleted()); - newTask.setCompletedAt(oldTask.getCompletedAt()); - newTask.setCompletedBy(oldTask.getCompletedBy()); - - // Preserve task-specific properties - switch (oldTask) { - case ConfirmationTask oldConfirmationTask when newTask instanceof ConfirmationTask newConfirmationTask -> - newConfirmationTask.setButtonText(oldConfirmationTask.getButtonText()); - case TodoListTask oldTodoTask when newTask instanceof TodoListTask newTodoTask -> - newTodoTask.setTodoItems(oldTodoTask.getTodoItems()); - case PhotoTask oldPhotoTask when newTask instanceof PhotoTask newPhotoTask -> { - newPhotoTask.setMinPhotoCount(oldPhotoTask.getMinPhotoCount()); - newPhotoTask.setMaxPhotoCount(oldPhotoTask.getMaxPhotoCount()); - } - case BarcodeTask oldBarcodeTask when newTask instanceof BarcodeTask newBarcodeTask -> { - newBarcodeTask.setMinBarcodeCount(oldBarcodeTask.getMinBarcodeCount()); - newBarcodeTask.setMaxBarcodeCount(oldBarcodeTask.getMaxBarcodeCount()); - } - case CommentTask oldCommentTask when newTask instanceof CommentTask newCommentTask -> { - newCommentTask.setCommentText(oldCommentTask.getCommentText()); - newCommentTask.setRequired(oldCommentTask.isRequired()); - } - default -> { - } - } - - // Replace in state and preserve order - int index = tasksState.indexOf(oldTask); - if (index >= 0) { - newTask.setTaskOrder(index); // Preserve the order - tasksState.set(index, newTask); - currentTask[0] = newTask; // Update the reference - } - - updateTaskConfiguration(configContainer, newTask); - triggerValidation(); - updateTabLabels(); - } - }); - - // Set initial configuration - taskTypeCombo.setValue(TaskType.CONFIRMATION); - updateTaskConfiguration(configContainer, currentTask[0]); - triggerValidation(); - updateTabLabels(); - - tasksList.add(taskContainer); - } - - private BaseTask createTaskByType(TaskType taskType) { - return switch (taskType) { - case CONFIRMATION -> new ConfirmationTask(""); - case SIGNATURE -> new SignatureTask(); - case TODOLIST -> new TodoListTask(new ArrayList<>()); - case PHOTO -> new PhotoTask(1, 10); - case BARCODE -> new BarcodeTask(1, 10); - case COMMENT -> new CommentTask("", false); - }; - } - - private void reorderTasksAfterDeletion() { - // Reorder all tasks in tasksState to maintain correct sequence - for (int i = 0; i < tasksState.size(); i++) { - BaseTask task = tasksState.get(i); - if (task != null) { - task.setTaskOrder(i); // Reset order to match current position - } - } - } - - private void updateTaskConfiguration(VerticalLayout configContainer, BaseTask task) { - configContainer.removeAll(); - - TaskType taskType = TaskType.valueOf(task.getTaskType()); - - switch (taskType) { - case CONFIRMATION: - // Description field (required) - TextField descriptionField = new TextField(getTranslation("addjob.tasks.description")); - descriptionField.setPlaceholder(getTranslation("addjob.tasks.description.placeholder")); - descriptionField.setWidthFull(); - descriptionField.setRequiredIndicatorVisible(true); - descriptionField.setValue(task.getDescription() != null ? task.getDescription() : ""); - descriptionField.addValueChangeListener(ev -> { - task.setDescription(ev.getValue()); - // Update field styling based on value - boolean isEmpty = ev.getValue() == null || ev.getValue().trim().isEmpty(); - if (isEmpty) { - descriptionField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); - descriptionField.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); - } else { - descriptionField.getStyle().remove("--vaadin-input-field-background"); - descriptionField.getStyle().remove("--vaadin-input-field-border-color"); - } - triggerValidation(); - updateTabLabels(); - }); - // Initial styling for empty field - if (task.getDescription() == null || task.getDescription().trim().isEmpty()) { - descriptionField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); - descriptionField.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); - } - - // Button text field (required) - TextField buttonTextField = new TextField(getTranslation("addjob.tasks.buttontext")); - buttonTextField.setPlaceholder(getTranslation("addjob.tasks.buttontext.placeholder")); - buttonTextField.setWidthFull(); - buttonTextField.setRequiredIndicatorVisible(true); - ConfirmationTask confirmationTask = (ConfirmationTask) task; - buttonTextField.setValue(confirmationTask.getButtonText() != null ? confirmationTask.getButtonText() : ""); - buttonTextField.addValueChangeListener(ev -> { - // Find the current ConfirmationTask in tasksState and update it - for (BaseTask stateTask : tasksState) { - if (stateTask instanceof ConfirmationTask) { - ((ConfirmationTask) stateTask).setButtonText(ev.getValue()); - } - } - // Update field styling based on value - boolean isEmpty = ev.getValue() == null || ev.getValue().trim().isEmpty(); - if (isEmpty) { - buttonTextField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); - buttonTextField.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); - } else { - buttonTextField.getStyle().remove("--vaadin-input-field-background"); - buttonTextField.getStyle().remove("--vaadin-input-field-border-color"); - } - triggerValidation(); - updateTabLabels(); - }); - // Initial styling for empty field - if (confirmationTask.getButtonText() == null || confirmationTask.getButtonText().trim().isEmpty()) { - buttonTextField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); - buttonTextField.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); - } - configContainer.add(descriptionField, buttonTextField); - break; - - case SIGNATURE: - // No additional configuration needed - Span info = new Span(getTranslation("addjob.tasks.signature.noconfig")); - info.getStyle().set("color", "var(--lumo-secondary-text-color)"); - info.getStyle().set("font-style", "italic"); - configContainer.add(info); - break; - - case TODOLIST: - VerticalLayout todoContainer = new VerticalLayout(); - todoContainer.setPadding(false); - todoContainer.setSpacing(true); - - H3 todoTitle = new H3(getTranslation("addjob.tasks.todolist.title")); - todoTitle.getStyle().set("margin", "0"); - todoContainer.add(todoTitle); - - // Dynamic todo list - VerticalLayout todoList = new VerticalLayout(); - todoList.setPadding(false); - todoList.setSpacing(true); - - // Helper to update todo field styling based on value - java.util.function.Consumer updateTodoFieldStyling = (field) -> { - boolean isEmpty = field.getValue() == null || field.getValue().trim().isEmpty(); - if (isEmpty) { - field.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); - field.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); - } else { - field.getStyle().remove("--vaadin-input-field-background"); - field.getStyle().remove("--vaadin-input-field-border-color"); - } - }; - - java.util.function.Consumer addTodoItem = (v) -> { - HorizontalLayout todoRow = new HorizontalLayout(); - todoRow.setWidthFull(); - todoRow.setAlignItems(FlexComponent.Alignment.END); - - TextField todoField = new TextField(); - todoField.setPlaceholder(getTranslation("addjob.tasks.todolist.item.placeholder")); - todoField.setWidth("100%"); - todoField.setRequiredIndicatorVisible(true); - // Initial red styling for empty field - updateTodoFieldStyling.accept(todoField); - - Button removeTodo = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); - removeTodo.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); - removeTodo.addClickListener(e -> { - todoList.remove(todoRow); - updateTodoItems(todoList, task); - }); - - todoRow.add(todoField, removeTodo); - todoRow.setFlexGrow(1, todoField); - todoList.add(todoRow); - - todoField.addValueChangeListener(ev -> { - updateTodoFieldStyling.accept(todoField); - updateTodoItems(todoList, task); - }); - }; - - Button addTodoBtn = new Button(getTranslation("addjob.tasks.todolist.add"), new Icon(VaadinIcon.PLUS)); - addTodoBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - addTodoBtn.addClickListener(e -> addTodoItem.accept(null)); - - // Load existing todo items from the task, or add one empty item if none exist - if (task instanceof TodoListTask) { - TodoListTask todoTask = (TodoListTask) task; - if (todoTask.getTodoItems() != null && !todoTask.getTodoItems().isEmpty()) { - // Create UI rows for existing todo items - for (String todoText : todoTask.getTodoItems()) { - HorizontalLayout todoRow = new HorizontalLayout(); - todoRow.setWidthFull(); - todoRow.setAlignItems(FlexComponent.Alignment.END); - - TextField todoField = new TextField(); - todoField.setPlaceholder(getTranslation("addjob.tasks.todolist.item.placeholder")); - todoField.setWidth("100%"); - todoField.setRequiredIndicatorVisible(true); - todoField.setValue(todoText != null ? todoText : ""); // Set the saved text - // Apply styling based on value - updateTodoFieldStyling.accept(todoField); - - Button removeTodo = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); - removeTodo.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); - removeTodo.addClickListener(e -> { - todoList.remove(todoRow); - updateTodoItems(todoList, task); - }); - - todoRow.add(todoField, removeTodo); - todoRow.setFlexGrow(1, todoField); - todoList.add(todoRow); - - todoField.addValueChangeListener(ev -> { - updateTodoFieldStyling.accept(todoField); - updateTodoItems(todoList, task); - }); - } - } else { - // Add initial empty todo item if no existing items - addTodoItem.accept(null); - } - } else { - // Add initial empty todo item for new tasks - addTodoItem.accept(null); - } - - todoContainer.add(todoList, addTodoBtn); - configContainer.add(todoContainer); - break; - - case PHOTO: - HorizontalLayout photoLayout = new HorizontalLayout(); - photoLayout.setWidthFull(); - photoLayout.setSpacing(true); - - PhotoTask photoTask = (PhotoTask) task; - IntegerField minPhotos = new IntegerField(getTranslation("addjob.tasks.photo.min")); - minPhotos.setPlaceholder("1"); - minPhotos.setMin(1); - minPhotos.setValue(photoTask.getMinPhotoCount() != null ? photoTask.getMinPhotoCount() : 1); - - IntegerField maxPhotos = new IntegerField(getTranslation("addjob.tasks.photo.max")); - maxPhotos.setPlaceholder("10"); - maxPhotos.setMin(1); - maxPhotos.setValue(photoTask.getMaxPhotoCount() != null ? photoTask.getMaxPhotoCount() : 10); - - photoLayout.add(minPhotos, maxPhotos); - - minPhotos.addValueChangeListener(ev -> { - photoTask.setMinPhotoCount(ev.getValue()); - }); - - maxPhotos.addValueChangeListener(ev -> { - photoTask.setMaxPhotoCount(ev.getValue()); - }); - - configContainer.add(photoLayout); - break; - - case BARCODE: - HorizontalLayout barcodeLayout = new HorizontalLayout(); - barcodeLayout.setWidthFull(); - barcodeLayout.setSpacing(true); - - BarcodeTask barcodeTask = (BarcodeTask) task; - IntegerField minBarcodes = new IntegerField(getTranslation("addjob.tasks.barcode.min")); - minBarcodes.setPlaceholder("1"); - minBarcodes.setMin(1); - minBarcodes.setValue(barcodeTask.getMinBarcodeCount() != null ? barcodeTask.getMinBarcodeCount() : 1); - - IntegerField maxBarcodes = new IntegerField(getTranslation("addjob.tasks.barcode.max")); - maxBarcodes.setPlaceholder("10"); - maxBarcodes.setMin(1); - maxBarcodes.setValue(barcodeTask.getMaxBarcodeCount() != null ? barcodeTask.getMaxBarcodeCount() : 10); - - barcodeLayout.add(minBarcodes, maxBarcodes); - - minBarcodes.addValueChangeListener(ev -> { - barcodeTask.setMinBarcodeCount(ev.getValue()); - }); - - maxBarcodes.addValueChangeListener(ev -> { - barcodeTask.setMaxBarcodeCount(ev.getValue()); - }); - - configContainer.add(barcodeLayout); - break; - - case COMMENT: - CommentTask commentTask = (CommentTask) task; - - TextField commentTextField = new TextField(getTranslation("addjob.tasks.comment.label")); - commentTextField.setPlaceholder(getTranslation("addjob.tasks.comment.placeholder")); - commentTextField.setWidthFull(); - commentTextField.setValue(commentTask.getCommentText() != null ? commentTask.getCommentText() : ""); - commentTextField.addValueChangeListener(ev -> { - commentTask.setCommentText(ev.getValue()); - }); - - com.vaadin.flow.component.checkbox.Checkbox requiredCheckbox = new com.vaadin.flow.component.checkbox.Checkbox( - getTranslation("addjob.tasks.comment.required")); - requiredCheckbox.setValue(commentTask.isRequired()); - requiredCheckbox.addValueChangeListener(ev -> { - commentTask.setRequired(ev.getValue()); - }); - - configContainer.add(commentTextField, requiredCheckbox); - break; - - default: - throw new IllegalArgumentException("Unbekannter TaskType: " + taskType); - } - - // Optional checkbox – applies to all task types - com.vaadin.flow.component.checkbox.Checkbox optionalCheckbox = new com.vaadin.flow.component.checkbox.Checkbox( - getTranslation("addjob.tasks.optional")); - optionalCheckbox.setValue(task.isOptional()); - optionalCheckbox.addValueChangeListener(ev -> task.setOptional(ev.getValue())); - configContainer.add(optionalCheckbox); - } - - private void updateTodoItems(VerticalLayout todoList, BaseTask task) { - List todoItems = todoList.getChildren().map(component -> { - if (component instanceof HorizontalLayout row) { - TextField field = (TextField) row.getChildren().findFirst().orElse(null); - return field != null ? field.getValue() : null; - } - return null; - }).filter(Objects::nonNull).filter(item -> !item.trim().isEmpty()) - .collect(java.util.stream.Collectors.toList()); - - if (task instanceof TodoListTask) { - ((TodoListTask) task).setTodoItems(todoItems); - } - - // Trigger validation to update submit button state - triggerValidation(); - updateTabLabels(); - } - - /** - * Speichert die aktuell konfigurierten Aufgaben als Template - */ - private void saveTasksAsTemplate() { - try { - // Check if there are any tasks to save - if (tasksState.isEmpty()) { - Notification.show(getTranslation("addjob.tasks.template.no.tasks"), 3000, - Notification.Position.BOTTOM_END); - return; - } - - // Create dialog for template name input - Dialog dialog = new Dialog(); - dialog.setHeaderTitle(getTranslation("addjob.tasks.template.save.title")); - dialog.setWidth("400px"); - - VerticalLayout dialogLayout = new VerticalLayout(); - dialogLayout.setPadding(false); - dialogLayout.setSpacing(true); - - TextField templateNameField = new TextField(getTranslation("addjob.tasks.template.name")); - templateNameField.setPlaceholder(getTranslation("addjob.tasks.template.name.placeholder")); - templateNameField.setWidthFull(); - templateNameField.setRequiredIndicatorVisible(true); - - Button saveButton = new Button(getTranslation("button.savechanges")); - saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - saveButton.addClickListener(e -> { - String templateName = templateNameField.getValue(); - if (templateName == null || templateName.trim().isEmpty()) { - Notification.show(getTranslation("addjob.tasks.template.name.required"), 3000, - Notification.Position.BOTTOM_END); - return; - } - - try { - // Create deep copies of current tasks - List tasksCopy = new ArrayList<>(); - for (BaseTask task : tasksState) { - // Create a copy of each task to avoid reference issues - BaseTask taskCopy = createTaskCopy(task); - tasksCopy.add(taskCopy); - } - - // Save template with task type information and specific data - taskTemplateService.createTemplate(securityService.getCurrentDatabaseUser().getId(), - templateName.trim(), tasksCopy); - - dialog.close(); - loadTemplatesIntoComboBox(templateComboBox); - Notification.show(getTranslation("addjob.tasks.template.saved", templateName), 3000, - Notification.Position.BOTTOM_END); - - } catch (RuntimeException ex) { - Notification.show(ex.getMessage(), 4000, Notification.Position.MIDDLE); - } catch (Exception ex) { - log.error("Error saving task template", ex); - Notification.show(getTranslation("addjob.tasks.template.save.error", ex.getMessage()), 4000, - Notification.Position.MIDDLE); - } - }); - - Button cancelButton = new Button(getTranslation("button.cancel")); - cancelButton.addClickListener(e -> dialog.close()); - - HorizontalLayout buttonLayout = new HorizontalLayout(cancelButton, saveButton); - buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.END); - - dialogLayout.add(templateNameField, buttonLayout); - dialog.add(dialogLayout); - dialog.open(); - - } catch (Exception e) { - log.error("Error opening save template dialog", e); - Notification.show(getTranslation("addjob.tasks.template.dialog.error", e.getMessage()), 4000, - Notification.Position.MIDDLE); - } - } - - /** - * Creates a deep copy of a task to avoid reference issues in templates Saves - * all task-specific data including type and specific properties - */ - private BaseTask createTaskCopy(BaseTask original) { - BaseTask copy = null; - - if (original instanceof ConfirmationTask) { - ConfirmationTask origTask = (ConfirmationTask) original; - copy = new ConfirmationTask(); - // Copy all specific data for ConfirmationTask - if (origTask.getButtonText() != null) { - ((ConfirmationTask) copy).setButtonText(origTask.getButtonText()); - } - } else if (original instanceof SignatureTask) { - // SignatureTask has no specific data beyond the base task - copy = new SignatureTask(); - } else if (original instanceof TodoListTask) { - TodoListTask origTask = (TodoListTask) original; - copy = new TodoListTask(); - // Copy all todo items - if (origTask.getTodoItems() != null) { - ((TodoListTask) copy).setTodoItems(new ArrayList<>(origTask.getTodoItems())); - } - } else if (original instanceof PhotoTask) { - PhotoTask origTask = (PhotoTask) original; - // Copy with all photo-specific parameters - copy = new PhotoTask(origTask.getMinPhotoCount() != null ? origTask.getMinPhotoCount() : 1, - origTask.getMaxPhotoCount() != null ? origTask.getMaxPhotoCount() : 10); - } else if (original instanceof BarcodeTask) { - BarcodeTask origTask = (BarcodeTask) original; - // Copy with all barcode-specific parameters - copy = new BarcodeTask(origTask.getMinBarcodeCount() != null ? origTask.getMinBarcodeCount() : 1, - origTask.getMaxBarcodeCount() != null ? origTask.getMaxBarcodeCount() : 10); - } else if (original instanceof CommentTask) { - CommentTask origTask = (CommentTask) original; - // Copy with all comment-specific parameters - copy = new CommentTask(origTask.getCommentText() != null ? origTask.getCommentText() : "", - origTask.isRequired()); - } - - if (copy != null) { - // Copy all base task properties - copy.setDescription(original.getDescription()); - copy.setTaskOrder(original.getTaskOrder() != null ? original.getTaskOrder() : 0); - copy.setCompleted(original.isCompleted()); - copy.setCompletedAt(original.getCompletedAt()); - copy.setCompletedBy(original.getCompletedBy()); - } - - return copy; - } - - /** - * Loads available templates into the ComboBox - */ - private void loadTemplatesIntoComboBox(ComboBox templateComboBox) { - try { - List templates = taskTemplateService - .findByUserId(securityService.getCurrentDatabaseUser().getId()); - templateComboBox.setItems(templates); - } catch (Exception e) { - log.error("Error loading templates", e); - Notification.show(getTranslation("addjob.tasks.template.load.templates.error", e.getMessage()), 4000, - Notification.Position.MIDDLE); - } - } - - /** - * Loads tasks from selected template with confirmation dialog - */ - private void loadTasksFromTemplate(TaskTemplate template, ComboBox templateComboBox) { - ConfirmDialog confirmDialog = new ConfirmDialog(); - confirmDialog.setHeader(getTranslation("addjob.tasks.template.load.title")); - confirmDialog.setText(getTranslation("addjob.tasks.template.load.text", template.getTemplateName())); - confirmDialog.setCancelable(true); - confirmDialog.setCancelText(getTranslation("button.cancel")); - confirmDialog.setConfirmText(getTranslation("addjob.tasks.template.load.confirm")); - confirmDialog.setConfirmButtonTheme("primary"); - - confirmDialog.addConfirmListener(e -> { - try { - // Clear current tasks - tasksState.clear(); - tasksList.removeAll(); - - // Add tasks from template - if (template.getTasks() != null) { - for (BaseTask templateTask : template.getTasks()) { - BaseTask taskCopy = createTaskCopy(templateTask); - if (taskCopy != null) { - tasksState.add(taskCopy); - createTaskRowFromTask(taskCopy); - } - } - } - - // Clear the combobox selection - templateComboBox.clear(); - - // Re-validate to enable submit button if all fields are valid - triggerValidation(); - updateTabLabels(); - - Notification.show(getTranslation("addjob.tasks.template.loaded", template.getTemplateName()), 3000, - Notification.Position.BOTTOM_END); - - } catch (Exception ex) { - log.error("Error loading template tasks", ex); - Notification.show(getTranslation("addjob.tasks.template.load.error", ex.getMessage()), 4000, - Notification.Position.MIDDLE); - } - }); - - confirmDialog.addCancelListener(e -> { - // Clear the combobox selection if user cancels - templateComboBox.clear(); - }); - - confirmDialog.open(); - } - - /** - * Creates a task row from an existing task (used when loading templates) This - * creates a UI row and populates it with the task's specific data - */ - private void createTaskRowFromTask(BaseTask task) { - // Don't call createTaskRow() directly, as it would create a default - // ConfirmationTask - // Instead, create the UI components and set them up with the loaded task - - VerticalLayout taskContainer = new VerticalLayout(); - taskContainer.setPadding(true); - taskContainer.setSpacing(true); - taskContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - taskContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - taskContainer.getStyle().set("background-color", "var(--lumo-base-color)"); - taskContainer.getStyle().set("position", "relative"); - - // Task type selection - ComboBox taskTypeCombo = new ComboBox<>(getTranslation("addjob.tasks.tasktype")); - taskTypeCombo.setItems(TaskType.values()); - taskTypeCombo.setItemLabelGenerator(TaskType::getDisplayName); - taskTypeCombo.setPlaceholder(getTranslation("addjob.tasks.tasktype.placeholder")); - taskTypeCombo.setWidthFull(); - - // Configuration container for dynamic fields - VerticalLayout configContainer = new VerticalLayout(); - configContainer.setPadding(false); - configContainer.setSpacing(true); - - // Red X button positioned in top-right corner - Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); - deleteXButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); - deleteXButton.getStyle().set("position", "absolute"); - deleteXButton.getStyle().set("top", "8px"); - deleteXButton.getStyle().set("right", "8px"); - deleteXButton.getStyle().set("z-index", "10"); - deleteXButton.getStyle().set("padding", "4px"); - deleteXButton.getStyle().set("min-width", "24px"); - deleteXButton.getStyle().set("min-height", "24px"); - deleteXButton.addClickListener(e -> { - int idx = tasksList.getChildren().toList().indexOf(taskContainer); - if (idx >= 0 && idx < tasksState.size()) { - tasksState.remove(idx); - reorderTasksAfterDeletion(); - } - tasksList.remove(taskContainer); - }); - - taskContainer.add(taskTypeCombo, configContainer); - taskContainer.add(deleteXButton); - - final BaseTask[] currentTask = { task }; - - // Set the combo value BEFORE registering the listener so the listener does - // NOT fire during initialization. The loaded task object is already correct - // and is already in tasksState — no replacement needed. - TaskType taskType = getTaskTypeFromTask(task); - if (taskType != null) { - taskTypeCombo.setValue(taskType); - } - - // Register the listener for user-initiated type changes only - taskTypeCombo.addValueChangeListener(ev -> { - TaskType selectedType = ev.getValue(); - if (selectedType != null) { - BaseTask newTask = createTaskByType(selectedType); - BaseTask oldTask = currentTask[0]; - - newTask.setDescription(oldTask.getDescription()); - newTask.setOptional(oldTask.isOptional()); - newTask.setCompleted(oldTask.isCompleted()); - newTask.setCompletedAt(oldTask.getCompletedAt()); - newTask.setCompletedBy(oldTask.getCompletedBy()); - - // Preserve task-specific properties - switch (oldTask) { - case ConfirmationTask oldConfirmationTask when newTask instanceof ConfirmationTask newConfirmationTask -> - newConfirmationTask.setButtonText(oldConfirmationTask.getButtonText()); - case TodoListTask oldTodoTask when newTask instanceof TodoListTask newTodoTask -> - newTodoTask.setTodoItems(oldTodoTask.getTodoItems()); - case PhotoTask oldPhotoTask when newTask instanceof PhotoTask newPhotoTask -> { - newPhotoTask.setMinPhotoCount(oldPhotoTask.getMinPhotoCount()); - newPhotoTask.setMaxPhotoCount(oldPhotoTask.getMaxPhotoCount()); - } - case BarcodeTask oldBarcodeTask when newTask instanceof BarcodeTask newBarcodeTask -> { - newBarcodeTask.setMinBarcodeCount(oldBarcodeTask.getMinBarcodeCount()); - newBarcodeTask.setMaxBarcodeCount(oldBarcodeTask.getMaxBarcodeCount()); - } - case CommentTask oldCommentTask when newTask instanceof CommentTask newCommentTask -> { - newCommentTask.setCommentText(oldCommentTask.getCommentText()); - newCommentTask.setRequired(oldCommentTask.isRequired()); - } - default -> { - } - } - - // Replace in state and preserve order - int index = tasksState.indexOf(oldTask); - if (index >= 0) { - newTask.setTaskOrder(index); - tasksState.set(index, newTask); - currentTask[0] = newTask; - } - - updateTaskConfiguration(configContainer, newTask); - triggerValidation(); - updateTabLabels(); - } - }); - - // Render the UI with the loaded task directly (which IS in tasksState) - updateTaskConfiguration(configContainer, task); - triggerValidation(); - updateTabLabels(); - - tasksList.add(taskContainer); - } - - /** - * Gets the TaskType enum value from a BaseTask instance - */ - private TaskType getTaskTypeFromTask(BaseTask task) { - if (task instanceof ConfirmationTask) - return TaskType.CONFIRMATION; - if (task instanceof SignatureTask) - return TaskType.SIGNATURE; - if (task instanceof TodoListTask) - return TaskType.TODOLIST; - if (task instanceof PhotoTask) - return TaskType.PHOTO; - if (task instanceof BarcodeTask) - return TaskType.BARCODE; - if (task instanceof CommentTask) - return TaskType.COMMENT; - return TaskType.CONFIRMATION; // fallback - } - // ============================================ // Adressvalidierung // ============================================ - /** - * Wird aufgerufen, wenn der Benutzer den Tab wechselt. Prüft, ob vom Tab - * "Auftraggeber & Adressen" gewechselt wird und ob die Adressen geändert - * wurden. - */ - private void onTabChange(com.vaadin.flow.component.tabs.TabSheet.SelectedChangeEvent event) { - com.vaadin.flow.component.tabs.Tab previousTab = event.getPreviousTab(); - com.vaadin.flow.component.tabs.Tab selectedTab = event.getSelectedTab(); - - // Nur prüfen, wenn vom Adress-Tab weg gewechselt wird - if (previousTab != addressesTab) { - return; - } - - log.debug("Tab-Wechsel von Adress-Tab zu '{}' - addressesDirty={}", selectedTab.getLabel(), addressesDirty); - - // Wenn der Validierungsdialog gerade geöffnet ist, nichts tun - if (validationDialogOpen) { - log.debug("Validierungsdialog ist geöffnet - ignoriere Tab-Wechsel"); - return; - } - - // Prüfen, ob Adressen geändert wurden - boolean pickupChanged = hasPickupAddressChanged(); - boolean deliveryChanged = hasDeliveryAddressChanged(); - - log.debug("Adressänderung: pickupChanged={}, deliveryChanged={}", pickupChanged, deliveryChanged); - - if (!pickupChanged && !deliveryChanged) { - // Adressen nicht geändert, nichts zu tun - log.debug("Keine Adressänderung - Dialog wird nicht angezeigt"); - return; - } - - // Tab-Wechsel vorübergehend verhindern - // Flag setzen, Dialog anzeigen, und Tab zurücksetzen - validationDialogOpen = true; - tabSheet.setSelectedTab(addressesTab); - - // Validierungsdialog anzeigen - log.debug("Zeige Validierungsdialog an"); - showAddressValidationDialog(selectedTab); - } - - /** - * Prüft, ob die Abholadresse gültig ist (Pflichtfelder ausgefüllt). - */ - private boolean hasPickupAddressChanged() { - String currentStreet = getValueOrEmpty(pickupStreet); - String currentZip = getValueOrEmpty(pickupZip); - String currentCity = getValueOrEmpty(pickupCity); - - // Nur true zurückgeben, wenn alle Pflichtfelder ausgefüllt sind und Validierung - // nötig ist - return addressesDirty && !currentStreet.isEmpty() && !currentZip.isEmpty() && !currentCity.isEmpty(); - } - - /** - * Prüft, ob mindestens eine Lieferstation gültige Adressdaten hat. - */ - private boolean hasDeliveryAddressChanged() { - if (!addressesDirty) { - return false; - } - return deliveryStationTiles.stream().anyMatch(DeliveryStationTile::hasAddressForValidation); - } - - private String getValueOrEmpty(TextField field) { - return field.getValue() != null ? field.getValue().trim() : ""; - } - - /** - * Zeigt den Adressvalidierungsdialog an. Die Prüfung erfolgt im Hintergrund und - * der Dialog wird aktualisiert, sobald die Ergebnisse vorliegen. - */ - private void showAddressValidationDialog(com.vaadin.flow.component.tabs.Tab targetTab) { - final Dialog dialog = new Dialog(); - dialog.setHeaderTitle(getTranslation("addjob.validation.dialog.title")); - dialog.setWidth("500px"); - dialog.setModal(true); - dialog.setCloseOnOutsideClick(false); - dialog.setCloseOnEsc(false); - - final VerticalLayout content = new VerticalLayout(); - content.setPadding(true); - content.setSpacing(true); - - // Initiale Meldung mit Progress - final Span loadingMessage = new Span(getTranslation("addjob.validation.dialog.loading")); - loadingMessage.getStyle().set("font-style", "italic"); - loadingMessage.getStyle().set("color", "var(--lumo-secondary-text-color)"); - - // Progress-Indikator - final com.vaadin.flow.component.progressbar.ProgressBar progressBar = new com.vaadin.flow.component.progressbar.ProgressBar(); - progressBar.setIndeterminate(true); - progressBar.setWidthFull(); - - content.add(loadingMessage, progressBar); - - // Layout für die Ergebnisanzeige (initial versteckt) - final VerticalLayout resultLayout = new VerticalLayout(); - resultLayout.setVisible(false); - resultLayout.setPadding(false); - resultLayout.setSpacing(true); - - final Span pickupResultLabel = new Span(); - final Span deliveryResultLabel = new Span(); - - // Route-Label für die Anzeige der berechneten Strecke - final Span routeResultLabel = new Span(); - routeResultLabel.getStyle().set("font-weight", "bold"); - routeResultLabel.getStyle().set("margin-top", "var(--lumo-space-s)"); - routeResultLabel.setVisible(false); - - resultLayout.add(pickupResultLabel, deliveryResultLabel, routeResultLabel); - content.add(resultLayout); - - // Button-Layout (initial versteckt) - final HorizontalLayout buttonLayout = new HorizontalLayout(); - buttonLayout.setWidthFull(); - buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.END); - buttonLayout.setVisible(false); - - final Button cancelButton = new Button(getTranslation("addjob.validation.dialog.back"), e -> { - dialog.close(); - // Im Adress-Tab bleiben - validationDialogOpen = false; - log.debug("Dialog geschlossen (Zurück), validationDialogOpen=false"); - }); - cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - - final Button continueButton = new Button(getTranslation("addjob.validation.dialog.continue.anyway"), e -> { - dialog.close(); - // Zum Ziel-Tab wechseln - tabSheet.setSelectedTab(targetTab); - // Adressen als validiert markieren - markAddressesAsValidated(); - validationDialogOpen = false; - log.debug("Dialog geschlossen (Weiter), validationDialogOpen=false"); - }); - continueButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - - buttonLayout.add(cancelButton, continueButton); - content.add(buttonLayout); - - dialog.add(content); - dialog.open(); - - // Adressdaten speichern für die Validierung - final String pickupStreetValue = getValueOrEmpty(pickupStreet); - final String pickupHouseNumberValue = getValueOrEmpty(pickupHouseNumber); - final String pickupZipValue = getValueOrEmpty(pickupZip); - final String pickupCityValue = getValueOrEmpty(pickupCity); - final boolean pickupChanged = hasPickupAddressChanged(); - final boolean deliveryChanged = hasDeliveryAddressChanged(); - - // Capture delivery station data for async validation - final List stationData = new ArrayList<>(); - for (DeliveryStationTile tile : deliveryStationTiles) { - if (tile.hasAddressForValidation()) { - stationData.add(new String[] { tile.getStreetValue(), tile.getHouseNumberValue(), tile.getZipValue(), - tile.getCityValue() }); - } - } - - // Asynchrone Validierung im Hintergrund durchführen - getUI().ifPresent(ui -> { - java.util.concurrent.CompletableFuture.runAsync(() -> { - // Abholadresse validieren - final AddressValidationResult[] pickupResultHolder = new AddressValidationResult[1]; - if (pickupChanged) { - pickupResultHolder[0] = addressValidationService.validateAddress("pickup", pickupStreetValue, - pickupHouseNumberValue, pickupZipValue, pickupCityValue); - } - - // Alle Lieferstationen validieren - final List deliveryResults = new ArrayList<>(); - if (deliveryChanged) { - for (int i = 0; i < stationData.size(); i++) { - String[] data = stationData.get(i); - AddressValidationResult result = addressValidationService.validateAddress("delivery_" + i, - data[0], data[1], data[2], data[3]); - deliveryResults.add(result); - } - } - - // UI aktualisieren mit den Ergebnissen - ui.access(() -> { - if (pickupResultHolder[0] != null) { - addressValidationResults.put("pickup", pickupResultHolder[0]); - } - for (int i = 0; i < deliveryResults.size(); i++) { - addressValidationResults.put("delivery_" + i, deliveryResults.get(i)); - } - - AddressValidationResult pickupResult = pickupResultHolder[0] != null ? pickupResultHolder[0] - : addressValidationResults.get("pickup"); - - // Use first delivery station result for route calculation (backward compat) - AddressValidationResult firstDeliveryResult = !deliveryResults.isEmpty() ? deliveryResults.get(0) - : addressValidationResults.get("delivery_0"); - - // Lade-Anzeige ausblenden - loadingMessage.setVisible(false); - progressBar.setVisible(false); - - // Prüfen ob Abholadresse und erste Lieferadresse gültig sind - boolean bothValid = (pickupResult != null && pickupResult.isValid()) - && (firstDeliveryResult != null && firstDeliveryResult.isValid()); - - // Route berechnen wenn beide gültig - if (bothValid) { - routeCalculationResult = addressValidationService.calculateRoute(pickupResult, - firstDeliveryResult); - } - - // Ergebnisse anzeigen - show first delivery station result in existing UI - updateValidationDialogResults(pickupResult, firstDeliveryResult, pickupResultLabel, - deliveryResultLabel, routeResultLabel, resultLayout, buttonLayout, continueButton, - targetTab); - - // Show additional station results - for (int i = 1; i < deliveryResults.size(); i++) { - AddressValidationResult stationResult = deliveryResults.get(i); - Span stationLabel = new Span(); - if (stationResult.isValid()) { - stationLabel.setText("\u2713 " + getTranslation("addjob.station.delivery", i + 1) + ": " - + stationResult.getFormattedAddress()); - stationLabel.getStyle().set("color", "var(--lumo-success-text-color)"); - } else { - stationLabel.setText("\u26A0 " + getTranslation("addjob.station.delivery", i + 1) + ": " - + stationResult.getValidationMessage()); - stationLabel.getStyle().set("color", "var(--lumo-error-text-color)"); - } - resultLayout.addComponentAtIndex(resultLayout.getComponentCount() - 1, stationLabel); - } - }); - }); - }); - } - - /** - * Aktualisiert die Ergebnisanzeige im Validierungsdialog. - */ - private void updateValidationDialogResults(AddressValidationResult pickupResult, - AddressValidationResult deliveryResult, Span pickupResultLabel, Span deliveryResultLabel, - Span routeResultLabel, VerticalLayout resultLayout, HorizontalLayout buttonLayout, Button continueButton, - com.vaadin.flow.component.tabs.Tab targetTab) { - - boolean hasInvalidAddress = false; - boolean bothAddressesValid = true; - - // Abholadresse anzeigen - if (pickupResult != null) { - if (pickupResult.isValid()) { - pickupResultLabel.setText("✓ " + getTranslation("addjob.validation.pickup.address") + ": " - + pickupResult.getFormattedAddress()); - pickupResultLabel.getStyle().set("color", "var(--lumo-success-text-color)"); - } else { - pickupResultLabel.setText("⚠ " + getTranslation("addjob.validation.pickup.address") + ": " - + pickupResult.getValidationMessage()); - pickupResultLabel.getStyle().set("color", "var(--lumo-error-text-color)"); - hasInvalidAddress = true; - bothAddressesValid = false; - } - } else { - pickupResultLabel.setVisible(false); - } - - // Lieferadresse anzeigen - if (deliveryResult != null) { - if (deliveryResult.isValid()) { - deliveryResultLabel.setText("✓ " + getTranslation("addjob.validation.delivery.address") + ": " - + deliveryResult.getFormattedAddress()); - deliveryResultLabel.getStyle().set("color", "var(--lumo-success-text-color)"); - } else { - deliveryResultLabel.setText("⚠ " + getTranslation("addjob.validation.delivery.address") + ": " - + deliveryResult.getValidationMessage()); - deliveryResultLabel.getStyle().set("color", "var(--lumo-error-text-color)"); - hasInvalidAddress = true; - bothAddressesValid = false; - } - } else { - deliveryResultLabel.setVisible(false); - } - - // Route anzeigen, wenn beide Adressen gültig sind - if (bothAddressesValid && routeCalculationResult != null && routeCalculationResult.isValid()) { - routeResultLabel.setText("🚛 " + getTranslation("addjob.validation.route") + ": " - + String.format("%.1f km", routeCalculationResult.getDistanceKm()) + " (" - + getTranslation("addjob.route.duration") + ": " + routeCalculationResult.getFormattedDurationLong() - + ")"); - routeResultLabel.getStyle().set("color", "var(--lumo-primary-text-color)"); - routeResultLabel.setVisible(true); - } else { - routeResultLabel.setVisible(false); - } - - // Ergebnisse anzeigen - resultLayout.setVisible(true); - - // Farbliche Markierung der Adressfelder - updateAddressFieldStyles(pickupResult, deliveryResult); - - // Buttons anzeigen - buttonLayout.setVisible(true); - - // Wenn beide Adressen gültig sind, direkt weiter - if (!hasInvalidAddress) { - continueButton.setText(getTranslation("addjob.validation.dialog.continue")); - } else { - continueButton.setText(getTranslation("addjob.validation.dialog.continue.anyway")); - } - - // Route-Info im Preis-Tab aktualisieren - updateRouteInfoBox(); - } - - /** - * Aktualisiert die Route-Info-Box im Preis-Tab mit den aktuellen Routendaten. - * Zeigt entweder die berechnete Route oder die manuelle Eingabe an. - */ - private void updateRouteInfoBox() { - if (routeInfoBox == null || manualRouteInputBox == null) { - return; - } - - if (routeCalculationResult != null && routeCalculationResult.isValid()) { - // Berechnete Route anzeigen - routeDistanceLabel.setText(String.format("%.1f km", routeCalculationResult.getDistanceKm())); - routeDurationLabel.setText(routeCalculationResult.getFormattedDurationLong()); - routeInfoBox.setVisible(true); - manualRouteInputBox.setVisible(false); - - // Update price summary and grid with new route distance - updatePriceSummary(); - if (servicesGrid != null) { - servicesGrid.getDataProvider().refreshAll(); - } - } else { - // Manuelle Eingabe anzeigen - routeInfoBox.setVisible(false); - manualRouteInputBox.setVisible(true); - } - } - - /** - * Aktualisiert die Hintergrundfarbe der Adressfelder basierend auf dem - * Validierungsergebnis. Hellgrün für validierte Adressen, hellgelb für nicht - * validierte. - */ - private void updateAddressFieldStyles(AddressValidationResult pickupResult, - AddressValidationResult deliveryResult) { - // Abholadresse - hellgrün (#90EE90) für validiert, hellgelb (#FFFACD) für nicht - // validiert - String pickupColor = (pickupResult != null && pickupResult.isValid()) ? "rgba(144, 238, 144, 0.5)" // Hellgrün - // mit - // Transparenz - : "rgba(255, 250, 205, 0.5)"; // Hellgelb mit Transparenz - - pickupStreet.getStyle().set("--vaadin-input-field-background", pickupColor); - pickupHouseNumber.getStyle().set("--vaadin-input-field-background", pickupColor); - pickupZip.getStyle().set("--vaadin-input-field-background", pickupColor); - pickupCity.getStyle().set("--vaadin-input-field-background", pickupColor); - - // Delivery station field styling is handled inside the tiles - } - - /** - * Markiert die Adressen als validiert (nach erfolgreicher Prüfung). - */ - private void markAddressesAsValidated() { - addressesDirty = false; - log.debug("Adressen als validiert markiert"); - } - /** * Gibt das Validierungsergebnis für die Abholadresse zurück. Kann null sein, * wenn noch keine Validierung durchgeführt wurde. @@ -3374,16 +1702,13 @@ public class AddJobView extends Main implements HasDynamicTitle { * bewirkt, dass der Validierungsdialog beim Tab-Wechsel erneut angezeigt wird. */ private void resetRouteInformation() { - // Flag setzen, dass Adressen geändert wurden - addressesDirty = true; - // Routenberechnung zurücksetzen routeCalculationResult = null; // Validierungsergebnisse zurücksetzen addressValidationResults.clear(); - // Route-Info-Box im Preis-Tab verstecken und manuelle Eingabe anzeigen + // Route-Info-Box verstecken und manuelle Eingabe anzeigen if (routeInfoBox != null) { routeInfoBox.setVisible(false); } diff --git a/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java b/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java index 3587e12..5745f25 100644 --- a/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java @@ -9,6 +9,8 @@ import java.util.List; public interface TaskRepository extends MongoRepository { List findByJobIdOrderByTaskOrderAsc(ObjectId jobId); + List findByJobIdAndStationOrderOrderByTaskOrderAsc(ObjectId jobId, int stationOrder); + /** * Count tasks by completion status */ diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 6ea1844..64dfa5a 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1,3 +1,7 @@ +# Common Dialog +dialog.cancel=Abbrechen +dialog.confirm=Bestätigen + # Navigation and Main Layout nav.jobs=Aufträge nav.job.create=Auftragserstellung diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 22322d9..58d4c9c 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -1,3 +1,7 @@ +# Common Dialog +dialog.cancel=Cancel +dialog.confirm=Confirm + # Navigation and Main Layout nav.jobs=Jobs nav.job.create=Create New Job